From 60781a8118ca5b05f5588c6364b52f75ad4e8893 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Wed, 12 Jun 2024 13:27:12 +0200 Subject: [PATCH 01/32] feat: adding custom errors (#77) * feat: add custom errors * fix: wrong revert condition * test: update the tests with the custom errors * feat: add natspec to custom errors * test: rename test cases * fix: rename BFactory custom errors * fix: rename BPool custom errors * feat: add custom errors in BNum * fix: if branches on reverts * feat: update deployment gas snapshots * feat: update gas snapshots for swapExactAmountIn --------- Co-authored-by: 0xAustrian --- .forge-snapshots/newBFactory.snap | 2 +- .forge-snapshots/newBPool.snap | 2 +- .forge-snapshots/swapExactAmountIn.snap | 2 +- src/contracts/BFactory.sol | 12 +- src/contracts/BNum.sol | 71 +++++- src/contracts/BPool.sol | 300 ++++++++++++++++++------ src/interfaces/IBFactory.sol | 10 + src/interfaces/IBPool.sol | 150 ++++++++++++ test/unit/BFactory.t.sol | 6 +- test/unit/BPool.t.sol | 213 +++++++++-------- 10 files changed, 576 insertions(+), 192 deletions(-) diff --git a/.forge-snapshots/newBFactory.snap b/.forge-snapshots/newBFactory.snap index dd9f6517..fcf5d2a1 100644 --- a/.forge-snapshots/newBFactory.snap +++ b/.forge-snapshots/newBFactory.snap @@ -1 +1 @@ -3811528 \ No newline at end of file +3593368 \ No newline at end of file diff --git a/.forge-snapshots/newBPool.snap b/.forge-snapshots/newBPool.snap index 9d12a22f..9b3ae76a 100644 --- a/.forge-snapshots/newBPool.snap +++ b/.forge-snapshots/newBPool.snap @@ -1 +1 @@ -3567680 \ No newline at end of file +3371373 \ No newline at end of file diff --git a/.forge-snapshots/swapExactAmountIn.snap b/.forge-snapshots/swapExactAmountIn.snap index 85c66e53..3a563cd1 100644 --- a/.forge-snapshots/swapExactAmountIn.snap +++ b/.forge-snapshots/swapExactAmountIn.snap @@ -1 +1 @@ -107618 \ No newline at end of file +108434 \ No newline at end of file diff --git a/src/contracts/BFactory.sol b/src/contracts/BFactory.sol index 8929c8c6..3818e159 100644 --- a/src/contracts/BFactory.sol +++ b/src/contracts/BFactory.sol @@ -30,17 +30,23 @@ contract BFactory is IBFactory { /// @inheritdoc IBFactory function setBLabs(address b) external { - require(msg.sender == _blabs, 'ERR_NOT_BLABS'); + if (msg.sender != _blabs) { + revert BFactory_NotBLabs(); + } emit LOG_BLABS(msg.sender, b); _blabs = b; } /// @inheritdoc IBFactory function collect(IBPool pool) external { - require(msg.sender == _blabs, 'ERR_NOT_BLABS'); + if (msg.sender != _blabs) { + revert BFactory_NotBLabs(); + } uint256 collected = pool.balanceOf(address(this)); bool xfer = pool.transfer(_blabs, collected); - require(xfer, 'ERR_ERC20_FAILED'); + if (!xfer) { + revert BFactory_ERC20TransferFailed(); + } } /// @inheritdoc IBFactory diff --git a/src/contracts/BNum.sol b/src/contracts/BNum.sol index 040ac892..c75da935 100644 --- a/src/contracts/BNum.sol +++ b/src/contracts/BNum.sol @@ -6,6 +6,41 @@ import {BConst} from './BConst.sol'; // solhint-disable private-vars-leading-underscore // solhint-disable named-return-values contract BNum is BConst { + /** + * @notice Thrown when an overflow is encountered inside the add function + */ + error BNum_AddOverflow(); + + /** + * @notice Thrown when an underflow is encountered inside the sub function + */ + error BNum_SubUnderflow(); + + /** + * @notice Thrown when an overflow is encountered inside the mul function + */ + error BNum_MulOverflow(); + + /** + * @notice Thrown when attempting to divide by zero + */ + error BNum_DivZero(); + + /** + * @notice Thrown when an internal error occurs inside div function + */ + error BNum_DivInternal(); + + /** + * @notice Thrown when the base is too low in the bpow function + */ + error BNum_BPowBaseTooLow(); + + /** + * @notice Thrown when the base is too high in the bpow function + */ + error BNum_BPowBaseTooHigh(); + function btoi(uint256 a) internal pure returns (uint256) { unchecked { return a / BONE; @@ -21,7 +56,9 @@ contract BNum is BConst { function badd(uint256 a, uint256 b) internal pure returns (uint256) { unchecked { uint256 c = a + b; - require(c >= a, 'ERR_ADD_OVERFLOW'); + if (c < a) { + revert BNum_AddOverflow(); + } return c; } } @@ -29,7 +66,9 @@ contract BNum is BConst { function bsub(uint256 a, uint256 b) internal pure returns (uint256) { unchecked { (uint256 c, bool flag) = bsubSign(a, b); - require(!flag, 'ERR_SUB_UNDERFLOW'); + if (flag) { + revert BNum_SubUnderflow(); + } return c; } } @@ -47,9 +86,13 @@ contract BNum is BConst { function bmul(uint256 a, uint256 b) internal pure returns (uint256) { unchecked { uint256 c0 = a * b; - require(a == 0 || c0 / a == b, 'ERR_MUL_OVERFLOW'); + if (a != 0 && c0 / a != b) { + revert BNum_MulOverflow(); + } uint256 c1 = c0 + (BONE / 2); - require(c1 >= c0, 'ERR_MUL_OVERFLOW'); + if (c1 < c0) { + revert BNum_MulOverflow(); + } uint256 c2 = c1 / BONE; return c2; } @@ -57,11 +100,17 @@ contract BNum is BConst { function bdiv(uint256 a, uint256 b) internal pure returns (uint256) { unchecked { - require(b != 0, 'ERR_DIV_ZERO'); + if (b == 0) { + revert BNum_DivZero(); + } uint256 c0 = a * BONE; - require(a == 0 || c0 / a == BONE, 'ERR_DIV_INTERNAL'); // bmul overflow + if (a != 0 && c0 / a != BONE) { + revert BNum_DivInternal(); // bmul overflow + } uint256 c1 = c0 + (b / 2); - require(c1 >= c0, 'ERR_DIV_INTERNAL'); // badd require + if (c1 < c0) { + revert BNum_DivInternal(); // badd require + } uint256 c2 = c1 / b; return c2; } @@ -88,8 +137,12 @@ contract BNum is BConst { // of approximation of b^0.w function bpow(uint256 base, uint256 exp) internal pure returns (uint256) { unchecked { - require(base >= MIN_BPOW_BASE, 'ERR_BPOW_BASE_TOO_LOW'); - require(base <= MAX_BPOW_BASE, 'ERR_BPOW_BASE_TOO_HIGH'); + if (base < MIN_BPOW_BASE) { + revert BNum_BPowBaseTooLow(); + } + if (base > MAX_BPOW_BASE) { + revert BNum_BPowBaseTooHigh(); + } uint256 whole = bfloor(exp); uint256 remain = bsub(exp, whole); diff --git a/src/contracts/BPool.sol b/src/contracts/BPool.sol index 62586ba0..1fde5ea3 100644 --- a/src/contracts/BPool.sol +++ b/src/contracts/BPool.sol @@ -36,7 +36,9 @@ contract BPool is BToken, BMath, IBPool { /// @dev Prevents reentrancy in non-view functions modifier _lock_() { - require(!_mutex, 'ERR_REENTRY'); + if (_mutex) { + revert BPool_Reentrancy(); + } _mutex = true; _; _mutex = false; @@ -44,7 +46,9 @@ contract BPool is BToken, BMath, IBPool { /// @dev Prevents reentrancy in view functions modifier _viewlock_() { - require(!_mutex, 'ERR_REENTRY'); + if (_mutex) { + revert BPool_Reentrancy(); + } _; } @@ -57,24 +61,40 @@ contract BPool is BToken, BMath, IBPool { /// @inheritdoc IBPool function setSwapFee(uint256 swapFee) external _logs_ _lock_ { - require(!_finalized, 'ERR_IS_FINALIZED'); - require(msg.sender == _controller, 'ERR_NOT_CONTROLLER'); - require(swapFee >= MIN_FEE, 'ERR_MIN_FEE'); - require(swapFee <= MAX_FEE, 'ERR_MAX_FEE'); + if (_finalized) { + revert BPool_PoolIsFinalized(); + } + if (msg.sender != _controller) { + revert BPool_CallerIsNotController(); + } + if (swapFee < MIN_FEE) { + revert BPool_FeeBelowMinimum(); + } + if (swapFee > MAX_FEE) { + revert BPool_FeeAboveMaximum(); + } _swapFee = swapFee; } /// @inheritdoc IBPool function setController(address manager) external _logs_ _lock_ { - require(msg.sender == _controller, 'ERR_NOT_CONTROLLER'); + if (msg.sender != _controller) { + revert BPool_CallerIsNotController(); + } _controller = manager; } /// @inheritdoc IBPool function finalize() external _logs_ _lock_ { - require(msg.sender == _controller, 'ERR_NOT_CONTROLLER'); - require(!_finalized, 'ERR_IS_FINALIZED'); - require(_tokens.length >= MIN_BOUND_TOKENS, 'ERR_MIN_TOKENS'); + if (msg.sender != _controller) { + revert BPool_CallerIsNotController(); + } + if (_finalized) { + revert BPool_PoolIsFinalized(); + } + if (_tokens.length < MIN_BOUND_TOKENS) { + revert BPool_TokensBelowMinimum(); + } _finalized = true; @@ -84,18 +104,34 @@ contract BPool is BToken, BMath, IBPool { /// @inheritdoc IBPool function bind(address token, uint256 balance, uint256 denorm) external _logs_ _lock_ { - require(msg.sender == _controller, 'ERR_NOT_CONTROLLER'); - require(!_records[token].bound, 'ERR_IS_BOUND'); - require(!_finalized, 'ERR_IS_FINALIZED'); + if (msg.sender != _controller) { + revert BPool_CallerIsNotController(); + } + if (_records[token].bound) { + revert BPool_TokenAlreadyBound(); + } + if (_finalized) { + revert BPool_PoolIsFinalized(); + } - require(_tokens.length < MAX_BOUND_TOKENS, 'ERR_MAX_TOKENS'); + if (_tokens.length >= MAX_BOUND_TOKENS) { + revert BPool_TokensAboveMaximum(); + } - require(denorm >= MIN_WEIGHT, 'ERR_MIN_WEIGHT'); - require(denorm <= MAX_WEIGHT, 'ERR_MAX_WEIGHT'); - require(balance >= MIN_BALANCE, 'ERR_MIN_BALANCE'); + if (denorm < MIN_WEIGHT) { + revert BPool_WeightBelowMinimum(); + } + if (denorm > MAX_WEIGHT) { + revert BPool_WeightAboveMaximum(); + } + if (balance < MIN_BALANCE) { + revert BPool_BalanceBelowMinimum(); + } _totalWeight = badd(_totalWeight, denorm); - require(_totalWeight <= MAX_TOTAL_WEIGHT, 'ERR_MAX_TOTAL_WEIGHT'); + if (_totalWeight > MAX_TOTAL_WEIGHT) { + revert BPool_TotalWeightAboveMaximum(); + } _records[token] = Record({bound: true, index: _tokens.length, denorm: denorm}); _tokens.push(token); @@ -105,9 +141,15 @@ contract BPool is BToken, BMath, IBPool { /// @inheritdoc IBPool function unbind(address token) external _logs_ _lock_ { - require(msg.sender == _controller, 'ERR_NOT_CONTROLLER'); - require(_records[token].bound, 'ERR_NOT_BOUND'); - require(!_finalized, 'ERR_IS_FINALIZED'); + if (msg.sender != _controller) { + revert BPool_CallerIsNotController(); + } + if (!_records[token].bound) { + revert BPool_TokenNotBound(); + } + if (_finalized) { + revert BPool_PoolIsFinalized(); + } _totalWeight = bsub(_totalWeight, _records[token].denorm); @@ -125,18 +167,26 @@ contract BPool is BToken, BMath, IBPool { /// @inheritdoc IBPool function joinPool(uint256 poolAmountOut, uint256[] calldata maxAmountsIn) external _logs_ _lock_ { - require(_finalized, 'ERR_NOT_FINALIZED'); + if (!_finalized) { + revert BPool_PoolNotFinalized(); + } uint256 poolTotal = totalSupply(); uint256 ratio = bdiv(poolAmountOut, poolTotal); - require(ratio != 0, 'ERR_MATH_APPROX'); + if (ratio == 0) { + revert BPool_InvalidPoolRatio(); + } for (uint256 i = 0; i < _tokens.length; i++) { address t = _tokens[i]; uint256 bal = IERC20(t).balanceOf(address(this)); uint256 tokenAmountIn = bmul(ratio, bal); - require(tokenAmountIn != 0, 'ERR_MATH_APPROX'); - require(tokenAmountIn <= maxAmountsIn[i], 'ERR_LIMIT_IN'); + if (tokenAmountIn == 0) { + revert BPool_InvalidTokenAmountIn(); + } + if (tokenAmountIn > maxAmountsIn[i]) { + revert BPool_TokenAmountInAboveMaxAmountIn(); + } emit LOG_JOIN(msg.sender, t, tokenAmountIn); _pullUnderlying(t, msg.sender, tokenAmountIn); } @@ -146,13 +196,17 @@ contract BPool is BToken, BMath, IBPool { /// @inheritdoc IBPool function exitPool(uint256 poolAmountIn, uint256[] calldata minAmountsOut) external _logs_ _lock_ { - require(_finalized, 'ERR_NOT_FINALIZED'); + if (!_finalized) { + revert BPool_PoolNotFinalized(); + } uint256 poolTotal = totalSupply(); uint256 exitFee = bmul(poolAmountIn, EXIT_FEE); uint256 pAiAfterExitFee = bsub(poolAmountIn, exitFee); uint256 ratio = bdiv(pAiAfterExitFee, poolTotal); - require(ratio != 0, 'ERR_MATH_APPROX'); + if (ratio == 0) { + revert BPool_InvalidPoolRatio(); + } _pullPoolShare(msg.sender, poolAmountIn); _pushPoolShare(_factory, exitFee); @@ -162,8 +216,12 @@ contract BPool is BToken, BMath, IBPool { address t = _tokens[i]; uint256 bal = IERC20(t).balanceOf(address(this)); uint256 tokenAmountOut = bmul(ratio, bal); - require(tokenAmountOut != 0, 'ERR_MATH_APPROX'); - require(tokenAmountOut >= minAmountsOut[i], 'ERR_LIMIT_OUT'); + if (tokenAmountOut == 0) { + revert BPool_InvalidTokenAmountOut(); + } + if (tokenAmountOut < minAmountsOut[i]) { + revert BPool_TokenAmountOutBelowMinAmountOut(); + } emit LOG_EXIT(msg.sender, t, tokenAmountOut); _pushUnderlying(t, msg.sender, tokenAmountOut); } @@ -177,9 +235,15 @@ contract BPool is BToken, BMath, IBPool { uint256 minAmountOut, uint256 maxPrice ) external _logs_ _lock_ returns (uint256 tokenAmountOut, uint256 spotPriceAfter) { - require(_records[tokenIn].bound, 'ERR_NOT_BOUND'); - require(_records[tokenOut].bound, 'ERR_NOT_BOUND'); - require(_finalized, 'ERR_NOT_FINALIZED'); + if (!_records[tokenIn].bound) { + revert BPool_TokenNotBound(); + } + if (!_records[tokenOut].bound) { + revert BPool_TokenNotBound(); + } + if (!_finalized) { + revert BPool_PoolNotFinalized(); + } Record storage inRecord = _records[address(tokenIn)]; Record storage outRecord = _records[address(tokenOut)]; @@ -187,23 +251,35 @@ contract BPool is BToken, BMath, IBPool { uint256 tokenInBalance = IERC20(tokenIn).balanceOf(address(this)); uint256 tokenOutBalance = IERC20(tokenOut).balanceOf(address(this)); - require(tokenAmountIn <= bmul(tokenInBalance, MAX_IN_RATIO), 'ERR_MAX_IN_RATIO'); + if (tokenAmountIn > bmul(tokenInBalance, MAX_IN_RATIO)) { + revert BPool_TokenAmountInAboveMaxIn(); + } uint256 spotPriceBefore = calcSpotPrice(tokenInBalance, inRecord.denorm, tokenOutBalance, outRecord.denorm, _swapFee); - require(spotPriceBefore <= maxPrice, 'ERR_BAD_LIMIT_PRICE'); + if (spotPriceBefore > maxPrice) { + revert BPool_SpotPriceAboveMaxPrice(); + } tokenAmountOut = calcOutGivenIn(tokenInBalance, inRecord.denorm, tokenOutBalance, outRecord.denorm, tokenAmountIn, _swapFee); - require(tokenAmountOut >= minAmountOut, 'ERR_LIMIT_OUT'); + if (tokenAmountOut < minAmountOut) { + revert BPool_TokenAmountOutBelowMinOut(); + } tokenInBalance = badd(tokenInBalance, tokenAmountIn); tokenOutBalance = bsub(tokenOutBalance, tokenAmountOut); spotPriceAfter = calcSpotPrice(tokenInBalance, inRecord.denorm, tokenOutBalance, outRecord.denorm, _swapFee); - require(spotPriceAfter >= spotPriceBefore, 'ERR_MATH_APPROX'); - require(spotPriceAfter <= maxPrice, 'ERR_LIMIT_PRICE'); - require(spotPriceBefore <= bdiv(tokenAmountIn, tokenAmountOut), 'ERR_MATH_APPROX'); + if (spotPriceAfter < spotPriceBefore) { + revert BPool_SpotPriceAfterBelowSpotPriceBefore(); + } + if (spotPriceAfter > maxPrice) { + revert BPool_SpotPriceAfterBelowMaxPrice(); + } + if (spotPriceBefore > bdiv(tokenAmountIn, tokenAmountOut)) { + revert BPool_SpotPriceBeforeAboveTokenRatio(); + } emit LOG_SWAP(msg.sender, tokenIn, tokenOut, tokenAmountIn, tokenAmountOut); @@ -221,9 +297,15 @@ contract BPool is BToken, BMath, IBPool { uint256 tokenAmountOut, uint256 maxPrice ) external _logs_ _lock_ returns (uint256 tokenAmountIn, uint256 spotPriceAfter) { - require(_records[tokenIn].bound, 'ERR_NOT_BOUND'); - require(_records[tokenOut].bound, 'ERR_NOT_BOUND'); - require(_finalized, 'ERR_NOT_FINALIZED'); + if (!_records[tokenIn].bound) { + revert BPool_TokenNotBound(); + } + if (!_records[tokenOut].bound) { + revert BPool_TokenNotBound(); + } + if (!_finalized) { + revert BPool_PoolNotFinalized(); + } Record storage inRecord = _records[address(tokenIn)]; Record storage outRecord = _records[address(tokenOut)]; @@ -231,23 +313,35 @@ contract BPool is BToken, BMath, IBPool { uint256 tokenInBalance = IERC20(tokenIn).balanceOf(address(this)); uint256 tokenOutBalance = IERC20(tokenOut).balanceOf(address(this)); - require(tokenAmountOut <= bmul(tokenOutBalance, MAX_OUT_RATIO), 'ERR_MAX_OUT_RATIO'); + if (tokenAmountOut > bmul(tokenOutBalance, MAX_OUT_RATIO)) { + revert BPool_TokenAmountOutAboveMaxOut(); + } uint256 spotPriceBefore = calcSpotPrice(tokenInBalance, inRecord.denorm, tokenOutBalance, outRecord.denorm, _swapFee); - require(spotPriceBefore <= maxPrice, 'ERR_BAD_LIMIT_PRICE'); + if (spotPriceBefore > maxPrice) { + revert BPool_SpotPriceAboveMaxPrice(); + } tokenAmountIn = calcInGivenOut(tokenInBalance, inRecord.denorm, tokenOutBalance, outRecord.denorm, tokenAmountOut, _swapFee); - require(tokenAmountIn <= maxAmountIn, 'ERR_LIMIT_IN'); + if (tokenAmountIn > maxAmountIn) { + revert BPool_TokenAmountInAboveMaxAmountIn(); + } tokenInBalance = badd(tokenInBalance, tokenAmountIn); tokenOutBalance = bsub(tokenOutBalance, tokenAmountOut); spotPriceAfter = calcSpotPrice(tokenInBalance, inRecord.denorm, tokenOutBalance, outRecord.denorm, _swapFee); - require(spotPriceAfter >= spotPriceBefore, 'ERR_MATH_APPROX'); - require(spotPriceAfter <= maxPrice, 'ERR_LIMIT_PRICE'); - require(spotPriceBefore <= bdiv(tokenAmountIn, tokenAmountOut), 'ERR_MATH_APPROX'); + if (spotPriceAfter < spotPriceBefore) { + revert BPool_SpotPriceAfterBelowSpotPriceBefore(); + } + if (spotPriceAfter > maxPrice) { + revert BPool_SpotPriceAfterBelowMaxPrice(); + } + if (spotPriceBefore > bdiv(tokenAmountIn, tokenAmountOut)) { + revert BPool_SpotPriceBeforeAboveTokenRatio(); + } emit LOG_SWAP(msg.sender, tokenIn, tokenOut, tokenAmountIn, tokenAmountOut); @@ -263,16 +357,24 @@ contract BPool is BToken, BMath, IBPool { uint256 tokenAmountIn, uint256 minPoolAmountOut ) external _logs_ _lock_ returns (uint256 poolAmountOut) { - require(_finalized, 'ERR_NOT_FINALIZED'); - require(_records[tokenIn].bound, 'ERR_NOT_BOUND'); + if (!_finalized) { + revert BPool_PoolNotFinalized(); + } + if (!_records[tokenIn].bound) { + revert BPool_TokenNotBound(); + } Record storage inRecord = _records[tokenIn]; uint256 tokenInBalance = IERC20(tokenIn).balanceOf(address(this)); - require(tokenAmountIn <= bmul(tokenInBalance, MAX_IN_RATIO), 'ERR_MAX_IN_RATIO'); + if (tokenAmountIn > bmul(tokenInBalance, MAX_IN_RATIO)) { + revert BPool_TokenAmountInAboveMaxIn(); + } poolAmountOut = calcPoolOutGivenSingleIn(tokenInBalance, inRecord.denorm, totalSupply(), _totalWeight, tokenAmountIn, _swapFee); - require(poolAmountOut >= minPoolAmountOut, 'ERR_LIMIT_OUT'); + if (poolAmountOut < minPoolAmountOut) { + revert BPool_PoolAmountOutBelowMinPoolAmountOut(); + } emit LOG_JOIN(msg.sender, tokenIn, tokenAmountIn); @@ -289,8 +391,12 @@ contract BPool is BToken, BMath, IBPool { uint256 poolAmountOut, uint256 maxAmountIn ) external _logs_ _lock_ returns (uint256 tokenAmountIn) { - require(_finalized, 'ERR_NOT_FINALIZED'); - require(_records[tokenIn].bound, 'ERR_NOT_BOUND'); + if (!_finalized) { + revert BPool_PoolNotFinalized(); + } + if (!_records[tokenIn].bound) { + revert BPool_TokenNotBound(); + } Record storage inRecord = _records[tokenIn]; uint256 tokenInBalance = IERC20(tokenIn).balanceOf(address(this)); @@ -298,9 +404,15 @@ contract BPool is BToken, BMath, IBPool { tokenAmountIn = calcSingleInGivenPoolOut(tokenInBalance, inRecord.denorm, totalSupply(), _totalWeight, poolAmountOut, _swapFee); - require(tokenAmountIn != 0, 'ERR_MATH_APPROX'); - require(tokenAmountIn <= maxAmountIn, 'ERR_LIMIT_IN'); - require(tokenAmountIn <= bmul(tokenInBalance, MAX_IN_RATIO), 'ERR_MAX_IN_RATIO'); + if (tokenAmountIn == 0) { + revert BPool_InvalidTokenAmountIn(); + } + if (tokenAmountIn > maxAmountIn) { + revert BPool_TokenAmountInAboveMaxAmountIn(); + } + if (tokenAmountIn > bmul(tokenInBalance, MAX_IN_RATIO)) { + revert BPool_TokenAmountInAboveMaxIn(); + } emit LOG_JOIN(msg.sender, tokenIn, tokenAmountIn); @@ -317,8 +429,12 @@ contract BPool is BToken, BMath, IBPool { uint256 poolAmountIn, uint256 minAmountOut ) external _logs_ _lock_ returns (uint256 tokenAmountOut) { - require(_finalized, 'ERR_NOT_FINALIZED'); - require(_records[tokenOut].bound, 'ERR_NOT_BOUND'); + if (!_finalized) { + revert BPool_PoolNotFinalized(); + } + if (!_records[tokenOut].bound) { + revert BPool_TokenNotBound(); + } Record storage outRecord = _records[tokenOut]; uint256 tokenOutBalance = IERC20(tokenOut).balanceOf(address(this)); @@ -326,8 +442,12 @@ contract BPool is BToken, BMath, IBPool { tokenAmountOut = calcSingleOutGivenPoolIn(tokenOutBalance, outRecord.denorm, totalSupply(), _totalWeight, poolAmountIn, _swapFee); - require(tokenAmountOut >= minAmountOut, 'ERR_LIMIT_OUT'); - require(tokenAmountOut <= bmul(tokenOutBalance, MAX_OUT_RATIO), 'ERR_MAX_OUT_RATIO'); + if (tokenAmountOut < minAmountOut) { + revert BPool_TokenAmountOutBelowMinAmountOut(); + } + if (tokenAmountOut > bmul(tokenOutBalance, MAX_OUT_RATIO)) { + revert BPool_TokenAmountOutAboveMaxOut(); + } uint256 exitFee = bmul(poolAmountIn, EXIT_FEE); @@ -347,17 +467,27 @@ contract BPool is BToken, BMath, IBPool { uint256 tokenAmountOut, uint256 maxPoolAmountIn ) external _logs_ _lock_ returns (uint256 poolAmountIn) { - require(_finalized, 'ERR_NOT_FINALIZED'); - require(_records[tokenOut].bound, 'ERR_NOT_BOUND'); + if (!_finalized) { + revert BPool_PoolNotFinalized(); + } + if (!_records[tokenOut].bound) { + revert BPool_TokenNotBound(); + } Record storage outRecord = _records[tokenOut]; uint256 tokenOutBalance = IERC20(tokenOut).balanceOf(address(this)); - require(tokenAmountOut <= bmul(tokenOutBalance, MAX_OUT_RATIO), 'ERR_MAX_OUT_RATIO'); + if (tokenAmountOut > bmul(tokenOutBalance, MAX_OUT_RATIO)) { + revert BPool_TokenAmountOutAboveMaxOut(); + } poolAmountIn = calcPoolInGivenSingleOut(tokenOutBalance, outRecord.denorm, totalSupply(), _totalWeight, tokenAmountOut, _swapFee); - require(poolAmountIn != 0, 'ERR_MATH_APPROX'); - require(poolAmountIn <= maxPoolAmountIn, 'ERR_LIMIT_IN'); + if (poolAmountIn == 0) { + revert BPool_InvalidPoolAmountIn(); + } + if (poolAmountIn > maxPoolAmountIn) { + revert BPool_PoolAmountInAboveMaxPoolAmountIn(); + } uint256 exitFee = bmul(poolAmountIn, EXIT_FEE); @@ -373,8 +503,12 @@ contract BPool is BToken, BMath, IBPool { /// @inheritdoc IBPool function getSpotPrice(address tokenIn, address tokenOut) external view _viewlock_ returns (uint256 spotPrice) { - require(_records[tokenIn].bound, 'ERR_NOT_BOUND'); - require(_records[tokenOut].bound, 'ERR_NOT_BOUND'); + if (!_records[tokenIn].bound) { + revert BPool_TokenNotBound(); + } + if (!_records[tokenOut].bound) { + revert BPool_TokenNotBound(); + } Record storage inRecord = _records[tokenIn]; Record storage outRecord = _records[tokenOut]; return calcSpotPrice( @@ -388,8 +522,12 @@ contract BPool is BToken, BMath, IBPool { /// @inheritdoc IBPool function getSpotPriceSansFee(address tokenIn, address tokenOut) external view _viewlock_ returns (uint256 spotPrice) { - require(_records[tokenIn].bound, 'ERR_NOT_BOUND'); - require(_records[tokenOut].bound, 'ERR_NOT_BOUND'); + if (!_records[tokenIn].bound) { + revert BPool_TokenNotBound(); + } + if (!_records[tokenOut].bound) { + revert BPool_TokenNotBound(); + } Record storage inRecord = _records[tokenIn]; Record storage outRecord = _records[tokenOut]; return calcSpotPrice( @@ -423,13 +561,17 @@ contract BPool is BToken, BMath, IBPool { /// @inheritdoc IBPool function getFinalTokens() external view _viewlock_ returns (address[] memory tokens) { - require(_finalized, 'ERR_NOT_FINALIZED'); + if (!_finalized) { + revert BPool_PoolNotFinalized(); + } return _tokens; } /// @inheritdoc IBPool function getDenormalizedWeight(address token) external view _viewlock_ returns (uint256) { - require(_records[token].bound, 'ERR_NOT_BOUND'); + if (!_records[token].bound) { + revert BPool_TokenNotBound(); + } return _records[token].denorm; } @@ -440,14 +582,18 @@ contract BPool is BToken, BMath, IBPool { /// @inheritdoc IBPool function getNormalizedWeight(address token) external view _viewlock_ returns (uint256) { - require(_records[token].bound, 'ERR_NOT_BOUND'); + if (!_records[token].bound) { + revert BPool_TokenNotBound(); + } uint256 denorm = _records[token].denorm; return bdiv(denorm, _totalWeight); } /// @inheritdoc IBPool function getBalance(address token) external view _viewlock_ returns (uint256) { - require(_records[token].bound, 'ERR_NOT_BOUND'); + if (!_records[token].bound) { + revert BPool_TokenNotBound(); + } return IERC20(token).balanceOf(address(this)); } @@ -469,7 +615,9 @@ contract BPool is BToken, BMath, IBPool { */ function _pullUnderlying(address erc20, address from, uint256 amount) internal virtual { bool xfer = IERC20(erc20).transferFrom(from, address(this), amount); - require(xfer, 'ERR_ERC20_FALSE'); + if (!xfer) { + revert BPool_ERC20TransferFailed(); + } } /** @@ -480,7 +628,9 @@ contract BPool is BToken, BMath, IBPool { */ function _pushUnderlying(address erc20, address to, uint256 amount) internal virtual { bool xfer = IERC20(erc20).transfer(to, amount); - require(xfer, 'ERR_ERC20_FALSE'); + if (!xfer) { + revert BPool_ERC20TransferFailed(); + } } /** diff --git a/src/interfaces/IBFactory.sol b/src/interfaces/IBFactory.sol index dc0999dd..c40cf9ca 100644 --- a/src/interfaces/IBFactory.sol +++ b/src/interfaces/IBFactory.sol @@ -18,6 +18,16 @@ interface IBFactory { */ event LOG_BLABS(address indexed caller, address indexed bLabs); + /** + * @notice Thrown when caller is not BLabs address + */ + error BFactory_NotBLabs(); + + /** + * @notice Thrown when the ERC20 transfer fails + */ + error BFactory_ERC20TransferFailed(); + /** * @notice Creates a new BPool, assigning the caller as the pool controller * @return pool The new BPool diff --git a/src/interfaces/IBPool.sol b/src/interfaces/IBPool.sol index c02f1a98..c2f0f04c 100644 --- a/src/interfaces/IBPool.sol +++ b/src/interfaces/IBPool.sol @@ -56,6 +56,156 @@ interface IBPool is IERC20 { */ event LOG_CALL(bytes4 indexed sig, address indexed caller, bytes data) anonymous; + /** + * @notice Thrown when a reentrant call is made + */ + error BPool_Reentrancy(); + + /** + * @notice Thrown when the pool is finalized + */ + error BPool_PoolIsFinalized(); + + /** + * @notice Thrown when the caller is not the controller + */ + error BPool_CallerIsNotController(); + + /** + * @notice Thrown when the pool is not finalized + */ + error BPool_FeeBelowMinimum(); + + /** + * @notice Thrown when the fee to set is above the maximum + */ + error BPool_FeeAboveMaximum(); + + /** + * @notice Thrown when the tokens array is below the minimum + */ + error BPool_TokensBelowMinimum(); + + /** + * @notice Thrown when the token is already bound in the pool + */ + error BPool_TokenAlreadyBound(); + + /** + * @notice Thrown when the tokens array is above the maximum + */ + error BPool_TokensAboveMaximum(); + + /** + * @notice Thrown when the weight to set is below the minimum + */ + error BPool_WeightBelowMinimum(); + + /** + * @notice Thrown when the weight to set is above the maximum + */ + error BPool_WeightAboveMaximum(); + + /** + * @notice Thrown when the balance to add is below the minimum + */ + error BPool_BalanceBelowMinimum(); + + /** + * @notice Thrown when the total weight is above the maximum + */ + error BPool_TotalWeightAboveMaximum(); + + /** + * @notice Thrown when the ratio between the pool token amount and the total supply is zero + */ + error BPool_InvalidPoolRatio(); + + /** + * @notice Thrown when the calculated token amount in is zero + */ + error BPool_InvalidTokenAmountIn(); + + /** + * @notice Thrown when the token amount in is above maximum amount in allowed by the caller + */ + error BPool_TokenAmountInAboveMaxAmountIn(); + + /** + * @notice Thrown when the calculated token amount out is zero + */ + error BPool_InvalidTokenAmountOut(); + + /** + * @notice Thrown when the token amount out is below minimum amount out allowed by the caller + */ + error BPool_TokenAmountOutBelowMinAmountOut(); + + /** + * @notice Thrown when the token is not bound in the pool + */ + error BPool_TokenNotBound(); + + /** + * @notice Thrown when the pool is not finalized + */ + error BPool_PoolNotFinalized(); + + /** + * @notice Thrown when the token amount in surpasses the maximum in allowed by the pool + */ + error BPool_TokenAmountInAboveMaxIn(); + + /** + * @notice Thrown when the spot price before the swap is above the max allowed by the caller + */ + error BPool_SpotPriceAboveMaxPrice(); + + /** + * @notice Thrown when the token amount out is below the minimum out allowed by the caller + */ + error BPool_TokenAmountOutBelowMinOut(); + + /** + * @notice Thrown when the spot price after the swap is below the spot price before the swap + */ + error BPool_SpotPriceAfterBelowSpotPriceBefore(); + + /** + * @notice Thrown when the spot price after the swap is above the max allowed by the caller + */ + error BPool_SpotPriceAfterBelowMaxPrice(); + + /** + * @notice Thrown when the spot price before the swap is above the ratio between the two tokens in the pool + */ + error BPool_SpotPriceBeforeAboveTokenRatio(); + + /** + * @notice Thrown when the token amount out surpasses the maximum out allowed by the pool + */ + error BPool_TokenAmountOutAboveMaxOut(); + + /** + * @notice Thrown when the pool token amount out is below the minimum pool token amount out allowed by the caller + */ + error BPool_PoolAmountOutBelowMinPoolAmountOut(); + + /** + * @notice Thrown when the calculated pool token amount in is zero + */ + error BPool_InvalidPoolAmountIn(); + + /** + * @notice Thrown when the pool token amount in is above the maximum amount in allowed by the caller + */ + error BPool_PoolAmountInAboveMaxPoolAmountIn(); + + /** + * @notice Thrown when the ERC20 transfer fails + */ + error BPool_ERC20TransferFailed(); + /** * @notice Sets the new swap fee * @param swapFee The new swap fee diff --git a/test/unit/BFactory.t.sol b/test/unit/BFactory.t.sol index 0914bcec..e462cc07 100644 --- a/test/unit/BFactory.t.sol +++ b/test/unit/BFactory.t.sol @@ -107,7 +107,7 @@ contract BFactory_Unit_SetBLabs is Base { */ function test_Revert_NotLabs(address _randomCaller) public { vm.assume(_randomCaller != owner); - vm.expectRevert('ERR_NOT_BLABS'); + vm.expectRevert(IBFactory.BFactory_NotBLabs.selector); vm.prank(_randomCaller); bFactory.setBLabs(_randomCaller); } @@ -138,7 +138,7 @@ contract BFactory_Unit_Collect is Base { */ function test_Revert_NotLabs(address _randomCaller) public { vm.assume(_randomCaller != owner); - vm.expectRevert('ERR_NOT_BLABS'); + vm.expectRevert(IBFactory.BFactory_NotBLabs.selector); vm.prank(_randomCaller); bFactory.collect(IBPool(address(0))); } @@ -180,7 +180,7 @@ contract BFactory_Unit_Collect is Base { vm.mockCall(_lpToken, abi.encodeWithSelector(IERC20.balanceOf.selector, address(bFactory)), abi.encode(_toCollect)); vm.mockCall(_lpToken, abi.encodeWithSelector(IERC20.transfer.selector, owner, _toCollect), abi.encode(false)); - vm.expectRevert('ERR_ERC20_FAILED'); + vm.expectRevert(IBFactory.BFactory_ERC20TransferFailed.selector); vm.prank(owner); bFactory.collect(IBPool(_lpToken)); } diff --git a/test/unit/BPool.t.sol b/test/unit/BPool.t.sol index 0ea4f884..f3f96fe1 100644 --- a/test/unit/BPool.t.sol +++ b/test/unit/BPool.t.sol @@ -87,7 +87,7 @@ abstract contract BasePoolTest is Test, BConst, Utils, BMath { // Simulate ongoing call to the contract bPool.set__mutex(true); - vm.expectRevert('ERR_REENTRY'); + vm.expectRevert(IBPool.BPool_Reentrancy.selector); } function _assumeCalcSpotPrice( @@ -317,7 +317,7 @@ contract BPool_Unit_GetFinalTokens is BasePoolTest { _setRandomTokens(_length); _setFinalize(false); - vm.expectRevert('ERR_NOT_FINALIZED'); + vm.expectRevert(IBPool.BPool_PoolNotFinalized.selector); bPool.getFinalTokens(); } } @@ -335,7 +335,7 @@ contract BPool_Unit_GetDenormalizedWeight is BasePoolTest { } function test_Revert_NotBound(address _token) public { - vm.expectRevert('ERR_NOT_BOUND'); + vm.expectRevert(IBPool.BPool_TokenNotBound.selector); bPool.getDenormalizedWeight(_token); } } @@ -370,7 +370,7 @@ contract BPool_Unit_GetNormalizedWeight is BasePoolTest { } function test_Revert_NotBound(address _token) public { - vm.expectRevert('ERR_NOT_BOUND'); + vm.expectRevert(IBPool.BPool_TokenNotBound.selector); bPool.getNormalizedWeight(_token); } } @@ -391,7 +391,7 @@ contract BPool_Unit_GetBalance is BasePoolTest { } function test_Revert_NotBound(address _token) public { - vm.expectRevert('ERR_NOT_BOUND'); + vm.expectRevert(IBPool.BPool_TokenNotBound.selector); bPool.getBalance(_token); } } @@ -432,7 +432,7 @@ contract BPool_Unit_SetSwapFee is BasePoolTest { function test_Revert_Finalized(uint256 _fee) public happyPath(_fee) { _setFinalize(true); - vm.expectRevert('ERR_IS_FINALIZED'); + vm.expectRevert(IBPool.BPool_PoolIsFinalized.selector); bPool.setSwapFee(_fee); } @@ -440,7 +440,7 @@ contract BPool_Unit_SetSwapFee is BasePoolTest { vm.assume(_controller != _caller); bPool.set__controller(_controller); - vm.expectRevert('ERR_NOT_CONTROLLER'); + vm.expectRevert(IBPool.BPool_CallerIsNotController.selector); vm.prank(_caller); bPool.setSwapFee(_fee); } @@ -448,14 +448,14 @@ contract BPool_Unit_SetSwapFee is BasePoolTest { function test_Revert_MinFee(uint256 _fee) public { vm.assume(_fee < MIN_FEE); - vm.expectRevert('ERR_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('ERR_MAX_FEE'); + vm.expectRevert(IBPool.BPool_FeeAboveMaximum.selector); bPool.setSwapFee(_fee); } @@ -487,7 +487,7 @@ contract BPool_Unit_SetController is BasePoolTest { vm.assume(_controller != _caller); bPool.set__controller(_controller); - vm.expectRevert('ERR_NOT_CONTROLLER'); + vm.expectRevert(IBPool.BPool_CallerIsNotController.selector); vm.prank(_caller); bPool.setController(_newController); } @@ -528,14 +528,14 @@ contract BPool_Unit_Finalize is BasePoolTest { bPool.set__controller(_controller); vm.prank(_caller); - vm.expectRevert('ERR_NOT_CONTROLLER'); + vm.expectRevert(IBPool.BPool_CallerIsNotController.selector); bPool.finalize(); } function test_Revert_Finalized(uint256 _tokensLength) public happyPath(_tokensLength) { _setFinalize(true); - vm.expectRevert('ERR_IS_FINALIZED'); + vm.expectRevert(IBPool.BPool_PoolIsFinalized.selector); bPool.finalize(); } @@ -543,7 +543,7 @@ contract BPool_Unit_Finalize is BasePoolTest { _tokensLength = bound(_tokensLength, 0, MIN_BOUND_TOKENS - 1); _setRandomTokens(_tokensLength); - vm.expectRevert('ERR_MIN_TOKENS'); + vm.expectRevert(IBPool.BPool_TokensBelowMinimum.selector); bPool.finalize(); } @@ -632,21 +632,21 @@ contract BPool_Unit_Bind is BasePoolTest { bPool.set__controller(_controller); vm.prank(_caller); - vm.expectRevert('ERR_NOT_CONTROLLER'); + vm.expectRevert(IBPool.BPool_CallerIsNotController.selector); bPool.bind(_fuzz.token, _fuzz.balance, _fuzz.denorm); } function test_Revert_IsBound(Bind_FuzzScenario memory _fuzz, address _token) public happyPath(_fuzz) { _setRecord(_token, IBPool.Record({bound: true, index: 0, denorm: 0})); - vm.expectRevert('ERR_IS_BOUND'); + vm.expectRevert(IBPool.BPool_TokenAlreadyBound.selector); bPool.bind(_token, _fuzz.balance, _fuzz.denorm); } function test_Revert_Finalized(Bind_FuzzScenario memory _fuzz) public happyPath(_fuzz) { _setFinalize(true); - vm.expectRevert('ERR_IS_FINALIZED'); + vm.expectRevert(IBPool.BPool_PoolIsFinalized.selector); bPool.bind(_fuzz.token, _fuzz.balance, _fuzz.denorm); } @@ -656,7 +656,7 @@ contract BPool_Unit_Bind is BasePoolTest { vm.assume(_fuzz.token != _tokens[i]); } - vm.expectRevert('ERR_MAX_TOKENS'); + vm.expectRevert(IBPool.BPool_TokensAboveMaximum.selector); bPool.bind(_fuzz.token, _fuzz.balance, _fuzz.denorm); } @@ -685,21 +685,21 @@ contract BPool_Unit_Bind is BasePoolTest { function test_Revert_MinWeight(Bind_FuzzScenario memory _fuzz, uint256 _denorm) public happyPath(_fuzz) { vm.assume(_denorm < MIN_WEIGHT); - vm.expectRevert('ERR_MIN_WEIGHT'); + vm.expectRevert(IBPool.BPool_WeightBelowMinimum.selector); bPool.bind(_fuzz.token, _fuzz.balance, _denorm); } function test_Revert_MaxWeight(Bind_FuzzScenario memory _fuzz, uint256 _denorm) public happyPath(_fuzz) { vm.assume(_denorm > MAX_WEIGHT); - vm.expectRevert('ERR_MAX_WEIGHT'); + vm.expectRevert(IBPool.BPool_WeightAboveMaximum.selector); bPool.bind(_fuzz.token, _fuzz.balance, _denorm); } function test_Revert_MinBalance(Bind_FuzzScenario memory _fuzz, uint256 _balance) public happyPath(_fuzz) { vm.assume(_balance < MIN_BALANCE); - vm.expectRevert('ERR_MIN_BALANCE'); + vm.expectRevert(IBPool.BPool_BalanceBelowMinimum.selector); bPool.bind(_fuzz.token, _balance, _fuzz.denorm); } @@ -718,7 +718,7 @@ contract BPool_Unit_Bind is BasePoolTest { _denorm = bound(_denorm, MIN_WEIGHT, MAX_WEIGHT); _setTotalWeight(MAX_TOTAL_WEIGHT); - vm.expectRevert('ERR_MAX_TOTAL_WEIGHT'); + vm.expectRevert(IBPool.BPool_TotalWeightAboveMaximum.selector); bPool.bind(_fuzz.token, _fuzz.balance, _denorm); } @@ -791,21 +791,21 @@ contract BPool_Unit_Unbind is BasePoolTest { bPool.set__controller(_controller); vm.prank(_caller); - vm.expectRevert('ERR_NOT_CONTROLLER'); + vm.expectRevert(IBPool.BPool_CallerIsNotController.selector); bPool.unbind(_fuzz.token); } function test_Revert_NotBound(Unbind_FuzzScenario memory _fuzz, address _token) public happyPath(_fuzz) { _setRecord(_token, IBPool.Record({bound: false, index: _fuzz.tokenIndex, denorm: _fuzz.denorm})); - vm.expectRevert('ERR_NOT_BOUND'); + vm.expectRevert(IBPool.BPool_TokenNotBound.selector); bPool.unbind(_token); } function test_Revert_Finalized(Unbind_FuzzScenario memory _fuzz) public happyPath(_fuzz) { _setFinalize(true); - vm.expectRevert('ERR_IS_FINALIZED'); + vm.expectRevert(IBPool.BPool_PoolIsFinalized.selector); bPool.unbind(_fuzz.token); } @@ -913,7 +913,7 @@ contract BPool_Unit_GetSpotPrice is BasePoolTest { vm.assume(_tokenIn != _fuzz.tokenIn); vm.assume(_tokenIn != _fuzz.tokenOut); - vm.expectRevert('ERR_NOT_BOUND'); + vm.expectRevert(IBPool.BPool_TokenNotBound.selector); bPool.getSpotPrice(_tokenIn, _fuzz.tokenOut); } @@ -924,7 +924,7 @@ contract BPool_Unit_GetSpotPrice is BasePoolTest { vm.assume(_tokenOut != _fuzz.tokenIn); vm.assume(_tokenOut != _fuzz.tokenOut); - vm.expectRevert('ERR_NOT_BOUND'); + vm.expectRevert(IBPool.BPool_TokenNotBound.selector); bPool.getSpotPrice(_fuzz.tokenIn, _tokenOut); } @@ -980,7 +980,7 @@ contract BPool_Unit_GetSpotPriceSansFee is BasePoolTest { vm.assume(_tokenIn != _fuzz.tokenIn); vm.assume(_tokenIn != _fuzz.tokenOut); - vm.expectRevert('ERR_NOT_BOUND'); + vm.expectRevert(IBPool.BPool_TokenNotBound.selector); bPool.getSpotPriceSansFee(_tokenIn, _fuzz.tokenOut); } @@ -991,7 +991,7 @@ contract BPool_Unit_GetSpotPriceSansFee is BasePoolTest { vm.assume(_tokenOut != _fuzz.tokenIn); vm.assume(_tokenOut != _fuzz.tokenOut); - vm.expectRevert('ERR_NOT_BOUND'); + vm.expectRevert(IBPool.BPool_TokenNotBound.selector); bPool.getSpotPriceSansFee(_fuzz.tokenIn, _tokenOut); } @@ -1066,28 +1066,31 @@ contract BPool_Unit_JoinPool is BasePoolTest { function test_Revert_NotFinalized(JoinPool_FuzzScenario memory _fuzz) public { _setFinalize(false); - vm.expectRevert('ERR_NOT_FINALIZED'); + vm.expectRevert(IBPool.BPool_PoolNotFinalized.selector); bPool.joinPool(_fuzz.poolAmountOut, _maxArray(tokens.length)); } - function test_Revert_MathApprox(JoinPool_FuzzScenario memory _fuzz, uint256 _poolAmountOut) public happyPath(_fuzz) { + function test_Revert_InvalidPoolRatio( + JoinPool_FuzzScenario memory _fuzz, + uint256 _poolAmountOut + ) public happyPath(_fuzz) { _poolAmountOut = bound(_poolAmountOut, 0, (INIT_POOL_SUPPLY / 2 / BONE) - 1); // bdiv rounds up - vm.expectRevert('ERR_MATH_APPROX'); + vm.expectRevert(IBPool.BPool_InvalidPoolRatio.selector); bPool.joinPool(_poolAmountOut, _maxArray(tokens.length)); } - function test_Revert_TokenArrayMathApprox(JoinPool_FuzzScenario memory _fuzz, uint256 _tokenIndex) public { + function test_Revert_InvalidTokenAmountIn(JoinPool_FuzzScenario memory _fuzz, uint256 _tokenIndex) public { _assumeHappyPath(_fuzz); _tokenIndex = bound(_tokenIndex, 0, TOKENS_AMOUNT - 1); _fuzz.balance[_tokenIndex] = 0; _setValues(_fuzz); - vm.expectRevert('ERR_MATH_APPROX'); + vm.expectRevert(IBPool.BPool_InvalidTokenAmountIn.selector); bPool.joinPool(_fuzz.poolAmountOut, _maxArray(tokens.length)); } - function test_Revert_TokenArrayLimitIn( + function test_Revert_TokenAmountInAboveMaxAmountIn( JoinPool_FuzzScenario memory _fuzz, uint256 _tokenIndex, uint256[TOKENS_AMOUNT] memory _maxAmountsIn @@ -1100,7 +1103,7 @@ contract BPool_Unit_JoinPool is BasePoolTest { _maxAmountsIn[i] = _tokenIndex == i ? _tokenAmountIn - 1 : _tokenAmountIn; } - vm.expectRevert('ERR_LIMIT_IN'); + vm.expectRevert(IBPool.BPool_TokenAmountInAboveMaxAmountIn.selector); bPool.joinPool(_fuzz.poolAmountOut, _staticToDynamicUintArray(_maxAmountsIn)); } @@ -1225,14 +1228,17 @@ contract BPool_Unit_ExitPool is BasePoolTest { function test_Revert_NotFinalized(ExitPool_FuzzScenario memory _fuzz) public happyPath(_fuzz) { _setFinalize(false); - vm.expectRevert('ERR_NOT_FINALIZED'); + vm.expectRevert(IBPool.BPool_PoolNotFinalized.selector); bPool.exitPool(_fuzz.poolAmountIn, _zeroArray(tokens.length)); } - function test_Revert_MathApprox(ExitPool_FuzzScenario memory _fuzz, uint256 _poolAmountIn) public happyPath(_fuzz) { + function test_Revert_InvalidPoolRatio( + ExitPool_FuzzScenario memory _fuzz, + uint256 _poolAmountIn + ) public happyPath(_fuzz) { _poolAmountIn = bound(_poolAmountIn, 0, (INIT_POOL_SUPPLY / 2 / BONE) - 1); // bdiv rounds up - vm.expectRevert('ERR_MATH_APPROX'); + vm.expectRevert(IBPool.BPool_InvalidPoolRatio.selector); bPool.exitPool(_poolAmountIn, _zeroArray(tokens.length)); } @@ -1264,7 +1270,7 @@ contract BPool_Unit_ExitPool is BasePoolTest { assertEq(bPool.totalSupply(), _totalSupplyBefore - _pAiAfterExitFee); } - function test_Revert_TokenArrayMathApprox( + function test_Revert_InvalidTokenAmountOut( ExitPool_FuzzScenario memory _fuzz, uint256 _tokenIndex ) public happyPath(_fuzz) { @@ -1273,11 +1279,11 @@ contract BPool_Unit_ExitPool is BasePoolTest { _fuzz.balance[_tokenIndex] = 0; _setValues(_fuzz); - vm.expectRevert('ERR_MATH_APPROX'); + vm.expectRevert(IBPool.BPool_InvalidTokenAmountOut.selector); bPool.exitPool(_fuzz.poolAmountIn, _zeroArray(tokens.length)); } - function test_Revert_TokenArrayLimitOut( + function test_Revert_TokenAmountOutBelowMinAmountOut( ExitPool_FuzzScenario memory _fuzz, uint256 _tokenIndex, uint256[TOKENS_AMOUNT] memory _minAmountsOut @@ -1296,7 +1302,7 @@ contract BPool_Unit_ExitPool is BasePoolTest { _minAmountsOut[i] = _tokenIndex == i ? _tokenAmountOut + 1 : _tokenAmountOut; } - vm.expectRevert('ERR_LIMIT_OUT'); + vm.expectRevert(IBPool.BPool_TokenAmountOutBelowMinAmountOut.selector); bPool.exitPool(_fuzz.poolAmountIn, _staticToDynamicUintArray(_minAmountsOut)); } @@ -1455,7 +1461,7 @@ contract BPool_Unit_SwapExactAmountIn is BasePoolTest { vm.assume(_tokenIn != tokenIn); vm.assume(_tokenIn != tokenOut); - vm.expectRevert('ERR_NOT_BOUND'); + vm.expectRevert(IBPool.BPool_TokenNotBound.selector); bPool.swapExactAmountIn(_tokenIn, _fuzz.tokenAmountIn, tokenOut, 0, type(uint256).max); } @@ -1467,25 +1473,25 @@ contract BPool_Unit_SwapExactAmountIn is BasePoolTest { vm.assume(_tokenOut != tokenIn); vm.assume(_tokenOut != tokenOut); - vm.expectRevert('ERR_NOT_BOUND'); + vm.expectRevert(IBPool.BPool_TokenNotBound.selector); bPool.swapExactAmountIn(tokenIn, _fuzz.tokenAmountIn, _tokenOut, 0, type(uint256).max); } function test_Revert_NotFinalized(SwapExactAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { _setFinalize(false); - vm.expectRevert('ERR_NOT_FINALIZED'); + vm.expectRevert(IBPool.BPool_PoolNotFinalized.selector); bPool.swapExactAmountIn(tokenIn, _fuzz.tokenAmountIn, tokenOut, 0, type(uint256).max); } - function test_Revert_MaxInRatio(SwapExactAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { + function test_Revert_TokenAmountInAboveMaxIn(SwapExactAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { uint256 _tokenAmountIn = bmul(_fuzz.tokenInBalance, MAX_IN_RATIO) + 1; - vm.expectRevert('ERR_MAX_IN_RATIO'); + vm.expectRevert(IBPool.BPool_TokenAmountInAboveMaxIn.selector); bPool.swapExactAmountIn(tokenIn, _tokenAmountIn, tokenOut, 0, type(uint256).max); } - function test_Revert_BadLimitPrice( + function test_Revert_SpotPriceAboveMaxPrice( SwapExactAmountIn_FuzzScenario memory _fuzz, uint256 _maxPrice ) public happyPath(_fuzz) { @@ -1495,11 +1501,11 @@ contract BPool_Unit_SwapExactAmountIn is BasePoolTest { vm.assume(_spotPriceBefore > 0); _maxPrice = bound(_maxPrice, 0, _spotPriceBefore - 1); - vm.expectRevert('ERR_BAD_LIMIT_PRICE'); + vm.expectRevert(IBPool.BPool_SpotPriceAboveMaxPrice.selector); bPool.swapExactAmountIn(tokenIn, _fuzz.tokenAmountIn, tokenOut, 0, _maxPrice); } - function test_Revert_LimitOut( + function test_Revert_TokenAmountOutBelowMinOut( SwapExactAmountIn_FuzzScenario memory _fuzz, uint256 _minAmountOut ) public happyPath(_fuzz) { @@ -1513,7 +1519,7 @@ contract BPool_Unit_SwapExactAmountIn is BasePoolTest { ); _minAmountOut = bound(_minAmountOut, _tokenAmountOut + 1, type(uint256).max); - vm.expectRevert('ERR_LIMIT_OUT'); + vm.expectRevert(IBPool.BPool_TokenAmountOutBelowMinOut.selector); bPool.swapExactAmountIn(tokenIn, _fuzz.tokenAmountIn, tokenOut, _minAmountOut, type(uint256).max); } @@ -1522,12 +1528,12 @@ contract BPool_Unit_SwapExactAmountIn is BasePoolTest { bPool.swapExactAmountIn(tokenIn, _fuzz.tokenAmountIn, tokenOut, 0, type(uint256).max); } - function test_Revert_MathApprox() public { + function test_Revert_SpotPriceAfterBelowSpotPriceBefore() public { vm.skip(true); // TODO: this revert might be unreachable. Find a way to test it or remove the revert in the code. } - function test_Revert_LimitPrice(SwapExactAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { + function test_Revert_SpotPriceAfterBelowMaxPrice(SwapExactAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { uint256 _tokenAmountOut = calcOutGivenIn( _fuzz.tokenInBalance, _fuzz.tokenInDenorm, @@ -1548,11 +1554,11 @@ contract BPool_Unit_SwapExactAmountIn is BasePoolTest { ); vm.assume(_spotPriceAfter > _spotPriceBefore); - vm.expectRevert('ERR_LIMIT_PRICE'); + vm.expectRevert(IBPool.BPool_SpotPriceAfterBelowMaxPrice.selector); bPool.swapExactAmountIn(tokenIn, _fuzz.tokenAmountIn, tokenOut, 0, _spotPriceBefore); } - function test_Revert_MathApprox2(SwapExactAmountIn_FuzzScenario memory _fuzz) public { + function test_Revert_SpotPriceBeforeAboveTokenRatio(SwapExactAmountIn_FuzzScenario memory _fuzz) public { // Replicating _assumeHappyPath, but removing irrelevant assumptions and conditioning the revert _fuzz.tokenInDenorm = bound(_fuzz.tokenInDenorm, MIN_WEIGHT, MAX_WEIGHT); _fuzz.tokenOutDenorm = bound(_fuzz.tokenOutDenorm, MIN_WEIGHT, MAX_WEIGHT); @@ -1589,7 +1595,7 @@ contract BPool_Unit_SwapExactAmountIn is BasePoolTest { _setValues(_fuzz); - vm.expectRevert('ERR_MATH_APPROX'); + vm.expectRevert(IBPool.BPool_SpotPriceBeforeAboveTokenRatio.selector); bPool.swapExactAmountIn(tokenIn, _fuzz.tokenAmountIn, tokenOut, 0, type(uint256).max); } @@ -1789,7 +1795,7 @@ contract BPool_Unit_SwapExactAmountOut is BasePoolTest { vm.assume(_tokenIn != tokenIn); vm.assume(_tokenIn != tokenOut); - vm.expectRevert('ERR_NOT_BOUND'); + vm.expectRevert(IBPool.BPool_TokenNotBound.selector); bPool.swapExactAmountOut(_tokenIn, type(uint256).max, tokenOut, _fuzz.tokenAmountOut, type(uint256).max); } @@ -1801,25 +1807,25 @@ contract BPool_Unit_SwapExactAmountOut is BasePoolTest { vm.assume(_tokenOut != tokenIn); vm.assume(_tokenOut != tokenOut); - vm.expectRevert('ERR_NOT_BOUND'); + vm.expectRevert(IBPool.BPool_TokenNotBound.selector); bPool.swapExactAmountOut(tokenIn, type(uint256).max, _tokenOut, _fuzz.tokenAmountOut, type(uint256).max); } function test_Revert_NotFinalized(SwapExactAmountOut_FuzzScenario memory _fuzz) public happyPath(_fuzz) { _setFinalize(false); - vm.expectRevert('ERR_NOT_FINALIZED'); + vm.expectRevert(IBPool.BPool_PoolNotFinalized.selector); bPool.swapExactAmountOut(tokenIn, type(uint256).max, tokenOut, _fuzz.tokenAmountOut, type(uint256).max); } - function test_Revert_MaxOutRatio(SwapExactAmountOut_FuzzScenario memory _fuzz) public happyPath(_fuzz) { + function test_Revert_TokenAmountOutAboveMaxOut(SwapExactAmountOut_FuzzScenario memory _fuzz) public happyPath(_fuzz) { uint256 _tokenAmountOut = bmul(_fuzz.tokenOutBalance, MAX_OUT_RATIO) + 1; - vm.expectRevert('ERR_MAX_OUT_RATIO'); + vm.expectRevert(IBPool.BPool_TokenAmountOutAboveMaxOut.selector); bPool.swapExactAmountOut(tokenIn, type(uint256).max, tokenOut, _tokenAmountOut, type(uint256).max); } - function test_Revert_BadLimitPrice( + function test_Revert_SpotPriceAboveMaxPrice( SwapExactAmountOut_FuzzScenario memory _fuzz, uint256 _maxPrice ) public happyPath(_fuzz) { @@ -1829,11 +1835,11 @@ contract BPool_Unit_SwapExactAmountOut is BasePoolTest { vm.assume(_spotPriceBefore > 0); _maxPrice = bound(_maxPrice, 0, _spotPriceBefore - 1); - vm.expectRevert('ERR_BAD_LIMIT_PRICE'); + vm.expectRevert(IBPool.BPool_SpotPriceAboveMaxPrice.selector); bPool.swapExactAmountOut(tokenIn, type(uint256).max, tokenOut, _fuzz.tokenAmountOut, _maxPrice); } - function test_Revert_LimitIn( + function test_Revert_TokenAmountInAboveMaxAmountIn( SwapExactAmountOut_FuzzScenario memory _fuzz, uint256 _maxAmountIn ) public happyPath(_fuzz) { @@ -1847,7 +1853,7 @@ contract BPool_Unit_SwapExactAmountOut is BasePoolTest { ); _maxAmountIn = bound(_maxAmountIn, 0, _tokenAmountIn - 1); - vm.expectRevert('ERR_LIMIT_IN'); + vm.expectRevert(IBPool.BPool_TokenAmountInAboveMaxAmountIn.selector); bPool.swapExactAmountOut(tokenIn, _maxAmountIn, tokenOut, _fuzz.tokenAmountOut, type(uint256).max); } @@ -1861,7 +1867,10 @@ contract BPool_Unit_SwapExactAmountOut is BasePoolTest { // TODO: this revert might be unreachable. Find a way to test it or remove the revert in the code. } - function test_Revert_LimitPrice(SwapExactAmountOut_FuzzScenario memory _fuzz) public happyPath(_fuzz) { + function test_Revert_SpotPriceAfterBelowMaxPrice(SwapExactAmountOut_FuzzScenario memory _fuzz) + public + happyPath(_fuzz) + { uint256 _tokenAmountIn = calcInGivenOut( _fuzz.tokenInBalance, _fuzz.tokenInDenorm, @@ -1882,11 +1891,11 @@ contract BPool_Unit_SwapExactAmountOut is BasePoolTest { ); vm.assume(_spotPriceAfter > _spotPriceBefore); - vm.expectRevert('ERR_LIMIT_PRICE'); + vm.expectRevert(IBPool.BPool_SpotPriceAfterBelowMaxPrice.selector); bPool.swapExactAmountOut(tokenIn, type(uint256).max, tokenOut, _fuzz.tokenAmountOut, _spotPriceBefore); } - function test_Revert_MathApprox2(SwapExactAmountOut_FuzzScenario memory _fuzz) public { + function test_Revert_SpotPriceBeforeAboveTokenRatio(SwapExactAmountOut_FuzzScenario memory _fuzz) public { // Replicating _assumeHappyPath, but removing irrelevant assumptions and conditioning the revert _fuzz.tokenInDenorm = bound(_fuzz.tokenInDenorm, MIN_WEIGHT, MAX_WEIGHT); _fuzz.tokenOutDenorm = bound(_fuzz.tokenOutDenorm, MIN_WEIGHT, MAX_WEIGHT); @@ -1929,7 +1938,7 @@ contract BPool_Unit_SwapExactAmountOut is BasePoolTest { _setValues(_fuzz); - vm.expectRevert('ERR_MATH_APPROX'); + vm.expectRevert(IBPool.BPool_SpotPriceBeforeAboveTokenRatio.selector); bPool.swapExactAmountOut(tokenIn, type(uint256).max, tokenOut, _fuzz.tokenAmountOut, type(uint256).max); } @@ -2082,7 +2091,7 @@ contract BPool_Unit_JoinswapExternAmountIn is BasePoolTest { function test_Revert_NotFinalized(JoinswapExternAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { _setFinalize(false); - vm.expectRevert('ERR_NOT_FINALIZED'); + vm.expectRevert(IBPool.BPool_PoolNotFinalized.selector); bPool.joinswapExternAmountIn(tokenIn, _fuzz.tokenAmountIn, 0); } @@ -2092,18 +2101,21 @@ contract BPool_Unit_JoinswapExternAmountIn is BasePoolTest { ) public happyPath(_fuzz) { assumeNotForgeAddress(_tokenIn); - vm.expectRevert('ERR_NOT_BOUND'); + vm.expectRevert(IBPool.BPool_TokenNotBound.selector); bPool.joinswapExternAmountIn(_tokenIn, _fuzz.tokenAmountIn, 0); } - function test_Revert_MaxInRatio(JoinswapExternAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { + function test_Revert_TokenAmountInAboveMaxIn(JoinswapExternAmountIn_FuzzScenario memory _fuzz) + public + happyPath(_fuzz) + { uint256 _tokenAmountIn = bmul(_fuzz.tokenInBalance, MAX_IN_RATIO); - vm.expectRevert('ERR_MAX_IN_RATIO'); + vm.expectRevert(IBPool.BPool_TokenAmountInAboveMaxIn.selector); bPool.joinswapExternAmountIn(tokenIn, _tokenAmountIn + 1, 0); } - function test_Revert_LimitOut( + function test_Revert_PoolAmountOutBelowMinPoolAmountOut( JoinswapExternAmountIn_FuzzScenario memory _fuzz, uint256 _minPoolAmountOut ) public happyPath(_fuzz) { @@ -2117,7 +2129,7 @@ contract BPool_Unit_JoinswapExternAmountIn is BasePoolTest { ); _minPoolAmountOut = bound(_minPoolAmountOut, _poolAmountIn + 1, type(uint256).max); - vm.expectRevert('ERR_LIMIT_OUT'); + vm.expectRevert(IBPool.BPool_PoolAmountOutBelowMinPoolAmountOut.selector); bPool.joinswapExternAmountIn(tokenIn, _fuzz.tokenAmountIn, _minPoolAmountOut); } @@ -2264,7 +2276,7 @@ contract BPool_Unit_JoinswapPoolAmountOut is BasePoolTest { function test_Revert_NotFinalized(JoinswapPoolAmountOut_FuzzScenario memory _fuzz) public happyPath(_fuzz) { _setFinalize(false); - vm.expectRevert('ERR_NOT_FINALIZED'); + vm.expectRevert(IBPool.BPool_PoolNotFinalized.selector); bPool.joinswapPoolAmountOut(tokenIn, _fuzz.poolAmountOut, type(uint256).max); } @@ -2274,18 +2286,18 @@ contract BPool_Unit_JoinswapPoolAmountOut is BasePoolTest { ) public happyPath(_fuzz) { assumeNotForgeAddress(_tokenIn); - vm.expectRevert('ERR_NOT_BOUND'); + vm.expectRevert(IBPool.BPool_TokenNotBound.selector); bPool.joinswapPoolAmountOut(_tokenIn, _fuzz.poolAmountOut, type(uint256).max); } - function test_Revert_MathApprox(JoinswapPoolAmountOut_FuzzScenario memory _fuzz) public happyPath(_fuzz) { + function test_Revert_InvalidTokenAmountIn(JoinswapPoolAmountOut_FuzzScenario memory _fuzz) public happyPath(_fuzz) { _fuzz.poolAmountOut = 0; - vm.expectRevert('ERR_MATH_APPROX'); + vm.expectRevert(IBPool.BPool_InvalidTokenAmountIn.selector); bPool.joinswapPoolAmountOut(tokenIn, _fuzz.poolAmountOut, type(uint256).max); } - function test_Revert_LimitIn( + function test_Revert_TokenAmountInAboveMaxAmountIn( JoinswapPoolAmountOut_FuzzScenario memory _fuzz, uint256 _maxAmountIn ) public happyPath(_fuzz) { @@ -2299,11 +2311,11 @@ contract BPool_Unit_JoinswapPoolAmountOut is BasePoolTest { ); _maxAmountIn = bound(_maxAmountIn, 0, _tokenAmountIn - 1); - vm.expectRevert('ERR_LIMIT_IN'); + vm.expectRevert(IBPool.BPool_TokenAmountInAboveMaxAmountIn.selector); bPool.joinswapPoolAmountOut(tokenIn, _fuzz.poolAmountOut, _maxAmountIn); } - function test_Revert_MaxInRatio(JoinswapPoolAmountOut_FuzzScenario memory _fuzz) public { + function test_Revert_TokenAmountInAboveMaxIn(JoinswapPoolAmountOut_FuzzScenario memory _fuzz) public { // Replicating _assumeHappyPath, but removing irrelevant assumptions and conditioning the revert _fuzz.tokenInDenorm = bound(_fuzz.tokenInDenorm, MIN_WEIGHT, MAX_WEIGHT); _fuzz.swapFee = bound(_fuzz.swapFee, MIN_FEE, MAX_FEE); @@ -2326,7 +2338,7 @@ contract BPool_Unit_JoinswapPoolAmountOut is BasePoolTest { _setValues(_fuzz); - vm.expectRevert('ERR_MAX_IN_RATIO'); + vm.expectRevert(IBPool.BPool_TokenAmountInAboveMaxIn.selector); bPool.joinswapPoolAmountOut(tokenIn, _fuzz.poolAmountOut, type(uint256).max); } @@ -2497,7 +2509,7 @@ contract BPool_Unit_ExitswapPoolAmountIn is BasePoolTest { function test_Revert_NotFinalized(ExitswapPoolAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { _setFinalize(false); - vm.expectRevert('ERR_NOT_FINALIZED'); + vm.expectRevert(IBPool.BPool_PoolNotFinalized.selector); bPool.exitswapPoolAmountIn(tokenOut, _fuzz.poolAmountIn, 0); } @@ -2507,11 +2519,11 @@ contract BPool_Unit_ExitswapPoolAmountIn is BasePoolTest { ) public happyPath(_fuzz) { assumeNotForgeAddress(_tokenIn); - vm.expectRevert('ERR_NOT_BOUND'); + vm.expectRevert(IBPool.BPool_TokenNotBound.selector); bPool.exitswapPoolAmountIn(_tokenIn, _fuzz.poolAmountIn, 0); } - function test_Revert_LimitOut( + function test_Revert_TokenAmountOutBelowMinAmountOut( ExitswapPoolAmountIn_FuzzScenario memory _fuzz, uint256 _minAmountOut ) public happyPath(_fuzz) { @@ -2525,11 +2537,11 @@ contract BPool_Unit_ExitswapPoolAmountIn is BasePoolTest { ); _minAmountOut = bound(_minAmountOut, _tokenAmountOut + 1, type(uint256).max); - vm.expectRevert('ERR_LIMIT_OUT'); + vm.expectRevert(IBPool.BPool_TokenAmountOutBelowMinAmountOut.selector); bPool.exitswapPoolAmountIn(tokenOut, _fuzz.poolAmountIn, _minAmountOut); } - function test_Revert_MaxOutRatio(ExitswapPoolAmountIn_FuzzScenario memory _fuzz) public { + function test_Revert_TokenAmountOutAboveMaxOut(ExitswapPoolAmountIn_FuzzScenario memory _fuzz) public { // Replicating _assumeHappyPath, but removing irrelevant assumptions and conditioning the revert _fuzz.tokenOutDenorm = bound(_fuzz.tokenOutDenorm, MIN_WEIGHT, MAX_WEIGHT); _fuzz.swapFee = bound(_fuzz.swapFee, MIN_FEE, MAX_FEE); @@ -2558,7 +2570,7 @@ contract BPool_Unit_ExitswapPoolAmountIn is BasePoolTest { _setValues(_fuzz); - vm.expectRevert('ERR_MAX_OUT_RATIO'); + vm.expectRevert(IBPool.BPool_TokenAmountOutAboveMaxOut.selector); bPool.exitswapPoolAmountIn(tokenOut, _fuzz.poolAmountIn, 0); } @@ -2748,7 +2760,7 @@ contract BPool_Unit_ExitswapExternAmountOut is BasePoolTest { function test_Revert_NotFinalized(ExitswapExternAmountOut_FuzzScenario memory _fuzz) public { _setFinalize(false); - vm.expectRevert('ERR_NOT_FINALIZED'); + vm.expectRevert(IBPool.BPool_PoolNotFinalized.selector); bPool.exitswapExternAmountOut(tokenOut, _fuzz.tokenAmountOut, type(uint256).max); } @@ -2758,25 +2770,28 @@ contract BPool_Unit_ExitswapExternAmountOut is BasePoolTest { ) public happyPath(_fuzz) { assumeNotForgeAddress(_tokenOut); - vm.expectRevert('ERR_NOT_BOUND'); + vm.expectRevert(IBPool.BPool_TokenNotBound.selector); bPool.exitswapExternAmountOut(_tokenOut, _fuzz.tokenAmountOut, type(uint256).max); } - function test_Revert_MaxOutRatio(ExitswapExternAmountOut_FuzzScenario memory _fuzz) public happyPath(_fuzz) { + function test_Revert_TokenAmountOutAboveMaxOut(ExitswapExternAmountOut_FuzzScenario memory _fuzz) + public + happyPath(_fuzz) + { uint256 _maxTokenAmountOut = bmul(_fuzz.tokenOutBalance, MAX_OUT_RATIO); - vm.expectRevert('ERR_MAX_OUT_RATIO'); + vm.expectRevert(IBPool.BPool_TokenAmountOutAboveMaxOut.selector); bPool.exitswapExternAmountOut(tokenOut, _maxTokenAmountOut + 1, type(uint256).max); } - function test_Revert_MathApprox(ExitswapExternAmountOut_FuzzScenario memory _fuzz) public happyPath(_fuzz) { + function test_Revert_InvalidPoolAmountIn(ExitswapExternAmountOut_FuzzScenario memory _fuzz) public happyPath(_fuzz) { _fuzz.tokenAmountOut = 0; - vm.expectRevert('ERR_MATH_APPROX'); + vm.expectRevert(IBPool.BPool_InvalidPoolAmountIn.selector); bPool.exitswapExternAmountOut(tokenOut, _fuzz.tokenAmountOut, type(uint256).max); } - function test_Revert_LimitIn( + function test_Revert_PoolAmountInAboveMaxPoolAmountIn( ExitswapExternAmountOut_FuzzScenario memory _fuzz, uint256 _maxPoolAmountIn ) public happyPath(_fuzz) { @@ -2790,7 +2805,7 @@ contract BPool_Unit_ExitswapExternAmountOut is BasePoolTest { ); _maxPoolAmountIn = bound(_maxPoolAmountIn, 0, _poolAmountIn - 1); - vm.expectRevert('ERR_LIMIT_IN'); + vm.expectRevert(IBPool.BPool_PoolAmountInAboveMaxPoolAmountIn.selector); bPool.exitswapExternAmountOut(tokenOut, _fuzz.tokenAmountOut, _maxPoolAmountIn); } @@ -2909,7 +2924,7 @@ contract BPool_Unit__PullUnderlying is BasePoolTest { _erc20, abi.encodeWithSelector(IERC20.transferFrom.selector, _from, address(bPool), _amount), abi.encode(false) ); - vm.expectRevert('ERR_ERC20_FALSE'); + vm.expectRevert(IBPool.BPool_ERC20TransferFailed.selector); bPool.call__pullUnderlying(_erc20, _from, _amount); } } @@ -2929,7 +2944,7 @@ contract BPool_Unit__PushUnderlying is BasePoolTest { vm.mockCall(_erc20, abi.encodeWithSelector(IERC20.transfer.selector, _to, _amount), abi.encode(false)); - vm.expectRevert('ERR_ERC20_FALSE'); + vm.expectRevert(IBPool.BPool_ERC20TransferFailed.selector); bPool.call__pushUnderlying(_erc20, _to, _amount); } } From d36e19b316b902228fc8ef78146d01e5f92d4e5d Mon Sep 17 00:00:00 2001 From: teddy Date: Wed, 12 Jun 2024 08:58:51 -0300 Subject: [PATCH 02/32] feat: insert CowBPool (#75) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: import BCoWPool as-is from #26 * chore: update gas costs for different hardfork * chore: dumb updates to comply with style guide * test: unit tests for BCoWPools constructor * chore: add manually merged BCoWPool mock with all methods * test: unit tests for setting allowances on finalization * chore: move constants out of the contract * chore: unify onlyController * chore: use custom errors in BCoWPool * chore: explicitly track mocked files, remove ci step * refactor: use _afterFinalize hook instead of overriding finalize * chore: rename missing .t.sol * chore: suggested changes from code review these are untested, which is a bit scary. but we'll test them in time. * docs: add natspec for included interfaces * fix: comments in PR * fix: multi line comment --------- Co-authored-by: Weißer Hase --- .forge-snapshots/newBFactory.snap | 2 +- .forge-snapshots/newBPool.snap | 2 +- .forge-snapshots/swapExactAmountIn.snap | 2 +- .github/workflows/ci.yml | 6 - .gitignore | 3 - .solhint.json | 1 + .solhintignore | 3 +- foundry.toml | 1 + package.json | 1 + remappings.txt | 3 +- src/contracts/BCoWPool.sol | 203 +++++++++++++ src/contracts/BPool.sol | 67 ++--- src/interfaces/IBCoWPool.sol | 70 +++++ src/interfaces/ISettlement.sol | 16 ++ test/manual-smock/MockBCoWPool.sol | 364 ++++++++++++++++++++++++ test/smock/MockBFactory.sol | 45 +++ test/smock/MockBPool.sol | 340 ++++++++++++++++++++++ test/smock/MockBToken.sol | 60 ++++ test/unit/BCoWPool.t.sol | 61 ++++ test/unit/BPool.t.sol | 7 +- yarn.lock | 4 + 21 files changed, 1214 insertions(+), 47 deletions(-) create mode 100644 src/contracts/BCoWPool.sol create mode 100644 src/interfaces/IBCoWPool.sol create mode 100644 src/interfaces/ISettlement.sol create mode 100644 test/manual-smock/MockBCoWPool.sol create mode 100644 test/smock/MockBFactory.sol create mode 100644 test/smock/MockBPool.sol create mode 100644 test/smock/MockBToken.sol create mode 100644 test/unit/BCoWPool.t.sol diff --git a/.forge-snapshots/newBFactory.snap b/.forge-snapshots/newBFactory.snap index fcf5d2a1..36a0c47a 100644 --- a/.forge-snapshots/newBFactory.snap +++ b/.forge-snapshots/newBFactory.snap @@ -1 +1 @@ -3593368 \ No newline at end of file +3520860 \ No newline at end of file diff --git a/.forge-snapshots/newBPool.snap b/.forge-snapshots/newBPool.snap index 9b3ae76a..0e8f8d0e 100644 --- a/.forge-snapshots/newBPool.snap +++ b/.forge-snapshots/newBPool.snap @@ -1 +1 @@ -3371373 \ No newline at end of file +3307804 \ No newline at end of file diff --git a/.forge-snapshots/swapExactAmountIn.snap b/.forge-snapshots/swapExactAmountIn.snap index 3a563cd1..b424fd2d 100644 --- a/.forge-snapshots/swapExactAmountIn.snap +++ b/.forge-snapshots/swapExactAmountIn.snap @@ -1 +1 @@ -108434 \ No newline at end of file +107968 \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 300f519c..e7b79eeb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,9 +31,6 @@ jobs: - name: Install dependencies run: yarn --frozen-lockfile --network-concurrency 1 - - name: Create mock files with smock - run: yarn smock - - name: Precompile using 0.8.14 and via-ir=false run: yarn build @@ -61,9 +58,6 @@ jobs: - name: Install dependencies run: yarn --frozen-lockfile --network-concurrency 1 - - name: Create mock files with smock - run: yarn smock - - name: Precompile using 0.8.14 and via-ir=false run: yarn build diff --git a/.gitignore b/.gitignore index c6c30df9..9209344d 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,3 @@ broadcast/*/*/* # Out dir out - -# Smock dir -test/smock diff --git a/.solhint.json b/.solhint.json index 0b571399..209b914a 100644 --- a/.solhint.json +++ b/.solhint.json @@ -4,6 +4,7 @@ "avoid-low-level-calls": "off", "constructor-syntax": "warn", "func-visibility": ["warn", { "ignoreConstructors": true }], + "no-inline-assembly": "off", "ordering": "warn", "private-vars-leading-underscore": ["warn", { "strict": false }], "quotes": "off", diff --git a/.solhintignore b/.solhintignore index 9fdb1048..0790ddf4 100644 --- a/.solhintignore +++ b/.solhintignore @@ -1 +1,2 @@ -test/smock/* \ No newline at end of file +test/smock/* +test/manual-smock/* diff --git a/foundry.toml b/foundry.toml index a38fc11a..fead9a16 100644 --- a/foundry.toml +++ b/foundry.toml @@ -13,6 +13,7 @@ solc_version = '0.8.25' libs = ["node_modules", "lib"] optimizer_runs = 50 # TODO: increase for production and add via-ir ffi = true +evm_version = 'cancun' fs_permissions = [{ access = "read-write", path = ".forge-snapshots/"}] [profile.optimized] diff --git a/package.json b/package.json index f22f3f22..97651268 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "package.json": "sort-package-json" }, "dependencies": { + "@cowprotocol/contracts": "github:cowprotocol/contracts.git#a10f40788a", "@openzeppelin/contracts": "5.0.2", "solmate": "github:transmissions11/solmate#c892309" }, diff --git a/remappings.txt b/remappings.txt index 8378f597..22f73eb2 100644 --- a/remappings.txt +++ b/remappings.txt @@ -2,6 +2,7 @@ ds-test/=node_modules/ds-test/src forge-std/=node_modules/forge-std/src forge-gas-snapshot/=node_modules/forge-gas-snapshot/src solmate/=node_modules/solmate/src +@cowprotocol/=node_modules/@cowprotocol/contracts/src/contracts contracts/=src/contracts -interfaces/=src/interfaces \ No newline at end of file +interfaces/=src/interfaces diff --git a/src/contracts/BCoWPool.sol b/src/contracts/BCoWPool.sol new file mode 100644 index 00000000..3cad22ca --- /dev/null +++ b/src/contracts/BCoWPool.sol @@ -0,0 +1,203 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.25; + +import {IERC1271} from '@openzeppelin/contracts/interfaces/IERC1271.sol'; +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; + +import {GPv2Order} from '@cowprotocol/libraries/GPv2Order.sol'; + +import {BPool} from './BPool.sol'; +import {IBCoWPool} from 'interfaces/IBCoWPool.sol'; + +import {ISettlement} from 'interfaces/ISettlement.sol'; + +/** + * @dev The value representing the absence of a commitment. + */ +bytes32 constant EMPTY_COMMITMENT = bytes32(0); + +/** + * @dev The value representing that no trading parameters are currently + * accepted as valid by this contract, meaning that no trading can occur. + */ +bytes32 constant NO_TRADING = bytes32(0); + +/** + * @dev The largest possible duration of any AMM order, starting from the + * current block timestamp. + */ +uint32 constant MAX_ORDER_DURATION = 5 * 60; + +/** + * @dev The transient storage slot specified in this variable stores the + * value of the order commitment, that is, the only order hash that can be + * validated by calling `isValidSignature`. + * The hash corresponding to the constant `EMPTY_COMMITMENT` has special + * semantics, discussed in the related documentation. + * @dev This value is: + * uint256(keccak256("CoWAMM.ConstantProduct.commitment")) - 1 + */ +uint256 constant COMMITMENT_SLOT = 0x6c3c90245457060f6517787b2c4b8cf500ca889d2304af02043bd5b513e3b593; + +/** + * @title BCoWPool + * @notice Inherits BPool contract and can trade on CoWSwap Protocol. + */ +contract BCoWPool is BPool, IERC1271, IBCoWPool { + using GPv2Order for GPv2Order.Data; + + /** + * @notice The address that can pull funds from the AMM vault to execute an order + */ + address public immutable VAULT_RELAYER; + + /** + * @notice The domain separator used for hashing CoW Protocol orders. + */ + bytes32 public immutable SOLUTION_SETTLER_DOMAIN_SEPARATOR; + + /** + * @notice The address of the CoW Protocol settlement contract. It is the + * only address that can set commitments. + */ + ISettlement public immutable SOLUTION_SETTLER; + + /** + * The hash of the data describing which `TradingParams` currently apply + * to this AMM. If this parameter is set to `NO_TRADING`, then the AMM + * does not accept any order as valid. + * If trading is enabled, then this value will be the [`hash`] of the only + * admissible [`TradingParams`]. + */ + bytes32 public appDataHash; + + constructor(address _cowSolutionSettler) BPool() { + SOLUTION_SETTLER = ISettlement(_cowSolutionSettler); + SOLUTION_SETTLER_DOMAIN_SEPARATOR = ISettlement(_cowSolutionSettler).domainSeparator(); + VAULT_RELAYER = ISettlement(_cowSolutionSettler).vaultRelayer(); + } + + /** + * @notice Once this function is called, it will be possible to trade with + * this AMM on CoW Protocol. + * @param appData Trading is enabled with the appData specified here. + */ + function enableTrading(bytes32 appData) external onlyController { + bytes32 _appDataHash = keccak256(abi.encode(appData)); + appDataHash = _appDataHash; + emit TradingEnabled(_appDataHash, appData); + } + + /** + * @notice Disable any form of trading on CoW Protocol by this AMM. + */ + function disableTrading() external onlyController { + appDataHash = NO_TRADING; + emit TradingDisabled(); + } + + /** + * @notice Restricts a specific AMM to being able to trade only the order + * with the specified hash. + * @dev The commitment is used to enforce that exactly one AMM order is + * valid when a CoW Protocol batch is settled. + * @param orderHash the order hash that will be enforced by the order + * verification function. + */ + function commit(bytes32 orderHash) external { + if (msg.sender != address(SOLUTION_SETTLER)) { + revert CommitOutsideOfSettlement(); + } + assembly ("memory-safe") { + tstore(COMMITMENT_SLOT, orderHash) + } + } + + /** + * @inheritdoc IERC1271 + * @dev this function reverts if the order hash does not match the current commitment + */ + function isValidSignature(bytes32 _hash, bytes memory signature) external view returns (bytes4) { + (GPv2Order.Data memory order) = abi.decode(signature, (GPv2Order.Data)); + + if (appDataHash != keccak256(abi.encode(order.appData))) revert AppDataDoNotMatchHash(); + bytes32 orderHash = order.hash(SOLUTION_SETTLER_DOMAIN_SEPARATOR); + if (orderHash != _hash) revert OrderDoesNotMatchMessageHash(); + + if (orderHash != commitment()) { + revert OrderDoesNotMatchCommitmentHash(); + } + + verify(order); + + // A signature is valid according to EIP-1271 if this function returns + // its selector as the so-called "magic value". + return this.isValidSignature.selector; + } + + function commitment() public view returns (bytes32 value) { + assembly ("memory-safe") { + value := tload(COMMITMENT_SLOT) + } + } + + /** + * @notice This function checks that the input order is admissible for the + * constant-product curve for the given trading parameters. + * @param order `GPv2Order.Data` of a discrete order to be verified. + */ + function verify(GPv2Order.Data memory order) public view { + Record memory inRecord = _records[address(order.sellToken)]; + Record memory outRecord = _records[address(order.buyToken)]; + + if (!inRecord.bound || !inRecord.bound) { + revert BPool_TokenNotBound(); + } + if (order.validTo >= block.timestamp + MAX_ORDER_DURATION) { + revert BCoWPool_OrderValidityTooLong(); + } + if (order.feeAmount != 0) { + revert BCoWPool_FeeMustBeZero(); + } + if (order.kind != GPv2Order.KIND_SELL) { + revert BCoWPool_InvalidOperation(); + } + if (order.buyTokenBalance != GPv2Order.BALANCE_ERC20 || order.sellTokenBalance != GPv2Order.BALANCE_ERC20) { + revert BCoWPool_InvalidBalanceMarker(); + } + + uint256 tokenAmountOut = calcOutGivenIn({ + tokenBalanceIn: order.sellToken.balanceOf(address(this)), + tokenWeightIn: inRecord.denorm, + tokenBalanceOut: order.buyToken.balanceOf(address(this)), + tokenWeightOut: outRecord.denorm, + tokenAmountIn: order.sellAmount, + swapFee: 0 + }); + + if (tokenAmountOut < order.buyAmount) { + revert BPool_TokenAmountOutBelowMinOut(); + } + } + + /** + * @notice Approves the spender to transfer an unlimited amount of tokens + * and reverts if the approval was unsuccessful. + * @param token The ERC-20 token to approve. + * @param spender The address that can transfer on behalf of this contract. + */ + function _approveUnlimited(IERC20 token, address spender) internal { + token.approve(spender, type(uint256).max); + } + + /** + * @inheritdoc BPool + * @dev Grants infinite approval to the vault relayer for all tokens in the + * pool after the finalization of the setup. + */ + function _afterFinalize() internal override { + for (uint256 i; i < _tokens.length; i++) { + _approveUnlimited(IERC20(_tokens[i]), VAULT_RELAYER); + } + } +} diff --git a/src/contracts/BPool.sol b/src/contracts/BPool.sol index 1fde5ea3..ab45098b 100644 --- a/src/contracts/BPool.sol +++ b/src/contracts/BPool.sol @@ -52,6 +52,16 @@ contract BPool is BToken, BMath, IBPool { _; } + /** + * @notice Throws an error if caller is not controller + */ + modifier onlyController() { + if (msg.sender != _controller) { + revert BPool_CallerIsNotController(); + } + _; + } + constructor() { _controller = msg.sender; _factory = msg.sender; @@ -60,13 +70,10 @@ contract BPool is BToken, BMath, IBPool { } /// @inheritdoc IBPool - function setSwapFee(uint256 swapFee) external _logs_ _lock_ { + function setSwapFee(uint256 swapFee) external _logs_ _lock_ onlyController { if (_finalized) { revert BPool_PoolIsFinalized(); } - if (msg.sender != _controller) { - revert BPool_CallerIsNotController(); - } if (swapFee < MIN_FEE) { revert BPool_FeeBelowMinimum(); } @@ -77,18 +84,12 @@ contract BPool is BToken, BMath, IBPool { } /// @inheritdoc IBPool - function setController(address manager) external _logs_ _lock_ { - if (msg.sender != _controller) { - revert BPool_CallerIsNotController(); - } + function setController(address manager) external _logs_ _lock_ onlyController { _controller = manager; } /// @inheritdoc IBPool - function finalize() external _logs_ _lock_ { - if (msg.sender != _controller) { - revert BPool_CallerIsNotController(); - } + function finalize() external _logs_ _lock_ onlyController { if (_finalized) { revert BPool_PoolIsFinalized(); } @@ -100,13 +101,11 @@ contract BPool is BToken, BMath, IBPool { _mintPoolShare(INIT_POOL_SUPPLY); _pushPoolShare(msg.sender, INIT_POOL_SUPPLY); + _afterFinalize(); } /// @inheritdoc IBPool - function bind(address token, uint256 balance, uint256 denorm) external _logs_ _lock_ { - if (msg.sender != _controller) { - revert BPool_CallerIsNotController(); - } + function bind(address token, uint256 balance, uint256 denorm) external _logs_ _lock_ onlyController { if (_records[token].bound) { revert BPool_TokenAlreadyBound(); } @@ -140,10 +139,7 @@ contract BPool is BToken, BMath, IBPool { } /// @inheritdoc IBPool - function unbind(address token) external _logs_ _lock_ { - if (msg.sender != _controller) { - revert BPool_CallerIsNotController(); - } + function unbind(address token) external _logs_ _lock_ onlyController { if (!_records[token].bound) { revert BPool_TokenNotBound(); } @@ -609,9 +605,9 @@ contract BPool is BToken, BMath, IBPool { /** * @dev Pulls tokens from the sender. Tokens needs to be approved first. Calls are not locked. - * @param erc20 address of the token to pull - * @param from address to pull the tokens from - * @param amount amount of tokens to pull + * @param erc20 The address of the token to pull + * @param from The address to pull the tokens from + * @param amount The amount of tokens to pull */ function _pullUnderlying(address erc20, address from, uint256 amount) internal virtual { bool xfer = IERC20(erc20).transferFrom(from, address(this), amount); @@ -622,9 +618,9 @@ contract BPool is BToken, BMath, IBPool { /** * @dev Pushes tokens to the receiver. Calls are not locked. - * @param erc20 address of the token to push - * @param to address to push the tokens to - * @param amount amount of tokens to push + * @param erc20 The address of the token to push + * @param to The address to push the tokens to + * @param amount The amount of tokens to push */ function _pushUnderlying(address erc20, address to, uint256 amount) internal virtual { bool xfer = IERC20(erc20).transfer(to, amount); @@ -633,10 +629,17 @@ contract BPool is BToken, BMath, IBPool { } } + /** + * @dev Hook for extensions to execute custom logic when a pool is finalized, + * e.g. Setting infinite allowance on BCoWPool + */ + // solhint-disable-next-line no-empty-blocks + function _afterFinalize() internal virtual {} + /** * @dev Pulls pool tokens from the sender. - * @param from address to pull the pool tokens from - * @param amount amount of pool tokens to pull + * @param from The address to pull the pool tokens from + * @param amount The amount of pool tokens to pull */ function _pullPoolShare(address from, uint256 amount) internal { _pull(from, amount); @@ -644,8 +647,8 @@ contract BPool is BToken, BMath, IBPool { /** * @dev Pushes pool tokens to the receiver. - * @param to address to push the pool tokens to - * @param amount amount of pool tokens to push + * @param to The address to push the pool tokens to + * @param amount The amount of pool tokens to push */ function _pushPoolShare(address to, uint256 amount) internal { _push(to, amount); @@ -653,7 +656,7 @@ contract BPool is BToken, BMath, IBPool { /** * @dev Mints an amount of pool tokens. - * @param amount amount of pool tokens to mint + * @param amount The amount of pool tokens to mint */ function _mintPoolShare(uint256 amount) internal { _mint(address(this), amount); @@ -661,7 +664,7 @@ contract BPool is BToken, BMath, IBPool { /** * @dev Burns an amount of pool tokens. - * @param amount amount of pool tokens to burn + * @param amount The amount of pool tokens to burn */ function _burnPoolShare(uint256 amount) internal { _burn(address(this), amount); diff --git a/src/interfaces/IBCoWPool.sol b/src/interfaces/IBCoWPool.sol new file mode 100644 index 00000000..c5ba5cf3 --- /dev/null +++ b/src/interfaces/IBCoWPool.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.25; + +import {IERC1271} from '@openzeppelin/contracts/interfaces/IERC1271.sol'; + +interface IBCoWPool is IERC1271 { + /** + * Emitted when the manager disables all trades by the AMM. Existing open + * order will not be tradeable. Note that the AMM could resume trading with + * different parameters at a later point. + */ + event TradingDisabled(); + + /** + * Emitted when the manager enables the AMM to trade on CoW Protocol. + * @param hash The hash of the trading parameters. + * @param appData Trading has been enabled for this appData. + */ + event TradingEnabled(bytes32 indexed hash, bytes32 appData); + + /** + * @notice thrown when a CoW order has a non-zero fee + */ + error BCoWPool_FeeMustBeZero(); + + /** + * @notice thrown when a CoW order is executed after its deadline + */ + error BCoWPool_OrderValidityTooLong(); + + /** + * @notice thrown when a CoW order has an unkown type (must be GPv2Order.KIND_SELL) + */ + error BCoWPool_InvalidOperation(); + + /** + * @notice thrown when a CoW order has an invalid balance marker. BCoWPool + * only supports BALANCE_ERC20, instructing to use the underlying ERC20 + * balance directly instead of balancer's internal accounting + */ + error BCoWPool_InvalidBalanceMarker(); + + /** + * @notice The `commit` function can only be called inside a CoW Swap + * settlement. This error is thrown when the function is called from another + * context. + */ + error CommitOutsideOfSettlement(); + + /** + * @notice Error thrown when a solver tries to settle an AMM order on CoW + * Protocol whose hash doesn't match the one that has been committed to. + */ + error OrderDoesNotMatchCommitmentHash(); + + /** + * @notice On signature verification, the hash of the order supplied as part + * of the signature does not match the provided message hash. + * This usually means that the verification function is being provided a + * signature that belongs to a different order. + */ + error OrderDoesNotMatchMessageHash(); + + /** + * @notice The order trade parameters that were provided during signature + * verification does not match the data stored in this contract _or_ the + * AMM has not enabled trading. + */ + error AppDataDoNotMatchHash(); +} diff --git a/src/interfaces/ISettlement.sol b/src/interfaces/ISettlement.sol new file mode 100644 index 00000000..99e56dae --- /dev/null +++ b/src/interfaces/ISettlement.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.25; + +interface ISettlement { + /** + * @return The domain separator for IERC1271 signature + * @dev Immutable value, would not change on chain forks + */ + function domainSeparator() external view returns (bytes32); + + /** + * @return The address that'll use the pool liquidity in CoWprotocol swaps + * @dev Address that will transfer and transferFrom the pool. Has an infinite allowance. + */ + function vaultRelayer() external view returns (address); +} diff --git a/test/manual-smock/MockBCoWPool.sol b/test/manual-smock/MockBCoWPool.sol new file mode 100644 index 00000000..dd914b77 --- /dev/null +++ b/test/manual-smock/MockBCoWPool.sol @@ -0,0 +1,364 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.0; + +import {BCoWPool, BPool, GPv2Order, IBCoWPool, IERC1271, IERC20, ISettlement} from '../../src/contracts/BCoWPool.sol'; +import {BMath, IBPool} from '../../src/contracts/BPool.sol'; +import {Test} from 'forge-std/Test.sol'; + +contract MockBCoWPool is BCoWPool, Test { + /// MockBCoWPool mock methods + function set_appDataHash(bytes32 _appDataHash) public { + appDataHash = _appDataHash; + } + + function mock_call_appDataHash(bytes32 _value) public { + vm.mockCall(address(this), abi.encodeWithSignature('appDataHash()'), abi.encode(_value)); + } + + constructor(address _cowSolutionSettler) BCoWPool(_cowSolutionSettler) {} + + function mock_call_enableTrading(bytes32 appData) public { + vm.mockCall(address(this), abi.encodeWithSignature('enableTrading(bytes32)', appData), abi.encode()); + } + + function mock_call_disableTrading() public { + vm.mockCall(address(this), abi.encodeWithSignature('disableTrading()'), abi.encode()); + } + + function mock_call_commit(bytes32 orderHash) public { + vm.mockCall(address(this), abi.encodeWithSignature('commit(bytes32)', orderHash), abi.encode()); + } + + function mock_call_isValidSignature(bytes32 _hash, bytes memory signature, bytes4 _returnParam0) public { + vm.mockCall( + address(this), + abi.encodeWithSignature('isValidSignature(bytes32,bytes)', _hash, signature), + abi.encode(_returnParam0) + ); + } + + function mock_call_commitment(bytes32 value) public { + vm.mockCall(address(this), abi.encodeWithSignature('commitment()'), abi.encode(value)); + } + + function mock_call_verify(GPv2Order.Data memory order) public { + vm.mockCall(address(this), abi.encodeWithSignature('verify(GPv2Order.Data)', order), abi.encode()); + } + + function mock_call_hash(bytes32 appData, bytes32 _returnParam0) public { + vm.mockCall(address(this), abi.encodeWithSignature('hash(bytes32)', appData), abi.encode(_returnParam0)); + } + /// BPool Mocked methods + + function set__mutex(bool __mutex) public { + _mutex = __mutex; + } + + function call__mutex() public view returns (bool) { + return _mutex; + } + + function set__factory(address __factory) public { + _factory = __factory; + } + + function call__factory() public view returns (address) { + return _factory; + } + + function set__controller(address __controller) public { + _controller = __controller; + } + + function call__controller() public view returns (address) { + return _controller; + } + + function set__swapFee(uint256 __swapFee) public { + _swapFee = __swapFee; + } + + function call__swapFee() public view returns (uint256) { + return _swapFee; + } + + function set__finalized(bool __finalized) public { + _finalized = __finalized; + } + + function call__finalized() public view returns (bool) { + return _finalized; + } + + function set__tokens(address[] memory __tokens) public { + _tokens = __tokens; + } + + function call__tokens() public view returns (address[] memory) { + return _tokens; + } + + function set__records(address _key0, IBPool.Record memory _value) public { + _records[_key0] = _value; + } + + function call__records(address _key0) public view returns (IBPool.Record memory) { + return _records[_key0]; + } + + function set__totalWeight(uint256 __totalWeight) public { + _totalWeight = __totalWeight; + } + + function call__totalWeight() public view returns (uint256) { + return _totalWeight; + } + + function mock_call_setSwapFee(uint256 swapFee) public { + vm.mockCall(address(this), abi.encodeWithSignature('setSwapFee(uint256)', swapFee), abi.encode()); + } + + function mock_call_setController(address manager) public { + vm.mockCall(address(this), abi.encodeWithSignature('setController(address)', manager), abi.encode()); + } + + function mock_call_finalize() public { + vm.mockCall(address(this), abi.encodeWithSignature('finalize()'), abi.encode()); + } + + function mock_call_bind(address token, uint256 balance, uint256 denorm) public { + vm.mockCall( + address(this), abi.encodeWithSignature('bind(address,uint256,uint256)', token, balance, denorm), abi.encode() + ); + } + + function mock_call_unbind(address token) public { + vm.mockCall(address(this), abi.encodeWithSignature('unbind(address)', token), abi.encode()); + } + + function mock_call_joinPool(uint256 poolAmountOut, uint256[] calldata maxAmountsIn) public { + vm.mockCall( + address(this), abi.encodeWithSignature('joinPool(uint256,uint256[])', poolAmountOut, maxAmountsIn), abi.encode() + ); + } + + function mock_call_exitPool(uint256 poolAmountIn, uint256[] calldata minAmountsOut) public { + vm.mockCall( + address(this), abi.encodeWithSignature('exitPool(uint256,uint256[])', poolAmountIn, minAmountsOut), abi.encode() + ); + } + + function mock_call_swapExactAmountIn( + address tokenIn, + uint256 tokenAmountIn, + address tokenOut, + uint256 minAmountOut, + uint256 maxPrice, + uint256 tokenAmountOut, + uint256 spotPriceAfter + ) public { + vm.mockCall( + address(this), + abi.encodeWithSignature( + 'swapExactAmountIn(address,uint256,address,uint256,uint256)', + tokenIn, + tokenAmountIn, + tokenOut, + minAmountOut, + maxPrice + ), + abi.encode(tokenAmountOut, spotPriceAfter) + ); + } + + function mock_call_swapExactAmountOut( + address tokenIn, + uint256 maxAmountIn, + address tokenOut, + uint256 tokenAmountOut, + uint256 maxPrice, + uint256 tokenAmountIn, + uint256 spotPriceAfter + ) public { + vm.mockCall( + address(this), + abi.encodeWithSignature( + 'swapExactAmountOut(address,uint256,address,uint256,uint256)', + tokenIn, + maxAmountIn, + tokenOut, + tokenAmountOut, + maxPrice + ), + abi.encode(tokenAmountIn, spotPriceAfter) + ); + } + + function mock_call_joinswapExternAmountIn( + address tokenIn, + uint256 tokenAmountIn, + uint256 minPoolAmountOut, + uint256 poolAmountOut + ) public { + vm.mockCall( + address(this), + abi.encodeWithSignature( + 'joinswapExternAmountIn(address,uint256,uint256)', tokenIn, tokenAmountIn, minPoolAmountOut + ), + abi.encode(poolAmountOut) + ); + } + + function mock_call_joinswapPoolAmountOut( + address tokenIn, + uint256 poolAmountOut, + uint256 maxAmountIn, + uint256 tokenAmountIn + ) public { + vm.mockCall( + address(this), + abi.encodeWithSignature('joinswapPoolAmountOut(address,uint256,uint256)', tokenIn, poolAmountOut, maxAmountIn), + abi.encode(tokenAmountIn) + ); + } + + function mock_call_exitswapPoolAmountIn( + address tokenOut, + uint256 poolAmountIn, + uint256 minAmountOut, + uint256 tokenAmountOut + ) public { + vm.mockCall( + address(this), + abi.encodeWithSignature('exitswapPoolAmountIn(address,uint256,uint256)', tokenOut, poolAmountIn, minAmountOut), + abi.encode(tokenAmountOut) + ); + } + + function mock_call_exitswapExternAmountOut( + address tokenOut, + uint256 tokenAmountOut, + uint256 maxPoolAmountIn, + uint256 poolAmountIn + ) public { + vm.mockCall( + address(this), + abi.encodeWithSignature( + 'exitswapExternAmountOut(address,uint256,uint256)', tokenOut, tokenAmountOut, maxPoolAmountIn + ), + abi.encode(poolAmountIn) + ); + } + + function mock_call_getSpotPrice(address tokenIn, address tokenOut, uint256 spotPrice) public { + vm.mockCall( + address(this), abi.encodeWithSignature('getSpotPrice(address,address)', tokenIn, tokenOut), abi.encode(spotPrice) + ); + } + + function mock_call_getSpotPriceSansFee(address tokenIn, address tokenOut, uint256 spotPrice) public { + vm.mockCall( + address(this), + abi.encodeWithSignature('getSpotPriceSansFee(address,address)', tokenIn, tokenOut), + abi.encode(spotPrice) + ); + } + + function mock_call_isFinalized(bool _returnParam0) public { + vm.mockCall(address(this), abi.encodeWithSignature('isFinalized()'), abi.encode(_returnParam0)); + } + + function mock_call_isBound(address t, bool _returnParam0) public { + vm.mockCall(address(this), abi.encodeWithSignature('isBound(address)', t), abi.encode(_returnParam0)); + } + + function mock_call_getNumTokens(uint256 _returnParam0) public { + vm.mockCall(address(this), abi.encodeWithSignature('getNumTokens()'), abi.encode(_returnParam0)); + } + + function mock_call_getCurrentTokens(address[] memory tokens) public { + vm.mockCall(address(this), abi.encodeWithSignature('getCurrentTokens()'), abi.encode(tokens)); + } + + function mock_call_getFinalTokens(address[] memory tokens) public { + vm.mockCall(address(this), abi.encodeWithSignature('getFinalTokens()'), abi.encode(tokens)); + } + + function mock_call_getDenormalizedWeight(address token, uint256 _returnParam0) public { + vm.mockCall( + address(this), abi.encodeWithSignature('getDenormalizedWeight(address)', token), abi.encode(_returnParam0) + ); + } + + function mock_call_getTotalDenormalizedWeight(uint256 _returnParam0) public { + vm.mockCall(address(this), abi.encodeWithSignature('getTotalDenormalizedWeight()'), abi.encode(_returnParam0)); + } + + function mock_call_getNormalizedWeight(address token, uint256 _returnParam0) public { + vm.mockCall( + address(this), abi.encodeWithSignature('getNormalizedWeight(address)', token), abi.encode(_returnParam0) + ); + } + + function mock_call_getBalance(address token, uint256 _returnParam0) public { + vm.mockCall(address(this), abi.encodeWithSignature('getBalance(address)', token), abi.encode(_returnParam0)); + } + + function mock_call_getSwapFee(uint256 _returnParam0) public { + vm.mockCall(address(this), abi.encodeWithSignature('getSwapFee()'), abi.encode(_returnParam0)); + } + + function mock_call_getController(address _returnParam0) public { + vm.mockCall(address(this), abi.encodeWithSignature('getController()'), abi.encode(_returnParam0)); + } + + function mock_call__pullUnderlying(address erc20, address from, uint256 amount) public { + vm.mockCall( + address(this), + abi.encodeWithSignature('_pullUnderlying(address,address,uint256)', erc20, from, amount), + abi.encode() + ); + } + + function _pullUnderlying(address erc20, address from, uint256 amount) internal override { + (bool _success, bytes memory _data) = + address(this).call(abi.encodeWithSignature('_pullUnderlying(address,address,uint256)', erc20, from, amount)); + + if (_success) return abi.decode(_data, ()); + else return super._pullUnderlying(erc20, from, amount); + } + + function call__pullUnderlying(address erc20, address from, uint256 amount) public { + return _pullUnderlying(erc20, from, amount); + } + + function expectCall__pullUnderlying(address erc20, address from, uint256 amount) public { + vm.expectCall( + address(this), abi.encodeWithSignature('_pullUnderlying(address,address,uint256)', erc20, from, amount) + ); + } + + function mock_call__pushUnderlying(address erc20, address to, uint256 amount) public { + vm.mockCall( + address(this), + abi.encodeWithSignature('_pushUnderlying(address,address,uint256)', erc20, to, amount), + abi.encode() + ); + } + + function _pushUnderlying(address erc20, address to, uint256 amount) internal override { + (bool _success, bytes memory _data) = + address(this).call(abi.encodeWithSignature('_pushUnderlying(address,address,uint256)', erc20, to, amount)); + + if (_success) return abi.decode(_data, ()); + else return super._pushUnderlying(erc20, to, amount); + } + + function call__pushUnderlying(address erc20, address to, uint256 amount) public { + return _pushUnderlying(erc20, to, amount); + } + + function expectCall__pushUnderlying(address erc20, address to, uint256 amount) public { + vm.expectCall(address(this), abi.encodeWithSignature('_pushUnderlying(address,address,uint256)', erc20, to, amount)); + } +} diff --git a/test/smock/MockBFactory.sol b/test/smock/MockBFactory.sol new file mode 100644 index 00000000..6ba92879 --- /dev/null +++ b/test/smock/MockBFactory.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.0; + +import {BFactory, BPool, IBFactory, IBPool} from '../../src/contracts/BFactory.sol'; +import {Test} from 'forge-std/Test.sol'; + +contract MockBFactory is BFactory, Test { + 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; + } + + constructor() BFactory() {} + + function mock_call_newBPool(IBPool _pool) public { + vm.mockCall(address(this), abi.encodeWithSignature('newBPool()'), abi.encode(_pool)); + } + + function mock_call_setBLabs(address b) public { + vm.mockCall(address(this), abi.encodeWithSignature('setBLabs(address)', b), abi.encode()); + } + + function mock_call_collect(IBPool pool) public { + vm.mockCall(address(this), abi.encodeWithSignature('collect(IBPool)', pool), abi.encode()); + } + + function mock_call_isBPool(address b, bool _returnParam0) public { + vm.mockCall(address(this), abi.encodeWithSignature('isBPool(address)', b), abi.encode(_returnParam0)); + } + + function mock_call_getBLabs(address _returnParam0) public { + vm.mockCall(address(this), abi.encodeWithSignature('getBLabs()'), abi.encode(_returnParam0)); + } +} diff --git a/test/smock/MockBPool.sol b/test/smock/MockBPool.sol new file mode 100644 index 00000000..24f3912a --- /dev/null +++ b/test/smock/MockBPool.sol @@ -0,0 +1,340 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.0; + +import {BMath, BPool, BToken, IBPool, IERC20} from '../../src/contracts/BPool.sol'; +import {Test} from 'forge-std/Test.sol'; + +contract MockBPool is BPool, Test { + function set__mutex(bool __mutex) public { + _mutex = __mutex; + } + + function call__mutex() public view returns (bool) { + return _mutex; + } + + function set__factory(address __factory) public { + _factory = __factory; + } + + function call__factory() public view returns (address) { + return _factory; + } + + function set__controller(address __controller) public { + _controller = __controller; + } + + function call__controller() public view returns (address) { + return _controller; + } + + function set__swapFee(uint256 __swapFee) public { + _swapFee = __swapFee; + } + + function call__swapFee() public view returns (uint256) { + return _swapFee; + } + + function set__finalized(bool __finalized) public { + _finalized = __finalized; + } + + function call__finalized() public view returns (bool) { + return _finalized; + } + + function set__tokens(address[] memory __tokens) public { + _tokens = __tokens; + } + + function call__tokens() public view returns (address[] memory) { + return _tokens; + } + + function set__records(address _key0, IBPool.Record memory _value) public { + _records[_key0] = _value; + } + + function call__records(address _key0) public view returns (IBPool.Record memory) { + return _records[_key0]; + } + + function set__totalWeight(uint256 __totalWeight) public { + _totalWeight = __totalWeight; + } + + function call__totalWeight() public view returns (uint256) { + return _totalWeight; + } + + constructor() BPool() {} + + function mock_call_setSwapFee(uint256 swapFee) public { + vm.mockCall(address(this), abi.encodeWithSignature('setSwapFee(uint256)', swapFee), abi.encode()); + } + + function mock_call_setController(address manager) public { + vm.mockCall(address(this), abi.encodeWithSignature('setController(address)', manager), abi.encode()); + } + + function mock_call_finalize() public { + vm.mockCall(address(this), abi.encodeWithSignature('finalize()'), abi.encode()); + } + + function mock_call_bind(address token, uint256 balance, uint256 denorm) public { + vm.mockCall( + address(this), abi.encodeWithSignature('bind(address,uint256,uint256)', token, balance, denorm), abi.encode() + ); + } + + function mock_call_unbind(address token) public { + vm.mockCall(address(this), abi.encodeWithSignature('unbind(address)', token), abi.encode()); + } + + function mock_call_joinPool(uint256 poolAmountOut, uint256[] calldata maxAmountsIn) public { + vm.mockCall( + address(this), abi.encodeWithSignature('joinPool(uint256,uint256[])', poolAmountOut, maxAmountsIn), abi.encode() + ); + } + + function mock_call_exitPool(uint256 poolAmountIn, uint256[] calldata minAmountsOut) public { + vm.mockCall( + address(this), abi.encodeWithSignature('exitPool(uint256,uint256[])', poolAmountIn, minAmountsOut), abi.encode() + ); + } + + function mock_call_swapExactAmountIn( + address tokenIn, + uint256 tokenAmountIn, + address tokenOut, + uint256 minAmountOut, + uint256 maxPrice, + uint256 tokenAmountOut, + uint256 spotPriceAfter + ) public { + vm.mockCall( + address(this), + abi.encodeWithSignature( + 'swapExactAmountIn(address,uint256,address,uint256,uint256)', + tokenIn, + tokenAmountIn, + tokenOut, + minAmountOut, + maxPrice + ), + abi.encode(tokenAmountOut, spotPriceAfter) + ); + } + + function mock_call_swapExactAmountOut( + address tokenIn, + uint256 maxAmountIn, + address tokenOut, + uint256 tokenAmountOut, + uint256 maxPrice, + uint256 tokenAmountIn, + uint256 spotPriceAfter + ) public { + vm.mockCall( + address(this), + abi.encodeWithSignature( + 'swapExactAmountOut(address,uint256,address,uint256,uint256)', + tokenIn, + maxAmountIn, + tokenOut, + tokenAmountOut, + maxPrice + ), + abi.encode(tokenAmountIn, spotPriceAfter) + ); + } + + function mock_call_joinswapExternAmountIn( + address tokenIn, + uint256 tokenAmountIn, + uint256 minPoolAmountOut, + uint256 poolAmountOut + ) public { + vm.mockCall( + address(this), + abi.encodeWithSignature( + 'joinswapExternAmountIn(address,uint256,uint256)', tokenIn, tokenAmountIn, minPoolAmountOut + ), + abi.encode(poolAmountOut) + ); + } + + function mock_call_joinswapPoolAmountOut( + address tokenIn, + uint256 poolAmountOut, + uint256 maxAmountIn, + uint256 tokenAmountIn + ) public { + vm.mockCall( + address(this), + abi.encodeWithSignature('joinswapPoolAmountOut(address,uint256,uint256)', tokenIn, poolAmountOut, maxAmountIn), + abi.encode(tokenAmountIn) + ); + } + + function mock_call_exitswapPoolAmountIn( + address tokenOut, + uint256 poolAmountIn, + uint256 minAmountOut, + uint256 tokenAmountOut + ) public { + vm.mockCall( + address(this), + abi.encodeWithSignature('exitswapPoolAmountIn(address,uint256,uint256)', tokenOut, poolAmountIn, minAmountOut), + abi.encode(tokenAmountOut) + ); + } + + function mock_call_exitswapExternAmountOut( + address tokenOut, + uint256 tokenAmountOut, + uint256 maxPoolAmountIn, + uint256 poolAmountIn + ) public { + vm.mockCall( + address(this), + abi.encodeWithSignature( + 'exitswapExternAmountOut(address,uint256,uint256)', tokenOut, tokenAmountOut, maxPoolAmountIn + ), + abi.encode(poolAmountIn) + ); + } + + function mock_call_getSpotPrice(address tokenIn, address tokenOut, uint256 spotPrice) public { + vm.mockCall( + address(this), abi.encodeWithSignature('getSpotPrice(address,address)', tokenIn, tokenOut), abi.encode(spotPrice) + ); + } + + function mock_call_getSpotPriceSansFee(address tokenIn, address tokenOut, uint256 spotPrice) public { + vm.mockCall( + address(this), + abi.encodeWithSignature('getSpotPriceSansFee(address,address)', tokenIn, tokenOut), + abi.encode(spotPrice) + ); + } + + function mock_call_isFinalized(bool _returnParam0) public { + vm.mockCall(address(this), abi.encodeWithSignature('isFinalized()'), abi.encode(_returnParam0)); + } + + function mock_call_isBound(address t, bool _returnParam0) public { + vm.mockCall(address(this), abi.encodeWithSignature('isBound(address)', t), abi.encode(_returnParam0)); + } + + function mock_call_getNumTokens(uint256 _returnParam0) public { + vm.mockCall(address(this), abi.encodeWithSignature('getNumTokens()'), abi.encode(_returnParam0)); + } + + function mock_call_getCurrentTokens(address[] memory tokens) public { + vm.mockCall(address(this), abi.encodeWithSignature('getCurrentTokens()'), abi.encode(tokens)); + } + + function mock_call_getFinalTokens(address[] memory tokens) public { + vm.mockCall(address(this), abi.encodeWithSignature('getFinalTokens()'), abi.encode(tokens)); + } + + function mock_call_getDenormalizedWeight(address token, uint256 _returnParam0) public { + vm.mockCall( + address(this), abi.encodeWithSignature('getDenormalizedWeight(address)', token), abi.encode(_returnParam0) + ); + } + + function mock_call_getTotalDenormalizedWeight(uint256 _returnParam0) public { + vm.mockCall(address(this), abi.encodeWithSignature('getTotalDenormalizedWeight()'), abi.encode(_returnParam0)); + } + + function mock_call_getNormalizedWeight(address token, uint256 _returnParam0) public { + vm.mockCall( + address(this), abi.encodeWithSignature('getNormalizedWeight(address)', token), abi.encode(_returnParam0) + ); + } + + function mock_call_getBalance(address token, uint256 _returnParam0) public { + vm.mockCall(address(this), abi.encodeWithSignature('getBalance(address)', token), abi.encode(_returnParam0)); + } + + function mock_call_getSwapFee(uint256 _returnParam0) public { + vm.mockCall(address(this), abi.encodeWithSignature('getSwapFee()'), abi.encode(_returnParam0)); + } + + function mock_call_getController(address _returnParam0) public { + vm.mockCall(address(this), abi.encodeWithSignature('getController()'), abi.encode(_returnParam0)); + } + + function mock_call__pullUnderlying(address erc20, address from, uint256 amount) public { + vm.mockCall( + address(this), + abi.encodeWithSignature('_pullUnderlying(address,address,uint256)', erc20, from, amount), + abi.encode() + ); + } + + function _pullUnderlying(address erc20, address from, uint256 amount) internal override { + (bool _success, bytes memory _data) = + address(this).call(abi.encodeWithSignature('_pullUnderlying(address,address,uint256)', erc20, from, amount)); + + if (_success) return abi.decode(_data, ()); + else return super._pullUnderlying(erc20, from, amount); + } + + function call__pullUnderlying(address erc20, address from, uint256 amount) public { + return _pullUnderlying(erc20, from, amount); + } + + function expectCall__pullUnderlying(address erc20, address from, uint256 amount) public { + vm.expectCall( + address(this), abi.encodeWithSignature('_pullUnderlying(address,address,uint256)', erc20, from, amount) + ); + } + + function mock_call__pushUnderlying(address erc20, address to, uint256 amount) public { + vm.mockCall( + address(this), + abi.encodeWithSignature('_pushUnderlying(address,address,uint256)', erc20, to, amount), + abi.encode() + ); + } + + function _pushUnderlying(address erc20, address to, uint256 amount) internal override { + (bool _success, bytes memory _data) = + address(this).call(abi.encodeWithSignature('_pushUnderlying(address,address,uint256)', erc20, to, amount)); + + if (_success) return abi.decode(_data, ()); + else return super._pushUnderlying(erc20, to, amount); + } + + function call__pushUnderlying(address erc20, address to, uint256 amount) public { + return _pushUnderlying(erc20, to, amount); + } + + function expectCall__pushUnderlying(address erc20, address to, uint256 amount) public { + vm.expectCall(address(this), abi.encodeWithSignature('_pushUnderlying(address,address,uint256)', erc20, to, amount)); + } + + function mock_call__afterFinalize() public { + vm.mockCall(address(this), abi.encodeWithSignature('_afterFinalize()'), abi.encode()); + } + + function _afterFinalize() internal override { + (bool _success, bytes memory _data) = address(this).call(abi.encodeWithSignature('_afterFinalize()')); + + if (_success) return abi.decode(_data, ()); + else return super._afterFinalize(); + } + + function call__afterFinalize() public { + return _afterFinalize(); + } + + function expectCall__afterFinalize() public { + vm.expectCall(address(this), abi.encodeWithSignature('_afterFinalize()')); + } +} diff --git a/test/smock/MockBToken.sol b/test/smock/MockBToken.sol new file mode 100644 index 00000000..697d4069 --- /dev/null +++ b/test/smock/MockBToken.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.0; + +import {BToken, ERC20} from '../../src/contracts/BToken.sol'; +import {Test} from 'forge-std/Test.sol'; + +contract MockBToken is BToken, Test { + constructor() BToken() {} + + function mock_call_increaseApproval(address dst, uint256 amt, bool _returnParam0) public { + vm.mockCall( + address(this), abi.encodeWithSignature('increaseApproval(address,uint256)', dst, amt), abi.encode(_returnParam0) + ); + } + + function mock_call_decreaseApproval(address dst, uint256 amt, bool _returnParam0) public { + vm.mockCall( + address(this), abi.encodeWithSignature('decreaseApproval(address,uint256)', dst, amt), abi.encode(_returnParam0) + ); + } + + function mock_call__push(address to, uint256 amt) public { + vm.mockCall(address(this), abi.encodeWithSignature('_push(address,uint256)', to, amt), abi.encode()); + } + + function _push(address to, uint256 amt) internal override { + (bool _success, bytes memory _data) = address(this).call(abi.encodeWithSignature('_push(address,uint256)', to, amt)); + + if (_success) return abi.decode(_data, ()); + else return super._push(to, amt); + } + + function call__push(address to, uint256 amt) public { + return _push(to, amt); + } + + function expectCall__push(address to, uint256 amt) public { + vm.expectCall(address(this), abi.encodeWithSignature('_push(address,uint256)', to, amt)); + } + + function mock_call__pull(address from, uint256 amt) public { + vm.mockCall(address(this), abi.encodeWithSignature('_pull(address,uint256)', from, amt), abi.encode()); + } + + function _pull(address from, uint256 amt) internal override { + (bool _success, bytes memory _data) = + address(this).call(abi.encodeWithSignature('_pull(address,uint256)', from, amt)); + + if (_success) return abi.decode(_data, ()); + else return super._pull(from, amt); + } + + function call__pull(address from, uint256 amt) public { + return _pull(from, amt); + } + + function expectCall__pull(address from, uint256 amt) public { + vm.expectCall(address(this), abi.encodeWithSignature('_pull(address,uint256)', from, amt)); + } +} diff --git a/test/unit/BCoWPool.t.sol b/test/unit/BCoWPool.t.sol new file mode 100644 index 00000000..5671c0ea --- /dev/null +++ b/test/unit/BCoWPool.t.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; + +import {BasePoolTest} from './BPool.t.sol'; +import {ISettlement} from 'interfaces/ISettlement.sol'; +import {MockBCoWPool} from 'test/manual-smock/MockBCoWPool.sol'; +import {MockBPool} from 'test/smock/MockBPool.sol'; + +abstract contract BaseCoWPoolTest is BasePoolTest { + address public cowSolutionSettler = makeAddr('cowSolutionSettler'); + bytes32 public domainSeparator = bytes32(bytes2(0xf00b)); + address public vaultRelayer = makeAddr('vaultRelayer'); + + function setUp() public override { + super.setUp(); + vm.mockCall(cowSolutionSettler, abi.encodePacked(ISettlement.domainSeparator.selector), abi.encode(domainSeparator)); + vm.mockCall(cowSolutionSettler, abi.encodePacked(ISettlement.vaultRelayer.selector), abi.encode(vaultRelayer)); + bPool = MockBPool(address(new MockBCoWPool(cowSolutionSettler))); + } +} + +contract BCoWPool_Unit_Constructor is BaseCoWPoolTest { + function test_Set_SolutionSettler(address _settler) public { + assumeNotForgeAddress(_settler); + vm.mockCall(_settler, abi.encodePacked(ISettlement.domainSeparator.selector), abi.encode(domainSeparator)); + vm.mockCall(_settler, abi.encodePacked(ISettlement.vaultRelayer.selector), abi.encode(vaultRelayer)); + MockBCoWPool pool = new MockBCoWPool(_settler); + assertEq(address(pool.SOLUTION_SETTLER()), _settler); + } + + function test_Set_DomainSeparator(address _settler, bytes32 _separator) public { + assumeNotForgeAddress(_settler); + vm.mockCall(_settler, abi.encodePacked(ISettlement.domainSeparator.selector), abi.encode(_separator)); + vm.mockCall(_settler, abi.encodePacked(ISettlement.vaultRelayer.selector), abi.encode(vaultRelayer)); + MockBCoWPool pool = new MockBCoWPool(_settler); + assertEq(pool.SOLUTION_SETTLER_DOMAIN_SEPARATOR(), _separator); + } + + function test_Set_VaultRelayer(address _settler, address _relayer) public { + assumeNotForgeAddress(_settler); + vm.mockCall(_settler, abi.encodePacked(ISettlement.domainSeparator.selector), abi.encode(domainSeparator)); + vm.mockCall(_settler, abi.encodePacked(ISettlement.vaultRelayer.selector), abi.encode(_relayer)); + MockBCoWPool pool = new MockBCoWPool(_settler); + assertEq(pool.VAULT_RELAYER(), _relayer); + } +} + +contract BCoWPool_Unit_Finalize is BaseCoWPoolTest { + function test_setsApprovals(uint256 _tokensLength) public { + _tokensLength = bound(_tokensLength, MIN_BOUND_TOKENS, MAX_BOUND_TOKENS); + _setRandomTokens(_tokensLength); + address[] memory tokens = _getDeterministicTokenArray(_tokensLength); + for (uint256 i = 0; i < bPool.getNumTokens(); i++) { + vm.mockCall(tokens[i], abi.encodePacked(IERC20.approve.selector), abi.encode(true)); + vm.expectCall(tokens[i], abi.encodeCall(IERC20.approve, (vaultRelayer, type(uint256).max)), 1); + } + bPool.finalize(); + } +} diff --git a/test/unit/BPool.t.sol b/test/unit/BPool.t.sol index f3f96fe1..56ca970b 100644 --- a/test/unit/BPool.t.sol +++ b/test/unit/BPool.t.sol @@ -20,7 +20,7 @@ abstract contract BasePoolTest is Test, BConst, Utils, BMath { // If the call fails, it means that the function overflowed, then we reject the fuzzed inputs Pow public pow = new Pow(); - function setUp() public { + function setUp() public virtual { bPool = new MockBPool(); // Create fake tokens @@ -558,6 +558,11 @@ contract BPool_Unit_Finalize is BasePoolTest { assertEq(bPool.call__finalized(), true); } + 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(); diff --git a/yarn.lock b/yarn.lock index 362a5ade..efe07ba0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -181,6 +181,10 @@ "@types/conventional-commits-parser" "^5.0.0" chalk "^5.3.0" +"@cowprotocol/contracts@github:cowprotocol/contracts.git#a10f40788a": + version "1.6.0" + resolved "https://codeload.github.com/cowprotocol/contracts/tar.gz/a10f40788af29467e87de3dbf2196662b0a6b500" + "@defi-wonderland/natspec-smells@1.1.1": version "1.1.1" resolved "https://registry.yarnpkg.com/@defi-wonderland/natspec-smells/-/natspec-smells-1.1.1.tgz#1eed85765ebe4799f8861247308d6365dbf6dbaa" From 718009c733f41ab78fa2612eafb0b534cdf96ef3 Mon Sep 17 00:00:00 2001 From: Austrian <114922365+0xAustrian@users.noreply.github.com> Date: Wed, 12 Jun 2024 22:50:00 -0300 Subject: [PATCH 03/32] fix: add missing parts in IBCowPool (#84) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: add missing parts * fix: move natspec to interface * fix: add solhint-disable-next-line * feat: move cow constants (#85) * feat: moving cow constants to BCoWConst * fix: comment in interface --------- Co-authored-by: Weißer Hase --- src/contracts/BCoWConst.sol | 36 ++++++++++++++++ src/contracts/BCoWPool.sol | 79 +++++------------------------------- src/interfaces/IBCoWPool.sol | 78 +++++++++++++++++++++++++++++++++-- 3 files changed, 122 insertions(+), 71 deletions(-) create mode 100644 src/contracts/BCoWConst.sol diff --git a/src/contracts/BCoWConst.sol b/src/contracts/BCoWConst.sol new file mode 100644 index 00000000..87e83df6 --- /dev/null +++ b/src/contracts/BCoWConst.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.25; + +contract BCoWConst { + /** + * @notice The value representing the absence of a commitment. + * @return _emptyCommitment The commitment value representing no commitment. + */ + bytes32 public constant EMPTY_COMMITMENT = bytes32(0); + + /** + * @notice The value representing that no trading parameters are currently + * accepted as valid by this contract, meaning that no trading can occur. + * @return _noTrading The value representing no trading. + */ + bytes32 public constant NO_TRADING = bytes32(0); + + /** + * @notice The largest possible duration of any AMM order, starting from the + * current block timestamp. + * @return _maxOrderDuration The maximum order duration. + */ + uint32 public constant MAX_ORDER_DURATION = 5 * 60; + + /** + * @notice The transient storage slot specified in this variable stores the + * value of the order commitment, that is, the only order hash that can be + * validated by calling `isValidSignature`. + * The hash corresponding to the constant `EMPTY_COMMITMENT` has special + * semantics, discussed in the related documentation. + * @dev This value is: + * uint256(keccak256("CoWAMM.ConstantProduct.commitment")) - 1 + * @return _commitmentSlot The slot where the commitment is stored. + */ + uint256 public constant COMMITMENT_SLOT = 0x6c3c90245457060f6517787b2c4b8cf500ca889d2304af02043bd5b513e3b593; +} diff --git a/src/contracts/BCoWPool.sol b/src/contracts/BCoWPool.sol index 3cad22ca..5d4c866a 100644 --- a/src/contracts/BCoWPool.sol +++ b/src/contracts/BCoWPool.sol @@ -6,69 +6,28 @@ import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import {GPv2Order} from '@cowprotocol/libraries/GPv2Order.sol'; +import {BCoWConst} from './BCoWConst.sol'; import {BPool} from './BPool.sol'; import {IBCoWPool} from 'interfaces/IBCoWPool.sol'; - import {ISettlement} from 'interfaces/ISettlement.sol'; -/** - * @dev The value representing the absence of a commitment. - */ -bytes32 constant EMPTY_COMMITMENT = bytes32(0); - -/** - * @dev The value representing that no trading parameters are currently - * accepted as valid by this contract, meaning that no trading can occur. - */ -bytes32 constant NO_TRADING = bytes32(0); - -/** - * @dev The largest possible duration of any AMM order, starting from the - * current block timestamp. - */ -uint32 constant MAX_ORDER_DURATION = 5 * 60; - -/** - * @dev The transient storage slot specified in this variable stores the - * value of the order commitment, that is, the only order hash that can be - * validated by calling `isValidSignature`. - * The hash corresponding to the constant `EMPTY_COMMITMENT` has special - * semantics, discussed in the related documentation. - * @dev This value is: - * uint256(keccak256("CoWAMM.ConstantProduct.commitment")) - 1 - */ -uint256 constant COMMITMENT_SLOT = 0x6c3c90245457060f6517787b2c4b8cf500ca889d2304af02043bd5b513e3b593; - /** * @title BCoWPool * @notice Inherits BPool contract and can trade on CoWSwap Protocol. */ -contract BCoWPool is BPool, IERC1271, IBCoWPool { +contract BCoWPool is IERC1271, IBCoWPool, BPool, BCoWConst { using GPv2Order for GPv2Order.Data; - /** - * @notice The address that can pull funds from the AMM vault to execute an order - */ + /// @inheritdoc IBCoWPool address public immutable VAULT_RELAYER; - /** - * @notice The domain separator used for hashing CoW Protocol orders. - */ + /// @inheritdoc IBCoWPool bytes32 public immutable SOLUTION_SETTLER_DOMAIN_SEPARATOR; - /** - * @notice The address of the CoW Protocol settlement contract. It is the - * only address that can set commitments. - */ + /// @inheritdoc IBCoWPool ISettlement public immutable SOLUTION_SETTLER; - /** - * The hash of the data describing which `TradingParams` currently apply - * to this AMM. If this parameter is set to `NO_TRADING`, then the AMM - * does not accept any order as valid. - * If trading is enabled, then this value will be the [`hash`] of the only - * admissible [`TradingParams`]. - */ + /// @inheritdoc IBCoWPool bytes32 public appDataHash; constructor(address _cowSolutionSettler) BPool() { @@ -77,33 +36,20 @@ contract BCoWPool is BPool, IERC1271, IBCoWPool { VAULT_RELAYER = ISettlement(_cowSolutionSettler).vaultRelayer(); } - /** - * @notice Once this function is called, it will be possible to trade with - * this AMM on CoW Protocol. - * @param appData Trading is enabled with the appData specified here. - */ + /// @inheritdoc IBCoWPool function enableTrading(bytes32 appData) external onlyController { bytes32 _appDataHash = keccak256(abi.encode(appData)); appDataHash = _appDataHash; emit TradingEnabled(_appDataHash, appData); } - /** - * @notice Disable any form of trading on CoW Protocol by this AMM. - */ + /// @inheritdoc IBCoWPool function disableTrading() external onlyController { appDataHash = NO_TRADING; emit TradingDisabled(); } - /** - * @notice Restricts a specific AMM to being able to trade only the order - * with the specified hash. - * @dev The commitment is used to enforce that exactly one AMM order is - * valid when a CoW Protocol batch is settled. - * @param orderHash the order hash that will be enforced by the order - * verification function. - */ + /// @inheritdoc IBCoWPool function commit(bytes32 orderHash) external { if (msg.sender != address(SOLUTION_SETTLER)) { revert CommitOutsideOfSettlement(); @@ -135,17 +81,14 @@ contract BCoWPool is BPool, IERC1271, IBCoWPool { return this.isValidSignature.selector; } + /// @inheritdoc IBCoWPool function commitment() public view returns (bytes32 value) { assembly ("memory-safe") { value := tload(COMMITMENT_SLOT) } } - /** - * @notice This function checks that the input order is admissible for the - * constant-product curve for the given trading parameters. - * @param order `GPv2Order.Data` of a discrete order to be verified. - */ + /// @inheritdoc IBCoWPool function verify(GPv2Order.Data memory order) public view { Record memory inRecord = _records[address(order.sellToken)]; Record memory outRecord = _records[address(order.buyToken)]; diff --git a/src/interfaces/IBCoWPool.sol b/src/interfaces/IBCoWPool.sol index c5ba5cf3..93170bfa 100644 --- a/src/interfaces/IBCoWPool.sol +++ b/src/interfaces/IBCoWPool.sol @@ -1,18 +1,21 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.25; +import {GPv2Order} from '@cowprotocol/libraries/GPv2Order.sol'; import {IERC1271} from '@openzeppelin/contracts/interfaces/IERC1271.sol'; +import {IBPool} from 'interfaces/IBPool.sol'; +import {ISettlement} from 'interfaces/ISettlement.sol'; -interface IBCoWPool is IERC1271 { +interface IBCoWPool is IERC1271, IBPool { /** - * Emitted when the manager disables all trades by the AMM. Existing open + * @notice Emitted when the manager disables all trades by the AMM. Existing open * order will not be tradeable. Note that the AMM could resume trading with * different parameters at a later point. */ event TradingDisabled(); /** - * Emitted when the manager enables the AMM to trade on CoW Protocol. + * @notice Emitted when the manager enables the AMM to trade on CoW Protocol. * @param hash The hash of the trading parameters. * @param appData Trading has been enabled for this appData. */ @@ -67,4 +70,73 @@ interface IBCoWPool is IERC1271 { * AMM has not enabled trading. */ error AppDataDoNotMatchHash(); + + /** + * @notice Once this function is called, it will be possible to trade with + * this AMM on CoW Protocol. + * @param appData Trading is enabled with the appData specified here. + */ + function enableTrading(bytes32 appData) external; + + /** + * @notice Disable any form of trading on CoW Protocol by this AMM. + */ + function disableTrading() external; + + /** + * @notice Restricts a specific AMM to being able to trade only the order + * with the specified hash. + * @dev The commitment is used to enforce that exactly one AMM order is + * valid when a CoW Protocol batch is settled. + * @param orderHash the order hash that will be enforced by the order + * verification function. + */ + function commit(bytes32 orderHash) external; + + /** + * @notice The address that can pull funds from the AMM vault to execute an order + * @return _vaultRelayer The address of the vault relayer. + */ + // solhint-disable-next-line style-guide-casing + function VAULT_RELAYER() external view returns (address _vaultRelayer); + + /** + * @notice The domain separator used for hashing CoW Protocol orders. + * @return _solutionSettlerDomainSeparator The domain separator. + */ + // solhint-disable-next-line style-guide-casing + function SOLUTION_SETTLER_DOMAIN_SEPARATOR() external view returns (bytes32 _solutionSettlerDomainSeparator); + + /** + * @notice The address of the CoW Protocol settlement contract. It is the + * only address that can set commitments. + * @return _solutionSettler The address of the solution settler. + */ + // solhint-disable-next-line style-guide-casing + function SOLUTION_SETTLER() external view returns (ISettlement _solutionSettler); + + /** + * @notice The hash of the data describing which `GPv2Order.AppData` currently + * apply to this AMM. If this parameter is set to `NO_TRADING`, then the AMM + * does not accept any order as valid. + * If trading is enabled, then this value will be the [`hash`] of the only + * admissible [`GPv2Order.AppData`]. + * @return _appDataHash The hash of the allowed GPv2Order AppData. + */ + function appDataHash() external view returns (bytes32 _appDataHash); + + /** + * @notice This function returns the commitment hash that has been set by the + * `commit` function. If no commitment has been set, then the value will be + * `EMPTY_COMMITMENT`. + * @return _commitment The commitment hash. + */ + function commitment() external view returns (bytes32 _commitment); + + /** + * @notice This function checks that the input order is admissible for the + * constant-product curve for the given trading parameters. + * @param order `GPv2Order.Data` of a discrete order to be verified. + */ + function verify(GPv2Order.Data memory order) external view; } From aa6271c3562edd5d2e160c9ff615bae5b3b43652 Mon Sep 17 00:00:00 2001 From: teddy Date: Thu, 13 Jun 2024 05:23:32 -0300 Subject: [PATCH 04/32] test: unit testing {en-dis}-able trading and commit (#86) * refactor: variable of type MockBCoWPool to avoid casts * test: test commit & commitment * refactor: remove already-tested internal function * test: test enable & disable trading * fix: update names to comply with convention * refactor: use already-mocked tokens for modularization and simplicity --- src/contracts/BCoWPool.sol | 12 +----- test/unit/BCoWPool.t.sol | 85 ++++++++++++++++++++++++++++++++++---- 2 files changed, 79 insertions(+), 18 deletions(-) diff --git a/src/contracts/BCoWPool.sol b/src/contracts/BCoWPool.sol index 5d4c866a..a3ec48ba 100644 --- a/src/contracts/BCoWPool.sol +++ b/src/contracts/BCoWPool.sol @@ -123,16 +123,6 @@ contract BCoWPool is IERC1271, IBCoWPool, BPool, BCoWConst { } } - /** - * @notice Approves the spender to transfer an unlimited amount of tokens - * and reverts if the approval was unsuccessful. - * @param token The ERC-20 token to approve. - * @param spender The address that can transfer on behalf of this contract. - */ - function _approveUnlimited(IERC20 token, address spender) internal { - token.approve(spender, type(uint256).max); - } - /** * @inheritdoc BPool * @dev Grants infinite approval to the vault relayer for all tokens in the @@ -140,7 +130,7 @@ contract BCoWPool is IERC1271, IBCoWPool, BPool, BCoWConst { */ function _afterFinalize() internal override { for (uint256 i; i < _tokens.length; i++) { - _approveUnlimited(IERC20(_tokens[i]), VAULT_RELAYER); + IERC20(_tokens[i]).approve(VAULT_RELAYER, type(uint256).max); } } } diff --git a/test/unit/BCoWPool.t.sol b/test/unit/BCoWPool.t.sol index 5671c0ea..9ecc7832 100644 --- a/test/unit/BCoWPool.t.sol +++ b/test/unit/BCoWPool.t.sol @@ -4,6 +4,9 @@ pragma solidity 0.8.25; import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import {BasePoolTest} from './BPool.t.sol'; + +import {IBCoWPool} from 'interfaces/IBCoWPool.sol'; +import {IBPool} from 'interfaces/IBPool.sol'; import {ISettlement} from 'interfaces/ISettlement.sol'; import {MockBCoWPool} from 'test/manual-smock/MockBCoWPool.sol'; import {MockBPool} from 'test/smock/MockBPool.sol'; @@ -13,11 +16,15 @@ abstract contract BaseCoWPoolTest is BasePoolTest { bytes32 public domainSeparator = bytes32(bytes2(0xf00b)); address public vaultRelayer = makeAddr('vaultRelayer'); + MockBCoWPool bCoWPool; + function setUp() public override { super.setUp(); vm.mockCall(cowSolutionSettler, abi.encodePacked(ISettlement.domainSeparator.selector), abi.encode(domainSeparator)); vm.mockCall(cowSolutionSettler, abi.encodePacked(ISettlement.vaultRelayer.selector), abi.encode(vaultRelayer)); - bPool = MockBPool(address(new MockBCoWPool(cowSolutionSettler))); + bCoWPool = new MockBCoWPool(cowSolutionSettler); + bPool = MockBPool(address(bCoWPool)); + _setRandomTokens(TOKENS_AMOUNT); } } @@ -48,14 +55,78 @@ contract BCoWPool_Unit_Constructor is BaseCoWPoolTest { } contract BCoWPool_Unit_Finalize is BaseCoWPoolTest { - function test_setsApprovals(uint256 _tokensLength) public { - _tokensLength = bound(_tokensLength, MIN_BOUND_TOKENS, MAX_BOUND_TOKENS); - _setRandomTokens(_tokensLength); - address[] memory tokens = _getDeterministicTokenArray(_tokensLength); - for (uint256 i = 0; i < bPool.getNumTokens(); i++) { + function test_Set_Approvals() public { + for (uint256 i = 0; i < TOKENS_AMOUNT; i++) { vm.mockCall(tokens[i], abi.encodePacked(IERC20.approve.selector), abi.encode(true)); vm.expectCall(tokens[i], abi.encodeCall(IERC20.approve, (vaultRelayer, type(uint256).max)), 1); } - bPool.finalize(); + bCoWPool.finalize(); + } +} + +/// @notice this tests both commit and commitment +contract BCoWPool_Unit_Commit is BaseCoWPoolTest { + function test_Revert_NonSolutionSettler(address sender, bytes32 orderHash) public { + vm.assume(sender != cowSolutionSettler); + vm.prank(sender); + vm.expectRevert(IBCoWPool.CommitOutsideOfSettlement.selector); + bCoWPool.commit(orderHash); + } + + function test_Set_Commitment(bytes32 orderHash) public { + vm.prank(cowSolutionSettler); + bCoWPool.commit(orderHash); + assertEq(bCoWPool.commitment(), orderHash); + } +} + +contract BCoWPool_Unit_DisableTranding is BaseCoWPoolTest { + function test_Revert_NonController(address sender) public { + // contract is deployed by this contract without any pranks + vm.assume(sender != address(this)); + vm.prank(sender); + vm.expectRevert(IBPool.BPool_CallerIsNotController.selector); + bCoWPool.disableTrading(); + } + + function test_Clear_AppdataHash(bytes32 appDataHash) public { + vm.assume(appDataHash != bytes32(0)); + bCoWPool.set_appDataHash(appDataHash); + bCoWPool.disableTrading(); + assertEq(bCoWPool.appDataHash(), bytes32(0)); + } + + function test_Emit_TradingDisabledEvent() public { + vm.expectEmit(); + emit IBCoWPool.TradingDisabled(); + bCoWPool.disableTrading(); + } + + function test_Succeed_AlreadyZeroAppdata() public { + bCoWPool.set_appDataHash(bytes32(0)); + bCoWPool.disableTrading(); + } +} + +contract BCoWPool_Unit_EnableTrading is BaseCoWPoolTest { + function test_Revert_NonController(address sender, bytes32 appDataHash) public { + // contract is deployed by this contract without any pranks + vm.assume(sender != address(this)); + vm.prank(sender); + vm.expectRevert(IBPool.BPool_CallerIsNotController.selector); + bCoWPool.enableTrading(appDataHash); + } + + function test_Set_AppDataHash(bytes32 appData) public { + bytes32 appDataHash = keccak256(abi.encode(appData)); + bCoWPool.enableTrading(appData); + assertEq(bCoWPool.appDataHash(), appDataHash); + } + + function test_Emit_TradingEnabled(bytes32 appData) public { + bytes32 appDataHash = keccak256(abi.encode(appData)); + vm.expectEmit(); + emit IBCoWPool.TradingEnabled(appDataHash, appData); + bCoWPool.enableTrading(appData); } } From 85046ef3861cfde306bfff5c9eabfe856aadc9da Mon Sep 17 00:00:00 2001 From: Austrian <114922365+0xAustrian@users.noreply.github.com> Date: Thu, 13 Jun 2024 13:10:26 -0300 Subject: [PATCH 05/32] test: BCowPool integration tests (#83) * test: initial structure for BCowPool integration tests * test: small improvements * test: add more code, wip * test: add commit as interaction, wip * test: small improvements, wip * test: fully working * test: add assertions * test: clean up * test: add gas snapshot --- .forge-snapshots/settle.snap | 1 + src/interfaces/ISettlement.sol | 18 +++ test/integration/BCowPool.t.sol | 184 ++++++++++++++++++++++++++ test/integration/GPv2TradeEncoder.sol | 112 ++++++++++++++++ 4 files changed, 315 insertions(+) create mode 100644 .forge-snapshots/settle.snap create mode 100644 test/integration/BCowPool.t.sol create mode 100644 test/integration/GPv2TradeEncoder.sol diff --git a/.forge-snapshots/settle.snap b/.forge-snapshots/settle.snap new file mode 100644 index 00000000..fc3e290e --- /dev/null +++ b/.forge-snapshots/settle.snap @@ -0,0 +1 @@ +206786 \ No newline at end of file diff --git a/src/interfaces/ISettlement.sol b/src/interfaces/ISettlement.sol index 99e56dae..8bf85dc8 100644 --- a/src/interfaces/ISettlement.sol +++ b/src/interfaces/ISettlement.sol @@ -1,7 +1,25 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.25; +import {IERC20} from '@cowprotocol/interfaces/IERC20.sol'; +import {GPv2Interaction} from '@cowprotocol/libraries/GPv2Interaction.sol'; +import {GPv2Trade} from '@cowprotocol/libraries/GPv2Trade.sol'; + interface ISettlement { + /** + * @notice Settles a batch of trades. + * @param tokens The tokens that are traded in the batch. + * @param clearingPrices The clearing prices of the trades. + * @param trades The trades to settle. + * @param interactions The interactions to execute. + */ + function settle( + IERC20[] calldata tokens, + uint256[] calldata clearingPrices, + GPv2Trade.Data[] calldata trades, + GPv2Interaction.Data[][3] calldata interactions + ) external; + /** * @return The domain separator for IERC1271 signature * @dev Immutable value, would not change on chain forks diff --git a/test/integration/BCowPool.t.sol b/test/integration/BCowPool.t.sol new file mode 100644 index 00000000..984a8d5b --- /dev/null +++ b/test/integration/BCowPool.t.sol @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import {GPv2TradeEncoder} from './GPv2TradeEncoder.sol'; +import {IERC20} from '@cowprotocol/interfaces/IERC20.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 {BCoWConst} from 'contracts/BCoWConst.sol'; +import {BCoWPool} from 'contracts/BCoWPool.sol'; +import {BMath} from 'contracts/BMath.sol'; +import {GasSnapshot} from 'forge-gas-snapshot/GasSnapshot.sol'; +import {Test, Vm} from 'forge-std/Test.sol'; +import {IBCoWPool} from 'interfaces/IBCoWPool.sol'; +import {IBPool} from 'interfaces/IBPool.sol'; +import {ISettlement} from 'interfaces/ISettlement.sol'; + +contract BCowPoolIntegrationTest is Test, BCoWConst, BMath, GasSnapshot { + using GPv2Order for GPv2Order.Data; + + IBCoWPool public pool; + + IERC20 public dai = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); + IERC20 public weth = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + ISettlement public settlement = ISettlement(0x9008D19f58AAbD9eD0D60971565AA8510560ab41); + address public solver = address(0xa5559C2E1302c5Ce82582A6b1E4Aec562C2FbCf4); + + address public controller = makeAddr('controller'); + Vm.Wallet swapper = vm.createWallet('swapper'); + + bytes32 public constant APP_DATA = bytes32('exampleIntegrationAppData'); + + uint256 public constant HUNDRED_UNITS = 100 ether; + uint256 public constant ONE_UNIT = 1 ether; + + function setUp() public { + vm.createSelectFork('mainnet', 20_012_063); + + // deal controller + deal(address(dai), controller, HUNDRED_UNITS); + deal(address(weth), controller, HUNDRED_UNITS); + + // deal swapper + deal(address(weth), swapper.addr, HUNDRED_UNITS); + + vm.startPrank(controller); + // deploy + // TODO: deploy with BCoWFactory + pool = new BCoWPool(address(settlement)); + // bind + dai.approve(address(pool), type(uint256).max); + weth.approve(address(pool), type(uint256).max); + IBPool(pool).bind(address(dai), HUNDRED_UNITS, 2e18); + IBPool(pool).bind(address(weth), HUNDRED_UNITS, 8e18); + // finalize + IBPool(pool).finalize(); + // enable trading + pool.enableTrading(APP_DATA); + } + + function testBCowPoolSwap() public { + uint256 buyAmount = HUNDRED_UNITS; + uint256 sellAmount = calcOutGivenIn({ + tokenBalanceIn: dai.balanceOf(address(pool)), + tokenWeightIn: pool.getDenormalizedWeight(address(dai)), + tokenBalanceOut: weth.balanceOf(address(pool)), + tokenWeightOut: pool.getDenormalizedWeight(address(weth)), + tokenAmountIn: buyAmount, + swapFee: 0 + }); + + uint32 latestValidTimestamp = uint32(block.timestamp) + MAX_ORDER_DURATION - 1; + + // swapper approves weth to vaultRelayer + vm.startPrank(swapper.addr); + weth.approve(settlement.vaultRelayer(), type(uint256).max); + + // swapper creates the order + GPv2Order.Data memory swapperOrder = GPv2Order.Data({ + sellToken: weth, + buyToken: dai, + receiver: GPv2Order.RECEIVER_SAME_AS_OWNER, + sellAmount: sellAmount, // WETH + buyAmount: buyAmount, // 100 DAI + validTo: latestValidTimestamp, + appData: APP_DATA, + feeAmount: 0, + kind: GPv2Order.KIND_BUY, + partiallyFillable: false, + buyTokenBalance: GPv2Order.BALANCE_ERC20, + sellTokenBalance: GPv2Order.BALANCE_ERC20 + }); + + // swapper signs the order + (uint8 v, bytes32 r, bytes32 s) = + vm.sign(swapper.privateKey, GPv2Order.hash(swapperOrder, settlement.domainSeparator())); + bytes memory swapperSig = abi.encodePacked(r, s, v); + + // order for bPool is generated + GPv2Order.Data memory poolOrder = GPv2Order.Data({ + sellToken: dai, + buyToken: weth, + receiver: GPv2Order.RECEIVER_SAME_AS_OWNER, + sellAmount: buyAmount, // 100 DAI + buyAmount: sellAmount, // WETH + validTo: latestValidTimestamp, + appData: APP_DATA, + feeAmount: 0, + kind: GPv2Order.KIND_SELL, + partiallyFillable: true, + sellTokenBalance: GPv2Order.BALANCE_ERC20, + buyTokenBalance: GPv2Order.BALANCE_ERC20 + }); + bytes memory poolSig = abi.encode(poolOrder); + + // solver prepares for call settle() + IERC20[] memory tokens = new IERC20[](2); + tokens[0] = IERC20(weth); + tokens[1] = IERC20(dai); + + uint256[] memory clearingPrices = new uint256[](2); + // TODO: we can use more accurate clearing prices here + clearingPrices[0] = buyAmount; + clearingPrices[1] = sellAmount; + + GPv2Trade.Data[] memory trades = new GPv2Trade.Data[](2); + + // pool's trade + trades[0] = GPv2Trade.Data({ + sellTokenIndex: 1, + buyTokenIndex: 0, + receiver: poolOrder.receiver, + sellAmount: poolOrder.sellAmount, + buyAmount: poolOrder.buyAmount, + validTo: poolOrder.validTo, + appData: poolOrder.appData, + feeAmount: poolOrder.feeAmount, + flags: GPv2TradeEncoder.encodeFlags(poolOrder, GPv2Signing.Scheme.Eip1271), + executedAmount: poolOrder.sellAmount, + signature: abi.encodePacked(address(pool), poolSig) + }); + + // swapper's trade + trades[1] = GPv2Trade.Data({ + sellTokenIndex: 0, + buyTokenIndex: 1, + receiver: swapperOrder.receiver, + sellAmount: swapperOrder.sellAmount, + buyAmount: swapperOrder.buyAmount, + validTo: swapperOrder.validTo, + appData: swapperOrder.appData, + feeAmount: swapperOrder.feeAmount, + flags: GPv2TradeEncoder.encodeFlags(swapperOrder, GPv2Signing.Scheme.Eip712), + executedAmount: swapperOrder.sellAmount, + signature: swapperSig + }); + + // in the first interactions, save the commitment + GPv2Interaction.Data[][3] memory interactions = + [new GPv2Interaction.Data[](1), new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0)]; + + interactions[0][0] = GPv2Interaction.Data({ + target: address(pool), + value: 0, + callData: abi.encodeWithSelector( + IBCoWPool.commit.selector, poolOrder.hash(pool.SOLUTION_SETTLER_DOMAIN_SEPARATOR()) + ) + }); + + // finally, settle + vm.startPrank(solver); + snapStart('settle'); + settlement.settle(tokens, clearingPrices, trades, interactions); + snapEnd(); + + // assert swapper balance + assertEq(dai.balanceOf(swapper.addr), buyAmount); + assertEq(weth.balanceOf(swapper.addr), HUNDRED_UNITS - sellAmount); + // assert pool balance + assertEq(dai.balanceOf(address(pool)), HUNDRED_UNITS - buyAmount); + assertEq(weth.balanceOf(address(pool)), HUNDRED_UNITS + sellAmount); + } +} diff --git a/test/integration/GPv2TradeEncoder.sol b/test/integration/GPv2TradeEncoder.sol new file mode 100644 index 00000000..24d9d9a2 --- /dev/null +++ b/test/integration/GPv2TradeEncoder.sol @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.25; + +import {GPv2Order} from '@cowprotocol/libraries/GPv2Order.sol'; +import {GPv2Signing} from '@cowprotocol/mixins/GPv2Signing.sol'; +import {IERC20} from '@openzeppelin/contracts/interfaces/IERC20.sol'; + +/* solhint-disable-next-line max-line-length */ +/// @notice This library is an almost exact copy of https://github.com/cowprotocol/composable-cow/blob/9ea0394e15eec16550ba4c327d10c920880eb355/test/vendored/GPv2TradeEncoder.sol + +/// @title Gnosis Protocol v2 Trade Library. +/// @author mfw78 +/// @dev This library provides functions for encoding trade flags +/// Encoding methodology is adapted from upstream at +/* solhint-disable-next-line max-line-length */ +/// https://github.com/cowprotocol/contracts/blob/d043b0bfac7a09463c74dfe1613d0612744ed91c/src/contracts/libraries/GPv2Trade.sol +library GPv2TradeEncoder { + using GPv2Order for GPv2Order.Data; + + // uint256 constant FLAG_ORDER_KIND_SELL = 0x00; + uint256 constant FLAG_ORDER_KIND_BUY = 0x01; + + // uint256 constant FLAG_FILL_FOK = 0x00; + uint256 constant FLAG_FILL_PARTIAL = 0x02; + + // uint256 constant FLAG_SELL_TOKEN_ERC20_TOKEN_BALANCE = 0x00; + uint256 constant FLAG_SELL_TOKEN_BALANCER_EXTERNAL = 0x08; + uint256 constant FLAG_SELL_TOKEN_BALANCER_INTERNAL = 0x0c; + + // uint256 constant FLAG_BUY_TOKEN_ERC20_TOKEN_BALANCE = 0x00; + uint256 constant FLAG_BUY_TOKEN_BALANCER_INTERNAL = 0x10; + + // uint256 constant FLAG_SIGNATURE_SCHEME_EIP712 = 0x00; + uint256 constant FLAG_SIGNATURE_SCHEME_ETHSIGN = 0x20; + uint256 constant FLAG_SIGNATURE_SCHEME_EIP1271 = 0x40; + uint256 constant FLAG_SIGNATURE_SCHEME_PRESIGN = 0x60; + + /// @dev Decodes trade flags. + /// + /// Trade flags are used to tightly encode information on how to decode + /// an order. Examples that directly affect the structure of an order are + /// the kind of order (either a sell or a buy order) as well as whether the + /// order is partially fillable or if it is a "fill-or-kill" order. It also + /// encodes the signature scheme used to validate the order. As the most + /// likely values are fill-or-kill sell orders by an externally owned + /// account, the flags are chosen such that `0x00` represents this kind of + /// order. The flags byte uses the following format: + /// + /// ``` + /// bit | 31 ... | 6 | 5 | 4 | 3 | 2 | 1 | 0 | + /// ----+----------+-------+---+-------+---+---+ + /// | reserved | * * | * | * * | * | * | + /// | | | | | | | + /// | | | | | | +---- order kind bit, 0 for a sell order + /// | | | | | | and 1 for a buy order + /// | | | | | | + /// | | | | | +-------- order fill bit, 0 for fill-or-kill + /// | | | | | and 1 for a partially fillable order + /// | | | | | + /// | | | +---+------------ use internal sell token balance bit: + /// | | | 0x: ERC20 token balance + /// | | | 10: external Balancer Vault balance + /// | | | 11: internal Balancer Vault balance + /// | | | + /// | | +-------------------- use buy token balance bit + /// | | 0: ERC20 token balance + /// | | 1: internal Balancer Vault balance + /// | | + /// +---+------------------------ signature scheme bits: + /// 00: EIP-712 + /// 01: eth_sign + /// 10: EIP-1271 + /// 11: pre_sign + /// ``` + function encodeFlags( + GPv2Order.Data memory order, + GPv2Signing.Scheme signingScheme + ) internal pure returns (uint256 flags) { + // set the zero index bit if the order is a buy order + if (order.kind == GPv2Order.KIND_BUY) { + flags |= FLAG_ORDER_KIND_BUY; + } + + // set the first index bit if the order is partially fillable + if (order.partiallyFillable) { + flags |= FLAG_FILL_PARTIAL; + } + + // set the second and third index bit based on the sell token liquidity + if (order.sellTokenBalance == GPv2Order.BALANCE_EXTERNAL) { + flags |= FLAG_SELL_TOKEN_BALANCER_EXTERNAL; + } else if (order.sellTokenBalance == GPv2Order.BALANCE_INTERNAL) { + flags |= FLAG_SELL_TOKEN_BALANCER_INTERNAL; + } + + // set the fourth index bit based on the buy token liquidity + if (order.buyTokenBalance == GPv2Order.BALANCE_INTERNAL) { + flags |= FLAG_BUY_TOKEN_BALANCER_INTERNAL; + } + + // set the fifth and sixth index bit based on the signature scheme + if (signingScheme == GPv2Signing.Scheme.EthSign) { + flags |= FLAG_SIGNATURE_SCHEME_ETHSIGN; + } else if (signingScheme == GPv2Signing.Scheme.Eip1271) { + flags |= FLAG_SIGNATURE_SCHEME_EIP1271; + } else if (signingScheme == GPv2Signing.Scheme.PreSign) { + flags |= FLAG_SIGNATURE_SCHEME_PRESIGN; + } + + return flags; + } +} From b6f62da173c192c095e66f8c671a1733b797bdcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Thu, 13 Jun 2024 18:47:41 +0200 Subject: [PATCH 06/32] chore: fixing dependabot alerts (#82) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 61 +++++++++++++++++++------------------------------------ 1 file changed, 21 insertions(+), 40 deletions(-) diff --git a/yarn.lock b/yarn.lock index efe07ba0..6a50cc67 100644 --- a/yarn.lock +++ b/yarn.lock @@ -323,12 +323,7 @@ ansi-escapes@^6.2.0: resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-6.2.1.tgz#76c54ce9b081dad39acec4b5d53377913825fb0f" integrity sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig== -ansi-regex@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" - integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== - -ansi-regex@^5.0.1: +ansi-regex@^5.0.0, ansi-regex@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== @@ -421,11 +416,11 @@ brace-expansion@^2.0.1: balanced-match "^1.0.0" braces@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== dependencies: - fill-range "^7.0.1" + fill-range "^7.1.1" call-bind@^1.0.2, call-bind@^1.0.7: version "1.0.7" @@ -787,10 +782,10 @@ fastq@^1.6.0: dependencies: reusify "^1.0.4" -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== dependencies: to-regex-range "^5.0.1" @@ -1086,9 +1081,9 @@ ini@4.1.1: integrity sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g== ini@^1.3.4: - version "1.3.5" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" - integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== + version "1.3.8" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" + integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== is-arguments@^1.0.4: version "1.1.1" @@ -1362,13 +1357,6 @@ log-update@^6.0.0: strip-ansi "^7.1.0" wrap-ansi "^9.0.0" -lru-cache@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" - integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== - dependencies: - yallist "^4.0.0" - memorystream@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2" @@ -1629,21 +1617,19 @@ run-parallel@^1.1.9: queue-microtask "^1.2.2" semver@^5.5.0: - version "5.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== + version "5.7.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" + integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== semver@^6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== semver@^7.6.0: - version "7.6.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d" - integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg== - dependencies: - lru-cache "^6.0.0" + version "7.6.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13" + integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w== set-function-length@^1.2.1: version "1.2.2" @@ -2083,11 +2069,6 @@ y18n@^5.0.5: resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== -yallist@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" - integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== - yaml@2.3.4: version "2.3.4" resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.4.tgz#53fc1d514be80aabf386dc6001eb29bf3b7523b2" From df1b9ad4d05940661db3cc14b2a7d77a17c41188 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Thu, 13 Jun 2024 18:47:55 +0200 Subject: [PATCH 07/32] chore: adding graffiti to BCoWPool (#88) --- src/contracts/BCoWPool.sol | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/contracts/BCoWPool.sol b/src/contracts/BCoWPool.sol index a3ec48ba..cadc80a1 100644 --- a/src/contracts/BCoWPool.sol +++ b/src/contracts/BCoWPool.sol @@ -1,6 +1,21 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.25; +/* + +Coded for Balancer and CoW Swap with ♥ by + +░██╗░░░░░░░██╗░█████╗░███╗░░██╗██████╗░███████╗██████╗░██╗░░░░░░█████╗░███╗░░██╗██████╗░ +░██║░░██╗░░██║██╔══██╗████╗░██║██╔══██╗██╔════╝██╔══██╗██║░░░░░██╔══██╗████╗░██║██╔══██╗ +░╚██╗████╗██╔╝██║░░██║██╔██╗██║██║░░██║█████╗░░██████╔╝██║░░░░░███████║██╔██╗██║██║░░██║ +░░████╔═████║░██║░░██║██║╚████║██║░░██║██╔══╝░░██╔══██╗██║░░░░░██╔══██║██║╚████║██║░░██║ +░░╚██╔╝░╚██╔╝░╚█████╔╝██║░╚███║██████╔╝███████╗██║░░██║███████╗██║░░██║██║░╚███║██████╔╝ +░░░╚═╝░░░╚═╝░░░╚════╝░╚═╝░░╚══╝╚═════╝░╚══════╝╚═╝░░╚═╝╚══════╝╚═╝░░╚═╝╚═╝░░╚══╝╚═════╝░ + +https://defi.sucks + +*/ + import {IERC1271} from '@openzeppelin/contracts/interfaces/IERC1271.sol'; import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; From 9c91c8845ca2afd24f018150c812c8d57841e47b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Thu, 13 Jun 2024 19:01:21 +0200 Subject: [PATCH 08/32] feat: updating readme for BCoW AMM (#87) * feat: updating readme for BCoW AMM * fix: adding gulp deprecation --- README.md | 99 +++++++++++++++++-------------------------------------- 1 file changed, 31 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index 8583cf1b..467949b2 100644 --- a/README.md +++ b/README.md @@ -1,83 +1,46 @@ -

-balancer pebbles logo -

+

Balancer CoW AMM

-

- - - - - - - - - -

- -

balancer

- -**Balancer** is an automated **portfolio manager**, **liquidity provider**, and **price sensor**. - -Balancer turns the concept of an index fund on its head: instead of a paying fees -to portfolio managers to rebalance your portfolio, you collect fees from traders, who rebalance -your portfolio by following arbitrage opportunities. +**Balancer CoW AMM** is an automated **portfolio manager**, **liquidity provider**, and **price sensor**, that allows swaps to be executed via the CoW Protocol. Balancer is based on an N-dimensional invariant surface which is a generalization of the constant product formula described by Vitalik Buterin and proven viable by the popular Uniswap dapp. -## 🍂 Bronze Release 🍂 - -The *🍂Bronze Release🍂* is the first of 3 planned releases of the Balancer Protocol. Bronze emphasizes code clarity for audit and verification, and does not go to great lengths to optimize for gas. - -The *❄️Silver Release❄️* will bring many gas optimizations and architecture changes that will reduce transaction overhead and enable more flexibility for managed pools. - -The *☀️Golden Release☀️* will introduce a curious new liquidity mechanism to the market. - ## Documentation -The full documentation can be found at [https://docs.balancer.finance](https://docs.balancer.finance) - +The full documentation can be found at `TODO BAL-98`. ## Development -Most users will want to consume the ABI definitions for BFactory and BPool. +Most users will want to consume the ABI definitions for BPool, BCoWPool, BFactory and BCoWFactory. -This project follows the standard Truffle project structure. +This project follows the standard Foundry project structure. ``` -yarn compile # build artifacts to `build/contracts` -yarn testrpc # run ganache +yarn build # build artifacts to `out/` yarn test # run the tests ``` -Tests can be run verbosely to view approximation diffs: - -``` -yarn test:verbose -``` - -``` - Contract: BPool - With fees -pAi -expected: 10.891089108910892) -actual : 10.891089106783580001) -relDif : 1.9532588879656032e-10) -Pool Balance -expected: 98010000000000030000) -actual : 98010000001320543977) -relDif : 1.3473294888276702e-11) -Dirt Balance -expected: 3921200210105053000) -actual : 3921200210099248361) -relDif : 1.480428360949332e-12) -Rock Balance -expected: 11763600630315160000) -actual : 11763600630334527239) -relDif : 1.6464292361378058e-12) - ✓ exitswap_ExternAmountOut (537ms) -``` - -Complete API docs are available at [https://docs.balancer.finance/smart-contracts/api](https://docs.balancer.finance/smart-contracts/api) - - -

+## Changes on BPool from (Balancer V1)[https://github.com/balancer/balancer-core] +- Migrated to Foundry project structure +- Implementation of interfaces with Natspec documentation +- Replaced `require(cond, 'STRING')` for `if(!cond) revert CustomError()` +- Bumped Solidity version from `0.5.12` to `0.8.25` (required for transient storage) + - Added explicit `unchecked` blocks to `BNum` operations (to avoid Solidity overflow checks) +- Deprecated `Record.balance` storage (in favour of `ERC20.balanceOf(address(this))`) +- Deprecated `gulp` method (not needed since reading ERC20 balances) +- Deprecated manageable pools: + - Deprecated `isPublicSwap` mechanism (for pools to be swapped before being finalized) + - Deprecated `rebind` method (in favour of `bind + unbind + bind`) + - Deprecated exit fee on `unbind` (since the pool is not supposed to have collected any fees) +- Deprecated `BBaseToken` (in favour of OpenZeppelin `ERC20` implementation) +- Deprecated `BColor` and `BBronze` (unused contracts) +- Deprecated `Migrations` contract (not needed) +- Added an `_afterFinalize` hook (to be called at the end of the finalize routine) + +## Features on BCoWPool (added via inheritance to BPool) +- Immutably stores CoW Protocol's `SolutionSettler` and `VaultRelayer` addresses at deployment +- Immutably stores Cow Protocol's a Domain Separator at deployment (to avoid replay attacks) +- Gives infinite ERC20 approval to the CoW Protocol's `VaultRelayer` contract +- Implements IERC1271 `isValidSignature` method to allow for validating intentions of swaps +- Implements a `commit` method to avoid multiple swaps from conflicting with each other +- Allows the controller to allow only one `GPv2Order.appData` at a time +- Validates the `GPv2Order` requirements before allowing the swap \ No newline at end of file From f2494eff29297bdc7751bf1ba89495bceb01210b Mon Sep 17 00:00:00 2001 From: Austrian <114922365+0xAustrian@users.noreply.github.com> Date: Thu, 13 Jun 2024 18:47:45 -0300 Subject: [PATCH 09/32] feat: add revert in enableTrading (#89) --- src/contracts/BCoWPool.sol | 4 ++++ test/unit/BCoWPool.t.sol | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/src/contracts/BCoWPool.sol b/src/contracts/BCoWPool.sol index cadc80a1..69ea8708 100644 --- a/src/contracts/BCoWPool.sol +++ b/src/contracts/BCoWPool.sol @@ -53,6 +53,10 @@ contract BCoWPool is IERC1271, IBCoWPool, BPool, BCoWConst { /// @inheritdoc IBCoWPool function enableTrading(bytes32 appData) external onlyController { + if (!_finalized) { + revert BPool_PoolNotFinalized(); + } + bytes32 _appDataHash = keccak256(abi.encode(appData)); appDataHash = _appDataHash; emit TradingEnabled(_appDataHash, appData); diff --git a/test/unit/BCoWPool.t.sol b/test/unit/BCoWPool.t.sol index 9ecc7832..05481519 100644 --- a/test/unit/BCoWPool.t.sol +++ b/test/unit/BCoWPool.t.sol @@ -109,6 +109,11 @@ contract BCoWPool_Unit_DisableTranding is BaseCoWPoolTest { } contract BCoWPool_Unit_EnableTrading is BaseCoWPoolTest { + function test_Revert_NotFinalized(bytes32 appDataHash) public { + vm.expectRevert(IBPool.BPool_PoolNotFinalized.selector); + bCoWPool.enableTrading(appDataHash); + } + function test_Revert_NonController(address sender, bytes32 appDataHash) public { // contract is deployed by this contract without any pranks vm.assume(sender != address(this)); @@ -118,12 +123,14 @@ contract BCoWPool_Unit_EnableTrading is BaseCoWPoolTest { } function test_Set_AppDataHash(bytes32 appData) public { + bCoWPool.set__finalized(true); bytes32 appDataHash = keccak256(abi.encode(appData)); bCoWPool.enableTrading(appData); assertEq(bCoWPool.appDataHash(), appDataHash); } function test_Emit_TradingEnabled(bytes32 appData) public { + bCoWPool.set__finalized(true); bytes32 appDataHash = keccak256(abi.encode(appData)); vm.expectEmit(); emit IBCoWPool.TradingEnabled(appDataHash, appData); From c1785ce5cba2c316f699c424927b1cdbcfc30b07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Fri, 14 Jun 2024 13:13:57 +0200 Subject: [PATCH 10/32] fix: rm exeptions in solhint file (#91) --- .solhint.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.solhint.json b/.solhint.json index 209b914a..b5849fdc 100644 --- a/.solhint.json +++ b/.solhint.json @@ -9,8 +9,6 @@ "private-vars-leading-underscore": ["warn", { "strict": false }], "quotes": "off", "one-contract-per-file": "warn", - "TODO": "REMOVE_TEMPORARY_LINTER_SETTINGS_BELOW", - "style-guide-casing": ["warn", { "ignoreEvents": true } ], - "custom-errors": "off" + "style-guide-casing": ["warn", { "ignoreEvents": true } ] } } From ce4e8555ec6413ecdf9ad23cf38beb12a3ad466f Mon Sep 17 00:00:00 2001 From: teddy Date: Fri, 14 Jun 2024 12:59:30 -0300 Subject: [PATCH 11/32] feat: adding BCoWFactory (#94) * refactor: modify BFactory tests so they can be run against other contracts * feat: contract BCoWFactory * chore: add required smock file * fix: feedback from pr --- src/contracts/BCoWFactory.sol | 33 ++++++++++++++++++++++++++ src/contracts/BFactory.sol | 2 +- test/smock/MockBCoWFactory.sol | 13 +++++++++++ test/unit/BCoWFactory.t.sol | 42 ++++++++++++++++++++++++++++++++++ test/unit/BFactory.t.sol | 32 ++++++++++++++++++-------- 5 files changed, 112 insertions(+), 10 deletions(-) create mode 100644 src/contracts/BCoWFactory.sol create mode 100644 test/smock/MockBCoWFactory.sol create mode 100644 test/unit/BCoWFactory.t.sol diff --git a/src/contracts/BCoWFactory.sol b/src/contracts/BCoWFactory.sol new file mode 100644 index 00000000..f8ef8d04 --- /dev/null +++ b/src/contracts/BCoWFactory.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.25; + +import {BCoWPool} from './BCoWPool.sol'; + +import {BFactory} from './BFactory.sol'; +import {IBFactory} from 'interfaces/IBFactory.sol'; +import {IBPool} from 'interfaces/IBPool.sol'; + +/** + * @title BCoWFactory + * @notice Creates new BCoWPools, logging their addresses and acting as a registry of pools. + */ +contract BCoWFactory is BFactory { + address public immutable SOLUTION_SETTLER; + + constructor(address _solutionSettler) BFactory() { + SOLUTION_SETTLER = _solutionSettler; + } + + /** + * @inheritdoc IBFactory + * @dev Deploys a BCoWPool instead of a regular BPool, maintains the interface + * to minimize required changes to existing tooling + */ + function newBPool() external override returns (IBPool _pool) { + IBPool bpool = new BCoWPool(SOLUTION_SETTLER); + _isBPool[address(bpool)] = true; + emit LOG_NEW_POOL(msg.sender, address(bpool)); + bpool.setController(msg.sender); + return bpool; + } +} diff --git a/src/contracts/BFactory.sol b/src/contracts/BFactory.sol index 3818e159..70da5798 100644 --- a/src/contracts/BFactory.sol +++ b/src/contracts/BFactory.sol @@ -20,7 +20,7 @@ contract BFactory is IBFactory { } /// @inheritdoc IBFactory - function newBPool() external returns (IBPool _pool) { + function newBPool() external virtual returns (IBPool _pool) { IBPool bpool = new BPool(); _isBPool[address(bpool)] = true; emit LOG_NEW_POOL(msg.sender, address(bpool)); diff --git a/test/smock/MockBCoWFactory.sol b/test/smock/MockBCoWFactory.sol new file mode 100644 index 00000000..73a97e05 --- /dev/null +++ b/test/smock/MockBCoWFactory.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.0; + +import {BCoWFactory, BCoWPool, BFactory, IBFactory, IBPool} from '../../src/contracts/BCoWFactory.sol'; +import {Test} from 'forge-std/Test.sol'; + +contract MockBCoWFactory is BCoWFactory, Test { + constructor(address _solutionSettler) BCoWFactory(_solutionSettler) {} + + function mock_call_newBPool(IBPool _pool) public { + vm.mockCall(address(this), abi.encodeWithSignature('newBPool()'), abi.encode(_pool)); + } +} diff --git a/test/unit/BCoWFactory.t.sol b/test/unit/BCoWFactory.t.sol new file mode 100644 index 00000000..614737a5 --- /dev/null +++ b/test/unit/BCoWFactory.t.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; + +import {Base, BaseBFactory_Unit_Constructor, BaseBFactory_Unit_NewBPool} from './BFactory.t.sol'; + +import {IBCoWPool} from 'interfaces/IBCoWPool.sol'; +import {IBFactory} from 'interfaces/IBFactory.sol'; +import {ISettlement} from 'interfaces/ISettlement.sol'; +import {MockBCoWFactory} from 'test/smock/MockBCoWFactory.sol'; + +abstract contract BCoWFactoryTest is Base { + address public solutionSettler = makeAddr('solutionSettler'); + + function _configureBFactory() internal override returns (IBFactory) { + vm.mockCall(solutionSettler, abi.encodePacked(ISettlement.domainSeparator.selector), abi.encode(bytes32(0))); + vm.mockCall( + solutionSettler, abi.encodePacked(ISettlement.vaultRelayer.selector), abi.encode(makeAddr('vault relayer')) + ); + vm.prank(owner); + return new MockBCoWFactory(solutionSettler); + } +} + +contract BCoWFactory_Unit_Constructor is BaseBFactory_Unit_Constructor, BCoWFactoryTest { + function test_Set_SolutionSettler(address _settler) public { + MockBCoWFactory factory = new MockBCoWFactory(_settler); + assertEq(factory.SOLUTION_SETTLER(), _settler); + } +} + +contract BCoWFactory_Unit_NewBPool is BaseBFactory_Unit_NewBPool, BCoWFactoryTest { + function test_Set_SolutionSettler(address _settler) public { + vm.prank(owner); + bFactory = new MockBCoWFactory(_settler); + vm.mockCall(_settler, abi.encodePacked(ISettlement.domainSeparator.selector), abi.encode(bytes32(0))); + vm.mockCall(_settler, abi.encodePacked(ISettlement.vaultRelayer.selector), abi.encode(makeAddr('vault relayer'))); + IBCoWPool bCoWPool = IBCoWPool(address(bFactory.newBPool())); + assertEq(address(bCoWPool.SOLUTION_SETTLER()), _settler); + } +} diff --git a/test/unit/BFactory.t.sol b/test/unit/BFactory.t.sol index e462cc07..f2eec270 100644 --- a/test/unit/BFactory.t.sol +++ b/test/unit/BFactory.t.sol @@ -10,16 +10,24 @@ import {IBFactory} from 'interfaces/IBFactory.sol'; import {IBPool} from 'interfaces/IBPool.sol'; abstract contract Base is Test { - BFactory public bFactory; + IBFactory public bFactory; address public owner = makeAddr('owner'); - function setUp() public { + function _configureBFactory() internal virtual returns (IBFactory); + + function setUp() public virtual { + bFactory = _configureBFactory(); + } +} + +abstract contract BFactoryTest is Base { + function _configureBFactory() internal override returns (IBFactory) { vm.prank(owner); - bFactory = new BFactory(); + return new BFactory(); } } -contract BFactory_Unit_Constructor is Base { +abstract contract BaseBFactory_Unit_Constructor is Base { /** * @notice Test that the owner is set correctly */ @@ -28,7 +36,10 @@ contract BFactory_Unit_Constructor is Base { } } -contract BFactory_Unit_IsBPool is Base { +// solhint-disable-next-line no-empty-blocks +contract BFactory_Unit_Constructor is BFactoryTest, BaseBFactory_Unit_Constructor {} + +contract BFactory_Unit_IsBPool is BFactoryTest { /** * @notice Test that a valid pool is present on the mapping */ @@ -47,7 +58,7 @@ contract BFactory_Unit_IsBPool is Base { } } -contract BFactory_Unit_NewBPool is Base { +abstract contract BaseBFactory_Unit_NewBPool is Base { /** * @notice Test that the pool is set on the mapping */ @@ -90,7 +101,10 @@ contract BFactory_Unit_NewBPool is Base { } } -contract BFactory_Unit_GetBLabs is Base { +// solhint-disable-next-line no-empty-blocks +contract BFactory_Unit_NewBPool is BFactoryTest, BaseBFactory_Unit_NewBPool {} + +contract BFactory_Unit_GetBLabs is BFactoryTest { /** * @notice Test that the correct owner is returned */ @@ -101,7 +115,7 @@ contract BFactory_Unit_GetBLabs is Base { } } -contract BFactory_Unit_SetBLabs is Base { +contract BFactory_Unit_SetBLabs is BFactoryTest { /** * @notice Test that only the owner can set the BLabs */ @@ -132,7 +146,7 @@ contract BFactory_Unit_SetBLabs is Base { } } -contract BFactory_Unit_Collect is Base { +contract BFactory_Unit_Collect is BFactoryTest { /** * @notice Test that only the owner can collect */ From 4aa0f35f2efc51f144880c59ff2f8f4a357866d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Sun, 16 Jun 2024 16:13:32 +0200 Subject: [PATCH 12/32] fix: formula comments in bmath (#90) --- src/contracts/BMath.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/contracts/BMath.sol b/src/contracts/BMath.sol index 9e670d56..19e2424e 100644 --- a/src/contracts/BMath.sol +++ b/src/contracts/BMath.sol @@ -164,9 +164,9 @@ contract BMath is BConst, BNum { * pS = poolSupply || --------- | ^ | --------- || * bI - bI * pAo = poolAmountOut \\ pS / \(wI / tW)// * bI = balanceIn tAi = -------------------------------------------- - * wI = weightIn / wI \ - * tW = totalWeight | 1 - ---- | * sF - * sF = swapFee \ tW / + * wI = weightIn / wI \ + * tW = totalWeight | 1 - ---- * sF | + * sF = swapFee \ tW / */ function calcSingleInGivenPoolOut( uint256 tokenBalanceIn, @@ -250,7 +250,7 @@ contract BMath is BConst, BNum { * @dev Formula: * pAi = poolAmountIn // / tAo \\ / wO \ \ * bO = tokenBalanceOut // | bO - -------------------------- |\ | ---- | \ - * tAo = tokenAmountOut pS - || \ 1 - ((1 - (tO / tW)) * sF)/ | ^ \ tW / * pS | + * tAo = tokenAmountOut pS - || \ 1 - ((1 - (w0 / tW)) * sF)/ | ^ \ tW / * pS | * ps = poolSupply \\ -----------------------------------/ / * wO = tokenWeightOut pAi = \\ bO / / * tW = totalWeight ------------------------------------------------------------- From df44f98c461af3f607e42cdcd7fe4ceb06f33b2c Mon Sep 17 00:00:00 2001 From: teddy Date: Mon, 17 Jun 2024 07:44:03 -0300 Subject: [PATCH 13/32] test: isValidSignature unit tests (#93) * test: isValidSignature unit tests * fix: linter autofixes * test: improvements from self-review * fix: feedback from review --- src/contracts/BCoWPool.sol | 10 ++-- test/manual-smock/MockBCoWPool.sol | 20 +++++++ test/unit/BCoWPool.t.sol | 87 +++++++++++++++++++++++++++++- 3 files changed, 112 insertions(+), 5 deletions(-) diff --git a/src/contracts/BCoWPool.sol b/src/contracts/BCoWPool.sol index 69ea8708..50215174 100644 --- a/src/contracts/BCoWPool.sol +++ b/src/contracts/BCoWPool.sol @@ -85,9 +85,13 @@ contract BCoWPool is IERC1271, IBCoWPool, BPool, BCoWConst { function isValidSignature(bytes32 _hash, bytes memory signature) external view returns (bytes4) { (GPv2Order.Data memory order) = abi.decode(signature, (GPv2Order.Data)); - if (appDataHash != keccak256(abi.encode(order.appData))) revert AppDataDoNotMatchHash(); + if (appDataHash != keccak256(abi.encode(order.appData))) { + revert AppDataDoNotMatchHash(); + } bytes32 orderHash = order.hash(SOLUTION_SETTLER_DOMAIN_SEPARATOR); - if (orderHash != _hash) revert OrderDoesNotMatchMessageHash(); + if (orderHash != _hash) { + revert OrderDoesNotMatchMessageHash(); + } if (orderHash != commitment()) { revert OrderDoesNotMatchCommitmentHash(); @@ -108,7 +112,7 @@ contract BCoWPool is IERC1271, IBCoWPool, BPool, BCoWConst { } /// @inheritdoc IBCoWPool - function verify(GPv2Order.Data memory order) public view { + function verify(GPv2Order.Data memory order) public view virtual { Record memory inRecord = _records[address(order.sellToken)]; Record memory outRecord = _records[address(order.buyToken)]; diff --git a/test/manual-smock/MockBCoWPool.sol b/test/manual-smock/MockBCoWPool.sol index dd914b77..b2d27099 100644 --- a/test/manual-smock/MockBCoWPool.sol +++ b/test/manual-smock/MockBCoWPool.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.0; import {BCoWPool, BPool, GPv2Order, IBCoWPool, IERC1271, IERC20, ISettlement} from '../../src/contracts/BCoWPool.sol'; import {BMath, IBPool} from '../../src/contracts/BPool.sol'; +import {GPv2Order} from '@cowprotocol/libraries/GPv2Order.sol'; import {Test} from 'forge-std/Test.sol'; contract MockBCoWPool is BCoWPool, Test { @@ -11,6 +12,12 @@ contract MockBCoWPool is BCoWPool, Test { appDataHash = _appDataHash; } + function set_commitment(bytes32 _commitment) public { + assembly ("memory-safe") { + tstore(COMMITMENT_SLOT, _commitment) + } + } + function mock_call_appDataHash(bytes32 _value) public { vm.mockCall(address(this), abi.encodeWithSignature('appDataHash()'), abi.encode(_value)); } @@ -361,4 +368,17 @@ contract MockBCoWPool is BCoWPool, Test { function expectCall__pushUnderlying(address erc20, address to, uint256 amount) public { vm.expectCall(address(this), abi.encodeWithSignature('_pushUnderlying(address,address,uint256)', erc20, to, amount)); } + // BCoWPool overrides + + function verify(GPv2Order.Data memory order) public view override { + (bool _success, bytes memory _data) = + address(this).staticcall(abi.encodeWithSignature('verify(GPv2Order.Data)', order)); + + if (_success) return abi.decode(_data, ()); + else return super.verify(order); + } + + function expectCall_verify(GPv2Order.Data memory order) public { + vm.expectCall(address(this), abi.encodeWithSignature('verify(GPv2Order.Data)', order)); + } } diff --git a/test/unit/BCoWPool.t.sol b/test/unit/BCoWPool.t.sol index 05481519..78b5f3b9 100644 --- a/test/unit/BCoWPool.t.sol +++ b/test/unit/BCoWPool.t.sol @@ -1,7 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.25; -import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import {IERC20} from '@cowprotocol/interfaces/IERC20.sol'; + +import {GPv2Order} from '@cowprotocol/libraries/GPv2Order.sol'; +import {IERC1271} from '@openzeppelin/contracts/interfaces/IERC1271.sol'; import {BasePoolTest} from './BPool.t.sol'; @@ -18,7 +21,7 @@ abstract contract BaseCoWPoolTest is BasePoolTest { MockBCoWPool bCoWPool; - function setUp() public override { + function setUp() public virtual override { super.setUp(); vm.mockCall(cowSolutionSettler, abi.encodePacked(ISettlement.domainSeparator.selector), abi.encode(domainSeparator)); vm.mockCall(cowSolutionSettler, abi.encodePacked(ISettlement.vaultRelayer.selector), abi.encode(vaultRelayer)); @@ -137,3 +140,83 @@ contract BCoWPool_Unit_EnableTrading is BaseCoWPoolTest { bCoWPool.enableTrading(appData); } } + +contract BCoWPool_Unit_IsValidSignature is BaseCoWPoolTest { + function setUp() public virtual override { + super.setUp(); + for (uint256 i = 0; i < TOKENS_AMOUNT; i++) { + vm.mockCall(tokens[i], abi.encodePacked(IERC20.approve.selector), abi.encode(true)); + } + bCoWPool.finalize(); + } + + modifier _withTradingEnabled(bytes32 _appData) { + bCoWPool.set_appDataHash(keccak256(abi.encode(_appData))); + _; + } + + modifier _withValidCommitment(GPv2Order.Data memory _order) { + bytes32 _orderHash = GPv2Order.hash(_order, domainSeparator); + bCoWPool.set_commitment(_orderHash); + _; + } + + function test_Revert_OrderWithWrongAppdata( + bytes32 _appData, + GPv2Order.Data memory _order + ) public _withTradingEnabled(_appData) { + vm.assume(_order.appData != _appData); + bytes32 _appDataHash = keccak256(abi.encode(_appData)); + vm.expectRevert(IBCoWPool.AppDataDoNotMatchHash.selector); + bCoWPool.isValidSignature(_appDataHash, abi.encode(_order)); + } + + function test_Revert_OrderSignedWithWrongDomainSeparator( + GPv2Order.Data memory _order, + bytes32 _differentDomainSeparator + ) public _withTradingEnabled(_order.appData) _withValidCommitment(_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 _withTradingEnabled(_order.appData) { + vm.expectRevert(IBCoWPool.OrderDoesNotMatchMessageHash.selector); + bCoWPool.isValidSignature(_orderHash, abi.encode(_order)); + } + + function test_Revert_OrderHashDifferentFromCommitment( + GPv2Order.Data memory _order, + bytes32 _differentCommitment + ) public _withTradingEnabled(_order.appData) { + bCoWPool.set_commitment(_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 + _withTradingEnabled(_order.appData) + _withValidCommitment(_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 + _withTradingEnabled(_order.appData) + _withValidCommitment(_order) + { + bytes32 _orderHash = GPv2Order.hash(_order, domainSeparator); + bCoWPool.mock_call_verify(_order); + assertEq(bCoWPool.isValidSignature(_orderHash, abi.encode(_order)), IERC1271.isValidSignature.selector); + } +} From abc3bda8c05afd3dfe37b426acc988c2c6e6a4ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Mon, 17 Jun 2024 14:03:05 +0200 Subject: [PATCH 14/32] fix: assuming not forge address in unit test (#97) --- test/unit/BCoWFactory.t.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/test/unit/BCoWFactory.t.sol b/test/unit/BCoWFactory.t.sol index 614737a5..d7f8dfdf 100644 --- a/test/unit/BCoWFactory.t.sol +++ b/test/unit/BCoWFactory.t.sol @@ -33,6 +33,7 @@ contract BCoWFactory_Unit_Constructor is BaseBFactory_Unit_Constructor, BCoWFact contract BCoWFactory_Unit_NewBPool is BaseBFactory_Unit_NewBPool, BCoWFactoryTest { function test_Set_SolutionSettler(address _settler) public { vm.prank(owner); + assumeNotForgeAddress(_settler); bFactory = new MockBCoWFactory(_settler); vm.mockCall(_settler, abi.encodePacked(ISettlement.domainSeparator.selector), abi.encode(bytes32(0))); vm.mockCall(_settler, abi.encodePacked(ISettlement.vaultRelayer.selector), abi.encode(makeAddr('vault relayer'))); From 5701a3d672fcd6bf1290aadc9e9cdddc94c6fc58 Mon Sep 17 00:00:00 2001 From: Austrian <114922365+0xAustrian@users.noreply.github.com> Date: Mon, 17 Jun 2024 09:08:41 -0300 Subject: [PATCH 15/32] test: improve integration tests (#95) * test: merge test into PoolSwap.t.sol, test failing * fix: wrong tokens in verify(), test working * test: add skip in old BCoWPool integration * test: clean up * test: clean settlement contract before testing * refactor: separate tests in 2 files * test: rename test * test: small improvements, add cowFactory --- .forge-snapshots/settle.snap | 2 +- .forge-snapshots/swapExactAmountIn.snap | 2 +- src/contracts/BCoWPool.sol | 12 +-- test/integration/BCowPool.t.sol | 116 ++++++++++-------------- test/integration/PoolSwap.t.sol | 94 ++++++++----------- 5 files changed, 94 insertions(+), 132 deletions(-) diff --git a/.forge-snapshots/settle.snap b/.forge-snapshots/settle.snap index fc3e290e..a0a31cd1 100644 --- a/.forge-snapshots/settle.snap +++ b/.forge-snapshots/settle.snap @@ -1 +1 @@ -206786 \ No newline at end of file +244038 \ No newline at end of file diff --git a/.forge-snapshots/swapExactAmountIn.snap b/.forge-snapshots/swapExactAmountIn.snap index b424fd2d..88a77d8a 100644 --- a/.forge-snapshots/swapExactAmountIn.snap +++ b/.forge-snapshots/swapExactAmountIn.snap @@ -1 +1 @@ -107968 \ No newline at end of file +107667 \ No newline at end of file diff --git a/src/contracts/BCoWPool.sol b/src/contracts/BCoWPool.sol index 50215174..a09a42b7 100644 --- a/src/contracts/BCoWPool.sol +++ b/src/contracts/BCoWPool.sol @@ -113,8 +113,8 @@ contract BCoWPool is IERC1271, IBCoWPool, BPool, BCoWConst { /// @inheritdoc IBCoWPool function verify(GPv2Order.Data memory order) public view virtual { - Record memory inRecord = _records[address(order.sellToken)]; - Record memory outRecord = _records[address(order.buyToken)]; + Record memory inRecord = _records[address(order.buyToken)]; + Record memory outRecord = _records[address(order.sellToken)]; if (!inRecord.bound || !inRecord.bound) { revert BPool_TokenNotBound(); @@ -133,15 +133,15 @@ contract BCoWPool is IERC1271, IBCoWPool, BPool, BCoWConst { } uint256 tokenAmountOut = calcOutGivenIn({ - tokenBalanceIn: order.sellToken.balanceOf(address(this)), + tokenBalanceIn: order.buyToken.balanceOf(address(this)), tokenWeightIn: inRecord.denorm, - tokenBalanceOut: order.buyToken.balanceOf(address(this)), + tokenBalanceOut: order.sellToken.balanceOf(address(this)), tokenWeightOut: outRecord.denorm, - tokenAmountIn: order.sellAmount, + tokenAmountIn: order.buyAmount, swapFee: 0 }); - if (tokenAmountOut < order.buyAmount) { + if (tokenAmountOut < order.sellAmount) { revert BPool_TokenAmountOutBelowMinOut(); } } diff --git a/test/integration/BCowPool.t.sol b/test/integration/BCowPool.t.sol index 984a8d5b..8cb9fc8c 100644 --- a/test/integration/BCowPool.t.sol +++ b/test/integration/BCowPool.t.sol @@ -2,87 +2,70 @@ pragma solidity 0.8.25; import {GPv2TradeEncoder} from './GPv2TradeEncoder.sol'; +import {PoolSwapIntegrationTest} from './PoolSwap.t.sol'; import {IERC20} from '@cowprotocol/interfaces/IERC20.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 {BCoWConst} from 'contracts/BCoWConst.sol'; -import {BCoWPool} from 'contracts/BCoWPool.sol'; -import {BMath} from 'contracts/BMath.sol'; -import {GasSnapshot} from 'forge-gas-snapshot/GasSnapshot.sol'; -import {Test, Vm} from 'forge-std/Test.sol'; +import {BCoWFactory} from 'contracts/BCoWFactory.sol'; import {IBCoWPool} from 'interfaces/IBCoWPool.sol'; -import {IBPool} from 'interfaces/IBPool.sol'; import {ISettlement} from 'interfaces/ISettlement.sol'; -contract BCowPoolIntegrationTest is Test, BCoWConst, BMath, GasSnapshot { +contract BCowPoolIntegrationTest is PoolSwapIntegrationTest, BCoWConst { using GPv2Order for GPv2Order.Data; - IBCoWPool public pool; + BCoWFactory public cowfactory; - IERC20 public dai = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); - IERC20 public weth = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); - ISettlement public settlement = ISettlement(0x9008D19f58AAbD9eD0D60971565AA8510560ab41); address public solver = address(0xa5559C2E1302c5Ce82582A6b1E4Aec562C2FbCf4); - address public controller = makeAddr('controller'); - Vm.Wallet swapper = vm.createWallet('swapper'); + ISettlement public settlement = ISettlement(0x9008D19f58AAbD9eD0D60971565AA8510560ab41); bytes32 public constant APP_DATA = bytes32('exampleIntegrationAppData'); - uint256 public constant HUNDRED_UNITS = 100 ether; - uint256 public constant ONE_UNIT = 1 ether; - - function setUp() public { + function setUp() public override { vm.createSelectFork('mainnet', 20_012_063); - // deal controller - deal(address(dai), controller, HUNDRED_UNITS); - deal(address(weth), controller, HUNDRED_UNITS); - - // deal swapper - deal(address(weth), swapper.addr, HUNDRED_UNITS); - - vm.startPrank(controller); - // deploy - // TODO: deploy with BCoWFactory - pool = new BCoWPool(address(settlement)); - // bind - dai.approve(address(pool), type(uint256).max); - weth.approve(address(pool), type(uint256).max); - IBPool(pool).bind(address(dai), HUNDRED_UNITS, 2e18); - IBPool(pool).bind(address(weth), HUNDRED_UNITS, 8e18); + deal(address(dai), lp, HUNDRED_UNITS); + deal(address(weth), lp, HUNDRED_UNITS); + + deal(address(dai), swapper.addr, ONE_UNIT); + + cowfactory = new BCoWFactory(address(settlement)); + + vm.startPrank(lp); + pool = address(cowfactory.newBPool()); + + dai.approve(pool, type(uint256).max); + weth.approve(pool, type(uint256).max); + IBCoWPool(pool).bind(address(dai), ONE_UNIT, 2e18); // 20% weight + IBCoWPool(pool).bind(address(weth), ONE_UNIT, 8e18); // 80% weight // finalize - IBPool(pool).finalize(); + IBCoWPool(pool).finalize(); // enable trading - pool.enableTrading(APP_DATA); - } + IBCoWPool(pool).enableTrading(APP_DATA); - function testBCowPoolSwap() public { - uint256 buyAmount = HUNDRED_UNITS; - uint256 sellAmount = calcOutGivenIn({ - tokenBalanceIn: dai.balanceOf(address(pool)), - tokenWeightIn: pool.getDenormalizedWeight(address(dai)), - tokenBalanceOut: weth.balanceOf(address(pool)), - tokenWeightOut: pool.getDenormalizedWeight(address(weth)), - tokenAmountIn: buyAmount, - swapFee: 0 - }); + // clean dai and weth from the settlement + vm.startPrank(address(settlement)); + dai.transfer(address(0), dai.balanceOf(address(settlement))); + weth.transfer(address(0), weth.balanceOf(address(settlement))); + } + function _makeSwap() internal override { uint32 latestValidTimestamp = uint32(block.timestamp) + MAX_ORDER_DURATION - 1; // swapper approves weth to vaultRelayer vm.startPrank(swapper.addr); - weth.approve(settlement.vaultRelayer(), type(uint256).max); + dai.approve(settlement.vaultRelayer(), type(uint256).max); // swapper creates the order GPv2Order.Data memory swapperOrder = GPv2Order.Data({ - sellToken: weth, - buyToken: dai, + sellToken: dai, + buyToken: weth, receiver: GPv2Order.RECEIVER_SAME_AS_OWNER, - sellAmount: sellAmount, // WETH - buyAmount: buyAmount, // 100 DAI + sellAmount: DAI_AMOUNT, + buyAmount: WETH_AMOUNT, validTo: latestValidTimestamp, appData: APP_DATA, feeAmount: 0, @@ -99,11 +82,11 @@ contract BCowPoolIntegrationTest is Test, BCoWConst, BMath, GasSnapshot { // order for bPool is generated GPv2Order.Data memory poolOrder = GPv2Order.Data({ - sellToken: dai, - buyToken: weth, + sellToken: weth, + buyToken: dai, receiver: GPv2Order.RECEIVER_SAME_AS_OWNER, - sellAmount: buyAmount, // 100 DAI - buyAmount: sellAmount, // WETH + sellAmount: WETH_AMOUNT, + buyAmount: DAI_AMOUNT, validTo: latestValidTimestamp, appData: APP_DATA, feeAmount: 0, @@ -121,15 +104,15 @@ contract BCowPoolIntegrationTest is Test, BCoWConst, BMath, GasSnapshot { uint256[] memory clearingPrices = new uint256[](2); // TODO: we can use more accurate clearing prices here - clearingPrices[0] = buyAmount; - clearingPrices[1] = sellAmount; + clearingPrices[0] = DAI_AMOUNT; + clearingPrices[1] = WETH_AMOUNT; GPv2Trade.Data[] memory trades = new GPv2Trade.Data[](2); // pool's trade trades[0] = GPv2Trade.Data({ - sellTokenIndex: 1, - buyTokenIndex: 0, + sellTokenIndex: 0, + buyTokenIndex: 1, receiver: poolOrder.receiver, sellAmount: poolOrder.sellAmount, buyAmount: poolOrder.buyAmount, @@ -138,13 +121,13 @@ contract BCowPoolIntegrationTest is Test, BCoWConst, BMath, GasSnapshot { feeAmount: poolOrder.feeAmount, flags: GPv2TradeEncoder.encodeFlags(poolOrder, GPv2Signing.Scheme.Eip1271), executedAmount: poolOrder.sellAmount, - signature: abi.encodePacked(address(pool), poolSig) + signature: abi.encodePacked(pool, poolSig) }); // swapper's trade trades[1] = GPv2Trade.Data({ - sellTokenIndex: 0, - buyTokenIndex: 1, + sellTokenIndex: 1, + buyTokenIndex: 0, receiver: swapperOrder.receiver, sellAmount: swapperOrder.sellAmount, buyAmount: swapperOrder.buyAmount, @@ -161,10 +144,10 @@ contract BCowPoolIntegrationTest is Test, BCoWConst, BMath, GasSnapshot { [new GPv2Interaction.Data[](1), new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0)]; interactions[0][0] = GPv2Interaction.Data({ - target: address(pool), + target: pool, value: 0, callData: abi.encodeWithSelector( - IBCoWPool.commit.selector, poolOrder.hash(pool.SOLUTION_SETTLER_DOMAIN_SEPARATOR()) + IBCoWPool.commit.selector, poolOrder.hash(IBCoWPool(pool).SOLUTION_SETTLER_DOMAIN_SEPARATOR()) ) }); @@ -173,12 +156,5 @@ contract BCowPoolIntegrationTest is Test, BCoWConst, BMath, GasSnapshot { snapStart('settle'); settlement.settle(tokens, clearingPrices, trades, interactions); snapEnd(); - - // assert swapper balance - assertEq(dai.balanceOf(swapper.addr), buyAmount); - assertEq(weth.balanceOf(swapper.addr), HUNDRED_UNITS - sellAmount); - // assert pool balance - assertEq(dai.balanceOf(address(pool)), HUNDRED_UNITS - buyAmount); - assertEq(weth.balanceOf(address(pool)), HUNDRED_UNITS + sellAmount); } } diff --git a/test/integration/PoolSwap.t.sol b/test/integration/PoolSwap.t.sol index 016b4d7c..a48a58c5 100644 --- a/test/integration/PoolSwap.t.sol +++ b/test/integration/PoolSwap.t.sol @@ -1,62 +1,63 @@ +// SPDX-License-Identifier: MIT pragma solidity 0.8.25; -import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; -import {Test} from 'forge-std/Test.sol'; - +import {IERC20} from '@cowprotocol/interfaces/IERC20.sol'; import {BFactory} from 'contracts/BFactory.sol'; -import {IBPool} from 'interfaces/IBPool.sol'; - import {GasSnapshot} from 'forge-gas-snapshot/GasSnapshot.sol'; +import {Test, Vm} from 'forge-std/Test.sol'; +import {IBFactory} from 'interfaces/IBFactory.sol'; +import {IBPool} from 'interfaces/IBPool.sol'; abstract contract PoolSwapIntegrationTest is Test, GasSnapshot { - BFactory public factory; - IBPool public pool; + address public pool; + IBFactory public factory; - IERC20 public tokenA; - IERC20 public tokenB; + IERC20 public dai = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); + IERC20 public weth = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); - address public lp = address(420); - address public swapper = address(69); + address public lp = makeAddr('lp'); - function setUp() public { - tokenA = IERC20(address(deployMockERC20('TokenA', 'TKA', 18))); - tokenB = IERC20(address(deployMockERC20('TokenB', 'TKB', 18))); + Vm.Wallet swapper = vm.createWallet('swapper'); - deal(address(tokenA), address(lp), 100e18); - deal(address(tokenB), address(lp), 100e18); + uint256 public constant HUNDRED_UNITS = 100 ether; + uint256 public constant ONE_UNIT = 1 ether; + // NOTE: hardcoded from test result + uint256 public constant WETH_AMOUNT = 0.096397921069149814e18; + uint256 public constant DAI_AMOUNT = 0.5e18; - deal(address(tokenA), address(swapper), 1e18); + function setUp() public virtual { + vm.createSelectFork('mainnet', 20_012_063); factory = new BFactory(); - vm.startPrank(lp); - pool = factory.newBPool(); - - tokenA.approve(address(pool), type(uint256).max); - tokenB.approve(address(pool), type(uint256).max); + deal(address(dai), lp, HUNDRED_UNITS); + deal(address(weth), lp, HUNDRED_UNITS); - pool.bind(address(tokenA), 1e18, 2e18); // 20% weight? - pool.bind(address(tokenB), 1e18, 8e18); // 80% + deal(address(dai), swapper.addr, ONE_UNIT); - pool.finalize(); - vm.stopPrank(); + vm.startPrank(lp); + pool = address(factory.newBPool()); + + dai.approve(pool, type(uint256).max); + weth.approve(pool, type(uint256).max); + IBPool(pool).bind(address(dai), ONE_UNIT, 2e18); // 20% weight + IBPool(pool).bind(address(weth), ONE_UNIT, 8e18); // 80% weight + // finalize + IBPool(pool).finalize(); } function testSimpleSwap() public { _makeSwap(); - assertEq(tokenA.balanceOf(address(swapper)), 0.5e18); - // NOTE: hardcoded from test result - assertEq(tokenB.balanceOf(address(swapper)), 0.096397921069149814e18); + assertEq(dai.balanceOf(swapper.addr), DAI_AMOUNT); + assertEq(weth.balanceOf(swapper.addr), WETH_AMOUNT); vm.startPrank(lp); - uint256 lpBalance = pool.balanceOf(address(lp)); - pool.exitPool(lpBalance, new uint256[](2)); + uint256 lpBalance = IBPool(pool).balanceOf(lp); + IBPool(pool).exitPool(lpBalance, new uint256[](2)); - // NOTE: no swap fees involved - assertEq(tokenA.balanceOf(address(lp)), 100.5e18); // initial 100 + 0.5 tokenA - // NOTE: hardcoded from test result - assertEq(tokenB.balanceOf(address(lp)), 99.903602078930850186e18); // initial 100 - ~0.09 tokenB + assertEq(dai.balanceOf(lp), HUNDRED_UNITS + DAI_AMOUNT); // initial 100 + 0.5 dai + assertEq(weth.balanceOf(lp), HUNDRED_UNITS - WETH_AMOUNT); // initial 100 - ~0.09 weth } function _makeSwap() internal virtual; @@ -64,29 +65,14 @@ abstract contract PoolSwapIntegrationTest is Test, GasSnapshot { contract DirectPoolSwapIntegrationTest is PoolSwapIntegrationTest { function _makeSwap() internal override { - vm.startPrank(swapper); - tokenA.approve(address(pool), type(uint256).max); + vm.startPrank(swapper.addr); + dai.approve(pool, type(uint256).max); - // swap 0.5 tokenA for tokenB + // swap 0.5 dai for weth snapStart('swapExactAmountIn'); - pool.swapExactAmountIn(address(tokenA), 0.5e18, address(tokenB), 0, type(uint256).max); + IBPool(pool).swapExactAmountIn(address(dai), DAI_AMOUNT, address(weth), 0, type(uint256).max); snapEnd(); vm.stopPrank(); } } - -contract IndirectPoolSwapIntegrationTest is PoolSwapIntegrationTest { - function _makeSwap() internal override { - vm.startPrank(address(pool)); - tokenA.approve(address(swapper), type(uint256).max); - tokenB.approve(address(swapper), type(uint256).max); - vm.stopPrank(); - - vm.startPrank(swapper); - // swap 0.5 tokenA for tokenB - tokenA.transfer(address(pool), 0.5e18); - tokenB.transferFrom(address(pool), address(swapper), 0.096397921069149814e18); - vm.stopPrank(); - } -} From ec6717506bb185e3a0c89ab50f09506a4065941e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Mon, 17 Jun 2024 14:18:21 +0200 Subject: [PATCH 16/32] feat: adding inverse path on integration test (#96) * test: small improvements, add cowFactory * refactor: simplifying setUp * refactor: casting pool to IBPool on tests * fix: comment --- .forge-snapshots/settle.snap | 1 - .forge-snapshots/settlementCoWSwap.snap | 1 + .../settlementCoWSwapInverse.snap | 1 + .forge-snapshots/swapExactAmountIn.snap | 2 +- .../swapExactAmountInInverse.snap | 1 + test/integration/BCowPool.t.sol | 145 ++++++++++++++---- test/integration/PoolSwap.t.sol | 67 ++++++-- test/unit/BCoWFactory.t.sol | 1 - 8 files changed, 175 insertions(+), 44 deletions(-) delete mode 100644 .forge-snapshots/settle.snap create mode 100644 .forge-snapshots/settlementCoWSwap.snap create mode 100644 .forge-snapshots/settlementCoWSwapInverse.snap create mode 100644 .forge-snapshots/swapExactAmountInInverse.snap diff --git a/.forge-snapshots/settle.snap b/.forge-snapshots/settle.snap deleted file mode 100644 index a0a31cd1..00000000 --- a/.forge-snapshots/settle.snap +++ /dev/null @@ -1 +0,0 @@ -244038 \ No newline at end of file diff --git a/.forge-snapshots/settlementCoWSwap.snap b/.forge-snapshots/settlementCoWSwap.snap new file mode 100644 index 00000000..1aa0d382 --- /dev/null +++ b/.forge-snapshots/settlementCoWSwap.snap @@ -0,0 +1 @@ +209838 \ No newline at end of file diff --git a/.forge-snapshots/settlementCoWSwapInverse.snap b/.forge-snapshots/settlementCoWSwapInverse.snap new file mode 100644 index 00000000..26ecbba8 --- /dev/null +++ b/.forge-snapshots/settlementCoWSwapInverse.snap @@ -0,0 +1 @@ +188532 \ No newline at end of file diff --git a/.forge-snapshots/swapExactAmountIn.snap b/.forge-snapshots/swapExactAmountIn.snap index 88a77d8a..9b366fcf 100644 --- a/.forge-snapshots/swapExactAmountIn.snap +++ b/.forge-snapshots/swapExactAmountIn.snap @@ -1 +1 @@ -107667 \ No newline at end of file +107769 \ No newline at end of file diff --git a/.forge-snapshots/swapExactAmountInInverse.snap b/.forge-snapshots/swapExactAmountInInverse.snap new file mode 100644 index 00000000..659b0e08 --- /dev/null +++ b/.forge-snapshots/swapExactAmountInInverse.snap @@ -0,0 +1 @@ +86282 \ No newline at end of file diff --git a/test/integration/BCowPool.t.sol b/test/integration/BCowPool.t.sol index 8cb9fc8c..245d0327 100644 --- a/test/integration/BCowPool.t.sol +++ b/test/integration/BCowPool.t.sol @@ -10,14 +10,14 @@ 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'; + import {IBCoWPool} from 'interfaces/IBCoWPool.sol'; +import {IBFactory} from 'interfaces/IBFactory.sol'; import {ISettlement} from 'interfaces/ISettlement.sol'; contract BCowPoolIntegrationTest is PoolSwapIntegrationTest, BCoWConst { using GPv2Order for GPv2Order.Data; - BCoWFactory public cowfactory; - address public solver = address(0xa5559C2E1302c5Ce82582A6b1E4Aec562C2FbCf4); ISettlement public settlement = ISettlement(0x9008D19f58AAbD9eD0D60971565AA8510560ab41); @@ -25,37 +25,20 @@ contract BCowPoolIntegrationTest is PoolSwapIntegrationTest, BCoWConst { bytes32 public constant APP_DATA = bytes32('exampleIntegrationAppData'); function setUp() public override { - vm.createSelectFork('mainnet', 20_012_063); - - deal(address(dai), lp, HUNDRED_UNITS); - deal(address(weth), lp, HUNDRED_UNITS); - - deal(address(dai), swapper.addr, ONE_UNIT); + super.setUp(); - cowfactory = new BCoWFactory(address(settlement)); - - vm.startPrank(lp); - pool = address(cowfactory.newBPool()); - - dai.approve(pool, type(uint256).max); - weth.approve(pool, type(uint256).max); - IBCoWPool(pool).bind(address(dai), ONE_UNIT, 2e18); // 20% weight - IBCoWPool(pool).bind(address(weth), ONE_UNIT, 8e18); // 80% weight - // finalize - IBCoWPool(pool).finalize(); // enable trading - IBCoWPool(pool).enableTrading(APP_DATA); + IBCoWPool(address(pool)).enableTrading(APP_DATA); + } - // clean dai and weth from the settlement - vm.startPrank(address(settlement)); - dai.transfer(address(0), dai.balanceOf(address(settlement))); - weth.transfer(address(0), weth.balanceOf(address(settlement))); + function _deployFactory() internal override returns (IBFactory) { + return new BCoWFactory(address(settlement)); } function _makeSwap() internal override { uint32 latestValidTimestamp = uint32(block.timestamp) + MAX_ORDER_DURATION - 1; - // swapper approves weth to vaultRelayer + // swapper approves dai to vaultRelayer vm.startPrank(swapper.addr); dai.approve(settlement.vaultRelayer(), type(uint256).max); @@ -144,16 +127,122 @@ contract BCowPoolIntegrationTest is PoolSwapIntegrationTest, BCoWConst { [new GPv2Interaction.Data[](1), new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0)]; interactions[0][0] = GPv2Interaction.Data({ - target: pool, + target: address(pool), + value: 0, + callData: abi.encodeWithSelector( + IBCoWPool.commit.selector, poolOrder.hash(IBCoWPool(address(pool)).SOLUTION_SETTLER_DOMAIN_SEPARATOR()) + ) + }); + + // finally, settle + vm.startPrank(solver); + snapStart('settlementCoWSwap'); + settlement.settle(tokens, clearingPrices, trades, interactions); + snapEnd(); + } + + function _makeSwapInverse() internal override { + uint32 latestValidTimestamp = uint32(block.timestamp) + MAX_ORDER_DURATION - 1; + + // swapper approves weth to vaultRelayer + vm.startPrank(swapperInverse.addr); + weth.approve(settlement.vaultRelayer(), type(uint256).max); + + // swapper creates the order + GPv2Order.Data memory swapperOrder = GPv2Order.Data({ + sellToken: weth, + buyToken: dai, + receiver: GPv2Order.RECEIVER_SAME_AS_OWNER, + sellAmount: WETH_AMOUNT_INVERSE, + buyAmount: DAI_AMOUNT_INVERSE, + validTo: latestValidTimestamp, + appData: APP_DATA, + feeAmount: 0, + kind: GPv2Order.KIND_BUY, + partiallyFillable: false, + buyTokenBalance: GPv2Order.BALANCE_ERC20, + sellTokenBalance: GPv2Order.BALANCE_ERC20 + }); + + // swapper signs the order + (uint8 v, bytes32 r, bytes32 s) = + vm.sign(swapperInverse.privateKey, GPv2Order.hash(swapperOrder, settlement.domainSeparator())); + bytes memory swapperSig = abi.encodePacked(r, s, v); + + // order for bPool is generated + GPv2Order.Data memory poolOrder = GPv2Order.Data({ + sellToken: dai, + buyToken: weth, + receiver: GPv2Order.RECEIVER_SAME_AS_OWNER, + sellAmount: DAI_AMOUNT_INVERSE, + buyAmount: WETH_AMOUNT_INVERSE, + validTo: latestValidTimestamp, + appData: APP_DATA, + feeAmount: 0, + kind: GPv2Order.KIND_SELL, + partiallyFillable: true, + sellTokenBalance: GPv2Order.BALANCE_ERC20, + buyTokenBalance: GPv2Order.BALANCE_ERC20 + }); + bytes memory poolSig = abi.encode(poolOrder); + + // solver prepares for call settle() + IERC20[] memory tokens = new IERC20[](2); + tokens[0] = IERC20(dai); + tokens[1] = IERC20(weth); + + uint256[] memory clearingPrices = new uint256[](2); + // TODO: we can use more accurate clearing prices here + clearingPrices[0] = WETH_AMOUNT_INVERSE; + clearingPrices[1] = DAI_AMOUNT_INVERSE; + + GPv2Trade.Data[] memory trades = new GPv2Trade.Data[](2); + + // pool's trade + trades[0] = GPv2Trade.Data({ + sellTokenIndex: 0, + buyTokenIndex: 1, + receiver: poolOrder.receiver, + sellAmount: poolOrder.sellAmount, + buyAmount: poolOrder.buyAmount, + validTo: poolOrder.validTo, + appData: poolOrder.appData, + feeAmount: poolOrder.feeAmount, + flags: GPv2TradeEncoder.encodeFlags(poolOrder, GPv2Signing.Scheme.Eip1271), + executedAmount: poolOrder.sellAmount, + signature: abi.encodePacked(pool, poolSig) + }); + + // swapper's trade + trades[1] = GPv2Trade.Data({ + sellTokenIndex: 1, + buyTokenIndex: 0, + receiver: swapperOrder.receiver, + sellAmount: swapperOrder.sellAmount, + buyAmount: swapperOrder.buyAmount, + validTo: swapperOrder.validTo, + appData: swapperOrder.appData, + feeAmount: swapperOrder.feeAmount, + flags: GPv2TradeEncoder.encodeFlags(swapperOrder, GPv2Signing.Scheme.Eip712), + executedAmount: swapperOrder.sellAmount, + signature: swapperSig + }); + + // in the first interactions, save the commitment + GPv2Interaction.Data[][3] memory interactions = + [new GPv2Interaction.Data[](1), new GPv2Interaction.Data[](0), new GPv2Interaction.Data[](0)]; + + interactions[0][0] = GPv2Interaction.Data({ + target: address(pool), value: 0, callData: abi.encodeWithSelector( - IBCoWPool.commit.selector, poolOrder.hash(IBCoWPool(pool).SOLUTION_SETTLER_DOMAIN_SEPARATOR()) + IBCoWPool.commit.selector, poolOrder.hash(IBCoWPool(address(pool)).SOLUTION_SETTLER_DOMAIN_SEPARATOR()) ) }); // finally, settle vm.startPrank(solver); - snapStart('settle'); + snapStart('settlementCoWSwapInverse'); settlement.settle(tokens, clearingPrices, trades, interactions); snapEnd(); } diff --git a/test/integration/PoolSwap.t.sol b/test/integration/PoolSwap.t.sol index a48a58c5..60429c3d 100644 --- a/test/integration/PoolSwap.t.sol +++ b/test/integration/PoolSwap.t.sol @@ -9,7 +9,7 @@ import {IBFactory} from 'interfaces/IBFactory.sol'; import {IBPool} from 'interfaces/IBPool.sol'; abstract contract PoolSwapIntegrationTest is Test, GasSnapshot { - address public pool; + IBPool public pool; IBFactory public factory; IERC20 public dai = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); @@ -18,59 +18,100 @@ abstract contract PoolSwapIntegrationTest is Test, GasSnapshot { address public lp = makeAddr('lp'); Vm.Wallet swapper = vm.createWallet('swapper'); + Vm.Wallet swapperInverse = vm.createWallet('swapperInverse'); uint256 public constant HUNDRED_UNITS = 100 ether; uint256 public constant ONE_UNIT = 1 ether; + // NOTE: hardcoded from test result uint256 public constant WETH_AMOUNT = 0.096397921069149814e18; uint256 public constant DAI_AMOUNT = 0.5e18; + uint256 public constant WETH_AMOUNT_INVERSE = 0.1e18; + // NOTE: hardcoded from test result + uint256 public constant DAI_AMOUNT_INVERSE = 0.316986296266343639e18; + function setUp() public virtual { vm.createSelectFork('mainnet', 20_012_063); - factory = new BFactory(); + factory = _deployFactory(); deal(address(dai), lp, HUNDRED_UNITS); deal(address(weth), lp, HUNDRED_UNITS); deal(address(dai), swapper.addr, ONE_UNIT); + deal(address(weth), swapperInverse.addr, ONE_UNIT); vm.startPrank(lp); - pool = address(factory.newBPool()); + pool = factory.newBPool(); - dai.approve(pool, type(uint256).max); - weth.approve(pool, type(uint256).max); - IBPool(pool).bind(address(dai), ONE_UNIT, 2e18); // 20% weight - IBPool(pool).bind(address(weth), ONE_UNIT, 8e18); // 80% weight + dai.approve(address(pool), type(uint256).max); + weth.approve(address(pool), type(uint256).max); + pool.bind(address(dai), ONE_UNIT, 2e18); // 20% weight + pool.bind(address(weth), ONE_UNIT, 8e18); // 80% weight // finalize - IBPool(pool).finalize(); + pool.finalize(); } function testSimpleSwap() public { _makeSwap(); - assertEq(dai.balanceOf(swapper.addr), DAI_AMOUNT); + assertEq(dai.balanceOf(swapper.addr), ONE_UNIT - DAI_AMOUNT); assertEq(weth.balanceOf(swapper.addr), WETH_AMOUNT); vm.startPrank(lp); - uint256 lpBalance = IBPool(pool).balanceOf(lp); - IBPool(pool).exitPool(lpBalance, new uint256[](2)); + uint256 lpBalance = pool.balanceOf(lp); + pool.exitPool(lpBalance, new uint256[](2)); assertEq(dai.balanceOf(lp), HUNDRED_UNITS + DAI_AMOUNT); // initial 100 + 0.5 dai assertEq(weth.balanceOf(lp), HUNDRED_UNITS - WETH_AMOUNT); // initial 100 - ~0.09 weth } + function testSimpleSwapInverse() public { + _makeSwapInverse(); + assertEq(dai.balanceOf(swapperInverse.addr), DAI_AMOUNT_INVERSE); + assertEq(weth.balanceOf(swapperInverse.addr), ONE_UNIT - WETH_AMOUNT_INVERSE); + + vm.startPrank(lp); + + uint256 lpBalance = pool.balanceOf(address(lp)); + pool.exitPool(lpBalance, new uint256[](2)); + + assertEq(dai.balanceOf(address(lp)), HUNDRED_UNITS - DAI_AMOUNT_INVERSE); // initial 100 - ~0.5 dai + assertEq(weth.balanceOf(address(lp)), HUNDRED_UNITS + WETH_AMOUNT_INVERSE); // initial 100 + 0.1 tokenB + } + + function _deployFactory() internal virtual returns (IBFactory); + function _makeSwap() internal virtual; + + function _makeSwapInverse() internal virtual; } contract DirectPoolSwapIntegrationTest is PoolSwapIntegrationTest { + function _deployFactory() internal override returns (IBFactory) { + return new BFactory(); + } + function _makeSwap() internal override { vm.startPrank(swapper.addr); - dai.approve(pool, type(uint256).max); + dai.approve(address(pool), type(uint256).max); // swap 0.5 dai for weth snapStart('swapExactAmountIn'); - IBPool(pool).swapExactAmountIn(address(dai), DAI_AMOUNT, address(weth), 0, type(uint256).max); + pool.swapExactAmountIn(address(dai), DAI_AMOUNT, address(weth), 0, type(uint256).max); + snapEnd(); + + vm.stopPrank(); + } + + function _makeSwapInverse() internal override { + vm.startPrank(swapperInverse.addr); + weth.approve(address(pool), type(uint256).max); + + // swap 0.1 weth for dai + snapStart('swapExactAmountInInverse'); + pool.swapExactAmountIn(address(weth), WETH_AMOUNT_INVERSE, address(dai), 0, type(uint256).max); snapEnd(); vm.stopPrank(); diff --git a/test/unit/BCoWFactory.t.sol b/test/unit/BCoWFactory.t.sol index d7f8dfdf..614737a5 100644 --- a/test/unit/BCoWFactory.t.sol +++ b/test/unit/BCoWFactory.t.sol @@ -33,7 +33,6 @@ contract BCoWFactory_Unit_Constructor is BaseBFactory_Unit_Constructor, BCoWFact contract BCoWFactory_Unit_NewBPool is BaseBFactory_Unit_NewBPool, BCoWFactoryTest { function test_Set_SolutionSettler(address _settler) public { vm.prank(owner); - assumeNotForgeAddress(_settler); bFactory = new MockBCoWFactory(_settler); vm.mockCall(_settler, abi.encodePacked(ISettlement.domainSeparator.selector), abi.encode(bytes32(0))); vm.mockCall(_settler, abi.encodePacked(ISettlement.vaultRelayer.selector), abi.encode(makeAddr('vault relayer'))); From 0b00b3c84f418a37962a5d432fb5053a2c3c3140 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Mon, 17 Jun 2024 16:07:09 +0200 Subject: [PATCH 17/32] fix: assuming not forge address in test (#101) --- test/unit/BCoWFactory.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/BCoWFactory.t.sol b/test/unit/BCoWFactory.t.sol index 614737a5..14fe6b67 100644 --- a/test/unit/BCoWFactory.t.sol +++ b/test/unit/BCoWFactory.t.sol @@ -32,7 +32,7 @@ contract BCoWFactory_Unit_Constructor is BaseBFactory_Unit_Constructor, BCoWFact contract BCoWFactory_Unit_NewBPool is BaseBFactory_Unit_NewBPool, BCoWFactoryTest { function test_Set_SolutionSettler(address _settler) public { - vm.prank(owner); + assumeNotForgeAddress(_settler); bFactory = new MockBCoWFactory(_settler); vm.mockCall(_settler, abi.encodePacked(ISettlement.domainSeparator.selector), abi.encode(bytes32(0))); vm.mockCall(_settler, abi.encodePacked(ISettlement.vaultRelayer.selector), abi.encode(makeAddr('vault relayer'))); From 8c050c270e3e0ea3c10fd27d89b6a5a034001145 Mon Sep 17 00:00:00 2001 From: Austrian <114922365+0xAustrian@users.noreply.github.com> Date: Mon, 17 Jun 2024 11:51:36 -0300 Subject: [PATCH 18/32] feat: add deployment scripts (#99) * feat: add deployment scripts * feat: remove deployer on script * feat: update deploy scripts on package.json * feat: add settlement address * fix: settlement contract name * fix: lint warning --- package.json | 6 +++-- script/DeployBCoWFactory.s.sol | 17 ++++++++++++++ script/{Deploy.s.sol => DeployBFactory.s.sol} | 7 +++--- script/Params.s.sol | 23 +++++++++++++++---- 4 files changed, 42 insertions(+), 11 deletions(-) create mode 100644 script/DeployBCoWFactory.s.sol rename script/{Deploy.s.sol => DeployBFactory.s.sol} (72%) diff --git a/package.json b/package.json index 97651268..7aa72093 100644 --- a/package.json +++ b/package.json @@ -16,8 +16,10 @@ "build": "forge build", "build:optimized": "FOUNDRY_PROFILE=optimized forge build", "coverage": "forge coverage --match-contract Unit", - "deploy:mainnet": "bash -c 'source .env && forge script Deploy -vvvvv --rpc-url $MAINNET_RPC --broadcast --chain mainnet --private-key $MAINNET_DEPLOYER_PK --verify --etherscan-api-key $ETHERSCAN_API_KEY'", - "deploy:testnet": "bash -c 'source .env && forge script Deploy -vvvvv --rpc-url $SEPOLIA_RPC --broadcast --chain sepolia --private-key $SEPOLIA_DEPLOYER_PK --verify --etherscan-api-key $ETHERSCAN_API_KEY'", + "deploy:bcowfactory:mainnet": "bash -c 'source .env && forge script DeployBCoWFactory -vvvvv --rpc-url $MAINNET_RPC --broadcast --chain mainnet --private-key $MAINNET_DEPLOYER_PK --verify --etherscan-api-key $ETHERSCAN_API_KEY'", + "deploy:bcowfactory:testnet": "bash -c 'source .env && forge script DeployBCoWFactory -vvvvv --rpc-url $SEPOLIA_RPC --broadcast --chain sepolia --private-key $SEPOLIA_DEPLOYER_PK --verify --etherscan-api-key $ETHERSCAN_API_KEY'", + "deploy:bfactory:mainnet": "bash -c 'source .env && forge script DeployBFactory -vvvvv --rpc-url $MAINNET_RPC --broadcast --chain mainnet --private-key $MAINNET_DEPLOYER_PK --verify --etherscan-api-key $ETHERSCAN_API_KEY'", + "deploy:bfactory:testnet": "bash -c 'source .env && forge script DeployBFactory -vvvvv --rpc-url $SEPOLIA_RPC --broadcast --chain sepolia --private-key $SEPOLIA_DEPLOYER_PK --verify --etherscan-api-key $ETHERSCAN_API_KEY'", "lint:check": "solhint 'src/**/*.sol' 'test/**/*.sol' 'script/**/*.sol' && forge fmt --check", "lint:fix": "solhint --fix 'src/**/*.sol' 'test/**/*.sol' 'script/**/*.sol' && sort-package-json && forge fmt", "lint:natspec": "npx @defi-wonderland/natspec-smells --config natspec-smells.config.js", diff --git a/script/DeployBCoWFactory.s.sol b/script/DeployBCoWFactory.s.sol new file mode 100644 index 00000000..2f16873f --- /dev/null +++ b/script/DeployBCoWFactory.s.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import {BCoWFactory} from 'contracts/BCoWFactory.sol'; +import {Script} from 'forge-std/Script.sol'; +import {Params} from 'script/Params.s.sol'; + +contract DeployBCoWFactory is Script, Params { + function run() public { + BCoWFactoryDeploymentParams memory _params = _bCoWFactoryDeploymentParams[block.chainid]; + + vm.startBroadcast(); + BCoWFactory bCoWFactory = new BCoWFactory(_params.settlement); + bCoWFactory.setBLabs(_params.bLabs); + vm.stopBroadcast(); + } +} diff --git a/script/Deploy.s.sol b/script/DeployBFactory.s.sol similarity index 72% rename from script/Deploy.s.sol rename to script/DeployBFactory.s.sol index 608af6bc..6987cc81 100644 --- a/script/Deploy.s.sol +++ b/script/DeployBFactory.s.sol @@ -2,13 +2,12 @@ pragma solidity 0.8.25; import {BFactory} from 'contracts/BFactory.sol'; -import {Params} from 'script/Params.s.sol'; - import {Script} from 'forge-std/Script.sol'; +import {Params} from 'script/Params.s.sol'; -contract Deploy is Script, Params { +contract DeployBFactory is Script, Params { function run() public { - DeploymentParams memory _params = _deploymentParams[block.chainid]; + BFactoryDeploymentParams memory _params = _bFactoryDeploymentParams[block.chainid]; vm.startBroadcast(); BFactory bFactory = new BFactory(); diff --git a/script/Params.s.sol b/script/Params.s.sol index 15a5aaa6..3cbeb2a3 100644 --- a/script/Params.s.sol +++ b/script/Params.s.sol @@ -2,18 +2,31 @@ pragma solidity 0.8.25; contract Params { - struct DeploymentParams { + struct BFactoryDeploymentParams { address bLabs; } - /// @notice Deployment parameters for each chain - mapping(uint256 _chainId => DeploymentParams _params) internal _deploymentParams; + struct BCoWFactoryDeploymentParams { + address bLabs; + address settlement; + } + + /// @notice BFactory deployment parameters for each chain + mapping(uint256 _chainId => BFactoryDeploymentParams _params) internal _bFactoryDeploymentParams; + + /// @notice BCoWFactory deployment parameters for each chain + mapping(uint256 _chainId => BCoWFactoryDeploymentParams _params) internal _bCoWFactoryDeploymentParams; + + /// @notice Settlement address + address internal constant _GPV2_SETTLEMENT = 0x9008D19f58AAbD9eD0D60971565AA8510560ab41; constructor() { // Mainnet - _deploymentParams[1] = DeploymentParams(address(this)); + _bFactoryDeploymentParams[1] = BFactoryDeploymentParams(address(this)); + _bCoWFactoryDeploymentParams[1] = BCoWFactoryDeploymentParams(address(this), _GPV2_SETTLEMENT); // Sepolia - _deploymentParams[11_155_111] = DeploymentParams(address(this)); + _bFactoryDeploymentParams[11_155_111] = BFactoryDeploymentParams(address(this)); + _bCoWFactoryDeploymentParams[11_155_111] = BCoWFactoryDeploymentParams(address(this), _GPV2_SETTLEMENT); } } From cdc425f78d4515e7abf50e59e89ef84f8a830884 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Mon, 17 Jun 2024 18:03:54 +0200 Subject: [PATCH 19/32] feat: improving amounts expectations on integration tests (#98) --- .forge-snapshots/settlementCoWSwap.snap | 2 +- .../settlementCoWSwapInverse.snap | 2 +- .forge-snapshots/swapExactAmountIn.snap | 2 +- .../swapExactAmountInInverse.snap | 2 +- test/integration/BCowPool.t.sol | 12 ++-- test/integration/PoolSwap.t.sol | 59 ++++++++++++------- 6 files changed, 48 insertions(+), 31 deletions(-) diff --git a/.forge-snapshots/settlementCoWSwap.snap b/.forge-snapshots/settlementCoWSwap.snap index 1aa0d382..26ecbba8 100644 --- a/.forge-snapshots/settlementCoWSwap.snap +++ b/.forge-snapshots/settlementCoWSwap.snap @@ -1 +1 @@ -209838 \ No newline at end of file +188532 \ No newline at end of file diff --git a/.forge-snapshots/settlementCoWSwapInverse.snap b/.forge-snapshots/settlementCoWSwapInverse.snap index 26ecbba8..69865296 100644 --- a/.forge-snapshots/settlementCoWSwapInverse.snap +++ b/.forge-snapshots/settlementCoWSwapInverse.snap @@ -1 +1 @@ -188532 \ No newline at end of file +198372 \ No newline at end of file diff --git a/.forge-snapshots/swapExactAmountIn.snap b/.forge-snapshots/swapExactAmountIn.snap index 9b366fcf..536f7edc 100644 --- a/.forge-snapshots/swapExactAmountIn.snap +++ b/.forge-snapshots/swapExactAmountIn.snap @@ -1 +1 @@ -107769 \ No newline at end of file +86463 \ No newline at end of file diff --git a/.forge-snapshots/swapExactAmountInInverse.snap b/.forge-snapshots/swapExactAmountInInverse.snap index 659b0e08..16907fa9 100644 --- a/.forge-snapshots/swapExactAmountInInverse.snap +++ b/.forge-snapshots/swapExactAmountInInverse.snap @@ -1 +1 @@ -86282 \ No newline at end of file +96122 \ No newline at end of file diff --git a/test/integration/BCowPool.t.sol b/test/integration/BCowPool.t.sol index 245d0327..469fe76e 100644 --- a/test/integration/BCowPool.t.sol +++ b/test/integration/BCowPool.t.sol @@ -48,7 +48,7 @@ contract BCowPoolIntegrationTest is PoolSwapIntegrationTest, BCoWConst { buyToken: weth, receiver: GPv2Order.RECEIVER_SAME_AS_OWNER, sellAmount: DAI_AMOUNT, - buyAmount: WETH_AMOUNT, + buyAmount: WETH_OUT_AMOUNT, validTo: latestValidTimestamp, appData: APP_DATA, feeAmount: 0, @@ -68,7 +68,7 @@ contract BCowPoolIntegrationTest is PoolSwapIntegrationTest, BCoWConst { sellToken: weth, buyToken: dai, receiver: GPv2Order.RECEIVER_SAME_AS_OWNER, - sellAmount: WETH_AMOUNT, + sellAmount: WETH_OUT_AMOUNT, buyAmount: DAI_AMOUNT, validTo: latestValidTimestamp, appData: APP_DATA, @@ -88,7 +88,7 @@ contract BCowPoolIntegrationTest is PoolSwapIntegrationTest, BCoWConst { uint256[] memory clearingPrices = new uint256[](2); // TODO: we can use more accurate clearing prices here clearingPrices[0] = DAI_AMOUNT; - clearingPrices[1] = WETH_AMOUNT; + clearingPrices[1] = WETH_OUT_AMOUNT; GPv2Trade.Data[] memory trades = new GPv2Trade.Data[](2); @@ -154,7 +154,7 @@ contract BCowPoolIntegrationTest is PoolSwapIntegrationTest, BCoWConst { buyToken: dai, receiver: GPv2Order.RECEIVER_SAME_AS_OWNER, sellAmount: WETH_AMOUNT_INVERSE, - buyAmount: DAI_AMOUNT_INVERSE, + buyAmount: DAI_OUT_AMOUNT_INVERSE, validTo: latestValidTimestamp, appData: APP_DATA, feeAmount: 0, @@ -174,7 +174,7 @@ contract BCowPoolIntegrationTest is PoolSwapIntegrationTest, BCoWConst { sellToken: dai, buyToken: weth, receiver: GPv2Order.RECEIVER_SAME_AS_OWNER, - sellAmount: DAI_AMOUNT_INVERSE, + sellAmount: DAI_OUT_AMOUNT_INVERSE, buyAmount: WETH_AMOUNT_INVERSE, validTo: latestValidTimestamp, appData: APP_DATA, @@ -194,7 +194,7 @@ contract BCowPoolIntegrationTest is PoolSwapIntegrationTest, BCoWConst { uint256[] memory clearingPrices = new uint256[](2); // TODO: we can use more accurate clearing prices here clearingPrices[0] = WETH_AMOUNT_INVERSE; - clearingPrices[1] = DAI_AMOUNT_INVERSE; + clearingPrices[1] = DAI_OUT_AMOUNT_INVERSE; GPv2Trade.Data[] memory trades = new GPv2Trade.Data[](2); diff --git a/test/integration/PoolSwap.t.sol b/test/integration/PoolSwap.t.sol index 60429c3d..224a4c6d 100644 --- a/test/integration/PoolSwap.t.sol +++ b/test/integration/PoolSwap.t.sol @@ -20,65 +20,82 @@ abstract contract PoolSwapIntegrationTest is Test, GasSnapshot { Vm.Wallet swapper = vm.createWallet('swapper'); Vm.Wallet swapperInverse = vm.createWallet('swapperInverse'); - uint256 public constant HUNDRED_UNITS = 100 ether; + /** + * For the simplicity of this test, a 1000 DAI:1 ETH reference quote is used. + * A weight distribution of 80% DAI and 20% ETH is used. + * To achieve the reference quote, the pool should have 4000 DAI and 1 ETH. + * + * On the one swap, 100 DAI is swapped for ~0.1 ETH. + * On the inverse swap, 0.1 ETH is swapped for ~100 DAI. + */ + + // unit amounts + uint256 public constant ONE_TENTH_UNIT = 0.1 ether; uint256 public constant ONE_UNIT = 1 ether; + uint256 public constant HUNDRED_UNITS = 100 ether; + uint256 public constant FOUR_THOUSAND_UNITS = 4000 ether; + + // pool amounts + uint256 public constant DAI_LP_AMOUNT = FOUR_THOUSAND_UNITS; + uint256 public constant WETH_LP_AMOUNT = ONE_UNIT; - // NOTE: hardcoded from test result - uint256 public constant WETH_AMOUNT = 0.096397921069149814e18; - uint256 public constant DAI_AMOUNT = 0.5e18; + // swap amounts IN + uint256 public constant DAI_AMOUNT = HUNDRED_UNITS; + uint256 public constant WETH_AMOUNT_INVERSE = ONE_TENTH_UNIT; - uint256 public constant WETH_AMOUNT_INVERSE = 0.1e18; - // NOTE: hardcoded from test result - uint256 public constant DAI_AMOUNT_INVERSE = 0.316986296266343639e18; + // 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 + uint256 public constant DAI_OUT_AMOUNT_INVERSE = 94_183_552_501_642_552_000; // 94.1 DAI function setUp() public virtual { vm.createSelectFork('mainnet', 20_012_063); factory = _deployFactory(); - deal(address(dai), lp, HUNDRED_UNITS); - deal(address(weth), lp, HUNDRED_UNITS); + deal(address(dai), lp, DAI_LP_AMOUNT); + deal(address(weth), lp, WETH_LP_AMOUNT); - deal(address(dai), swapper.addr, ONE_UNIT); - deal(address(weth), swapperInverse.addr, ONE_UNIT); + deal(address(dai), swapper.addr, DAI_AMOUNT); + deal(address(weth), swapperInverse.addr, WETH_AMOUNT_INVERSE); vm.startPrank(lp); pool = factory.newBPool(); dai.approve(address(pool), type(uint256).max); weth.approve(address(pool), type(uint256).max); - pool.bind(address(dai), ONE_UNIT, 2e18); // 20% weight - pool.bind(address(weth), ONE_UNIT, 8e18); // 80% weight + pool.bind(address(dai), DAI_LP_AMOUNT, 8e18); // 80% weight + pool.bind(address(weth), WETH_LP_AMOUNT, 2e18); // 20% weight // finalize pool.finalize(); } function testSimpleSwap() public { _makeSwap(); - assertEq(dai.balanceOf(swapper.addr), ONE_UNIT - DAI_AMOUNT); - assertEq(weth.balanceOf(swapper.addr), WETH_AMOUNT); + assertEq(dai.balanceOf(swapper.addr), 0); + assertEq(weth.balanceOf(swapper.addr), WETH_OUT_AMOUNT); vm.startPrank(lp); uint256 lpBalance = pool.balanceOf(lp); pool.exitPool(lpBalance, new uint256[](2)); - assertEq(dai.balanceOf(lp), HUNDRED_UNITS + DAI_AMOUNT); // initial 100 + 0.5 dai - assertEq(weth.balanceOf(lp), HUNDRED_UNITS - WETH_AMOUNT); // initial 100 - ~0.09 weth + assertEq(dai.balanceOf(lp), DAI_LP_AMOUNT + DAI_AMOUNT); // initial 4k + 100 dai + assertEq(weth.balanceOf(lp), WETH_LP_AMOUNT - WETH_OUT_AMOUNT); // initial 1 - ~0.09 weth } function testSimpleSwapInverse() public { _makeSwapInverse(); - assertEq(dai.balanceOf(swapperInverse.addr), DAI_AMOUNT_INVERSE); - assertEq(weth.balanceOf(swapperInverse.addr), ONE_UNIT - WETH_AMOUNT_INVERSE); + assertEq(dai.balanceOf(swapperInverse.addr), DAI_OUT_AMOUNT_INVERSE); + assertEq(weth.balanceOf(swapperInverse.addr), 0); vm.startPrank(lp); uint256 lpBalance = pool.balanceOf(address(lp)); pool.exitPool(lpBalance, new uint256[](2)); - assertEq(dai.balanceOf(address(lp)), HUNDRED_UNITS - DAI_AMOUNT_INVERSE); // initial 100 - ~0.5 dai - assertEq(weth.balanceOf(address(lp)), HUNDRED_UNITS + WETH_AMOUNT_INVERSE); // initial 100 + 0.1 tokenB + assertEq(dai.balanceOf(address(lp)), DAI_LP_AMOUNT - DAI_OUT_AMOUNT_INVERSE); // initial 4k - ~100 dai + assertEq(weth.balanceOf(address(lp)), WETH_LP_AMOUNT + WETH_AMOUNT_INVERSE); // initial 1 + 0.1 eth } function _deployFactory() internal virtual returns (IBFactory); From ea50ff26f3ded38940c13f7b8655023380c9daab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Mon, 17 Jun 2024 19:37:12 +0200 Subject: [PATCH 20/32] refactor: renaming onlyController to _controller_ standard (#103) --- src/contracts/BCoWPool.sol | 4 ++-- src/contracts/BPool.sol | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/contracts/BCoWPool.sol b/src/contracts/BCoWPool.sol index a09a42b7..5f442572 100644 --- a/src/contracts/BCoWPool.sol +++ b/src/contracts/BCoWPool.sol @@ -52,7 +52,7 @@ contract BCoWPool is IERC1271, IBCoWPool, BPool, BCoWConst { } /// @inheritdoc IBCoWPool - function enableTrading(bytes32 appData) external onlyController { + function enableTrading(bytes32 appData) external _controller_ { if (!_finalized) { revert BPool_PoolNotFinalized(); } @@ -63,7 +63,7 @@ contract BCoWPool is IERC1271, IBCoWPool, BPool, BCoWConst { } /// @inheritdoc IBCoWPool - function disableTrading() external onlyController { + function disableTrading() external _controller_ { appDataHash = NO_TRADING; emit TradingDisabled(); } diff --git a/src/contracts/BPool.sol b/src/contracts/BPool.sol index ab45098b..a61fb707 100644 --- a/src/contracts/BPool.sol +++ b/src/contracts/BPool.sol @@ -55,7 +55,7 @@ contract BPool is BToken, BMath, IBPool { /** * @notice Throws an error if caller is not controller */ - modifier onlyController() { + modifier _controller_() { if (msg.sender != _controller) { revert BPool_CallerIsNotController(); } @@ -70,7 +70,7 @@ contract BPool is BToken, BMath, IBPool { } /// @inheritdoc IBPool - function setSwapFee(uint256 swapFee) external _logs_ _lock_ onlyController { + function setSwapFee(uint256 swapFee) external _logs_ _lock_ _controller_ { if (_finalized) { revert BPool_PoolIsFinalized(); } @@ -84,12 +84,12 @@ contract BPool is BToken, BMath, IBPool { } /// @inheritdoc IBPool - function setController(address manager) external _logs_ _lock_ onlyController { + function setController(address manager) external _logs_ _lock_ _controller_ { _controller = manager; } /// @inheritdoc IBPool - function finalize() external _logs_ _lock_ onlyController { + function finalize() external _logs_ _lock_ _controller_ { if (_finalized) { revert BPool_PoolIsFinalized(); } @@ -105,7 +105,7 @@ contract BPool is BToken, BMath, IBPool { } /// @inheritdoc IBPool - function bind(address token, uint256 balance, uint256 denorm) external _logs_ _lock_ onlyController { + function bind(address token, uint256 balance, uint256 denorm) external _logs_ _lock_ _controller_ { if (_records[token].bound) { revert BPool_TokenAlreadyBound(); } @@ -139,7 +139,7 @@ contract BPool is BToken, BMath, IBPool { } /// @inheritdoc IBPool - function unbind(address token) external _logs_ _lock_ onlyController { + function unbind(address token) external _logs_ _lock_ _controller_ { if (!_records[token].bound) { revert BPool_TokenNotBound(); } From be37fcb3f48b505d0df144e09f1270efd89c076c Mon Sep 17 00:00:00 2001 From: teddy Date: Mon, 17 Jun 2024 15:19:31 -0300 Subject: [PATCH 21/32] test: bcowpool unit tests for verify function (#92) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: manual mocks for calcOutGivenIn * test: incomplete testing of verify function * test: verify test using mocked function * fix: comply with testcase naming convention * fix: comply with function ordering in BMath * fix: check both in&out records * fix: fuzz all test parameters * chore: move common methods for swapExactAmountIn into its own abstract contract * revert: ba8925fe325acf9eeb908f565560d84a6a6eb4dd This reverts commit "fix: comply with function ordering in BMath" * test: fuzz calcOutGivenIn inputs while ensuring fees are not paid to LPs * revert: unfortunate outdated write to bmath * fix: use constant instead of hard-coded value for tests * fix: feedback from review * feat: removing extra assumptions on verify test (#104) Co-authored-by: teddy * test: happy path for verify --------- Co-authored-by: Weißer Hase --- src/contracts/BCoWPool.sol | 2 +- test/unit/BCoWPool.t.sol | 141 ++++++++++++++++++++++++- test/unit/BPool.t.sol | 210 +++++++++++++++++++------------------ 3 files changed, 245 insertions(+), 108 deletions(-) diff --git a/src/contracts/BCoWPool.sol b/src/contracts/BCoWPool.sol index 5f442572..0b8e2c81 100644 --- a/src/contracts/BCoWPool.sol +++ b/src/contracts/BCoWPool.sol @@ -116,7 +116,7 @@ contract BCoWPool is IERC1271, IBCoWPool, BPool, BCoWConst { Record memory inRecord = _records[address(order.buyToken)]; Record memory outRecord = _records[address(order.sellToken)]; - if (!inRecord.bound || !inRecord.bound) { + if (!inRecord.bound || !outRecord.bound) { revert BPool_TokenNotBound(); } if (order.validTo >= block.timestamp + MAX_ORDER_DURATION) { diff --git a/test/unit/BCoWPool.t.sol b/test/unit/BCoWPool.t.sol index 78b5f3b9..09f1e416 100644 --- a/test/unit/BCoWPool.t.sol +++ b/test/unit/BCoWPool.t.sol @@ -2,22 +2,23 @@ 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 {BasePoolTest} from './BPool.t.sol'; +import {BasePoolTest, SwapExactAmountInUtils} from './BPool.t.sol'; +import {BCoWConst} from 'contracts/BCoWConst.sol'; import {IBCoWPool} from 'interfaces/IBCoWPool.sol'; import {IBPool} from 'interfaces/IBPool.sol'; import {ISettlement} from 'interfaces/ISettlement.sol'; import {MockBCoWPool} from 'test/manual-smock/MockBCoWPool.sol'; import {MockBPool} from 'test/smock/MockBPool.sol'; -abstract contract BaseCoWPoolTest is BasePoolTest { +abstract contract BaseCoWPoolTest is BasePoolTest, BCoWConst { address public cowSolutionSettler = makeAddr('cowSolutionSettler'); bytes32 public domainSeparator = bytes32(bytes2(0xf00b)); address public vaultRelayer = makeAddr('vaultRelayer'); + GPv2Order.Data correctOrder; MockBCoWPool bCoWPool; @@ -28,6 +29,20 @@ abstract contract BaseCoWPoolTest is BasePoolTest { bCoWPool = new MockBCoWPool(cowSolutionSettler); bPool = MockBPool(address(bCoWPool)); _setRandomTokens(TOKENS_AMOUNT); + correctOrder = GPv2Order.Data({ + sellToken: IERC20(tokens[1]), + buyToken: IERC20(tokens[0]), + receiver: address(0), + sellAmount: 0, + buyAmount: 0, + validTo: uint32(block.timestamp + 1 minutes), + appData: bytes32(0), + feeAmount: 0, + kind: GPv2Order.KIND_SELL, + partiallyFillable: false, + sellTokenBalance: GPv2Order.BALANCE_ERC20, + buyTokenBalance: GPv2Order.BALANCE_ERC20 + }); } } @@ -141,6 +156,126 @@ contract BCoWPool_Unit_EnableTrading is BaseCoWPoolTest { } } +contract BCoWPool_Unit_Verify is BaseCoWPoolTest, SwapExactAmountInUtils { + function setUp() public virtual override(BaseCoWPoolTest, BasePoolTest) { + BaseCoWPoolTest.setUp(); + } + + function _assumeHappyPath(SwapExactAmountIn_FuzzScenario memory _fuzz) internal pure override { + // safe bound assumptions + _fuzz.tokenInDenorm = bound(_fuzz.tokenInDenorm, MIN_WEIGHT, MAX_WEIGHT); + _fuzz.tokenOutDenorm = bound(_fuzz.tokenOutDenorm, MIN_WEIGHT, MAX_WEIGHT); + // LP fee when swapping via CoW will always be zero + _fuzz.swapFee = 0; + + // min - max - calcSpotPrice (spotPriceBefore) + _fuzz.tokenInBalance = bound(_fuzz.tokenInBalance, MIN_BALANCE, type(uint256).max / _fuzz.tokenInDenorm); + _fuzz.tokenOutBalance = bound(_fuzz.tokenOutBalance, MIN_BALANCE, type(uint256).max / _fuzz.tokenOutDenorm); + + // MAX_IN_RATIO + vm.assume(_fuzz.tokenAmountIn <= bmul(_fuzz.tokenInBalance, MAX_IN_RATIO)); + + _assumeCalcOutGivenIn(_fuzz.tokenInBalance, _fuzz.tokenAmountIn, _fuzz.swapFee); + uint256 _tokenAmountOut = calcOutGivenIn( + _fuzz.tokenInBalance, + _fuzz.tokenInDenorm, + _fuzz.tokenOutBalance, + _fuzz.tokenOutDenorm, + _fuzz.tokenAmountIn, + _fuzz.swapFee + ); + vm.assume(_tokenAmountOut > BONE); + } + + modifier assumeNotBoundToken(address _token) { + for (uint256 i = 0; i < TOKENS_AMOUNT; i++) { + vm.assume(tokens[i] != _token); + } + _; + } + + function test_Revert_NonBoundBuyToken(address _otherToken) public assumeNotBoundToken(_otherToken) { + GPv2Order.Data memory order = correctOrder; + order.buyToken = IERC20(_otherToken); + vm.expectRevert(IBPool.BPool_TokenNotBound.selector); + bCoWPool.verify(order); + } + + function test_Revert_NonBoundSellToken(address _otherToken) public assumeNotBoundToken(_otherToken) { + GPv2Order.Data memory order = correctOrder; + order.sellToken = IERC20(_otherToken); + vm.expectRevert(IBPool.BPool_TokenNotBound.selector); + bCoWPool.verify(order); + } + + function test_Revert_LargeDurationOrder(uint256 _timeOffset) public { + _timeOffset = bound(_timeOffset, MAX_ORDER_DURATION, type(uint32).max - block.timestamp); + GPv2Order.Data memory order = correctOrder; + order.validTo = uint32(block.timestamp + _timeOffset); + vm.expectRevert(IBCoWPool.BCoWPool_OrderValidityTooLong.selector); + bCoWPool.verify(order); + } + + function test_Revert_NonZeroFee(uint256 _fee) public { + _fee = bound(_fee, 1, type(uint256).max); + GPv2Order.Data memory order = correctOrder; + order.feeAmount = _fee; + vm.expectRevert(IBCoWPool.BCoWPool_FeeMustBeZero.selector); + bCoWPool.verify(order); + } + + function test_Revert_InvalidOrderKind(bytes32 _orderKind) public { + vm.assume(_orderKind != GPv2Order.KIND_SELL); + GPv2Order.Data memory order = correctOrder; + order.kind = _orderKind; + vm.expectRevert(IBCoWPool.BCoWPool_InvalidOperation.selector); + bCoWPool.verify(order); + } + + function test_Revert_InvalidBuyBalanceKind(bytes32 _balanceKind) public { + vm.assume(_balanceKind != GPv2Order.BALANCE_ERC20); + GPv2Order.Data memory order = correctOrder; + order.buyTokenBalance = _balanceKind; + vm.expectRevert(IBCoWPool.BCoWPool_InvalidBalanceMarker.selector); + bCoWPool.verify(order); + } + + function test_Revert_InvalidSellBalanceKind(bytes32 _balanceKind) public { + vm.assume(_balanceKind != GPv2Order.BALANCE_ERC20); + GPv2Order.Data memory order = correctOrder; + order.sellTokenBalance = _balanceKind; + vm.expectRevert(IBCoWPool.BCoWPool_InvalidBalanceMarker.selector); + bCoWPool.verify(order); + } + + function test_Revert_InsufficientReturn( + SwapExactAmountIn_FuzzScenario memory _fuzz, + uint256 _offset + ) public happyPath(_fuzz) { + uint256 _tokenAmountOut = calcOutGivenIn( + _fuzz.tokenInBalance, _fuzz.tokenInDenorm, _fuzz.tokenOutBalance, _fuzz.tokenOutDenorm, _fuzz.tokenAmountIn, 0 + ); + _offset = bound(_offset, 1, _tokenAmountOut); + GPv2Order.Data memory order = correctOrder; + order.buyAmount = _fuzz.tokenAmountIn; + order.sellAmount = _tokenAmountOut + _offset; + + vm.expectRevert(IBPool.BPool_TokenAmountOutBelowMinOut.selector); + bCoWPool.verify(order); + } + + function test_Success_HappyPath(SwapExactAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { + uint256 _tokenAmountOut = calcOutGivenIn( + _fuzz.tokenInBalance, _fuzz.tokenInDenorm, _fuzz.tokenOutBalance, _fuzz.tokenOutDenorm, _fuzz.tokenAmountIn, 0 + ); + GPv2Order.Data memory order = correctOrder; + order.buyAmount = _fuzz.tokenAmountIn; + order.sellAmount = _tokenAmountOut; + + bCoWPool.verify(order); + } +} + contract BCoWPool_Unit_IsValidSignature is BaseCoWPoolTest { function setUp() public virtual override { super.setUp(); diff --git a/test/unit/BPool.t.sol b/test/unit/BPool.t.sol index 56ca970b..71000b85 100644 --- a/test/unit/BPool.t.sol +++ b/test/unit/BPool.t.sol @@ -245,6 +245,111 @@ abstract contract BasePoolTest is Test, BConst, Utils, BMath { } } +abstract contract SwapExactAmountInUtils is BasePoolTest { + address tokenIn; + address tokenOut; + + struct SwapExactAmountIn_FuzzScenario { + uint256 tokenAmountIn; + uint256 tokenInBalance; + uint256 tokenInDenorm; + uint256 tokenOutBalance; + uint256 tokenOutDenorm; + uint256 swapFee; + } + + function _setValues(SwapExactAmountIn_FuzzScenario memory _fuzz) internal { + tokenIn = tokens[0]; + tokenOut = tokens[1]; + + // Create mocks for tokenIn and tokenOut (only use the first 2 tokens) + _mockTransferFrom(tokenIn); + _mockTransfer(tokenOut); + + // Set balances + _setRecord( + tokenIn, + IBPool.Record({ + bound: true, + index: 0, // NOTE: irrelevant for this method + denorm: _fuzz.tokenInDenorm + }) + ); + _mockPoolBalance(tokenIn, _fuzz.tokenInBalance); + + _setRecord( + tokenOut, + IBPool.Record({ + bound: true, + index: 0, // NOTE: irrelevant for this method + denorm: _fuzz.tokenOutDenorm + }) + ); + _mockPoolBalance(tokenOut, _fuzz.tokenOutBalance); + + // Set swapFee + _setSwapFee(_fuzz.swapFee); + // Set finalize + _setFinalize(true); + } + + function _assumeHappyPath(SwapExactAmountIn_FuzzScenario memory _fuzz) internal view virtual { + // safe bound assumptions + _fuzz.tokenInDenorm = bound(_fuzz.tokenInDenorm, MIN_WEIGHT, MAX_WEIGHT); + _fuzz.tokenOutDenorm = bound(_fuzz.tokenOutDenorm, MIN_WEIGHT, MAX_WEIGHT); + _fuzz.swapFee = bound(_fuzz.swapFee, MIN_FEE, MAX_FEE); + + // min - max - calcSpotPrice (spotPriceBefore) + _fuzz.tokenInBalance = bound(_fuzz.tokenInBalance, MIN_BALANCE, type(uint256).max / _fuzz.tokenInDenorm); + _fuzz.tokenOutBalance = bound(_fuzz.tokenOutBalance, MIN_BALANCE, type(uint256).max / _fuzz.tokenOutDenorm); + + // max - calcSpotPrice (spotPriceAfter) + vm.assume(_fuzz.tokenAmountIn < type(uint256).max - _fuzz.tokenInBalance); + vm.assume(_fuzz.tokenInBalance + _fuzz.tokenAmountIn < type(uint256).max / _fuzz.tokenInDenorm); + + // internal calculation for calcSpotPrice (spotPriceBefore) + _assumeCalcSpotPrice( + _fuzz.tokenInBalance, _fuzz.tokenInDenorm, _fuzz.tokenOutBalance, _fuzz.tokenOutDenorm, _fuzz.swapFee + ); + + // MAX_IN_RATIO + vm.assume(_fuzz.tokenAmountIn <= bmul(_fuzz.tokenInBalance, MAX_IN_RATIO)); + + // L338 BPool.sol + uint256 _spotPriceBefore = calcSpotPrice( + _fuzz.tokenInBalance, _fuzz.tokenInDenorm, _fuzz.tokenOutBalance, _fuzz.tokenOutDenorm, _fuzz.swapFee + ); + + _assumeCalcOutGivenIn(_fuzz.tokenInBalance, _fuzz.tokenAmountIn, _fuzz.swapFee); + uint256 _tokenAmountOut = calcOutGivenIn( + _fuzz.tokenInBalance, + _fuzz.tokenInDenorm, + _fuzz.tokenOutBalance, + _fuzz.tokenOutDenorm, + _fuzz.tokenAmountIn, + _fuzz.swapFee + ); + vm.assume(_tokenAmountOut > BONE); + + // internal calculation for calcSpotPrice (spotPriceAfter) + _assumeCalcSpotPrice( + _fuzz.tokenInBalance + _fuzz.tokenAmountIn, + _fuzz.tokenInDenorm, + _fuzz.tokenOutBalance - _tokenAmountOut, + _fuzz.tokenOutDenorm, + _fuzz.swapFee + ); + + vm.assume(bmul(_spotPriceBefore, _tokenAmountOut) <= _fuzz.tokenAmountIn); + } + + modifier happyPath(SwapExactAmountIn_FuzzScenario memory _fuzz) { + _assumeHappyPath(_fuzz); + _setValues(_fuzz); + _; + } +} + contract BPool_Unit_Constructor is BasePoolTest { function test_Deploy(address _deployer) public { vm.prank(_deployer); @@ -1354,110 +1459,7 @@ contract BPool_Unit_ExitPool is BasePoolTest { } } -contract BPool_Unit_SwapExactAmountIn is BasePoolTest { - address tokenIn; - address tokenOut; - - struct SwapExactAmountIn_FuzzScenario { - uint256 tokenAmountIn; - uint256 tokenInBalance; - uint256 tokenInDenorm; - uint256 tokenOutBalance; - uint256 tokenOutDenorm; - uint256 swapFee; - } - - function _setValues(SwapExactAmountIn_FuzzScenario memory _fuzz) internal { - tokenIn = tokens[0]; - tokenOut = tokens[1]; - - // Create mocks for tokenIn and tokenOut (only use the first 2 tokens) - _mockTransferFrom(tokenIn); - _mockTransfer(tokenOut); - - // Set balances - _setRecord( - tokenIn, - IBPool.Record({ - bound: true, - index: 0, // NOTE: irrelevant for this method - denorm: _fuzz.tokenInDenorm - }) - ); - _mockPoolBalance(tokenIn, _fuzz.tokenInBalance); - - _setRecord( - tokenOut, - IBPool.Record({ - bound: true, - index: 0, // NOTE: irrelevant for this method - denorm: _fuzz.tokenOutDenorm - }) - ); - _mockPoolBalance(tokenOut, _fuzz.tokenOutBalance); - - // Set swapFee - _setSwapFee(_fuzz.swapFee); - // Set finalize - _setFinalize(true); - } - - function _assumeHappyPath(SwapExactAmountIn_FuzzScenario memory _fuzz) internal pure { - // safe bound assumptions - _fuzz.tokenInDenorm = bound(_fuzz.tokenInDenorm, MIN_WEIGHT, MAX_WEIGHT); - _fuzz.tokenOutDenorm = bound(_fuzz.tokenOutDenorm, MIN_WEIGHT, MAX_WEIGHT); - _fuzz.swapFee = bound(_fuzz.swapFee, MIN_FEE, MAX_FEE); - - // min - max - calcSpotPrice (spotPriceBefore) - _fuzz.tokenInBalance = bound(_fuzz.tokenInBalance, MIN_BALANCE, type(uint256).max / _fuzz.tokenInDenorm); - _fuzz.tokenOutBalance = bound(_fuzz.tokenOutBalance, MIN_BALANCE, type(uint256).max / _fuzz.tokenOutDenorm); - - // max - calcSpotPrice (spotPriceAfter) - vm.assume(_fuzz.tokenAmountIn < type(uint256).max - _fuzz.tokenInBalance); - vm.assume(_fuzz.tokenInBalance + _fuzz.tokenAmountIn < type(uint256).max / _fuzz.tokenInDenorm); - - // internal calculation for calcSpotPrice (spotPriceBefore) - _assumeCalcSpotPrice( - _fuzz.tokenInBalance, _fuzz.tokenInDenorm, _fuzz.tokenOutBalance, _fuzz.tokenOutDenorm, _fuzz.swapFee - ); - - // MAX_IN_RATIO - vm.assume(_fuzz.tokenAmountIn <= bmul(_fuzz.tokenInBalance, MAX_IN_RATIO)); - - // L338 BPool.sol - uint256 _spotPriceBefore = calcSpotPrice( - _fuzz.tokenInBalance, _fuzz.tokenInDenorm, _fuzz.tokenOutBalance, _fuzz.tokenOutDenorm, _fuzz.swapFee - ); - - _assumeCalcOutGivenIn(_fuzz.tokenInBalance, _fuzz.tokenAmountIn, _fuzz.swapFee); - uint256 _tokenAmountOut = calcOutGivenIn( - _fuzz.tokenInBalance, - _fuzz.tokenInDenorm, - _fuzz.tokenOutBalance, - _fuzz.tokenOutDenorm, - _fuzz.tokenAmountIn, - _fuzz.swapFee - ); - vm.assume(_tokenAmountOut > BONE); - - // internal calculation for calcSpotPrice (spotPriceAfter) - _assumeCalcSpotPrice( - _fuzz.tokenInBalance + _fuzz.tokenAmountIn, - _fuzz.tokenInDenorm, - _fuzz.tokenOutBalance - _tokenAmountOut, - _fuzz.tokenOutDenorm, - _fuzz.swapFee - ); - - vm.assume(bmul(_spotPriceBefore, _tokenAmountOut) <= _fuzz.tokenAmountIn); - } - - modifier happyPath(SwapExactAmountIn_FuzzScenario memory _fuzz) { - _assumeHappyPath(_fuzz); - _setValues(_fuzz); - _; - } - +contract BPool_Unit_SwapExactAmountIn is SwapExactAmountInUtils { function test_Revert_NotBoundTokenIn( SwapExactAmountIn_FuzzScenario memory _fuzz, address _tokenIn From b20434e4342299cb10921d56c40b0db5b1452241 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Tue, 18 Jun 2024 15:15:29 +0200 Subject: [PATCH 22/32] feat: adding max in ratio check on cow swap path (#100) * feat: adding max in ratio check on cow swap path * test: unit tests for swap amount exeeding MAX_IN_RATIO * fix: call the actual method Im supposed to test --------- Co-authored-by: teddy --- src/contracts/BCoWPool.sol | 7 ++++++- test/unit/BCoWPool.t.sol | 13 +++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/contracts/BCoWPool.sol b/src/contracts/BCoWPool.sol index 0b8e2c81..de4688e7 100644 --- a/src/contracts/BCoWPool.sol +++ b/src/contracts/BCoWPool.sol @@ -132,8 +132,13 @@ contract BCoWPool is IERC1271, IBCoWPool, BPool, BCoWConst { revert BCoWPool_InvalidBalanceMarker(); } + uint256 buyTokenBalance = order.buyToken.balanceOf(address(this)); + if (order.buyAmount > bmul(buyTokenBalance, MAX_IN_RATIO)) { + revert BPool_TokenAmountInAboveMaxIn(); + } + uint256 tokenAmountOut = calcOutGivenIn({ - tokenBalanceIn: order.buyToken.balanceOf(address(this)), + tokenBalanceIn: buyTokenBalance, tokenWeightIn: inRecord.denorm, tokenBalanceOut: order.sellToken.balanceOf(address(this)), tokenWeightOut: outRecord.denorm, diff --git a/test/unit/BCoWPool.t.sol b/test/unit/BCoWPool.t.sol index 09f1e416..9e2af8fe 100644 --- a/test/unit/BCoWPool.t.sol +++ b/test/unit/BCoWPool.t.sol @@ -248,6 +248,19 @@ contract BCoWPool_Unit_Verify is BaseCoWPoolTest, SwapExactAmountInUtils { bCoWPool.verify(order); } + function test_Revert_TokenAmountInAboveMaxIn( + SwapExactAmountIn_FuzzScenario memory _fuzz, + uint256 _offset + ) public happyPath(_fuzz) { + _offset = bound(_offset, 1, type(uint256).max - _fuzz.tokenInBalance); + uint256 _tokenAmountIn = bmul(_fuzz.tokenInBalance, MAX_IN_RATIO) + _offset; + GPv2Order.Data memory order = correctOrder; + order.buyAmount = _tokenAmountIn; + + vm.expectRevert(IBPool.BPool_TokenAmountInAboveMaxIn.selector); + bCoWPool.verify(order); + } + function test_Revert_InsufficientReturn( SwapExactAmountIn_FuzzScenario memory _fuzz, uint256 _offset From 0c785403a296590af1d9e458baa48f97a1d6c010 Mon Sep 17 00:00:00 2001 From: Austrian <114922365+0xAustrian@users.noreply.github.com> Date: Tue, 18 Jun 2024 11:42:48 -0300 Subject: [PATCH 23/32] fix: enforce receiver same as owner in amm order (#106) * fix: enforce receiver same as owner in amm order * fix: change revert order --- .forge-snapshots/settlementCoWSwap.snap | 2 +- .forge-snapshots/settlementCoWSwapInverse.snap | 2 +- src/contracts/BCoWPool.sol | 3 +++ src/interfaces/IBCoWPool.sol | 5 +++++ test/unit/BCoWPool.t.sol | 10 +++++++++- 5 files changed, 19 insertions(+), 3 deletions(-) diff --git a/.forge-snapshots/settlementCoWSwap.snap b/.forge-snapshots/settlementCoWSwap.snap index 26ecbba8..270b2978 100644 --- a/.forge-snapshots/settlementCoWSwap.snap +++ b/.forge-snapshots/settlementCoWSwap.snap @@ -1 +1 @@ -188532 \ No newline at end of file +188873 \ No newline at end of file diff --git a/.forge-snapshots/settlementCoWSwapInverse.snap b/.forge-snapshots/settlementCoWSwapInverse.snap index 69865296..0474381f 100644 --- a/.forge-snapshots/settlementCoWSwapInverse.snap +++ b/.forge-snapshots/settlementCoWSwapInverse.snap @@ -1 +1 @@ -198372 \ No newline at end of file +198713 \ No newline at end of file diff --git a/src/contracts/BCoWPool.sol b/src/contracts/BCoWPool.sol index de4688e7..fa850f34 100644 --- a/src/contracts/BCoWPool.sol +++ b/src/contracts/BCoWPool.sol @@ -119,6 +119,9 @@ contract BCoWPool is IERC1271, IBCoWPool, BPool, BCoWConst { if (!inRecord.bound || !outRecord.bound) { revert BPool_TokenNotBound(); } + if (order.receiver != GPv2Order.RECEIVER_SAME_AS_OWNER) { + revert BCoWPool_ReceiverIsNotBCoWPool(); + } if (order.validTo >= block.timestamp + MAX_ORDER_DURATION) { revert BCoWPool_OrderValidityTooLong(); } diff --git a/src/interfaces/IBCoWPool.sol b/src/interfaces/IBCoWPool.sol index 93170bfa..1c240645 100644 --- a/src/interfaces/IBCoWPool.sol +++ b/src/interfaces/IBCoWPool.sol @@ -71,6 +71,11 @@ interface IBCoWPool is IERC1271, IBPool { */ error AppDataDoNotMatchHash(); + /** + * @notice Thrown when the receiver of the order is not the bCoWPool itself. + */ + error BCoWPool_ReceiverIsNotBCoWPool(); + /** * @notice Once this function is called, it will be possible to trade with * this AMM on CoW Protocol. diff --git a/test/unit/BCoWPool.t.sol b/test/unit/BCoWPool.t.sol index 9e2af8fe..29823840 100644 --- a/test/unit/BCoWPool.t.sol +++ b/test/unit/BCoWPool.t.sol @@ -32,7 +32,7 @@ abstract contract BaseCoWPoolTest is BasePoolTest, BCoWConst { correctOrder = GPv2Order.Data({ sellToken: IERC20(tokens[1]), buyToken: IERC20(tokens[0]), - receiver: address(0), + receiver: GPv2Order.RECEIVER_SAME_AS_OWNER, sellAmount: 0, buyAmount: 0, validTo: uint32(block.timestamp + 1 minutes), @@ -208,6 +208,14 @@ contract BCoWPool_Unit_Verify is BaseCoWPoolTest, SwapExactAmountInUtils { bCoWPool.verify(order); } + function test_Revert_ReceiverIsNotBCoWPool(address _receiver) public { + vm.assume(_receiver != GPv2Order.RECEIVER_SAME_AS_OWNER); + GPv2Order.Data memory order = correctOrder; + order.receiver = _receiver; + vm.expectRevert(IBCoWPool.BCoWPool_ReceiverIsNotBCoWPool.selector); + bCoWPool.verify(order); + } + function test_Revert_LargeDurationOrder(uint256 _timeOffset) public { _timeOffset = bound(_timeOffset, MAX_ORDER_DURATION, type(uint32).max - block.timestamp); GPv2Order.Data memory order = correctOrder; From a51072bcc50aa20b3cc9ea1a7670c026d3edaeb3 Mon Sep 17 00:00:00 2001 From: Austrian <114922365+0xAustrian@users.noreply.github.com> Date: Tue, 18 Jun 2024 11:54:26 -0300 Subject: [PATCH 24/32] feat: vendor GPv2TradeEncoder (#105) * feat: vendor GPv2TradeEncoder * feat: update composable-cow dependency commit * feat: add another remapping to reduce number of changes * feat: rename composable-cow remapping --- package.json | 1 + remappings.txt | 2 + test/integration/BCowPool.t.sol | 2 +- test/integration/GPv2TradeEncoder.sol | 112 -------------------------- yarn.lock | 4 + 5 files changed, 8 insertions(+), 113 deletions(-) delete mode 100644 test/integration/GPv2TradeEncoder.sol diff --git a/package.json b/package.json index 7aa72093..15edec87 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "dependencies": { "@cowprotocol/contracts": "github:cowprotocol/contracts.git#a10f40788a", "@openzeppelin/contracts": "5.0.2", + "composable-cow": "github:cowprotocol/composable-cow.git#24d556b", "solmate": "github:transmissions11/solmate#c892309" }, "devDependencies": { diff --git a/remappings.txt b/remappings.txt index 22f73eb2..5b011a43 100644 --- a/remappings.txt +++ b/remappings.txt @@ -3,6 +3,8 @@ forge-std/=node_modules/forge-std/src forge-gas-snapshot/=node_modules/forge-gas-snapshot/src solmate/=node_modules/solmate/src @cowprotocol/=node_modules/@cowprotocol/contracts/src/contracts +cowprotocol/=node_modules/@cowprotocol/contracts/src/ +@composable-cow/=node_modules/composable-cow/ contracts/=src/contracts interfaces/=src/interfaces diff --git a/test/integration/BCowPool.t.sol b/test/integration/BCowPool.t.sol index 469fe76e..3fb6cb6a 100644 --- a/test/integration/BCowPool.t.sol +++ b/test/integration/BCowPool.t.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.25; -import {GPv2TradeEncoder} from './GPv2TradeEncoder.sol'; import {PoolSwapIntegrationTest} from './PoolSwap.t.sol'; +import {GPv2TradeEncoder} from '@composable-cow/test/vendored/GPv2TradeEncoder.sol'; import {IERC20} from '@cowprotocol/interfaces/IERC20.sol'; import {GPv2Interaction} from '@cowprotocol/libraries/GPv2Interaction.sol'; import {GPv2Order} from '@cowprotocol/libraries/GPv2Order.sol'; diff --git a/test/integration/GPv2TradeEncoder.sol b/test/integration/GPv2TradeEncoder.sol deleted file mode 100644 index 24d9d9a2..00000000 --- a/test/integration/GPv2TradeEncoder.sol +++ /dev/null @@ -1,112 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 -pragma solidity 0.8.25; - -import {GPv2Order} from '@cowprotocol/libraries/GPv2Order.sol'; -import {GPv2Signing} from '@cowprotocol/mixins/GPv2Signing.sol'; -import {IERC20} from '@openzeppelin/contracts/interfaces/IERC20.sol'; - -/* solhint-disable-next-line max-line-length */ -/// @notice This library is an almost exact copy of https://github.com/cowprotocol/composable-cow/blob/9ea0394e15eec16550ba4c327d10c920880eb355/test/vendored/GPv2TradeEncoder.sol - -/// @title Gnosis Protocol v2 Trade Library. -/// @author mfw78 -/// @dev This library provides functions for encoding trade flags -/// Encoding methodology is adapted from upstream at -/* solhint-disable-next-line max-line-length */ -/// https://github.com/cowprotocol/contracts/blob/d043b0bfac7a09463c74dfe1613d0612744ed91c/src/contracts/libraries/GPv2Trade.sol -library GPv2TradeEncoder { - using GPv2Order for GPv2Order.Data; - - // uint256 constant FLAG_ORDER_KIND_SELL = 0x00; - uint256 constant FLAG_ORDER_KIND_BUY = 0x01; - - // uint256 constant FLAG_FILL_FOK = 0x00; - uint256 constant FLAG_FILL_PARTIAL = 0x02; - - // uint256 constant FLAG_SELL_TOKEN_ERC20_TOKEN_BALANCE = 0x00; - uint256 constant FLAG_SELL_TOKEN_BALANCER_EXTERNAL = 0x08; - uint256 constant FLAG_SELL_TOKEN_BALANCER_INTERNAL = 0x0c; - - // uint256 constant FLAG_BUY_TOKEN_ERC20_TOKEN_BALANCE = 0x00; - uint256 constant FLAG_BUY_TOKEN_BALANCER_INTERNAL = 0x10; - - // uint256 constant FLAG_SIGNATURE_SCHEME_EIP712 = 0x00; - uint256 constant FLAG_SIGNATURE_SCHEME_ETHSIGN = 0x20; - uint256 constant FLAG_SIGNATURE_SCHEME_EIP1271 = 0x40; - uint256 constant FLAG_SIGNATURE_SCHEME_PRESIGN = 0x60; - - /// @dev Decodes trade flags. - /// - /// Trade flags are used to tightly encode information on how to decode - /// an order. Examples that directly affect the structure of an order are - /// the kind of order (either a sell or a buy order) as well as whether the - /// order is partially fillable or if it is a "fill-or-kill" order. It also - /// encodes the signature scheme used to validate the order. As the most - /// likely values are fill-or-kill sell orders by an externally owned - /// account, the flags are chosen such that `0x00` represents this kind of - /// order. The flags byte uses the following format: - /// - /// ``` - /// bit | 31 ... | 6 | 5 | 4 | 3 | 2 | 1 | 0 | - /// ----+----------+-------+---+-------+---+---+ - /// | reserved | * * | * | * * | * | * | - /// | | | | | | | - /// | | | | | | +---- order kind bit, 0 for a sell order - /// | | | | | | and 1 for a buy order - /// | | | | | | - /// | | | | | +-------- order fill bit, 0 for fill-or-kill - /// | | | | | and 1 for a partially fillable order - /// | | | | | - /// | | | +---+------------ use internal sell token balance bit: - /// | | | 0x: ERC20 token balance - /// | | | 10: external Balancer Vault balance - /// | | | 11: internal Balancer Vault balance - /// | | | - /// | | +-------------------- use buy token balance bit - /// | | 0: ERC20 token balance - /// | | 1: internal Balancer Vault balance - /// | | - /// +---+------------------------ signature scheme bits: - /// 00: EIP-712 - /// 01: eth_sign - /// 10: EIP-1271 - /// 11: pre_sign - /// ``` - function encodeFlags( - GPv2Order.Data memory order, - GPv2Signing.Scheme signingScheme - ) internal pure returns (uint256 flags) { - // set the zero index bit if the order is a buy order - if (order.kind == GPv2Order.KIND_BUY) { - flags |= FLAG_ORDER_KIND_BUY; - } - - // set the first index bit if the order is partially fillable - if (order.partiallyFillable) { - flags |= FLAG_FILL_PARTIAL; - } - - // set the second and third index bit based on the sell token liquidity - if (order.sellTokenBalance == GPv2Order.BALANCE_EXTERNAL) { - flags |= FLAG_SELL_TOKEN_BALANCER_EXTERNAL; - } else if (order.sellTokenBalance == GPv2Order.BALANCE_INTERNAL) { - flags |= FLAG_SELL_TOKEN_BALANCER_INTERNAL; - } - - // set the fourth index bit based on the buy token liquidity - if (order.buyTokenBalance == GPv2Order.BALANCE_INTERNAL) { - flags |= FLAG_BUY_TOKEN_BALANCER_INTERNAL; - } - - // set the fifth and sixth index bit based on the signature scheme - if (signingScheme == GPv2Signing.Scheme.EthSign) { - flags |= FLAG_SIGNATURE_SCHEME_ETHSIGN; - } else if (signingScheme == GPv2Signing.Scheme.Eip1271) { - flags |= FLAG_SIGNATURE_SCHEME_EIP1271; - } else if (signingScheme == GPv2Signing.Scheme.PreSign) { - flags |= FLAG_SIGNATURE_SCHEME_PRESIGN; - } - - return flags; - } -} diff --git a/yarn.lock b/yarn.lock index 6a50cc67..73799a41 100644 --- a/yarn.lock +++ b/yarn.lock @@ -548,6 +548,10 @@ compare-func@^2.0.0: array-ify "^1.0.0" dot-prop "^5.1.0" +"composable-cow@github:cowprotocol/composable-cow.git#24d556b": + version "0.0.0" + resolved "https://codeload.github.com/cowprotocol/composable-cow/tar.gz/24d556b634e21065e0ee70dd27469a6e699a8998" + conventional-changelog-angular@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/conventional-changelog-angular/-/conventional-changelog-angular-7.0.0.tgz#5eec8edbff15aa9b1680a8dcfbd53e2d7eb2ba7a" From b0399f1b63a83f8df5ab1ab8aa2e557ae5e5bfb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Tue, 18 Jun 2024 18:57:43 +0200 Subject: [PATCH 25/32] feat: making appData immutable (#107) * feat: making appData immutable * fix: comment on interface * fix: addressing comments in PR * fix: rm NO_TRADING const --- .forge-snapshots/settlementCoWSwap.snap | 2 +- .../settlementCoWSwapInverse.snap | 2 +- script/DeployBCoWFactory.s.sol | 2 +- script/Params.s.sol | 14 ++- src/contracts/BCoWConst.sol | 7 -- src/contracts/BCoWFactory.sol | 6 +- src/contracts/BCoWPool.sol | 27 +--- src/interfaces/IBCoWPool.sol | 45 ++----- test/integration/BCowPool.t.sol | 9 +- test/manual-smock/MockBCoWPool.sol | 17 +-- test/smock/MockBCoWFactory.sol | 2 +- test/unit/BCoWFactory.t.sol | 18 ++- test/unit/BCoWPool.t.sol | 116 ++++-------------- 13 files changed, 75 insertions(+), 192 deletions(-) diff --git a/.forge-snapshots/settlementCoWSwap.snap b/.forge-snapshots/settlementCoWSwap.snap index 270b2978..5cd515d8 100644 --- a/.forge-snapshots/settlementCoWSwap.snap +++ b/.forge-snapshots/settlementCoWSwap.snap @@ -1 +1 @@ -188873 \ No newline at end of file +186615 \ No newline at end of file diff --git a/.forge-snapshots/settlementCoWSwapInverse.snap b/.forge-snapshots/settlementCoWSwapInverse.snap index 0474381f..a588d5cf 100644 --- a/.forge-snapshots/settlementCoWSwapInverse.snap +++ b/.forge-snapshots/settlementCoWSwapInverse.snap @@ -1 +1 @@ -198713 \ No newline at end of file +196455 \ No newline at end of file diff --git a/script/DeployBCoWFactory.s.sol b/script/DeployBCoWFactory.s.sol index 2f16873f..99be7268 100644 --- a/script/DeployBCoWFactory.s.sol +++ b/script/DeployBCoWFactory.s.sol @@ -10,7 +10,7 @@ contract DeployBCoWFactory is Script, Params { BCoWFactoryDeploymentParams memory _params = _bCoWFactoryDeploymentParams[block.chainid]; vm.startBroadcast(); - BCoWFactory bCoWFactory = new BCoWFactory(_params.settlement); + BCoWFactory bCoWFactory = new BCoWFactory(_params.settlement, _params.appData); bCoWFactory.setBLabs(_params.bLabs); vm.stopBroadcast(); } diff --git a/script/Params.s.sol b/script/Params.s.sol index 3cbeb2a3..b2f0eeab 100644 --- a/script/Params.s.sol +++ b/script/Params.s.sol @@ -9,24 +9,28 @@ contract Params { struct BCoWFactoryDeploymentParams { address bLabs; address settlement; + bytes32 appData; } + /// @notice Settlement address + address internal constant _GPV2_SETTLEMENT = 0x9008D19f58AAbD9eD0D60971565AA8510560ab41; + + /// @notice AppData identifier + bytes32 internal constant _APP_DATA = bytes32('appData'); + /// @notice BFactory deployment parameters for each chain mapping(uint256 _chainId => BFactoryDeploymentParams _params) internal _bFactoryDeploymentParams; /// @notice BCoWFactory deployment parameters for each chain mapping(uint256 _chainId => BCoWFactoryDeploymentParams _params) internal _bCoWFactoryDeploymentParams; - /// @notice Settlement address - address internal constant _GPV2_SETTLEMENT = 0x9008D19f58AAbD9eD0D60971565AA8510560ab41; - constructor() { // Mainnet _bFactoryDeploymentParams[1] = BFactoryDeploymentParams(address(this)); - _bCoWFactoryDeploymentParams[1] = BCoWFactoryDeploymentParams(address(this), _GPV2_SETTLEMENT); + _bCoWFactoryDeploymentParams[1] = BCoWFactoryDeploymentParams(address(this), _GPV2_SETTLEMENT, _APP_DATA); // Sepolia _bFactoryDeploymentParams[11_155_111] = BFactoryDeploymentParams(address(this)); - _bCoWFactoryDeploymentParams[11_155_111] = BCoWFactoryDeploymentParams(address(this), _GPV2_SETTLEMENT); + _bCoWFactoryDeploymentParams[11_155_111] = BCoWFactoryDeploymentParams(address(this), _GPV2_SETTLEMENT, _APP_DATA); } } diff --git a/src/contracts/BCoWConst.sol b/src/contracts/BCoWConst.sol index 87e83df6..1464118a 100644 --- a/src/contracts/BCoWConst.sol +++ b/src/contracts/BCoWConst.sol @@ -8,13 +8,6 @@ contract BCoWConst { */ bytes32 public constant EMPTY_COMMITMENT = bytes32(0); - /** - * @notice The value representing that no trading parameters are currently - * accepted as valid by this contract, meaning that no trading can occur. - * @return _noTrading The value representing no trading. - */ - bytes32 public constant NO_TRADING = bytes32(0); - /** * @notice The largest possible duration of any AMM order, starting from the * current block timestamp. diff --git a/src/contracts/BCoWFactory.sol b/src/contracts/BCoWFactory.sol index f8ef8d04..bc2538c5 100644 --- a/src/contracts/BCoWFactory.sol +++ b/src/contracts/BCoWFactory.sol @@ -13,9 +13,11 @@ import {IBPool} from 'interfaces/IBPool.sol'; */ contract BCoWFactory is BFactory { address public immutable SOLUTION_SETTLER; + bytes32 public immutable APP_DATA; - constructor(address _solutionSettler) BFactory() { + constructor(address _solutionSettler, bytes32 _appData) BFactory() { SOLUTION_SETTLER = _solutionSettler; + APP_DATA = _appData; } /** @@ -24,7 +26,7 @@ contract BCoWFactory is BFactory { * to minimize required changes to existing tooling */ function newBPool() external override returns (IBPool _pool) { - IBPool bpool = new BCoWPool(SOLUTION_SETTLER); + IBPool bpool = new BCoWPool(SOLUTION_SETTLER, APP_DATA); _isBPool[address(bpool)] = true; emit LOG_NEW_POOL(msg.sender, address(bpool)); bpool.setController(msg.sender); diff --git a/src/contracts/BCoWPool.sol b/src/contracts/BCoWPool.sol index fa850f34..9d9ad3ba 100644 --- a/src/contracts/BCoWPool.sol +++ b/src/contracts/BCoWPool.sol @@ -43,29 +43,13 @@ contract BCoWPool is IERC1271, IBCoWPool, BPool, BCoWConst { ISettlement public immutable SOLUTION_SETTLER; /// @inheritdoc IBCoWPool - bytes32 public appDataHash; + bytes32 public immutable APP_DATA; - constructor(address _cowSolutionSettler) BPool() { + constructor(address _cowSolutionSettler, bytes32 _appData) BPool() { SOLUTION_SETTLER = ISettlement(_cowSolutionSettler); SOLUTION_SETTLER_DOMAIN_SEPARATOR = ISettlement(_cowSolutionSettler).domainSeparator(); VAULT_RELAYER = ISettlement(_cowSolutionSettler).vaultRelayer(); - } - - /// @inheritdoc IBCoWPool - function enableTrading(bytes32 appData) external _controller_ { - if (!_finalized) { - revert BPool_PoolNotFinalized(); - } - - bytes32 _appDataHash = keccak256(abi.encode(appData)); - appDataHash = _appDataHash; - emit TradingEnabled(_appDataHash, appData); - } - - /// @inheritdoc IBCoWPool - function disableTrading() external _controller_ { - appDataHash = NO_TRADING; - emit TradingDisabled(); + APP_DATA = _appData; } /// @inheritdoc IBCoWPool @@ -85,9 +69,10 @@ contract BCoWPool is IERC1271, IBCoWPool, BPool, BCoWConst { function isValidSignature(bytes32 _hash, bytes memory signature) external view returns (bytes4) { (GPv2Order.Data memory order) = abi.decode(signature, (GPv2Order.Data)); - if (appDataHash != keccak256(abi.encode(order.appData))) { - revert AppDataDoNotMatchHash(); + if (order.appData != APP_DATA) { + revert AppDataDoesNotMatch(); } + bytes32 orderHash = order.hash(SOLUTION_SETTLER_DOMAIN_SEPARATOR); if (orderHash != _hash) { revert OrderDoesNotMatchMessageHash(); diff --git a/src/interfaces/IBCoWPool.sol b/src/interfaces/IBCoWPool.sol index 1c240645..3777e797 100644 --- a/src/interfaces/IBCoWPool.sol +++ b/src/interfaces/IBCoWPool.sol @@ -7,20 +7,6 @@ import {IBPool} from 'interfaces/IBPool.sol'; import {ISettlement} from 'interfaces/ISettlement.sol'; interface IBCoWPool is IERC1271, IBPool { - /** - * @notice Emitted when the manager disables all trades by the AMM. Existing open - * order will not be tradeable. Note that the AMM could resume trading with - * different parameters at a later point. - */ - event TradingDisabled(); - - /** - * @notice Emitted when the manager enables the AMM to trade on CoW Protocol. - * @param hash The hash of the trading parameters. - * @param appData Trading has been enabled for this appData. - */ - event TradingEnabled(bytes32 indexed hash, bytes32 appData); - /** * @notice thrown when a CoW order has a non-zero fee */ @@ -65,29 +51,16 @@ interface IBCoWPool is IERC1271, IBPool { error OrderDoesNotMatchMessageHash(); /** - * @notice The order trade parameters that were provided during signature - * verification does not match the data stored in this contract _or_ the - * AMM has not enabled trading. + * @notice Thrown when AppData that was provided during signature verification + * does not match the one stored in this contract. */ - error AppDataDoNotMatchHash(); + error AppDataDoesNotMatch(); /** * @notice Thrown when the receiver of the order is not the bCoWPool itself. */ error BCoWPool_ReceiverIsNotBCoWPool(); - /** - * @notice Once this function is called, it will be possible to trade with - * this AMM on CoW Protocol. - * @param appData Trading is enabled with the appData specified here. - */ - function enableTrading(bytes32 appData) external; - - /** - * @notice Disable any form of trading on CoW Protocol by this AMM. - */ - function disableTrading() external; - /** * @notice Restricts a specific AMM to being able to trade only the order * with the specified hash. @@ -121,14 +94,12 @@ interface IBCoWPool is IERC1271, IBPool { function SOLUTION_SETTLER() external view returns (ISettlement _solutionSettler); /** - * @notice The hash of the data describing which `GPv2Order.AppData` currently - * apply to this AMM. If this parameter is set to `NO_TRADING`, then the AMM - * does not accept any order as valid. - * If trading is enabled, then this value will be the [`hash`] of the only - * admissible [`GPv2Order.AppData`]. - * @return _appDataHash The hash of the allowed GPv2Order AppData. + * @notice The identifier describing which `GPv2Order.AppData` currently + * apply to this AMM. + * @return _appData The 32 bytes identifier of the allowed GPv2Order AppData. */ - function appDataHash() external view returns (bytes32 _appDataHash); + // solhint-disable-next-line style-guide-casing + function APP_DATA() external view returns (bytes32 _appData); /** * @notice This function returns the commitment hash that has been set by the diff --git a/test/integration/BCowPool.t.sol b/test/integration/BCowPool.t.sol index 3fb6cb6a..cd34318b 100644 --- a/test/integration/BCowPool.t.sol +++ b/test/integration/BCowPool.t.sol @@ -24,15 +24,8 @@ contract BCowPoolIntegrationTest is PoolSwapIntegrationTest, BCoWConst { bytes32 public constant APP_DATA = bytes32('exampleIntegrationAppData'); - function setUp() public override { - super.setUp(); - - // enable trading - IBCoWPool(address(pool)).enableTrading(APP_DATA); - } - function _deployFactory() internal override returns (IBFactory) { - return new BCoWFactory(address(settlement)); + return new BCoWFactory(address(settlement), APP_DATA); } function _makeSwap() internal override { diff --git a/test/manual-smock/MockBCoWPool.sol b/test/manual-smock/MockBCoWPool.sol index b2d27099..bdaf76a8 100644 --- a/test/manual-smock/MockBCoWPool.sol +++ b/test/manual-smock/MockBCoWPool.sol @@ -8,9 +8,6 @@ import {Test} from 'forge-std/Test.sol'; contract MockBCoWPool is BCoWPool, Test { /// MockBCoWPool mock methods - function set_appDataHash(bytes32 _appDataHash) public { - appDataHash = _appDataHash; - } function set_commitment(bytes32 _commitment) public { assembly ("memory-safe") { @@ -18,19 +15,7 @@ contract MockBCoWPool is BCoWPool, Test { } } - function mock_call_appDataHash(bytes32 _value) public { - vm.mockCall(address(this), abi.encodeWithSignature('appDataHash()'), abi.encode(_value)); - } - - constructor(address _cowSolutionSettler) BCoWPool(_cowSolutionSettler) {} - - function mock_call_enableTrading(bytes32 appData) public { - vm.mockCall(address(this), abi.encodeWithSignature('enableTrading(bytes32)', appData), abi.encode()); - } - - function mock_call_disableTrading() public { - vm.mockCall(address(this), abi.encodeWithSignature('disableTrading()'), abi.encode()); - } + constructor(address _cowSolutionSettler, bytes32 _appData) BCoWPool(_cowSolutionSettler, _appData) {} function mock_call_commit(bytes32 orderHash) public { vm.mockCall(address(this), abi.encodeWithSignature('commit(bytes32)', orderHash), abi.encode()); diff --git a/test/smock/MockBCoWFactory.sol b/test/smock/MockBCoWFactory.sol index 73a97e05..a6d84aa7 100644 --- a/test/smock/MockBCoWFactory.sol +++ b/test/smock/MockBCoWFactory.sol @@ -5,7 +5,7 @@ import {BCoWFactory, BCoWPool, BFactory, IBFactory, IBPool} from '../../src/cont import {Test} from 'forge-std/Test.sol'; contract MockBCoWFactory is BCoWFactory, Test { - constructor(address _solutionSettler) BCoWFactory(_solutionSettler) {} + constructor(address _solutionSettler, bytes32 _appData) BCoWFactory(_solutionSettler, _appData) {} function mock_call_newBPool(IBPool _pool) public { vm.mockCall(address(this), abi.encodeWithSignature('newBPool()'), abi.encode(_pool)); diff --git a/test/unit/BCoWFactory.t.sol b/test/unit/BCoWFactory.t.sol index 14fe6b67..5752e713 100644 --- a/test/unit/BCoWFactory.t.sol +++ b/test/unit/BCoWFactory.t.sol @@ -12,6 +12,7 @@ import {MockBCoWFactory} from 'test/smock/MockBCoWFactory.sol'; abstract contract BCoWFactoryTest is Base { address public solutionSettler = makeAddr('solutionSettler'); + bytes32 public appData = bytes32('appData'); function _configureBFactory() internal override returns (IBFactory) { vm.mockCall(solutionSettler, abi.encodePacked(ISettlement.domainSeparator.selector), abi.encode(bytes32(0))); @@ -19,24 +20,35 @@ abstract contract BCoWFactoryTest is Base { solutionSettler, abi.encodePacked(ISettlement.vaultRelayer.selector), abi.encode(makeAddr('vault relayer')) ); vm.prank(owner); - return new MockBCoWFactory(solutionSettler); + return new MockBCoWFactory(solutionSettler, appData); } } contract BCoWFactory_Unit_Constructor is BaseBFactory_Unit_Constructor, BCoWFactoryTest { function test_Set_SolutionSettler(address _settler) public { - MockBCoWFactory factory = new MockBCoWFactory(_settler); + MockBCoWFactory factory = new MockBCoWFactory(_settler, appData); assertEq(factory.SOLUTION_SETTLER(), _settler); } + + function test_Set_AppData(bytes32 _appData) public { + MockBCoWFactory factory = new MockBCoWFactory(solutionSettler, _appData); + assertEq(factory.APP_DATA(), _appData); + } } contract BCoWFactory_Unit_NewBPool is BaseBFactory_Unit_NewBPool, BCoWFactoryTest { function test_Set_SolutionSettler(address _settler) public { assumeNotForgeAddress(_settler); - bFactory = new MockBCoWFactory(_settler); + bFactory = new MockBCoWFactory(_settler, appData); vm.mockCall(_settler, abi.encodePacked(ISettlement.domainSeparator.selector), abi.encode(bytes32(0))); vm.mockCall(_settler, abi.encodePacked(ISettlement.vaultRelayer.selector), abi.encode(makeAddr('vault relayer'))); IBCoWPool bCoWPool = IBCoWPool(address(bFactory.newBPool())); assertEq(address(bCoWPool.SOLUTION_SETTLER()), _settler); } + + function test_Set_AppData(bytes32 _appData) public { + bFactory = new MockBCoWFactory(solutionSettler, _appData); + IBCoWPool bCoWPool = IBCoWPool(address(bFactory.newBPool())); + assertEq(bCoWPool.APP_DATA(), _appData); + } } diff --git a/test/unit/BCoWPool.t.sol b/test/unit/BCoWPool.t.sol index 29823840..22359ea6 100644 --- a/test/unit/BCoWPool.t.sol +++ b/test/unit/BCoWPool.t.sol @@ -18,6 +18,8 @@ abstract contract BaseCoWPoolTest is BasePoolTest, BCoWConst { address public cowSolutionSettler = makeAddr('cowSolutionSettler'); bytes32 public domainSeparator = bytes32(bytes2(0xf00b)); address public vaultRelayer = makeAddr('vaultRelayer'); + bytes32 public appData = bytes32('appData'); + GPv2Order.Data correctOrder; MockBCoWPool bCoWPool; @@ -26,7 +28,7 @@ abstract contract BaseCoWPoolTest is BasePoolTest, BCoWConst { super.setUp(); vm.mockCall(cowSolutionSettler, abi.encodePacked(ISettlement.domainSeparator.selector), abi.encode(domainSeparator)); vm.mockCall(cowSolutionSettler, abi.encodePacked(ISettlement.vaultRelayer.selector), abi.encode(vaultRelayer)); - bCoWPool = new MockBCoWPool(cowSolutionSettler); + bCoWPool = new MockBCoWPool(cowSolutionSettler, appData); bPool = MockBPool(address(bCoWPool)); _setRandomTokens(TOKENS_AMOUNT); correctOrder = GPv2Order.Data({ @@ -36,7 +38,7 @@ abstract contract BaseCoWPoolTest is BasePoolTest, BCoWConst { sellAmount: 0, buyAmount: 0, validTo: uint32(block.timestamp + 1 minutes), - appData: bytes32(0), + appData: appData, feeAmount: 0, kind: GPv2Order.KIND_SELL, partiallyFillable: false, @@ -51,7 +53,7 @@ contract BCoWPool_Unit_Constructor is BaseCoWPoolTest { assumeNotForgeAddress(_settler); vm.mockCall(_settler, abi.encodePacked(ISettlement.domainSeparator.selector), abi.encode(domainSeparator)); vm.mockCall(_settler, abi.encodePacked(ISettlement.vaultRelayer.selector), abi.encode(vaultRelayer)); - MockBCoWPool pool = new MockBCoWPool(_settler); + MockBCoWPool pool = new MockBCoWPool(_settler, appData); assertEq(address(pool.SOLUTION_SETTLER()), _settler); } @@ -59,7 +61,7 @@ contract BCoWPool_Unit_Constructor is BaseCoWPoolTest { assumeNotForgeAddress(_settler); vm.mockCall(_settler, abi.encodePacked(ISettlement.domainSeparator.selector), abi.encode(_separator)); vm.mockCall(_settler, abi.encodePacked(ISettlement.vaultRelayer.selector), abi.encode(vaultRelayer)); - MockBCoWPool pool = new MockBCoWPool(_settler); + MockBCoWPool pool = new MockBCoWPool(_settler, appData); assertEq(pool.SOLUTION_SETTLER_DOMAIN_SEPARATOR(), _separator); } @@ -67,9 +69,14 @@ contract BCoWPool_Unit_Constructor is BaseCoWPoolTest { assumeNotForgeAddress(_settler); vm.mockCall(_settler, abi.encodePacked(ISettlement.domainSeparator.selector), abi.encode(domainSeparator)); vm.mockCall(_settler, abi.encodePacked(ISettlement.vaultRelayer.selector), abi.encode(_relayer)); - MockBCoWPool pool = new MockBCoWPool(_settler); + MockBCoWPool pool = new MockBCoWPool(_settler, appData); assertEq(pool.VAULT_RELAYER(), _relayer); } + + function test_Set_AppData(bytes32 _appData) public { + MockBCoWPool pool = new MockBCoWPool(cowSolutionSettler, _appData); + assertEq(pool.APP_DATA(), _appData); + } } contract BCoWPool_Unit_Finalize is BaseCoWPoolTest { @@ -98,64 +105,6 @@ contract BCoWPool_Unit_Commit is BaseCoWPoolTest { } } -contract BCoWPool_Unit_DisableTranding is BaseCoWPoolTest { - function test_Revert_NonController(address sender) public { - // contract is deployed by this contract without any pranks - vm.assume(sender != address(this)); - vm.prank(sender); - vm.expectRevert(IBPool.BPool_CallerIsNotController.selector); - bCoWPool.disableTrading(); - } - - function test_Clear_AppdataHash(bytes32 appDataHash) public { - vm.assume(appDataHash != bytes32(0)); - bCoWPool.set_appDataHash(appDataHash); - bCoWPool.disableTrading(); - assertEq(bCoWPool.appDataHash(), bytes32(0)); - } - - function test_Emit_TradingDisabledEvent() public { - vm.expectEmit(); - emit IBCoWPool.TradingDisabled(); - bCoWPool.disableTrading(); - } - - function test_Succeed_AlreadyZeroAppdata() public { - bCoWPool.set_appDataHash(bytes32(0)); - bCoWPool.disableTrading(); - } -} - -contract BCoWPool_Unit_EnableTrading is BaseCoWPoolTest { - function test_Revert_NotFinalized(bytes32 appDataHash) public { - vm.expectRevert(IBPool.BPool_PoolNotFinalized.selector); - bCoWPool.enableTrading(appDataHash); - } - - function test_Revert_NonController(address sender, bytes32 appDataHash) public { - // contract is deployed by this contract without any pranks - vm.assume(sender != address(this)); - vm.prank(sender); - vm.expectRevert(IBPool.BPool_CallerIsNotController.selector); - bCoWPool.enableTrading(appDataHash); - } - - function test_Set_AppDataHash(bytes32 appData) public { - bCoWPool.set__finalized(true); - bytes32 appDataHash = keccak256(abi.encode(appData)); - bCoWPool.enableTrading(appData); - assertEq(bCoWPool.appDataHash(), appDataHash); - } - - function test_Emit_TradingEnabled(bytes32 appData) public { - bCoWPool.set__finalized(true); - bytes32 appDataHash = keccak256(abi.encode(appData)); - vm.expectEmit(); - emit IBCoWPool.TradingEnabled(appDataHash, appData); - bCoWPool.enableTrading(appData); - } -} - contract BCoWPool_Unit_Verify is BaseCoWPoolTest, SwapExactAmountInUtils { function setUp() public virtual override(BaseCoWPoolTest, BasePoolTest) { BaseCoWPoolTest.setUp(); @@ -306,31 +255,28 @@ contract BCoWPool_Unit_IsValidSignature is BaseCoWPoolTest { bCoWPool.finalize(); } - modifier _withTradingEnabled(bytes32 _appData) { - bCoWPool.set_appDataHash(keccak256(abi.encode(_appData))); - _; - } + modifier happyPath(GPv2Order.Data memory _order) { + // sets the order appData to the one defined at deployment (setUp) + _order.appData = appData; - modifier _withValidCommitment(GPv2Order.Data memory _order) { + // stores the order hash in the transient storage slot bytes32 _orderHash = GPv2Order.hash(_order, domainSeparator); bCoWPool.set_commitment(_orderHash); _; } - function test_Revert_OrderWithWrongAppdata( - bytes32 _appData, - GPv2Order.Data memory _order - ) public _withTradingEnabled(_appData) { - vm.assume(_order.appData != _appData); - bytes32 _appDataHash = keccak256(abi.encode(_appData)); - vm.expectRevert(IBCoWPool.AppDataDoNotMatchHash.selector); - bCoWPool.isValidSignature(_appDataHash, abi.encode(_order)); + 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 _withTradingEnabled(_order.appData) _withValidCommitment(_order) { + ) public happyPath(_order) { vm.assume(_differentDomainSeparator != domainSeparator); bytes32 _orderHash = GPv2Order.hash(_order, _differentDomainSeparator); vm.expectRevert(IBCoWPool.OrderDoesNotMatchMessageHash.selector); @@ -340,7 +286,7 @@ contract BCoWPool_Unit_IsValidSignature is BaseCoWPoolTest { function test_Revert_OrderWithUnrelatedSignature( GPv2Order.Data memory _order, bytes32 _orderHash - ) public _withTradingEnabled(_order.appData) { + ) public happyPath(_order) { vm.expectRevert(IBCoWPool.OrderDoesNotMatchMessageHash.selector); bCoWPool.isValidSignature(_orderHash, abi.encode(_order)); } @@ -348,29 +294,21 @@ contract BCoWPool_Unit_IsValidSignature is BaseCoWPoolTest { function test_Revert_OrderHashDifferentFromCommitment( GPv2Order.Data memory _order, bytes32 _differentCommitment - ) public _withTradingEnabled(_order.appData) { + ) public happyPath(_order) { bCoWPool.set_commitment(_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 - _withTradingEnabled(_order.appData) - _withValidCommitment(_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 - _withTradingEnabled(_order.appData) - _withValidCommitment(_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); From 41e2a4d4b5492ec16455525f8d2b7f1b862bdb25 Mon Sep 17 00:00:00 2001 From: teddy Date: Tue, 18 Jun 2024 15:36:42 -0300 Subject: [PATCH 26/32] feat: using transient storage reentrancy locks (#108) * chore: manually track BPool mock * refactor: use transient storage for reentrancy locks * chore: update forge snapshots with sweet gas savings * fix: function order * fix: use arbitrary transient storage slot * fix: improve natspec * fix: more formal natspec * fix: extra natspec improvements --- .forge-snapshots/newBFactory.snap | 2 +- .forge-snapshots/newBPool.snap | 2 +- .forge-snapshots/swapExactAmountIn.snap | 2 +- .../swapExactAmountInInverse.snap | 2 +- src/contracts/BConst.sol | 9 +++++ src/contracts/BPool.sol | 34 +++++++++++++++---- test/manual-smock/MockBCoWPool.sol | 8 ----- test/{smock => manual-smock}/MockBPool.sol | 12 ++++--- test/unit/BCoWPool.t.sol | 2 +- test/unit/BPool.t.sol | 6 ++-- 10 files changed, 53 insertions(+), 26 deletions(-) rename test/{smock => manual-smock}/MockBPool.sol (97%) diff --git a/.forge-snapshots/newBFactory.snap b/.forge-snapshots/newBFactory.snap index 36a0c47a..6ba4a701 100644 --- a/.forge-snapshots/newBFactory.snap +++ b/.forge-snapshots/newBFactory.snap @@ -1 +1 @@ -3520860 \ No newline at end of file +3479342 \ No newline at end of file diff --git a/.forge-snapshots/newBPool.snap b/.forge-snapshots/newBPool.snap index 0e8f8d0e..946d21eb 100644 --- a/.forge-snapshots/newBPool.snap +++ b/.forge-snapshots/newBPool.snap @@ -1 +1 @@ -3307804 \ No newline at end of file +3269056 \ No newline at end of file diff --git a/.forge-snapshots/swapExactAmountIn.snap b/.forge-snapshots/swapExactAmountIn.snap index 536f7edc..de31e89b 100644 --- a/.forge-snapshots/swapExactAmountIn.snap +++ b/.forge-snapshots/swapExactAmountIn.snap @@ -1 +1 @@ -86463 \ No newline at end of file +81475 \ No newline at end of file diff --git a/.forge-snapshots/swapExactAmountInInverse.snap b/.forge-snapshots/swapExactAmountInInverse.snap index 16907fa9..be80398b 100644 --- a/.forge-snapshots/swapExactAmountInInverse.snap +++ b/.forge-snapshots/swapExactAmountInInverse.snap @@ -1 +1 @@ -96122 \ No newline at end of file +91134 \ No newline at end of file diff --git a/src/contracts/BConst.sol b/src/contracts/BConst.sol index 1936aec6..bb893f25 100644 --- a/src/contracts/BConst.sol +++ b/src/contracts/BConst.sol @@ -24,4 +24,13 @@ contract BConst { uint256 public constant MAX_IN_RATIO = BONE / 2; uint256 public constant MAX_OUT_RATIO = (BONE / 3) + 1 wei; + + // Using an arbitrary storage slot to prevent possible future + // _transient_ variables defined by solidity from overriding it, if they were + // to start on slot zero as regular storage variables do. Value is: + // uint256(keccak256('BPool.transientStorageLock')) - 1; + uint256 internal constant _MUTEX_TRANSIENT_STORAGE_SLOT = + 0x3f8f4c536ce1b925b469af1b09a44da237dab5bbc584585648c12be1ca25a8c4; + bytes32 internal constant _MUTEX_FREE = bytes32(uint256(0)); + bytes32 internal constant _MUTEX_TAKEN = bytes32(uint256(1)); } diff --git a/src/contracts/BPool.sol b/src/contracts/BPool.sol index a61fb707..4aed30ea 100644 --- a/src/contracts/BPool.sol +++ b/src/contracts/BPool.sol @@ -11,8 +11,6 @@ import {IBPool} from 'interfaces/IBPool.sol'; * @notice Pool contract that holds tokens, allows to swap, add and remove liquidity. */ contract BPool is BToken, BMath, IBPool { - /// @dev True if a call to the contract is in progress, False otherwise - bool internal _mutex; /// @dev BFactory address to push token exitFee to address internal _factory; /// @dev Has CONTROL role @@ -36,17 +34,17 @@ contract BPool is BToken, BMath, IBPool { /// @dev Prevents reentrancy in non-view functions modifier _lock_() { - if (_mutex) { + if (_getLock() != _MUTEX_FREE) { revert BPool_Reentrancy(); } - _mutex = true; + _setLock(_MUTEX_TAKEN); _; - _mutex = false; + _setLock(_MUTEX_FREE); } /// @dev Prevents reentrancy in view functions modifier _viewlock_() { - if (_mutex) { + if (_getLock() != _MUTEX_FREE) { revert BPool_Reentrancy(); } _; @@ -603,6 +601,18 @@ contract BPool is BToken, BMath, IBPool { return _controller; } + /** + * @notice Sets the value of the transient storage slot used for reentrancy locks + * @param _value The value of the transient storage slot used for reentrancy locks. + * @dev Should be set to _MUTEX_FREE after a call, any other value will + * be interpreted as locked + */ + function _setLock(bytes32 _value) internal { + assembly ("memory-safe") { + tstore(_MUTEX_TRANSIENT_STORAGE_SLOT, _value) + } + } + /** * @dev Pulls tokens from the sender. Tokens needs to be approved first. Calls are not locked. * @param erc20 The address of the token to pull @@ -669,4 +679,16 @@ contract BPool is BToken, BMath, IBPool { function _burnPoolShare(uint256 amount) internal { _burn(address(this), amount); } + + /** + * @notice Gets the value of the transient storage slot used for reentrancy locks + * @return _value Contents of transient storage slot used for reentrancy locks. + * @dev Should only be compared against _MUTEX_FREE for the purposes of + * allowing calls + */ + function _getLock() internal view returns (bytes32 _value) { + assembly ("memory-safe") { + _value := tload(_MUTEX_TRANSIENT_STORAGE_SLOT) + } + } } diff --git a/test/manual-smock/MockBCoWPool.sol b/test/manual-smock/MockBCoWPool.sol index bdaf76a8..0963f30a 100644 --- a/test/manual-smock/MockBCoWPool.sol +++ b/test/manual-smock/MockBCoWPool.sol @@ -42,14 +42,6 @@ contract MockBCoWPool is BCoWPool, Test { } /// BPool Mocked methods - function set__mutex(bool __mutex) public { - _mutex = __mutex; - } - - function call__mutex() public view returns (bool) { - return _mutex; - } - function set__factory(address __factory) public { _factory = __factory; } diff --git a/test/smock/MockBPool.sol b/test/manual-smock/MockBPool.sol similarity index 97% rename from test/smock/MockBPool.sol rename to test/manual-smock/MockBPool.sol index 24f3912a..6f46ced5 100644 --- a/test/smock/MockBPool.sol +++ b/test/manual-smock/MockBPool.sol @@ -5,12 +5,16 @@ import {BMath, BPool, BToken, IBPool, IERC20} from '../../src/contracts/BPool.so import {Test} from 'forge-std/Test.sol'; contract MockBPool is BPool, Test { - function set__mutex(bool __mutex) public { - _mutex = __mutex; + function call__setLock(bytes32 _value) public { + assembly ("memory-safe") { + tstore(_MUTEX_TRANSIENT_STORAGE_SLOT, _value) + } } - function call__mutex() public view returns (bool) { - return _mutex; + function call__getLock() public view returns (bytes32 _value) { + assembly ("memory-safe") { + _value := tload(_MUTEX_TRANSIENT_STORAGE_SLOT) + } } function set__factory(address __factory) public { diff --git a/test/unit/BCoWPool.t.sol b/test/unit/BCoWPool.t.sol index 22359ea6..6e1a7223 100644 --- a/test/unit/BCoWPool.t.sol +++ b/test/unit/BCoWPool.t.sol @@ -12,7 +12,7 @@ import {IBCoWPool} from 'interfaces/IBCoWPool.sol'; import {IBPool} from 'interfaces/IBPool.sol'; import {ISettlement} from 'interfaces/ISettlement.sol'; import {MockBCoWPool} from 'test/manual-smock/MockBCoWPool.sol'; -import {MockBPool} from 'test/smock/MockBPool.sol'; +import {MockBPool} from 'test/manual-smock/MockBPool.sol'; abstract contract BaseCoWPoolTest is BasePoolTest, BCoWConst { address public cowSolutionSettler = makeAddr('cowSolutionSettler'); diff --git a/test/unit/BPool.t.sol b/test/unit/BPool.t.sol index 71000b85..94453db3 100644 --- a/test/unit/BPool.t.sol +++ b/test/unit/BPool.t.sol @@ -5,7 +5,7 @@ import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import {BPool} from 'contracts/BPool.sol'; import {IBPool} from 'interfaces/IBPool.sol'; -import {MockBPool} from 'test/smock/MockBPool.sol'; +import {MockBPool} from 'test/manual-smock/MockBPool.sol'; import {BConst} from 'contracts/BConst.sol'; import {BMath} from 'contracts/BMath.sol'; @@ -82,10 +82,10 @@ abstract contract BasePoolTest is Test, BConst, Utils, BMath { function _expectRevertByReentrancy() internal { // Assert that the contract is accessible - assertEq(bPool.call__mutex(), false); + assertEq(bPool.call__getLock(), _MUTEX_FREE); // Simulate ongoing call to the contract - bPool.set__mutex(true); + bPool.call__setLock(_MUTEX_TAKEN); vm.expectRevert(IBPool.BPool_Reentrancy.selector); } From 5852bc4b53463bb85951a4b8fc174e9bf3a50c96 Mon Sep 17 00:00:00 2001 From: Austrian <114922365+0xAustrian@users.noreply.github.com> Date: Wed, 19 Jun 2024 10:55:41 -0300 Subject: [PATCH 27/32] feat: emit event when finalize (#109) * feat: emit COWAMMPoolCreated when finalize, no tests * test: add unit tests * feat: update mocks folder * feat: rename event * feat: revert instead of just return * chore: delete unused smock files * test: small improvement * feat: add try-catch * fix: add new line in comment * test: assert caller in events * docs: document the new function --- .forge-snapshots/newBFactory.snap | 2 +- .forge-snapshots/newBPool.snap | 2 +- README.md | 5 +- src/contracts/BCoWFactory.sol | 12 +- src/contracts/BCoWPool.sol | 19 +- src/interfaces/IBCoWFactory.sol | 22 ++ .../MockBCoWFactory.sol | 10 +- test/smock/MockBCoWPool.sol | 39 ++ test/smock/MockBPool.sol | 332 ++++++++++++++++++ test/unit/BCoWFactory.t.sol | 29 +- test/unit/BCoWPool.t.sol | 37 +- 11 files changed, 489 insertions(+), 20 deletions(-) create mode 100644 src/interfaces/IBCoWFactory.sol rename test/{smock => manual-smock}/MockBCoWFactory.sol (53%) create mode 100644 test/smock/MockBCoWPool.sol create mode 100644 test/smock/MockBPool.sol diff --git a/.forge-snapshots/newBFactory.snap b/.forge-snapshots/newBFactory.snap index 6ba4a701..cf164054 100644 --- a/.forge-snapshots/newBFactory.snap +++ b/.forge-snapshots/newBFactory.snap @@ -1 +1 @@ -3479342 \ No newline at end of file +3525470 \ No newline at end of file diff --git a/.forge-snapshots/newBPool.snap b/.forge-snapshots/newBPool.snap index 946d21eb..b4fbdb7e 100644 --- a/.forge-snapshots/newBPool.snap +++ b/.forge-snapshots/newBPool.snap @@ -1 +1 @@ -3269056 \ No newline at end of file +3315224 \ No newline at end of file diff --git a/README.md b/README.md index 467949b2..94ddceac 100644 --- a/README.md +++ b/README.md @@ -43,4 +43,7 @@ yarn test # run the tests - Implements IERC1271 `isValidSignature` method to allow for validating intentions of swaps - Implements a `commit` method to avoid multiple swaps from conflicting with each other - Allows the controller to allow only one `GPv2Order.appData` at a time -- Validates the `GPv2Order` requirements before allowing the swap \ No newline at end of file +- Validates the `GPv2Order` requirements before allowing the swap + +## Features on BCoWFactory +- Added a `logBCoWPool` to log the finalization of BCoWPool contracts, to be called by a child pool \ No newline at end of file diff --git a/src/contracts/BCoWFactory.sol b/src/contracts/BCoWFactory.sol index bc2538c5..017d8ab4 100644 --- a/src/contracts/BCoWFactory.sol +++ b/src/contracts/BCoWFactory.sol @@ -2,8 +2,8 @@ pragma solidity 0.8.25; import {BCoWPool} from './BCoWPool.sol'; - import {BFactory} from './BFactory.sol'; +import {IBCoWFactory} from 'interfaces/IBCoWFactory.sol'; import {IBFactory} from 'interfaces/IBFactory.sol'; import {IBPool} from 'interfaces/IBPool.sol'; @@ -11,7 +11,7 @@ import {IBPool} from 'interfaces/IBPool.sol'; * @title BCoWFactory * @notice Creates new BCoWPools, logging their addresses and acting as a registry of pools. */ -contract BCoWFactory is BFactory { +contract BCoWFactory is BFactory, IBCoWFactory { address public immutable SOLUTION_SETTLER; bytes32 public immutable APP_DATA; @@ -25,11 +25,17 @@ contract BCoWFactory is BFactory { * @dev Deploys a BCoWPool instead of a regular BPool, maintains the interface * to minimize required changes to existing tooling */ - function newBPool() external override returns (IBPool _pool) { + function newBPool() external override(BFactory, IBFactory) returns (IBPool _pool) { IBPool bpool = new BCoWPool(SOLUTION_SETTLER, APP_DATA); _isBPool[address(bpool)] = true; emit LOG_NEW_POOL(msg.sender, address(bpool)); bpool.setController(msg.sender); return bpool; } + + /// @inheritdoc IBCoWFactory + function logBCoWPool() external { + if (!_isBPool[msg.sender]) revert BCoWFactory_NotValidBCoWPool(); + emit COWAMMPoolCreated(msg.sender); + } } diff --git a/src/contracts/BCoWPool.sol b/src/contracts/BCoWPool.sol index 9d9ad3ba..42e6809c 100644 --- a/src/contracts/BCoWPool.sol +++ b/src/contracts/BCoWPool.sol @@ -16,13 +16,13 @@ https://defi.sucks */ +import {BCoWConst} from './BCoWConst.sol'; +import {BPool} from './BPool.sol'; +import {GPv2Order} from '@cowprotocol/libraries/GPv2Order.sol'; import {IERC1271} from '@openzeppelin/contracts/interfaces/IERC1271.sol'; import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; -import {GPv2Order} from '@cowprotocol/libraries/GPv2Order.sol'; - -import {BCoWConst} from './BCoWConst.sol'; -import {BPool} from './BPool.sol'; +import {IBCoWFactory} from 'interfaces/IBCoWFactory.sol'; import {IBCoWPool} from 'interfaces/IBCoWPool.sol'; import {ISettlement} from 'interfaces/ISettlement.sol'; @@ -142,11 +142,20 @@ contract BCoWPool is IERC1271, IBCoWPool, BPool, BCoWConst { /** * @inheritdoc BPool * @dev Grants infinite approval to the vault relayer for all tokens in the - * pool after the finalization of the setup. + * pool after the finalization of the setup. Also emits COWAMMPoolCreated() event. */ function _afterFinalize() internal override { for (uint256 i; i < _tokens.length; i++) { IERC20(_tokens[i]).approve(VAULT_RELAYER, type(uint256).max); } + + // Make the factory emit the event, to be easily indexed by off-chain agents + // If this pool was not deployed using a bCoWFactory, this will revert and catch + // And the event will be emitted by this contract instead + // solhint-disable-next-line no-empty-blocks + try IBCoWFactory(_factory).logBCoWPool() {} + catch { + emit IBCoWFactory.COWAMMPoolCreated(address(this)); + } } } diff --git a/src/interfaces/IBCoWFactory.sol b/src/interfaces/IBCoWFactory.sol new file mode 100644 index 00000000..58e9b304 --- /dev/null +++ b/src/interfaces/IBCoWFactory.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.25; + +import {IBFactory} from 'interfaces/IBFactory.sol'; + +interface IBCoWFactory is IBFactory { + /** + * @notice Emitted when a bCoWPool created by this factory is finalized + * @param bCoWPool The pool just finalized + */ + event COWAMMPoolCreated(address indexed bCoWPool); + + /** + * @notice thrown when the caller of `logBCoWPool()` is not a bCoWPool created by this factory + */ + error BCoWFactory_NotValidBCoWPool(); + + /** + * @notice Emits the COWAMMPoolCreated event if the caller is a bCoWPool, to be indexed by off-chain agents + */ + function logBCoWPool() external; +} diff --git a/test/smock/MockBCoWFactory.sol b/test/manual-smock/MockBCoWFactory.sol similarity index 53% rename from test/smock/MockBCoWFactory.sol rename to test/manual-smock/MockBCoWFactory.sol index a6d84aa7..2f3433b4 100644 --- a/test/smock/MockBCoWFactory.sol +++ b/test/manual-smock/MockBCoWFactory.sol @@ -1,13 +1,21 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.0; -import {BCoWFactory, BCoWPool, BFactory, IBFactory, IBPool} from '../../src/contracts/BCoWFactory.sol'; +import {BCoWFactory, BCoWPool, BFactory, IBCoWFactory, IBFactory, IBPool} from '../../src/contracts/BCoWFactory.sol'; import {Test} from 'forge-std/Test.sol'; contract MockBCoWFactory is BCoWFactory, Test { constructor(address _solutionSettler, bytes32 _appData) BCoWFactory(_solutionSettler, _appData) {} + function set__isBPool(address _key0, bool _value) public { + _isBPool[_key0] = _value; + } + function mock_call_newBPool(IBPool _pool) public { vm.mockCall(address(this), abi.encodeWithSignature('newBPool()'), abi.encode(_pool)); } + + function mock_call_logBCoWPool() public { + vm.mockCall(address(this), abi.encodeWithSignature('logBCoWPool()'), abi.encode()); + } } diff --git a/test/smock/MockBCoWPool.sol b/test/smock/MockBCoWPool.sol new file mode 100644 index 00000000..4ba59f07 --- /dev/null +++ b/test/smock/MockBCoWPool.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.0; + +import { + BCoWConst, + BCoWPool, + BPool, + GPv2Order, + IBCoWFactory, + IBCoWPool, + IERC1271, + IERC20, + ISettlement +} from '../../src/contracts/BCoWPool.sol'; +import {Test} from 'forge-std/Test.sol'; + +contract MockBCoWPool is BCoWPool, Test { + constructor(address _cowSolutionSettler, bytes32 _appData) BCoWPool(_cowSolutionSettler, _appData) {} + + function mock_call_commit(bytes32 orderHash) public { + vm.mockCall(address(this), abi.encodeWithSignature('commit(bytes32)', orderHash), abi.encode()); + } + + function mock_call_isValidSignature(bytes32 _hash, bytes memory signature, bytes4 _returnParam0) public { + vm.mockCall( + address(this), + abi.encodeWithSignature('isValidSignature(bytes32,bytes)', _hash, signature), + abi.encode(_returnParam0) + ); + } + + function mock_call_commitment(bytes32 value) public { + vm.mockCall(address(this), abi.encodeWithSignature('commitment()'), abi.encode(value)); + } + + function mock_call_verify(GPv2Order.Data memory order) public { + vm.mockCall(address(this), abi.encodeWithSignature('verify(GPv2Order.Data)', order), abi.encode()); + } +} diff --git a/test/smock/MockBPool.sol b/test/smock/MockBPool.sol new file mode 100644 index 00000000..474683b3 --- /dev/null +++ b/test/smock/MockBPool.sol @@ -0,0 +1,332 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.0; + +import {BMath, BPool, BToken, IBPool, IERC20} from '../../src/contracts/BPool.sol'; +import {Test} from 'forge-std/Test.sol'; + +contract MockBPool is BPool, Test { + function set__factory(address __factory) public { + _factory = __factory; + } + + function call__factory() public view returns (address) { + return _factory; + } + + function set__controller(address __controller) public { + _controller = __controller; + } + + function call__controller() public view returns (address) { + return _controller; + } + + function set__swapFee(uint256 __swapFee) public { + _swapFee = __swapFee; + } + + function call__swapFee() public view returns (uint256) { + return _swapFee; + } + + function set__finalized(bool __finalized) public { + _finalized = __finalized; + } + + function call__finalized() public view returns (bool) { + return _finalized; + } + + function set__tokens(address[] memory __tokens) public { + _tokens = __tokens; + } + + function call__tokens() public view returns (address[] memory) { + return _tokens; + } + + function set__records(address _key0, IBPool.Record memory _value) public { + _records[_key0] = _value; + } + + function call__records(address _key0) public view returns (IBPool.Record memory) { + return _records[_key0]; + } + + function set__totalWeight(uint256 __totalWeight) public { + _totalWeight = __totalWeight; + } + + function call__totalWeight() public view returns (uint256) { + return _totalWeight; + } + + constructor() BPool() {} + + function mock_call_setSwapFee(uint256 swapFee) public { + vm.mockCall(address(this), abi.encodeWithSignature('setSwapFee(uint256)', swapFee), abi.encode()); + } + + function mock_call_setController(address manager) public { + vm.mockCall(address(this), abi.encodeWithSignature('setController(address)', manager), abi.encode()); + } + + function mock_call_finalize() public { + vm.mockCall(address(this), abi.encodeWithSignature('finalize()'), abi.encode()); + } + + function mock_call_bind(address token, uint256 balance, uint256 denorm) public { + vm.mockCall( + address(this), abi.encodeWithSignature('bind(address,uint256,uint256)', token, balance, denorm), abi.encode() + ); + } + + function mock_call_unbind(address token) public { + vm.mockCall(address(this), abi.encodeWithSignature('unbind(address)', token), abi.encode()); + } + + function mock_call_joinPool(uint256 poolAmountOut, uint256[] calldata maxAmountsIn) public { + vm.mockCall( + address(this), abi.encodeWithSignature('joinPool(uint256,uint256[])', poolAmountOut, maxAmountsIn), abi.encode() + ); + } + + function mock_call_exitPool(uint256 poolAmountIn, uint256[] calldata minAmountsOut) public { + vm.mockCall( + address(this), abi.encodeWithSignature('exitPool(uint256,uint256[])', poolAmountIn, minAmountsOut), abi.encode() + ); + } + + function mock_call_swapExactAmountIn( + address tokenIn, + uint256 tokenAmountIn, + address tokenOut, + uint256 minAmountOut, + uint256 maxPrice, + uint256 tokenAmountOut, + uint256 spotPriceAfter + ) public { + vm.mockCall( + address(this), + abi.encodeWithSignature( + 'swapExactAmountIn(address,uint256,address,uint256,uint256)', + tokenIn, + tokenAmountIn, + tokenOut, + minAmountOut, + maxPrice + ), + abi.encode(tokenAmountOut, spotPriceAfter) + ); + } + + function mock_call_swapExactAmountOut( + address tokenIn, + uint256 maxAmountIn, + address tokenOut, + uint256 tokenAmountOut, + uint256 maxPrice, + uint256 tokenAmountIn, + uint256 spotPriceAfter + ) public { + vm.mockCall( + address(this), + abi.encodeWithSignature( + 'swapExactAmountOut(address,uint256,address,uint256,uint256)', + tokenIn, + maxAmountIn, + tokenOut, + tokenAmountOut, + maxPrice + ), + abi.encode(tokenAmountIn, spotPriceAfter) + ); + } + + function mock_call_joinswapExternAmountIn( + address tokenIn, + uint256 tokenAmountIn, + uint256 minPoolAmountOut, + uint256 poolAmountOut + ) public { + vm.mockCall( + address(this), + abi.encodeWithSignature( + 'joinswapExternAmountIn(address,uint256,uint256)', tokenIn, tokenAmountIn, minPoolAmountOut + ), + abi.encode(poolAmountOut) + ); + } + + function mock_call_joinswapPoolAmountOut( + address tokenIn, + uint256 poolAmountOut, + uint256 maxAmountIn, + uint256 tokenAmountIn + ) public { + vm.mockCall( + address(this), + abi.encodeWithSignature('joinswapPoolAmountOut(address,uint256,uint256)', tokenIn, poolAmountOut, maxAmountIn), + abi.encode(tokenAmountIn) + ); + } + + function mock_call_exitswapPoolAmountIn( + address tokenOut, + uint256 poolAmountIn, + uint256 minAmountOut, + uint256 tokenAmountOut + ) public { + vm.mockCall( + address(this), + abi.encodeWithSignature('exitswapPoolAmountIn(address,uint256,uint256)', tokenOut, poolAmountIn, minAmountOut), + abi.encode(tokenAmountOut) + ); + } + + function mock_call_exitswapExternAmountOut( + address tokenOut, + uint256 tokenAmountOut, + uint256 maxPoolAmountIn, + uint256 poolAmountIn + ) public { + vm.mockCall( + address(this), + abi.encodeWithSignature( + 'exitswapExternAmountOut(address,uint256,uint256)', tokenOut, tokenAmountOut, maxPoolAmountIn + ), + abi.encode(poolAmountIn) + ); + } + + function mock_call_getSpotPrice(address tokenIn, address tokenOut, uint256 spotPrice) public { + vm.mockCall( + address(this), abi.encodeWithSignature('getSpotPrice(address,address)', tokenIn, tokenOut), abi.encode(spotPrice) + ); + } + + function mock_call_getSpotPriceSansFee(address tokenIn, address tokenOut, uint256 spotPrice) public { + vm.mockCall( + address(this), + abi.encodeWithSignature('getSpotPriceSansFee(address,address)', tokenIn, tokenOut), + abi.encode(spotPrice) + ); + } + + function mock_call_isFinalized(bool _returnParam0) public { + vm.mockCall(address(this), abi.encodeWithSignature('isFinalized()'), abi.encode(_returnParam0)); + } + + function mock_call_isBound(address t, bool _returnParam0) public { + vm.mockCall(address(this), abi.encodeWithSignature('isBound(address)', t), abi.encode(_returnParam0)); + } + + function mock_call_getNumTokens(uint256 _returnParam0) public { + vm.mockCall(address(this), abi.encodeWithSignature('getNumTokens()'), abi.encode(_returnParam0)); + } + + function mock_call_getCurrentTokens(address[] memory tokens) public { + vm.mockCall(address(this), abi.encodeWithSignature('getCurrentTokens()'), abi.encode(tokens)); + } + + function mock_call_getFinalTokens(address[] memory tokens) public { + vm.mockCall(address(this), abi.encodeWithSignature('getFinalTokens()'), abi.encode(tokens)); + } + + function mock_call_getDenormalizedWeight(address token, uint256 _returnParam0) public { + vm.mockCall( + address(this), abi.encodeWithSignature('getDenormalizedWeight(address)', token), abi.encode(_returnParam0) + ); + } + + function mock_call_getTotalDenormalizedWeight(uint256 _returnParam0) public { + vm.mockCall(address(this), abi.encodeWithSignature('getTotalDenormalizedWeight()'), abi.encode(_returnParam0)); + } + + function mock_call_getNormalizedWeight(address token, uint256 _returnParam0) public { + vm.mockCall( + address(this), abi.encodeWithSignature('getNormalizedWeight(address)', token), abi.encode(_returnParam0) + ); + } + + function mock_call_getBalance(address token, uint256 _returnParam0) public { + vm.mockCall(address(this), abi.encodeWithSignature('getBalance(address)', token), abi.encode(_returnParam0)); + } + + function mock_call_getSwapFee(uint256 _returnParam0) public { + vm.mockCall(address(this), abi.encodeWithSignature('getSwapFee()'), abi.encode(_returnParam0)); + } + + function mock_call_getController(address _returnParam0) public { + vm.mockCall(address(this), abi.encodeWithSignature('getController()'), abi.encode(_returnParam0)); + } + + function mock_call__pullUnderlying(address erc20, address from, uint256 amount) public { + vm.mockCall( + address(this), + abi.encodeWithSignature('_pullUnderlying(address,address,uint256)', erc20, from, amount), + abi.encode() + ); + } + + function _pullUnderlying(address erc20, address from, uint256 amount) internal override { + (bool _success, bytes memory _data) = + address(this).call(abi.encodeWithSignature('_pullUnderlying(address,address,uint256)', erc20, from, amount)); + + if (_success) return abi.decode(_data, ()); + else return super._pullUnderlying(erc20, from, amount); + } + + function call__pullUnderlying(address erc20, address from, uint256 amount) public { + return _pullUnderlying(erc20, from, amount); + } + + function expectCall__pullUnderlying(address erc20, address from, uint256 amount) public { + vm.expectCall( + address(this), abi.encodeWithSignature('_pullUnderlying(address,address,uint256)', erc20, from, amount) + ); + } + + function mock_call__pushUnderlying(address erc20, address to, uint256 amount) public { + vm.mockCall( + address(this), + abi.encodeWithSignature('_pushUnderlying(address,address,uint256)', erc20, to, amount), + abi.encode() + ); + } + + function _pushUnderlying(address erc20, address to, uint256 amount) internal override { + (bool _success, bytes memory _data) = + address(this).call(abi.encodeWithSignature('_pushUnderlying(address,address,uint256)', erc20, to, amount)); + + if (_success) return abi.decode(_data, ()); + else return super._pushUnderlying(erc20, to, amount); + } + + function call__pushUnderlying(address erc20, address to, uint256 amount) public { + return _pushUnderlying(erc20, to, amount); + } + + function expectCall__pushUnderlying(address erc20, address to, uint256 amount) public { + vm.expectCall(address(this), abi.encodeWithSignature('_pushUnderlying(address,address,uint256)', erc20, to, amount)); + } + + function mock_call__afterFinalize() public { + vm.mockCall(address(this), abi.encodeWithSignature('_afterFinalize()'), abi.encode()); + } + + function _afterFinalize() internal override { + (bool _success, bytes memory _data) = address(this).call(abi.encodeWithSignature('_afterFinalize()')); + + if (_success) return abi.decode(_data, ()); + else return super._afterFinalize(); + } + + function call__afterFinalize() public { + return _afterFinalize(); + } + + function expectCall__afterFinalize() public { + vm.expectCall(address(this), abi.encodeWithSignature('_afterFinalize()')); + } +} diff --git a/test/unit/BCoWFactory.t.sol b/test/unit/BCoWFactory.t.sol index 5752e713..09c42965 100644 --- a/test/unit/BCoWFactory.t.sol +++ b/test/unit/BCoWFactory.t.sol @@ -1,14 +1,13 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.25; -import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; - import {Base, BaseBFactory_Unit_Constructor, BaseBFactory_Unit_NewBPool} from './BFactory.t.sol'; - +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import {IBCoWFactory} from 'interfaces/IBCoWFactory.sol'; import {IBCoWPool} from 'interfaces/IBCoWPool.sol'; import {IBFactory} from 'interfaces/IBFactory.sol'; import {ISettlement} from 'interfaces/ISettlement.sol'; -import {MockBCoWFactory} from 'test/smock/MockBCoWFactory.sol'; +import {MockBCoWFactory} from 'test/manual-smock/MockBCoWFactory.sol'; abstract contract BCoWFactoryTest is Base { address public solutionSettler = makeAddr('solutionSettler'); @@ -52,3 +51,25 @@ contract BCoWFactory_Unit_NewBPool is BaseBFactory_Unit_NewBPool, BCoWFactoryTes assertEq(bCoWPool.APP_DATA(), _appData); } } + +contract BCoWPoolFactory_Unit_LogBCoWPool is BCoWFactoryTest { + function test_Revert_NotValidBCoWPool(address _pool) public { + bFactory = new MockBCoWFactory(solutionSettler, appData); + MockBCoWFactory(address(bFactory)).set__isBPool(address(_pool), false); + + vm.expectRevert(IBCoWFactory.BCoWFactory_NotValidBCoWPool.selector); + + vm.prank(_pool); + IBCoWFactory(address(bFactory)).logBCoWPool(); + } + + function test_Emit_COWAMMPoolCreated(address _pool) public { + bFactory = new MockBCoWFactory(solutionSettler, appData); + MockBCoWFactory(address(bFactory)).set__isBPool(address(_pool), true); + vm.expectEmit(address(bFactory)); + emit IBCoWFactory.COWAMMPoolCreated(_pool); + + vm.prank(_pool); + IBCoWFactory(address(bFactory)).logBCoWPool(); + } +} diff --git a/test/unit/BCoWPool.t.sol b/test/unit/BCoWPool.t.sol index 6e1a7223..57114f5b 100644 --- a/test/unit/BCoWPool.t.sol +++ b/test/unit/BCoWPool.t.sol @@ -1,13 +1,12 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.25; +import {BasePoolTest, SwapExactAmountInUtils} from './BPool.t.sol'; import {IERC20} from '@cowprotocol/interfaces/IERC20.sol'; import {GPv2Order} from '@cowprotocol/libraries/GPv2Order.sol'; import {IERC1271} from '@openzeppelin/contracts/interfaces/IERC1271.sol'; - -import {BasePoolTest, SwapExactAmountInUtils} from './BPool.t.sol'; - import {BCoWConst} from 'contracts/BCoWConst.sol'; +import {IBCoWFactory} from 'interfaces/IBCoWFactory.sol'; import {IBCoWPool} from 'interfaces/IBCoWPool.sol'; import {IBPool} from 'interfaces/IBPool.sol'; import {ISettlement} from 'interfaces/ISettlement.sol'; @@ -80,13 +79,40 @@ contract BCoWPool_Unit_Constructor is BaseCoWPoolTest { } contract BCoWPool_Unit_Finalize is BaseCoWPoolTest { - function test_Set_Approvals() public { + 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.call__factory()), abi.encodeWithSelector(IBCoWFactory.logBCoWPool.selector), abi.encode() + ); + } + + function test_Set_Approvals() public { + for (uint256 i = 0; i < TOKENS_AMOUNT; i++) { vm.expectCall(tokens[i], abi.encodeCall(IERC20.approve, (vaultRelayer, type(uint256).max)), 1); } bCoWPool.finalize(); } + + function test_Log_IfRevert() public { + vm.mockCallRevert( + address(bCoWPool.call__factory()), abi.encodeWithSelector(IBCoWFactory.logBCoWPool.selector), abi.encode() + ); + + vm.expectEmit(address(bCoWPool)); + emit IBCoWFactory.COWAMMPoolCreated(address(bCoWPool)); + + bCoWPool.finalize(); + } + + function test_Call_LogBCoWPool() public { + vm.expectCall(address(bCoWPool.call__factory()), abi.encodeWithSelector(IBCoWFactory.logBCoWPool.selector), 1); + bCoWPool.finalize(); + } } /// @notice this tests both commit and commitment @@ -252,6 +278,9 @@ contract BCoWPool_Unit_IsValidSignature is BaseCoWPoolTest { for (uint256 i = 0; i < TOKENS_AMOUNT; i++) { vm.mockCall(tokens[i], abi.encodePacked(IERC20.approve.selector), abi.encode(true)); } + vm.mockCall( + address(bCoWPool.call__factory()), abi.encodeWithSelector(IBCoWFactory.logBCoWPool.selector), abi.encode() + ); bCoWPool.finalize(); } From 6c0b990219839b0ddb2cde5bb6cbdf3e30af6916 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Wed, 19 Jun 2024 16:20:52 +0200 Subject: [PATCH 28/32] fix: section in readme about AppData (#112) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 94ddceac..6a4a4a89 100644 --- a/README.md +++ b/README.md @@ -39,11 +39,11 @@ yarn test # run the tests ## Features on BCoWPool (added via inheritance to BPool) - Immutably stores CoW Protocol's `SolutionSettler` and `VaultRelayer` addresses at deployment - Immutably stores Cow Protocol's a Domain Separator at deployment (to avoid replay attacks) +- Immutably stores Cow Protocol's `GPv2Order.appData` to be allowed to swap - Gives infinite ERC20 approval to the CoW Protocol's `VaultRelayer` contract - Implements IERC1271 `isValidSignature` method to allow for validating intentions of swaps - Implements a `commit` method to avoid multiple swaps from conflicting with each other -- Allows the controller to allow only one `GPv2Order.appData` at a time - Validates the `GPv2Order` requirements before allowing the swap ## Features on BCoWFactory -- Added a `logBCoWPool` to log the finalization of BCoWPool contracts, to be called by a child pool \ No newline at end of file +- Added a `logBCoWPool` to log the finalization of BCoWPool contracts, to be called by a child pool From 4a683b5a7356b0445a3cf8a3ff30fe78b04d7c65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Wed, 19 Jun 2024 16:39:58 +0200 Subject: [PATCH 29/32] refactor: internalizing newBPool logic (#111) * refactor: internalizing newBPool logic * fix: rm unused import * fix: linter ordering in bcowFactory --- .forge-snapshots/newBFactory.snap | 2 +- .forge-snapshots/newBPool.snap | 2 +- .forge-snapshots/swapExactAmountIn.snap | 2 +- .../swapExactAmountInInverse.snap | 2 +- src/contracts/BCoWFactory.sol | 20 ++++------- src/contracts/BFactory.sol | 12 +++++-- test/manual-smock/MockBCoWFactory.sol | 23 ++++++++++--- test/smock/MockBFactory.sol | 19 +++++++++++ test/unit/BCoWFactory.t.sol | 8 +++++ test/unit/BFactory.t.sol | 33 +++++++++++++++++-- 10 files changed, 98 insertions(+), 25 deletions(-) diff --git a/.forge-snapshots/newBFactory.snap b/.forge-snapshots/newBFactory.snap index cf164054..ac9cbd4a 100644 --- a/.forge-snapshots/newBFactory.snap +++ b/.forge-snapshots/newBFactory.snap @@ -1 +1 @@ -3525470 \ No newline at end of file +3528278 \ No newline at end of file diff --git a/.forge-snapshots/newBPool.snap b/.forge-snapshots/newBPool.snap index b4fbdb7e..06165dc3 100644 --- a/.forge-snapshots/newBPool.snap +++ b/.forge-snapshots/newBPool.snap @@ -1 +1 @@ -3315224 \ No newline at end of file +3315258 \ No newline at end of file diff --git a/.forge-snapshots/swapExactAmountIn.snap b/.forge-snapshots/swapExactAmountIn.snap index de31e89b..c7145a32 100644 --- a/.forge-snapshots/swapExactAmountIn.snap +++ b/.forge-snapshots/swapExactAmountIn.snap @@ -1 +1 @@ -81475 \ No newline at end of file +81507 \ No newline at end of file diff --git a/.forge-snapshots/swapExactAmountInInverse.snap b/.forge-snapshots/swapExactAmountInInverse.snap index be80398b..9a5eb54a 100644 --- a/.forge-snapshots/swapExactAmountInInverse.snap +++ b/.forge-snapshots/swapExactAmountInInverse.snap @@ -1 +1 @@ -91134 \ No newline at end of file +91166 \ No newline at end of file diff --git a/src/contracts/BCoWFactory.sol b/src/contracts/BCoWFactory.sol index 017d8ab4..a721dc54 100644 --- a/src/contracts/BCoWFactory.sol +++ b/src/contracts/BCoWFactory.sol @@ -20,22 +20,16 @@ contract BCoWFactory is BFactory, IBCoWFactory { APP_DATA = _appData; } - /** - * @inheritdoc IBFactory - * @dev Deploys a BCoWPool instead of a regular BPool, maintains the interface - * to minimize required changes to existing tooling - */ - function newBPool() external override(BFactory, IBFactory) returns (IBPool _pool) { - IBPool bpool = new BCoWPool(SOLUTION_SETTLER, APP_DATA); - _isBPool[address(bpool)] = true; - emit LOG_NEW_POOL(msg.sender, address(bpool)); - bpool.setController(msg.sender); - return bpool; - } - /// @inheritdoc IBCoWFactory function logBCoWPool() external { if (!_isBPool[msg.sender]) revert BCoWFactory_NotValidBCoWPool(); emit COWAMMPoolCreated(msg.sender); } + + /** + * @dev Deploys a BCoWPool instead of a regular BPool. + */ + function _newBPool() internal virtual override returns (IBPool _pool) { + return new BCoWPool(SOLUTION_SETTLER, APP_DATA); + } } diff --git a/src/contracts/BFactory.sol b/src/contracts/BFactory.sol index 70da5798..289e8c8f 100644 --- a/src/contracts/BFactory.sol +++ b/src/contracts/BFactory.sol @@ -20,8 +20,8 @@ contract BFactory is IBFactory { } /// @inheritdoc IBFactory - function newBPool() external virtual returns (IBPool _pool) { - IBPool bpool = new BPool(); + function newBPool() external returns (IBPool _pool) { + IBPool bpool = _newBPool(); _isBPool[address(bpool)] = true; emit LOG_NEW_POOL(msg.sender, address(bpool)); bpool.setController(msg.sender); @@ -58,4 +58,12 @@ contract BFactory is IBFactory { function getBLabs() external view returns (address) { return _blabs; } + + /** + * @notice Deploys a new BPool. + * @dev Internal function to allow overriding in derived contracts. + */ + function _newBPool() internal virtual returns (IBPool _pool) { + return new BPool(); + } } diff --git a/test/manual-smock/MockBCoWFactory.sol b/test/manual-smock/MockBCoWFactory.sol index 2f3433b4..150da5bb 100644 --- a/test/manual-smock/MockBCoWFactory.sol +++ b/test/manual-smock/MockBCoWFactory.sol @@ -5,14 +5,29 @@ import {BCoWFactory, BCoWPool, BFactory, IBCoWFactory, IBFactory, IBPool} from ' import {Test} from 'forge-std/Test.sol'; contract MockBCoWFactory is BCoWFactory, Test { - constructor(address _solutionSettler, bytes32 _appData) BCoWFactory(_solutionSettler, _appData) {} - function set__isBPool(address _key0, bool _value) public { _isBPool[_key0] = _value; } - function mock_call_newBPool(IBPool _pool) public { - vm.mockCall(address(this), abi.encodeWithSignature('newBPool()'), abi.encode(_pool)); + constructor(address _solutionSettler, bytes32 _appData) BCoWFactory(_solutionSettler, _appData) {} + + function mock_call__newBPool(IBPool _pool) public { + vm.mockCall(address(this), abi.encodeWithSignature('_newBPool()'), abi.encode(_pool)); + } + + function _newBPool() internal override returns (IBPool _pool) { + (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 _pool) { + return _newBPool(); + } + + function expectCall__newBPool() public { + vm.expectCall(address(this), abi.encodeWithSignature('_newBPool()')); } function mock_call_logBCoWPool() public { diff --git a/test/smock/MockBFactory.sol b/test/smock/MockBFactory.sol index 6ba92879..541a4876 100644 --- a/test/smock/MockBFactory.sol +++ b/test/smock/MockBFactory.sol @@ -42,4 +42,23 @@ contract MockBFactory is BFactory, Test { function mock_call_getBLabs(address _returnParam0) public { vm.mockCall(address(this), abi.encodeWithSignature('getBLabs()'), abi.encode(_returnParam0)); } + + function mock_call__newBPool(IBPool _pool) public { + vm.mockCall(address(this), abi.encodeWithSignature('_newBPool()'), abi.encode(_pool)); + } + + function _newBPool() internal override returns (IBPool _pool) { + (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 _pool) { + return _newBPool(); + } + + function expectCall__newBPool() public { + vm.expectCall(address(this), abi.encodeWithSignature('_newBPool()')); + } } diff --git a/test/unit/BCoWFactory.t.sol b/test/unit/BCoWFactory.t.sol index 09c42965..c3ae9a51 100644 --- a/test/unit/BCoWFactory.t.sol +++ b/test/unit/BCoWFactory.t.sol @@ -21,6 +21,14 @@ abstract contract BCoWFactoryTest is Base { vm.prank(owner); return new MockBCoWFactory(solutionSettler, appData); } + + function _bPoolBytecode() internal virtual override returns (bytes memory _bytecode) { + vm.skip(true); + + // NOTE: "runtimeCode" is not available for contracts containing immutable variables. + // return type(BCoWPool).runtimeCode; + return _bytecode; + } } contract BCoWFactory_Unit_Constructor is BaseBFactory_Unit_Constructor, BCoWFactoryTest { diff --git a/test/unit/BFactory.t.sol b/test/unit/BFactory.t.sol index f2eec270..be71a541 100644 --- a/test/unit/BFactory.t.sol +++ b/test/unit/BFactory.t.sol @@ -2,19 +2,22 @@ pragma solidity 0.8.25; import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; - import {BFactory} from 'contracts/BFactory.sol'; import {BPool} from 'contracts/BPool.sol'; import {Test} from 'forge-std/Test.sol'; import {IBFactory} from 'interfaces/IBFactory.sol'; import {IBPool} from 'interfaces/IBPool.sol'; +import {MockBFactory} from 'test/smock/MockBFactory.sol'; + abstract contract Base is Test { IBFactory public bFactory; address public owner = makeAddr('owner'); function _configureBFactory() internal virtual returns (IBFactory); + function _bPoolBytecode() internal virtual returns (bytes memory); + function setUp() public virtual { bFactory = _configureBFactory(); } @@ -23,7 +26,11 @@ abstract contract Base is Test { abstract contract BFactoryTest is Base { function _configureBFactory() internal override returns (IBFactory) { vm.prank(owner); - return new BFactory(); + return new MockBFactory(); + } + + function _bPoolBytecode() internal pure virtual override returns (bytes memory) { + return type(BPool).runtimeCode; } } @@ -99,6 +106,20 @@ abstract contract BaseBFactory_Unit_NewBPool is Base { IBPool _pool = bFactory.newBPool(); assertEq(_expectedPoolAddress, address(_pool)); } + + /** + * @notice Test that the internal function is called + */ + function test_Call_NewBPool(address _bPool) public { + assumeNotForgeAddress(_bPool); + MockBFactory(address(bFactory)).mock_call__newBPool(IBPool(_bPool)); + MockBFactory(address(bFactory)).expectCall__newBPool(); + vm.mockCall(_bPool, abi.encodeWithSignature('setController(address)'), abi.encode()); + + IBPool _pool = bFactory.newBPool(); + + assertEq(_bPool, address(_pool)); + } } // solhint-disable-next-line no-empty-blocks @@ -199,3 +220,11 @@ contract BFactory_Unit_Collect is BFactoryTest { bFactory.collect(IBPool(_lpToken)); } } + +contract BFactory_Internal_NewBPool is BFactoryTest { + function test_Deploy_NewBPool() public { + IBPool _pool = MockBFactory(address(bFactory)).call__newBPool(); + + assertEq(_bPoolBytecode(), address(_pool).code); + } +} From bba79e7a103ee7bb9b098abfdd514eb81f8bc1a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Wed, 19 Jun 2024 16:46:59 +0200 Subject: [PATCH 30/32] feat: improving docs (#102) * feat: improving docs * fix: rm enableTrading from readme * fix: adding missing natspec --- README.md | 13 +++++++--- package.json | 2 +- src/contracts/BCoWConst.sol | 4 +++ src/contracts/BCoWFactory.sol | 4 +++ src/contracts/BCoWPool.sol | 3 ++- src/contracts/BConst.sol | 32 ++++++++++++++++++++--- src/contracts/BMath.sol | 4 +++ src/contracts/BNum.sol | 5 ++++ src/contracts/BToken.sol | 4 +++ src/interfaces/IBCoWFactory.sol | 16 ++++++++++++ src/interfaces/ISettlement.sol | 4 +++ yarn.lock | 45 ++++++++++++--------------------- 12 files changed, 97 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 6a4a4a89..ed3cadec 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,6 @@ Balancer is based on an N-dimensional invariant surface which is a generalization of the constant product formula described by Vitalik Buterin and proven viable by the popular Uniswap dapp. -## Documentation - -The full documentation can be found at `TODO BAL-98`. - ## Development Most users will want to consume the ABI definitions for BPool, BCoWPool, BFactory and BCoWFactory. @@ -47,3 +43,12 @@ yarn test # run the tests ## Features on BCoWFactory - Added a `logBCoWPool` to log the finalization of BCoWPool contracts, to be called by a child pool + +## Creating a Pool +- Create a new pool by calling `IBFactory.newBPool()` +- Give ERC20 allowance to the pool by calling `IERC20.approve(pool, amount)` +- Bind tokens one by one by calling `IBPool.bind(token, amount, weight)` + - The amount represents the initial balance of the token in the pool (pulled from the caller's balance) + - The weight represents the intended distribution of value between the tokens in the pool +- Modify the pool's swap fee by calling `IBPool.setSwapFee(fee)` +- Finalize the pool by calling `IBPool.finalize()` diff --git a/package.json b/package.json index 15edec87..e77b57d2 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "devDependencies": { "@commitlint/cli": "19.3.0", "@commitlint/config-conventional": "19.2.2", - "@defi-wonderland/natspec-smells": "1.1.1", + "@defi-wonderland/natspec-smells": "1.1.3", "@defi-wonderland/smock-foundry": "1.5.0", "forge-gas-snapshot": "github:marktoda/forge-gas-snapshot#9161f7c", "forge-std": "github:foundry-rs/forge-std#5475f85", diff --git a/src/contracts/BCoWConst.sol b/src/contracts/BCoWConst.sol index 1464118a..18c03de7 100644 --- a/src/contracts/BCoWConst.sol +++ b/src/contracts/BCoWConst.sol @@ -1,6 +1,10 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.25; +/** + * @title BCoWConst + * @notice Constants used in the scope of the BCoWPool contract. + */ contract BCoWConst { /** * @notice The value representing the absence of a commitment. diff --git a/src/contracts/BCoWFactory.sol b/src/contracts/BCoWFactory.sol index a721dc54..d46cf0aa 100644 --- a/src/contracts/BCoWFactory.sol +++ b/src/contracts/BCoWFactory.sol @@ -10,9 +10,13 @@ import {IBPool} from 'interfaces/IBPool.sol'; /** * @title BCoWFactory * @notice Creates new BCoWPools, logging their addresses and acting as a registry of pools. + * @dev Inherits BFactory contract functionalities, but deploys BCoWPools instead of BPool. */ contract BCoWFactory is BFactory, IBCoWFactory { + /// @inheritdoc IBCoWFactory address public immutable SOLUTION_SETTLER; + + /// @inheritdoc IBCoWFactory bytes32 public immutable APP_DATA; constructor(address _solutionSettler, bytes32 _appData) BFactory() { diff --git a/src/contracts/BCoWPool.sol b/src/contracts/BCoWPool.sol index 42e6809c..d384e389 100644 --- a/src/contracts/BCoWPool.sol +++ b/src/contracts/BCoWPool.sol @@ -28,7 +28,8 @@ import {ISettlement} from 'interfaces/ISettlement.sol'; /** * @title BCoWPool - * @notice Inherits BPool contract and can trade on CoWSwap Protocol. + * @notice Pool contract that holds tokens, allows to swap, add and remove liquidity. + * @dev Inherits BPool contract functionalities, and can trade on CoW Swap Protocol. */ contract BCoWPool is IERC1271, IBCoWPool, BPool, BCoWConst { using GPv2Order for GPv2Order.Data; diff --git a/src/contracts/BConst.sol b/src/contracts/BConst.sol index bb893f25..c6f8cd30 100644 --- a/src/contracts/BConst.sol +++ b/src/contracts/BConst.sol @@ -1,36 +1,60 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.25; +/** + * @title BConst + * @notice Constants used in the scope of the BPool contract. + */ contract BConst { + /// @notice The unit of precision used in the calculations. uint256 public constant BONE = 10 ** 18; + /// @notice The minimum number of bound tokens in a pool. uint256 public constant MIN_BOUND_TOKENS = 2; + /// @notice The maximum number of bound tokens in a pool. uint256 public constant MAX_BOUND_TOKENS = 8; + /// @notice The minimum swap fee that can be set. uint256 public constant MIN_FEE = BONE / 10 ** 6; + /// @notice The maximum swap fee that can be set. uint256 public constant MAX_FEE = BONE / 10; + /// @notice The immutable exit fee percentage uint256 public constant EXIT_FEE = 0; + /// @notice The minimum weight that a token can have. 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. uint256 public constant MAX_TOTAL_WEIGHT = BONE * 50; + /// @notice The minimum balance that a token must have. uint256 public constant MIN_BALANCE = BONE / 10 ** 12; + /// @notice The initial total supply of the pool tokens (minted to the pool creator). uint256 public constant INIT_POOL_SUPPLY = BONE * 100; + /// @notice The minimum base value for the bpow calculation. uint256 public constant MIN_BPOW_BASE = 1 wei; + /// @notice The maximum base value for the bpow calculation. uint256 public constant MAX_BPOW_BASE = (2 * BONE) - 1 wei; + /// @notice The precision of the bpow calculation. uint256 public constant BPOW_PRECISION = BONE / 10 ** 10; + /// @notice The maximum ratio of input tokens vs the current pool balance. uint256 public constant MAX_IN_RATIO = BONE / 2; + /// @notice The maximum ratio of output tokens vs the current pool balance. uint256 public constant MAX_OUT_RATIO = (BONE / 3) + 1 wei; - // Using an arbitrary storage slot to prevent possible future - // _transient_ variables defined by solidity from overriding it, if they were - // to start on slot zero as regular storage variables do. Value is: - // uint256(keccak256('BPool.transientStorageLock')) - 1; + /** + * @notice The storage slot used to write transient data. + * @dev Using an arbitrary storage slot to prevent possible future + * transient variables defined by solidity from overriding it. + * @dev Value is: uint256(keccak256('BPool.transientStorageLock')) - 1; + */ uint256 internal constant _MUTEX_TRANSIENT_STORAGE_SLOT = 0x3f8f4c536ce1b925b469af1b09a44da237dab5bbc584585648c12be1ca25a8c4; + /// @notice The value representing an unlocked state of the mutex. bytes32 internal constant _MUTEX_FREE = bytes32(uint256(0)); + /// @notice The value representing a locked state of the mutex. bytes32 internal constant _MUTEX_TAKEN = bytes32(uint256(1)); } diff --git a/src/contracts/BMath.sol b/src/contracts/BMath.sol index 19e2424e..cb162019 100644 --- a/src/contracts/BMath.sol +++ b/src/contracts/BMath.sol @@ -4,6 +4,10 @@ pragma solidity 0.8.25; import {BConst} from './BConst.sol'; import {BNum} from './BNum.sol'; +/** + * @title BMath + * @notice Includes functions for calculating the BPool related math. + */ contract BMath is BConst, BNum { /** * @notice Calculate the spot price of a token in terms of another one diff --git a/src/contracts/BNum.sol b/src/contracts/BNum.sol index c75da935..52429928 100644 --- a/src/contracts/BNum.sol +++ b/src/contracts/BNum.sol @@ -3,6 +3,11 @@ pragma solidity 0.8.25; import {BConst} from './BConst.sol'; +/** + * @title BNum + * @notice Includes functions for arithmetic operations with fixed-point numbers. + * @dev The arithmetic operations are implemented with a precision of BONE. + */ // solhint-disable private-vars-leading-underscore // solhint-disable named-return-values contract BNum is BConst { diff --git a/src/contracts/BToken.sol b/src/contracts/BToken.sol index 0aaefc48..55c0e565 100644 --- a/src/contracts/BToken.sol +++ b/src/contracts/BToken.sol @@ -3,6 +3,10 @@ pragma solidity 0.8.25; import {ERC20} from '@openzeppelin/contracts/token/ERC20/ERC20.sol'; +/** + * @title BToken + * @notice Balancer Pool Token base contract, providing ERC20 functionality. + */ contract BToken is ERC20 { constructor() ERC20('Balancer Pool Token', 'BPT') {} diff --git a/src/interfaces/IBCoWFactory.sol b/src/interfaces/IBCoWFactory.sol index 58e9b304..b7ef368f 100644 --- a/src/interfaces/IBCoWFactory.sol +++ b/src/interfaces/IBCoWFactory.sol @@ -19,4 +19,20 @@ interface IBCoWFactory is IBFactory { * @notice Emits the COWAMMPoolCreated event if the caller is a bCoWPool, to be indexed by off-chain agents */ function logBCoWPool() external; + + /** + * @notice The address of the CoW Protocol settlement contract. It is the + * only address that can set commitments. + * @return _solutionSettler The address of the solution settler. + */ + // solhint-disable-next-line style-guide-casing + function SOLUTION_SETTLER() external view returns (address _solutionSettler); + + /** + * @notice The identifier describing which `GPv2Order.AppData` currently + * apply to this AMM. + * @return _appData The 32 bytes identifier of the allowed GPv2Order AppData. + */ + // solhint-disable-next-line style-guide-casing + function APP_DATA() external view returns (bytes32 _appData); } diff --git a/src/interfaces/ISettlement.sol b/src/interfaces/ISettlement.sol index 8bf85dc8..086cfae2 100644 --- a/src/interfaces/ISettlement.sol +++ b/src/interfaces/ISettlement.sol @@ -5,6 +5,10 @@ import {IERC20} from '@cowprotocol/interfaces/IERC20.sol'; import {GPv2Interaction} from '@cowprotocol/libraries/GPv2Interaction.sol'; import {GPv2Trade} from '@cowprotocol/libraries/GPv2Trade.sol'; +/** + * @title ISettlement + * @notice External interface of CoW Protocol's SolutionSettler contract. + */ interface ISettlement { /** * @notice Settles a batch of trades. diff --git a/yarn.lock b/yarn.lock index 73799a41..27b5429f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -185,13 +185,13 @@ version "1.6.0" resolved "https://codeload.github.com/cowprotocol/contracts/tar.gz/a10f40788af29467e87de3dbf2196662b0a6b500" -"@defi-wonderland/natspec-smells@1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@defi-wonderland/natspec-smells/-/natspec-smells-1.1.1.tgz#1eed85765ebe4799f8861247308d6365dbf6dbaa" - integrity sha512-vtOKN4j32Rxl8c1txXxs/iDlkPRmrL/UzUYWOzowe1HYH8ioPMRrsKAYVriDBLsvSi2bY06Sl3cN9SQJSbVzsA== +"@defi-wonderland/natspec-smells@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@defi-wonderland/natspec-smells/-/natspec-smells-1.1.3.tgz#6d4c7e289b24264856170fec33e0cae0c844bd32" + integrity sha512-QfZ7uD2bseU/QwQgY8uFrGSD5+a4y1S+GqCNePfmjgRGnEaV7zSFL5FO+zd1GUMtWQNGLgSuRknJMu6DEp3njw== dependencies: fast-glob "3.3.2" - solc-typed-ast "18.1.2" + solc-typed-ast "18.1.6" yargs "17.7.2" "@defi-wonderland/smock-foundry@1.5.0": @@ -394,7 +394,7 @@ available-typed-arrays@^1.0.7: dependencies: possible-typed-array-names "^1.0.0" -axios@^1.6.7, axios@^1.6.8: +axios@^1.6.8: version "1.6.8" resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.8.tgz#66d294951f5d988a00e87a0ffb955316a619ea66" integrity sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ== @@ -1699,26 +1699,26 @@ slice-ansi@^7.0.0: ansi-styles "^6.2.1" is-fullwidth-code-point "^5.0.0" -solc-typed-ast@18.1.2: - version "18.1.2" - resolved "https://registry.yarnpkg.com/solc-typed-ast/-/solc-typed-ast-18.1.2.tgz#bc958fe3aead765cf6c2e06ce3d53c61fd06e70c" - integrity sha512-57IKzvXHcyjqdjHEdX7NQuWkPALlH8V4eJ6UUehWrzgHDVzKVOCFplwgLDRnOZ8kDMO8+Ms8sQhfrivFK+v5FA== +solc-typed-ast@18.1.3: + version "18.1.3" + resolved "https://registry.yarnpkg.com/solc-typed-ast/-/solc-typed-ast-18.1.3.tgz#243cc0c5a4f701445ac10341224bf8c18a6ed252" + integrity sha512-11iBtavJJtkzrmQdlAiZ7sz3C3WOQ8MaN7+r4b9C6B/3ORqg4oTUW5/ANyGyus5ppXDXzPyT90BYCfP73df3HA== dependencies: - axios "^1.6.7" + axios "^1.6.8" commander "^12.0.0" decimal.js "^10.4.3" findup-sync "^5.0.0" fs-extra "^11.2.0" jsel "^1.1.6" semver "^7.6.0" - solc "0.8.24" + solc "0.8.25" src-location "^1.1.0" web3-eth-abi "^4.2.0" -solc-typed-ast@18.1.3: - version "18.1.3" - resolved "https://registry.yarnpkg.com/solc-typed-ast/-/solc-typed-ast-18.1.3.tgz#243cc0c5a4f701445ac10341224bf8c18a6ed252" - integrity sha512-11iBtavJJtkzrmQdlAiZ7sz3C3WOQ8MaN7+r4b9C6B/3ORqg4oTUW5/ANyGyus5ppXDXzPyT90BYCfP73df3HA== +solc-typed-ast@18.1.6: + version "18.1.6" + resolved "https://registry.yarnpkg.com/solc-typed-ast/-/solc-typed-ast-18.1.6.tgz#997e986583f0fdddb3ddb1960c33d5c63b3f729a" + integrity sha512-nBk24fdju+P2xsy32tG6HLqkXI+Tn+W84Fqm5+XD1Xby2/8YLlsMgI3ADoRPhhO7DeWjq/kflm//dGNkEb3ILA== dependencies: axios "^1.6.8" commander "^12.0.0" @@ -1731,19 +1731,6 @@ solc-typed-ast@18.1.3: src-location "^1.1.0" web3-eth-abi "^4.2.0" -solc@0.8.24: - version "0.8.24" - resolved "https://registry.yarnpkg.com/solc/-/solc-0.8.24.tgz#6e5693d28208d00a20ff2bdabc1dec85a5329bbb" - integrity sha512-G5yUqjTUPc8Np74sCFwfsevhBPlUifUOfhYrgyu6CmYlC6feSw0YS6eZW47XDT23k3JYdKx5nJ+Q7whCEmNcoA== - dependencies: - command-exists "^1.2.8" - commander "^8.1.0" - follow-redirects "^1.12.1" - js-sha3 "0.8.0" - memorystream "^0.3.1" - semver "^5.5.0" - tmp "0.0.33" - solc@0.8.25: version "0.8.25" resolved "https://registry.yarnpkg.com/solc/-/solc-0.8.25.tgz#393f3101617388fb4ba2a58c5b03ab02678e375c" From 4f9bc437e0f6a2d598c01f32bf82b43e47961127 Mon Sep 17 00:00:00 2001 From: teddy Date: Wed, 19 Jun 2024 14:11:36 -0300 Subject: [PATCH 31/32] feat: couple lock and commit (#110) * chore: update forge snapshots before implementation * feat: use same slot for CoW commitment and reentrancy locks * chore: update forge snapshots after implementation * refactor: use internal functions instead of bare assembly to read transient storage * feat: revert on commit if theres an active commitment or reentrancy lock * docs: document possible commitment inconsistency * chore: setup automaticaly generated mocked methods * test: ensure isValidSignature uses reentrancy locks as intended * test: ensure existing bpool methods set reentrancy lock * docs: add contents of this PR to README * fix: fixes from botched merge * refactor: manually mocked bpool is no longer necessary * fix: remove redundant asserts & assumes --- .forge-snapshots/settlementCoWSwap.snap | 2 +- .../settlementCoWSwapInverse.snap | 2 +- README.md | 5 +- package.json | 2 +- src/contracts/BCoWConst.sol | 12 - src/contracts/BCoWPool.sol | 9 +- src/contracts/BPool.sol | 4 +- src/interfaces/IBCoWPool.sol | 11 + test/manual-smock/MockBCoWPool.sol | 40 +- test/manual-smock/MockBPool.sol | 344 ------------------ test/smock/MockBPool.sol | 38 ++ test/unit/BCoWPool.t.sol | 26 +- test/unit/BPool.t.sol | 78 +++- 13 files changed, 192 insertions(+), 381 deletions(-) delete mode 100644 test/manual-smock/MockBPool.sol diff --git a/.forge-snapshots/settlementCoWSwap.snap b/.forge-snapshots/settlementCoWSwap.snap index 5cd515d8..d095a34d 100644 --- a/.forge-snapshots/settlementCoWSwap.snap +++ b/.forge-snapshots/settlementCoWSwap.snap @@ -1 +1 @@ -186615 \ No newline at end of file +186763 \ No newline at end of file diff --git a/.forge-snapshots/settlementCoWSwapInverse.snap b/.forge-snapshots/settlementCoWSwapInverse.snap index a588d5cf..a1231ec6 100644 --- a/.forge-snapshots/settlementCoWSwapInverse.snap +++ b/.forge-snapshots/settlementCoWSwapInverse.snap @@ -1 +1 @@ -196455 \ No newline at end of file +196603 \ No newline at end of file diff --git a/README.md b/README.md index ed3cadec..b98fe3c8 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ yarn test # run the tests - Deprecated `BColor` and `BBronze` (unused contracts) - Deprecated `Migrations` contract (not needed) - Added an `_afterFinalize` hook (to be called at the end of the finalize routine) +- Implemented reentrancy locks using transient storage. ## Features on BCoWPool (added via inheritance to BPool) - Immutably stores CoW Protocol's `SolutionSettler` and `VaultRelayer` addresses at deployment @@ -38,7 +39,9 @@ yarn test # run the tests - Immutably stores Cow Protocol's `GPv2Order.appData` to be allowed to swap - Gives infinite ERC20 approval to the CoW Protocol's `VaultRelayer` contract - Implements IERC1271 `isValidSignature` method to allow for validating intentions of swaps -- Implements a `commit` method to avoid multiple swaps from conflicting with each other +- Implements a `commit` method to avoid multiple swaps from conflicting with each other. + - This is stored in the same transient storage slot as reentrancy locks in order to prevent calls to swap/join functions within a settlement execution or vice versa. + - It's an error to override a commitment since that could be used to clear reentrancy locks. Commitments can only be cleared by ending a transaction. - Validates the `GPv2Order` requirements before allowing the swap ## Features on BCoWFactory diff --git a/package.json b/package.json index e77b57d2..26f0309a 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "prepare": "husky install", "smock": "smock-foundry --contracts src/contracts", "test": "forge test -vvv", - "test:integration": "forge test --match-contract Integration -vvv", + "test:integration": "forge test --match-contract Integration -vvv --isolate", "test:local": "FOUNDRY_FUZZ_RUNS=100 forge test -vvv", "test:unit": "forge test --match-contract Unit -vvv", "test:unit:deep": "FOUNDRY_FUZZ_RUNS=5000 yarn test:unit" diff --git a/src/contracts/BCoWConst.sol b/src/contracts/BCoWConst.sol index 18c03de7..79cff191 100644 --- a/src/contracts/BCoWConst.sol +++ b/src/contracts/BCoWConst.sol @@ -18,16 +18,4 @@ contract BCoWConst { * @return _maxOrderDuration The maximum order duration. */ uint32 public constant MAX_ORDER_DURATION = 5 * 60; - - /** - * @notice The transient storage slot specified in this variable stores the - * value of the order commitment, that is, the only order hash that can be - * validated by calling `isValidSignature`. - * The hash corresponding to the constant `EMPTY_COMMITMENT` has special - * semantics, discussed in the related documentation. - * @dev This value is: - * uint256(keccak256("CoWAMM.ConstantProduct.commitment")) - 1 - * @return _commitmentSlot The slot where the commitment is stored. - */ - uint256 public constant COMMITMENT_SLOT = 0x6c3c90245457060f6517787b2c4b8cf500ca889d2304af02043bd5b513e3b593; } diff --git a/src/contracts/BCoWPool.sol b/src/contracts/BCoWPool.sol index d384e389..9fc3d09a 100644 --- a/src/contracts/BCoWPool.sol +++ b/src/contracts/BCoWPool.sol @@ -58,9 +58,10 @@ contract BCoWPool is IERC1271, IBCoWPool, BPool, BCoWConst { if (msg.sender != address(SOLUTION_SETTLER)) { revert CommitOutsideOfSettlement(); } - assembly ("memory-safe") { - tstore(COMMITMENT_SLOT, orderHash) + if (_getLock() != _MUTEX_FREE) { + revert BCoWPool_CommitmentAlreadySet(); } + _setLock(orderHash); } /** @@ -92,9 +93,7 @@ contract BCoWPool is IERC1271, IBCoWPool, BPool, BCoWConst { /// @inheritdoc IBCoWPool function commitment() public view returns (bytes32 value) { - assembly ("memory-safe") { - value := tload(COMMITMENT_SLOT) - } + value = _getLock(); } /// @inheritdoc IBCoWPool diff --git a/src/contracts/BPool.sol b/src/contracts/BPool.sol index 4aed30ea..6ae83c92 100644 --- a/src/contracts/BPool.sol +++ b/src/contracts/BPool.sol @@ -607,7 +607,7 @@ contract BPool is BToken, BMath, IBPool { * @dev Should be set to _MUTEX_FREE after a call, any other value will * be interpreted as locked */ - function _setLock(bytes32 _value) internal { + function _setLock(bytes32 _value) internal virtual { assembly ("memory-safe") { tstore(_MUTEX_TRANSIENT_STORAGE_SLOT, _value) } @@ -686,7 +686,7 @@ contract BPool is BToken, BMath, IBPool { * @dev Should only be compared against _MUTEX_FREE for the purposes of * allowing calls */ - function _getLock() internal view returns (bytes32 _value) { + function _getLock() internal view virtual returns (bytes32 _value) { assembly ("memory-safe") { _value := tload(_MUTEX_TRANSIENT_STORAGE_SLOT) } diff --git a/src/interfaces/IBCoWPool.sol b/src/interfaces/IBCoWPool.sol index 3777e797..cbd7f34d 100644 --- a/src/interfaces/IBCoWPool.sol +++ b/src/interfaces/IBCoWPool.sol @@ -61,6 +61,14 @@ interface IBCoWPool is IERC1271, IBPool { */ error BCoWPool_ReceiverIsNotBCoWPool(); + /** + * @notice Thrown when trying to set a commitment but there's one already in effect + * @dev Commitments can only be cleared naturally by finishing a transaction + * @dev Since commitments and reentrancy locks share a storage slot, a + * malicious settler could set a zero commitment to clear a reentrancy lock + */ + error BCoWPool_CommitmentAlreadySet(); + /** * @notice Restricts a specific AMM to being able to trade only the order * with the specified hash. @@ -106,6 +114,9 @@ interface IBCoWPool is IERC1271, IBPool { * `commit` function. If no commitment has been set, then the value will be * `EMPTY_COMMITMENT`. * @return _commitment The commitment hash. + * @dev since commitments share a transient storage slot with reentrancy + * locks, this will return an invalid value while there's a reentrancy lock + * active */ function commitment() external view returns (bytes32 _commitment); diff --git a/test/manual-smock/MockBCoWPool.sol b/test/manual-smock/MockBCoWPool.sol index 0963f30a..095842e3 100644 --- a/test/manual-smock/MockBCoWPool.sol +++ b/test/manual-smock/MockBCoWPool.sol @@ -9,12 +9,6 @@ import {Test} from 'forge-std/Test.sol'; contract MockBCoWPool is BCoWPool, Test { /// MockBCoWPool mock methods - function set_commitment(bytes32 _commitment) public { - assembly ("memory-safe") { - tstore(COMMITMENT_SLOT, _commitment) - } - } - constructor(address _cowSolutionSettler, bytes32 _appData) BCoWPool(_cowSolutionSettler, _appData) {} function mock_call_commit(bytes32 orderHash) public { @@ -42,6 +36,40 @@ contract MockBCoWPool is BCoWPool, Test { } /// BPool Mocked methods + function _setLock(bytes32 _value) internal override { + (bool _success, bytes memory _data) = address(this).call(abi.encodeWithSignature('_setLock(bytes32)', _value)); + + if (_success) return abi.decode(_data, ()); + else return super._setLock(_value); + } + + function call__setLock(bytes32 _value) public { + return _setLock(_value); + } + + function expectCall__setLock(bytes32 _value) public { + vm.expectCall(address(this), abi.encodeWithSignature('_setLock(bytes32)', _value)); + } + + function mock_call__getLock(bytes32 _value) public { + vm.mockCall(address(this), abi.encodeWithSignature('_getLock()'), abi.encode(_value)); + } + + function _getLock() internal view override returns (bytes32 _value) { + (bool _success, bytes memory _data) = address(this).staticcall(abi.encodeWithSignature('_getLock()')); + + if (_success) return abi.decode(_data, (bytes32)); + else return super._getLock(); + } + + function call__getLock() public returns (bytes32 _value) { + return _getLock(); + } + + function expectCall__getLock() public { + vm.expectCall(address(this), abi.encodeWithSignature('_getLock()')); + } + function set__factory(address __factory) public { _factory = __factory; } diff --git a/test/manual-smock/MockBPool.sol b/test/manual-smock/MockBPool.sol deleted file mode 100644 index 6f46ced5..00000000 --- a/test/manual-smock/MockBPool.sol +++ /dev/null @@ -1,344 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.0; - -import {BMath, BPool, BToken, IBPool, IERC20} from '../../src/contracts/BPool.sol'; -import {Test} from 'forge-std/Test.sol'; - -contract MockBPool is BPool, Test { - function call__setLock(bytes32 _value) public { - assembly ("memory-safe") { - tstore(_MUTEX_TRANSIENT_STORAGE_SLOT, _value) - } - } - - function call__getLock() public view returns (bytes32 _value) { - assembly ("memory-safe") { - _value := tload(_MUTEX_TRANSIENT_STORAGE_SLOT) - } - } - - function set__factory(address __factory) public { - _factory = __factory; - } - - function call__factory() public view returns (address) { - return _factory; - } - - function set__controller(address __controller) public { - _controller = __controller; - } - - function call__controller() public view returns (address) { - return _controller; - } - - function set__swapFee(uint256 __swapFee) public { - _swapFee = __swapFee; - } - - function call__swapFee() public view returns (uint256) { - return _swapFee; - } - - function set__finalized(bool __finalized) public { - _finalized = __finalized; - } - - function call__finalized() public view returns (bool) { - return _finalized; - } - - function set__tokens(address[] memory __tokens) public { - _tokens = __tokens; - } - - function call__tokens() public view returns (address[] memory) { - return _tokens; - } - - function set__records(address _key0, IBPool.Record memory _value) public { - _records[_key0] = _value; - } - - function call__records(address _key0) public view returns (IBPool.Record memory) { - return _records[_key0]; - } - - function set__totalWeight(uint256 __totalWeight) public { - _totalWeight = __totalWeight; - } - - function call__totalWeight() public view returns (uint256) { - return _totalWeight; - } - - constructor() BPool() {} - - function mock_call_setSwapFee(uint256 swapFee) public { - vm.mockCall(address(this), abi.encodeWithSignature('setSwapFee(uint256)', swapFee), abi.encode()); - } - - function mock_call_setController(address manager) public { - vm.mockCall(address(this), abi.encodeWithSignature('setController(address)', manager), abi.encode()); - } - - function mock_call_finalize() public { - vm.mockCall(address(this), abi.encodeWithSignature('finalize()'), abi.encode()); - } - - function mock_call_bind(address token, uint256 balance, uint256 denorm) public { - vm.mockCall( - address(this), abi.encodeWithSignature('bind(address,uint256,uint256)', token, balance, denorm), abi.encode() - ); - } - - function mock_call_unbind(address token) public { - vm.mockCall(address(this), abi.encodeWithSignature('unbind(address)', token), abi.encode()); - } - - function mock_call_joinPool(uint256 poolAmountOut, uint256[] calldata maxAmountsIn) public { - vm.mockCall( - address(this), abi.encodeWithSignature('joinPool(uint256,uint256[])', poolAmountOut, maxAmountsIn), abi.encode() - ); - } - - function mock_call_exitPool(uint256 poolAmountIn, uint256[] calldata minAmountsOut) public { - vm.mockCall( - address(this), abi.encodeWithSignature('exitPool(uint256,uint256[])', poolAmountIn, minAmountsOut), abi.encode() - ); - } - - function mock_call_swapExactAmountIn( - address tokenIn, - uint256 tokenAmountIn, - address tokenOut, - uint256 minAmountOut, - uint256 maxPrice, - uint256 tokenAmountOut, - uint256 spotPriceAfter - ) public { - vm.mockCall( - address(this), - abi.encodeWithSignature( - 'swapExactAmountIn(address,uint256,address,uint256,uint256)', - tokenIn, - tokenAmountIn, - tokenOut, - minAmountOut, - maxPrice - ), - abi.encode(tokenAmountOut, spotPriceAfter) - ); - } - - function mock_call_swapExactAmountOut( - address tokenIn, - uint256 maxAmountIn, - address tokenOut, - uint256 tokenAmountOut, - uint256 maxPrice, - uint256 tokenAmountIn, - uint256 spotPriceAfter - ) public { - vm.mockCall( - address(this), - abi.encodeWithSignature( - 'swapExactAmountOut(address,uint256,address,uint256,uint256)', - tokenIn, - maxAmountIn, - tokenOut, - tokenAmountOut, - maxPrice - ), - abi.encode(tokenAmountIn, spotPriceAfter) - ); - } - - function mock_call_joinswapExternAmountIn( - address tokenIn, - uint256 tokenAmountIn, - uint256 minPoolAmountOut, - uint256 poolAmountOut - ) public { - vm.mockCall( - address(this), - abi.encodeWithSignature( - 'joinswapExternAmountIn(address,uint256,uint256)', tokenIn, tokenAmountIn, minPoolAmountOut - ), - abi.encode(poolAmountOut) - ); - } - - function mock_call_joinswapPoolAmountOut( - address tokenIn, - uint256 poolAmountOut, - uint256 maxAmountIn, - uint256 tokenAmountIn - ) public { - vm.mockCall( - address(this), - abi.encodeWithSignature('joinswapPoolAmountOut(address,uint256,uint256)', tokenIn, poolAmountOut, maxAmountIn), - abi.encode(tokenAmountIn) - ); - } - - function mock_call_exitswapPoolAmountIn( - address tokenOut, - uint256 poolAmountIn, - uint256 minAmountOut, - uint256 tokenAmountOut - ) public { - vm.mockCall( - address(this), - abi.encodeWithSignature('exitswapPoolAmountIn(address,uint256,uint256)', tokenOut, poolAmountIn, minAmountOut), - abi.encode(tokenAmountOut) - ); - } - - function mock_call_exitswapExternAmountOut( - address tokenOut, - uint256 tokenAmountOut, - uint256 maxPoolAmountIn, - uint256 poolAmountIn - ) public { - vm.mockCall( - address(this), - abi.encodeWithSignature( - 'exitswapExternAmountOut(address,uint256,uint256)', tokenOut, tokenAmountOut, maxPoolAmountIn - ), - abi.encode(poolAmountIn) - ); - } - - function mock_call_getSpotPrice(address tokenIn, address tokenOut, uint256 spotPrice) public { - vm.mockCall( - address(this), abi.encodeWithSignature('getSpotPrice(address,address)', tokenIn, tokenOut), abi.encode(spotPrice) - ); - } - - function mock_call_getSpotPriceSansFee(address tokenIn, address tokenOut, uint256 spotPrice) public { - vm.mockCall( - address(this), - abi.encodeWithSignature('getSpotPriceSansFee(address,address)', tokenIn, tokenOut), - abi.encode(spotPrice) - ); - } - - function mock_call_isFinalized(bool _returnParam0) public { - vm.mockCall(address(this), abi.encodeWithSignature('isFinalized()'), abi.encode(_returnParam0)); - } - - function mock_call_isBound(address t, bool _returnParam0) public { - vm.mockCall(address(this), abi.encodeWithSignature('isBound(address)', t), abi.encode(_returnParam0)); - } - - function mock_call_getNumTokens(uint256 _returnParam0) public { - vm.mockCall(address(this), abi.encodeWithSignature('getNumTokens()'), abi.encode(_returnParam0)); - } - - function mock_call_getCurrentTokens(address[] memory tokens) public { - vm.mockCall(address(this), abi.encodeWithSignature('getCurrentTokens()'), abi.encode(tokens)); - } - - function mock_call_getFinalTokens(address[] memory tokens) public { - vm.mockCall(address(this), abi.encodeWithSignature('getFinalTokens()'), abi.encode(tokens)); - } - - function mock_call_getDenormalizedWeight(address token, uint256 _returnParam0) public { - vm.mockCall( - address(this), abi.encodeWithSignature('getDenormalizedWeight(address)', token), abi.encode(_returnParam0) - ); - } - - function mock_call_getTotalDenormalizedWeight(uint256 _returnParam0) public { - vm.mockCall(address(this), abi.encodeWithSignature('getTotalDenormalizedWeight()'), abi.encode(_returnParam0)); - } - - function mock_call_getNormalizedWeight(address token, uint256 _returnParam0) public { - vm.mockCall( - address(this), abi.encodeWithSignature('getNormalizedWeight(address)', token), abi.encode(_returnParam0) - ); - } - - function mock_call_getBalance(address token, uint256 _returnParam0) public { - vm.mockCall(address(this), abi.encodeWithSignature('getBalance(address)', token), abi.encode(_returnParam0)); - } - - function mock_call_getSwapFee(uint256 _returnParam0) public { - vm.mockCall(address(this), abi.encodeWithSignature('getSwapFee()'), abi.encode(_returnParam0)); - } - - function mock_call_getController(address _returnParam0) public { - vm.mockCall(address(this), abi.encodeWithSignature('getController()'), abi.encode(_returnParam0)); - } - - function mock_call__pullUnderlying(address erc20, address from, uint256 amount) public { - vm.mockCall( - address(this), - abi.encodeWithSignature('_pullUnderlying(address,address,uint256)', erc20, from, amount), - abi.encode() - ); - } - - function _pullUnderlying(address erc20, address from, uint256 amount) internal override { - (bool _success, bytes memory _data) = - address(this).call(abi.encodeWithSignature('_pullUnderlying(address,address,uint256)', erc20, from, amount)); - - if (_success) return abi.decode(_data, ()); - else return super._pullUnderlying(erc20, from, amount); - } - - function call__pullUnderlying(address erc20, address from, uint256 amount) public { - return _pullUnderlying(erc20, from, amount); - } - - function expectCall__pullUnderlying(address erc20, address from, uint256 amount) public { - vm.expectCall( - address(this), abi.encodeWithSignature('_pullUnderlying(address,address,uint256)', erc20, from, amount) - ); - } - - function mock_call__pushUnderlying(address erc20, address to, uint256 amount) public { - vm.mockCall( - address(this), - abi.encodeWithSignature('_pushUnderlying(address,address,uint256)', erc20, to, amount), - abi.encode() - ); - } - - function _pushUnderlying(address erc20, address to, uint256 amount) internal override { - (bool _success, bytes memory _data) = - address(this).call(abi.encodeWithSignature('_pushUnderlying(address,address,uint256)', erc20, to, amount)); - - if (_success) return abi.decode(_data, ()); - else return super._pushUnderlying(erc20, to, amount); - } - - function call__pushUnderlying(address erc20, address to, uint256 amount) public { - return _pushUnderlying(erc20, to, amount); - } - - function expectCall__pushUnderlying(address erc20, address to, uint256 amount) public { - vm.expectCall(address(this), abi.encodeWithSignature('_pushUnderlying(address,address,uint256)', erc20, to, amount)); - } - - function mock_call__afterFinalize() public { - vm.mockCall(address(this), abi.encodeWithSignature('_afterFinalize()'), abi.encode()); - } - - function _afterFinalize() internal override { - (bool _success, bytes memory _data) = address(this).call(abi.encodeWithSignature('_afterFinalize()')); - - if (_success) return abi.decode(_data, ()); - else return super._afterFinalize(); - } - - function call__afterFinalize() public { - return _afterFinalize(); - } - - function expectCall__afterFinalize() public { - vm.expectCall(address(this), abi.encodeWithSignature('_afterFinalize()')); - } -} diff --git a/test/smock/MockBPool.sol b/test/smock/MockBPool.sol index 474683b3..f2f73396 100644 --- a/test/smock/MockBPool.sol +++ b/test/smock/MockBPool.sol @@ -261,6 +261,25 @@ contract MockBPool is BPool, Test { vm.mockCall(address(this), abi.encodeWithSignature('getController()'), abi.encode(_returnParam0)); } + function mock_call__setLock(bytes32 _value) public { + vm.mockCall(address(this), abi.encodeWithSignature('_setLock(bytes32)', _value), abi.encode()); + } + + function _setLock(bytes32 _value) internal override { + (bool _success, bytes memory _data) = address(this).call(abi.encodeWithSignature('_setLock(bytes32)', _value)); + + if (_success) return abi.decode(_data, ()); + else return super._setLock(_value); + } + + function call__setLock(bytes32 _value) public { + return _setLock(_value); + } + + function expectCall__setLock(bytes32 _value) public { + vm.expectCall(address(this), abi.encodeWithSignature('_setLock(bytes32)', _value)); + } + function mock_call__pullUnderlying(address erc20, address from, uint256 amount) public { vm.mockCall( address(this), @@ -329,4 +348,23 @@ contract MockBPool is BPool, Test { function expectCall__afterFinalize() public { vm.expectCall(address(this), abi.encodeWithSignature('_afterFinalize()')); } + + function mock_call__getLock(bytes32 _value) public { + vm.mockCall(address(this), abi.encodeWithSignature('_getLock()'), abi.encode(_value)); + } + + function _getLock() internal view override returns (bytes32 _value) { + (bool _success, bytes memory _data) = address(this).staticcall(abi.encodeWithSignature('_getLock()')); + + if (_success) return abi.decode(_data, (bytes32)); + else return super._getLock(); + } + + function call__getLock() public returns (bytes32 _value) { + return _getLock(); + } + + function expectCall__getLock() public { + vm.expectCall(address(this), abi.encodeWithSignature('_getLock()')); + } } diff --git a/test/unit/BCoWPool.t.sol b/test/unit/BCoWPool.t.sol index 57114f5b..972fe11d 100644 --- a/test/unit/BCoWPool.t.sol +++ b/test/unit/BCoWPool.t.sol @@ -11,7 +11,7 @@ import {IBCoWPool} from 'interfaces/IBCoWPool.sol'; import {IBPool} from 'interfaces/IBPool.sol'; import {ISettlement} from 'interfaces/ISettlement.sol'; import {MockBCoWPool} from 'test/manual-smock/MockBCoWPool.sol'; -import {MockBPool} from 'test/manual-smock/MockBPool.sol'; +import {MockBPool} from 'test/smock/MockBPool.sol'; abstract contract BaseCoWPoolTest is BasePoolTest, BCoWConst { address public cowSolutionSettler = makeAddr('cowSolutionSettler'); @@ -124,11 +124,31 @@ contract BCoWPool_Unit_Commit is BaseCoWPoolTest { bCoWPool.commit(orderHash); } + function test_Revert_CommitmentAlreadySet(bytes32 _existingCommitment, bytes32 _newCommitment) public { + vm.assume(_existingCommitment != bytes32(0)); + bCoWPool.call__setLock(_existingCommitment); + vm.prank(cowSolutionSettler); + vm.expectRevert(IBCoWPool.BCoWPool_CommitmentAlreadySet.selector); + bCoWPool.commit(_newCommitment); + } + function test_Set_Commitment(bytes32 orderHash) public { vm.prank(cowSolutionSettler); bCoWPool.commit(orderHash); assertEq(bCoWPool.commitment(), orderHash); } + + function test_Call_SetLock(bytes32 orderHash) public { + bCoWPool.expectCall__setLock(orderHash); + vm.prank(cowSolutionSettler); + bCoWPool.commit(orderHash); + } + + function test_Set_ReentrancyLock(bytes32 orderHash) public { + vm.prank(cowSolutionSettler); + bCoWPool.commit(orderHash); + assertEq(bCoWPool.call__getLock(), orderHash); + } } contract BCoWPool_Unit_Verify is BaseCoWPoolTest, SwapExactAmountInUtils { @@ -290,7 +310,7 @@ contract BCoWPool_Unit_IsValidSignature is BaseCoWPoolTest { // stores the order hash in the transient storage slot bytes32 _orderHash = GPv2Order.hash(_order, domainSeparator); - bCoWPool.set_commitment(_orderHash); + bCoWPool.call__setLock(_orderHash); _; } @@ -324,7 +344,7 @@ contract BCoWPool_Unit_IsValidSignature is BaseCoWPoolTest { GPv2Order.Data memory _order, bytes32 _differentCommitment ) public happyPath(_order) { - bCoWPool.set_commitment(_differentCommitment); + bCoWPool.call__setLock(_differentCommitment); bytes32 _orderHash = GPv2Order.hash(_order, domainSeparator); vm.expectRevert(IBCoWPool.OrderDoesNotMatchCommitmentHash.selector); bCoWPool.isValidSignature(_orderHash, abi.encode(_order)); diff --git a/test/unit/BPool.t.sol b/test/unit/BPool.t.sol index 94453db3..9048da78 100644 --- a/test/unit/BPool.t.sol +++ b/test/unit/BPool.t.sol @@ -5,7 +5,7 @@ import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import {BPool} from 'contracts/BPool.sol'; import {IBPool} from 'interfaces/IBPool.sol'; -import {MockBPool} from 'test/manual-smock/MockBPool.sol'; +import {MockBPool} from 'test/smock/MockBPool.sol'; import {BConst} from 'contracts/BConst.sol'; import {BMath} from 'contracts/BMath.sol'; @@ -83,13 +83,19 @@ abstract contract BasePoolTest is Test, BConst, Utils, BMath { function _expectRevertByReentrancy() internal { // Assert that the contract is accessible assertEq(bPool.call__getLock(), _MUTEX_FREE); - // Simulate ongoing call to the contract bPool.call__setLock(_MUTEX_TAKEN); vm.expectRevert(IBPool.BPool_Reentrancy.selector); } + function _expectSetReentrancyLock() internal { + // Assert that the contract is accessible + assertEq(bPool.call__getLock(), _MUTEX_FREE); + // Expect reentrancy lock to be set + bPool.expectCall__setLock(_MUTEX_TAKEN); + } + function _assumeCalcSpotPrice( uint256 _tokenInBalance, uint256 _tokenInDenorm, @@ -570,14 +576,16 @@ contract BPool_Unit_SetSwapFee is BasePoolTest { } function test_Set_SwapFee(uint256 _fee) public happyPath(_fee) { - vm.assume(_fee >= MIN_FEE); - vm.assume(_fee <= MAX_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); @@ -615,6 +623,11 @@ contract BPool_Unit_SetController is BasePoolTest { bPool.setController(_controller); } + + function test_Set_ReentrancyLock(address _controller) public { + _expectSetReentrancyLock(); + bPool.setController(_controller); + } } contract BPool_Unit_Finalize is BasePoolTest { @@ -663,6 +676,11 @@ contract BPool_Unit_Finalize is BasePoolTest { 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(); @@ -770,6 +788,11 @@ contract BPool_Unit_Bind is BasePoolTest { bPool.bind(_fuzz.token, _fuzz.balance, _fuzz.denorm); } + function test_Set_ReentrancyLock(Bind_FuzzScenario memory _fuzz) public happyPath(_fuzz) { + _expectSetReentrancyLock(); + bPool.bind(_fuzz.token, _fuzz.balance, _fuzz.denorm); + } + function test_Set_Record(Bind_FuzzScenario memory _fuzz) public happyPath(_fuzz) { bPool.bind(_fuzz.token, _fuzz.balance, _fuzz.denorm); @@ -930,6 +953,11 @@ contract BPool_Unit_Unbind is BasePoolTest { assertEq(bPool.call__totalWeight(), _fuzz.totalWeight - _fuzz.denorm); } + function test_Set_ReentrancyLock(Unbind_FuzzScenario memory _fuzz) public happyPath(_fuzz) { + _expectSetReentrancyLock(); + bPool.unbind(_fuzz.token); + } + function test_Set_TokenArray(Unbind_FuzzScenario memory _fuzz) public happyPath(_fuzz) { address _lastTokenBefore = bPool.call__tokens()[bPool.call__tokens().length - 1]; @@ -1235,6 +1263,11 @@ contract BPool_Unit_JoinPool is BasePoolTest { bPool.joinPool(_fuzz.poolAmountOut, _maxArray(tokens.length)); } + function test_Set_ReentrancyLock(JoinPool_FuzzScenario memory _fuzz) public happyPath(_fuzz) { + _expectSetReentrancyLock(); + bPool.joinPool(_fuzz.poolAmountOut, _maxArray(tokens.length)); + } + function test_Pull_TokenArrayTokenAmountIn(JoinPool_FuzzScenario memory _fuzz) public happyPath(_fuzz) { uint256 _poolTotal = _fuzz.initPoolSupply; uint256 _ratio = bdiv(_fuzz.poolAmountOut, _poolTotal); @@ -1380,6 +1413,11 @@ contract BPool_Unit_ExitPool is BasePoolTest { assertEq(bPool.totalSupply(), _totalSupplyBefore - _pAiAfterExitFee); } + function test_Set_ReentrancyLock(ExitPool_FuzzScenario memory _fuzz) public happyPath(_fuzz) { + _expectSetReentrancyLock(); + bPool.exitPool(_fuzz.poolAmountIn, _zeroArray(tokens.length)); + } + function test_Revert_InvalidTokenAmountOut( ExitPool_FuzzScenario memory _fuzz, uint256 _tokenIndex @@ -1621,6 +1659,11 @@ contract BPool_Unit_SwapExactAmountIn is SwapExactAmountInUtils { bPool.swapExactAmountIn(tokenIn, _fuzz.tokenAmountIn, tokenOut, 0, type(uint256).max); } + function test_Set_ReentrancyLock(SwapExactAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { + _expectSetReentrancyLock(); + bPool.swapExactAmountIn(tokenIn, _fuzz.tokenAmountIn, tokenOut, 0, type(uint256).max); + } + function test_Pull_TokenAmountIn(SwapExactAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { vm.expectCall( address(tokenIn), @@ -1964,6 +2007,11 @@ contract BPool_Unit_SwapExactAmountOut is BasePoolTest { bPool.swapExactAmountOut(tokenIn, type(uint256).max, tokenOut, _fuzz.tokenAmountOut, type(uint256).max); } + function test_Set_ReentrancyLock(SwapExactAmountOut_FuzzScenario memory _fuzz) public happyPath(_fuzz) { + _expectSetReentrancyLock(); + bPool.swapExactAmountOut(tokenIn, type(uint256).max, tokenOut, _fuzz.tokenAmountOut, type(uint256).max); + } + function test_Pull_TokenAmountIn(SwapExactAmountOut_FuzzScenario memory _fuzz) public happyPath(_fuzz) { uint256 _tokenAmountIn = calcInGivenOut( _fuzz.tokenInBalance, @@ -2152,6 +2200,11 @@ contract BPool_Unit_JoinswapExternAmountIn is BasePoolTest { bPool.joinswapExternAmountIn(tokenIn, _fuzz.tokenAmountIn, 0); } + function test_Set_ReentrancyLock(JoinswapExternAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { + _expectSetReentrancyLock(); + bPool.joinswapExternAmountIn(tokenIn, _fuzz.tokenAmountIn, 0); + } + function test_Mint_PoolShare(JoinswapExternAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { (uint256 _poolAmountOut) = bPool.joinswapExternAmountIn(tokenIn, _fuzz.tokenAmountIn, 0); @@ -2369,6 +2422,11 @@ contract BPool_Unit_JoinswapPoolAmountOut is BasePoolTest { bPool.joinswapPoolAmountOut(tokenIn, _fuzz.poolAmountOut, type(uint256).max); } + function test_Set_ReentrancyLock(JoinswapPoolAmountOut_FuzzScenario memory _fuzz) public happyPath(_fuzz) { + _expectSetReentrancyLock(); + bPool.joinswapPoolAmountOut(tokenIn, _fuzz.poolAmountOut, type(uint256).max); + } + function test_Mint_PoolShare(JoinswapPoolAmountOut_FuzzScenario memory _fuzz) public happyPath(_fuzz) { bPool.joinswapPoolAmountOut(tokenIn, _fuzz.poolAmountOut, type(uint256).max); @@ -2610,6 +2668,11 @@ contract BPool_Unit_ExitswapPoolAmountIn is BasePoolTest { assertEq(bPool.balanceOf(address(this)), _balanceBefore - _fuzz.poolAmountIn); } + function test_Set_ReentrancyLock(ExitswapPoolAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { + _expectSetReentrancyLock(); + bPool.exitswapPoolAmountIn(tokenOut, _fuzz.poolAmountIn, 0); + } + function test_Burn_PoolShare(ExitswapPoolAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { uint256 _totalSupplyBefore = bPool.totalSupply(); uint256 _exitFee = bmul(_fuzz.poolAmountIn, EXIT_FEE); @@ -2828,6 +2891,11 @@ contract BPool_Unit_ExitswapExternAmountOut is BasePoolTest { bPool.exitswapExternAmountOut(tokenOut, _fuzz.tokenAmountOut, type(uint256).max); } + function test_Set_ReentrancyLock(ExitswapExternAmountOut_FuzzScenario memory _fuzz) public happyPath(_fuzz) { + _expectSetReentrancyLock(); + bPool.exitswapExternAmountOut(tokenOut, _fuzz.tokenAmountOut, type(uint256).max); + } + function test_Pull_PoolShare(ExitswapExternAmountOut_FuzzScenario memory _fuzz) public happyPath(_fuzz) { uint256 _balanceBefore = bPool.balanceOf(address(this)); uint256 _poolAmountIn = calcPoolInGivenSingleOut( From 99f6910aabf2fb16baefe836e1cd412af2ea00f7 Mon Sep 17 00:00:00 2001 From: teddy Date: Wed, 19 Jun 2024 17:32:41 -0300 Subject: [PATCH 32/32] feat: natspec improvements and run natspec check on CI (#72) * chore: run natspec-smells as part of CI * chore: fix natspec issues --- .github/workflows/ci.yml | 8 ++++++-- natspec-smells.config.js | 4 +++- src/contracts/BCoWFactory.sol | 1 + src/contracts/BFactory.sol | 1 + src/contracts/BMath.sol | 9 +++++++++ src/interfaces/IBFactory.sol | 4 ++-- 6 files changed, 22 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e7b79eeb..d75456bf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,7 +65,7 @@ jobs: run: yarn test:integration lint: - name: Lint Commit Messages + name: Static Analysis runs-on: ubuntu-latest steps: @@ -89,4 +89,8 @@ jobs: - name: Install dependencies run: yarn --frozen-lockfile --network-concurrency 1 - - run: yarn lint:check + - name: Run forge-fmt && solhint + run: yarn lint:check + + - name: Run natspec-smells + run: yarn lint:natspec 2>&1 >/dev/null | grep 'No issues found' diff --git a/natspec-smells.config.js b/natspec-smells.config.js index 54baf0c0..c3e688ef 100644 --- a/natspec-smells.config.js +++ b/natspec-smells.config.js @@ -4,5 +4,7 @@ /** @type {import('@defi-wonderland/natspec-smells').Config} */ module.exports = { - include: 'src/**/*.sol' + enforceInheritdoc: false, + include: 'src/**/*.sol', + exclude: 'src/contracts/B(Num|Const).sol', }; diff --git a/src/contracts/BCoWFactory.sol b/src/contracts/BCoWFactory.sol index d46cf0aa..c2147917 100644 --- a/src/contracts/BCoWFactory.sol +++ b/src/contracts/BCoWFactory.sol @@ -32,6 +32,7 @@ contract BCoWFactory is BFactory, IBCoWFactory { /** * @dev Deploys a BCoWPool instead of a regular BPool. + * @return _pool The deployed BCoWPool */ function _newBPool() internal virtual override returns (IBPool _pool) { return new BCoWPool(SOLUTION_SETTLER, APP_DATA); diff --git a/src/contracts/BFactory.sol b/src/contracts/BFactory.sol index 289e8c8f..6e2d8436 100644 --- a/src/contracts/BFactory.sol +++ b/src/contracts/BFactory.sol @@ -62,6 +62,7 @@ contract BFactory is IBFactory { /** * @notice Deploys a new BPool. * @dev Internal function to allow overriding in derived contracts. + * @return _pool The deployed BPool */ function _newBPool() internal virtual returns (IBPool _pool) { return new BPool(); diff --git a/src/contracts/BMath.sol b/src/contracts/BMath.sol index cb162019..27c72fea 100644 --- a/src/contracts/BMath.sol +++ b/src/contracts/BMath.sol @@ -19,6 +19,7 @@ contract BMath is BConst, BNum { * @param tokenBalanceOut The balance of the output token in the pool * @param tokenWeightOut The weight of the output token in the pool * @param swapFee The swap fee of the pool + * @return spotPrice The spot price of a token in terms of another one * @dev Formula: * sP = spotPrice * bI = tokenBalanceIn ( bI / wI ) 1 @@ -49,6 +50,7 @@ contract BMath is BConst, BNum { * @param tokenWeightOut The weight of the output token in the pool * @param tokenAmountIn The amount of the input token * @param swapFee The swap fee of the pool + * @return tokenAmountOut The amount of token out given the amount of token in for a swap * @dev Formula: * aO = tokenAmountOut * bO = tokenBalanceOut @@ -84,6 +86,7 @@ contract BMath is BConst, BNum { * @param tokenWeightOut The weight of the output token in the pool * @param tokenAmountOut The amount of the output token * @param swapFee The swap fee of the pool + * @return tokenAmountIn The amount of token in given the amount of token out for a swap * @dev Formula: * aI = tokenAmountIn * bO = tokenBalanceOut / / bO \ (wO / wI) \ @@ -120,6 +123,7 @@ contract BMath is BConst, BNum { * @param totalWeight The total weight of the pool * @param tokenAmountIn The amount of the input token * @param swapFee The swap fee of the pool + * @return poolAmountOut The amount of balancer pool tokens that will be minted * @dev Formula: * pAo = poolAmountOut / \ * tAi = tokenAmountIn /// / // wI \ \\ \ wI \ @@ -163,6 +167,7 @@ contract BMath is BConst, BNum { * @param totalWeight The sum of the weight of all tokens in the pool * @param poolAmountOut The expected amount of pool tokens * @param swapFee The swap fee of the pool + * @return tokenAmountIn The amount of token in requred to mint poolAmountIn token pools * @dev Formula: * tAi = tokenAmountIn //(pS + pAo)\ / 1 \\ * pS = poolSupply || --------- | ^ | --------- || * bI - bI @@ -205,6 +210,8 @@ contract BMath is BConst, BNum { * @param totalWeight The total weight of the pool * @param poolAmountIn The amount of pool tokens * @param swapFee The swap fee of the pool + * @return tokenAmountOut The amount of underlying token out from burning + * poolAmountIn pool tokens * @dev Formula: * tAo = tokenAmountOut / / \\ * bO = tokenBalanceOut / // pS - (pAi * (1 - eF)) \ / 1 \ \\ @@ -251,6 +258,8 @@ contract BMath is BConst, BNum { * @param totalWeight The total weight of the pool * @param tokenAmountOut The amount of the output token * @param swapFee The swap fee of the pool + * @return poolAmountIn The amount of pool tokens to burn in order to receive + * `tokeAmountOut` underlying tokens * @dev Formula: * pAi = poolAmountIn // / tAo \\ / wO \ \ * bO = tokenBalanceOut // | bO - -------------------------- |\ | ---- | \ diff --git a/src/interfaces/IBFactory.sol b/src/interfaces/IBFactory.sol index c40cf9ca..3203d7a7 100644 --- a/src/interfaces/IBFactory.sol +++ b/src/interfaces/IBFactory.sol @@ -30,9 +30,9 @@ interface IBFactory { /** * @notice Creates a new BPool, assigning the caller as the pool controller - * @return pool The new BPool + * @return _pool The new BPool */ - function newBPool() external returns (IBPool pool); + function newBPool() external returns (IBPool _pool); /** * @notice Sets the BLabs address in the factory