diff --git a/.solhint.json b/.solhint.json index 94f58b24..a8b002b7 100644 --- a/.solhint.json +++ b/.solhint.json @@ -13,6 +13,9 @@ "immutable-name-snakecase": "warn", "avoid-low-level-calls": "off", "no-console": "off", - "max-line-length": ["warn", 120] + "max-line-length": ["warn", 120], + "TODO": "REMOVE_TEMPORARY_LINTER_SETTINGS_BELOW", + "custom-errors": "warn", + "definition-name-capwords": "warn" } } diff --git a/foundry.toml b/foundry.toml index 1e9d2ee4..26cc6e77 100644 --- a/foundry.toml +++ b/foundry.toml @@ -26,6 +26,7 @@ src = 'src/interfaces/' [fuzz] runs = 1000 +max_test_rejects = 500000 [rpc_endpoints] mainnet = "${MAINNET_RPC}" diff --git a/package.json b/package.json index 4a099721..eb4e08f9 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "package.json": "sort-package-json" }, "dependencies": { - "solmate": "github:transmissions11/solmate#a9e3ea2" + "solmate": "github:transmissions11/solmate#c892309" }, "devDependencies": { "@commitlint/cli": "19.3.0", diff --git a/test/unit/BPool.t.sol b/test/unit/BPool.t.sol new file mode 100644 index 00000000..64038c18 --- /dev/null +++ b/test/unit/BPool.t.sol @@ -0,0 +1,615 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import {BConst} from 'contracts/BConst.sol'; +import {BPool} from 'contracts/BPool.sol'; +import {IERC20} from 'contracts/BToken.sol'; +import {Test} from 'forge-std/Test.sol'; +import {LibString} from 'solmate/utils/LibString.sol'; +import {Utils} from 'test/unit/Utils.sol'; + +// TODO: remove once `private` keyword is removed in all test cases +/* solhint-disable */ + +abstract contract BasePoolTest is Test, BConst, Utils { + using LibString for *; + + uint256 public constant TOKENS_AMOUNT = 3; + uint256 internal constant _RECORD_MAPPING_SLOT_NUMBER = 10; + uint256 internal constant _TOKENS_ARRAY_SLOT_NUMBER = 9; + + BPool public bPool; + address[TOKENS_AMOUNT] public tokens; + + function setUp() public { + bPool = new BPool(); + + // Create fake tokens + for (uint256 i = 0; i < tokens.length; i++) { + tokens[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++) { + _tokens[i] = tokens[i]; + } + } + + 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)); + } + + 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)); + } + + function _setTokens(address[] memory _tokens) internal { + _writeArrayLengthToStorage(address(bPool), _TOKENS_ARRAY_SLOT_NUMBER, _tokens.length); // write length + for (uint256 i = 0; i < _tokens.length; i++) { + _writeAddressArrayItemToStorage(address(bPool), _TOKENS_ARRAY_SLOT_NUMBER, i, _tokens[i]); // write token + } + } + + function _setRecordBound(address _token) internal { + _writeStructPropertyAtAddressMapping(address(bPool), _RECORD_MAPPING_SLOT_NUMBER, _token, 0, 1); // bound (1 == true) + } + + function _setRecordBalance(address _token, uint256 _balance) internal { + _writeStructPropertyAtAddressMapping(address(bPool), _RECORD_MAPPING_SLOT_NUMBER, _token, 3, _balance); // balance + } + + function _setPublicSwap(bool _isPublicSwap) internal { + // TODO: make it depend on the bool value + _writeUintToStorage(address(bPool), 6, 0x0000000000000000000000010000000000000000000000000000000000000000); + } + + function _setFinalize(bool _isFinalized) internal { + // TODO: make it depend on the bool value + _writeUintToStorage(address(bPool), 8, 1); + } + + function _setTotalSupply(uint256 _totalSupply) internal { + _writeUintToStorage(address(bPool), 2, _totalSupply); + } +} + +contract BPool_Unit_Constructor is BasePoolTest { + function test_Deploy() private view {} +} + +contract BPool_Unit_IsPublicSwap is BasePoolTest { + function test_Returns_IsPublicSwap() private view {} +} + +contract BPool_Unit_IsFinalized is BasePoolTest { + function test_Returns_IsFinalized() private view {} +} + +contract BPool_Unit_IsBound is BasePoolTest { + function test_Returns_IsBound() private view {} + + function test_Returns_IsNotBound() private view {} +} + +contract BPool_Unit_GetNumTokens is BasePoolTest { + function test_Returns_NumTokens() private view {} +} + +contract BPool_Unit_GetCurrentTokens is BasePoolTest { + function test_Returns_CurrentTokens() private view {} + + function test_Revert_Reentrancy() private view {} +} + +contract BPool_Unit_GetFinalTokens is BasePoolTest { + function test_Returns_FinalTokens() private view {} + + function test_Revert_Reentrancy() private view {} + + function test_Revert_NotFinalized() private view {} +} + +contract BPool_Unit_GetDenormalizedWeight is BasePoolTest { + function test_Returns_DenormalizedWeight() private view {} + + function test_Revert_Reentrancy() private view {} + + function test_Revert_NotBound() private view {} +} + +contract BPool_Unit_GetTotalDenormalizedWeight is BasePoolTest { + function test_Returns_TotalDenormalizedWeight() private view {} + + function test_Revert_Reentrancy() private view {} +} + +contract BPool_Unit_GetNormalizedWeight is BasePoolTest { + function test_Returns_NormalizedWeight() private view {} + + function test_Revert_Reentrancy() private view {} + + function test_Revert_NotBound() private view {} +} + +contract BPool_Unit_GetBalance is BasePoolTest { + function test_Returns_Balance() private view {} + + function test_Revert_Reentrancy() private view {} + + function test_Revert_NotBound() private view {} +} + +contract BPool_Unit_GetSwapFee is BasePoolTest { + function test_Returns_SwapFee() private view {} + + function test_Revert_Reentrancy() private view {} +} + +contract BPool_Unit_GetController is BasePoolTest { + function test_Returns_Controller() private view {} + + function test_Revert_Reentrancy() private view {} +} + +contract BPool_Unit_SetSwapFee is BasePoolTest { + function test_Revert_Finalized() private view {} + + function test_Revert_NotController() private view {} + + function test_Revert_MinFee() private view {} + + function test_Revert_MaxFee() private view {} + + function test_Revert_Reentrancy() private view {} + + function test_Set_SwapFee() private view {} + + function test_Emit_LogCall() private view {} +} + +contract BPool_Unit_SetController is BasePoolTest { + function test_Revert_NotController() private view {} + + function test_Revert_Reentrancy() private view {} + + function test_Set_Controller() private view {} + + function test_Emit_LogCall() private view {} +} + +contract BPool_Unit_SetPublicSwap is BasePoolTest { + function test_Revert_Finalized() private view {} + + function test_Revert_NotController() private view {} + + function test_Revert_Reentrancy() private view {} + + function test_Set_PublicSwap() private view {} + + function test_Emit_LogCall() private view {} +} + +contract BPool_Unit_Finalize is BasePoolTest { + function test_Revert_NotController() private view {} + + function test_Revert_Finalized() private view {} + + function test_Revert_MinTokens() private view {} + + function test_Revert_Reentrancy() private view {} + + function test_Set_Finalize() private view {} + + function test_Set_PublicSwap() private view {} + + function test_Mint_InitPoolSupply() private view {} + + function test_Push_InitPoolSupply() private view {} + + function test_Emit_LogCall() private view {} +} + +contract BPool_Unit_Bind is BasePoolTest { + function test_Revert_NotController() private view {} + + function test_Revert_IsBound() private view {} + + function test_Revert_Finalized() private view {} + + function test_Revert_MaxPoolTokens() private view {} + + function test_Set_Record() private view {} + + function test_Set_TokenArray() private view {} + + function test_Emit_LogCall() private view {} + + function test_Call_Rebind() private view {} +} + +contract BPool_Unit_Rebind is BasePoolTest { + function test_Revert_NotController() private view {} + + function test_Revert_NotBound() private view {} + + function test_Revert_Finalized() private view {} + + function test_Revert_MinWeight() private view {} + + function test_Revert_MaxWeight() private view {} + + function test_Revert_MinBalance() private view {} + + function test_Revert_Reentrancy() private view {} + + function test_Set_TotalWeightIfDenormMoreThanOldWeight() private view {} + + function test_Set_TotalWeightIfDenormLessThanOldWeight() private view {} + + function test_Revert_MaxTotalWeight() private view {} + + function test_Set_Denorm() private view {} + + function test_Set_Balance() private view {} + + function test_Pull_IfBalanceMoreThanOldBalance() private view {} + + function test_Push_UnderlyingIfBalanceLessThanOldBalance() private view {} + + function test_Push_FeeIfBalanceLessThanOldBalance() private view {} + + function test_Emit_LogCall() private view {} +} + +contract BPool_Unit_Unbind is BasePoolTest { + function test_Revert_NotController() private view {} + + function test_Revert_NotBound() private view {} + + function test_Revert_Finalized() private view {} + + function test_Revert_Reentrancy() private view {} + + function test_Set_TotalWeight() private view {} + + function test_Set_TokenArray() private view {} + + function test_Set_Index() private view {} + + function test_Unset_TokenArray() private view {} + + function test_Unset_Record() private view {} + + function test_Push_UnderlyingBalance() private view {} + + function test_Push_UnderlyingFee() private view {} + + function test_Emit_LogCall() private view {} +} + +contract BPool_Unit_Gulp is BasePoolTest { + function test_Revert_NotBound() private view {} + + function test_Revert_Reentrancy() private view {} + + function test_Set_Balance() private view {} + + function test_Emit_LogCall() private view {} +} + +contract BPool_Unit_GetSpotPrice is BasePoolTest { + function test_Revert_NotBoundTokenIn() private view {} + + function test_Revert_NotBoundTokenOut() private view {} + + function test_Returns_SpotPrice() private view {} + + function test_Revert_Reentrancy() private view {} +} + +contract BPool_Unit_GetSpotPriceSansFee is BasePoolTest { + function test_Revert_NotBoundTokenIn() private view {} + + function test_Revert_NotBoundTokenOut() private view {} + + function test_Returns_SpotPrice() private view {} + + function test_Revert_Reentrancy() private view {} +} + +contract BPool_Unit_JoinPool is BasePoolTest { + struct JoinPool_FuzzScenario { + uint256 poolAmountOut; + uint256 initPoolSupply; + uint256[TOKENS_AMOUNT] balance; + } + + function _setValues(JoinPool_FuzzScenario memory _fuzz) internal { + // Create mocks + for (uint256 i = 0; i < tokens.length; i++) { + _mockTransfer(tokens[i]); + _mockTransferFrom(tokens[i]); + } + + // Set tokens + _setTokens(_tokensToMemory()); + + // Set balances + for (uint256 i = 0; i < tokens.length; i++) { + _setRecordBound(tokens[i]); + _setRecordBalance(tokens[i], _fuzz.balance[i]); + } + + // Set public swap + _setPublicSwap(true); + // Set finalize + _setFinalize(true); + // Set totalSupply + _setTotalSupply(_fuzz.initPoolSupply); + } + + function _assumeHappyPath(JoinPool_FuzzScenario memory _fuzz) internal pure { + vm.assume(_fuzz.initPoolSupply >= INIT_POOL_SUPPLY); + vm.assume(_fuzz.poolAmountOut >= _fuzz.initPoolSupply); + vm.assume(_fuzz.poolAmountOut < type(uint256).max / BONE); + + uint256 _ratio = (_fuzz.poolAmountOut * BONE) / _fuzz.initPoolSupply; // bdiv uses '* BONE' + uint256 _maxTokenAmountIn = type(uint256).max / _ratio; + + for (uint256 i = 0; i < _fuzz.balance.length; i++) { + vm.assume(_fuzz.balance[i] >= MIN_BALANCE); + vm.assume(_fuzz.balance[i] <= _maxTokenAmountIn); // L272 + } + } + + modifier happyPath(JoinPool_FuzzScenario memory _fuzz) { + _assumeHappyPath(_fuzz); + _setValues(_fuzz); + _; + } + + function test_HappyPath(JoinPool_FuzzScenario memory _fuzz) public happyPath(_fuzz) { + uint256[] memory maxAmountsIn = new uint256[](tokens.length); + for (uint256 i = 0; i < tokens.length; i++) { + maxAmountsIn[i] = type(uint256).max; + } // Using max possible amounts + + bPool.joinPool(_fuzz.poolAmountOut, maxAmountsIn); + } + + function test_Revert_NotFinalized() private view {} + + function test_Revert_MathApprox() private view {} + + function test_Revert_TokenArrayMathApprox() private view {} + + function test_Revert_TokenArrayLimitIn() private view {} + + function test_Revert_Reentrancy() private view {} + + function test_Set_TokenArrayBalance() private view {} + + function test_Emit_TokenArrayLogJoin() private view {} + + function test_Pull_TokenArrayTokenAmountIn() private view {} + + function test_Mint_PoolShare() private view {} + + function test_Push_PoolShare() private view {} + + function test_Emit_LogCall() private view {} +} + +contract BPool_Unit_ExitPool is BasePoolTest { + function test_Revert_NotFinalized() private view {} + + function test_Revert_MathApprox() private view {} + + function test_Pull_PoolShare() private view {} + + function test_Push_PoolShare() private view {} + + function test_Burn_PoolShare() private view {} + + function test_Revert_TokenArrayMathApprox() private view {} + + function test_Revert_TokenArrayLimitOut() private view {} + + function test_Revert_Reentrancy() private view {} + + function test_Set_TokenArrayBalance() private view {} + + function test_Emit_TokenArrayLogExit() private view {} + + function test_Push_TokenArrayTokenAmountOut() private view {} + + function test_Emit_LogCall() private view {} +} + +contract BPool_Unit_SwapExactAmountIn is BasePoolTest { + function test_Revert_NotBoundTokenIn() private view {} + + function test_Revert_NotBoundTokenOut() private view {} + + function test_Revert_NotPublic() private view {} + + function test_Revert_MaxInRatio() private view {} + + function test_Revert_BadLimitPrice() private view {} + + function test_Revert_LimitOut() private view {} + + function test_Revert_Reentrancy() private view {} + + function test_Set_InRecord() private view {} + + function test_Set_OutRecord() private view {} + + function test_Revert_MathApprox() private view {} + + function test_Revert_LimitPrice() private view {} + + function test_Revert_MathApprox2() private view {} + + function test_Emit_LogSwap() private view {} + + function test_Pull_TokenAmountIn() private view {} + + function test_Push_TokenAmountOut() private view {} + + function test_Returns_AmountAndPrice() private view {} + + function test_Emit_LogCall() private view {} +} + +contract BPool_Unit_SwapExactAmountOut is BasePoolTest { + function test_Revert_NotBoundTokenIn() private view {} + + function test_Revert_NotBoundTokenOut() private view {} + + function test_Revert_NotPublic() private view {} + + function test_Revert_MaxOutRatio() private view {} + + function test_Revert_BadLimitPrice() private view {} + + function test_Revert_LimitIn() private view {} + + function test_Revert_Reentrancy() private view {} + + function test_Set_InRecord() private view {} + + function test_Set_OutRecord() private view {} + + function test_Revert_MathApprox() private view {} + + function test_Revert_LimitPrice() private view {} + + function test_Revert_MathApprox2() private view {} + + function test_Emit_LogSwap() private view {} + + function test_Pull_TokenAmountIn() private view {} + + function test_Push_TokenAmountOut() private view {} + + function test_Returns_AmountAndPrice() private view {} + + function test_Emit_LogCall() private view {} +} + +contract BPool_Unit_JoinswapExternAmountIn is BasePoolTest { + function test_Revert_NotFinalized() private view {} + + function test_Revert_NotBound() private view {} + + function test_Revert_MaxInRatio() private view {} + + function test_Revert_LimitOut() private view {} + + function test_Revert_Reentrancy() private view {} + + function test_Set_Balance() private view {} + + function test_Emit_LogJoin() private view {} + + function test_Mint_PoolShare() private view {} + + function test_Push_PoolShare() private view {} + + function test_Pull_Underlying() private view {} + + function test_Returns_PoolAmountOut() private view {} + + function test_Emit_LogCall() private view {} +} + +contract BPool_Unit_JoinswapExternAmountOut is BasePoolTest { + function test_Revert_NotFinalized() private view {} + + function test_Revert_NotBound() private view {} + + function test_Revert_MaxApprox() private view {} + + function test_Revert_LimitIn() private view {} + + function test_Revert_MaxInRatio() private view {} + + function test_Revert_Reentrancy() private view {} + + function test_Set_Balance() private view {} + + function test_Emit_LogJoin() private view {} + + function test_Mint_PoolShare() private view {} + + function test_Push_PoolShare() private view {} + + function test_Pull_Underlying() private view {} + + function test_Returns_TokenAmountIn() private view {} + + function test_Emit_LogCall() private view {} +} + +contract BPool_Unit_ExitswapPoolAmountIn is BasePoolTest { + function test_Revert_NotFinalized() private view {} + + function test_Revert_NotBound() private view {} + + function test_Revert_LimitOut() private view {} + + function test_Revert_MaxOutRatio() private view {} + + function test_Revert_Reentrancy() private view {} + + function test_Set_Balance() private view {} + + function test_Emit_LogExit() private view {} + + function test_Pull_PoolShare() private view {} + + function test_Burn_PoolShare() private view {} + + function test_Push_PoolShare() private view {} + + function test_Push_Underlying() private view {} + + function test_Returns_TokenAmountOut() private view {} + + function test_Emit_LogCall() private view {} +} + +contract BPool_Unit_ExitswapPoolAmountOut is BasePoolTest { + function test_Revert_NotFinalized() private view {} + + function test_Revert_NotBound() private view {} + + function test_Revert_MaxOutRatio() private view {} + + function test_Revert_MathApprox() private view {} + + function test_Revert_LimitIn() private view {} + + function test_Revert_Reentrancy() private view {} + + function test_Set_Balance() private view {} + + function test_Emit_LogExit() private view {} + + function test_Pull_PoolShare() private view {} + + function test_Burn_PoolShare() private view {} + + function test_Push_PoolShare() private view {} + + function test_Push_Underlying() private view {} + + function test_Returns_PoolAmountIn() private view {} + + function test_Emit_LogCall() private view {} +} diff --git a/test/unit/Utils.sol b/test/unit/Utils.sol new file mode 100644 index 00000000..ba1c5e25 --- /dev/null +++ b/test/unit/Utils.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import {Test} from 'forge-std/Test.sol'; + +contract Utils is Test { + /** + * @dev Write a uint256 value to a storage slot. + * @param _target The address of the contract. + * @param _slotNumber The slot number to write to. + * @param _value The value to write. + */ + function _writeUintToStorage(address _target, uint256 _slotNumber, uint256 _value) internal { + vm.store(_target, bytes32(_slotNumber), bytes32(_value)); + } + + /** + * @dev Write the length of an array in storage. + * @dev This must be performed before writing any items to the array. + * @param _target The address of the contract. + * @param _arraySlotNumber The slot number of the array. + * @param _arrayLength The length of the array. + */ + function _writeArrayLengthToStorage(address _target, uint256 _arraySlotNumber, uint256 _arrayLength) internal { + _writeUintToStorage(_target, _arraySlotNumber, _arrayLength); + } + + /** + * @dev Write an address array item to a storage slot. + * @param _target The address of the contract. + * @param _arraySlotNumber The slot number of the array. + * @param _index The index of the item in the array. + * @param _value The address value to write. + */ + function _writeAddressArrayItemToStorage( + address _target, + uint256 _arraySlotNumber, + uint256 _index, + address _value + ) internal { + bytes memory _arraySlot = abi.encode(_arraySlotNumber); + bytes32 _hashArraySlot = keccak256(_arraySlot); + vm.store(_target, bytes32(uint256(_hashArraySlot) + _index), bytes32(abi.encode(_value))); + } + + /** + * @dev Write a struct property to a mapping in storage. + * @param _target The address of the contract. + * @param _mappingSlotNumber The slot number of the mapping. + * @param _mappingKey The address key of the mapping. + * @param _propertySlotNumber The slot number of the property in the struct. + * @param _value The value to write. + */ + function _writeStructPropertyAtAddressMapping( + address _target, + uint256 _mappingSlotNumber, + address _mappingKey, + uint256 _propertySlotNumber, + uint256 _value + ) internal { + bytes32 _slot = keccak256(abi.encode(_mappingKey, _mappingSlotNumber)); + _writeUintToStorage(_target, uint256(_slot) + _propertySlotNumber, _value); + } +} diff --git a/yarn.lock b/yarn.lock index 861f5795..8c16eda5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1769,9 +1769,9 @@ solc@0.8.25: optionalDependencies: prettier "^2.8.3" -"solmate@github:transmissions11/solmate#a9e3ea2": - version "6.0.0" - resolved "https://codeload.github.com/transmissions11/solmate/tar.gz/a9e3ea26a2dc73bfa87f0cb189687d029028e0c5" +"solmate@github:transmissions11/solmate#c892309": + version "6.2.0" + resolved "https://codeload.github.com/transmissions11/solmate/tar.gz/c892309933b25c03d32b1b0d674df7ae292ba925" sort-object-keys@^1.1.3: version "1.1.3"