diff --git a/package.json b/package.json index 836757e0..90eb4343 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "@cowprotocol/contracts": "github:cowprotocol/contracts.git#a10f40788a", "@openzeppelin/contracts": "5.0.2", "composable-cow": "github:cowprotocol/composable-cow.git#24d556b", + "cow-amm": "github:cowprotocol/cow-amm.git#6566128", "solmate": "github:transmissions11/solmate#c892309" }, "devDependencies": { diff --git a/remappings.txt b/remappings.txt index 5b011a43..08fc002a 100644 --- a/remappings.txt +++ b/remappings.txt @@ -5,6 +5,9 @@ solmate/=node_modules/solmate/src @cowprotocol/=node_modules/@cowprotocol/contracts/src/contracts cowprotocol/=node_modules/@cowprotocol/contracts/src/ @composable-cow/=node_modules/composable-cow/ +@cow-amm/=node_modules/cow-amm/src +lib/openzeppelin/=node_modules/@openzeppelin contracts/=src/contracts interfaces/=src/interfaces +libraries/=src/libraries diff --git a/src/contracts/BCoWHelper.sol b/src/contracts/BCoWHelper.sol new file mode 100644 index 00000000..4562bcda --- /dev/null +++ b/src/contracts/BCoWHelper.sol @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.25; + +import {IBCoWFactory} from 'interfaces/IBCoWFactory.sol'; +import {IBCoWPool} from 'interfaces/IBCoWPool.sol'; + +import {ICOWAMMPoolHelper} from '@cow-amm/interfaces/ICOWAMMPoolHelper.sol'; +import {GetTradeableOrder} from '@cow-amm/libraries/GetTradeableOrder.sol'; + +import {IERC20} from '@cowprotocol/interfaces/IERC20.sol'; +import {GPv2Interaction} from '@cowprotocol/libraries/GPv2Interaction.sol'; +import {GPv2Order} from '@cowprotocol/libraries/GPv2Order.sol'; + +import {BMath} from 'contracts/BMath.sol'; + +/** + * @title BCoWHelper + * @notice Helper contract that allows to trade on CoW Swap Protocol. + * @dev This contract supports only 2-token equal-weights pools. + */ +contract BCoWHelper is ICOWAMMPoolHelper, BMath { + using GPv2Order for GPv2Order.Data; + + /// @notice The app data used by this helper's factory. + bytes32 internal immutable _APP_DATA; + + /// @inheritdoc ICOWAMMPoolHelper + // solhint-disable-next-line style-guide-casing + address public immutable factory; + + constructor(address factory_) { + factory = factory_; + _APP_DATA = IBCoWFactory(factory_).APP_DATA(); + } + + /// @inheritdoc ICOWAMMPoolHelper + function order( + address pool, + uint256[] calldata prices + ) + external + view + returns ( + GPv2Order.Data memory order_, + GPv2Interaction.Data[] memory preInteractions, + GPv2Interaction.Data[] memory postInteractions, + bytes memory sig + ) + { + address[] memory tokens_ = tokens(pool); + + GetTradeableOrder.GetTradeableOrderParams memory params = GetTradeableOrder.GetTradeableOrderParams({ + pool: pool, + token0: IERC20(tokens_[0]), + token1: IERC20(tokens_[1]), + // The price of this function is expressed as amount of + // token1 per amount of token0. The `prices` vector is + // expressed the other way around. + priceNumerator: prices[1], + priceDenominator: prices[0], + appData: _APP_DATA + }); + + order_ = GetTradeableOrder.getTradeableOrder(params); + + { + // NOTE: Using calcOutGivenIn for the sell amount in order to avoid possible rounding + // issues that may cause invalid orders. This prevents CoW Protocol back-end from generating + // orders that may be ignored due to rounding-induced reverts. + + uint256 balanceToken0 = IERC20(tokens_[0]).balanceOf(pool); + uint256 balanceToken1 = IERC20(tokens_[1]).balanceOf(pool); + (uint256 balanceIn, uint256 balanceOut) = + address(order_.buyToken) == tokens_[0] ? (balanceToken0, balanceToken1) : (balanceToken1, balanceToken0); + + order_.sellAmount = calcOutGivenIn({ + tokenBalanceIn: balanceIn, + tokenWeightIn: 1e18, + tokenBalanceOut: balanceOut, + tokenWeightOut: 1e18, + tokenAmountIn: order_.buyAmount, + swapFee: 0 + }); + } + + // A ERC-1271 signature on CoW Protocol is composed of two parts: the + // signer address and the valid ERC-1271 signature data for that signer. + bytes memory eip1271sig; + eip1271sig = abi.encode(order_); + sig = abi.encodePacked(pool, eip1271sig); + + // Generate the order commitment pre-interaction + bytes32 domainSeparator = IBCoWPool(pool).SOLUTION_SETTLER_DOMAIN_SEPARATOR(); + bytes32 orderCommitment = order_.hash(domainSeparator); + + preInteractions = new GPv2Interaction.Data[](1); + preInteractions[0] = GPv2Interaction.Data({ + target: pool, + value: 0, + callData: abi.encodeWithSelector(IBCoWPool.commit.selector, orderCommitment) + }); + + return (order_, preInteractions, postInteractions, sig); + } + + /// @inheritdoc ICOWAMMPoolHelper + function tokens(address pool) public view virtual returns (address[] memory tokens_) { + // reverts in case pool is not deployed by the helper's factory + if (!IBCoWFactory(factory).isBPool(pool)) { + revert PoolDoesNotExist(); + } + + // call reverts with `BPool_PoolNotFinalized()` in case pool is not finalized + tokens_ = IBCoWPool(pool).getFinalTokens(); + + // reverts in case pool is not supported (non-2-token pool) + if (tokens_.length != 2) { + revert PoolDoesNotExist(); + } + // reverts in case pool is not supported (non-equal weights) + if (IBCoWPool(pool).getNormalizedWeight(tokens_[0]) != IBCoWPool(pool).getNormalizedWeight(tokens_[1])) { + revert PoolDoesNotExist(); + } + } +} diff --git a/test/integration/BCoWHelper.t.sol b/test/integration/BCoWHelper.t.sol new file mode 100644 index 00000000..7c1fc536 --- /dev/null +++ b/test/integration/BCoWHelper.t.sol @@ -0,0 +1,181 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.24; + +import {Test} from 'forge-std/Test.sol'; + +import {IERC20} from '@cowprotocol/interfaces/IERC20.sol'; + +import {IBCoWPool} from 'interfaces/IBCoWPool.sol'; +import {IBPool} from 'interfaces/IBPool.sol'; +import {ISettlement} from 'interfaces/ISettlement.sol'; + +import {ICOWAMMPoolHelper} from '@cow-amm/interfaces/ICOWAMMPoolHelper.sol'; +import {GPv2Interaction} from '@cowprotocol/libraries/GPv2Interaction.sol'; +import {GPv2Order} from '@cowprotocol/libraries/GPv2Order.sol'; +import {GPv2Trade} from '@cowprotocol/libraries/GPv2Trade.sol'; +import {GPv2Signing} from '@cowprotocol/mixins/GPv2Signing.sol'; + +import {GPv2TradeEncoder} from '@composable-cow/test/vendored/GPv2TradeEncoder.sol'; + +import {BCoWFactory} from 'contracts/BCoWFactory.sol'; +import {BCoWHelper} from 'contracts/BCoWHelper.sol'; + +contract BCoWHelperIntegrationTest is Test { + using GPv2Order for GPv2Order.Data; + + BCoWHelper private helper; + + // All hardcoded addresses are mainnet addresses + address public lp = makeAddr('lp'); + + ISettlement private settlement = ISettlement(0x9008D19f58AAbD9eD0D60971565AA8510560ab41); + address private vaultRelayer; + + address private solver = 0x423cEc87f19F0778f549846e0801ee267a917935; + + BCoWFactory private ammFactory; + IBPool private weightedPool; + IBPool private basicPool; + + IERC20 private constant DAI = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); + IERC20 private constant WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + + uint256 constant TEN_PERCENT = 0.1 ether; + // NOTE: 1 ETH = 1000 DAI + uint256 constant INITIAL_DAI_BALANCE = 1000 ether; + uint256 constant INITIAL_WETH_BALANCE = 1 ether; + uint256 constant INITIAL_SPOT_PRICE = 0.001e18; + + uint256 constant SKEWENESS_RATIO = 95; // -5% skewness + uint256 constant EXPECTED_FINAL_SPOT_PRICE = INITIAL_SPOT_PRICE * 100 / SKEWENESS_RATIO; + + function setUp() public { + vm.createSelectFork('mainnet', 20_012_063); + + vaultRelayer = address(settlement.vaultRelayer()); + + ammFactory = new BCoWFactory(address(settlement), bytes32('appData')); + helper = new BCoWHelper(address(ammFactory)); + + deal(address(DAI), lp, type(uint256).max, false); + deal(address(WETH), lp, type(uint256).max, false); + + vm.startPrank(lp); + basicPool = ammFactory.newBPool(); + weightedPool = ammFactory.newBPool(); + + DAI.approve(address(basicPool), type(uint256).max); + WETH.approve(address(basicPool), type(uint256).max); + basicPool.bind(address(DAI), INITIAL_DAI_BALANCE, 4.2e18); // no weight + basicPool.bind(address(WETH), INITIAL_WETH_BALANCE, 4.2e18); // no weight + + DAI.approve(address(weightedPool), type(uint256).max); + WETH.approve(address(weightedPool), type(uint256).max); + // NOTE: pool is 80-20 DAI-WETH, has 4xDAI balance than basic, same spot price + weightedPool.bind(address(DAI), 4 * INITIAL_DAI_BALANCE, 8e18); // 80% weight + weightedPool.bind(address(WETH), INITIAL_WETH_BALANCE, 2e18); // 20% weight + + // finalize + basicPool.finalize(); + weightedPool.finalize(); + + vm.stopPrank(); + } + + function testBasicOrder() public { + IBCoWPool pool = IBCoWPool(address(basicPool)); + + uint256 spotPrice = pool.getSpotPriceSansFee(address(WETH), address(DAI)); + assertEq(spotPrice, INITIAL_SPOT_PRICE); + + _executeHelperOrder(pool); + + uint256 postSpotPrice = pool.getSpotPriceSansFee(address(WETH), address(DAI)); + assertEq(postSpotPrice, EXPECTED_FINAL_SPOT_PRICE); + } + + // NOTE: reverting test, weighted pools are not supported + function testWeightedOrder() public { + IBCoWPool pool = IBCoWPool(address(weightedPool)); + + uint256 spotPrice = pool.getSpotPriceSansFee(address(WETH), address(DAI)); + assertEq(spotPrice, INITIAL_SPOT_PRICE); + + vm.expectRevert(ICOWAMMPoolHelper.PoolDoesNotExist.selector); + helper.order(address(pool), new uint256[](2)); + } + + function _executeHelperOrder(IBPool pool) internal { + address[] memory tokens = helper.tokens(address(pool)); + uint256 daiIndex = 0; + uint256 wethIndex = 1; + assertEq(tokens.length, 2); + assertEq(tokens[daiIndex], address(DAI)); + assertEq(tokens[wethIndex], address(WETH)); + + // Prepare the price vector used in the execution of the settlement in + // CoW Protocol. We skew the price by ~5% towards a cheaper WETH, so + // that the AMM wants to buy WETH. + uint256[] memory prices = new uint256[](2); + // Note: oracle price are expressed in the same format as prices in + // a call to `settle`, where the price vector is expressed so that + // if the first token is DAI and the second WETH then a price of 3000 + // DAI per WETH means a price vector of [1, 3000] (if the decimals are + // different, as in WETH/USDC, then the atom amount is what counts). + prices[daiIndex] = INITIAL_WETH_BALANCE; + prices[wethIndex] = INITIAL_DAI_BALANCE * SKEWENESS_RATIO / 100; + + // The helper generates the AMM order + GPv2Order.Data memory ammOrder; + GPv2Interaction.Data[] memory preInteractions; + GPv2Interaction.Data[] memory postInteractions; + bytes memory sig; + (ammOrder, preInteractions, postInteractions, sig) = helper.order(address(pool), prices); + + // We expect a commit interaction in pre interactions + assertEq(preInteractions.length, 1); + assertEq(postInteractions.length, 0); + + // Because of how we changed the price, we expect to buy WETH + assertEq(address(ammOrder.sellToken), address(DAI)); + assertEq(address(ammOrder.buyToken), address(WETH)); + + // Check that the amounts and price aren't unreasonable. We changed the + // price by about 5%, so the amounts aren't expected to change + // significantly more (say, about 2.5% of the original balance). + assertApproxEqRel(ammOrder.sellAmount, INITIAL_DAI_BALANCE * 25 / 1000, TEN_PERCENT); + assertApproxEqRel(ammOrder.buyAmount, INITIAL_WETH_BALANCE * 25 / 1000, TEN_PERCENT); + + GPv2Trade.Data[] memory trades = new GPv2Trade.Data[](1); + + // pool's trade + trades[0] = GPv2Trade.Data({ + sellTokenIndex: 0, + buyTokenIndex: 1, + receiver: ammOrder.receiver, + sellAmount: ammOrder.sellAmount, + buyAmount: ammOrder.buyAmount, + validTo: ammOrder.validTo, + appData: ammOrder.appData, + feeAmount: ammOrder.feeAmount, + flags: GPv2TradeEncoder.encodeFlags(ammOrder, GPv2Signing.Scheme.Eip1271), + executedAmount: ammOrder.sellAmount, + signature: sig + }); + + GPv2Interaction.Data[][3] memory interactions = + [new GPv2Interaction.Data[](1), new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0)]; + + interactions[0][0] = preInteractions[0]; + + // cast tokens array to IERC20 array + IERC20[] memory ierc20vec; + assembly { + ierc20vec := tokens + } + + // finally, settle + vm.prank(solver); + settlement.settle(ierc20vec, prices, trades, interactions); + } +} diff --git a/test/integration/BCowPool.t.sol b/test/integration/BCowPool.t.sol index 83a16922..da711035 100644 --- a/test/integration/BCowPool.t.sol +++ b/test/integration/BCowPool.t.sol @@ -8,6 +8,7 @@ import {GPv2Interaction} from '@cowprotocol/libraries/GPv2Interaction.sol'; import {GPv2Order} from '@cowprotocol/libraries/GPv2Order.sol'; import {GPv2Trade} from '@cowprotocol/libraries/GPv2Trade.sol'; import {GPv2Signing} from '@cowprotocol/mixins/GPv2Signing.sol'; + import {BCoWConst} from 'contracts/BCoWConst.sol'; import {BCoWFactory} from 'contracts/BCoWFactory.sol'; diff --git a/test/manual-smock/MockBCoWFactory.sol b/test/manual-smock/MockBCoWFactory.sol index 5e7baa5a..c4b2e427 100644 --- a/test/manual-smock/MockBCoWFactory.sol +++ b/test/manual-smock/MockBCoWFactory.sol @@ -5,6 +5,16 @@ import {BCoWFactory, BCoWPool, BFactory, IBCoWFactory, IBPool} from '../../src/c import {Test} from 'forge-std/Test.sol'; contract MockBCoWFactory is BCoWFactory, Test { + // NOTE: manually added methods (immutable overrides not supported in smock) + function mock_call_APP_DATA(bytes32 _appData) public { + vm.mockCall(address(this), abi.encodeWithSignature('APP_DATA()'), abi.encode(_appData)); + } + + function expectCall_APP_DATA() public { + vm.expectCall(address(this), abi.encodeWithSignature('APP_DATA()')); + } + + // BCoWFactory methods constructor(address solutionSettler, bytes32 appData) BCoWFactory(solutionSettler, appData) {} function mock_call_logBCoWPool() public { @@ -31,8 +41,39 @@ contract MockBCoWFactory is BCoWFactory, Test { } // MockBFactory methods - function set__isBPool(address _key0, bool _value) public { _isBPool[_key0] = _value; } + + function call__isBPool(address _key0) public view returns (bool) { + return _isBPool[_key0]; + } + + function set__bDao(address __bDao) public { + _bDao = __bDao; + } + + function call__bDao() public view returns (address) { + return _bDao; + } + + function mock_call_newBPool(IBPool bPool) public { + vm.mockCall(address(this), abi.encodeWithSignature('newBPool()'), abi.encode(bPool)); + } + + function mock_call_setBDao(address bDao) public { + vm.mockCall(address(this), abi.encodeWithSignature('setBDao(address)', bDao), abi.encode()); + } + + function mock_call_collect(IBPool bPool) public { + vm.mockCall(address(this), abi.encodeWithSignature('collect(IBPool)', bPool), abi.encode()); + } + + function mock_call_isBPool(address bPool, bool _returnParam0) public { + vm.mockCall(address(this), abi.encodeWithSignature('isBPool(address)', bPool), abi.encode(_returnParam0)); + } + + function mock_call_getBDao(address _returnParam0) public { + vm.mockCall(address(this), abi.encodeWithSignature('getBDao()'), abi.encode(_returnParam0)); + } } diff --git a/test/manual-smock/MockBCoWHelper.sol b/test/manual-smock/MockBCoWHelper.sol new file mode 100644 index 00000000..003b4ce2 --- /dev/null +++ b/test/manual-smock/MockBCoWHelper.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.0; + +import { + BCoWHelper, + BMath, + GPv2Interaction, + GPv2Order, + GetTradeableOrder, + IBCoWFactory, + IBCoWPool, + ICOWAMMPoolHelper, + IERC20 +} from '../../src/contracts/BCoWHelper.sol'; +import {Test} from 'forge-std/Test.sol'; + +contract MockBCoWHelper is BCoWHelper, Test { + // NOTE: manually added methods (internal immutable exposers not supported in smock) + function call__APP_DATA() external view returns (bytes32) { + return _APP_DATA; + } + + // NOTE: manually added method (public overrides not supported in smock) + function tokens(address pool) public view override returns (address[] memory tokens_) { + (bool _success, bytes memory _data) = address(this).staticcall(abi.encodeWithSignature('tokens(address)', pool)); + + if (_success) return abi.decode(_data, (address[])); + else return super.tokens(pool); + } + + // NOTE: manually added method (public overrides not supported in smock) + function expectCall_tokens(address pool) public { + vm.expectCall(address(this), abi.encodeWithSignature('tokens(address)', pool)); + } + + // BCoWHelper methods + constructor(address factory_) BCoWHelper(factory_) {} + + function mock_call_order( + address pool, + uint256[] calldata prices, + GPv2Order.Data memory order_, + GPv2Interaction.Data[] memory preInteractions, + GPv2Interaction.Data[] memory postInteractions, + bytes memory sig + ) public { + vm.mockCall( + address(this), + abi.encodeWithSignature('order(address,uint256[])', pool, prices), + abi.encode(order_, preInteractions, postInteractions, sig) + ); + } + + function mock_call_tokens(address pool, address[] memory tokens_) public { + vm.mockCall(address(this), abi.encodeWithSignature('tokens(address)', pool), abi.encode(tokens_)); + } +} diff --git a/test/manual-smock/MockBCoWPool.sol b/test/manual-smock/MockBCoWPool.sol index c94a6bb1..a0f1176c 100644 --- a/test/manual-smock/MockBCoWPool.sol +++ b/test/manual-smock/MockBCoWPool.sol @@ -21,8 +21,18 @@ contract MockBCoWPool is BCoWPool, Test { vm.expectCall(address(this), abi.encodeWithSignature('verify(GPv2Order.Data)', order)); } - /// MockBCoWPool mock methods + // NOTE: manually added methods (immutable overrides not supported in smock) + function mock_call_SOLUTION_SETTLER_DOMAIN_SEPARATOR(bytes32 domainSeparator) public { + vm.mockCall( + address(this), abi.encodeWithSignature('SOLUTION_SETTLER_DOMAIN_SEPARATOR()'), abi.encode(domainSeparator) + ); + } + function expectCall_SOLUTION_SETTLER_DOMAIN_SEPARATOR() public { + vm.expectCall(address(this), abi.encodeWithSignature('SOLUTION_SETTLER_DOMAIN_SEPARATOR()')); + } + + /// MockBCoWPool mock methods constructor(address cowSolutionSettler, bytes32 appData) BCoWPool(cowSolutionSettler, appData) {} function mock_call_commit(bytes32 orderHash) public { @@ -359,6 +369,34 @@ contract MockBCoWPool is BCoWPool, Test { vm.expectCall(address(this), abi.encodeWithSignature('_pushUnderlying(address,address,uint256)', token, to, amount)); } + function call__afterFinalize() public { + return _afterFinalize(); + } + + function expectCall__afterFinalize() public { + 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()); } @@ -398,12 +436,23 @@ contract MockBCoWPool is BCoWPool, Test { vm.expectCall(address(this), abi.encodeWithSignature('_mintPoolShare(uint256)', amount)); } - function call__afterFinalize() public { - return _afterFinalize(); + function mock_call__burnPoolShare(uint256 amount) public { + vm.mockCall(address(this), abi.encodeWithSignature('_burnPoolShare(uint256)', amount), abi.encode()); } - function expectCall__afterFinalize() public { - vm.expectCall(address(this), abi.encodeWithSignature('_afterFinalize()')); + 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 { @@ -417,7 +466,7 @@ contract MockBCoWPool is BCoWPool, Test { else return super._getLock(); } - function call__getLock() public returns (bytes32 value) { + function call__getLock() public view returns (bytes32 value) { return _getLock(); } diff --git a/test/unit/BCoWHelper.t.sol b/test/unit/BCoWHelper.t.sol new file mode 100644 index 00000000..ef6d03b5 --- /dev/null +++ b/test/unit/BCoWHelper.t.sol @@ -0,0 +1,222 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.25; + +import {Test} from 'forge-std/Test.sol'; +import {MockBCoWHelper} from 'test/manual-smock/MockBCoWHelper.sol'; + +import {IBCoWPool} from 'interfaces/IBCoWPool.sol'; +import {IBPool} from 'interfaces/IBPool.sol'; +import {ISettlement} from 'interfaces/ISettlement.sol'; + +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; + +import {ICOWAMMPoolHelper} from '@cow-amm/interfaces/ICOWAMMPoolHelper.sol'; +import {GPv2Interaction} from '@cowprotocol/libraries/GPv2Interaction.sol'; +import {GPv2Order} from '@cowprotocol/libraries/GPv2Order.sol'; + +import {MockBCoWFactory} from 'test/manual-smock/MockBCoWFactory.sol'; +import {MockBCoWPool} from 'test/manual-smock/MockBCoWPool.sol'; + +contract BCoWHelperTest is Test { + MockBCoWHelper helper; + + MockBCoWFactory factory; + MockBCoWPool pool; + address invalidPool = makeAddr('invalidPool'); + address[] tokens = new address[](2); + uint256[] priceVector = new uint256[](2); + + uint256 constant VALID_WEIGHT = 1e18; + uint256 constant BASE = 1e18; + + function setUp() external { + factory = new MockBCoWFactory(address(0), bytes32(0)); + + address solutionSettler = makeAddr('solutionSettler'); + vm.mockCall( + solutionSettler, abi.encodePacked(ISettlement.domainSeparator.selector), abi.encode(bytes32('domainSeparator')) + ); + vm.mockCall( + solutionSettler, abi.encodePacked(ISettlement.vaultRelayer.selector), abi.encode(makeAddr('vaultRelayer')) + ); + pool = new MockBCoWPool(makeAddr('solutionSettler'), bytes32(0)); + + // creating a valid pool setup + factory.mock_call_isBPool(address(pool), true); + tokens[0] = makeAddr('token0'); + tokens[1] = makeAddr('token1'); + pool.set__tokens(tokens); + pool.set__records(tokens[0], IBPool.Record({bound: true, index: 0, denorm: VALID_WEIGHT})); + pool.set__records(tokens[1], IBPool.Record({bound: true, index: 1, denorm: VALID_WEIGHT})); + pool.set__totalWeight(2 * VALID_WEIGHT); + pool.set__finalized(true); + + priceVector[0] = 1e18; + priceVector[1] = 1.05e18; + + vm.mockCall(tokens[0], abi.encodePacked(IERC20.balanceOf.selector), abi.encode(priceVector[0])); + vm.mockCall(tokens[1], abi.encodePacked(IERC20.balanceOf.selector), abi.encode(priceVector[1])); + + factory.mock_call_APP_DATA(bytes32('appData')); + helper = new MockBCoWHelper(address(factory)); + } + + function test_ConstructorWhenCalled(bytes32 _appData) external { + factory.expectCall_APP_DATA(); + factory.mock_call_APP_DATA(_appData); + helper = new MockBCoWHelper(address(factory)); + // it should set factory + assertEq(helper.factory(), address(factory)); + // it should set app data from factory + assertEq(helper.call__APP_DATA(), _appData); + } + + function test_TokensRevertWhen_PoolIsNotRegisteredInFactory() external { + factory.mock_call_isBPool(address(pool), false); + // it should revert + vm.expectRevert(ICOWAMMPoolHelper.PoolDoesNotExist.selector); + helper.tokens(address(pool)); + } + + function test_TokensRevertWhen_PoolHasLessThan2Tokens() external { + address[] memory invalidTokens = new address[](1); + invalidTokens[0] = makeAddr('token0'); + pool.set__tokens(invalidTokens); + // it should revert + vm.expectRevert(ICOWAMMPoolHelper.PoolDoesNotExist.selector); + helper.tokens(address(pool)); + } + + function test_TokensRevertWhen_PoolHasMoreThan2Tokens() external { + address[] memory invalidTokens = new address[](3); + invalidTokens[0] = makeAddr('token0'); + invalidTokens[1] = makeAddr('token1'); + invalidTokens[2] = makeAddr('token2'); + pool.set__tokens(invalidTokens); + // it should revert + vm.expectRevert(ICOWAMMPoolHelper.PoolDoesNotExist.selector); + helper.tokens(address(pool)); + } + + function test_TokensRevertWhen_PoolTokensHaveDifferentWeights() external { + pool.mock_call_getNormalizedWeight(tokens[0], VALID_WEIGHT); + pool.mock_call_getNormalizedWeight(tokens[1], VALID_WEIGHT + 1); + + vm.expectRevert(ICOWAMMPoolHelper.PoolDoesNotExist.selector); + // it should revert + helper.tokens(address(pool)); + } + + function test_TokensWhenPoolIsSupported() external view { + // it should return pool tokens + address[] memory returned = helper.tokens(address(pool)); + assertEq(returned[0], tokens[0]); + assertEq(returned[1], tokens[1]); + } + + function test_OrderRevertWhen_ThePoolIsNotSupported() external { + // it should revert + vm.expectRevert(ICOWAMMPoolHelper.PoolDoesNotExist.selector); + helper.order(invalidPool, priceVector); + } + + function test_OrderWhenThePoolIsSupported(bytes32 domainSeparator) external { + // it should call tokens + helper.mock_call_tokens(address(pool), tokens); + helper.expectCall_tokens(address(pool)); + + // it should query the domain separator from the pool + pool.expectCall_SOLUTION_SETTLER_DOMAIN_SEPARATOR(); + pool.mock_call_SOLUTION_SETTLER_DOMAIN_SEPARATOR(domainSeparator); + + ( + GPv2Order.Data memory order_, + GPv2Interaction.Data[] memory preInteractions, + GPv2Interaction.Data[] memory postInteractions, + bytes memory sig + ) = helper.order(address(pool), priceVector); + + // it should return a valid pool order + assertEq(order_.receiver, GPv2Order.RECEIVER_SAME_AS_OWNER); + assertLe(order_.validTo, block.timestamp + 5 minutes); + assertEq(order_.feeAmount, 0); + assertEq(order_.appData, factory.APP_DATA()); + assertEq(order_.kind, GPv2Order.KIND_SELL); + assertEq(order_.buyTokenBalance, GPv2Order.BALANCE_ERC20); + assertEq(order_.sellTokenBalance, GPv2Order.BALANCE_ERC20); + + // it should return a commit pre-interaction + assertEq(preInteractions.length, 1); + assertEq(preInteractions[0].target, address(pool)); + assertEq(preInteractions[0].value, 0); + bytes memory commitment = abi.encodeCall(IBCoWPool.commit, GPv2Order.hash(order_, domainSeparator)); + assertEq(keccak256(preInteractions[0].callData), keccak256(commitment)); + + // it should return an empty post-interaction + assertTrue(postInteractions.length == 0); + + // it should return a valid signature + bytes memory validSig = abi.encodePacked(pool, abi.encode(order_)); + assertEq(keccak256(validSig), keccak256(sig)); + } + + function test_OrderGivenAPriceSkewenessToToken1( + uint256 priceSkewness, + uint256 balanceToken0, + uint256 balanceToken1 + ) external { + // skew the price by max 50% (more could result in reverts bc of max swap ratio) + // avoids no-skewness revert + priceSkewness = bound(priceSkewness, BASE + 0.0001e18, 1.5e18); + + balanceToken0 = bound(balanceToken0, 1e18, 1e27); + balanceToken1 = bound(balanceToken1, 1e18, 1e27); + vm.mockCall(tokens[0], abi.encodePacked(IERC20.balanceOf.selector), abi.encode(balanceToken0)); + vm.mockCall(tokens[1], abi.encodePacked(IERC20.balanceOf.selector), abi.encode(balanceToken1)); + + // NOTE: the price of token 1 is increased by the skeweness + uint256[] memory prices = new uint256[](2); + prices[0] = balanceToken1; + prices[1] = balanceToken0 * priceSkewness / BASE; + + // it should return a valid pool order + (GPv2Order.Data memory ammOrder,,,) = helper.order(address(pool), prices); + + // it should buy token0 + assertEq(address(ammOrder.buyToken), tokens[0]); + + // it should return a valid pool order + // this call should not revert + pool.verify(ammOrder); + } + + function test_OrderGivenAPriceSkewenessToToken0( + uint256 priceSkewness, + uint256 balanceToken0, + uint256 balanceToken1 + ) external { + // skew the price by max 50% (more could result in reverts bc of max swap ratio) + // avoids no-skewness revert + priceSkewness = bound(priceSkewness, 0.5e18, BASE - 0.0001e18); + + balanceToken0 = bound(balanceToken0, 1e18, 1e27); + balanceToken1 = bound(balanceToken1, 1e18, 1e27); + vm.mockCall(tokens[0], abi.encodePacked(IERC20.balanceOf.selector), abi.encode(balanceToken0)); + vm.mockCall(tokens[1], abi.encodePacked(IERC20.balanceOf.selector), abi.encode(balanceToken1)); + + // NOTE: the price of token 1 is decrease by the skeweness + uint256[] memory prices = new uint256[](2); + prices[0] = balanceToken1; + prices[1] = balanceToken0 * priceSkewness / BASE; + + // it should return a valid pool order + (GPv2Order.Data memory ammOrder,,,) = helper.order(address(pool), prices); + + // it should buy token1 + assertEq(address(ammOrder.buyToken), tokens[1]); + + // it should return a valid pool order + // this call should not revert + pool.verify(ammOrder); + } +} diff --git a/test/unit/BCoWHelper.tree b/test/unit/BCoWHelper.tree new file mode 100644 index 00000000..e27a5207 --- /dev/null +++ b/test/unit/BCoWHelper.tree @@ -0,0 +1,33 @@ +BCoWHelperTest::constructor +└── when called + ├── it should set factory + └── it should set app data from factory + +BCoWHelperTest::tokens +├── when pool is not registered in factory +│ └── it should revert +├── when pool has less than 2 tokens +│ └── it should revert +├── when pool has more than 2 tokens +│ └── it should revert +├── when pool tokens have different weights +│ └── it should revert +└── when pool is supported + └── it should return pool tokens + +BCoWHelperTest::order +├── when the pool is not supported +│ └── it should revert +├── when the pool is supported +│ ├── it should call tokens +│ ├── it should query the domain separator from the pool +│ ├── it should return a valid pool order +│ ├── it should return a commit pre-interaction +│ ├── it should return an empty post-interaction +│ └── it should return a valid signature +├── given a price skeweness to token1 +│ ├── it should buy token0 +│ └── it should return a valid pool order +└── given a price skeweness to token0 + ├── it should buy token1 + └── it should return a valid pool order diff --git a/test/unit/BCoWPool.t.sol b/test/unit/BCoWPool.t.sol index 350ebb05..eb8acad5 100644 --- a/test/unit/BCoWPool.t.sol +++ b/test/unit/BCoWPool.t.sol @@ -46,73 +46,3 @@ abstract contract BaseCoWPoolTest is BasePoolTest, BCoWConst { }); } } - -contract BCoWPool_Unit_IsValidSignature is BaseCoWPoolTest { - function setUp() public virtual override { - super.setUp(); - for (uint256 i = 0; i < TOKENS_AMOUNT; i++) { - vm.mockCall(tokens[i], abi.encodePacked(IERC20.approve.selector), abi.encode(true)); - } - vm.mockCall(address(bCoWPool.FACTORY()), abi.encodeWithSelector(IBCoWFactory.logBCoWPool.selector), abi.encode()); - bCoWPool.finalize(); - } - - modifier happyPath(GPv2Order.Data memory _order) { - // sets the order appData to the one defined at deployment (setUp) - _order.appData = appData; - - // stores the order hash in the transient storage slot - bytes32 _orderHash = GPv2Order.hash(_order, domainSeparator); - bCoWPool.call__setLock(_orderHash); - _; - } - - function test_Revert_OrderWithWrongAppdata(GPv2Order.Data memory _order, bytes32 _appData) public { - vm.assume(_appData != appData); - _order.appData = _appData; - bytes32 _orderHash = GPv2Order.hash(_order, domainSeparator); - vm.expectRevert(IBCoWPool.AppDataDoesNotMatch.selector); - bCoWPool.isValidSignature(_orderHash, abi.encode(_order)); - } - - function test_Revert_OrderSignedWithWrongDomainSeparator( - GPv2Order.Data memory _order, - bytes32 _differentDomainSeparator - ) public happyPath(_order) { - vm.assume(_differentDomainSeparator != domainSeparator); - bytes32 _orderHash = GPv2Order.hash(_order, _differentDomainSeparator); - vm.expectRevert(IBCoWPool.OrderDoesNotMatchMessageHash.selector); - bCoWPool.isValidSignature(_orderHash, abi.encode(_order)); - } - - function test_Revert_OrderWithUnrelatedSignature( - GPv2Order.Data memory _order, - bytes32 _orderHash - ) public happyPath(_order) { - vm.expectRevert(IBCoWPool.OrderDoesNotMatchMessageHash.selector); - bCoWPool.isValidSignature(_orderHash, abi.encode(_order)); - } - - function test_Revert_OrderHashDifferentFromCommitment( - GPv2Order.Data memory _order, - bytes32 _differentCommitment - ) public happyPath(_order) { - bCoWPool.call__setLock(_differentCommitment); - bytes32 _orderHash = GPv2Order.hash(_order, domainSeparator); - vm.expectRevert(IBCoWPool.OrderDoesNotMatchCommitmentHash.selector); - bCoWPool.isValidSignature(_orderHash, abi.encode(_order)); - } - - function test_Call_Verify(GPv2Order.Data memory _order) public happyPath(_order) { - bytes32 _orderHash = GPv2Order.hash(_order, domainSeparator); - bCoWPool.mock_call_verify(_order); - bCoWPool.expectCall_verify(_order); - bCoWPool.isValidSignature(_orderHash, abi.encode(_order)); - } - - function test_Return_MagicValue(GPv2Order.Data memory _order) public happyPath(_order) { - bytes32 _orderHash = GPv2Order.hash(_order, domainSeparator); - bCoWPool.mock_call_verify(_order); - assertEq(bCoWPool.isValidSignature(_orderHash, abi.encode(_order)), IERC1271.isValidSignature.selector); - } -} diff --git a/test/unit/BCoWPool/BCoWPool_IsValidSignature.t.sol b/test/unit/BCoWPool/BCoWPool_IsValidSignature.t.sol new file mode 100644 index 00000000..aeb8086b --- /dev/null +++ b/test/unit/BCoWPool/BCoWPool_IsValidSignature.t.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import {IERC20} from '@cowprotocol/interfaces/IERC20.sol'; + +import {GPv2Order} from '@cowprotocol/libraries/GPv2Order.sol'; +import {IERC1271} from '@openzeppelin/contracts/interfaces/IERC1271.sol'; + +import {BCoWPoolBase} from './BCoWPoolBase.sol'; +import {IBCoWPool} from 'interfaces/IBCoWPool.sol'; + +contract BCoWPoolIsValidSignature is BCoWPoolBase { + GPv2Order.Data validOrder; + bytes32 validHash; + + function setUp() public virtual override { + super.setUp(); + // only set up the values that are checked in this method + validOrder.appData = appData; + validHash = GPv2Order.hash(validOrder, domainSeparator); + + bCoWPool.mock_call_verify(validOrder); + } + + function test_RevertWhen_OrdersAppdataIsDifferentThanOneSetAtConstruction(bytes32 appData_) external { + vm.assume(appData != appData_); + validOrder.appData = appData_; + // it should revert + vm.expectRevert(IBCoWPool.AppDataDoesNotMatch.selector); + bCoWPool.isValidSignature(validHash, abi.encode(validOrder)); + } + + function test_RevertWhen_OrderHashDoesNotMatchHashedOrder(bytes32 orderHash) external { + vm.assume(orderHash != validHash); + // it should revert + vm.expectRevert(IBCoWPool.OrderDoesNotMatchMessageHash.selector); + bCoWPool.isValidSignature(orderHash, abi.encode(validOrder)); + } + + function test_RevertWhen_HashedOrderDoesNotMatchCommitment(bytes32 commitment) external { + vm.assume(validHash != commitment); + bCoWPool.call__setLock(commitment); + // it should revert + vm.expectRevert(IBCoWPool.OrderDoesNotMatchCommitmentHash.selector); + bCoWPool.isValidSignature(validHash, abi.encode(validOrder)); + } + + function test_WhenPreconditionsAreMet() external { + // can't do it in setUp because transient storage is wiped in between + bCoWPool.call__setLock(validHash); + // it calls verify + bCoWPool.expectCall_verify(validOrder); + // it returns EIP-1271 magic value + assertEq(bCoWPool.isValidSignature(validHash, abi.encode(validOrder)), IERC1271.isValidSignature.selector); + } +} diff --git a/test/unit/BCoWPool/BCoWPool_IsValidSignature.tree b/test/unit/BCoWPool/BCoWPool_IsValidSignature.tree new file mode 100644 index 00000000..524e8290 --- /dev/null +++ b/test/unit/BCoWPool/BCoWPool_IsValidSignature.tree @@ -0,0 +1,10 @@ +BCoWPool::IsValidSignature +├── when orders appdata is different than one set at construction +│ └── it should revert +├── when orderHash does not match hashed order +│ └── it should revert +├── when hashed order does not match commitment +│ └── it should revert +└── when preconditions are met + ├── it calls verify + └── it returns EIP-1271 magic value diff --git a/test/unit/BPool.t.sol b/test/unit/BPool.t.sol index 7e80c886..9010ae93 100644 --- a/test/unit/BPool.t.sol +++ b/test/unit/BPool.t.sol @@ -245,467 +245,6 @@ abstract contract BasePoolTest is Test, BConst, Utils, BMath { } } -contract BPool_Unit_GetCurrentTokens is BasePoolTest { - function test_Returns_CurrentTokens(uint256 _length) public { - vm.assume(_length > 0); - vm.assume(_length <= MAX_BOUND_TOKENS); - address[] memory _tokensToAdd = _setRandomTokens(_length); - - assertEq(bPool.getCurrentTokens(), _tokensToAdd); - } - - function test_Revert_Reentrancy() public { - _expectRevertByReentrancy(); - bPool.getCurrentTokens(); - } -} - -contract BPool_Unit_GetFinalTokens is BasePoolTest { - function test_Returns_FinalTokens(uint256 _length) public { - vm.assume(_length > 0); - vm.assume(_length <= MAX_BOUND_TOKENS); - address[] memory _tokensToAdd = _setRandomTokens(_length); - _setFinalize(true); - - assertEq(bPool.getFinalTokens(), _tokensToAdd); - } - - function test_Revert_Reentrancy() public { - _expectRevertByReentrancy(); - bPool.getFinalTokens(); - } - - function test_Revert_NotFinalized(uint256 _length) public { - vm.assume(_length > 0); - vm.assume(_length <= MAX_BOUND_TOKENS); - _setRandomTokens(_length); - _setFinalize(false); - - vm.expectRevert(IBPool.BPool_PoolNotFinalized.selector); - bPool.getFinalTokens(); - } -} - -contract BPool_Unit_GetDenormalizedWeight is BasePoolTest { - function test_Returns_DenormalizedWeight(address _token, uint256 _weight) public { - bPool.set__records(_token, IBPool.Record({bound: true, index: 0, denorm: _weight})); - - assertEq(bPool.getDenormalizedWeight(_token), _weight); - } - - function test_Revert_Reentrancy() public { - _expectRevertByReentrancy(); - bPool.getDenormalizedWeight(address(0)); - } - - function test_Revert_NotBound(address _token) public { - vm.expectRevert(IBPool.BPool_TokenNotBound.selector); - bPool.getDenormalizedWeight(_token); - } -} - -contract BPool_Unit_GetTotalDenormalizedWeight is BasePoolTest { - function test_Returns_TotalDenormalizedWeight(uint256 _totalWeight) public { - _setTotalWeight(_totalWeight); - - assertEq(bPool.getTotalDenormalizedWeight(), _totalWeight); - } - - function test_Revert_Reentrancy() public { - _expectRevertByReentrancy(); - bPool.getTotalDenormalizedWeight(); - } -} - -contract BPool_Unit_GetNormalizedWeight is BasePoolTest { - function test_Returns_NormalizedWeight(address _token, uint256 _weight, uint256 _totalWeight) public { - _weight = bound(_weight, MIN_WEIGHT, MAX_WEIGHT); - _totalWeight = bound(_totalWeight, MIN_WEIGHT, MAX_TOTAL_WEIGHT); - vm.assume(_weight < _totalWeight); - bPool.set__records(_token, IBPool.Record({bound: true, index: 0, denorm: _weight})); - _setTotalWeight(_totalWeight); - - assertEq(bPool.getNormalizedWeight(_token), bdiv(_weight, _totalWeight)); - } - - function test_Revert_Reentrancy() public { - _expectRevertByReentrancy(); - bPool.getNormalizedWeight(address(0)); - } - - function test_Revert_NotBound(address _token) public { - vm.expectRevert(IBPool.BPool_TokenNotBound.selector); - bPool.getNormalizedWeight(_token); - } -} - -contract BPool_Unit_GetBalance is BasePoolTest { - function test_Returns_Balance(address _token, uint256 _balance) public { - assumeNotForgeAddress(_token); - - bPool.set__records(_token, IBPool.Record({bound: true, index: 0, denorm: 0})); - _mockPoolBalance(_token, _balance); - - assertEq(bPool.getBalance(_token), _balance); - } - - function test_Revert_Reentrancy() public { - _expectRevertByReentrancy(); - bPool.getBalance(address(0)); - } - - function test_Revert_NotBound(address _token) public { - vm.expectRevert(IBPool.BPool_TokenNotBound.selector); - bPool.getBalance(_token); - } -} - -contract BPool_Unit_GetSwapFee is BasePoolTest { - function test_Returns_SwapFee(uint256 _swapFee) public { - _setSwapFee(_swapFee); - - assertEq(bPool.getSwapFee(), _swapFee); - } - - function test_Revert_Reentrancy() public { - _expectRevertByReentrancy(); - bPool.getSwapFee(); - } -} - -contract BPool_Unit_GetController is BasePoolTest { - function test_Returns_Controller(address _controller) public { - bPool.set__controller(_controller); - - assertEq(bPool.getController(), _controller); - } - - function test_Revert_Reentrancy() public { - _expectRevertByReentrancy(); - bPool.getController(); - } -} - -contract BPool_Unit_SetSwapFee is BasePoolTest { - modifier happyPath(uint256 _fee) { - vm.assume(_fee >= MIN_FEE); - vm.assume(_fee <= MAX_FEE); - _; - } - - function test_Revert_Finalized(uint256 _fee) public happyPath(_fee) { - _setFinalize(true); - - vm.expectRevert(IBPool.BPool_PoolIsFinalized.selector); - bPool.setSwapFee(_fee); - } - - function test_Revert_NotController(address _controller, address _caller, uint256 _fee) public happyPath(_fee) { - vm.assume(_controller != _caller); - bPool.set__controller(_controller); - - vm.expectRevert(IBPool.BPool_CallerIsNotController.selector); - vm.prank(_caller); - bPool.setSwapFee(_fee); - } - - function test_Revert_MinFee(uint256 _fee) public { - vm.assume(_fee < MIN_FEE); - - vm.expectRevert(IBPool.BPool_FeeBelowMinimum.selector); - bPool.setSwapFee(_fee); - } - - function test_Revert_MaxFee(uint256 _fee) public { - vm.assume(_fee > MAX_FEE); - - vm.expectRevert(IBPool.BPool_FeeAboveMaximum.selector); - bPool.setSwapFee(_fee); - } - - function test_Revert_Reentrancy(uint256 _fee) public happyPath(_fee) { - _expectRevertByReentrancy(); - bPool.setSwapFee(_fee); - } - - function test_Set_SwapFee(uint256 _fee) public happyPath(_fee) { - bPool.setSwapFee(_fee); - - assertEq(bPool.call__swapFee(), _fee); - } - - function test_Set_ReentrancyLock(uint256 _fee) public happyPath(_fee) { - _expectSetReentrancyLock(); - bPool.setSwapFee(_fee); - } - - function test_Emit_LogCall(uint256 _fee) public happyPath(_fee) { - vm.expectEmit(); - bytes memory _data = abi.encodeWithSelector(BPool.setSwapFee.selector, _fee); - emit IBPool.LOG_CALL(BPool.setSwapFee.selector, address(this), _data); - - bPool.setSwapFee(_fee); - } -} - -contract BPool_Unit_SetController is BasePoolTest { - function test_Revert_NotController(address _controller, address _caller, address _newController) public { - vm.assume(_newController != address(0)); - vm.assume(_controller != _caller); - bPool.set__controller(_controller); - - vm.expectRevert(IBPool.BPool_CallerIsNotController.selector); - vm.prank(_caller); - bPool.setController(_newController); - } - - function test_Revert_Reentrancy(address _controller) public { - _expectRevertByReentrancy(); - bPool.setController(_controller); - } - - function test_Revert_AddressZero() public { - vm.expectRevert(IBPool.BPool_AddressZero.selector); - - bPool.setController(address(0)); - } - - function test_Set_Controller(address _controller) public { - vm.assume(_controller != address(0)); - bPool.setController(_controller); - - assertEq(bPool.call__controller(), _controller); - } - - function test_Emit_LogCall(address _controller) public { - vm.assume(_controller != address(0)); - vm.expectEmit(); - bytes memory _data = abi.encodeWithSelector(BPool.setController.selector, _controller); - emit IBPool.LOG_CALL(BPool.setController.selector, address(this), _data); - - bPool.setController(_controller); - } - - function test_Set_ReentrancyLock(address _controller) public { - vm.assume(_controller != address(0)); - _expectSetReentrancyLock(); - bPool.setController(_controller); - } -} - -contract BPool_Unit_Finalize is BasePoolTest { - modifier happyPath(uint256 _tokensLength) { - _tokensLength = bound(_tokensLength, MIN_BOUND_TOKENS, MAX_BOUND_TOKENS); - _setRandomTokens(_tokensLength); - _; - } - - function test_Revert_NotController( - address _controller, - address _caller, - uint256 _tokensLength - ) public happyPath(_tokensLength) { - vm.assume(_controller != _caller); - bPool.set__controller(_controller); - - vm.prank(_caller); - vm.expectRevert(IBPool.BPool_CallerIsNotController.selector); - bPool.finalize(); - } - - function test_Revert_Finalized(uint256 _tokensLength) public happyPath(_tokensLength) { - _setFinalize(true); - - vm.expectRevert(IBPool.BPool_PoolIsFinalized.selector); - bPool.finalize(); - } - - function test_Revert_MinTokens(uint256 _tokensLength) public { - _tokensLength = bound(_tokensLength, 0, MIN_BOUND_TOKENS - 1); - _setRandomTokens(_tokensLength); - - vm.expectRevert(IBPool.BPool_TokensBelowMinimum.selector); - bPool.finalize(); - } - - function test_Revert_Reentrancy(uint256 _tokensLength) public happyPath(_tokensLength) { - _expectRevertByReentrancy(); - bPool.finalize(); - } - - function test_Set_Finalize(uint256 _tokensLength) public happyPath(_tokensLength) { - bPool.finalize(); - - assertEq(bPool.call__finalized(), true); - } - - function test_Set_ReentrancyLock(uint256 _tokensLength) public happyPath(_tokensLength) { - _expectSetReentrancyLock(); - bPool.finalize(); - } - - function test_Call_AfterFinalizeHook(uint256 _tokensLength) public happyPath(_tokensLength) { - bPool.expectCall__afterFinalize(); - bPool.finalize(); - } - - function test_Mint_InitPoolSupply(uint256 _tokensLength) public happyPath(_tokensLength) { - bPool.finalize(); - - assertEq(bPool.totalSupply(), INIT_POOL_SUPPLY); - } - - function test_Push_InitPoolSupply(uint256 _tokensLength) public happyPath(_tokensLength) { - bPool.finalize(); - - assertEq(bPool.balanceOf(address(this)), INIT_POOL_SUPPLY); - } - - function test_Emit_LogCall(uint256 _tokensLength) public happyPath(_tokensLength) { - vm.expectEmit(); - bytes memory _data = abi.encodeWithSelector(BPool.finalize.selector); - emit IBPool.LOG_CALL(BPool.finalize.selector, address(this), _data); - - bPool.finalize(); - } -} - -contract BPool_Unit_GetSpotPrice is BasePoolTest { - struct GetSpotPrice_FuzzScenario { - address tokenIn; - address tokenOut; - uint256 tokenInBalance; - uint256 tokenInDenorm; - uint256 tokenOutBalance; - uint256 tokenOutDenorm; - uint256 swapFee; - } - - function _setValues(GetSpotPrice_FuzzScenario memory _fuzz) internal { - bPool.set__records(_fuzz.tokenIn, IBPool.Record({bound: true, index: 0, denorm: _fuzz.tokenInDenorm})); - _mockPoolBalance(_fuzz.tokenIn, _fuzz.tokenInBalance); - bPool.set__records(_fuzz.tokenOut, IBPool.Record({bound: true, index: 0, denorm: _fuzz.tokenOutDenorm})); - _mockPoolBalance(_fuzz.tokenOut, _fuzz.tokenOutBalance); - _setSwapFee(_fuzz.swapFee); - } - - function _assumeHappyPath(GetSpotPrice_FuzzScenario memory _fuzz) internal pure { - assumeNotForgeAddress(_fuzz.tokenIn); - assumeNotForgeAddress(_fuzz.tokenOut); - vm.assume(_fuzz.tokenIn != _fuzz.tokenOut); - _assumeCalcSpotPrice( - _fuzz.tokenInBalance, _fuzz.tokenInDenorm, _fuzz.tokenOutBalance, _fuzz.tokenOutDenorm, _fuzz.swapFee - ); - } - - modifier happyPath(GetSpotPrice_FuzzScenario memory _fuzz) { - _assumeHappyPath(_fuzz); - _setValues(_fuzz); - _; - } - - function test_Revert_NotBoundTokenIn( - GetSpotPrice_FuzzScenario memory _fuzz, - address _tokenIn - ) public happyPath(_fuzz) { - vm.assume(_tokenIn != _fuzz.tokenIn); - vm.assume(_tokenIn != _fuzz.tokenOut); - - vm.expectRevert(IBPool.BPool_TokenNotBound.selector); - bPool.getSpotPrice(_tokenIn, _fuzz.tokenOut); - } - - function test_Revert_NotBoundTokenOut( - GetSpotPrice_FuzzScenario memory _fuzz, - address _tokenOut - ) public happyPath(_fuzz) { - vm.assume(_tokenOut != _fuzz.tokenIn); - vm.assume(_tokenOut != _fuzz.tokenOut); - - vm.expectRevert(IBPool.BPool_TokenNotBound.selector); - bPool.getSpotPrice(_fuzz.tokenIn, _tokenOut); - } - - function test_Returns_SpotPrice(GetSpotPrice_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - uint256 _expectedSpotPrice = calcSpotPrice( - _fuzz.tokenInBalance, _fuzz.tokenInDenorm, _fuzz.tokenOutBalance, _fuzz.tokenOutDenorm, _fuzz.swapFee - ); - uint256 _spotPrice = bPool.getSpotPrice(_fuzz.tokenIn, _fuzz.tokenOut); - assertEq(_spotPrice, _expectedSpotPrice); - } - - function test_Revert_Reentrancy(GetSpotPrice_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - _expectRevertByReentrancy(); - bPool.getSpotPrice(_fuzz.tokenIn, _fuzz.tokenOut); - } -} - -contract BPool_Unit_GetSpotPriceSansFee is BasePoolTest { - struct GetSpotPriceSansFee_FuzzScenario { - address tokenIn; - address tokenOut; - uint256 tokenInBalance; - uint256 tokenInDenorm; - uint256 tokenOutBalance; - uint256 tokenOutDenorm; - } - - function _setValues(GetSpotPriceSansFee_FuzzScenario memory _fuzz) internal { - bPool.set__records(_fuzz.tokenIn, IBPool.Record({bound: true, index: 0, denorm: _fuzz.tokenInDenorm})); - _mockPoolBalance(_fuzz.tokenIn, _fuzz.tokenInBalance); - bPool.set__records(_fuzz.tokenOut, IBPool.Record({bound: true, index: 0, denorm: _fuzz.tokenOutDenorm})); - _mockPoolBalance(_fuzz.tokenOut, _fuzz.tokenOutBalance); - _setSwapFee(0); - } - - function _assumeHappyPath(GetSpotPriceSansFee_FuzzScenario memory _fuzz) internal pure { - assumeNotForgeAddress(_fuzz.tokenIn); - assumeNotForgeAddress(_fuzz.tokenOut); - vm.assume(_fuzz.tokenIn != _fuzz.tokenOut); - _assumeCalcSpotPrice(_fuzz.tokenInBalance, _fuzz.tokenInDenorm, _fuzz.tokenOutBalance, _fuzz.tokenOutDenorm, 0); - } - - modifier happyPath(GetSpotPriceSansFee_FuzzScenario memory _fuzz) { - _assumeHappyPath(_fuzz); - _setValues(_fuzz); - _; - } - - function test_Revert_NotBoundTokenIn( - GetSpotPriceSansFee_FuzzScenario memory _fuzz, - address _tokenIn - ) public happyPath(_fuzz) { - vm.assume(_tokenIn != _fuzz.tokenIn); - vm.assume(_tokenIn != _fuzz.tokenOut); - - vm.expectRevert(IBPool.BPool_TokenNotBound.selector); - bPool.getSpotPriceSansFee(_tokenIn, _fuzz.tokenOut); - } - - function test_Revert_NotBoundTokenOut( - GetSpotPriceSansFee_FuzzScenario memory _fuzz, - address _tokenOut - ) public happyPath(_fuzz) { - vm.assume(_tokenOut != _fuzz.tokenIn); - vm.assume(_tokenOut != _fuzz.tokenOut); - - vm.expectRevert(IBPool.BPool_TokenNotBound.selector); - bPool.getSpotPriceSansFee(_fuzz.tokenIn, _tokenOut); - } - - function test_Returns_SpotPrice(GetSpotPriceSansFee_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - uint256 _expectedSpotPrice = - calcSpotPrice(_fuzz.tokenInBalance, _fuzz.tokenInDenorm, _fuzz.tokenOutBalance, _fuzz.tokenOutDenorm, 0); - uint256 _spotPrice = bPool.getSpotPriceSansFee(_fuzz.tokenIn, _fuzz.tokenOut); - assertEq(_spotPrice, _expectedSpotPrice); - } - - function test_Revert_Reentrancy(GetSpotPriceSansFee_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - _expectRevertByReentrancy(); - bPool.getSpotPriceSansFee(_fuzz.tokenIn, _fuzz.tokenOut); - } -} - contract BPool_Unit_JoinswapPoolAmountOut is BasePoolTest { address tokenIn; diff --git a/test/unit/BPool/BPool.t.sol b/test/unit/BPool/BPool.t.sol index 6bf2db80..7b1f2ec3 100644 --- a/test/unit/BPool/BPool.t.sol +++ b/test/unit/BPool/BPool.t.sol @@ -2,14 +2,45 @@ pragma solidity 0.8.25; import {BPoolBase} from './BPoolBase.sol'; + +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; + +import {BMath} from 'contracts/BMath.sol'; import {IBPool} from 'interfaces/IBPool.sol'; import {MockBPool} from 'test/smock/MockBPool.sol'; -contract BPool is BPoolBase { +contract BPool is BPoolBase, BMath { + address controller = makeAddr('controller'); + address randomCaller = makeAddr('random caller'); + address unknownToken = makeAddr('unknown token'); + uint256 swapFee = 0.1e18; + uint256 public tokenWeight = 1e18; + uint256 public totalWeight = 10e18; + uint256 public balanceTokenIn = 10e18; + uint256 public balanceTokenOut = 20e18; + + // sP = (tokenInBalance / tokenInWeight) / (tokenOutBalance/ tokenOutWeight) * (1 / (1 - swapFee)) + // tokenInWeight == tokenOutWeight + // sP = 10 / 20 = 0.5e18 + // sPf = (10 / 20) * (1 / (1-0.1)) = 0.555...e18 (round-up) + uint256 public spotPriceWithoutFee = 0.5e18; + uint256 public spotPrice = 0.555555555555555556e18; + + function setUp() public virtual override { + super.setUp(); + + bPool.set__finalized(true); + bPool.set__tokens(tokens); + bPool.set__records(tokens[0], IBPool.Record({bound: true, index: 0, denorm: tokenWeight})); + bPool.set__records(tokens[1], IBPool.Record({bound: true, index: 1, denorm: tokenWeight})); + bPool.set__totalWeight(totalWeight); + bPool.set__swapFee(swapFee); + bPool.set__controller(controller); + } function test_ConstructorWhenCalled(address _deployer) external { - vm.prank(_deployer); + vm.startPrank(_deployer); MockBPool _newBPool = new MockBPool(); // it sets caller as controller @@ -22,8 +53,96 @@ contract BPool is BPoolBase { assertEq(_newBPool.call__finalized(), false); } - function test_IsFinalizedWhenPoolIsFinalized() external { - bPool.set__finalized(true); + function test_SetSwapFeeRevertWhen_ReentrancyLockIsSet() external { + bPool.call__setLock(_MUTEX_TAKEN); + // it should revert + vm.expectRevert(IBPool.BPool_Reentrancy.selector); + bPool.setSwapFee(0); + } + + function test_SetSwapFeeRevertWhen_CallerIsNotController() external { + vm.prank(randomCaller); + // it should revert + vm.expectRevert(IBPool.BPool_CallerIsNotController.selector); + bPool.setSwapFee(0); + } + + function test_SetSwapFeeRevertWhen_PoolIsFinalized() external { + vm.prank(controller); + // it should revert + vm.expectRevert(IBPool.BPool_PoolIsFinalized.selector); + bPool.setSwapFee(0); + } + + function test_SetSwapFeeRevertWhen_SwapFeeIsBelowMIN_FEE() external { + bPool.set__finalized(false); + vm.prank(controller); + // it should revert + vm.expectRevert(IBPool.BPool_FeeBelowMinimum.selector); + bPool.setSwapFee(MIN_FEE - 1); + } + + function test_SetSwapFeeRevertWhen_SwapFeeIsAboveMAX_FEE() external { + bPool.set__finalized(false); + vm.prank(controller); + // it should revert + vm.expectRevert(IBPool.BPool_FeeAboveMaximum.selector); + bPool.setSwapFee(MAX_FEE + 1); + } + + function test_SetSwapFeeWhenPreconditionsAreMet(uint256 _swapFee) external { + bPool.set__finalized(false); + vm.prank(controller); + _swapFee = bound(_swapFee, MIN_FEE, MAX_FEE); + + // it emits LOG_CALL event + vm.expectEmit(); + bytes memory _data = abi.encodeWithSelector(IBPool.setSwapFee.selector, _swapFee); + emit IBPool.LOG_CALL(IBPool.setSwapFee.selector, controller, _data); + + bPool.setSwapFee(_swapFee); + + // it sets swap fee + assertEq(bPool.getSwapFee(), _swapFee); + } + + function test_SetControllerRevertWhen_ReentrancyLockIsSet() external { + bPool.call__setLock(_MUTEX_TAKEN); + // it should revert + vm.expectRevert(IBPool.BPool_Reentrancy.selector); + bPool.setController(controller); + } + + function test_SetControllerRevertWhen_CallerIsNotController() external { + vm.prank(randomCaller); + // it should revert + vm.expectRevert(IBPool.BPool_CallerIsNotController.selector); + bPool.setController(controller); + } + + function test_SetControllerRevertWhen_NewControllerIsZeroAddress() external { + vm.prank(controller); + // it should revert + vm.expectRevert(IBPool.BPool_AddressZero.selector); + bPool.setController(address(0)); + } + + function test_SetControllerWhenPreconditionsAreMet(address _controller) external { + vm.prank(controller); + vm.assume(_controller != address(0)); + + // it emits LOG_CALL event + vm.expectEmit(); + bytes memory _data = abi.encodeWithSelector(IBPool.setController.selector, _controller); + emit IBPool.LOG_CALL(IBPool.setController.selector, controller, _data); + + bPool.setController(_controller); + + // it sets new controller + assertEq(bPool.getController(), _controller); + } + + function test_IsFinalizedWhenPoolIsFinalized() external view { // it returns true assertTrue(bPool.isFinalized()); } @@ -53,22 +172,221 @@ contract BPool is BPoolBase { assertEq(bPool.getNumTokens(), _tokensToAdd); } + function test_GetFinalTokensRevertWhen_ReentrancyLockIsSet() external { + bPool.call__setLock(_MUTEX_TAKEN); + // it should revert + vm.expectRevert(IBPool.BPool_Reentrancy.selector); + bPool.getFinalTokens(); + } + + function test_GetFinalTokensRevertWhen_PoolIsNotFinalized() external { + bPool.set__finalized(false); + // it should revert + vm.expectRevert(IBPool.BPool_PoolNotFinalized.selector); + bPool.getFinalTokens(); + } + + function test_GetFinalTokensWhenPreconditionsAreMet() external view { + // it returns pool tokens + address[] memory _tokens = bPool.getFinalTokens(); + assertEq(_tokens.length, tokens.length); + assertEq(_tokens[0], tokens[0]); + assertEq(_tokens[1], tokens[1]); + } + + function test_GetCurrentTokensRevertWhen_ReentrancyLockIsSet() external { + bPool.call__setLock(_MUTEX_TAKEN); + // it should revert + vm.expectRevert(IBPool.BPool_Reentrancy.selector); + bPool.getCurrentTokens(); + } + + function test_GetCurrentTokensWhenPreconditionsAreMet() external view { + // it returns pool tokens + address[] memory _tokens = bPool.getCurrentTokens(); + assertEq(_tokens.length, tokens.length); + assertEq(_tokens[0], tokens[0]); + assertEq(_tokens[1], tokens[1]); + } + + function test_GetDenormalizedWeightRevertWhen_ReentrancyLockIsSet() external { + bPool.call__setLock(_MUTEX_TAKEN); + // it should revert + vm.expectRevert(IBPool.BPool_Reentrancy.selector); + bPool.getDenormalizedWeight(tokens[0]); + } + + function test_GetDenormalizedWeightRevertWhen_TokenIsNotBound() external { + // it should revert + vm.expectRevert(IBPool.BPool_TokenNotBound.selector); + bPool.getDenormalizedWeight(unknownToken); + } + + function test_GetDenormalizedWeightWhenPreconditionsAreMet() external view { + // it returns token weight + uint256 _tokenWeight = bPool.getDenormalizedWeight(tokens[0]); + assertEq(_tokenWeight, tokenWeight); + } + + function test_GetTotalDenormalizedWeightRevertWhen_ReentrancyLockIsSet() external { + bPool.call__setLock(_MUTEX_TAKEN); + // it should revert + vm.expectRevert(IBPool.BPool_Reentrancy.selector); + bPool.getTotalDenormalizedWeight(); + } + + function test_GetTotalDenormalizedWeightWhenPreconditionsAreMet() external view { + // it returns total weight + uint256 _totalWeight = bPool.getTotalDenormalizedWeight(); + assertEq(_totalWeight, totalWeight); + } + + function test_GetNormalizedWeightRevertWhen_ReentrancyLockIsSet() external { + bPool.call__setLock(_MUTEX_TAKEN); + // it should revert + vm.expectRevert(IBPool.BPool_Reentrancy.selector); + bPool.getNormalizedWeight(tokens[0]); + } + + function test_GetNormalizedWeightRevertWhen_TokenIsNotBound() external { + // it should revert + vm.expectRevert(IBPool.BPool_TokenNotBound.selector); + bPool.getNormalizedWeight(unknownToken); + } + + function test_GetNormalizedWeightWhenPreconditionsAreMet() external view { + // it returns normalized weight + // normalizedWeight = tokenWeight / totalWeight + uint256 _normalizedWeight = bPool.getNormalizedWeight(tokens[0]); + assertEq(_normalizedWeight, 0.1e18); + } + + function test_GetBalanceRevertWhen_ReentrancyLockIsSet() external { + bPool.call__setLock(_MUTEX_TAKEN); + // it should revert + vm.expectRevert(IBPool.BPool_Reentrancy.selector); + bPool.getBalance(tokens[0]); + } + + function test_GetBalanceRevertWhen_TokenIsNotBound() external { + // it should revert + vm.expectRevert(IBPool.BPool_TokenNotBound.selector); + bPool.getBalance(unknownToken); + } + + function test_GetBalanceWhenPreconditionsAreMet(uint256 tokenBalance) external { + vm.mockCall(tokens[0], abi.encodePacked(IERC20.balanceOf.selector), abi.encode(tokenBalance)); + // it queries token balance + vm.expectCall(tokens[0], abi.encodeWithSelector(IERC20.balanceOf.selector)); + // it returns token balance + uint256 _balance = bPool.getBalance(tokens[0]); + assertEq(_balance, tokenBalance); + } + + function test_GetSwapFeeRevertWhen_ReentrancyLockIsSet() external { + bPool.call__setLock(_MUTEX_TAKEN); + // it should revert + vm.expectRevert(IBPool.BPool_Reentrancy.selector); + bPool.getSwapFee(); + } + + function test_GetSwapFeeWhenPreconditionsAreMet() external view { + // it returns swap fee + uint256 _swapFee = bPool.getSwapFee(); + assertEq(_swapFee, swapFee); + } + + function test_GetControllerRevertWhen_ReentrancyLockIsSet() external { + bPool.call__setLock(_MUTEX_TAKEN); + // it should revert + vm.expectRevert(IBPool.BPool_Reentrancy.selector); + bPool.getController(); + } + + function test_GetControllerWhenPreconditionsAreMet() external view { + // it returns controller + address _controller = bPool.getController(); + assertEq(_controller, controller); + } + + function test_GetSpotPriceRevertWhen_ReentrancyLockIsSet() external { + bPool.call__setLock(_MUTEX_TAKEN); + // it should revert + vm.expectRevert(IBPool.BPool_Reentrancy.selector); + bPool.getSpotPrice(tokens[0], tokens[1]); + } + + function test_GetSpotPriceRevertWhen_TokenInIsNotBound() external { + // it should revert + vm.expectRevert(IBPool.BPool_TokenNotBound.selector); + bPool.getSpotPrice(unknownToken, tokens[1]); + } + + function test_GetSpotPriceRevertWhen_TokenOutIsNotBound() external { + // it should revert + vm.expectRevert(IBPool.BPool_TokenNotBound.selector); + bPool.getSpotPrice(tokens[0], unknownToken); + } + + function test_GetSpotPriceWhenPreconditionsAreMet() external { + // it queries token in balance + vm.mockCall(tokens[0], abi.encodePacked(IERC20.balanceOf.selector), abi.encode(balanceTokenIn)); + vm.expectCall(tokens[0], abi.encodeWithSelector(IERC20.balanceOf.selector)); + // it queries token out balance + vm.mockCall(tokens[1], abi.encodePacked(IERC20.balanceOf.selector), abi.encode(balanceTokenOut)); + vm.expectCall(tokens[1], abi.encodeWithSelector(IERC20.balanceOf.selector)); + // it returns spot price + assertEq(bPool.getSpotPrice(tokens[0], tokens[1]), spotPrice); + } + + function test_GetSpotPriceSansFeeRevertWhen_ReentrancyLockIsSet() external { + bPool.call__setLock(_MUTEX_TAKEN); + // it should revert + vm.expectRevert(IBPool.BPool_Reentrancy.selector); + bPool.getSpotPriceSansFee(tokens[0], tokens[1]); + } + + function test_GetSpotPriceSansFeeRevertWhen_TokenInIsNotBound() external { + // it should revert + vm.expectRevert(IBPool.BPool_TokenNotBound.selector); + bPool.getSpotPriceSansFee(unknownToken, tokens[1]); + } + + function test_GetSpotPriceSansFeeRevertWhen_TokenOutIsNotBound() external { + // it should revert + vm.expectRevert(IBPool.BPool_TokenNotBound.selector); + bPool.getSpotPriceSansFee(tokens[0], unknownToken); + } + + function test_GetSpotPriceSansFeeWhenPreconditionsAreMet() external { + // it queries token in balance + vm.mockCall(tokens[0], abi.encodePacked(IERC20.balanceOf.selector), abi.encode(balanceTokenIn)); + vm.expectCall(tokens[0], abi.encodeWithSelector(IERC20.balanceOf.selector)); + // it queries token out balance + vm.mockCall(tokens[1], abi.encodePacked(IERC20.balanceOf.selector), abi.encode(balanceTokenOut)); + vm.expectCall(tokens[1], abi.encodeWithSelector(IERC20.balanceOf.selector)); + // it returns spot price + assertEq(bPool.getSpotPriceSansFee(tokens[0], tokens[1]), spotPriceWithoutFee); + } + function test_FinalizeRevertWhen_CallerIsNotController(address _caller) external { vm.assume(_caller != address(this)); - vm.prank(_caller); + vm.startPrank(_caller); // it should revert vm.expectRevert(IBPool.BPool_CallerIsNotController.selector); bPool.finalize(); } function test_FinalizeRevertWhen_PoolIsFinalized() external { - bPool.set__finalized(true); + vm.startPrank(controller); // it should revert vm.expectRevert(IBPool.BPool_PoolIsFinalized.selector); bPool.finalize(); } function test_FinalizeRevertWhen_ThereAreTooFewTokensBound() external { + vm.startPrank(controller); + bPool.set__finalized(false); address[] memory tokens_ = new address[](1); tokens_[0] = tokens[0]; bPool.set__tokens(tokens_); @@ -78,22 +396,24 @@ contract BPool is BPoolBase { } function test_FinalizeWhenPreconditionsAreMet() external { + vm.startPrank(controller); + bPool.set__finalized(false); bPool.set__tokens(tokens); bPool.set__records(tokens[0], IBPool.Record({bound: true, index: 0, denorm: tokenWeight})); bPool.set__records(tokens[1], IBPool.Record({bound: true, index: 1, denorm: tokenWeight})); bPool.mock_call__mintPoolShare(INIT_POOL_SUPPLY); - bPool.mock_call__pushPoolShare(address(this), INIT_POOL_SUPPLY); + bPool.mock_call__pushPoolShare(controller, INIT_POOL_SUPPLY); // it calls _afterFinalize hook bPool.expectCall__afterFinalize(); // it mints initial pool shares bPool.expectCall__mintPoolShare(INIT_POOL_SUPPLY); // it sends initial pool shares to controller - bPool.expectCall__pushPoolShare(address(this), INIT_POOL_SUPPLY); + bPool.expectCall__pushPoolShare(controller, INIT_POOL_SUPPLY); // it emits a LOG_CALL event bytes memory data = abi.encodeCall(IBPool.finalize, ()); vm.expectEmit(address(bPool)); - emit IBPool.LOG_CALL(IBPool.finalize.selector, address(this), data); + emit IBPool.LOG_CALL(IBPool.finalize.selector, controller, data); bPool.finalize(); // it finalizes the pool diff --git a/test/unit/BPool/BPool.tree b/test/unit/BPool/BPool.tree index 1f8f368e..7a034e3a 100644 --- a/test/unit/BPool/BPool.tree +++ b/test/unit/BPool/BPool.tree @@ -5,6 +5,32 @@ BPool::constructor ├── it sets swap fee to MIN_FEE └── it does NOT finalize the pool +BPool::setSwapFee +├── when reentrancy lock is set +│ └── it should revert +├── when caller is not controller +│ └── it should revert +├── when pool is finalized +│ └── it should revert +├── when swap fee is below MIN_FEE +│ └── it should revert +├── when swap fee is above MAX_FEE +│ └── it should revert +└── when preconditions are met + ├── it emits LOG_CALL event + └── it sets swap fee + +BPool::setController +├── when reentrancy lock is set +│ └── it should revert +├── when caller is not controller +│ └── it should revert +├── when new controller is zero address +│ └── it should revert +└── when preconditions are met + ├── it emits LOG_CALL event + └── it sets new controller + BPool::isFinalized ├── when pool is finalized │ └── it returns true @@ -21,6 +47,87 @@ BPool::getNumTokens └── when called └── it returns number of tokens +BPool::getFinalTokens +├── when reentrancy lock is set +│ └── it should revert +├── when pool is not finalized +│ └── it should revert +└── when preconditions are met + └── it returns pool tokens + +BPool::getCurrentTokens +├── when reentrancy lock is set +│ └── it should revert +└── when preconditions are met + └── it returns pool tokens + +BPool::getDenormalizedWeight +├── when reentrancy lock is set +│ └── it should revert +├── when token is not bound +│ └── it should revert +└── when preconditions are met + └── it returns token weight + +BPool::getTotalDenormalizedWeight +├── when reentrancy lock is set +│ └── it should revert +└── when preconditions are met + └── it returns total weight + +BPool::getNormalizedWeight +├── when reentrancy lock is set +│ └── it should revert +├── when token is not bound +│ └── it should revert +└── when preconditions are met + └── it returns normalized weight + +BPool::getBalance +├── when reentrancy lock is set +│ └── it should revert +├── when token is not bound +│ └── it should revert +└── when preconditions are met + ├── it queries token balance + └── it returns token balance + +BPool::getSwapFee +├── when reentrancy lock is set +│ └── it should revert +└── when preconditions are met + └── it returns swap fee + +BPool::getController +├── when reentrancy lock is set +│ └── it should revert +└── when preconditions are met + └── it returns controller + +BPool::getSpotPrice +├── when reentrancy lock is set +│ └── it should revert +├── when token in is not bound +│ └── it should revert +├── when token out is not bound +│ └── it should revert +└── when preconditions are met + ├── it queries token in balance + ├── it queries token out balance + └── it returns spot price + +BPool::getSpotPriceSansFee +├── when reentrancy lock is set +│ └── it should revert +├── when token in is not bound +│ └── it should revert +├── when token out is not bound +│ └── it should revert +└── when preconditions are met + ├── it queries token in balance + ├── it queries token out balance + └── it returns spot price sans fee + BPool::finalize ├── when caller is not controller │ └── it should revert diff --git a/test/unit/BToken.t.sol b/test/unit/BToken.t.sol index ac16f760..1207a294 100644 --- a/test/unit/BToken.t.sol +++ b/test/unit/BToken.t.sol @@ -1,154 +1,123 @@ -// SPDX-License-Identifier: GPL-3 -pragma solidity ^0.8.25; +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.25; +import {IERC20} from '@openzeppelin/contracts/interfaces/IERC20.sol'; import {IERC20Errors} from '@openzeppelin/contracts/interfaces/draft-IERC6093.sol'; import {Test} from 'forge-std/Test.sol'; import {MockBToken} from 'test/smock/MockBToken.sol'; -contract BToken_Unit_Constructor is Test { - function test_ConstructorParams() public { - MockBToken btoken = new MockBToken(); - assertEq(btoken.name(), 'Balancer Pool Token'); - assertEq(btoken.symbol(), 'BPT'); - assertEq(btoken.decimals(), 18); +contract BToken is Test { + MockBToken public bToken; + uint256 public initialApproval = 100e18; + uint256 public initialBalance = 100e18; + address public caller = makeAddr('caller'); + address public spender = makeAddr('spender'); + address public target = makeAddr('target'); + + function setUp() external { + bToken = new MockBToken(); + + vm.startPrank(caller); + // sets initial approval (cannot be mocked) + bToken.approve(spender, initialApproval); } -} -abstract contract BToken_Unit_base is Test { - MockBToken internal bToken; + function test_ConstructorWhenCalled() external { + MockBToken _bToken = new MockBToken(); + // it sets token name + assertEq(_bToken.name(), 'Balancer Pool Token'); + // it sets token symbol + assertEq(_bToken.symbol(), 'BPT'); + } - modifier assumeNonZeroAddresses(address addr1, address addr2) { - vm.assume(addr1 != address(0)); - vm.assume(addr2 != address(0)); - _; + function test_IncreaseApprovalRevertWhen_SenderIsAddressZero() external { + vm.startPrank(address(0)); + // it should revert + vm.expectRevert(abi.encodeWithSelector(IERC20Errors.ERC20InvalidApprover.selector, address(0))); + + bToken.increaseApproval(spender, 100e18); } - modifier assumeNonZeroAddress(address addr) { - vm.assume(addr != address(0)); - _; + function test_IncreaseApprovalRevertWhen_SpenderIsAddressZero() external { + // it should revert + vm.expectRevert(abi.encodeWithSelector(IERC20Errors.ERC20InvalidSpender.selector, address(0))); + bToken.increaseApproval(address(0), 100e18); } - function setUp() public virtual { - bToken = new MockBToken(); + function test_IncreaseApprovalWhenCalled() external { + // it emits Approval event + vm.expectEmit(); + emit IERC20.Approval(caller, spender, 200e18); + + bToken.increaseApproval(spender, 100e18); + // it increases spender approval + assertEq(bToken.allowance(caller, spender), 200e18); } -} -contract BToken_Unit_IncreaseApproval is BToken_Unit_base { - function test_increasesApprovalFromZero( - address sender, - address spender, - uint256 amount - ) public assumeNonZeroAddresses(sender, spender) { - vm.prank(sender); - bToken.increaseApproval(spender, amount); - assertEq(bToken.allowance(sender, spender), amount); + function test_DecreaseApprovalRevertWhen_SenderIsAddressZero() external { + vm.startPrank(address(0)); + // it should revert + vm.expectRevert(abi.encodeWithSelector(IERC20Errors.ERC20InvalidApprover.selector, address(0))); + bToken.decreaseApproval(spender, 50e18); } - function test_increasesApprovalFromNonZero( - address sender, - address spender, - uint128 existingAllowance, - uint128 amount - ) public assumeNonZeroAddresses(sender, spender) { - vm.assume(existingAllowance > 0); - vm.startPrank(sender); - bToken.approve(spender, existingAllowance); - bToken.increaseApproval(spender, amount); - vm.stopPrank(); - assertEq(bToken.allowance(sender, spender), uint256(amount) + existingAllowance); + function test_DecreaseApprovalRevertWhen_SpenderIsAddressZero() external { + // it should revert + vm.expectRevert(abi.encodeWithSelector(IERC20Errors.ERC20InvalidSpender.selector, address(0))); + bToken.decreaseApproval(address(0), 50e18); } -} -contract BToken_Unit_DecreaseApproval is BToken_Unit_base { - function test_decreaseApprovalToNonZero( - address sender, - address spender, - uint256 existingAllowance, - uint256 amount - ) public assumeNonZeroAddresses(sender, spender) { - existingAllowance = bound(existingAllowance, 1, type(uint256).max); - amount = bound(amount, 0, existingAllowance - 1); - vm.startPrank(sender); - bToken.approve(spender, existingAllowance); - bToken.decreaseApproval(spender, amount); - vm.stopPrank(); - assertEq(bToken.allowance(sender, spender), existingAllowance - amount); + function test_DecreaseApprovalWhenDecrementIsBiggerThanCurrentApproval() external { + bToken.decreaseApproval(spender, 200e18); + // it decreases spender approval to 0 + assertEq(bToken.allowance(caller, spender), 0); } - function test_decreaseApprovalToZero( - address sender, - address spender, - uint256 existingAllowance, - uint256 amount - ) public assumeNonZeroAddresses(sender, spender) { - amount = bound(amount, existingAllowance, type(uint256).max); - vm.startPrank(sender); - bToken.approve(spender, existingAllowance); - bToken.decreaseApproval(spender, amount); - vm.stopPrank(); - assertEq(bToken.allowance(sender, spender), 0); + function test_DecreaseApprovalWhenCalled() external { + // it emits Approval event + vm.expectEmit(); + emit IERC20.Approval(caller, spender, 50e18); + + bToken.decreaseApproval(spender, 50e18); + // it decreases spender approval + assertEq(bToken.allowance(caller, spender), 50e18); } -} -contract BToken_Unit__push is BToken_Unit_base { - function test_revertsOnInsufficientSelfBalance( - address to, - uint128 existingBalance, - uint128 offset - ) public assumeNonZeroAddress(to) { - vm.assume(offset > 1); - deal(address(bToken), address(bToken), existingBalance); - vm.expectRevert( - abi.encodeWithSelector( - IERC20Errors.ERC20InsufficientBalance.selector, - address(bToken), - existingBalance, - uint256(existingBalance) + offset - ) - ); - bToken.call__push(to, uint256(existingBalance) + offset); + function test__pushRevertWhen_ContractDoesNotHaveEnoughBalance() external { + // it should revert + vm.expectRevert(abi.encodeWithSelector(IERC20Errors.ERC20InsufficientBalance.selector, address(bToken), 0, 50e18)); + bToken.call__push(target, 50e18); } - function test_sendsTokens( - address to, - uint128 existingBalance, - uint256 transferAmount - ) public assumeNonZeroAddress(to) { - vm.assume(to != address(bToken)); - transferAmount = bound(transferAmount, 0, existingBalance); - deal(address(bToken), address(bToken), existingBalance); - bToken.call__push(to, transferAmount); - assertEq(bToken.balanceOf(to), transferAmount); - assertEq(bToken.balanceOf(address(bToken)), existingBalance - transferAmount); + function test__pushWhenCalled() external { + deal(address(bToken), address(bToken), initialBalance); + // it emits Transfer event + vm.expectEmit(); + emit IERC20.Transfer(address(bToken), target, 50e18); + + bToken.call__push(target, 50e18); + + // it transfers tokens to recipient + assertEq(bToken.balanceOf(address(bToken)), 50e18); + assertEq(bToken.balanceOf(target), 50e18); } -} -contract BToken_Unit__pull is BToken_Unit_base { - function test_revertsOnInsufficientFromBalance( - address from, - uint128 existingBalance, - uint128 offset - ) public assumeNonZeroAddress(from) { - vm.assume(offset > 1); - deal(address(bToken), from, existingBalance); - vm.expectRevert( - abi.encodeWithSelector( - IERC20Errors.ERC20InsufficientBalance.selector, from, existingBalance, uint256(existingBalance) + offset - ) - ); - bToken.call__pull(from, uint256(existingBalance) + offset); + function test__pullRevertWhen_TargetDoesNotHaveEnoughBalance() external { + // it should revert + vm.expectRevert(abi.encodeWithSelector(IERC20Errors.ERC20InsufficientBalance.selector, target, 0, 50e18)); + bToken.call__pull(target, 50e18); } - function test_getsTokens( - address from, - uint128 existingBalance, - uint256 transferAmount - ) public assumeNonZeroAddress(from) { - vm.assume(from != address(bToken)); - transferAmount = bound(transferAmount, 0, existingBalance); - deal(address(bToken), address(from), existingBalance); - bToken.call__pull(from, transferAmount); - assertEq(bToken.balanceOf(address(bToken)), transferAmount); - assertEq(bToken.balanceOf(from), existingBalance - transferAmount); + function test__pullWhenCalled() external { + deal(address(bToken), address(target), initialBalance); + // it emits Transfer event + vm.expectEmit(); + emit IERC20.Transfer(target, address(bToken), 50e18); + + bToken.call__pull(target, 50e18); + + // it transfers tokens from sender + assertEq(bToken.balanceOf(target), 50e18); + assertEq(bToken.balanceOf(address(bToken)), 50e18); } } diff --git a/test/unit/BToken.tree b/test/unit/BToken.tree new file mode 100644 index 00000000..35f85bec --- /dev/null +++ b/test/unit/BToken.tree @@ -0,0 +1,38 @@ +BToken::constructor +└── when called + ├── it sets token name + └── it sets token symbol + +BToken::increaseApproval +├── when sender is address zero +│ └── it should revert +├── when spender is address zero +│ └── it should revert +└── when called + ├── it emits Approval event + └── it increases spender approval + +BToken::decreaseApproval +├── when sender is address zero +│ └── it should revert +├── when spender is address zero +│ └── it should revert +├── when decrement is bigger than current approval +│ └── it decreases spender approval to 0 +└── when called + ├── it emits Approval event + └── it decreases spender approval + +BToken::_push +├── when contract does not have enough balance +│ └── it should revert +└── when called + ├── it emits Transfer event + └── it transfers tokens to recipient + +BToken::_pull +├── when target does not have enough balance +│ └── it should revert +└── when called + ├── it emits Transfer event + └── it transfers tokens from sender diff --git a/yarn.lock b/yarn.lock index 27b5429f..bba6419e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -603,6 +603,10 @@ cosmiconfig@^9.0.0: js-yaml "^4.1.0" parse-json "^5.2.0" +"cow-amm@github:cowprotocol/cow-amm.git#6566128": + version "0.0.0" + resolved "https://codeload.github.com/cowprotocol/cow-amm/tar.gz/6566128b6c73008062cf4a6d1957db602409b719" + cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"