From 2614a242efcba499a0f02cf96500286f1ff313cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Tue, 4 Jun 2024 14:50:05 +0200 Subject: [PATCH 01/13] feat: adding live-balances to bPool (#28) * feat: adding live balances to bPool * fix: rm unused balance query * fix: comment * fix: compilation issues [test failing] * fix: wrong token amount in swapExactOut [test still failing] * fix: swapExactOut test * fix: joinExternAmountIn test * fix: unit tests conflicts * feat: deprecating gulp method * fix: improving selector declaration * test: add assumeNotForgeAddress * fix: improving notBound fuzz assumptions * feat: adding non forge assumption to test and test:local script * fix: rm commented line --------- Co-authored-by: 0xAustrian --- .forge-snapshots/swapExactAmountIn.snap | 2 +- package.json | 1 + src/contracts/BPool.sol | 104 +++++----- test/integration/PoolSwap.t.sol | 3 +- test/unit/BPool.t.sol | 253 +++++------------------- 5 files changed, 109 insertions(+), 254 deletions(-) diff --git a/.forge-snapshots/swapExactAmountIn.snap b/.forge-snapshots/swapExactAmountIn.snap index 4a35246a..aaed5c4e 100644 --- a/.forge-snapshots/swapExactAmountIn.snap +++ b/.forge-snapshots/swapExactAmountIn.snap @@ -1 +1 @@ -116075 \ No newline at end of file +107572 \ No newline at end of file diff --git a/package.json b/package.json index bebb3a85..57b9e042 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "smock": "smock-foundry --contracts src/contracts", "test": "forge test -vvv", "test:integration": "forge test --match-contract Integration -vvv", + "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/BPool.sol b/src/contracts/BPool.sol index deddf2a2..e85159df 100644 --- a/src/contracts/BPool.sol +++ b/src/contracts/BPool.sol @@ -10,7 +10,6 @@ contract BPool is BBronze, BToken, BMath { bool bound; // is token bound to pool uint256 index; // internal uint256 denorm; // denormalized weight - uint256 balance; } bool internal _mutex; @@ -93,8 +92,7 @@ contract BPool is BBronze, BToken, BMath { _records[token] = Record({ bound: true, index: _tokens.length, - denorm: 0, // balance and denorm will be validated - balance: 0 // and set by `rebind` + denorm: 0 // denorm will be validated }); _tokens.push(token); rebind(token, balance, denorm); @@ -120,8 +118,7 @@ contract BPool is BBronze, BToken, BMath { _records[token].denorm = denorm; // Adjust the balance record and actual token balance - uint256 oldBalance = _records[token].balance; - _records[token].balance = balance; + uint256 oldBalance = IERC20(token).balanceOf(address(this)); if (balance > oldBalance) { _pullUnderlying(token, msg.sender, bsub(balance, oldBalance)); } else if (balance < oldBalance) { @@ -139,7 +136,7 @@ contract BPool is BBronze, BToken, BMath { require(_records[token].bound, 'ERR_NOT_BOUND'); require(!_finalized, 'ERR_IS_FINALIZED'); - uint256 tokenBalance = _records[token].balance; + uint256 tokenBalance = IERC20(token).balanceOf(address(this)); uint256 tokenExitFee = bmul(tokenBalance, EXIT_FEE); _totalWeight = bsub(_totalWeight, _records[token].denorm); @@ -151,24 +148,24 @@ contract BPool is BBronze, BToken, BMath { _tokens[index] = _tokens[last]; _records[_tokens[index]].index = index; _tokens.pop(); - _records[token] = Record({bound: false, index: 0, denorm: 0, balance: 0}); + _records[token] = Record({bound: false, index: 0, denorm: 0}); _pushUnderlying(token, msg.sender, bsub(tokenBalance, tokenExitFee)); _pushUnderlying(token, _factory, tokenExitFee); } - // Absorb any tokens that have been sent to this contract into the pool - function gulp(address token) external _logs_ _lock_ { - require(_records[token].bound, 'ERR_NOT_BOUND'); - _records[token].balance = IERC20(token).balanceOf(address(this)); - } - 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'); Record storage inRecord = _records[tokenIn]; Record storage outRecord = _records[tokenOut]; - return calcSpotPrice(inRecord.balance, inRecord.denorm, outRecord.balance, outRecord.denorm, _swapFee); + return calcSpotPrice( + IERC20(tokenIn).balanceOf(address(this)), + inRecord.denorm, + IERC20(tokenOut).balanceOf(address(this)), + outRecord.denorm, + _swapFee + ); } function getSpotPriceSansFee(address tokenIn, address tokenOut) external view _viewlock_ returns (uint256 spotPrice) { @@ -176,7 +173,13 @@ contract BPool is BBronze, BToken, BMath { require(_records[tokenOut].bound, 'ERR_NOT_BOUND'); Record storage inRecord = _records[tokenIn]; Record storage outRecord = _records[tokenOut]; - return calcSpotPrice(inRecord.balance, inRecord.denorm, outRecord.balance, outRecord.denorm, 0); + return calcSpotPrice( + IERC20(tokenIn).balanceOf(address(this)), + inRecord.denorm, + IERC20(tokenOut).balanceOf(address(this)), + outRecord.denorm, + 0 + ); } function joinPool(uint256 poolAmountOut, uint256[] calldata maxAmountsIn) external _logs_ _lock_ { @@ -188,11 +191,10 @@ contract BPool is BBronze, BToken, BMath { for (uint256 i = 0; i < _tokens.length; i++) { address t = _tokens[i]; - uint256 bal = _records[t].balance; + 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'); - _records[t].balance = badd(_records[t].balance, tokenAmountIn); emit LOG_JOIN(msg.sender, t, tokenAmountIn); _pullUnderlying(t, msg.sender, tokenAmountIn); } @@ -215,11 +217,10 @@ contract BPool is BBronze, BToken, BMath { for (uint256 i = 0; i < _tokens.length; i++) { address t = _tokens[i]; - uint256 bal = _records[t].balance; + 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'); - _records[t].balance = bsub(_records[t].balance, tokenAmountOut); emit LOG_EXIT(msg.sender, t, tokenAmountOut); _pushUnderlying(t, msg.sender, tokenAmountOut); } @@ -239,20 +240,23 @@ contract BPool is BBronze, BToken, BMath { Record storage inRecord = _records[address(tokenIn)]; Record storage outRecord = _records[address(tokenOut)]; - require(tokenAmountIn <= bmul(inRecord.balance, MAX_IN_RATIO), 'ERR_MAX_IN_RATIO'); + 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'); uint256 spotPriceBefore = - calcSpotPrice(inRecord.balance, inRecord.denorm, outRecord.balance, outRecord.denorm, _swapFee); + calcSpotPrice(tokenInBalance, inRecord.denorm, tokenOutBalance, outRecord.denorm, _swapFee); require(spotPriceBefore <= maxPrice, 'ERR_BAD_LIMIT_PRICE'); tokenAmountOut = - calcOutGivenIn(inRecord.balance, inRecord.denorm, outRecord.balance, outRecord.denorm, tokenAmountIn, _swapFee); + calcOutGivenIn(tokenInBalance, inRecord.denorm, tokenOutBalance, outRecord.denorm, tokenAmountIn, _swapFee); require(tokenAmountOut >= minAmountOut, 'ERR_LIMIT_OUT'); - inRecord.balance = badd(inRecord.balance, tokenAmountIn); - outRecord.balance = bsub(outRecord.balance, tokenAmountOut); + tokenInBalance = badd(tokenInBalance, tokenAmountIn); + tokenOutBalance = bsub(tokenOutBalance, tokenAmountOut); - spotPriceAfter = calcSpotPrice(inRecord.balance, inRecord.denorm, outRecord.balance, outRecord.denorm, _swapFee); + 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'); @@ -279,20 +283,23 @@ contract BPool is BBronze, BToken, BMath { Record storage inRecord = _records[address(tokenIn)]; Record storage outRecord = _records[address(tokenOut)]; - require(tokenAmountOut <= bmul(outRecord.balance, MAX_OUT_RATIO), 'ERR_MAX_OUT_RATIO'); + 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'); uint256 spotPriceBefore = - calcSpotPrice(inRecord.balance, inRecord.denorm, outRecord.balance, outRecord.denorm, _swapFee); + calcSpotPrice(tokenInBalance, inRecord.denorm, tokenOutBalance, outRecord.denorm, _swapFee); require(spotPriceBefore <= maxPrice, 'ERR_BAD_LIMIT_PRICE'); tokenAmountIn = - calcInGivenOut(inRecord.balance, inRecord.denorm, outRecord.balance, outRecord.denorm, tokenAmountOut, _swapFee); + calcInGivenOut(tokenInBalance, inRecord.denorm, tokenOutBalance, outRecord.denorm, tokenAmountOut, _swapFee); require(tokenAmountIn <= maxAmountIn, 'ERR_LIMIT_IN'); - inRecord.balance = badd(inRecord.balance, tokenAmountIn); - outRecord.balance = bsub(outRecord.balance, tokenAmountOut); + tokenInBalance = badd(tokenInBalance, tokenAmountIn); + tokenOutBalance = bsub(tokenOutBalance, tokenAmountOut); - spotPriceAfter = calcSpotPrice(inRecord.balance, inRecord.denorm, outRecord.balance, outRecord.denorm, _swapFee); + 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'); @@ -312,16 +319,15 @@ contract BPool is BBronze, BToken, BMath { ) external _logs_ _lock_ returns (uint256 poolAmountOut) { require(_finalized, 'ERR_NOT_FINALIZED'); require(_records[tokenIn].bound, 'ERR_NOT_BOUND'); - require(tokenAmountIn <= bmul(_records[tokenIn].balance, MAX_IN_RATIO), 'ERR_MAX_IN_RATIO'); Record storage inRecord = _records[tokenIn]; + uint256 tokenInBalance = IERC20(tokenIn).balanceOf(address(this)); poolAmountOut = - calcPoolOutGivenSingleIn(inRecord.balance, inRecord.denorm, _totalSupply, _totalWeight, tokenAmountIn, _swapFee); + calcPoolOutGivenSingleIn(tokenInBalance, inRecord.denorm, _totalSupply, _totalWeight, tokenAmountIn, _swapFee); require(poolAmountOut >= minPoolAmountOut, 'ERR_LIMIT_OUT'); - - inRecord.balance = badd(inRecord.balance, tokenAmountIn); + require(tokenAmountIn <= bmul(tokenInBalance, MAX_IN_RATIO), 'ERR_MAX_IN_RATIO'); emit LOG_JOIN(msg.sender, tokenIn, tokenAmountIn); @@ -341,16 +347,14 @@ contract BPool is BBronze, BToken, BMath { require(_records[tokenIn].bound, 'ERR_NOT_BOUND'); Record storage inRecord = _records[tokenIn]; + uint256 tokenInBalance = IERC20(tokenIn).balanceOf(address(this)); tokenAmountIn = - calcSingleInGivenPoolOut(inRecord.balance, inRecord.denorm, _totalSupply, _totalWeight, poolAmountOut, _swapFee); + calcSingleInGivenPoolOut(tokenInBalance, inRecord.denorm, _totalSupply, _totalWeight, poolAmountOut, _swapFee); require(tokenAmountIn != 0, 'ERR_MATH_APPROX'); require(tokenAmountIn <= maxAmountIn, 'ERR_LIMIT_IN'); - - require(tokenAmountIn <= bmul(_records[tokenIn].balance, MAX_IN_RATIO), 'ERR_MAX_IN_RATIO'); - - inRecord.balance = badd(inRecord.balance, tokenAmountIn); + require(tokenAmountIn <= bmul(tokenInBalance, MAX_IN_RATIO), 'ERR_MAX_IN_RATIO'); emit LOG_JOIN(msg.sender, tokenIn, tokenAmountIn); @@ -370,15 +374,13 @@ contract BPool is BBronze, BToken, BMath { require(_records[tokenOut].bound, 'ERR_NOT_BOUND'); Record storage outRecord = _records[tokenOut]; + uint256 tokenOutBalance = IERC20(tokenOut).balanceOf(address(this)); tokenAmountOut = - calcSingleOutGivenPoolIn(outRecord.balance, outRecord.denorm, _totalSupply, _totalWeight, poolAmountIn, _swapFee); + calcSingleOutGivenPoolIn(tokenOutBalance, outRecord.denorm, _totalSupply, _totalWeight, poolAmountIn, _swapFee); require(tokenAmountOut >= minAmountOut, 'ERR_LIMIT_OUT'); - - require(tokenAmountOut <= bmul(_records[tokenOut].balance, MAX_OUT_RATIO), 'ERR_MAX_OUT_RATIO'); - - outRecord.balance = bsub(outRecord.balance, tokenAmountOut); + require(tokenAmountOut <= bmul(tokenOutBalance, MAX_OUT_RATIO), 'ERR_MAX_OUT_RATIO'); uint256 exitFee = bmul(poolAmountIn, EXIT_FEE); @@ -399,18 +401,16 @@ contract BPool is BBronze, BToken, BMath { ) external _logs_ _lock_ returns (uint256 poolAmountIn) { require(_finalized, 'ERR_NOT_FINALIZED'); require(_records[tokenOut].bound, 'ERR_NOT_BOUND'); - require(tokenAmountOut <= bmul(_records[tokenOut].balance, MAX_OUT_RATIO), 'ERR_MAX_OUT_RATIO'); Record storage outRecord = _records[tokenOut]; + uint256 tokenOutBalance = IERC20(tokenOut).balanceOf(address(this)); - poolAmountIn = calcPoolInGivenSingleOut( - outRecord.balance, outRecord.denorm, _totalSupply, _totalWeight, tokenAmountOut, _swapFee - ); + poolAmountIn = + calcPoolInGivenSingleOut(tokenOutBalance, outRecord.denorm, _totalSupply, _totalWeight, tokenAmountOut, _swapFee); require(poolAmountIn != 0, 'ERR_MATH_APPROX'); require(poolAmountIn <= maxPoolAmountIn, 'ERR_LIMIT_IN'); - - outRecord.balance = bsub(outRecord.balance, tokenAmountOut); + require(tokenAmountOut <= bmul(tokenOutBalance, MAX_OUT_RATIO), 'ERR_MAX_OUT_RATIO'); uint256 exitFee = bmul(poolAmountIn, EXIT_FEE); @@ -466,7 +466,7 @@ contract BPool is BBronze, BToken, BMath { function getBalance(address token) external view _viewlock_ returns (uint256) { require(_records[token].bound, 'ERR_NOT_BOUND'); - return _records[token].balance; + return IERC20(token).balanceOf(address(this)); } function getSwapFee() external view _viewlock_ returns (uint256) { diff --git a/test/integration/PoolSwap.t.sol b/test/integration/PoolSwap.t.sol index 585fb752..358f8076 100644 --- a/test/integration/PoolSwap.t.sol +++ b/test/integration/PoolSwap.t.sol @@ -76,8 +76,7 @@ contract DirectPoolSwapIntegrationTest is PoolSwapIntegrationTest { } } -// TODO: remove `abstract` keyword to make the test runnable -abstract contract IndirectPoolSwapIntegrationTest is PoolSwapIntegrationTest { +contract IndirectPoolSwapIntegrationTest is PoolSwapIntegrationTest { function _makeSwap() internal override { vm.startPrank(address(pool)); tokenA.approve(address(swapper), type(uint256).max); diff --git a/test/unit/BPool.t.sol b/test/unit/BPool.t.sol index d4d61b7d..f85925f5 100644 --- a/test/unit/BPool.t.sol +++ b/test/unit/BPool.t.sol @@ -34,25 +34,23 @@ abstract contract BasePoolTest is Test, BConst, Utils, BMath { _tokensToAdd = new address[](_length); for (uint256 i = 0; i < _length; i++) { _tokensToAdd[i] = makeAddr(i.toString()); - _setRecord(_tokensToAdd[i], BPool.Record({bound: true, index: i, denorm: 0, balance: 0})); + _setRecord(_tokensToAdd[i], BPool.Record({bound: true, index: i, denorm: 0})); } _setTokens(_tokensToAdd); } function _mockTransfer(address _token) internal { // TODO: add amount to transfer to check that it's called with the right amount - vm.mockCall(_token, abi.encodeWithSelector(IERC20(_token).transfer.selector), abi.encode(true)); + vm.mockCall(_token, abi.encodeWithSelector(IERC20.transfer.selector), abi.encode(true)); } function _mockTransferFrom(address _token) internal { // TODO: add from and amount to transfer to check that it's called with the right params - vm.mockCall(_token, abi.encodeWithSelector(IERC20(_token).transferFrom.selector), abi.encode(true)); + vm.mockCall(_token, abi.encodeWithSelector(IERC20.transferFrom.selector), abi.encode(true)); } - function _mockBalanceOf(address _token, address _account, uint256 _balance) internal { - vm.mockCall( - _token, abi.encodeWithSelector(IERC20(_token).balanceOf.selector, address(_account)), abi.encode(_balance) - ); + function _mockPoolBalance(address _token, uint256 _balance) internal { + vm.mockCall(_token, abi.encodeWithSelector(IERC20.balanceOf.selector, address(bPool)), abi.encode(_balance)); } function _setTokens(address[] memory _tokens) internal { @@ -281,7 +279,7 @@ contract BPool_Unit_IsFinalized is BasePoolTest { contract BPool_Unit_IsBound is BasePoolTest { function test_Returns_IsBound(address _token, bool _isBound) public { - _setRecord(_token, BPool.Record({bound: _isBound, index: 0, denorm: 0, balance: 0})); + _setRecord(_token, BPool.Record({bound: _isBound, index: 0, denorm: 0})); assertEq(bPool.isBound(_token), _isBound); } } @@ -341,7 +339,7 @@ contract BPool_Unit_GetFinalTokens is BasePoolTest { contract BPool_Unit_GetDenormalizedWeight is BasePoolTest { function test_Returns_DenormalizedWeight(address _token, uint256 _weight) public { - bPool.set__records(_token, BPool.Record({bound: true, index: 0, denorm: _weight, balance: 0})); + bPool.set__records(_token, BPool.Record({bound: true, index: 0, denorm: _weight})); assertEq(bPool.getDenormalizedWeight(_token), _weight); } @@ -375,7 +373,7 @@ contract BPool_Unit_GetNormalizedWeight is BasePoolTest { _weight = bound(_weight, MIN_WEIGHT, MAX_WEIGHT); _totalWeight = bound(_totalWeight, MIN_WEIGHT, MAX_TOTAL_WEIGHT); vm.assume(_weight < _totalWeight); - _setRecord(_token, BPool.Record({bound: true, index: 0, denorm: _weight, balance: 0})); + _setRecord(_token, BPool.Record({bound: true, index: 0, denorm: _weight})); _setTotalWeight(_totalWeight); assertEq(bPool.getNormalizedWeight(_token), bdiv(_weight, _totalWeight)); @@ -394,7 +392,10 @@ contract BPool_Unit_GetNormalizedWeight is BasePoolTest { contract BPool_Unit_GetBalance is BasePoolTest { function test_Returns_Balance(address _token, uint256 _balance) public { - bPool.set__records(_token, BPool.Record({bound: true, index: 0, denorm: 0, balance: _balance})); + assumeNotForgeAddress(_token); + + bPool.set__records(_token, BPool.Record({bound: true, index: 0, denorm: 0})); + _mockPoolBalance(_token, _balance); assertEq(bPool.getBalance(_token), _balance); } @@ -651,6 +652,7 @@ contract BPool_Unit_Bind is BasePoolTest { function _setValues(Bind_FuzzScenario memory _fuzz) internal { // Create mocks _mockTransferFrom(_fuzz.token); + _mockPoolBalance(_fuzz.token, 0); // Set tokens _setRandomTokens(_fuzz.previousTokensAmount); @@ -771,9 +773,8 @@ contract BPool_Unit_Rebind is BasePoolTest { _mockTransfer(_fuzz.token); // Set token - _setRecord( - _fuzz.token, BPool.Record({bound: true, index: 0, denorm: _fuzz.previousDenorm, balance: _fuzz.previousBalance}) - ); + _setRecord(_fuzz.token, BPool.Record({bound: true, index: 0, denorm: _fuzz.previousDenorm})); + _mockPoolBalance(_fuzz.token, _fuzz.previousBalance); // Set finalize _setFinalize(false); @@ -783,6 +784,7 @@ contract BPool_Unit_Rebind is BasePoolTest { function _assumeHappyPath(Rebind_FuzzScenario memory _fuzz) internal pure { assumeNotForgeAddress(_fuzz.token); + _fuzz.balance = bound(_fuzz.balance, MIN_BALANCE, type(uint256).max); _fuzz.previousBalance = bound(_fuzz.previousBalance, MIN_BALANCE, type(uint256).max); _fuzz.totalWeight = bound(_fuzz.totalWeight, MIN_WEIGHT, MAX_TOTAL_WEIGHT - MIN_WEIGHT); @@ -877,12 +879,6 @@ contract BPool_Unit_Rebind is BasePoolTest { assertEq(bPool.call__records(_fuzz.token).denorm, _fuzz.denorm); } - function test_Set_Balance(Rebind_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - bPool.rebind(_fuzz.token, _fuzz.balance, _fuzz.denorm); - - assertEq(bPool.call__records(_fuzz.token).balance, _fuzz.balance); - } - function test_Pull_IfBalanceMoreThanOldBalance(Rebind_FuzzScenario memory _fuzz) public happyPath(_fuzz) { vm.assume(_fuzz.balance > _fuzz.previousBalance); @@ -951,9 +947,9 @@ contract BPool_Unit_Unbind is BasePoolTest { // Set denorm and balance _setRecord( - _fuzz.previousTokens[_fuzz.tokenIndex], - BPool.Record({bound: true, index: _fuzz.tokenIndex, denorm: _fuzz.denorm, balance: _fuzz.balance}) + _fuzz.previousTokens[_fuzz.tokenIndex], BPool.Record({bound: true, index: _fuzz.tokenIndex, denorm: _fuzz.denorm}) ); + _mockPoolBalance(_fuzz.previousTokens[_fuzz.tokenIndex], _fuzz.balance); // Set finalize _setFinalize(false); @@ -1055,7 +1051,6 @@ contract BPool_Unit_Unbind is BasePoolTest { assertEq(bPool.call__records(_fuzz.previousTokens[_fuzz.tokenIndex]).index, 0); assertEq(bPool.call__records(_fuzz.previousTokens[_fuzz.tokenIndex]).bound, false); assertEq(bPool.call__records(_fuzz.previousTokens[_fuzz.tokenIndex]).denorm, 0); - assertEq(bPool.call__records(_fuzz.previousTokens[_fuzz.tokenIndex]).balance, 0); } function test_Push_UnderlyingBalance(Unbind_FuzzScenario memory _fuzz) public happyPath(_fuzz) { @@ -1087,46 +1082,6 @@ contract BPool_Unit_Unbind is BasePoolTest { } } -contract BPool_Unit_Gulp is BasePoolTest { - struct Gulp_FuzzScenario { - address token; - uint256 balance; - } - - modifier happyPath(Gulp_FuzzScenario memory _fuzz) { - assumeNotForgeAddress(_fuzz.token); - _mockBalanceOf(_fuzz.token, address(bPool), _fuzz.balance); - _setRecord(_fuzz.token, BPool.Record({bound: true, index: 0, denorm: 0, balance: _fuzz.balance})); - _; - } - - function test_Revert_NotBound(Gulp_FuzzScenario memory _fuzz, address _token) public happyPath(_fuzz) { - vm.assume(_token != _fuzz.token); - - vm.expectRevert('ERR_NOT_BOUND'); - bPool.gulp(_token); - } - - function test_Revert_Reentrancy(Gulp_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - _expectRevertByReentrancy(); - bPool.gulp(_fuzz.token); - } - - function test_Set_Balance(Gulp_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - bPool.gulp(_fuzz.token); - - assertEq(bPool.getBalance(_fuzz.token), _fuzz.balance); - } - - function test_Emit_LogCall(Gulp_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - vm.expectEmit(); - bytes memory _data = abi.encodeWithSelector(BPool.gulp.selector, _fuzz.token); - emit BPool.LOG_CALL(BPool.gulp.selector, address(this), _data); - - bPool.gulp(_fuzz.token); - } -} - contract BPool_Unit_GetSpotPrice is BasePoolTest { struct GetSpotPrice_FuzzScenario { address tokenIn; @@ -1139,17 +1094,16 @@ contract BPool_Unit_GetSpotPrice is BasePoolTest { } function _setValues(GetSpotPrice_FuzzScenario memory _fuzz) internal { - _setRecord( - _fuzz.tokenIn, BPool.Record({bound: true, index: 0, denorm: _fuzz.tokenInDenorm, balance: _fuzz.tokenInBalance}) - ); - _setRecord( - _fuzz.tokenOut, - BPool.Record({bound: true, index: 0, denorm: _fuzz.tokenOutDenorm, balance: _fuzz.tokenOutBalance}) - ); + _setRecord(_fuzz.tokenIn, BPool.Record({bound: true, index: 0, denorm: _fuzz.tokenInDenorm})); + _mockPoolBalance(_fuzz.tokenIn, _fuzz.tokenInBalance); + _setRecord(_fuzz.tokenOut, BPool.Record({bound: true, index: 0, denorm: _fuzz.tokenOutDenorm})); + _mockPoolBalance(_fuzz.tokenOut, _fuzz.tokenOutBalance); _setSwapFee(_fuzz.swapFee); } function _assumeHappyPath(GetSpotPrice_FuzzScenario memory _fuzz) internal pure { + assumeNotForgeAddress(_fuzz.tokenIn); + assumeNotForgeAddress(_fuzz.tokenOut); vm.assume(_fuzz.tokenIn != _fuzz.tokenOut); _assumeCalcSpotPrice( _fuzz.tokenInBalance, _fuzz.tokenInDenorm, _fuzz.tokenOutBalance, _fuzz.tokenOutDenorm, _fuzz.swapFee @@ -1209,17 +1163,16 @@ contract BPool_Unit_GetSpotPriceSansFee is BasePoolTest { } function _setValues(GetSpotPriceSansFee_FuzzScenario memory _fuzz) internal { - _setRecord( - _fuzz.tokenIn, BPool.Record({bound: true, index: 0, denorm: _fuzz.tokenInDenorm, balance: _fuzz.tokenInBalance}) - ); - _setRecord( - _fuzz.tokenOut, - BPool.Record({bound: true, index: 0, denorm: _fuzz.tokenOutDenorm, balance: _fuzz.tokenOutBalance}) - ); + _setRecord(_fuzz.tokenIn, BPool.Record({bound: true, index: 0, denorm: _fuzz.tokenInDenorm})); + _mockPoolBalance(_fuzz.tokenIn, _fuzz.tokenInBalance); + _setRecord(_fuzz.tokenOut, BPool.Record({bound: true, index: 0, denorm: _fuzz.tokenOutDenorm})); + _mockPoolBalance(_fuzz.tokenOut, _fuzz.tokenOutBalance); _setSwapFee(0); } function _assumeHappyPath(GetSpotPriceSansFee_FuzzScenario memory _fuzz) internal pure { + assumeNotForgeAddress(_fuzz.tokenIn); + assumeNotForgeAddress(_fuzz.tokenOut); vm.assume(_fuzz.tokenIn != _fuzz.tokenOut); _assumeCalcSpotPrice(_fuzz.tokenInBalance, _fuzz.tokenInDenorm, _fuzz.tokenOutBalance, _fuzz.tokenOutDenorm, 0); } @@ -1289,10 +1242,10 @@ contract BPool_Unit_JoinPool is BasePoolTest { BPool.Record({ bound: true, index: 0, // NOTE: irrelevant for this method - denorm: 0, // NOTE: irrelevant for this method - balance: _fuzz.balance[i] + denorm: 0 // NOTE: irrelevant for this method }) ); + _mockPoolBalance(tokens[i], _fuzz.balance[i]); } // Set public swap @@ -1368,19 +1321,6 @@ contract BPool_Unit_JoinPool is BasePoolTest { bPool.joinPool(_fuzz.poolAmountOut, _maxArray(tokens.length)); } - function test_Set_TokenArrayBalance(JoinPool_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - bPool.joinPool(_fuzz.poolAmountOut, _maxArray(tokens.length)); - - uint256 _poolTotal = _fuzz.initPoolSupply; - uint256 _ratio = bdiv(_fuzz.poolAmountOut, _poolTotal); - - for (uint256 i = 0; i < tokens.length; i++) { - uint256 _bal = _fuzz.balance[i]; - uint256 _tokenAmountIn = bmul(_ratio, _bal); - assertEq(bPool.getBalance(tokens[i]), _bal + _tokenAmountIn); - } - } - function test_Emit_TokenArrayLogJoin(JoinPool_FuzzScenario memory _fuzz) public happyPath(_fuzz) { uint256 _poolTotal = _fuzz.initPoolSupply; uint256 _ratio = bdiv(_fuzz.poolAmountOut, _poolTotal); @@ -1457,10 +1397,10 @@ contract BPool_Unit_ExitPool is BasePoolTest { BPool.Record({ bound: true, index: 0, // NOTE: irrelevant for this method - denorm: 0, // NOTE: irrelevant for this method - balance: _fuzz.balance[i] + denorm: 0 // NOTE: irrelevant for this method }) ); + _mockPoolBalance(tokens[i], _fuzz.balance[i]); } // Set LP token balance @@ -1579,25 +1519,6 @@ contract BPool_Unit_ExitPool is BasePoolTest { bPool.exitPool(_fuzz.poolAmountIn, _zeroArray(tokens.length)); } - function test_Set_TokenArrayBalance(ExitPool_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - uint256[] memory _balanceBefore = new uint256[](tokens.length); - for (uint256 i = 0; i < tokens.length; i++) { - _balanceBefore[i] = bPool.getBalance(tokens[i]); - } - - bPool.exitPool(_fuzz.poolAmountIn, _zeroArray(tokens.length)); - - uint256 _exitFee = bmul(_fuzz.poolAmountIn, EXIT_FEE); - uint256 _pAiAfterExitFee = bsub(_fuzz.poolAmountIn, _exitFee); - uint256 _ratio = bdiv(_pAiAfterExitFee, _fuzz.initPoolSupply); - - for (uint256 i = 0; i < tokens.length; i++) { - uint256 _bal = _fuzz.balance[i]; - uint256 _tokenAmountOut = bmul(_ratio, _bal); - assertEq(bPool.getBalance(tokens[i]), _balanceBefore[i] - _tokenAmountOut); - } - } - function test_Emit_TokenArrayLogExit(ExitPool_FuzzScenario memory _fuzz) public happyPath(_fuzz) { uint256 _exitFee = bmul(_fuzz.poolAmountIn, EXIT_FEE); uint256 _pAiAfterExitFee = bsub(_fuzz.poolAmountIn, _exitFee); @@ -1663,19 +1584,20 @@ contract BPool_Unit_SwapExactAmountIn is BasePoolTest { BPool.Record({ bound: true, index: 0, // NOTE: irrelevant for this method - denorm: _fuzz.tokenInDenorm, - balance: _fuzz.tokenInBalance + denorm: _fuzz.tokenInDenorm }) ); + _mockPoolBalance(tokenIn, _fuzz.tokenInBalance); + _setRecord( tokenOut, BPool.Record({ bound: true, index: 0, // NOTE: irrelevant for this method - denorm: _fuzz.tokenOutDenorm, - balance: _fuzz.tokenOutBalance + denorm: _fuzz.tokenOutDenorm }) ); + _mockPoolBalance(tokenOut, _fuzz.tokenOutBalance); // Set swapFee _setSwapFee(_fuzz.swapFee); @@ -1816,18 +1738,6 @@ contract BPool_Unit_SwapExactAmountIn is BasePoolTest { bPool.swapExactAmountIn(tokenIn, _fuzz.tokenAmountIn, tokenOut, 0, type(uint256).max); } - function test_Set_InRecord(SwapExactAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - bPool.swapExactAmountIn(tokenIn, _fuzz.tokenAmountIn, tokenOut, 0, type(uint256).max); - - assertEq(bPool.getBalance(tokenIn), _fuzz.tokenInBalance + _fuzz.tokenAmountIn); - } - - function test_Set_OutRecord(SwapExactAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - (uint256 _tokenAmountOut,) = bPool.swapExactAmountIn(tokenIn, _fuzz.tokenAmountIn, tokenOut, 0, type(uint256).max); - - assertEq(bPool.getBalance(tokenOut), _fuzz.tokenOutBalance - _tokenAmountOut); - } - function test_Revert_MathApprox() public { vm.skip(true); // TODO: this revert might be unreachable. Find a way to test it or remove the revert in the code. @@ -1998,19 +1908,20 @@ contract BPool_Unit_SwapExactAmountOut is BasePoolTest { BPool.Record({ bound: true, index: 0, // NOTE: irrelevant for this method - denorm: _fuzz.tokenInDenorm, - balance: _fuzz.tokenInBalance + denorm: _fuzz.tokenInDenorm }) ); + _mockPoolBalance(tokenIn, _fuzz.tokenInBalance); + _setRecord( tokenOut, BPool.Record({ bound: true, index: 0, // NOTE: irrelevant for this method - denorm: _fuzz.tokenOutDenorm, - balance: _fuzz.tokenOutBalance + denorm: _fuzz.tokenOutDenorm }) ); + _mockPoolBalance(tokenOut, _fuzz.tokenOutBalance); // Set swapFee _setSwapFee(_fuzz.swapFee); @@ -2162,19 +2073,6 @@ contract BPool_Unit_SwapExactAmountOut is BasePoolTest { bPool.swapExactAmountOut(tokenIn, type(uint256).max, tokenOut, _fuzz.tokenAmountOut, type(uint256).max); } - function test_Set_InRecord(SwapExactAmountOut_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - (uint256 _tokenAmountIn,) = - bPool.swapExactAmountOut(tokenIn, type(uint256).max, tokenOut, _fuzz.tokenAmountOut, type(uint256).max); - - assertEq(bPool.getBalance(tokenIn), _fuzz.tokenInBalance + _tokenAmountIn); - } - - function test_Set_OutRecord(SwapExactAmountOut_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - bPool.swapExactAmountOut(tokenIn, type(uint256).max, tokenOut, _fuzz.tokenAmountOut, type(uint256).max); - - assertEq(bPool.getBalance(tokenOut), _fuzz.tokenOutBalance - _fuzz.tokenAmountOut); - } - function test_Revert_MathApprox() public { vm.skip(true); // TODO: this revert might be unreachable. Find a way to test it or remove the revert in the code. @@ -2350,10 +2248,10 @@ contract BPool_Unit_JoinswapExternAmountIn is BasePoolTest { BPool.Record({ bound: true, index: 0, // NOTE: irrelevant for this method - denorm: _fuzz.tokenInDenorm, - balance: _fuzz.tokenInBalance + denorm: _fuzz.tokenInDenorm }) ); + _mockPoolBalance(tokenIn, _fuzz.tokenInBalance); // Set swapFee _setSwapFee(_fuzz.swapFee); @@ -2418,6 +2316,7 @@ contract BPool_Unit_JoinswapExternAmountIn is BasePoolTest { } function test_Revert_MaxInRatio(JoinswapExternAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { + vm.skip(true); // TODO: FIX! this test is failing uint256 _tokenAmountIn = bmul(_fuzz.tokenInBalance, MAX_IN_RATIO); vm.expectRevert('ERR_MAX_IN_RATIO'); @@ -2447,12 +2346,6 @@ contract BPool_Unit_JoinswapExternAmountIn is BasePoolTest { bPool.joinswapExternAmountIn(tokenIn, _fuzz.tokenAmountIn, 0); } - function test_Set_Balance(JoinswapExternAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - bPool.joinswapExternAmountIn(tokenIn, _fuzz.tokenAmountIn, 0); - - assertEq(bPool.getBalance(tokenIn), _fuzz.tokenInBalance + _fuzz.tokenAmountIn); - } - function test_Emit_LogJoin(JoinswapExternAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { vm.expectEmit(); emit BPool.LOG_JOIN(address(this), tokenIn, _fuzz.tokenAmountIn); @@ -2530,10 +2423,10 @@ contract BPool_Unit_JoinswapPoolAmountOut is BasePoolTest { BPool.Record({ bound: true, index: 0, // NOTE: irrelevant for this method - denorm: _fuzz.tokenInDenorm, - balance: _fuzz.tokenInBalance + denorm: _fuzz.tokenInDenorm }) ); + _mockPoolBalance(tokenIn, _fuzz.tokenInBalance); // Set swapFee _setSwapFee(_fuzz.swapFee); @@ -2664,23 +2557,6 @@ contract BPool_Unit_JoinswapPoolAmountOut is BasePoolTest { bPool.joinswapPoolAmountOut(tokenIn, _fuzz.poolAmountOut, type(uint256).max); } - function test_Set_Balance(JoinswapPoolAmountOut_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - uint256 _balanceBefore = bPool.getBalance(tokenIn); - - bPool.joinswapPoolAmountOut(tokenIn, _fuzz.poolAmountOut, type(uint256).max); - - uint256 _tokenAmountIn = calcSingleInGivenPoolOut( - _fuzz.tokenInBalance, - _fuzz.tokenInDenorm, - _fuzz.totalSupply, - _fuzz.totalWeight, - _fuzz.poolAmountOut, - _fuzz.swapFee - ); - - assertEq(bPool.getBalance(tokenIn), _balanceBefore + _tokenAmountIn); - } - function test_Emit_LogJoin(JoinswapPoolAmountOut_FuzzScenario memory _fuzz) public happyPath(_fuzz) { uint256 _tokenAmountIn = calcSingleInGivenPoolOut( _fuzz.tokenInBalance, @@ -2776,10 +2652,10 @@ contract BPool_Unit_ExitswapPoolAmountIn is BasePoolTest { BPool.Record({ bound: true, index: 0, // NOTE: irrelevant for this method - denorm: _fuzz.tokenOutDenorm, - balance: _fuzz.tokenOutBalance + denorm: _fuzz.tokenOutDenorm }) ); + _mockPoolBalance(tokenOut, _fuzz.tokenOutBalance); // Set swapFee _setSwapFee(_fuzz.swapFee); @@ -2915,22 +2791,6 @@ contract BPool_Unit_ExitswapPoolAmountIn is BasePoolTest { bPool.exitswapPoolAmountIn(tokenOut, _fuzz.poolAmountIn, 0); } - function test_Set_Balance(ExitswapPoolAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - uint256 _balanceBefore = bPool.getBalance(tokenOut); - uint256 _tokenAmountOut = calcSingleOutGivenPoolIn( - _fuzz.tokenOutBalance, - _fuzz.tokenOutDenorm, - _fuzz.totalSupply, - _fuzz.totalWeight, - _fuzz.poolAmountIn, - _fuzz.swapFee - ); - - bPool.exitswapPoolAmountIn(tokenOut, _fuzz.poolAmountIn, 0); - - assertEq(bPool.getBalance(tokenOut), _balanceBefore - _tokenAmountOut); - } - function test_Emit_LogExit(ExitswapPoolAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { uint256 _tokenAmountOut = calcSingleOutGivenPoolIn( _fuzz.tokenOutBalance, @@ -3036,10 +2896,10 @@ contract BPool_Unit_ExitswapExternAmountOut is BasePoolTest { BPool.Record({ bound: true, index: 0, // NOTE: irrelevant for this method - denorm: _fuzz.tokenOutDenorm, - balance: _fuzz.tokenOutBalance + denorm: _fuzz.tokenOutDenorm }) ); + _mockPoolBalance(tokenOut, _fuzz.tokenOutBalance); // Set swapFee _setSwapFee(_fuzz.swapFee); @@ -3129,6 +2989,7 @@ contract BPool_Unit_ExitswapExternAmountOut is BasePoolTest { } function test_Revert_MaxOutRatio(ExitswapExternAmountOut_FuzzScenario memory _fuzz) public happyPath(_fuzz) { + vm.skip(true); // TODO: FIX! this test is failing uint256 _maxTokenAmountOut = bmul(_fuzz.tokenOutBalance, MAX_OUT_RATIO); vm.expectRevert('ERR_MAX_OUT_RATIO'); @@ -3165,12 +3026,6 @@ contract BPool_Unit_ExitswapExternAmountOut is BasePoolTest { bPool.exitswapExternAmountOut(tokenOut, _fuzz.tokenAmountOut, type(uint256).max); } - function test_Set_Balance(ExitswapExternAmountOut_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - bPool.exitswapExternAmountOut(tokenOut, _fuzz.tokenAmountOut, type(uint256).max); - - assertEq(bPool.getBalance(tokenOut), _fuzz.tokenOutBalance - _fuzz.tokenAmountOut); - } - function test_Emit_LogExit(ExitswapExternAmountOut_FuzzScenario memory _fuzz) public happyPath(_fuzz) { vm.expectEmit(); emit BPool.LOG_EXIT(address(this), tokenOut, _fuzz.tokenAmountOut); From 69c6bb4389235c797419e7e032ef1afa4f96845f Mon Sep 17 00:00:00 2001 From: Austrian <114922365+0xAustrian@users.noreply.github.com> Date: Tue, 4 Jun 2024 12:31:03 -0300 Subject: [PATCH 02/13] fix: rollback change in require order (#57) --- src/contracts/BPool.sol | 6 ++---- test/unit/BPool.t.sol | 2 -- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/contracts/BPool.sol b/src/contracts/BPool.sol index e85159df..11edca7c 100644 --- a/src/contracts/BPool.sol +++ b/src/contracts/BPool.sol @@ -322,12 +322,11 @@ contract BPool is BBronze, BToken, BMath { Record storage inRecord = _records[tokenIn]; uint256 tokenInBalance = IERC20(tokenIn).balanceOf(address(this)); + require(tokenAmountIn <= bmul(tokenInBalance, MAX_IN_RATIO), 'ERR_MAX_IN_RATIO'); poolAmountOut = calcPoolOutGivenSingleIn(tokenInBalance, inRecord.denorm, _totalSupply, _totalWeight, tokenAmountIn, _swapFee); - require(poolAmountOut >= minPoolAmountOut, 'ERR_LIMIT_OUT'); - require(tokenAmountIn <= bmul(tokenInBalance, MAX_IN_RATIO), 'ERR_MAX_IN_RATIO'); emit LOG_JOIN(msg.sender, tokenIn, tokenAmountIn); @@ -404,13 +403,12 @@ contract BPool is BBronze, BToken, BMath { Record storage outRecord = _records[tokenOut]; uint256 tokenOutBalance = IERC20(tokenOut).balanceOf(address(this)); + require(tokenAmountOut <= bmul(tokenOutBalance, MAX_OUT_RATIO), 'ERR_MAX_OUT_RATIO'); poolAmountIn = calcPoolInGivenSingleOut(tokenOutBalance, outRecord.denorm, _totalSupply, _totalWeight, tokenAmountOut, _swapFee); - require(poolAmountIn != 0, 'ERR_MATH_APPROX'); require(poolAmountIn <= maxPoolAmountIn, 'ERR_LIMIT_IN'); - require(tokenAmountOut <= bmul(tokenOutBalance, MAX_OUT_RATIO), 'ERR_MAX_OUT_RATIO'); uint256 exitFee = bmul(poolAmountIn, EXIT_FEE); diff --git a/test/unit/BPool.t.sol b/test/unit/BPool.t.sol index f85925f5..f23892a2 100644 --- a/test/unit/BPool.t.sol +++ b/test/unit/BPool.t.sol @@ -2316,7 +2316,6 @@ contract BPool_Unit_JoinswapExternAmountIn is BasePoolTest { } function test_Revert_MaxInRatio(JoinswapExternAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - vm.skip(true); // TODO: FIX! this test is failing uint256 _tokenAmountIn = bmul(_fuzz.tokenInBalance, MAX_IN_RATIO); vm.expectRevert('ERR_MAX_IN_RATIO'); @@ -2989,7 +2988,6 @@ contract BPool_Unit_ExitswapExternAmountOut is BasePoolTest { } function test_Revert_MaxOutRatio(ExitswapExternAmountOut_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - vm.skip(true); // TODO: FIX! this test is failing uint256 _maxTokenAmountOut = bmul(_fuzz.tokenOutBalance, MAX_OUT_RATIO); vm.expectRevert('ERR_MAX_OUT_RATIO'); From 992ed1d2e9fb8474e9eb4ddfe4a308171db67cd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Tue, 4 Jun 2024 18:38:44 +0200 Subject: [PATCH 03/13] feat: deprecating public swap (#53) --- .forge-snapshots/swapExactAmountIn.snap | 2 +- src/contracts/BPool.sol | 17 +---- test/unit/BPool.t.sol | 83 ++----------------------- 3 files changed, 9 insertions(+), 93 deletions(-) diff --git a/.forge-snapshots/swapExactAmountIn.snap b/.forge-snapshots/swapExactAmountIn.snap index aaed5c4e..84d900dc 100644 --- a/.forge-snapshots/swapExactAmountIn.snap +++ b/.forge-snapshots/swapExactAmountIn.snap @@ -1 +1 @@ -107572 \ No newline at end of file +107497 \ No newline at end of file diff --git a/src/contracts/BPool.sol b/src/contracts/BPool.sol index 11edca7c..23277b01 100644 --- a/src/contracts/BPool.sol +++ b/src/contracts/BPool.sol @@ -16,7 +16,6 @@ contract BPool is BBronze, BToken, BMath { address internal _factory; // BFactory address to push token exitFee to address internal _controller; // has CONTROL role - bool internal _publicSwap; // true if PUBLIC can call SWAP functions // `setSwapFee` and `finalize` require CONTROL // `finalize` sets `PUBLIC can SWAP`, `PUBLIC can JOIN` @@ -45,7 +44,6 @@ contract BPool is BBronze, BToken, BMath { _controller = msg.sender; _factory = msg.sender; _swapFee = MIN_FEE; - _publicSwap = false; _finalized = false; } @@ -62,19 +60,12 @@ contract BPool is BBronze, BToken, BMath { _controller = manager; } - function setPublicSwap(bool public_) external _logs_ _lock_ { - require(!_finalized, 'ERR_IS_FINALIZED'); - require(msg.sender == _controller, 'ERR_NOT_CONTROLLER'); - _publicSwap = public_; - } - 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'); _finalized = true; - _publicSwap = true; _mintPoolShare(INIT_POOL_SUPPLY); _pushPoolShare(msg.sender, INIT_POOL_SUPPLY); @@ -235,7 +226,7 @@ contract BPool is BBronze, BToken, BMath { ) external _logs_ _lock_ returns (uint256 tokenAmountOut, uint256 spotPriceAfter) { require(_records[tokenIn].bound, 'ERR_NOT_BOUND'); require(_records[tokenOut].bound, 'ERR_NOT_BOUND'); - require(_publicSwap, 'ERR_SWAP_NOT_PUBLIC'); + require(_finalized, 'ERR_NOT_FINALIZED'); Record storage inRecord = _records[address(tokenIn)]; Record storage outRecord = _records[address(tokenOut)]; @@ -278,7 +269,7 @@ contract BPool is BBronze, BToken, BMath { ) external _logs_ _lock_ returns (uint256 tokenAmountIn, uint256 spotPriceAfter) { require(_records[tokenIn].bound, 'ERR_NOT_BOUND'); require(_records[tokenOut].bound, 'ERR_NOT_BOUND'); - require(_publicSwap, 'ERR_SWAP_NOT_PUBLIC'); + require(_finalized, 'ERR_NOT_FINALIZED'); Record storage inRecord = _records[address(tokenIn)]; Record storage outRecord = _records[address(tokenOut)]; @@ -422,10 +413,6 @@ contract BPool is BBronze, BToken, BMath { return poolAmountIn; } - function isPublicSwap() external view returns (bool) { - return _publicSwap; - } - function isFinalized() external view returns (bool) { return _finalized; } diff --git a/test/unit/BPool.t.sol b/test/unit/BPool.t.sol index f23892a2..0ebc4c44 100644 --- a/test/unit/BPool.t.sol +++ b/test/unit/BPool.t.sol @@ -61,10 +61,6 @@ abstract contract BasePoolTest is Test, BConst, Utils, BMath { bPool.set__records(_token, _record); } - function _setPublicSwap(bool _isPublicSwap) internal { - bPool.set__publicSwap(_isPublicSwap); - } - function _setSwapFee(uint256 _swapFee) internal { bPool.set__swapFee(_swapFee); } @@ -258,18 +254,10 @@ contract BPool_Unit_Constructor is BasePoolTest { assertEq(_newBPool.call__controller(), _deployer); assertEq(_newBPool.call__factory(), _deployer); assertEq(_newBPool.call__swapFee(), MIN_FEE); - assertEq(_newBPool.call__publicSwap(), false); assertEq(_newBPool.call__finalized(), false); } } -contract BPool_Unit_IsPublicSwap is BasePoolTest { - function test_Returns_IsPublicSwap(bool _isPublicSwap) public { - bPool.set__publicSwap(_isPublicSwap); - assertEq(bPool.isPublicSwap(), _isPublicSwap); - } -} - contract BPool_Unit_IsFinalized is BasePoolTest { function test_Returns_IsFinalized(bool _isFinalized) public { bPool.set__finalized(_isFinalized); @@ -527,43 +515,6 @@ contract BPool_Unit_SetController is BasePoolTest { } } -contract BPool_Unit_SetPublicSwap is BasePoolTest { - function test_Revert_Finalized(bool _isPublicSwap) public { - _setFinalize(true); - - vm.expectRevert('ERR_IS_FINALIZED'); - bPool.setPublicSwap(_isPublicSwap); - } - - function test_Revert_NotController(address _controller, address _caller, bool _isPublicSwap) public { - vm.assume(_controller != _caller); - bPool.set__controller(_controller); - - vm.expectRevert('ERR_NOT_CONTROLLER'); - vm.prank(_caller); - bPool.setPublicSwap(_isPublicSwap); - } - - function test_Revert_Reentrancy(bool _isPublicSwap) public { - _expectRevertByReentrancy(); - bPool.setPublicSwap(_isPublicSwap); - } - - function test_Set_PublicSwap(bool _isPublicSwap) public { - bPool.setPublicSwap(_isPublicSwap); - - assertEq(bPool.call__publicSwap(), _isPublicSwap); - } - - function test_Emit_LogCall(bool _isPublicSwap) public { - vm.expectEmit(); - bytes memory _data = abi.encodeWithSelector(BPool.setPublicSwap.selector, _isPublicSwap); - emit BPool.LOG_CALL(BPool.setPublicSwap.selector, address(this), _data); - - bPool.setPublicSwap(_isPublicSwap); - } -} - contract BPool_Unit_Finalize is BasePoolTest { modifier happyPath(uint256 _tokensLength) { _tokensLength = bound(_tokensLength, MIN_BOUND_TOKENS, MAX_BOUND_TOKENS); @@ -610,12 +561,6 @@ contract BPool_Unit_Finalize is BasePoolTest { assertEq(bPool.call__finalized(), true); } - function test_Set_PublicSwap(uint256 _tokensLength) public happyPath(_tokensLength) { - bPool.finalize(); - - assertEq(bPool.call__publicSwap(), true); - } - function test_Mint_InitPoolSupply(uint256 _tokensLength) public happyPath(_tokensLength) { bPool.finalize(); @@ -1248,8 +1193,6 @@ contract BPool_Unit_JoinPool is BasePoolTest { _mockPoolBalance(tokens[i], _fuzz.balance[i]); } - // Set public swap - _setPublicSwap(true); // Set finalize _setFinalize(true); // Set totalSupply @@ -1407,8 +1350,6 @@ contract BPool_Unit_ExitPool is BasePoolTest { _setPoolBalance(address(this), _fuzz.poolAmountIn); // give LP tokens to fn caller // Set totalSupply _setTotalSupply(_fuzz.initPoolSupply - _fuzz.poolAmountIn); - // Set public swap - _setPublicSwap(true); // Set finalize _setFinalize(true); } @@ -1601,8 +1542,6 @@ contract BPool_Unit_SwapExactAmountIn is BasePoolTest { // Set swapFee _setSwapFee(_fuzz.swapFee); - // Set public swap - _setPublicSwap(true); // Set finalize _setFinalize(true); } @@ -1687,10 +1626,10 @@ contract BPool_Unit_SwapExactAmountIn is BasePoolTest { bPool.swapExactAmountIn(tokenIn, _fuzz.tokenAmountIn, _tokenOut, 0, type(uint256).max); } - function test_Revert_NotPublic(SwapExactAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - _setPublicSwap(false); + function test_Revert_NotFinalized(SwapExactAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { + _setFinalize(false); - vm.expectRevert('ERR_SWAP_NOT_PUBLIC'); + vm.expectRevert('ERR_NOT_FINALIZED'); bPool.swapExactAmountIn(tokenIn, _fuzz.tokenAmountIn, tokenOut, 0, type(uint256).max); } @@ -1925,8 +1864,6 @@ contract BPool_Unit_SwapExactAmountOut is BasePoolTest { // Set swapFee _setSwapFee(_fuzz.swapFee); - // Set public swap - _setPublicSwap(true); // Set finalize _setFinalize(true); } @@ -2022,10 +1959,10 @@ contract BPool_Unit_SwapExactAmountOut is BasePoolTest { bPool.swapExactAmountOut(tokenIn, type(uint256).max, _tokenOut, _fuzz.tokenAmountOut, type(uint256).max); } - function test_Revert_NotPublic(SwapExactAmountOut_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - _setPublicSwap(false); + function test_Revert_NotFinalized(SwapExactAmountOut_FuzzScenario memory _fuzz) public happyPath(_fuzz) { + _setFinalize(false); - vm.expectRevert('ERR_SWAP_NOT_PUBLIC'); + vm.expectRevert('ERR_NOT_FINALIZED'); bPool.swapExactAmountOut(tokenIn, type(uint256).max, tokenOut, _fuzz.tokenAmountOut, type(uint256).max); } @@ -2255,8 +2192,6 @@ contract BPool_Unit_JoinswapExternAmountIn is BasePoolTest { // Set swapFee _setSwapFee(_fuzz.swapFee); - // Set public swap - _setPublicSwap(true); // Set finalize _setFinalize(true); // Set totalSupply @@ -2429,8 +2364,6 @@ contract BPool_Unit_JoinswapPoolAmountOut is BasePoolTest { // Set swapFee _setSwapFee(_fuzz.swapFee); - // Set public swap - _setPublicSwap(true); // Set finalize _setFinalize(true); // Set totalSupply @@ -2658,8 +2591,6 @@ contract BPool_Unit_ExitswapPoolAmountIn is BasePoolTest { // Set swapFee _setSwapFee(_fuzz.swapFee); - // Set public swap - _setPublicSwap(true); // Set finalize _setFinalize(true); // Set balance @@ -2902,8 +2833,6 @@ contract BPool_Unit_ExitswapExternAmountOut is BasePoolTest { // Set swapFee _setSwapFee(_fuzz.swapFee); - // Set public swap - _setPublicSwap(true); // Set finalize _setFinalize(true); // Set balance From 56ca377ac56d515b10fe07adc19e31104f47b627 Mon Sep 17 00:00:00 2001 From: Austrian <114922365+0xAustrian@users.noreply.github.com> Date: Tue, 4 Jun 2024 15:02:02 -0300 Subject: [PATCH 04/13] fix: test randomly failing (#59) --- test/unit/BPool.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/BPool.t.sol b/test/unit/BPool.t.sol index 0ebc4c44..97207548 100644 --- a/test/unit/BPool.t.sol +++ b/test/unit/BPool.t.sol @@ -2854,7 +2854,7 @@ contract BPool_Unit_ExitswapExternAmountOut is BasePoolTest { _fuzz.totalWeight = bound(_fuzz.totalWeight, MIN_WEIGHT * TOKENS_AMOUNT, MAX_TOTAL_WEIGHT); // min - _fuzz.totalSupply = bound(_fuzz.totalSupply, INIT_POOL_SUPPLY, type(uint256).max); + _fuzz.totalSupply = bound(_fuzz.totalSupply, INIT_POOL_SUPPLY, type(uint256).max / BONE); // MAX_OUT_RATIO vm.assume(_fuzz.tokenOutBalance < type(uint256).max / MAX_OUT_RATIO); From 6cf3f60d32d0debbcac08fbc0a8d66dee8fb193d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Wed, 5 Jun 2024 09:32:53 +0200 Subject: [PATCH 05/13] feat: deprecating rebind functionality (#54) * feat: adding live balances to bPool * fix: rm unused balance query * fix: comment * fix: compilation issues [test failing] * fix: wrong token amount in swapExactOut [test still failing] * fix: swapExactOut test * fix: joinExternAmountIn test * fix: unit tests conflicts * feat: deprecating gulp method * fix: improving selector declaration * test: add assumeNotForgeAddress * feat: deprecating rebind * feat: deprecate unbind exit fee * fix: foundry toml * refactor: reordering methods * chore: adding TODO comment * fix: rm unused library * fix: reordered arguments in test method * fix: reordered arguments in test method * test: prevent fuzzed token to be inside present tokens array (#58) * chore: rollback change in order of requirements * fix: rollback order change in tests --------- Co-authored-by: 0xAustrian Co-authored-by: Austrian <114922365+0xAustrian@users.noreply.github.com> --- .forge-snapshots/swapExactAmountIn.snap | 2 +- src/contracts/BPool.sol | 143 +++++-------- test/unit/BPool.t.sol | 259 +++++------------------- test/utils/Utils.sol | 10 + 4 files changed, 116 insertions(+), 298 deletions(-) diff --git a/.forge-snapshots/swapExactAmountIn.snap b/.forge-snapshots/swapExactAmountIn.snap index 84d900dc..ae5bdb92 100644 --- a/.forge-snapshots/swapExactAmountIn.snap +++ b/.forge-snapshots/swapExactAmountIn.snap @@ -1 +1 @@ -107497 \ No newline at end of file +107629 \ No newline at end of file diff --git a/src/contracts/BPool.sol b/src/contracts/BPool.sol index 23277b01..081bca30 100644 --- a/src/contracts/BPool.sol +++ b/src/contracts/BPool.sol @@ -40,6 +40,23 @@ contract BPool is BBronze, BToken, BMath { event LOG_CALL(bytes4 indexed sig, address indexed caller, bytes data) anonymous; + modifier _logs_() { + emit LOG_CALL(msg.sig, msg.sender, msg.data); + _; + } + + modifier _lock_() { + require(!_mutex, 'ERR_REENTRY'); + _mutex = true; + _; + _mutex = false; + } + + modifier _viewlock_() { + require(!_mutex, 'ERR_REENTRY'); + _; + } + constructor() { _controller = msg.sender; _factory = msg.sender; @@ -71,65 +88,31 @@ contract BPool is BBronze, BToken, BMath { _pushPoolShare(msg.sender, INIT_POOL_SUPPLY); } - function bind(address token, uint256 balance, uint256 denorm) external _logs_ - // _lock_ Bind does not lock because it jumps to `rebind`, which does - { + 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'); require(_tokens.length < MAX_BOUND_TOKENS, 'ERR_MAX_TOKENS'); - _records[token] = Record({ - bound: true, - index: _tokens.length, - denorm: 0 // denorm will be validated - }); - _tokens.push(token); - rebind(token, balance, denorm); - } - - function rebind(address token, uint256 balance, uint256 denorm) public _logs_ _lock_ { - require(msg.sender == _controller, 'ERR_NOT_CONTROLLER'); - require(_records[token].bound, 'ERR_NOT_BOUND'); - require(!_finalized, 'ERR_IS_FINALIZED'); - require(denorm >= MIN_WEIGHT, 'ERR_MIN_WEIGHT'); require(denorm <= MAX_WEIGHT, 'ERR_MAX_WEIGHT'); require(balance >= MIN_BALANCE, 'ERR_MIN_BALANCE'); - // Adjust the denorm and totalWeight - uint256 oldWeight = _records[token].denorm; - if (denorm > oldWeight) { - _totalWeight = badd(_totalWeight, bsub(denorm, oldWeight)); - require(_totalWeight <= MAX_TOTAL_WEIGHT, 'ERR_MAX_TOTAL_WEIGHT'); - } else if (denorm < oldWeight) { - _totalWeight = bsub(_totalWeight, bsub(oldWeight, denorm)); - } - _records[token].denorm = denorm; - - // Adjust the balance record and actual token balance - uint256 oldBalance = IERC20(token).balanceOf(address(this)); - if (balance > oldBalance) { - _pullUnderlying(token, msg.sender, bsub(balance, oldBalance)); - } else if (balance < oldBalance) { - // In this case liquidity is being withdrawn, so charge EXIT_FEE - uint256 tokenBalanceWithdrawn = bsub(oldBalance, balance); - uint256 tokenExitFee = bmul(tokenBalanceWithdrawn, EXIT_FEE); - _pushUnderlying(token, msg.sender, bsub(tokenBalanceWithdrawn, tokenExitFee)); - _pushUnderlying(token, _factory, tokenExitFee); - } + _totalWeight = badd(_totalWeight, denorm); + require(_totalWeight <= MAX_TOTAL_WEIGHT, 'ERR_MAX_TOTAL_WEIGHT'); + + _records[token] = Record({bound: true, index: _tokens.length, denorm: denorm}); + _tokens.push(token); + + _pullUnderlying(token, msg.sender, balance); } - // solhint-disable-next-line ordering 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'); - uint256 tokenBalance = IERC20(token).balanceOf(address(this)); - uint256 tokenExitFee = bmul(tokenBalance, EXIT_FEE); - _totalWeight = bsub(_totalWeight, _records[token].denorm); // Swap the token-to-unbind with the last token, @@ -141,36 +124,7 @@ contract BPool is BBronze, BToken, BMath { _tokens.pop(); _records[token] = Record({bound: false, index: 0, denorm: 0}); - _pushUnderlying(token, msg.sender, bsub(tokenBalance, tokenExitFee)); - _pushUnderlying(token, _factory, tokenExitFee); - } - - 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'); - Record storage inRecord = _records[tokenIn]; - Record storage outRecord = _records[tokenOut]; - return calcSpotPrice( - IERC20(tokenIn).balanceOf(address(this)), - inRecord.denorm, - IERC20(tokenOut).balanceOf(address(this)), - outRecord.denorm, - _swapFee - ); - } - - 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'); - Record storage inRecord = _records[tokenIn]; - Record storage outRecord = _records[tokenOut]; - return calcSpotPrice( - IERC20(tokenIn).balanceOf(address(this)), - inRecord.denorm, - IERC20(tokenOut).balanceOf(address(this)), - outRecord.denorm, - 0 - ); + _pushUnderlying(token, msg.sender, IERC20(token).balanceOf(address(this))); } function joinPool(uint256 poolAmountOut, uint256[] calldata maxAmountsIn) external _logs_ _lock_ { @@ -413,6 +367,34 @@ contract BPool is BBronze, BToken, BMath { return poolAmountIn; } + 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'); + Record storage inRecord = _records[tokenIn]; + Record storage outRecord = _records[tokenOut]; + return calcSpotPrice( + IERC20(tokenIn).balanceOf(address(this)), + inRecord.denorm, + IERC20(tokenOut).balanceOf(address(this)), + outRecord.denorm, + _swapFee + ); + } + + 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'); + Record storage inRecord = _records[tokenIn]; + Record storage outRecord = _records[tokenOut]; + return calcSpotPrice( + IERC20(tokenIn).balanceOf(address(this)), + inRecord.denorm, + IERC20(tokenOut).balanceOf(address(this)), + outRecord.denorm, + 0 + ); + } + function isFinalized() external view returns (bool) { return _finalized; } @@ -491,21 +473,4 @@ contract BPool is BBronze, BToken, BMath { function _burnPoolShare(uint256 amount) internal { _burn(amount); } - - modifier _logs_() { - emit LOG_CALL(msg.sig, msg.sender, msg.data); - _; - } - - modifier _lock_() { - require(!_mutex, 'ERR_REENTRY'); - _mutex = true; - _; - _mutex = false; - } - - modifier _viewlock_() { - require(!_mutex, 'ERR_REENTRY'); - _; - } } diff --git a/test/unit/BPool.t.sol b/test/unit/BPool.t.sol index 97207548..6a565153 100644 --- a/test/unit/BPool.t.sol +++ b/test/unit/BPool.t.sol @@ -8,13 +8,10 @@ import {BConst} from 'contracts/BConst.sol'; import {BMath} from 'contracts/BMath.sol'; import {IERC20} from 'contracts/BToken.sol'; import {Test} from 'forge-std/Test.sol'; -import {LibString} from 'solmate/utils/LibString.sol'; import {Pow} from 'test/utils/Pow.sol'; import {Utils} from 'test/utils/Utils.sol'; abstract contract BasePoolTest is Test, BConst, Utils, BMath { - using LibString for uint256; - MockBPool public bPool; // Deploy this external contract to perform a try-catch when calling bpow. @@ -25,15 +22,15 @@ abstract contract BasePoolTest is Test, BConst, Utils, BMath { bPool = new MockBPool(); // Create fake tokens + address[] memory _tokensToAdd = _getDeterministicTokenArray(TOKENS_AMOUNT); for (uint256 i = 0; i < tokens.length; i++) { - tokens[i] = makeAddr(i.toString()); + tokens[i] = _tokensToAdd[i]; } } function _setRandomTokens(uint256 _length) internal returns (address[] memory _tokensToAdd) { - _tokensToAdd = new address[](_length); + _tokensToAdd = _getDeterministicTokenArray(_length); for (uint256 i = 0; i < _length; i++) { - _tokensToAdd[i] = makeAddr(i.toString()); _setRecord(_tokensToAdd[i], BPool.Record({bound: true, index: i, denorm: 0})); } _setTokens(_tokensToAdd); @@ -273,8 +270,6 @@ contract BPool_Unit_IsBound is BasePoolTest { } contract BPool_Unit_GetNumTokens is BasePoolTest { - using LibString for *; - function test_Returns_NumTokens(uint256 _tokensToAdd) public { vm.assume(_tokensToAdd > 0); vm.assume(_tokensToAdd <= MAX_BOUND_TOKENS); @@ -583,15 +578,12 @@ contract BPool_Unit_Finalize is BasePoolTest { } contract BPool_Unit_Bind is BasePoolTest { - using LibString for *; - struct Bind_FuzzScenario { address token; uint256 balance; uint256 denorm; uint256 previousTokensAmount; uint256 totalWeight; - address[] previousTokens; } function _setValues(Bind_FuzzScenario memory _fuzz) internal { @@ -612,10 +604,10 @@ contract BPool_Unit_Bind is BasePoolTest { assumeNotForgeAddress(_fuzz.token); _fuzz.previousTokensAmount = bound(_fuzz.previousTokensAmount, 0, MAX_BOUND_TOKENS - 1); - _fuzz.previousTokens = new address[](_fuzz.previousTokensAmount); + + address[] memory _tokenArray = _getDeterministicTokenArray(_fuzz.previousTokensAmount); for (uint256 i = 0; i < _fuzz.previousTokensAmount; i++) { - _fuzz.previousTokens[i] = makeAddr(i.toString()); - vm.assume(_fuzz.token != _fuzz.previousTokens[i]); + vm.assume(_fuzz.token != _tokenArray[i]); } _fuzz.balance = bound(_fuzz.balance, MIN_BALANCE, type(uint256).max); @@ -642,12 +634,11 @@ contract BPool_Unit_Bind is BasePoolTest { bPool.bind(_fuzz.token, _fuzz.balance, _fuzz.denorm); } - function test_Revert_IsBound(Bind_FuzzScenario memory _fuzz, uint256 _tokenIndex) public happyPath(_fuzz) { - vm.assume(_fuzz.previousTokensAmount > 0); - _tokenIndex = bound(_tokenIndex, 0, _fuzz.previousTokens.length - 1); + function test_Revert_IsBound(Bind_FuzzScenario memory _fuzz, address _token) public happyPath(_fuzz) { + _setRecord(_token, BPool.Record({bound: true, index: 0, denorm: 0})); vm.expectRevert('ERR_IS_BOUND'); - bPool.bind(_fuzz.previousTokens[_tokenIndex], _fuzz.balance, _fuzz.denorm); + bPool.bind(_token, _fuzz.balance, _fuzz.denorm); } function test_Revert_Finalized(Bind_FuzzScenario memory _fuzz) public happyPath(_fuzz) { @@ -689,212 +680,82 @@ contract BPool_Unit_Bind is BasePoolTest { bPool.bind(_fuzz.token, _fuzz.balance, _fuzz.denorm); } - function test_Call_Rebind(Bind_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - // TODO: this behaviour is not possible to test in current environment. - vm.skip(true); - vm.expectCall( - address(bPool), abi.encodeWithSelector(BPool.rebind.selector, _fuzz.token, _fuzz.balance, _fuzz.denorm) - ); - - bPool.bind(_fuzz.token, _fuzz.balance, _fuzz.denorm); - } -} - -contract BPool_Unit_Rebind is BasePoolTest { - using LibString for *; - - struct Rebind_FuzzScenario { - address token; - uint256 balance; - uint256 previousBalance; - uint256 denorm; - uint256 previousDenorm; - uint256 totalWeight; - } - - function _setValues(Rebind_FuzzScenario memory _fuzz) internal { - // Create mocks - _mockTransferFrom(_fuzz.token); - _mockTransfer(_fuzz.token); - - // Set token - _setRecord(_fuzz.token, BPool.Record({bound: true, index: 0, denorm: _fuzz.previousDenorm})); - _mockPoolBalance(_fuzz.token, _fuzz.previousBalance); - - // Set finalize - _setFinalize(false); - // Set totalWeight - _setTotalWeight(_fuzz.totalWeight); - } - - function _assumeHappyPath(Rebind_FuzzScenario memory _fuzz) internal pure { - assumeNotForgeAddress(_fuzz.token); - - _fuzz.balance = bound(_fuzz.balance, MIN_BALANCE, type(uint256).max); - _fuzz.previousBalance = bound(_fuzz.previousBalance, MIN_BALANCE, type(uint256).max); - _fuzz.totalWeight = bound(_fuzz.totalWeight, MIN_WEIGHT, MAX_TOTAL_WEIGHT - MIN_WEIGHT); - _fuzz.previousDenorm = bound(_fuzz.previousDenorm, MIN_WEIGHT, _fuzz.totalWeight); - _fuzz.denorm = bound(_fuzz.denorm, MIN_WEIGHT, MAX_TOTAL_WEIGHT - _fuzz.totalWeight); - } - - modifier happyPath(Rebind_FuzzScenario memory _fuzz) { - _assumeHappyPath(_fuzz); - _setValues(_fuzz); - _; - } - - function test_Revert_NotController( - Rebind_FuzzScenario memory _fuzz, - address _controller, - address _caller - ) public happyPath(_fuzz) { - vm.assume(_controller != _caller); - bPool.set__controller(_controller); - - vm.prank(_caller); - vm.expectRevert('ERR_NOT_CONTROLLER'); - bPool.rebind(_fuzz.token, _fuzz.balance, _fuzz.denorm); - } - - function test_Revert_NotBound(Rebind_FuzzScenario memory _fuzz, address _token) public happyPath(_fuzz) { - vm.assume(_token != _fuzz.token); - - vm.expectRevert('ERR_NOT_BOUND'); - bPool.rebind(_token, _fuzz.balance, _fuzz.denorm); - } - - function test_Revert_Finalized(Rebind_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - _setFinalize(true); - - vm.expectRevert('ERR_IS_FINALIZED'); - bPool.rebind(_fuzz.token, _fuzz.balance, _fuzz.denorm); - } - - function test_Revert_MinWeight(Rebind_FuzzScenario memory _fuzz, uint256 _denorm) public happyPath(_fuzz) { + function test_Revert_MinWeight(Bind_FuzzScenario memory _fuzz, uint256 _denorm) public happyPath(_fuzz) { vm.assume(_denorm < MIN_WEIGHT); vm.expectRevert('ERR_MIN_WEIGHT'); - bPool.rebind(_fuzz.token, _fuzz.balance, _denorm); + bPool.bind(_fuzz.token, _fuzz.balance, _denorm); } - function test_Revert_MaxWeight(Rebind_FuzzScenario memory _fuzz, uint256 _denorm) public happyPath(_fuzz) { + function test_Revert_MaxWeight(Bind_FuzzScenario memory _fuzz, uint256 _denorm) public happyPath(_fuzz) { vm.assume(_denorm > MAX_WEIGHT); vm.expectRevert('ERR_MAX_WEIGHT'); - bPool.rebind(_fuzz.token, _fuzz.balance, _denorm); + bPool.bind(_fuzz.token, _fuzz.balance, _denorm); } - function test_Revert_MinBalance(Rebind_FuzzScenario memory _fuzz, uint256 _balance) public happyPath(_fuzz) { + function test_Revert_MinBalance(Bind_FuzzScenario memory _fuzz, uint256 _balance) public happyPath(_fuzz) { vm.assume(_balance < MIN_BALANCE); vm.expectRevert('ERR_MIN_BALANCE'); - bPool.rebind(_fuzz.token, _balance, _fuzz.denorm); + bPool.bind(_fuzz.token, _balance, _fuzz.denorm); } - function test_Revert_Reentrancy(Rebind_FuzzScenario memory _fuzz) public happyPath(_fuzz) { + function test_Revert_Reentrancy(Bind_FuzzScenario memory _fuzz) public happyPath(_fuzz) { _expectRevertByReentrancy(); - bPool.rebind(_fuzz.token, _fuzz.balance, _fuzz.denorm); - } - - function test_Set_TotalWeightIfDenormMoreThanOldWeight(Rebind_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - vm.assume(_fuzz.denorm > _fuzz.previousDenorm); - bPool.rebind(_fuzz.token, _fuzz.balance, _fuzz.denorm); - - assertEq(bPool.call__totalWeight(), _fuzz.totalWeight + (_fuzz.denorm - _fuzz.previousDenorm)); + bPool.bind(_fuzz.token, _fuzz.balance, _fuzz.denorm); } - function test_Set_TotalWeightIfDenormLessThanOldWeight(Rebind_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - vm.assume(_fuzz.denorm < _fuzz.previousDenorm); - bPool.rebind(_fuzz.token, _fuzz.balance, _fuzz.denorm); + function test_Set_TotalWeight(Bind_FuzzScenario memory _fuzz) public happyPath(_fuzz) { + bPool.bind(_fuzz.token, _fuzz.balance, _fuzz.denorm); - assertEq(bPool.call__totalWeight(), _fuzz.totalWeight - (_fuzz.previousDenorm - _fuzz.denorm)); + assertEq(bPool.call__totalWeight(), _fuzz.totalWeight + _fuzz.denorm); } - function test_Revert_MaxTotalWeight(Rebind_FuzzScenario memory _fuzz, uint256 _denorm) public happyPath(_fuzz) { - _denorm = bound(_denorm, _fuzz.previousDenorm + 1, MAX_WEIGHT); + function test_Revert_MaxTotalWeight(Bind_FuzzScenario memory _fuzz, uint256 _denorm) public happyPath(_fuzz) { + _denorm = bound(_denorm, MIN_WEIGHT, MAX_WEIGHT); _setTotalWeight(MAX_TOTAL_WEIGHT); vm.expectRevert('ERR_MAX_TOTAL_WEIGHT'); - bPool.rebind(_fuzz.token, _fuzz.balance, _denorm); + bPool.bind(_fuzz.token, _fuzz.balance, _denorm); } - function test_Set_Denorm(Rebind_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - bPool.rebind(_fuzz.token, _fuzz.balance, _fuzz.denorm); + function test_Set_Denorm(Bind_FuzzScenario memory _fuzz) public happyPath(_fuzz) { + bPool.bind(_fuzz.token, _fuzz.balance, _fuzz.denorm); assertEq(bPool.call__records(_fuzz.token).denorm, _fuzz.denorm); } - function test_Pull_IfBalanceMoreThanOldBalance(Rebind_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - vm.assume(_fuzz.balance > _fuzz.previousBalance); - + function test_Pull_Balance(Bind_FuzzScenario memory _fuzz) public happyPath(_fuzz) { vm.expectCall( address(_fuzz.token), - abi.encodeWithSelector( - IERC20.transferFrom.selector, address(this), address(bPool), _fuzz.balance - _fuzz.previousBalance - ) + abi.encodeWithSelector(IERC20.transferFrom.selector, address(this), address(bPool), _fuzz.balance) ); - bPool.rebind(_fuzz.token, _fuzz.balance, _fuzz.denorm); - } - - function test_Push_UnderlyingIfBalanceLessThanOldBalance(Rebind_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - vm.assume(_fuzz.balance < _fuzz.previousBalance); - - uint256 _tokenBalanceWithdrawn = _fuzz.previousBalance - _fuzz.balance; - uint256 _tokenExitFee = bmul(_tokenBalanceWithdrawn, EXIT_FEE); - vm.expectCall( - address(_fuzz.token), - abi.encodeWithSelector(IERC20.transfer.selector, address(this), _tokenBalanceWithdrawn - _tokenExitFee) - ); - - bPool.rebind(_fuzz.token, _fuzz.balance, _fuzz.denorm); - } - - function test_Push_FeeIfBalanceLessThanOldBalance(Rebind_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - vm.assume(_fuzz.balance < _fuzz.previousBalance); - - uint256 _tokenBalanceWithdrawn = _fuzz.previousBalance - _fuzz.balance; - uint256 _tokenExitFee = bmul(_tokenBalanceWithdrawn, EXIT_FEE); - vm.expectCall( - address(_fuzz.token), abi.encodeWithSelector(IERC20.transfer.selector, bPool.call__factory(), _tokenExitFee) - ); - - bPool.rebind(_fuzz.token, _fuzz.balance, _fuzz.denorm); - } - - function test_Emit_LogCall(Rebind_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - vm.expectEmit(); - bytes memory _data = abi.encodeWithSelector(BPool.rebind.selector, _fuzz.token, _fuzz.balance, _fuzz.denorm); - emit BPool.LOG_CALL(BPool.rebind.selector, address(this), _data); - - bPool.rebind(_fuzz.token, _fuzz.balance, _fuzz.denorm); + bPool.bind(_fuzz.token, _fuzz.balance, _fuzz.denorm); } } contract BPool_Unit_Unbind is BasePoolTest { - using LibString for *; - struct Unbind_FuzzScenario { + address token; uint256 tokenIndex; uint256 balance; uint256 denorm; uint256 previousTokensAmount; uint256 totalWeight; - address[] previousTokens; } function _setValues(Unbind_FuzzScenario memory _fuzz) internal { // Create mocks - _mockTransfer(_fuzz.previousTokens[_fuzz.tokenIndex]); + _mockTransfer(_fuzz.token); // Set tokens _setRandomTokens(_fuzz.previousTokensAmount); // Set denorm and balance - _setRecord( - _fuzz.previousTokens[_fuzz.tokenIndex], BPool.Record({bound: true, index: _fuzz.tokenIndex, denorm: _fuzz.denorm}) - ); - _mockPoolBalance(_fuzz.previousTokens[_fuzz.tokenIndex], _fuzz.balance); + _setRecord(_fuzz.token, BPool.Record({bound: true, index: _fuzz.tokenIndex, denorm: _fuzz.denorm})); + _mockPoolBalance(_fuzz.token, _fuzz.balance); // Set finalize _setFinalize(false); @@ -902,17 +763,15 @@ contract BPool_Unit_Unbind is BasePoolTest { _setTotalWeight(_fuzz.totalWeight); } - function _assumeHappyPath(Unbind_FuzzScenario memory _fuzz) internal { + function _assumeHappyPath(Unbind_FuzzScenario memory _fuzz) internal pure { + assumeNotForgeAddress(_fuzz.token); + _fuzz.balance = bound(_fuzz.balance, MIN_BALANCE, type(uint256).max); _fuzz.totalWeight = bound(_fuzz.totalWeight, MIN_WEIGHT, MAX_TOTAL_WEIGHT - MIN_WEIGHT); // The token to unbind will be included inside the array _fuzz.previousTokensAmount = bound(_fuzz.previousTokensAmount, 1, MAX_BOUND_TOKENS); _fuzz.tokenIndex = bound(_fuzz.tokenIndex, 0, _fuzz.previousTokensAmount - 1); _fuzz.denorm = bound(_fuzz.denorm, MIN_WEIGHT, _fuzz.totalWeight); - _fuzz.previousTokens = new address[](_fuzz.previousTokensAmount); - for (uint256 i = 0; i < _fuzz.previousTokensAmount; i++) { - _fuzz.previousTokens[i] = makeAddr(i.toString()); - } } modifier happyPath(Unbind_FuzzScenario memory _fuzz) { @@ -931,13 +790,11 @@ contract BPool_Unit_Unbind is BasePoolTest { vm.prank(_caller); vm.expectRevert('ERR_NOT_CONTROLLER'); - bPool.unbind(_fuzz.previousTokens[_fuzz.tokenIndex]); + bPool.unbind(_fuzz.token); } function test_Revert_NotBound(Unbind_FuzzScenario memory _fuzz, address _token) public happyPath(_fuzz) { - for (uint256 i = 0; i < _fuzz.previousTokensAmount; i++) { - vm.assume(_token != _fuzz.previousTokens[i]); - } + _setRecord(_token, BPool.Record({bound: false, index: _fuzz.tokenIndex, denorm: _fuzz.denorm})); vm.expectRevert('ERR_NOT_BOUND'); bPool.unbind(_token); @@ -947,16 +804,16 @@ contract BPool_Unit_Unbind is BasePoolTest { _setFinalize(true); vm.expectRevert('ERR_IS_FINALIZED'); - bPool.unbind(_fuzz.previousTokens[_fuzz.tokenIndex]); + bPool.unbind(_fuzz.token); } function test_Revert_Reentrancy(Unbind_FuzzScenario memory _fuzz) public happyPath(_fuzz) { _expectRevertByReentrancy(); - bPool.unbind(_fuzz.previousTokens[_fuzz.tokenIndex]); + bPool.unbind(_fuzz.token); } function test_Set_TotalWeight(Unbind_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - bPool.unbind(_fuzz.previousTokens[_fuzz.tokenIndex]); + bPool.unbind(_fuzz.token); assertEq(bPool.call__totalWeight(), _fuzz.totalWeight - _fuzz.denorm); } @@ -964,7 +821,7 @@ contract BPool_Unit_Unbind is BasePoolTest { function test_Set_TokenArray(Unbind_FuzzScenario memory _fuzz) public happyPath(_fuzz) { address _lastTokenBefore = bPool.call__tokens()[bPool.call__tokens().length - 1]; - bPool.unbind(_fuzz.previousTokens[_fuzz.tokenIndex]); + bPool.unbind(_fuzz.token); // Only check if the token is not the last of the array (that item is always poped out) if (_fuzz.tokenIndex != _fuzz.previousTokensAmount - 1) { @@ -976,7 +833,7 @@ contract BPool_Unit_Unbind is BasePoolTest { function test_Set_Index(Unbind_FuzzScenario memory _fuzz) public happyPath(_fuzz) { address _lastTokenBefore = bPool.call__tokens()[bPool.call__tokens().length - 1]; - bPool.unbind(_fuzz.previousTokens[_fuzz.tokenIndex]); + bPool.unbind(_fuzz.token); // Only check if the token is not the last of the array (that item is always poped out) if (_fuzz.tokenIndex != _fuzz.previousTokensAmount - 1) { @@ -985,45 +842,31 @@ contract BPool_Unit_Unbind is BasePoolTest { } function test_PopArray_TokenArray(Unbind_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - bPool.unbind(_fuzz.previousTokens[_fuzz.tokenIndex]); + bPool.unbind(_fuzz.token); assertEq(bPool.call__tokens().length, _fuzz.previousTokensAmount - 1); } function test_Set_Record(Unbind_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - bPool.unbind(_fuzz.previousTokens[_fuzz.tokenIndex]); + bPool.unbind(_fuzz.token); - assertEq(bPool.call__records(_fuzz.previousTokens[_fuzz.tokenIndex]).index, 0); - assertEq(bPool.call__records(_fuzz.previousTokens[_fuzz.tokenIndex]).bound, false); - assertEq(bPool.call__records(_fuzz.previousTokens[_fuzz.tokenIndex]).denorm, 0); + assertEq(bPool.call__records(_fuzz.token).index, 0); + assertEq(bPool.call__records(_fuzz.token).bound, false); + assertEq(bPool.call__records(_fuzz.token).denorm, 0); } function test_Push_UnderlyingBalance(Unbind_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - address _token = _fuzz.previousTokens[_fuzz.tokenIndex]; - uint256 _tokenExitFee = bmul(_fuzz.balance, EXIT_FEE); - vm.expectCall( - address(_token), abi.encodeWithSelector(IERC20.transfer.selector, address(this), _fuzz.balance - _tokenExitFee) - ); + vm.expectCall(_fuzz.token, abi.encodeWithSelector(IERC20.transfer.selector, address(this), _fuzz.balance)); - bPool.unbind(_token); - } - - function test_Push_UnderlyingFee(Unbind_FuzzScenario memory _fuzz) public happyPath(_fuzz) { - address _token = _fuzz.previousTokens[_fuzz.tokenIndex]; - uint256 _tokenExitFee = bmul(_fuzz.balance, EXIT_FEE); - vm.expectCall( - address(_token), abi.encodeWithSelector(IERC20.transfer.selector, bPool.call__factory(), _tokenExitFee) - ); - - bPool.unbind(_token); + bPool.unbind(_fuzz.token); } function test_Emit_LogCall(Unbind_FuzzScenario memory _fuzz) public happyPath(_fuzz) { vm.expectEmit(); - bytes memory _data = abi.encodeWithSelector(BPool.unbind.selector, _fuzz.previousTokens[_fuzz.tokenIndex]); + bytes memory _data = abi.encodeWithSelector(BPool.unbind.selector, _fuzz.token); emit BPool.LOG_CALL(BPool.unbind.selector, address(this), _data); - bPool.unbind(_fuzz.previousTokens[_fuzz.tokenIndex]); + bPool.unbind(_fuzz.token); } } diff --git a/test/utils/Utils.sol b/test/utils/Utils.sol index a4134359..db195352 100644 --- a/test/utils/Utils.sol +++ b/test/utils/Utils.sol @@ -2,12 +2,22 @@ pragma solidity 0.8.23; import {Test} from 'forge-std/Test.sol'; +import {LibString} from 'solmate/utils/LibString.sol'; contract Utils is Test { + using LibString for uint256; + uint256 public constant TOKENS_AMOUNT = 3; address[TOKENS_AMOUNT] public tokens; + function _getDeterministicTokenArray(uint256 _length) internal returns (address[] memory _tokenArray) { + _tokenArray = new address[](_length); + for (uint256 i = 0; i < _length; i++) { + _tokenArray[i] = makeAddr(i.toString()); + } + } + function _tokensToMemory() internal view returns (address[] memory _tokens) { _tokens = new address[](tokens.length); for (uint256 i = 0; i < tokens.length; i++) { From 782d4b23be56976f283cec535ac1605f7cd4cb68 Mon Sep 17 00:00:00 2001 From: Austrian <114922365+0xAustrian@users.noreply.github.com> Date: Wed, 5 Jun 2024 12:50:02 -0300 Subject: [PATCH 06/13] feat: add interfaces (#62) * feat: add interfaces for bPool * feat: add interfaces for bFactory * fix: parameter names in events from IBPool.sol * feat: address comments * feat: keep no underscores inside the interfaces --- remappings.txt | 3 +- src/contracts/BFactory.sol | 17 +++-- src/contracts/BPool.sol | 23 +------ src/interfaces/IBFactory.sol | 20 ++++++ src/interfaces/IBPool.sol | 106 ++++++++++++++++++++++++++++++++ test/integration/PoolSwap.t.sol | 4 +- test/unit/BFactory.t.sol | 20 +++--- test/unit/BPool.t.sol | 89 ++++++++++++++------------- 8 files changed, 195 insertions(+), 87 deletions(-) create mode 100644 src/interfaces/IBFactory.sol create mode 100644 src/interfaces/IBPool.sol diff --git a/remappings.txt b/remappings.txt index 7f4bc0f8..8378f597 100644 --- a/remappings.txt +++ b/remappings.txt @@ -3,4 +3,5 @@ forge-std/=node_modules/forge-std/src forge-gas-snapshot/=node_modules/forge-gas-snapshot/src solmate/=node_modules/solmate/src -contracts/=src/contracts \ No newline at end of file +contracts/=src/contracts +interfaces/=src/interfaces \ No newline at end of file diff --git a/src/contracts/BFactory.sol b/src/contracts/BFactory.sol index a601b3ed..2b634c2a 100644 --- a/src/contracts/BFactory.sol +++ b/src/contracts/BFactory.sol @@ -5,22 +5,19 @@ pragma solidity 0.8.23; import {BBronze} from './BColor.sol'; import {BPool} from './BPool.sol'; -import {IERC20} from './BToken.sol'; +import {IBFactory} from 'interfaces/IBFactory.sol'; +import {IBPool} from 'interfaces/IBPool.sol'; -contract BFactory is BBronze { +contract BFactory is BBronze, IBFactory { mapping(address => bool) internal _isBPool; address internal _blabs; - event LOG_NEW_POOL(address indexed caller, address indexed pool); - - event LOG_BLABS(address indexed caller, address indexed blabs); - constructor() { _blabs = msg.sender; } - function newBPool() external returns (BPool) { - BPool bpool = new BPool(); + function newBPool() external returns (IBPool _pool) { + IBPool bpool = new BPool(); _isBPool[address(bpool)] = true; emit LOG_NEW_POOL(msg.sender, address(bpool)); bpool.setController(msg.sender); @@ -33,9 +30,9 @@ contract BFactory is BBronze { _blabs = b; } - function collect(BPool pool) external { + function collect(IBPool pool) external { require(msg.sender == _blabs, 'ERR_NOT_BLABS'); - uint256 collected = IERC20(pool).balanceOf(address(this)); + uint256 collected = pool.balanceOf(address(this)); bool xfer = pool.transfer(_blabs, collected); require(xfer, 'ERR_ERC20_FAILED'); } diff --git a/src/contracts/BPool.sol b/src/contracts/BPool.sol index 081bca30..a817e405 100644 --- a/src/contracts/BPool.sol +++ b/src/contracts/BPool.sol @@ -4,14 +4,9 @@ pragma solidity 0.8.23; import {BBronze} from './BColor.sol'; import {BMath} from './BMath.sol'; import {BToken, IERC20} from './BToken.sol'; +import {IBPool} from 'interfaces/IBPool.sol'; -contract BPool is BBronze, BToken, BMath { - struct Record { - bool bound; // is token bound to pool - uint256 index; // internal - uint256 denorm; // denormalized weight - } - +contract BPool is BBronze, BToken, BMath, IBPool { bool internal _mutex; address internal _factory; // BFactory address to push token exitFee to @@ -26,20 +21,6 @@ contract BPool is BBronze, BToken, BMath { mapping(address => Record) internal _records; uint256 internal _totalWeight; - event LOG_SWAP( - address indexed caller, - address indexed tokenIn, - address indexed tokenOut, - uint256 tokenAmountIn, - uint256 tokenAmountOut - ); - - event LOG_JOIN(address indexed caller, address indexed tokenIn, uint256 tokenAmountIn); - - event LOG_EXIT(address indexed caller, address indexed tokenOut, uint256 tokenAmountOut); - - event LOG_CALL(bytes4 indexed sig, address indexed caller, bytes data) anonymous; - modifier _logs_() { emit LOG_CALL(msg.sig, msg.sender, msg.data); _; diff --git a/src/interfaces/IBFactory.sol b/src/interfaces/IBFactory.sol new file mode 100644 index 00000000..3507aff0 --- /dev/null +++ b/src/interfaces/IBFactory.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.23; + +import {IBPool} from 'interfaces/IBPool.sol'; + +interface IBFactory { + event LOG_NEW_POOL(address indexed caller, address indexed pool); + + event LOG_BLABS(address indexed caller, address indexed bLabs); + + function newBPool() external returns (IBPool pool); + + function setBLabs(address b) external; + + function collect(IBPool pool) external; + + function isBPool(address b) external view returns (bool isBPool); + + function getBLabs() external view returns (address bLabs); +} diff --git a/src/interfaces/IBPool.sol b/src/interfaces/IBPool.sol new file mode 100644 index 00000000..f4167684 --- /dev/null +++ b/src/interfaces/IBPool.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.23; + +import {IERC20} from 'forge-std/interfaces/IERC20.sol'; + +interface IBPool is IERC20 { + struct Record { + bool bound; // is token bound to pool + uint256 index; // internal + uint256 denorm; // denormalized weight + } + + event LOG_SWAP( + address indexed caller, + address indexed tokenIn, + address indexed tokenOut, + uint256 tokenAmountIn, + uint256 tokenAmountOut + ); + + event LOG_JOIN(address indexed caller, address indexed tokenIn, uint256 tokenAmountIn); + + event LOG_EXIT(address indexed caller, address indexed tokenOut, uint256 tokenAmountOut); + + event LOG_CALL(bytes4 indexed sig, address indexed caller, bytes data) anonymous; + + function setSwapFee(uint256 swapFee) external; + + function setController(address manager) external; + + function finalize() external; + + function bind(address token, uint256 balance, uint256 denorm) external; + + function unbind(address token) external; + + function joinPool(uint256 poolAmountOut, uint256[] calldata maxAmountsIn) external; + + function exitPool(uint256 poolAmountIn, uint256[] calldata minAmountsOut) external; + + function swapExactAmountIn( + address tokenIn, + uint256 tokenAmountIn, + address tokenOut, + uint256 minAmountOut, + uint256 maxPrice + ) external returns (uint256 tokenAmountOut, uint256 spotPriceAfter); + + function swapExactAmountOut( + address tokenIn, + uint256 maxAmountIn, + address tokenOut, + uint256 tokenAmountOut, + uint256 maxPrice + ) external returns (uint256 tokenAmountIn, uint256 spotPriceAfter); + + function joinswapExternAmountIn( + address tokenIn, + uint256 tokenAmountIn, + uint256 minPoolAmountOut + ) external returns (uint256 poolAmountOut); + + function joinswapPoolAmountOut( + address tokenIn, + uint256 poolAmountOut, + uint256 maxAmountIn + ) external returns (uint256 tokenAmountIn); + + function exitswapPoolAmountIn( + address tokenOut, + uint256 poolAmountIn, + uint256 minAmountOut + ) external returns (uint256 tokenAmountOut); + + function exitswapExternAmountOut( + address tokenOut, + uint256 tokenAmountOut, + uint256 maxPoolAmountIn + ) external returns (uint256 poolAmountIn); + + function getSpotPrice(address tokenIn, address tokenOut) external view returns (uint256 spotPrice); + + function getSpotPriceSansFee(address tokenIn, address tokenOut) external view returns (uint256 spotPrice); + + function isFinalized() external view returns (bool isFinalized); + + function isBound(address t) external view returns (bool isBound); + + function getNumTokens() external view returns (uint256 numTokens); + + function getCurrentTokens() external view returns (address[] memory tokens); + + function getFinalTokens() external view returns (address[] memory tokens); + + function getDenormalizedWeight(address token) external view returns (uint256 denormWeight); + + function getTotalDenormalizedWeight() external view returns (uint256 totalDenormWeight); + + function getNormalizedWeight(address token) external view returns (uint256 normWeight); + + function getBalance(address token) external view returns (uint256 balance); + + function getSwapFee() external view returns (uint256 swapFee); + + function getController() external view returns (address controller); +} diff --git a/test/integration/PoolSwap.t.sol b/test/integration/PoolSwap.t.sol index 358f8076..786bd726 100644 --- a/test/integration/PoolSwap.t.sol +++ b/test/integration/PoolSwap.t.sol @@ -3,14 +3,14 @@ pragma solidity 0.8.23; import {Test} from 'forge-std/Test.sol'; import {BFactory} from 'contracts/BFactory.sol'; -import {BPool} from 'contracts/BPool.sol'; import {IERC20} from 'contracts/BToken.sol'; +import {IBPool} from 'interfaces/IBPool.sol'; import {GasSnapshot} from 'forge-gas-snapshot/GasSnapshot.sol'; abstract contract PoolSwapIntegrationTest is Test, GasSnapshot { BFactory public factory; - BPool public pool; + IBPool public pool; IERC20 public tokenA; IERC20 public tokenB; diff --git a/test/unit/BFactory.t.sol b/test/unit/BFactory.t.sol index 3ee3e4fc..7c8c71cd 100644 --- a/test/unit/BFactory.t.sol +++ b/test/unit/BFactory.t.sol @@ -5,6 +5,8 @@ import {BFactory} from 'contracts/BFactory.sol'; import {BPool} from 'contracts/BPool.sol'; import {IERC20} from 'contracts/BToken.sol'; import {Test} from 'forge-std/Test.sol'; +import {IBFactory} from 'interfaces/IBFactory.sol'; +import {IBPool} from 'interfaces/IBPool.sol'; abstract contract Base is Test { BFactory public bFactory; @@ -49,7 +51,7 @@ contract BFactory_Unit_NewBPool is Base { * @notice Test that the pool is set on the mapping */ function test_Set_Pool() public { - BPool _pool = bFactory.newBPool(); + IBPool _pool = bFactory.newBPool(); assertTrue(bFactory.isBPool(address(_pool))); } @@ -61,7 +63,7 @@ contract BFactory_Unit_NewBPool is Base { vm.expectEmit(); address _expectedPoolAddress = vm.computeCreateAddress(address(bFactory), 1); - emit BFactory.LOG_NEW_POOL(_randomCaller, _expectedPoolAddress); + emit IBFactory.LOG_NEW_POOL(_randomCaller, _expectedPoolAddress); vm.prank(_randomCaller); bFactory.newBPool(); } @@ -73,7 +75,7 @@ contract BFactory_Unit_NewBPool is Base { assumeNotForgeAddress(_randomCaller); vm.prank(_randomCaller); - BPool _pool = bFactory.newBPool(); + IBPool _pool = bFactory.newBPool(); assertEq(_randomCaller, _pool.getController()); } @@ -82,7 +84,7 @@ contract BFactory_Unit_NewBPool is Base { */ function test_Returns_Pool() public { address _expectedPoolAddress = vm.computeCreateAddress(address(bFactory), 1); - BPool _pool = bFactory.newBPool(); + IBPool _pool = bFactory.newBPool(); assertEq(_expectedPoolAddress, address(_pool)); } } @@ -114,7 +116,7 @@ contract BFactory_Unit_SetBLabs is Base { */ function test_Emit_Log(address _addressToSet) public { vm.expectEmit(); - emit BFactory.LOG_BLABS(owner, _addressToSet); + emit IBFactory.LOG_BLABS(owner, _addressToSet); vm.prank(owner); bFactory.setBLabs(_addressToSet); } @@ -137,7 +139,7 @@ contract BFactory_Unit_Collect is Base { vm.assume(_randomCaller != owner); vm.expectRevert('ERR_NOT_BLABS'); vm.prank(_randomCaller); - bFactory.collect(BPool(address(0))); + bFactory.collect(IBPool(address(0))); } /** @@ -151,7 +153,7 @@ contract BFactory_Unit_Collect is Base { vm.expectCall(_lpToken, abi.encodeWithSelector(IERC20.balanceOf.selector, address(bFactory))); vm.prank(owner); - bFactory.collect(BPool(_lpToken)); + bFactory.collect(IBPool(_lpToken)); } /** @@ -165,7 +167,7 @@ contract BFactory_Unit_Collect is Base { vm.expectCall(_lpToken, abi.encodeWithSelector(IERC20.transfer.selector, owner, _toCollect)); vm.prank(owner); - bFactory.collect(BPool(_lpToken)); + bFactory.collect(IBPool(_lpToken)); } /** @@ -179,6 +181,6 @@ contract BFactory_Unit_Collect is Base { vm.expectRevert('ERR_ERC20_FAILED'); vm.prank(owner); - bFactory.collect(BPool(_lpToken)); + bFactory.collect(IBPool(_lpToken)); } } diff --git a/test/unit/BPool.t.sol b/test/unit/BPool.t.sol index 6a565153..1b4e897a 100644 --- a/test/unit/BPool.t.sol +++ b/test/unit/BPool.t.sol @@ -2,6 +2,7 @@ pragma solidity 0.8.23; import {BPool} from 'contracts/BPool.sol'; +import {IBPool} from 'interfaces/IBPool.sol'; import {MockBPool} from 'test/smock/MockBPool.sol'; import {BConst} from 'contracts/BConst.sol'; @@ -31,7 +32,7 @@ abstract contract BasePoolTest is Test, BConst, Utils, BMath { function _setRandomTokens(uint256 _length) internal returns (address[] memory _tokensToAdd) { _tokensToAdd = _getDeterministicTokenArray(_length); for (uint256 i = 0; i < _length; i++) { - _setRecord(_tokensToAdd[i], BPool.Record({bound: true, index: i, denorm: 0})); + _setRecord(_tokensToAdd[i], IBPool.Record({bound: true, index: i, denorm: 0})); } _setTokens(_tokensToAdd); } @@ -54,7 +55,7 @@ abstract contract BasePoolTest is Test, BConst, Utils, BMath { bPool.set__tokens(_tokens); } - function _setRecord(address _token, BPool.Record memory _record) internal { + function _setRecord(address _token, IBPool.Record memory _record) internal { bPool.set__records(_token, _record); } @@ -264,7 +265,7 @@ contract BPool_Unit_IsFinalized is BasePoolTest { contract BPool_Unit_IsBound is BasePoolTest { function test_Returns_IsBound(address _token, bool _isBound) public { - _setRecord(_token, BPool.Record({bound: _isBound, index: 0, denorm: 0})); + _setRecord(_token, IBPool.Record({bound: _isBound, index: 0, denorm: 0})); assertEq(bPool.isBound(_token), _isBound); } } @@ -322,7 +323,7 @@ contract BPool_Unit_GetFinalTokens is BasePoolTest { contract BPool_Unit_GetDenormalizedWeight is BasePoolTest { function test_Returns_DenormalizedWeight(address _token, uint256 _weight) public { - bPool.set__records(_token, BPool.Record({bound: true, index: 0, denorm: _weight})); + bPool.set__records(_token, IBPool.Record({bound: true, index: 0, denorm: _weight})); assertEq(bPool.getDenormalizedWeight(_token), _weight); } @@ -356,7 +357,7 @@ contract BPool_Unit_GetNormalizedWeight is BasePoolTest { _weight = bound(_weight, MIN_WEIGHT, MAX_WEIGHT); _totalWeight = bound(_totalWeight, MIN_WEIGHT, MAX_TOTAL_WEIGHT); vm.assume(_weight < _totalWeight); - _setRecord(_token, BPool.Record({bound: true, index: 0, denorm: _weight})); + _setRecord(_token, IBPool.Record({bound: true, index: 0, denorm: _weight})); _setTotalWeight(_totalWeight); assertEq(bPool.getNormalizedWeight(_token), bdiv(_weight, _totalWeight)); @@ -377,7 +378,7 @@ contract BPool_Unit_GetBalance is BasePoolTest { function test_Returns_Balance(address _token, uint256 _balance) public { assumeNotForgeAddress(_token); - bPool.set__records(_token, BPool.Record({bound: true, index: 0, denorm: 0})); + bPool.set__records(_token, IBPool.Record({bound: true, index: 0, denorm: 0})); _mockPoolBalance(_token, _balance); assertEq(bPool.getBalance(_token), _balance); @@ -474,7 +475,7 @@ contract BPool_Unit_SetSwapFee is BasePoolTest { function test_Emit_LogCall(uint256 _fee) public happyPath(_fee) { vm.expectEmit(); bytes memory _data = abi.encodeWithSelector(BPool.setSwapFee.selector, _fee); - emit BPool.LOG_CALL(BPool.setSwapFee.selector, address(this), _data); + emit IBPool.LOG_CALL(BPool.setSwapFee.selector, address(this), _data); bPool.setSwapFee(_fee); } @@ -504,7 +505,7 @@ contract BPool_Unit_SetController is BasePoolTest { function test_Emit_LogCall(address _controller) public { vm.expectEmit(); bytes memory _data = abi.encodeWithSelector(BPool.setController.selector, _controller); - emit BPool.LOG_CALL(BPool.setController.selector, address(this), _data); + emit IBPool.LOG_CALL(BPool.setController.selector, address(this), _data); bPool.setController(_controller); } @@ -571,7 +572,7 @@ contract BPool_Unit_Finalize is BasePoolTest { function test_Emit_LogCall(uint256 _tokensLength) public happyPath(_tokensLength) { vm.expectEmit(); bytes memory _data = abi.encodeWithSelector(BPool.finalize.selector); - emit BPool.LOG_CALL(BPool.finalize.selector, address(this), _data); + emit IBPool.LOG_CALL(BPool.finalize.selector, address(this), _data); bPool.finalize(); } @@ -635,7 +636,7 @@ contract BPool_Unit_Bind is BasePoolTest { } function test_Revert_IsBound(Bind_FuzzScenario memory _fuzz, address _token) public happyPath(_fuzz) { - _setRecord(_token, BPool.Record({bound: true, index: 0, denorm: 0})); + _setRecord(_token, IBPool.Record({bound: true, index: 0, denorm: 0})); vm.expectRevert('ERR_IS_BOUND'); bPool.bind(_token, _fuzz.balance, _fuzz.denorm); @@ -675,7 +676,7 @@ contract BPool_Unit_Bind is BasePoolTest { function test_Emit_LogCall(Bind_FuzzScenario memory _fuzz) public happyPath(_fuzz) { vm.expectEmit(); bytes memory _data = abi.encodeWithSelector(BPool.bind.selector, _fuzz.token, _fuzz.balance, _fuzz.denorm); - emit BPool.LOG_CALL(BPool.bind.selector, address(this), _data); + emit IBPool.LOG_CALL(BPool.bind.selector, address(this), _data); bPool.bind(_fuzz.token, _fuzz.balance, _fuzz.denorm); } @@ -754,7 +755,7 @@ contract BPool_Unit_Unbind is BasePoolTest { _setRandomTokens(_fuzz.previousTokensAmount); // Set denorm and balance - _setRecord(_fuzz.token, BPool.Record({bound: true, index: _fuzz.tokenIndex, denorm: _fuzz.denorm})); + _setRecord(_fuzz.token, IBPool.Record({bound: true, index: _fuzz.tokenIndex, denorm: _fuzz.denorm})); _mockPoolBalance(_fuzz.token, _fuzz.balance); // Set finalize @@ -794,7 +795,7 @@ contract BPool_Unit_Unbind is BasePoolTest { } function test_Revert_NotBound(Unbind_FuzzScenario memory _fuzz, address _token) public happyPath(_fuzz) { - _setRecord(_token, BPool.Record({bound: false, index: _fuzz.tokenIndex, denorm: _fuzz.denorm})); + _setRecord(_token, IBPool.Record({bound: false, index: _fuzz.tokenIndex, denorm: _fuzz.denorm})); vm.expectRevert('ERR_NOT_BOUND'); bPool.unbind(_token); @@ -864,7 +865,7 @@ contract BPool_Unit_Unbind is BasePoolTest { function test_Emit_LogCall(Unbind_FuzzScenario memory _fuzz) public happyPath(_fuzz) { vm.expectEmit(); bytes memory _data = abi.encodeWithSelector(BPool.unbind.selector, _fuzz.token); - emit BPool.LOG_CALL(BPool.unbind.selector, address(this), _data); + emit IBPool.LOG_CALL(BPool.unbind.selector, address(this), _data); bPool.unbind(_fuzz.token); } @@ -882,9 +883,9 @@ contract BPool_Unit_GetSpotPrice is BasePoolTest { } function _setValues(GetSpotPrice_FuzzScenario memory _fuzz) internal { - _setRecord(_fuzz.tokenIn, BPool.Record({bound: true, index: 0, denorm: _fuzz.tokenInDenorm})); + _setRecord(_fuzz.tokenIn, IBPool.Record({bound: true, index: 0, denorm: _fuzz.tokenInDenorm})); _mockPoolBalance(_fuzz.tokenIn, _fuzz.tokenInBalance); - _setRecord(_fuzz.tokenOut, BPool.Record({bound: true, index: 0, denorm: _fuzz.tokenOutDenorm})); + _setRecord(_fuzz.tokenOut, IBPool.Record({bound: true, index: 0, denorm: _fuzz.tokenOutDenorm})); _mockPoolBalance(_fuzz.tokenOut, _fuzz.tokenOutBalance); _setSwapFee(_fuzz.swapFee); } @@ -951,9 +952,9 @@ contract BPool_Unit_GetSpotPriceSansFee is BasePoolTest { } function _setValues(GetSpotPriceSansFee_FuzzScenario memory _fuzz) internal { - _setRecord(_fuzz.tokenIn, BPool.Record({bound: true, index: 0, denorm: _fuzz.tokenInDenorm})); + _setRecord(_fuzz.tokenIn, IBPool.Record({bound: true, index: 0, denorm: _fuzz.tokenInDenorm})); _mockPoolBalance(_fuzz.tokenIn, _fuzz.tokenInBalance); - _setRecord(_fuzz.tokenOut, BPool.Record({bound: true, index: 0, denorm: _fuzz.tokenOutDenorm})); + _setRecord(_fuzz.tokenOut, IBPool.Record({bound: true, index: 0, denorm: _fuzz.tokenOutDenorm})); _mockPoolBalance(_fuzz.tokenOut, _fuzz.tokenOutBalance); _setSwapFee(0); } @@ -1027,7 +1028,7 @@ contract BPool_Unit_JoinPool is BasePoolTest { for (uint256 i = 0; i < tokens.length; i++) { _setRecord( tokens[i], - BPool.Record({ + IBPool.Record({ bound: true, index: 0, // NOTE: irrelevant for this method denorm: 0 // NOTE: irrelevant for this method @@ -1115,7 +1116,7 @@ contract BPool_Unit_JoinPool is BasePoolTest { uint256 _bal = _fuzz.balance[i]; uint256 _tokenAmountIn = bmul(_ratio, _bal); vm.expectEmit(); - emit BPool.LOG_JOIN(address(this), tokens[i], _tokenAmountIn); + emit IBPool.LOG_JOIN(address(this), tokens[i], _tokenAmountIn); } bPool.joinPool(_fuzz.poolAmountOut, _maxArray(tokens.length)); } @@ -1154,7 +1155,7 @@ contract BPool_Unit_JoinPool is BasePoolTest { function test_Emit_LogCall(JoinPool_FuzzScenario memory _fuzz) public happyPath(_fuzz) { vm.expectEmit(); bytes memory _data = abi.encodeWithSelector(BPool.joinPool.selector, _fuzz.poolAmountOut, _maxArray(tokens.length)); - emit BPool.LOG_CALL(BPool.joinPool.selector, address(this), _data); + emit IBPool.LOG_CALL(BPool.joinPool.selector, address(this), _data); bPool.joinPool(_fuzz.poolAmountOut, _maxArray(tokens.length)); } @@ -1180,7 +1181,7 @@ contract BPool_Unit_ExitPool is BasePoolTest { for (uint256 i = 0; i < tokens.length; i++) { _setRecord( tokens[i], - BPool.Record({ + IBPool.Record({ bound: true, index: 0, // NOTE: irrelevant for this method denorm: 0 // NOTE: irrelevant for this method @@ -1312,7 +1313,7 @@ contract BPool_Unit_ExitPool is BasePoolTest { uint256 _bal = _fuzz.balance[i]; uint256 _tokenAmountOut = bmul(_ratio, _bal); vm.expectEmit(); - emit BPool.LOG_EXIT(address(this), tokens[i], _tokenAmountOut); + emit IBPool.LOG_EXIT(address(this), tokens[i], _tokenAmountOut); } bPool.exitPool(_fuzz.poolAmountIn, _zeroArray(tokens.length)); } @@ -1335,7 +1336,7 @@ contract BPool_Unit_ExitPool is BasePoolTest { function test_Emit_LogCall(ExitPool_FuzzScenario memory _fuzz) public happyPath(_fuzz) { vm.expectEmit(); bytes memory _data = abi.encodeWithSelector(BPool.exitPool.selector, _fuzz.poolAmountIn, _zeroArray(tokens.length)); - emit BPool.LOG_CALL(BPool.exitPool.selector, address(this), _data); + emit IBPool.LOG_CALL(BPool.exitPool.selector, address(this), _data); bPool.exitPool(_fuzz.poolAmountIn, _zeroArray(tokens.length)); } @@ -1365,7 +1366,7 @@ contract BPool_Unit_SwapExactAmountIn is BasePoolTest { // Set balances _setRecord( tokenIn, - BPool.Record({ + IBPool.Record({ bound: true, index: 0, // NOTE: irrelevant for this method denorm: _fuzz.tokenInDenorm @@ -1375,7 +1376,7 @@ contract BPool_Unit_SwapExactAmountIn is BasePoolTest { _setRecord( tokenOut, - BPool.Record({ + IBPool.Record({ bound: true, index: 0, // NOTE: irrelevant for this method denorm: _fuzz.tokenOutDenorm @@ -1602,7 +1603,7 @@ contract BPool_Unit_SwapExactAmountIn is BasePoolTest { ); vm.expectEmit(); - emit BPool.LOG_SWAP(address(this), tokenIn, tokenOut, _fuzz.tokenAmountIn, _tokenAmountOut); + emit IBPool.LOG_SWAP(address(this), tokenIn, tokenOut, _fuzz.tokenAmountIn, _tokenAmountOut); bPool.swapExactAmountIn(tokenIn, _fuzz.tokenAmountIn, tokenOut, 0, type(uint256).max); } @@ -1657,7 +1658,7 @@ contract BPool_Unit_SwapExactAmountIn is BasePoolTest { bytes memory _data = abi.encodeWithSelector( BPool.swapExactAmountIn.selector, tokenIn, _fuzz.tokenAmountIn, tokenOut, 0, type(uint256).max ); - emit BPool.LOG_CALL(BPool.swapExactAmountIn.selector, address(this), _data); + emit IBPool.LOG_CALL(BPool.swapExactAmountIn.selector, address(this), _data); bPool.swapExactAmountIn(tokenIn, _fuzz.tokenAmountIn, tokenOut, 0, type(uint256).max); } @@ -1687,7 +1688,7 @@ contract BPool_Unit_SwapExactAmountOut is BasePoolTest { // Set balances _setRecord( tokenIn, - BPool.Record({ + IBPool.Record({ bound: true, index: 0, // NOTE: irrelevant for this method denorm: _fuzz.tokenInDenorm @@ -1697,7 +1698,7 @@ contract BPool_Unit_SwapExactAmountOut is BasePoolTest { _setRecord( tokenOut, - BPool.Record({ + IBPool.Record({ bound: true, index: 0, // NOTE: irrelevant for this method denorm: _fuzz.tokenOutDenorm @@ -1941,7 +1942,7 @@ contract BPool_Unit_SwapExactAmountOut is BasePoolTest { ); vm.expectEmit(); - emit BPool.LOG_SWAP(address(this), tokenIn, tokenOut, _tokenAmountIn, _fuzz.tokenAmountOut); + emit IBPool.LOG_SWAP(address(this), tokenIn, tokenOut, _tokenAmountIn, _fuzz.tokenAmountOut); bPool.swapExactAmountOut(tokenIn, type(uint256).max, tokenOut, _fuzz.tokenAmountOut, type(uint256).max); } @@ -1998,7 +1999,7 @@ contract BPool_Unit_SwapExactAmountOut is BasePoolTest { bytes memory _data = abi.encodeWithSelector( BPool.swapExactAmountOut.selector, tokenIn, type(uint256).max, tokenOut, _fuzz.tokenAmountOut, type(uint256).max ); - emit BPool.LOG_CALL(BPool.swapExactAmountOut.selector, address(this), _data); + emit IBPool.LOG_CALL(BPool.swapExactAmountOut.selector, address(this), _data); bPool.swapExactAmountOut(tokenIn, type(uint256).max, tokenOut, _fuzz.tokenAmountOut, type(uint256).max); } @@ -2025,7 +2026,7 @@ contract BPool_Unit_JoinswapExternAmountIn is BasePoolTest { // Set balances _setRecord( tokenIn, - BPool.Record({ + IBPool.Record({ bound: true, index: 0, // NOTE: irrelevant for this method denorm: _fuzz.tokenInDenorm @@ -2125,7 +2126,7 @@ contract BPool_Unit_JoinswapExternAmountIn is BasePoolTest { function test_Emit_LogJoin(JoinswapExternAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { vm.expectEmit(); - emit BPool.LOG_JOIN(address(this), tokenIn, _fuzz.tokenAmountIn); + emit IBPool.LOG_JOIN(address(this), tokenIn, _fuzz.tokenAmountIn); bPool.joinswapExternAmountIn(tokenIn, _fuzz.tokenAmountIn, 0); } @@ -2170,7 +2171,7 @@ contract BPool_Unit_JoinswapExternAmountIn is BasePoolTest { function test_Emit_LogCall(JoinswapExternAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { vm.expectEmit(); bytes memory _data = abi.encodeWithSelector(BPool.joinswapExternAmountIn.selector, tokenIn, _fuzz.tokenAmountIn, 0); - emit BPool.LOG_CALL(BPool.joinswapExternAmountIn.selector, address(this), _data); + emit IBPool.LOG_CALL(BPool.joinswapExternAmountIn.selector, address(this), _data); bPool.joinswapExternAmountIn(tokenIn, _fuzz.tokenAmountIn, 0); } @@ -2197,7 +2198,7 @@ contract BPool_Unit_JoinswapPoolAmountOut is BasePoolTest { // Set balances _setRecord( tokenIn, - BPool.Record({ + IBPool.Record({ bound: true, index: 0, // NOTE: irrelevant for this method denorm: _fuzz.tokenInDenorm @@ -2343,7 +2344,7 @@ contract BPool_Unit_JoinswapPoolAmountOut is BasePoolTest { ); vm.expectEmit(); - emit BPool.LOG_JOIN(address(this), tokenIn, _tokenAmountIn); + emit IBPool.LOG_JOIN(address(this), tokenIn, _tokenAmountIn); bPool.joinswapPoolAmountOut(tokenIn, _fuzz.poolAmountOut, type(uint256).max); } @@ -2397,7 +2398,7 @@ contract BPool_Unit_JoinswapPoolAmountOut is BasePoolTest { vm.expectEmit(); bytes memory _data = abi.encodeWithSelector(BPool.joinswapPoolAmountOut.selector, tokenIn, _fuzz.poolAmountOut, type(uint256).max); - emit BPool.LOG_CALL(BPool.joinswapPoolAmountOut.selector, address(this), _data); + emit IBPool.LOG_CALL(BPool.joinswapPoolAmountOut.selector, address(this), _data); bPool.joinswapPoolAmountOut(tokenIn, _fuzz.poolAmountOut, type(uint256).max); } @@ -2424,7 +2425,7 @@ contract BPool_Unit_ExitswapPoolAmountIn is BasePoolTest { // Set balances _setRecord( tokenOut, - BPool.Record({ + IBPool.Record({ bound: true, index: 0, // NOTE: irrelevant for this method denorm: _fuzz.tokenOutDenorm @@ -2575,7 +2576,7 @@ contract BPool_Unit_ExitswapPoolAmountIn is BasePoolTest { ); vm.expectEmit(); - emit BPool.LOG_EXIT(address(this), tokenOut, _tokenAmountOut); + emit IBPool.LOG_EXIT(address(this), tokenOut, _tokenAmountOut); bPool.exitswapPoolAmountIn(tokenOut, _fuzz.poolAmountIn, 0); } @@ -2639,7 +2640,7 @@ contract BPool_Unit_ExitswapPoolAmountIn is BasePoolTest { function test_Emit_LogCall(ExitswapPoolAmountIn_FuzzScenario memory _fuzz) public happyPath(_fuzz) { vm.expectEmit(); bytes memory _data = abi.encodeWithSelector(BPool.exitswapPoolAmountIn.selector, tokenOut, _fuzz.poolAmountIn, 0); - emit BPool.LOG_CALL(BPool.exitswapPoolAmountIn.selector, address(this), _data); + emit IBPool.LOG_CALL(BPool.exitswapPoolAmountIn.selector, address(this), _data); bPool.exitswapPoolAmountIn(tokenOut, _fuzz.poolAmountIn, 0); } @@ -2666,7 +2667,7 @@ contract BPool_Unit_ExitswapExternAmountOut is BasePoolTest { // Set balances _setRecord( tokenOut, - BPool.Record({ + IBPool.Record({ bound: true, index: 0, // NOTE: irrelevant for this method denorm: _fuzz.tokenOutDenorm @@ -2798,7 +2799,7 @@ contract BPool_Unit_ExitswapExternAmountOut is BasePoolTest { function test_Emit_LogExit(ExitswapExternAmountOut_FuzzScenario memory _fuzz) public happyPath(_fuzz) { vm.expectEmit(); - emit BPool.LOG_EXIT(address(this), tokenOut, _fuzz.tokenAmountOut); + emit IBPool.LOG_EXIT(address(this), tokenOut, _fuzz.tokenAmountOut); bPool.exitswapExternAmountOut(tokenOut, _fuzz.tokenAmountOut, type(uint256).max); } @@ -2881,7 +2882,7 @@ contract BPool_Unit_ExitswapExternAmountOut is BasePoolTest { vm.expectEmit(); bytes memory _data = abi.encodeWithSelector(BPool.exitswapExternAmountOut.selector, tokenOut, _fuzz.tokenAmountOut, type(uint256).max); - emit BPool.LOG_CALL(BPool.exitswapExternAmountOut.selector, address(this), _data); + emit IBPool.LOG_CALL(BPool.exitswapExternAmountOut.selector, address(this), _data); bPool.exitswapExternAmountOut(tokenOut, _fuzz.tokenAmountOut, type(uint256).max); } From 3c5a99b6c30e51cf2f3e2d72c6bd7ef1521e02ba Mon Sep 17 00:00:00 2001 From: Austrian <114922365+0xAustrian@users.noreply.github.com> Date: Thu, 6 Jun 2024 06:21:23 -0300 Subject: [PATCH 07/13] fix: random div internal error (#66) --- test/unit/BPool.t.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/test/unit/BPool.t.sol b/test/unit/BPool.t.sol index 1b4e897a..14fc0f58 100644 --- a/test/unit/BPool.t.sol +++ b/test/unit/BPool.t.sol @@ -1757,6 +1757,7 @@ contract BPool_Unit_SwapExactAmountOut is BasePoolTest { ); vm.assume(_tokenAmountIn > BONE); + vm.assume(_tokenAmountIn < type(uint256).max / BONE); vm.assume(_spotPriceBefore <= bdiv(_tokenAmountIn, _fuzz.tokenAmountOut)); // max - calcSpotPrice (spotPriceAfter) From 72ac1d51845a7990e3ed3408c0146d45e558bca1 Mon Sep 17 00:00:00 2001 From: teddy Date: Thu, 6 Jun 2024 06:23:10 -0300 Subject: [PATCH 08/13] chore: repo small fixes (#65) * chore: make use of solhints multiple configuration files * chore: remove solhint rules already enforced by forge-fmt * chore: re-enable linter rules that are not triggering any errors --- .solhint.json | 17 +++++------------ .solhint.tests.json | 27 --------------------------- package.json | 13 +++++-------- test/.solhint.json | 12 ++++++++++++ yarn.lock | 33 +++++++++++++++++---------------- 5 files changed, 39 insertions(+), 63 deletions(-) delete mode 100644 .solhint.tests.json create mode 100644 test/.solhint.json diff --git a/.solhint.json b/.solhint.json index 48de62d1..4a803897 100644 --- a/.solhint.json +++ b/.solhint.json @@ -1,22 +1,15 @@ { "extends": "solhint:recommended", "rules": { - "compiler-version": ["off"], + "avoid-low-level-calls": "off", "constructor-syntax": "warn", - "quotes": ["error", "single"], "func-visibility": ["warn", { "ignoreConstructors": true }], - "not-rely-on-time": "off", - "no-inline-assembly": "off", - "no-empty-blocks": "off", - "private-vars-leading-underscore": ["warn", { "strict": false }], "ordering": "warn", - "immutable-name-snakecase": "warn", - "avoid-low-level-calls": "off", - "no-console": "off", - "max-line-length": ["warn", 120], + "private-vars-leading-underscore": ["warn", { "strict": false }], + "quotes": "off", "TODO": "REMOVE_TEMPORARY_LINTER_SETTINGS_BELOW", + "style-guide-casing": ["warn", { "ignoreEvents": true } ], "custom-errors": "off", - "one-contract-per-file": "off", - "definition-name-capwords": "off" + "one-contract-per-file": "off" } } diff --git a/.solhint.tests.json b/.solhint.tests.json deleted file mode 100644 index 887fba05..00000000 --- a/.solhint.tests.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "extends": "solhint:recommended", - "rules": { - "compiler-version": ["off"], - "constructor-syntax": "warn", - "quotes": ["error", "single"], - "func-visibility": ["warn", { "ignoreConstructors": true }], - "not-rely-on-time": "off", - "func-name-mixedcase": "off", - "var-name-mixedcase": "off", - "const-name-snakecase": "off", - "no-inline-assembly": "off", - "no-empty-blocks": "off", - "definition-name-capwords": "off", - "named-parameters-function": "off", - "no-global-import": "off", - "max-states-count": "off", - "no-unused-vars": "off", - "private-vars-leading-underscore": ["off"], - "ordering": "off", - "state-visibility": "off", - "immutable-name-snakecase": "warn", - "avoid-low-level-calls": "off", - "one-contract-per-file": "off", - "max-line-length": ["warn", 125] - } -} diff --git a/package.json b/package.json index 57b9e042..817a9793 100644 --- a/package.json +++ b/package.json @@ -18,11 +18,9 @@ "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'", - "lint:check": "yarn lint:sol-tests && yarn lint:sol-logic && forge fmt --check", - "lint:fix": "sort-package-json && forge fmt && yarn lint:sol-tests --fix && yarn lint:sol-logic --fix", + "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", - "lint:sol-logic": "solhint -c .solhint.json 'src/**/*.sol' 'script/**/*.sol'", - "lint:sol-tests": "solhint -c .solhint.tests.json 'test/**/*.sol'", "prepare": "husky install", "smock": "smock-foundry --contracts src/contracts", "test": "forge test -vvv", @@ -33,8 +31,7 @@ }, "lint-staged": { "*.{js,css,md,ts,sol}": "forge fmt", - "(src|script)/**/*.sol": "yarn lint:sol-logic", - "test/**/*.sol": "yarn lint:sol-tests", + "(src|script|test)/**/*.sol": "yarn lint:check", "package.json": "sort-package-json" }, "dependencies": { @@ -49,7 +46,7 @@ "forge-std": "github:foundry-rs/forge-std#5475f85", "husky": ">=8", "lint-staged": ">=10", - "solhint": "github:solhint-community/solhint-community#v4.0.0-rc01", + "solhint-community": "4.0.0", "sort-package-json": "2.10.0" } -} \ No newline at end of file +} diff --git a/test/.solhint.json b/test/.solhint.json new file mode 100644 index 00000000..aed4ae9b --- /dev/null +++ b/test/.solhint.json @@ -0,0 +1,12 @@ +{ + "rules": { + "max-states-count": "off", + "named-parameters-function": "off", + "no-unused-vars": "off", + "one-contract-per-file": "off", + "ordering": "off", + "private-vars-leading-underscore": ["off"], + "state-visibility": "off", + "style-guide-casing": ["warn", { "ignorePublicFunctions": true,"ignoreStructs": true, "ignoreExternalFunctions": true , "ignoreContracts": true} ] + } +} diff --git a/yarn.lock b/yarn.lock index 5bae79d8..369cab25 100644 --- a/yarn.lock +++ b/yarn.lock @@ -291,14 +291,14 @@ ajv@^6.12.6: uri-js "^4.2.2" ajv@^8.0.1, ajv@^8.11.0: - version "8.12.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1" - integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== + version "8.16.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.16.0.tgz#22e2a92b94f005f7e0f9c9d39652ef0b8f6f0cb4" + integrity sha512-F0twR8U1ZU67JIEtekUcLkXkoO5mMMmgGD8sK/xUFzJ805jxHQl92hImFAqqXMyMYjSPOyUPAwHYhB72g5sTXw== dependencies: - fast-deep-equal "^3.1.1" + fast-deep-equal "^3.1.3" json-schema-traverse "^1.0.0" require-from-string "^2.0.2" - uri-js "^4.2.2" + uri-js "^4.4.1" ansi-escapes@^6.2.0: version "6.2.1" @@ -741,7 +741,7 @@ expand-tilde@^2.0.0, expand-tilde@^2.0.2: dependencies: homedir-polyfill "^1.0.1" -fast-deep-equal@^3.1.1: +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== @@ -763,9 +763,9 @@ fast-glob@3.3.2, fast-glob@^3.3.0: micromatch "^4.0.4" fast-json-stable-stringify@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" - integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I= + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== fastq@^1.6.0: version "1.17.1" @@ -1749,9 +1749,10 @@ solc@0.8.25: semver "^5.5.0" tmp "0.0.33" -"solhint@github:solhint-community/solhint-community#v4.0.0-rc01": - version "4.0.0-rc01" - resolved "https://codeload.github.com/solhint-community/solhint-community/tar.gz/b04088d3d2bb6ceb8c9bf77783e71195d19fb653" +solhint-community@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/solhint-community/-/solhint-community-4.0.0.tgz#4dba66932ff54ced426a8c035b7ceaa13a224f24" + integrity sha512-BERw3qYzkJE64EwvYrp2+iiTN8yAZOJ74FCiL4bTBp7v0JFUvRYCEGZKAqfHcfi/koKkzM6qThsJUceKm9vvfg== dependencies: "@solidity-parser/parser" "^0.16.0" ajv "^6.12.6" @@ -1938,10 +1939,10 @@ universalify@^2.0.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== -uri-js@^4.2.2: - version "4.2.2" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" - integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== +uri-js@^4.2.2, uri-js@^4.4.1: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== dependencies: punycode "^2.1.0" From 0b6938f756713ff781cb005c9ac452d14dc22c63 Mon Sep 17 00:00:00 2001 From: teddy Date: Fri, 7 Jun 2024 05:36:07 -0300 Subject: [PATCH 09/13] feat: deprecate BaseBToken in favour of OZ ERC20 (#67) * chore: add oz dependency * chore: use oz IERC20 wherever possible * feat: use OZ ERC20 implementation * test: unit tests for BToken contract --- .forge-snapshots/swapExactAmountIn.snap | 2 +- package.json | 1 + src/contracts/BFactory.sol | 1 - src/contracts/BPool.sol | 17 +-- src/contracts/BToken.sol | 96 ++------------- src/interfaces/IBPool.sol | 2 +- test/integration/PoolSwap.t.sol | 2 +- test/unit/BFactory.t.sol | 3 +- test/unit/BPool.t.sol | 3 +- test/unit/BToken.t.sol | 154 ++++++++++++++++++++++++ yarn.lock | 63 ++++++---- 11 files changed, 224 insertions(+), 120 deletions(-) create mode 100644 test/unit/BToken.t.sol diff --git a/.forge-snapshots/swapExactAmountIn.snap b/.forge-snapshots/swapExactAmountIn.snap index ae5bdb92..85c66e53 100644 --- a/.forge-snapshots/swapExactAmountIn.snap +++ b/.forge-snapshots/swapExactAmountIn.snap @@ -1 +1 @@ -107629 \ No newline at end of file +107618 \ No newline at end of file diff --git a/package.json b/package.json index 817a9793..f22f3f22 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "package.json": "sort-package-json" }, "dependencies": { + "@openzeppelin/contracts": "5.0.2", "solmate": "github:transmissions11/solmate#c892309" }, "devDependencies": { diff --git a/src/contracts/BFactory.sol b/src/contracts/BFactory.sol index 2b634c2a..09ad4e7c 100644 --- a/src/contracts/BFactory.sol +++ b/src/contracts/BFactory.sol @@ -2,7 +2,6 @@ pragma solidity 0.8.23; // Builds new BPools, logging their addresses and providing `isBPool(address) -> (bool)` - import {BBronze} from './BColor.sol'; import {BPool} from './BPool.sol'; import {IBFactory} from 'interfaces/IBFactory.sol'; diff --git a/src/contracts/BPool.sol b/src/contracts/BPool.sol index a817e405..0c543d4c 100644 --- a/src/contracts/BPool.sol +++ b/src/contracts/BPool.sol @@ -2,8 +2,11 @@ pragma solidity 0.8.23; import {BBronze} from './BColor.sol'; + import {BMath} from './BMath.sol'; -import {BToken, IERC20} from './BToken.sol'; + +import {BToken} from './BToken.sol'; +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import {IBPool} from 'interfaces/IBPool.sol'; contract BPool is BBronze, BToken, BMath, IBPool { @@ -251,7 +254,7 @@ contract BPool is BBronze, BToken, BMath, IBPool { require(tokenAmountIn <= bmul(tokenInBalance, MAX_IN_RATIO), 'ERR_MAX_IN_RATIO'); poolAmountOut = - calcPoolOutGivenSingleIn(tokenInBalance, inRecord.denorm, _totalSupply, _totalWeight, tokenAmountIn, _swapFee); + calcPoolOutGivenSingleIn(tokenInBalance, inRecord.denorm, totalSupply(), _totalWeight, tokenAmountIn, _swapFee); require(poolAmountOut >= minPoolAmountOut, 'ERR_LIMIT_OUT'); emit LOG_JOIN(msg.sender, tokenIn, tokenAmountIn); @@ -275,7 +278,7 @@ contract BPool is BBronze, BToken, BMath, IBPool { uint256 tokenInBalance = IERC20(tokenIn).balanceOf(address(this)); tokenAmountIn = - calcSingleInGivenPoolOut(tokenInBalance, inRecord.denorm, _totalSupply, _totalWeight, poolAmountOut, _swapFee); + calcSingleInGivenPoolOut(tokenInBalance, inRecord.denorm, totalSupply(), _totalWeight, poolAmountOut, _swapFee); require(tokenAmountIn != 0, 'ERR_MATH_APPROX'); require(tokenAmountIn <= maxAmountIn, 'ERR_LIMIT_IN'); @@ -302,7 +305,7 @@ contract BPool is BBronze, BToken, BMath, IBPool { uint256 tokenOutBalance = IERC20(tokenOut).balanceOf(address(this)); tokenAmountOut = - calcSingleOutGivenPoolIn(tokenOutBalance, outRecord.denorm, _totalSupply, _totalWeight, poolAmountIn, _swapFee); + 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'); @@ -332,7 +335,7 @@ contract BPool is BBronze, BToken, BMath, IBPool { require(tokenAmountOut <= bmul(tokenOutBalance, MAX_OUT_RATIO), 'ERR_MAX_OUT_RATIO'); poolAmountIn = - calcPoolInGivenSingleOut(tokenOutBalance, outRecord.denorm, _totalSupply, _totalWeight, tokenAmountOut, _swapFee); + calcPoolInGivenSingleOut(tokenOutBalance, outRecord.denorm, totalSupply(), _totalWeight, tokenAmountOut, _swapFee); require(poolAmountIn != 0, 'ERR_MATH_APPROX'); require(poolAmountIn <= maxPoolAmountIn, 'ERR_LIMIT_IN'); @@ -448,10 +451,10 @@ contract BPool is BBronze, BToken, BMath, IBPool { } function _mintPoolShare(uint256 amount) internal { - _mint(amount); + _mint(address(this), amount); } function _burnPoolShare(uint256 amount) internal { - _burn(amount); + _burn(address(this), amount); } } diff --git a/src/contracts/BToken.sol b/src/contracts/BToken.sol index 31ad2402..98aa9b9b 100644 --- a/src/contracts/BToken.sol +++ b/src/contracts/BToken.sol @@ -2,106 +2,32 @@ pragma solidity 0.8.23; import {BNum} from './BNum.sol'; -import {IERC20} from 'forge-std/interfaces/IERC20.sol'; -abstract contract BTokenBase is BNum, IERC20 { - mapping(address => uint256) internal _balance; - mapping(address => mapping(address => uint256)) internal _allowance; - uint256 internal _totalSupply; +import {ERC20} from '@openzeppelin/contracts/token/ERC20/ERC20.sol'; - function _mint(uint256 amt) internal { - _balance[address(this)] = badd(_balance[address(this)], amt); - _totalSupply = badd(_totalSupply, amt); - emit Transfer(address(0), address(this), amt); - } - - function _burn(uint256 amt) internal { - require(_balance[address(this)] >= amt, 'ERR_INSUFFICIENT_BAL'); - _balance[address(this)] = bsub(_balance[address(this)], amt); - _totalSupply = bsub(_totalSupply, amt); - emit Transfer(address(this), address(0), amt); - } - - function _move(address src, address dst, uint256 amt) internal { - require(_balance[src] >= amt, 'ERR_INSUFFICIENT_BAL'); - _balance[src] = bsub(_balance[src], amt); - _balance[dst] = badd(_balance[dst], amt); - emit Transfer(src, dst, amt); - } - - function _push(address to, uint256 amt) internal { - _move(address(this), to, amt); - } - - function _pull(address from, uint256 amt) internal { - _move(from, address(this), amt); - } -} - -contract BToken is BTokenBase { - string internal _name = 'Balancer Pool Token'; - string internal _symbol = 'BPT'; - uint8 internal _decimals = 18; - - function approve(address dst, uint256 amt) external override returns (bool) { - _allowance[msg.sender][dst] = amt; - emit Approval(msg.sender, dst, amt); - return true; - } +contract BToken is BNum, ERC20 { + constructor() ERC20('Balancer Pool Token', 'BPT') {} function increaseApproval(address dst, uint256 amt) external returns (bool) { - _allowance[msg.sender][dst] = badd(_allowance[msg.sender][dst], amt); - emit Approval(msg.sender, dst, _allowance[msg.sender][dst]); + _approve(msg.sender, dst, allowance(msg.sender, dst) + amt); return true; } function decreaseApproval(address dst, uint256 amt) external returns (bool) { - uint256 oldValue = _allowance[msg.sender][dst]; + uint256 oldValue = allowance(msg.sender, dst); if (amt > oldValue) { - _allowance[msg.sender][dst] = 0; + _approve(msg.sender, dst, 0); } else { - _allowance[msg.sender][dst] = bsub(oldValue, amt); + _approve(msg.sender, dst, oldValue - amt); } - emit Approval(msg.sender, dst, _allowance[msg.sender][dst]); return true; } - function transfer(address dst, uint256 amt) external override returns (bool) { - _move(msg.sender, dst, amt); - return true; - } - - function transferFrom(address src, address dst, uint256 amt) external override returns (bool) { - require(msg.sender == src || amt <= _allowance[src][msg.sender], 'ERR_BTOKEN_BAD_CALLER'); - _move(src, dst, amt); - if (msg.sender != src && _allowance[src][msg.sender] != type(uint256).max) { - _allowance[src][msg.sender] = bsub(_allowance[src][msg.sender], amt); - emit Approval(msg.sender, dst, _allowance[src][msg.sender]); - } - return true; - } - - function allowance(address src, address dst) external view override returns (uint256) { - return _allowance[src][dst]; - } - - function balanceOf(address whom) external view override returns (uint256) { - return _balance[whom]; - } - - function totalSupply() public view override returns (uint256) { - return _totalSupply; - } - - function name() public view returns (string memory) { - return _name; - } - - function symbol() public view returns (string memory) { - return _symbol; + function _push(address to, uint256 amt) internal virtual { + _transfer(address(this), to, amt); } - function decimals() public view returns (uint8) { - return _decimals; + function _pull(address from, uint256 amt) internal virtual { + _transfer(from, address(this), amt); } } diff --git a/src/interfaces/IBPool.sol b/src/interfaces/IBPool.sol index f4167684..f06e6fbb 100644 --- a/src/interfaces/IBPool.sol +++ b/src/interfaces/IBPool.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.23; -import {IERC20} from 'forge-std/interfaces/IERC20.sol'; +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; interface IBPool is IERC20 { struct Record { diff --git a/test/integration/PoolSwap.t.sol b/test/integration/PoolSwap.t.sol index 786bd726..46150b3f 100644 --- a/test/integration/PoolSwap.t.sol +++ b/test/integration/PoolSwap.t.sol @@ -1,9 +1,9 @@ pragma solidity 0.8.23; +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import {Test} from 'forge-std/Test.sol'; import {BFactory} from 'contracts/BFactory.sol'; -import {IERC20} from 'contracts/BToken.sol'; import {IBPool} from 'interfaces/IBPool.sol'; import {GasSnapshot} from 'forge-gas-snapshot/GasSnapshot.sol'; diff --git a/test/unit/BFactory.t.sol b/test/unit/BFactory.t.sol index 7c8c71cd..bbd262ac 100644 --- a/test/unit/BFactory.t.sol +++ b/test/unit/BFactory.t.sol @@ -1,9 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; +import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; + import {BFactory} from 'contracts/BFactory.sol'; import {BPool} from 'contracts/BPool.sol'; -import {IERC20} from 'contracts/BToken.sol'; import {Test} from 'forge-std/Test.sol'; import {IBFactory} from 'interfaces/IBFactory.sol'; import {IBPool} from 'interfaces/IBPool.sol'; diff --git a/test/unit/BPool.t.sol b/test/unit/BPool.t.sol index 14fc0f58..4f260277 100644 --- a/test/unit/BPool.t.sol +++ b/test/unit/BPool.t.sol @@ -1,13 +1,14 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; +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 {BConst} from 'contracts/BConst.sol'; import {BMath} from 'contracts/BMath.sol'; -import {IERC20} from 'contracts/BToken.sol'; import {Test} from 'forge-std/Test.sol'; import {Pow} from 'test/utils/Pow.sol'; import {Utils} from 'test/utils/Utils.sol'; diff --git a/test/unit/BToken.t.sol b/test/unit/BToken.t.sol new file mode 100644 index 00000000..2d745f9e --- /dev/null +++ b/test/unit/BToken.t.sol @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: GPL-3 +pragma solidity ^0.8.23; + +import {IERC20Errors} from '@openzeppelin/contracts/interfaces/draft-IERC6093.sol'; +import {Test} from 'forge-std/Test.sol'; +import {MockBToken} from 'test/smock/MockBToken.sol'; + +contract BToken_Unit_Constructor is Test { + function test_ConstructorParams() public { + MockBToken btoken = new MockBToken(); + assertEq(btoken.name(), 'Balancer Pool Token'); + assertEq(btoken.symbol(), 'BPT'); + assertEq(btoken.decimals(), 18); + } +} + +abstract contract BToken_Unit_base is Test { + MockBToken internal bToken; + + modifier assumeNonZeroAddresses(address addr1, address addr2) { + vm.assume(addr1 != address(0)); + vm.assume(addr2 != address(0)); + _; + } + + modifier assumeNonZeroAddress(address addr) { + vm.assume(addr != address(0)); + _; + } + + function setUp() public virtual { + bToken = new MockBToken(); + } +} + +contract BToken_Unit_IncreaseApproval is BToken_Unit_base { + function test_increasesApprovalFromZero( + address sender, + address spender, + uint256 amount + ) public assumeNonZeroAddresses(sender, spender) { + vm.prank(sender); + bToken.increaseApproval(spender, amount); + assertEq(bToken.allowance(sender, spender), amount); + } + + function test_increasesApprovalFromNonZero( + address sender, + address spender, + uint128 existingAllowance, + uint128 amount + ) public assumeNonZeroAddresses(sender, spender) { + vm.assume(existingAllowance > 0); + vm.startPrank(sender); + bToken.approve(spender, existingAllowance); + bToken.increaseApproval(spender, amount); + vm.stopPrank(); + assertEq(bToken.allowance(sender, spender), uint256(amount) + existingAllowance); + } +} + +contract BToken_Unit_DecreaseApproval is BToken_Unit_base { + function test_decreaseApprovalToNonZero( + address sender, + address spender, + uint256 existingAllowance, + uint256 amount + ) public assumeNonZeroAddresses(sender, spender) { + existingAllowance = bound(existingAllowance, 1, type(uint256).max); + amount = bound(amount, 0, existingAllowance - 1); + vm.startPrank(sender); + bToken.approve(spender, existingAllowance); + bToken.decreaseApproval(spender, amount); + vm.stopPrank(); + assertEq(bToken.allowance(sender, spender), existingAllowance - amount); + } + + function test_decreaseApprovalToZero( + address sender, + address spender, + uint256 existingAllowance, + uint256 amount + ) public assumeNonZeroAddresses(sender, spender) { + amount = bound(amount, existingAllowance, type(uint256).max); + vm.startPrank(sender); + bToken.approve(spender, existingAllowance); + bToken.decreaseApproval(spender, amount); + vm.stopPrank(); + assertEq(bToken.allowance(sender, spender), 0); + } +} + +contract BToken_Unit__push is BToken_Unit_base { + function test_revertsOnInsufficientSelfBalance( + address to, + uint128 existingBalance, + uint128 offset + ) public assumeNonZeroAddress(to) { + vm.assume(offset > 1); + deal(address(bToken), address(bToken), existingBalance); + vm.expectRevert( + abi.encodeWithSelector( + IERC20Errors.ERC20InsufficientBalance.selector, + address(bToken), + existingBalance, + uint256(existingBalance) + offset + ) + ); + bToken.call__push(to, uint256(existingBalance) + offset); + } + + function test_sendsTokens( + address to, + uint128 existingBalance, + uint256 transferAmount + ) public assumeNonZeroAddress(to) { + vm.assume(to != address(bToken)); + transferAmount = bound(transferAmount, 0, existingBalance); + deal(address(bToken), address(bToken), existingBalance); + bToken.call__push(to, transferAmount); + assertEq(bToken.balanceOf(to), transferAmount); + assertEq(bToken.balanceOf(address(bToken)), existingBalance - transferAmount); + } +} + +contract BToken_Unit__pull is BToken_Unit_base { + function test_revertsOnInsufficientFromBalance( + address from, + uint128 existingBalance, + uint128 offset + ) public assumeNonZeroAddress(from) { + vm.assume(offset > 1); + deal(address(bToken), from, existingBalance); + vm.expectRevert( + abi.encodeWithSelector( + IERC20Errors.ERC20InsufficientBalance.selector, from, existingBalance, uint256(existingBalance) + offset + ) + ); + bToken.call__pull(from, uint256(existingBalance) + offset); + } + + function test_getsTokens( + address from, + uint128 existingBalance, + uint256 transferAmount + ) public assumeNonZeroAddress(from) { + vm.assume(from != address(bToken)); + transferAmount = bound(transferAmount, 0, existingBalance); + deal(address(bToken), address(from), existingBalance); + bToken.call__pull(from, transferAmount); + assertEq(bToken.balanceOf(address(bToken)), transferAmount); + assertEq(bToken.balanceOf(from), existingBalance - transferAmount); + } +} diff --git a/yarn.lock b/yarn.lock index 369cab25..362a5ade 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3,20 +3,27 @@ "@babel/code-frame@^7.0.0": - version "7.5.5" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.5.5.tgz#bc0782f6d69f7b7d49531219699b988f669a8f9d" - integrity sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw== + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.7.tgz#882fd9e09e8ee324e496bd040401c6f046ef4465" + integrity sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA== dependencies: - "@babel/highlight" "^7.0.0" + "@babel/highlight" "^7.24.7" + picocolors "^1.0.0" -"@babel/highlight@^7.0.0": - version "7.5.0" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.5.0.tgz#56d11312bd9248fa619591d02472be6e8cb32540" - integrity sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ== +"@babel/helper-validator-identifier@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz#75b889cfaf9e35c2aaf42cf0d72c8e91719251db" + integrity sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w== + +"@babel/highlight@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.24.7.tgz#a05ab1df134b286558aae0ed41e6c5f731bf409d" + integrity sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw== dependencies: - chalk "^2.0.0" - esutils "^2.0.2" + "@babel/helper-validator-identifier" "^7.24.7" + chalk "^2.4.2" js-tokens "^4.0.0" + picocolors "^1.0.0" "@commitlint/cli@19.3.0": version "19.3.0" @@ -226,6 +233,11 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@openzeppelin/contracts@5.0.2": + version "5.0.2" + resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-5.0.2.tgz#b1d03075e49290d06570b2fd42154d76c2a5d210" + integrity sha512-ytPc6eLGcHHnapAZ9S+5qsdomhjo6QBHTDRRBFfTxXIpsicMhVPouPgmUPebZZZGX7vt9USA+Z+0M0dSVtSUEA== + "@scure/base@~1.1.4": version "1.1.6" resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.6.tgz#8ce5d304b436e4c84f896e0550c83e4d88cb917d" @@ -263,9 +275,11 @@ "@types/node" "*" "@types/node@*": - version "12.12.3" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.3.tgz#ebfe83507ac506bc3486314a8aa395be66af8d23" - integrity sha512-opgSsy+cEF9N8MgaVPnWVtdJ3o4mV2aMHvDq7thkQUFt0EuOHJon4rQpJfhjmNHB+ikl0Cd6WhWIErOyQ+f7tw== + version "20.14.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.2.tgz#a5f4d2bcb4b6a87bffcaa717718c5a0f208f4a18" + integrity sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q== + dependencies: + undici-types "~5.26.4" JSONStream@^1.3.5: version "1.3.5" @@ -430,7 +444,7 @@ chalk@5.3.0, chalk@^5.3.0: resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385" integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w== -chalk@^2.0.0: +chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -699,11 +713,6 @@ escape-string-regexp@^1.0.5: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= -esutils@^2.0.2: - version "2.0.3" - resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" - integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== - ethereum-cryptography@^2.0.0: version "2.1.3" resolved "https://registry.yarnpkg.com/ethereum-cryptography/-/ethereum-cryptography-2.1.3.tgz#1352270ed3b339fe25af5ceeadcf1b9c8e30768a" @@ -1183,9 +1192,9 @@ isexe@^2.0.0: integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= jiti@^1.19.1: - version "1.21.0" - resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.0.tgz#7c97f8fe045724e136a397f7340475244156105d" - integrity sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q== + version "1.21.3" + resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.3.tgz#b2adb07489d7629b344d59082bbedb8c21c5f755" + integrity sha512-uy2bNX5zQ+tESe+TiC7ilGRz8AtRGmnJH55NC5S0nSUjvvvM2hJHmefHErugGXN4pNv4Qx7vLsnNw9qJ9mtIsw== js-sha3@0.8.0: version "0.8.0" @@ -1517,6 +1526,11 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== +picocolors@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" + integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew== + picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" @@ -1929,6 +1943,11 @@ uglify-js@^3.1.4: resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.17.4.tgz#61678cf5fa3f5b7eb789bb345df29afb8257c22c" integrity sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g== +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + unicorn-magic@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/unicorn-magic/-/unicorn-magic-0.1.0.tgz#1bb9a51c823aaf9d73a8bfcd3d1a23dde94b0ce4" From 49de45051a0bae7cc4ffa39352273e011ff4e8ec Mon Sep 17 00:00:00 2001 From: teddy Date: Fri, 7 Jun 2024 05:47:54 -0300 Subject: [PATCH 10/13] chore: deprecate bcolor contract (#69) * chore: remove BColor and BBronze * chore: re-enable one-contract-per-file --- .solhint.json | 4 ++-- src/contracts/BColor.sol | 12 ------------ src/contracts/BConst.sol | 4 +--- src/contracts/BFactory.sol | 3 +-- src/contracts/BMath.sol | 3 +-- src/contracts/BPool.sol | 4 +--- 6 files changed, 6 insertions(+), 24 deletions(-) delete mode 100644 src/contracts/BColor.sol diff --git a/.solhint.json b/.solhint.json index 4a803897..0b571399 100644 --- a/.solhint.json +++ b/.solhint.json @@ -7,9 +7,9 @@ "ordering": "warn", "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", - "one-contract-per-file": "off" + "custom-errors": "off" } } diff --git a/src/contracts/BColor.sol b/src/contracts/BColor.sol deleted file mode 100644 index 57d4c0e1..00000000 --- a/src/contracts/BColor.sol +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.23; - -abstract contract BColor { - function getColor() external view virtual returns (bytes32); -} - -contract BBronze is BColor { - function getColor() external pure override returns (bytes32) { - return bytes32('BRONZE'); - } -} diff --git a/src/contracts/BConst.sol b/src/contracts/BConst.sol index ade59b97..9f2c3ea4 100644 --- a/src/contracts/BConst.sol +++ b/src/contracts/BConst.sol @@ -1,9 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.23; -import {BBronze} from './BColor.sol'; - -contract BConst is BBronze { +contract BConst { uint256 public constant BONE = 10 ** 18; uint256 public constant MIN_BOUND_TOKENS = 2; diff --git a/src/contracts/BFactory.sol b/src/contracts/BFactory.sol index 09ad4e7c..b54ab2ba 100644 --- a/src/contracts/BFactory.sol +++ b/src/contracts/BFactory.sol @@ -2,12 +2,11 @@ pragma solidity 0.8.23; // Builds new BPools, logging their addresses and providing `isBPool(address) -> (bool)` -import {BBronze} from './BColor.sol'; import {BPool} from './BPool.sol'; import {IBFactory} from 'interfaces/IBFactory.sol'; import {IBPool} from 'interfaces/IBPool.sol'; -contract BFactory is BBronze, IBFactory { +contract BFactory is IBFactory { mapping(address => bool) internal _isBPool; address internal _blabs; diff --git a/src/contracts/BMath.sol b/src/contracts/BMath.sol index b8ec5b63..d6579e4c 100644 --- a/src/contracts/BMath.sol +++ b/src/contracts/BMath.sol @@ -1,11 +1,10 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.23; -import {BBronze} from './BColor.sol'; import {BConst} from './BConst.sol'; import {BNum} from './BNum.sol'; -contract BMath is BBronze, BConst, BNum { +contract BMath is BConst, BNum { /** * * calcSpotPrice diff --git a/src/contracts/BPool.sol b/src/contracts/BPool.sol index 0c543d4c..47b9169b 100644 --- a/src/contracts/BPool.sol +++ b/src/contracts/BPool.sol @@ -1,15 +1,13 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.23; -import {BBronze} from './BColor.sol'; - import {BMath} from './BMath.sol'; import {BToken} from './BToken.sol'; import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import {IBPool} from 'interfaces/IBPool.sol'; -contract BPool is BBronze, BToken, BMath, IBPool { +contract BPool is BToken, BMath, IBPool { bool internal _mutex; address internal _factory; // BFactory address to push token exitFee to From 0e518dfbb0916c061a8849378633eed6523967eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wei=C3=9Fer=20Hase?= Date: Fri, 7 Jun 2024 15:33:13 +0200 Subject: [PATCH 11/13] feat: adding deployment snapshots for factory and bPool (#63) * feat: adding deployment snapshots for factory and bPool * chore: new snapshots after merging dev * refactor: renaming to DeploymentGas --- .forge-snapshots/newBFactory.snap | 1 + .forge-snapshots/newBPool.snap | 1 + test/integration/DeploymentGas.t.sol | 27 +++++++++++++++++++++++++++ 3 files changed, 29 insertions(+) create mode 100644 .forge-snapshots/newBFactory.snap create mode 100644 .forge-snapshots/newBPool.snap create mode 100644 test/integration/DeploymentGas.t.sol diff --git a/.forge-snapshots/newBFactory.snap b/.forge-snapshots/newBFactory.snap new file mode 100644 index 00000000..dd9f6517 --- /dev/null +++ b/.forge-snapshots/newBFactory.snap @@ -0,0 +1 @@ +3811528 \ No newline at end of file diff --git a/.forge-snapshots/newBPool.snap b/.forge-snapshots/newBPool.snap new file mode 100644 index 00000000..9d12a22f --- /dev/null +++ b/.forge-snapshots/newBPool.snap @@ -0,0 +1 @@ +3567680 \ No newline at end of file diff --git a/test/integration/DeploymentGas.t.sol b/test/integration/DeploymentGas.t.sol new file mode 100644 index 00000000..992213cd --- /dev/null +++ b/test/integration/DeploymentGas.t.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import {BFactory} from 'contracts/BFactory.sol'; + +import {GasSnapshot} from 'forge-gas-snapshot/GasSnapshot.sol'; +import {Test} from 'forge-std/Test.sol'; + +contract DeploymentGasTest is Test, GasSnapshot { + BFactory public factory; + + function setUp() public { + factory = new BFactory(); + } + + function testFactoryDeployment() public { + snapStart('newBFactory'); + new BFactory(); + snapEnd(); + } + + function testDeployment() public { + snapStart('newBPool'); + factory.newBPool(); + snapEnd(); + } +} From cb23dbfe20171a67b1faa6f90207ba2cf8d6cd33 Mon Sep 17 00:00:00 2001 From: teddy Date: Fri, 7 Jun 2024 12:19:35 -0300 Subject: [PATCH 12/13] chore: bump solidity version to 0.8.25 (#70) --- foundry.toml | 2 +- script/Deploy.s.sol | 2 +- script/Params.s.sol | 2 +- src/contracts/BConst.sol | 2 +- src/contracts/BFactory.sol | 2 +- src/contracts/BMath.sol | 2 +- src/contracts/BNum.sol | 2 +- src/contracts/BPool.sol | 2 +- src/contracts/BToken.sol | 2 +- src/interfaces/IBFactory.sol | 2 +- src/interfaces/IBPool.sol | 2 +- test/integration/DeploymentGas.t.sol | 2 +- test/integration/PoolSwap.t.sol | 2 +- test/unit/BFactory.t.sol | 2 +- test/unit/BPool.t.sol | 2 +- test/unit/BToken.t.sol | 2 +- test/utils/Pow.sol | 2 +- test/utils/Utils.sol | 2 +- 18 files changed, 18 insertions(+), 18 deletions(-) diff --git a/foundry.toml b/foundry.toml index 046a7d75..a38fc11a 100644 --- a/foundry.toml +++ b/foundry.toml @@ -9,7 +9,7 @@ multiline_func_header = 'params_first' sort_imports = true [profile.default] -solc_version = '0.8.23' +solc_version = '0.8.25' libs = ["node_modules", "lib"] optimizer_runs = 50 # TODO: increase for production and add via-ir ffi = true diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index 33bf6eaf..608af6bc 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.25; import {BFactory} from 'contracts/BFactory.sol'; import {Params} from 'script/Params.s.sol'; diff --git a/script/Params.s.sol b/script/Params.s.sol index c943cf24..15a5aaa6 100644 --- a/script/Params.s.sol +++ b/script/Params.s.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.25; contract Params { struct DeploymentParams { diff --git a/src/contracts/BConst.sol b/src/contracts/BConst.sol index 9f2c3ea4..1936aec6 100644 --- a/src/contracts/BConst.sol +++ b/src/contracts/BConst.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.23; +pragma solidity 0.8.25; contract BConst { uint256 public constant BONE = 10 ** 18; diff --git a/src/contracts/BFactory.sol b/src/contracts/BFactory.sol index b54ab2ba..8036a9d7 100644 --- a/src/contracts/BFactory.sol +++ b/src/contracts/BFactory.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.23; +pragma solidity 0.8.25; // Builds new BPools, logging their addresses and providing `isBPool(address) -> (bool)` import {BPool} from './BPool.sol'; diff --git a/src/contracts/BMath.sol b/src/contracts/BMath.sol index d6579e4c..0bb625db 100644 --- a/src/contracts/BMath.sol +++ b/src/contracts/BMath.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.23; +pragma solidity 0.8.25; import {BConst} from './BConst.sol'; import {BNum} from './BNum.sol'; diff --git a/src/contracts/BNum.sol b/src/contracts/BNum.sol index b2cfe6fd..040ac892 100644 --- a/src/contracts/BNum.sol +++ b/src/contracts/BNum.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.23; +pragma solidity 0.8.25; import {BConst} from './BConst.sol'; diff --git a/src/contracts/BPool.sol b/src/contracts/BPool.sol index 47b9169b..660118af 100644 --- a/src/contracts/BPool.sol +++ b/src/contracts/BPool.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.23; +pragma solidity 0.8.25; import {BMath} from './BMath.sol'; diff --git a/src/contracts/BToken.sol b/src/contracts/BToken.sol index 98aa9b9b..884c335e 100644 --- a/src/contracts/BToken.sol +++ b/src/contracts/BToken.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.23; +pragma solidity 0.8.25; import {BNum} from './BNum.sol'; diff --git a/src/interfaces/IBFactory.sol b/src/interfaces/IBFactory.sol index 3507aff0..d07a3e6d 100644 --- a/src/interfaces/IBFactory.sol +++ b/src/interfaces/IBFactory.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.23; +pragma solidity 0.8.25; import {IBPool} from 'interfaces/IBPool.sol'; diff --git a/src/interfaces/IBPool.sol b/src/interfaces/IBPool.sol index f06e6fbb..c111948e 100644 --- a/src/interfaces/IBPool.sol +++ b/src/interfaces/IBPool.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity 0.8.23; +pragma solidity 0.8.25; import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; diff --git a/test/integration/DeploymentGas.t.sol b/test/integration/DeploymentGas.t.sol index 992213cd..5bc4715c 100644 --- a/test/integration/DeploymentGas.t.sol +++ b/test/integration/DeploymentGas.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.25; import {BFactory} from 'contracts/BFactory.sol'; diff --git a/test/integration/PoolSwap.t.sol b/test/integration/PoolSwap.t.sol index 46150b3f..016b4d7c 100644 --- a/test/integration/PoolSwap.t.sol +++ b/test/integration/PoolSwap.t.sol @@ -1,4 +1,4 @@ -pragma solidity 0.8.23; +pragma solidity 0.8.25; import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import {Test} from 'forge-std/Test.sol'; diff --git a/test/unit/BFactory.t.sol b/test/unit/BFactory.t.sol index bbd262ac..0914bcec 100644 --- a/test/unit/BFactory.t.sol +++ b/test/unit/BFactory.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.25; import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; diff --git a/test/unit/BPool.t.sol b/test/unit/BPool.t.sol index 4f260277..0ea4f884 100644 --- a/test/unit/BPool.t.sol +++ b/test/unit/BPool.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.25; import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; diff --git a/test/unit/BToken.t.sol b/test/unit/BToken.t.sol index 2d745f9e..ac16f760 100644 --- a/test/unit/BToken.t.sol +++ b/test/unit/BToken.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3 -pragma solidity ^0.8.23; +pragma solidity ^0.8.25; import {IERC20Errors} from '@openzeppelin/contracts/interfaces/draft-IERC6093.sol'; import {Test} from 'forge-std/Test.sol'; diff --git a/test/utils/Pow.sol b/test/utils/Pow.sol index b8f15d85..e9142c57 100644 --- a/test/utils/Pow.sol +++ b/test/utils/Pow.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.25; import {BNum} from 'contracts/BNum.sol'; diff --git a/test/utils/Utils.sol b/test/utils/Utils.sol index db195352..19225f9d 100644 --- a/test/utils/Utils.sol +++ b/test/utils/Utils.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.25; import {Test} from 'forge-std/Test.sol'; import {LibString} from 'solmate/utils/LibString.sol'; From 45d29b10f28e2fcea487e0cc7c2ee3d7814fa066 Mon Sep 17 00:00:00 2001 From: Austrian <114922365+0xAustrian@users.noreply.github.com> Date: Fri, 7 Jun 2024 12:55:00 -0300 Subject: [PATCH 13/13] feat: adding natspec (#64) * feat: add interfaces for bPool * feat: add interfaces for bFactory * fix: parameter names in events from IBPool.sol * feat: add natspec for IBFactory * feat: add natspec for IBPool.sol * fix: revert unnecessary change * feat: add inheritdoc tag * feat: add natspec for BMath.sol * feat: inheritdoc in one line * feat: improve bMath natspec * feat: improve bPool natspec * feat: improve IBFactory natspec * feat: improve IBPool natspec * feat: improve natspec * fix: notice line in BMath * feat: clarify price denomination in calcSpotPrice * fix: wrong path in natspec smells config * fix: missing natspec --- natspec-smells.config.js | 2 +- src/contracts/BFactory.sol | 12 ++- src/contracts/BMath.sol | 180 +++++++++++++++++++++-------------- src/contracts/BPool.sol | 85 ++++++++++++++--- src/contracts/BToken.sol | 26 ++++- src/interfaces/IBFactory.sol | 31 ++++++ src/interfaces/IBPool.sol | 177 +++++++++++++++++++++++++++++++++- 7 files changed, 422 insertions(+), 91 deletions(-) diff --git a/natspec-smells.config.js b/natspec-smells.config.js index 458623a3..54baf0c0 100644 --- a/natspec-smells.config.js +++ b/natspec-smells.config.js @@ -4,5 +4,5 @@ /** @type {import('@defi-wonderland/natspec-smells').Config} */ module.exports = { - include: 'src' + include: 'src/**/*.sol' }; diff --git a/src/contracts/BFactory.sol b/src/contracts/BFactory.sol index 8036a9d7..8929c8c6 100644 --- a/src/contracts/BFactory.sol +++ b/src/contracts/BFactory.sol @@ -1,19 +1,25 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.25; -// Builds new BPools, logging their addresses and providing `isBPool(address) -> (bool)` import {BPool} from './BPool.sol'; import {IBFactory} from 'interfaces/IBFactory.sol'; import {IBPool} from 'interfaces/IBPool.sol'; +/** + * @title BFactory + * @notice Creates new BPools, logging their addresses and acting as a registry of pools. + */ contract BFactory is IBFactory { + /// @dev Mapping indicating whether the address is a BPool. mapping(address => bool) internal _isBPool; + /// @dev bLabs address. address internal _blabs; constructor() { _blabs = msg.sender; } + /// @inheritdoc IBFactory function newBPool() external returns (IBPool _pool) { IBPool bpool = new BPool(); _isBPool[address(bpool)] = true; @@ -22,12 +28,14 @@ contract BFactory is IBFactory { return bpool; } + /// @inheritdoc IBFactory function setBLabs(address b) external { require(msg.sender == _blabs, 'ERR_NOT_BLABS'); emit LOG_BLABS(msg.sender, b); _blabs = b; } + /// @inheritdoc IBFactory function collect(IBPool pool) external { require(msg.sender == _blabs, 'ERR_NOT_BLABS'); uint256 collected = pool.balanceOf(address(this)); @@ -35,10 +43,12 @@ contract BFactory is IBFactory { require(xfer, 'ERR_ERC20_FAILED'); } + /// @inheritdoc IBFactory function isBPool(address b) external view returns (bool) { return _isBPool[b]; } + /// @inheritdoc IBFactory function getBLabs() external view returns (address) { return _blabs; } diff --git a/src/contracts/BMath.sol b/src/contracts/BMath.sol index 0bb625db..9e670d56 100644 --- a/src/contracts/BMath.sol +++ b/src/contracts/BMath.sol @@ -6,15 +6,22 @@ import {BNum} from './BNum.sol'; contract BMath is BConst, BNum { /** - * - * calcSpotPrice - * sP = spotPrice - * bI = tokenBalanceIn ( bI / wI ) 1 - * bO = tokenBalanceOut sP = ----------- * ---------- - * wI = tokenWeightIn ( bO / wO ) ( 1 - sF ) - * wO = tokenWeightOut - * sF = swapFee - * + * @notice Calculate the spot price of a token in terms of another one + * @dev The price denomination depends on the decimals of the tokens. + * @dev To obtain the price with 18 decimals the next formula should be applied to the result + * @dev spotPrice = spotPrice ÷ (10^tokenInDecimals) × (10^tokenOutDecimals) + * @param tokenBalanceIn The balance of the input token in the pool + * @param tokenWeightIn The weight of the input token in the pool + * @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 + * @dev Formula: + * sP = spotPrice + * bI = tokenBalanceIn ( bI / wI ) 1 + * bO = tokenBalanceOut sP = ----------- * ---------- + * wI = tokenWeightIn ( bO / wO ) ( 1 - sF ) + * wO = tokenWeightOut + * sF = swapFee */ function calcSpotPrice( uint256 tokenBalanceIn, @@ -31,16 +38,21 @@ contract BMath is BConst, BNum { } /** - * - * calcOutGivenIn - * aO = tokenAmountOut - * bO = tokenBalanceOut - * bI = tokenBalanceIn / / bI \ (wI / wO) \ - * aI = tokenAmountIn aO = bO * | 1 - | -------------------------- | ^ | - * wI = tokenWeightIn \ \ ( bI + ( aI * ( 1 - sF )) / / - * wO = tokenWeightOut - * sF = swapFee - * + * @notice Calculate the amount of token out given the amount of token in for a swap + * @param tokenBalanceIn The balance of the input token in the pool + * @param tokenWeightIn The weight of the input token in the pool + * @param tokenBalanceOut The balance of the output token in the pool + * @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 + * @dev Formula: + * aO = tokenAmountOut + * bO = tokenBalanceOut + * bI = tokenBalanceIn / / bI \ (wI / wO) \ + * aI = tokenAmountIn aO = bO * | 1 - | -------------------------- | ^ | + * wI = tokenWeightIn \ \ ( bI + ( aI * ( 1 - sF )) / / + * wO = tokenWeightOut + * sF = swapFee */ function calcOutGivenIn( uint256 tokenBalanceIn, @@ -61,16 +73,21 @@ contract BMath is BConst, BNum { } /** - * - * calcInGivenOut - * aI = tokenAmountIn - * bO = tokenBalanceOut / / bO \ (wO / wI) \ - * bI = tokenBalanceIn bI * | | ------------ | ^ - 1 | - * aO = tokenAmountOut aI = \ \ ( bO - aO ) / / - * wI = tokenWeightIn -------------------------------------------- - * wO = tokenWeightOut ( 1 - sF ) - * sF = swapFee - * + * @notice Calculate the amount of token in given the amount of token out for a swap + * @param tokenBalanceIn The balance of the input token in the pool + * @param tokenWeightIn The weight of the input token in the pool + * @param tokenBalanceOut The balance of the output token in the pool + * @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 + * @dev Formula: + * aI = tokenAmountIn + * bO = tokenBalanceOut / / bO \ (wO / wI) \ + * bI = tokenBalanceIn bI * | | ------------ | ^ - 1 | + * aO = tokenAmountOut aI = \ \ ( bO - aO ) / / + * wI = tokenWeightIn -------------------------------------------- + * wO = tokenWeightOut ( 1 - sF ) + * sF = swapFee */ function calcInGivenOut( uint256 tokenBalanceIn, @@ -91,16 +108,22 @@ contract BMath is BConst, BNum { } /** - * - * calcPoolOutGivenSingleIn - * pAo = poolAmountOut / \ - * tAi = tokenAmountIn /// / // wI \ \\ \ wI \ - * wI = tokenWeightIn //| tAi *| 1 - || 1 - -- | * sF || + tBi \ -- \ - * tW = totalWeight pAo=|| \ \ \\ tW / // | ^ tW | * pS - pS - * tBi = tokenBalanceIn \\ ------------------------------------- / / - * pS = poolSupply \\ tBi / / - * sF = swapFee \ / - * + * @notice Calculate the amount of pool tokens that should be minted, + * given a single token in when joining a pool + * @param tokenBalanceIn The balance of the input token in the pool + * @param tokenWeightIn The weight of the input token in the pool + * @param poolSupply The total supply of the pool tokens + * @param totalWeight The total weight of the pool + * @param tokenAmountIn The amount of the input token + * @param swapFee The swap fee of the pool + * @dev Formula: + * pAo = poolAmountOut / \ + * tAi = tokenAmountIn /// / // wI \ \\ \ wI \ + * wI = tokenWeightIn //| tAi *| 1 - || 1 - -- | * sF || + tBi \ -- \ + * tW = totalWeight pAo=|| \ \ \\ tW / // | ^ tW | * pS - pS + * tBi = tokenBalanceIn \\ ------------------------------------- / / + * pS = poolSupply \\ tBi / / + * sF = swapFee \ / */ function calcPoolOutGivenSingleIn( uint256 tokenBalanceIn, @@ -129,16 +152,21 @@ contract BMath is BConst, BNum { } /** - * - * calcSingleInGivenPoolOut - * tAi = tokenAmountIn //(pS + pAo)\ / 1 \\ - * pS = poolSupply || --------- | ^ | --------- || * bI - bI - * pAo = poolAmountOut \\ pS / \(wI / tW)// - * bI = balanceIn tAi = -------------------------------------------- - * wI = weightIn / wI \ - * tW = totalWeight | 1 - ---- | * sF - * sF = swapFee \ tW / - * + * @notice Given amount of pool tokens out, calculate the amount of tokens in that should be sent + * @param tokenBalanceIn The balance of the input token in the pool + * @param tokenWeightIn The weight of the input token in the pool + * @param poolSupply The current total supply + * @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 + * @dev Formula: + * tAi = tokenAmountIn //(pS + pAo)\ / 1 \\ + * pS = poolSupply || --------- | ^ | --------- || * bI - bI + * pAo = poolAmountOut \\ pS / \(wI / tW)// + * bI = balanceIn tAi = -------------------------------------------- + * wI = weightIn / wI \ + * tW = totalWeight | 1 - ---- | * sF + * sF = swapFee \ tW / */ function calcSingleInGivenPoolOut( uint256 tokenBalanceIn, @@ -166,17 +194,22 @@ contract BMath is BConst, BNum { } /** - * - * calcSingleOutGivenPoolIn - * tAo = tokenAmountOut / / \\ - * bO = tokenBalanceOut / // pS - (pAi * (1 - eF)) \ / 1 \ \\ - * pAi = poolAmountIn | bO - || ----------------------- | ^ | --------- | * b0 || - * ps = poolSupply \ \\ pS / \(wO / tW)/ // - * wI = tokenWeightIn tAo = \ \ // - * tW = totalWeight / / wO \ \ - * sF = swapFee * | 1 - | 1 - ---- | * sF | - * eF = exitFee \ \ tW / / - * + * @notice Calculate the amount of token out given the amount of pool tokens in + * @param tokenBalanceOut The balance of the output token in the pool + * @param tokenWeightOut The weight of the output token in the pool + * @param poolSupply The total supply of the pool tokens + * @param totalWeight The total weight of the pool + * @param poolAmountIn The amount of pool tokens + * @param swapFee The swap fee of the pool + * @dev Formula: + * tAo = tokenAmountOut / / \\ + * bO = tokenBalanceOut / // pS - (pAi * (1 - eF)) \ / 1 \ \\ + * pAi = poolAmountIn | bO - || ----------------------- | ^ | --------- | * b0 || + * ps = poolSupply \ \\ pS / \(wO / tW)/ // + * wI = tokenWeightIn tAo = \ \ // + * tW = totalWeight / / wO \ \ + * sF = swapFee * | 1 - | 1 - ---- | * sF | + * eF = exitFee \ \ tW / / */ function calcSingleOutGivenPoolIn( uint256 tokenBalanceOut, @@ -207,17 +240,22 @@ contract BMath is BConst, BNum { } /** - * - * calcPoolInGivenSingleOut - * pAi = poolAmountIn // / tAo \\ / wO \ \ - * bO = tokenBalanceOut // | bO - -------------------------- |\ | ---- | \ - * tAo = tokenAmountOut pS - || \ 1 - ((1 - (tO / tW)) * sF)/ | ^ \ tW / * pS | - * ps = poolSupply \\ -----------------------------------/ / - * wO = tokenWeightOut pAi = \\ bO / / - * tW = totalWeight ------------------------------------------------------------- - * sF = swapFee ( 1 - eF ) - * eF = exitFee - * + * @notice Calculate the amount of pool tokens in given an amount of single token out + * @param tokenBalanceOut The balance of the output token in the pool + * @param tokenWeightOut The weight of the output token in the pool + * @param poolSupply The total supply of the pool tokens + * @param totalWeight The total weight of the pool + * @param tokenAmountOut The amount of the output token + * @param swapFee The swap fee of the pool + * @dev Formula: + * pAi = poolAmountIn // / tAo \\ / wO \ \ + * bO = tokenBalanceOut // | bO - -------------------------- |\ | ---- | \ + * tAo = tokenAmountOut pS - || \ 1 - ((1 - (tO / tW)) * sF)/ | ^ \ tW / * pS | + * ps = poolSupply \\ -----------------------------------/ / + * wO = tokenWeightOut pAi = \\ bO / / + * tW = totalWeight ------------------------------------------------------------- + * sF = swapFee ( 1 - eF ) + * eF = exitFee */ function calcPoolInGivenSingleOut( uint256 tokenBalanceOut, diff --git a/src/contracts/BPool.sol b/src/contracts/BPool.sol index 660118af..62586ba0 100644 --- a/src/contracts/BPool.sol +++ b/src/contracts/BPool.sol @@ -2,31 +2,39 @@ pragma solidity 0.8.25; import {BMath} from './BMath.sol'; - import {BToken} from './BToken.sol'; import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; import {IBPool} from 'interfaces/IBPool.sol'; +/** + * @title BPool + * @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; - - address internal _factory; // BFactory address to push token exitFee to - address internal _controller; // has CONTROL role - - // `setSwapFee` and `finalize` require CONTROL - // `finalize` sets `PUBLIC can SWAP`, `PUBLIC can JOIN` + /// @dev BFactory address to push token exitFee to + address internal _factory; + /// @dev Has CONTROL role + address internal _controller; + /// @dev Fee for swapping uint256 internal _swapFee; + /// @dev Status of the pool. True if finalized, False otherwise bool internal _finalized; - + /// @dev Array of bound tokens address[] internal _tokens; + /// @dev Metadata for each bound token mapping(address => Record) internal _records; + /// @dev Sum of all token weights uint256 internal _totalWeight; + /// @dev Logs the call data modifier _logs_() { emit LOG_CALL(msg.sig, msg.sender, msg.data); _; } + /// @dev Prevents reentrancy in non-view functions modifier _lock_() { require(!_mutex, 'ERR_REENTRY'); _mutex = true; @@ -34,6 +42,7 @@ contract BPool is BToken, BMath, IBPool { _mutex = false; } + /// @dev Prevents reentrancy in view functions modifier _viewlock_() { require(!_mutex, 'ERR_REENTRY'); _; @@ -46,6 +55,7 @@ contract BPool is BToken, BMath, IBPool { _finalized = false; } + /// @inheritdoc IBPool function setSwapFee(uint256 swapFee) external _logs_ _lock_ { require(!_finalized, 'ERR_IS_FINALIZED'); require(msg.sender == _controller, 'ERR_NOT_CONTROLLER'); @@ -54,11 +64,13 @@ contract BPool is BToken, BMath, IBPool { _swapFee = swapFee; } + /// @inheritdoc IBPool function setController(address manager) external _logs_ _lock_ { require(msg.sender == _controller, 'ERR_NOT_CONTROLLER'); _controller = manager; } + /// @inheritdoc IBPool function finalize() external _logs_ _lock_ { require(msg.sender == _controller, 'ERR_NOT_CONTROLLER'); require(!_finalized, 'ERR_IS_FINALIZED'); @@ -70,6 +82,7 @@ contract BPool is BToken, BMath, IBPool { _pushPoolShare(msg.sender, INIT_POOL_SUPPLY); } + /// @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'); @@ -90,6 +103,7 @@ contract BPool is BToken, BMath, IBPool { _pullUnderlying(token, msg.sender, balance); } + /// @inheritdoc IBPool function unbind(address token) external _logs_ _lock_ { require(msg.sender == _controller, 'ERR_NOT_CONTROLLER'); require(_records[token].bound, 'ERR_NOT_BOUND'); @@ -109,6 +123,7 @@ contract BPool is BToken, BMath, IBPool { _pushUnderlying(token, msg.sender, IERC20(token).balanceOf(address(this))); } + /// @inheritdoc IBPool function joinPool(uint256 poolAmountOut, uint256[] calldata maxAmountsIn) external _logs_ _lock_ { require(_finalized, 'ERR_NOT_FINALIZED'); @@ -129,6 +144,7 @@ contract BPool is BToken, BMath, IBPool { _pushPoolShare(msg.sender, poolAmountOut); } + /// @inheritdoc IBPool function exitPool(uint256 poolAmountIn, uint256[] calldata minAmountsOut) external _logs_ _lock_ { require(_finalized, 'ERR_NOT_FINALIZED'); @@ -153,6 +169,7 @@ contract BPool is BToken, BMath, IBPool { } } + /// @inheritdoc IBPool function swapExactAmountIn( address tokenIn, uint256 tokenAmountIn, @@ -196,6 +213,7 @@ contract BPool is BToken, BMath, IBPool { return (tokenAmountOut, spotPriceAfter); } + /// @inheritdoc IBPool function swapExactAmountOut( address tokenIn, uint256 maxAmountIn, @@ -239,6 +257,7 @@ contract BPool is BToken, BMath, IBPool { return (tokenAmountIn, spotPriceAfter); } + /// @inheritdoc IBPool function joinswapExternAmountIn( address tokenIn, uint256 tokenAmountIn, @@ -264,6 +283,7 @@ contract BPool is BToken, BMath, IBPool { return poolAmountOut; } + /// @inheritdoc IBPool function joinswapPoolAmountOut( address tokenIn, uint256 poolAmountOut, @@ -291,6 +311,7 @@ contract BPool is BToken, BMath, IBPool { return tokenAmountIn; } + /// @inheritdoc IBPool function exitswapPoolAmountIn( address tokenOut, uint256 poolAmountIn, @@ -320,6 +341,7 @@ contract BPool is BToken, BMath, IBPool { return tokenAmountOut; } + /// @inheritdoc IBPool function exitswapExternAmountOut( address tokenOut, uint256 tokenAmountOut, @@ -349,6 +371,7 @@ contract BPool is BToken, BMath, IBPool { return poolAmountIn; } + /// @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'); @@ -363,6 +386,7 @@ 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'); @@ -377,81 +401,118 @@ contract BPool is BToken, BMath, IBPool { ); } + /// @inheritdoc IBPool function isFinalized() external view returns (bool) { return _finalized; } + /// @inheritdoc IBPool function isBound(address t) external view returns (bool) { return _records[t].bound; } + /// @inheritdoc IBPool function getNumTokens() external view returns (uint256) { return _tokens.length; } + /// @inheritdoc IBPool function getCurrentTokens() external view _viewlock_ returns (address[] memory tokens) { return _tokens; } + /// @inheritdoc IBPool function getFinalTokens() external view _viewlock_ returns (address[] memory tokens) { require(_finalized, 'ERR_NOT_FINALIZED'); return _tokens; } + /// @inheritdoc IBPool function getDenormalizedWeight(address token) external view _viewlock_ returns (uint256) { require(_records[token].bound, 'ERR_NOT_BOUND'); return _records[token].denorm; } + /// @inheritdoc IBPool function getTotalDenormalizedWeight() external view _viewlock_ returns (uint256) { return _totalWeight; } + /// @inheritdoc IBPool function getNormalizedWeight(address token) external view _viewlock_ returns (uint256) { require(_records[token].bound, 'ERR_NOT_BOUND'); 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'); return IERC20(token).balanceOf(address(this)); } + /// @inheritdoc IBPool function getSwapFee() external view _viewlock_ returns (uint256) { return _swapFee; } + /// @inheritdoc IBPool function getController() external view _viewlock_ returns (address) { return _controller; } - // == - // 'Underlying' token-manipulation functions make external calls but are NOT locked - // You must `_lock_` or otherwise ensure reentry-safety - + /** + * @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 + */ function _pullUnderlying(address erc20, address from, uint256 amount) internal virtual { bool xfer = IERC20(erc20).transferFrom(from, address(this), amount); require(xfer, 'ERR_ERC20_FALSE'); } + /** + * @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 + */ function _pushUnderlying(address erc20, address to, uint256 amount) internal virtual { bool xfer = IERC20(erc20).transfer(to, amount); require(xfer, 'ERR_ERC20_FALSE'); } + /** + * @dev Pulls pool tokens from the sender. + * @param from address to pull the pool tokens from + * @param amount amount of pool tokens to pull + */ function _pullPoolShare(address from, uint256 amount) internal { _pull(from, amount); } + /** + * @dev Pushes pool tokens to the receiver. + * @param to address to push the pool tokens to + * @param amount amount of pool tokens to push + */ function _pushPoolShare(address to, uint256 amount) internal { _push(to, amount); } + /** + * @dev Mints an amount of pool tokens. + * @param amount amount of pool tokens to mint + */ function _mintPoolShare(uint256 amount) internal { _mint(address(this), amount); } + /** + * @dev Burns an amount of pool tokens. + * @param amount amount of pool tokens to burn + */ function _burnPoolShare(uint256 amount) internal { _burn(address(this), amount); } diff --git a/src/contracts/BToken.sol b/src/contracts/BToken.sol index 884c335e..0aaefc48 100644 --- a/src/contracts/BToken.sol +++ b/src/contracts/BToken.sol @@ -1,18 +1,28 @@ // SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.25; -import {BNum} from './BNum.sol'; - import {ERC20} from '@openzeppelin/contracts/token/ERC20/ERC20.sol'; -contract BToken is BNum, ERC20 { +contract BToken is ERC20 { constructor() ERC20('Balancer Pool Token', 'BPT') {} + /** + * @notice Increase the allowance of the spender. + * @param dst The address which will spend the funds. + * @param amt The amount of tokens to increase the allowance by. + * @return True if the operation is successful. + */ function increaseApproval(address dst, uint256 amt) external returns (bool) { _approve(msg.sender, dst, allowance(msg.sender, dst) + amt); return true; } + /** + * @notice Decrease the allowance of the spender. + * @param dst The address which will spend the funds. + * @param amt The amount of tokens to decrease the allowance by. + * @return True if the operation is successful. + */ function decreaseApproval(address dst, uint256 amt) external returns (bool) { uint256 oldValue = allowance(msg.sender, dst); if (amt > oldValue) { @@ -23,10 +33,20 @@ contract BToken is BNum, ERC20 { return true; } + /** + * @notice Transfer tokens from one this contract to another. + * @param to The address which you want to transfer to. + * @param amt The amount of tokens to be transferred. + */ function _push(address to, uint256 amt) internal virtual { _transfer(address(this), to, amt); } + /** + * @notice Pull tokens from another address to this contract. + * @param from The address which you want to transfer from. + * @param amt The amount of tokens to be transferred. + */ function _pull(address from, uint256 amt) internal virtual { _transfer(from, address(this), amt); } diff --git a/src/interfaces/IBFactory.sol b/src/interfaces/IBFactory.sol index d07a3e6d..dc0999dd 100644 --- a/src/interfaces/IBFactory.sol +++ b/src/interfaces/IBFactory.sol @@ -4,17 +4,48 @@ pragma solidity 0.8.25; import {IBPool} from 'interfaces/IBPool.sol'; interface IBFactory { + /** + * @notice Emitted when creating a new pool + * @param caller The caller of the function that will be set as the controller + * @param pool The address of the new pool + */ event LOG_NEW_POOL(address indexed caller, address indexed pool); + /** + * @notice Emitted when setting the BLabs address + * @param caller The caller of the set BLabs function + * @param bLabs The address of the new BLabs + */ event LOG_BLABS(address indexed caller, address indexed bLabs); + /** + * @notice Creates a new BPool, assigning the caller as the pool controller + * @return pool The new BPool + */ function newBPool() external returns (IBPool pool); + /** + * @notice Sets the BLabs address in the factory + * @param b The new BLabs address + */ function setBLabs(address b) external; + /** + * @notice Collects the fees of a pool and transfers it to BLabs address + * @param pool The address of the pool to collect fees from + */ function collect(IBPool pool) external; + /** + * @notice Checks if an address is a BPool created from this factory + * @param b The address to check + * @return isBPool True if the address is a BPool, False otherwise + */ function isBPool(address b) external view returns (bool isBPool); + /** + * @notice Gets the BLabs address + * @return bLabs The address of the BLabs + */ function getBLabs() external view returns (address bLabs); } diff --git a/src/interfaces/IBPool.sol b/src/interfaces/IBPool.sol index c111948e..c02f1a98 100644 --- a/src/interfaces/IBPool.sol +++ b/src/interfaces/IBPool.sol @@ -4,12 +4,26 @@ pragma solidity 0.8.25; import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; interface IBPool is IERC20 { + /** + * @dev Struct for token records. + * @param bound If token is bound to pool. + * @param index Internal index of token array. + * @param denorm Denormalized weight of token. + */ struct Record { - bool bound; // is token bound to pool - uint256 index; // internal - uint256 denorm; // denormalized weight + bool bound; + uint256 index; + uint256 denorm; } + /** + * @notice Emitted when a swap is executed + * @param caller The caller of the swap function + * @param tokenIn The address of the token being swapped in + * @param tokenOut The address of the token being swapped out + * @param tokenAmountIn The amount of tokenIn being swapped in + * @param tokenAmountOut The amount of tokenOut being swapped out + */ event LOG_SWAP( address indexed caller, address indexed tokenIn, @@ -18,26 +32,85 @@ interface IBPool is IERC20 { uint256 tokenAmountOut ); + /** + * @notice Emitted when a join operation is executed + * @param caller The caller of the function + * @param tokenIn The address of the token being sent to the pool + * @param tokenAmountIn The balance of the token being sent to the pool + */ event LOG_JOIN(address indexed caller, address indexed tokenIn, uint256 tokenAmountIn); + /** + * @notice Emitted when a token amount is removed from the pool + * @param caller The caller of the function + * @param tokenOut The address of the token being removed from the pool + * @param tokenAmountOut The amount of the token being removed from the pool + */ event LOG_EXIT(address indexed caller, address indexed tokenOut, uint256 tokenAmountOut); + /** + * @notice Emitted when a call is executed on the pool + * @param sig The signature of the function selector being called + * @param caller The caller of the function + * @param data The complete data of the call + */ event LOG_CALL(bytes4 indexed sig, address indexed caller, bytes data) anonymous; + /** + * @notice Sets the new swap fee + * @param swapFee The new swap fee + */ function setSwapFee(uint256 swapFee) external; + /** + * @notice Sets the new controller + * @param manager The new controller + */ function setController(address manager) external; + /** + * @notice Finalize the pool, removing the restrictions on the pool + */ function finalize() external; + /** + * @notice Binds a token to the pool + * @param token The address of the token to bind + * @param balance The balance of the token to bind + * @param denorm The denormalized weight of the token to bind + */ function bind(address token, uint256 balance, uint256 denorm) external; + /** + * @notice Unbinds a token from the pool + * @param token The address of the token to unbind + */ function unbind(address token) external; + /** + * @notice Joins a pool, providing each token in the pool with a proportional amount + * @param poolAmountOut The amount of pool tokens to mint + * @param maxAmountsIn The maximum amount of tokens to send to the pool + */ function joinPool(uint256 poolAmountOut, uint256[] calldata maxAmountsIn) external; + /** + * @notice Exits a pool, receiving each token in the pool with a proportional amount + * @param poolAmountIn The amount of pool tokens to burn + * @param minAmountsOut The minimum amount of tokens to receive from the pool + */ function exitPool(uint256 poolAmountIn, uint256[] calldata minAmountsOut) external; + /** + * @notice Swaps an exact amount of tokens in for an amount of tokens out + * @param tokenIn The address of the token to swap in + * @param tokenAmountIn The amount of token to swap in + * @param tokenOut The address of the token to swap out + * @param minAmountOut The minimum amount of token to receive from the swap + * @param maxPrice The maximum price to pay for the swap + * @return tokenAmountOut The amount of token swapped out + * @return spotPriceAfter The spot price after the swap + */ function swapExactAmountIn( address tokenIn, uint256 tokenAmountIn, @@ -46,6 +119,16 @@ interface IBPool is IERC20 { uint256 maxPrice ) external returns (uint256 tokenAmountOut, uint256 spotPriceAfter); + /** + * @notice Swaps as many tokens in as possible for an exact amount of tokens out + * @param tokenIn The address of the token to swap in + * @param maxAmountIn The maximum amount of token to swap in + * @param tokenOut The address of the token to swap out + * @param tokenAmountOut The amount of token to swap out + * @param maxPrice The maximum price to pay for the swap + * @return tokenAmountIn The amount of token swapped in + * @return spotPriceAfter The spot price after the swap + */ function swapExactAmountOut( address tokenIn, uint256 maxAmountIn, @@ -54,53 +137,141 @@ interface IBPool is IERC20 { uint256 maxPrice ) external returns (uint256 tokenAmountIn, uint256 spotPriceAfter); + /** + * @notice Joins a pool providing a single token in, specifying the exact amount of token given + * @param tokenIn The address of the token to swap in and join + * @param tokenAmountIn The amount of token to join + * @param minPoolAmountOut The minimum amount of pool token to receive + * @return poolAmountOut The amount of pool token received + */ function joinswapExternAmountIn( address tokenIn, uint256 tokenAmountIn, uint256 minPoolAmountOut ) external returns (uint256 poolAmountOut); + /** + * @notice Joins a pool providing a single token in, specifying the exact amount of pool tokens received + * @param tokenIn The address of the token to swap in and join + * @param poolAmountOut The amount of pool token to receive + * @param maxAmountIn The maximum amount of token to introduce to the pool + * @return tokenAmountIn The amount of token in introduced + */ function joinswapPoolAmountOut( address tokenIn, uint256 poolAmountOut, uint256 maxAmountIn ) external returns (uint256 tokenAmountIn); + /** + * @notice Exits a pool providing a specific amount of pool tokens in, and receiving only a single token + * @param tokenOut The address of the token to swap out and exit + * @param poolAmountIn The amount of pool token to burn + * @param minAmountOut The minimum amount of token to receive + * @return tokenAmountOut The amount of token received + */ function exitswapPoolAmountIn( address tokenOut, uint256 poolAmountIn, uint256 minAmountOut ) external returns (uint256 tokenAmountOut); + /** + * @notice Exits a pool expecting a specific amount of token out, and providing pool token + * @param tokenOut The address of the token to swap out and exit + * @param tokenAmountOut The amount of token to receive + * @param maxPoolAmountIn The maximum amount of pool token to burn + * @return poolAmountIn The amount of pool token burned + */ function exitswapExternAmountOut( address tokenOut, uint256 tokenAmountOut, uint256 maxPoolAmountIn ) external returns (uint256 poolAmountIn); + /** + * @notice Gets the spot price of tokenIn in terms of tokenOut + * @param tokenIn The address of the token to swap in + * @param tokenOut The address of the token to swap out + * @return spotPrice The spot price of the swap + */ function getSpotPrice(address tokenIn, address tokenOut) external view returns (uint256 spotPrice); + /** + * @notice Gets the spot price of tokenIn in terms of tokenOut without the fee + * @param tokenIn The address of the token to swap in + * @param tokenOut The address of the token to swap out + * @return spotPrice The spot price of the swap without the fee + */ function getSpotPriceSansFee(address tokenIn, address tokenOut) external view returns (uint256 spotPrice); + /** + * @notice Gets the finalized status of the pool + * @return isFinalized True if the pool is finalized, False otherwise + */ function isFinalized() external view returns (bool isFinalized); + /** + * @notice Gets the bound status of a token + * @param t The address of the token to check + * @return isBound True if the token is bound, False otherwise + */ function isBound(address t) external view returns (bool isBound); + /** + * @notice Gets the number of tokens in the pool + * @return numTokens The number of tokens in the pool + */ function getNumTokens() external view returns (uint256 numTokens); + /** + * @notice Gets the current array of tokens in the pool, while the pool is not finalized + * @return tokens The array of tokens in the pool + */ function getCurrentTokens() external view returns (address[] memory tokens); + /** + * @notice Gets the final array of tokens in the pool, after finalization + * @return tokens The array of tokens in the pool + */ function getFinalTokens() external view returns (address[] memory tokens); + /** + * @notice Gets the denormalized weight of a token in the pool + * @param token The address of the token to check + * @return denormWeight The denormalized weight of the token in the pool + */ function getDenormalizedWeight(address token) external view returns (uint256 denormWeight); + /** + * @notice Gets the total denormalized weight of the pool + * @return totalDenormWeight The total denormalized weight of the pool + */ function getTotalDenormalizedWeight() external view returns (uint256 totalDenormWeight); + /** + * @notice Gets the normalized weight of a token in the pool + * @param token The address of the token to check + * @return normWeight The normalized weight of the token in the pool + */ function getNormalizedWeight(address token) external view returns (uint256 normWeight); + /** + * @notice Gets the Pool's ERC20 balance of a token + * @param token The address of the token to check + * @return balance The Pool's ERC20 balance of the token + */ function getBalance(address token) external view returns (uint256 balance); + /** + * @notice Gets the swap fee of the pool + * @return swapFee The swap fee of the pool + */ function getSwapFee() external view returns (uint256 swapFee); + /** + * @notice Gets the controller of the pool + * @return controller The controller of the pool + */ function getController() external view returns (address controller); }