msc-smart-contract-auditing/vulnerable-functions-base huggingface dataset:
Vulnerability: Attacker can make a signer gate creation fail
Severity: medium

Description:
DAOs can deploy a HSG using `deployHatsSignerGateAndSafe()` or deployMultiHatsSignerGateAndSafe().The parameters are encoded and passed to moduleProxyFactory.deployModule():\\n```\\n    bytes memory initializeParams = abi.encode(_ownerHatId, _signersHatId, _safe, hatsAddress, _minThreshold, \\n    _targetThreshold, _maxSigners, version );\\n        hsg = moduleProxyFactory.deployModule(hatsSignerGateSingleton, abi.encodeWithSignature("setUp(bytes)", \\n    initializeParams), _saltNonce );\\n```\\n\\nThis function will call createProxy():\\n```\\n    proxy = createProxy( masterCopy, keccak256(abi.encodePacked(keccak256(initializer), saltNonce)) );\\n```\\n\\nThe second parameter is the generated salt, which is created from the initializer and passed saltNonce. Finally `createProxy()` will use CREATE2 to create the contract:\\n```\\n        function createProxy(address target, bytes32 salt)  internal  returns (address result)\\n        {\\n            if (address(target) == address(0)) revert ZeroAddress(target);\\n            if (address(target).code.length == 0) revert \\n        TargetHasNoCode(target);\\n                bytes memory deployment = abi.encodePacked(\\n                  hex"602d8060093d393df3363d3d373d3d3d363d73", target, hex"5af43d82803e903d91602b57fd5bf3" );\\n            // solhint-disable-next-line no-inline-assembly\\n                assembly {\\n                     result := create2(0, add(deployment, 0x20), \\n        mload(deployment), salt)\\n              }\\n                  if (result == address(0)) revert TakenAddress(result);\\n             }\\n```\\n\\nAn issue could be that an attacker can frontrun the creation TX with their own creation request, with the same parameters. This would create the exact address created by the CREATE2 call, since the parameters and therefore the final salt will be the same. When the victim's transaction would be executed, the address is non-empty so the EVM would reject its creation. This would result in a bad UX for a user, who thinks the creation did not succeed. The result contract would still be usable, but would be hard to track as it was created in another TX.

Impact:

Recommended mitigation:
Use an ever-increasing nonce counter to guarantee unique contract addresses.
Vulnerability: Signers can backdoor the safe to execute any transaction in the future without consensus
Severity: medium

Description:
The function `checkAfterExecution()` is called by the safe after signer's request TX was executed (and authorized). It mainly checks that the linkage between the safe and the HSG has not been compromised.\\n```\\n        function checkAfterExecution(bytes32, bool) external override {\\n            if (abi.decode(StorageAccessible(address(safe)).getStorageAt(uint256(GUARD_STORAGE_SLOT), 1), (address))\\n                    != address(this)) \\n                    {\\n                revert CannotDisableThisGuard(address(this));\\n            }\\n            if (!IAvatar(address(safe)).isModuleEnabled(address(this))) {\\n                    revert CannotDisableProtectedModules(address(this));\\n            }\\n            if (safe.getThreshold() != _correctThreshold()) {\\n                     revert SignersCannotChangeThreshold();\\n            }\\n            // leave checked to catch underflows triggered by re-erntry\\n        attempts\\n            --guardEntries;\\n        }\\n```\\n\\nHowever, it is missing a check that no new modules have been introduced to the safe. When modules execute TXs on a Gnosis safe, the guard safety callbacks do not get called. As a result, any new module introduced is free to execute whatever it wishes on the safe. It constitutes a serious backdoor threat and undermines the HSG security model.

Impact:

Recommended mitigation:
Check that no new modules have been introduced to the safe, using the `getModulesPaginated()` utility.
Vulnerability: createHat does not detect MAX_LEVEL admin correctly
Severity: low

Description:
In `createHat()`, the contract checks user is not minting hats for the lowest hat tier:\\n```\\n        function createHat( uint256 _admin, string memory _details, uint32 _maxSupply, address _eligibility,\\n            address _toggle, bool _mutable,  string memory _imageURI)       \\n                 public returns (uint256 newHatId) {\\n        if (uint8(_admin) > 0) {\\n                    revert MaxLevelsReached();\\n                 }\\n             ….\\n        }\\n```\\n\\nThe issue is that it does not check for max level correctly, as it looks only at the lowest 8 bits. Each level is composed of 16 bits, so ID xx00 would pass this check. Fortunately, although the check is passed, the function will revert later. The call to `getNextId(_admin)` will return 0 for max-level admin, and _checkAdmin(0) is guaranteed to fail. However, the check should still be fixed as it is not exploitable only by chance.

Impact:

Recommended mitigation:
Change the conversion to uint16.
Vulnerability: Incorrect imageURI is returned for hats in certain cases
Severity: low

Description:
Function `getImageURIForHat()` should return the most relevant imageURI for the requested hatId. It will iterate backwards from the current level down to level 0, and return an image if it exists for that level.\\n```\\n            function getImageURIForHat(uint256 _hatId) public view returns (string memory) {\\n                // check _hatId first to potentially avoid the `getHatLevel` call\\n                      Hat memory hat = _hats[_hatId];\\n                        string memory imageURI = hat.imageURI; // save 1 SLOAD\\n                // if _hatId has an imageURI, we return it\\n                            if (bytes(imageURI).length > 0) {\\n                return imageURI;\\n                }\\n                // otherwise, we check its branch of admins\\n                        uint256 level = getHatLevel(_hatId);\\n                // but first we check if _hatId is a tophat, in which case we fall back to the global image uri\\n                if (level == 0) return baseImageURI;\\n                // otherwise, we check each of its admins for a valid imageURI\\n                     uint256 id;\\n                // already checked at `level` above, so we start the loop at `level - 1`\\n                for (uint256 i = level - 1; i > 0;) {\\n                     id = getAdminAtLevel(_hatId, uint8(i));\\n                        hat = _hats[id];\\n                            imageURI = hat.imageURI;\\n                if (bytes(imageURI).length > 0) {\\n                      return imageURI;\\n                 }\\n                // should not underflow given stopping condition is > 0\\n                    unchecked {\\n                         --i;\\n                    }\\n             }\\n                // if none of _hatId's admins has an imageURI of its own, we \\n            again fall back to the global image uri\\n                return baseImageURI;\\n            }\\n```\\n\\nIt can be observed that the loop body will not run for level 0. When the loop is finished, the code just returns the baseImageURI, which is a Hats-level fallback, rather than top hat level fallback. As a result, the image displayed will not be correct when querying for a level above 0, when all levels except level 0 have no registered image.

Impact:

Recommended mitigation:
Before returning the baseImageURI, check if level 0 admin has a registered image.
Vulnerability: Fetching of hat status may fail due to lack of input sanitization
Severity: low

Description:
The functions `_isActive()` and `_isEligible()` are used by `balanceOf()` and other functions, so they should not ever revert. However, they perform ABI decoding from external inputs.\\n```\\n        function _isActive(Hat memory _hat, uint256 _hatId) internal view  returns (bool) {\\n        bytes memory data = \\n             abi.encodeWithSignature("getHatStatus(uint256)", _hatId);\\n                (bool success, bytes memory returndata) = \\n        _hat.toggle.staticcall(data);\\n        if (success && returndata.length > 0) {\\n            return abi.decode(returndata, (bool));\\n                } else {\\n        return _getHatStatus(_hat);\\n                }\\n         }\\n```\\n\\nIf toggle returns invalid return data (whether malicious or by accident), `abi.decode()` would revert causing the entire function to revert.

Impact:

Recommended mitigation:
Wrap the decoding operation for both affected functions in a try/catch statement. Fall back to the `_getHatStatus()` result if necessary. Checking that returndata size is correct is not enough as bool encoding must be 64-bit encoded 0 or 1.
Vulnerability: Attacker can take over GMXAdapter implementation contract
Severity: low

Description:
GMXAdapter inherits from BaseExchangeAdapter. It is an implementation contract for a transparent proxy and has the following initializer:\\n```\\n       function initialize() external initializer {\\n         __Ownable_init();\\n       }\\n```\\n\\nTherefore, an attacker can call initialize() on the implementation contract and become the owner. At this point they can do just about anything to this contract, but it has no impact on the proxy as it is using separate storage. If there was a delegatecall coded in GMXAdapter, attacker could have used it to call an attacker's contract and execute the SELFDESTRUCT opcode, killing the implementation. With no implementation, the proxy itself would not be functional until it is updated to a new implementation. It is ill-advised to allow anyone to have control over implementation contracts as future upgrades may make the attack surface exploitable.

Impact:

Recommended mitigation:
The standard approach is to call from the constructor the _disableInitializers() from Open Zeppelin's Initializable module
Vulnerability: disordered fee calculated causes collateral changes to be inaccurate
Severity: high

Description:
`_increasePosition()` changes the Hedger's GMX position by sizeDelta amount and collateralDelta collateral. There are two collateralDelta corrections - one for swap fees and one for position fees. Since the swap fee depends on up-to-date collateralDelta, it's important to calculate it after the position fee, contrary to the current state. In practice, it may lead to the leverage ratio being higher than intended as collateralDelta sent to GMX is lower than it should be.\\n```\\n      if (isLong) {\\n          uint swapFeeBP = getSwapFeeBP(isLong, true, collateralDelta);\\n           collateralDelta = (collateralDelta * (BASIS_POINTS_DIVISOR + swapFeeBP)) / BASIS_POINTS_DIVISOR;\\n      }\\n      // add margin fee\\n      // when we increase position, fee always got deducted from collateral\\n          collateralDelta += _getPositionFee(currentPos.size, sizeDelta, currentPos.entryFundingRate);\\n```\\n

Impact:

Recommended mitigation:
Flip the order of `getSwapFeeBP()` and `_getPositionFee()`.
Vulnerability: small LP providers may be unable to withdraw their deposits
Severity: medium

Description:
In LiquidityPool's initiateWithdraw(), it's required that withdrawn value is above a minimum parameter, or that withdrawn tokens is above the minimum parameter.\\n```\\n      if (withdrawalValue < lpParams.minDepositWithdraw && \\n          amountLiquidityToken < lpParams.minDepositWithdraw) {\\n      revert MinimumWithdrawNotMet(address(this), withdrawalValue, lpParams.minDepositWithdraw);\\n      }\\n```\\n\\nThe issue is that minDepositWithdraw is measured in dollars while amountLiquidityToken is LP tokens. The intention was that if LP tokens lost value and a previous deposit is now worth less than minDepositWithdraw, it would still be withdrawable. However, the current implementation doesn't check for that correctly, since the LP to dollar exchange rate at deposit time is not known, and is practically being hardcoded as 1:1 here. The impact is that users may not be able to withdraw LP with the token amount that was above the minimum at deposit time, or vice versa

Impact:

Recommended mitigation:
Consider calculating an average exchange rate at which users have minted and use it to verify withdrawal amount is satisfactory.
Vulnerability: base to quote swaps trust GMX-provided minPrice and maxPrice to be correct, which may be manipulated
Severity: medium

Description:
exchangeFromExactBase() in GMXAdapter converts an amount of base to quote. It implements slippage protection by using the GMX vault's getMinPrice() and getMaxPrice() utilities. However, such protection is insufficient because GMX prices may be manipulated. Indeed, GMX supports “AMM pricing” mode where quotes are calculated from Uniswap reserves. A possible attack would be to drive up the base token (e.g. ETH) price, sell a large ETH amount to the GMXAdapter, and repay the flashloan used for manipulation. exchangeFromExactBase() is attacker-reachable from LiquidityPool's exchangeBase().\\n```\\n    uint tokenInPrice = _getMinPrice(address(baseAsset));\\n        uint tokenOutPrice = _getMaxPrice(address(quoteAsset));\\n    // rest of code\\n    uint minOut = tokenInPrice\\n      .multiplyDecimal(marketPricingParams[_optionMarket].minReturnPercent)\\n        .multiplyDecimal(_amountBase)\\n          .divideDecimal(tokenOutPrice);\\n```\\n

Impact:

Recommended mitigation:
Verify `getMinPrice()`, `getMinPrice()` outputs are close to Chainlink-provided prices as done in `getSpotPriceForMarket()`.
Vulnerability: recoverFunds() does not handle popular ERC20 tokens like BNB
Severity: medium

Description:
recoverFunds() is used for recovery in case of mistakenly-sent tokens. However, it uses unsafe transfer to send tokens back, which will not support 100s of non-compatible ERC20 tokens. Therefore it is likely unsupported tokens will be unrecoverable.\\n```\\n  if (token == quoteAsset || token == baseAsset || token == weth) {\\n      revert CannotRecoverRestrictedToken(address(this));\\n    }\\n        token.transfer(recipient, token.balanceOf(address(this)));\\n```\\n

Impact:

Recommended mitigation:
Use Open Zeppelin's SafeERC20 encapsulation of ERC20 transfer functions.
Vulnerability: setPositionRouter leaks approval to previous positionRouter
Severity: low

Description:
positionRouter is used to change GMX positions in GMXFuturesPoolHedger. It can be replaced by a new router if GMX redeploys, for example if a bug is found or the previous one is hacked. The new positionRouter receives approval from the contract. However, approval to the previous positionRouter is not revoked.\\n```\\n    function setPositionRouter(IPositionRouter _positionRouter) external onlyOwner {\\n      positionRouter = _positionRouter;\\n        router.approvePlugin(address(positionRouter));\\n    emit PositionRouterSet(_positionRouter);\\n       }\\n```\\n\\nA number of unlikely, yet dire scenarios could occur.

Impact:

Recommended mitigation:
Use router.denyPlugin() to remove privileges from the previous positionRouter.
Vulnerability: PoolHedger can receive ETH directly from anyone
Severity: low

Description:
A `receive()` function has been added to GMXFuturesPoolHedger, so that it is able to receive ETH from GMX as request refunds. However, it is not advisable to have an open `receive()` function if it is not necessary. Users may wrongly send ETH directly to PoolHedger and lose it forever.\\n```\\n           receive() external payable {}\\n```\\n

Impact:

Recommended mitigation:
Add a msg.sender check in the receive() function, and make sure sender is positionRouter.
Vulnerability: Lack of child rewarder reserves could lead to freeze of funds
Severity: high

Description:
In ComplexRewarder.sol, `onReward()` is used to distribute rewards for previous time period, using the complex rewarder and any child rewarders. If the complex rewarder does not have enough tokens to hand out the reward, it correctly stores the rewards owed in storage. However, child rewarded will attempt to hand out the reward and may revert:\\n```\\n   function onReward(uint _pid, address _user, address _to, uint, uint _amt) external override onlyParent nonReentrant {\\n      PoolInfo memory pool = updatePool(_pid);\\n         if (pool.lastRewardTime == 0) return;\\n            UserInfo storage user = userInfo[_pid][_user];\\n            uint pending;\\n         if (user.amount > 0) {\\n              pending = ((user.amount * pool.accRewardPerShare) / ACC_TOKEN_PRECISION) - user.rewardDebt;\\n      rewardToken.safeTransfer(_to, pending);\\n         }\\n         user.amount = _amt;\\n         user.rewardDebt = (_amt * pool.accRewardPerShare) / \\n            ACC_TOKEN_PRECISION;\\n      emit LogOnReward(_user, _pid, pending, _to);\\n      }\\n```\\n\\nImportantly, if the child rewarder fails, the parent's `onReward()` reverts too:\\n```\\n      uint len = childrenRewarders.length();\\n         for (uint i = 0; i < len; ) {\\n      IRewarder(childrenRewarders.at(i)).onReward(_pid, _user, _to, 0, \\n         _amt);\\n      unchecked {\\n         ++i;\\n         }\\n      }\\n```\\n\\nIn the worst-case scenario, this will lead the user's `withdraw()` call to V3 Vault, to revert.

Impact:

Recommended mitigation:
Introduce sufficient exception handling in the CompexRewarder.sol contract, so that `onReward()` would never fail.
Vulnerability: Wrong accounting of user's holdings allows theft of reward
Severity: high

Description:
In `deposit()`, `withdraw()` and `withdrawProfit()`, `rewarder.onReward()` is called for reward bookkeeping. It will transfer previous eligible rewards and update the current amount user has:\\n```\\n      user.amount = _amt;\\n      user.rewardDebt = (_amt * pool.accRewardPerShare) / ACC_TOKEN_PRECISION;\\n      user.rewardsOwed = rewardsOwed;\\n```\\n\\nIn `withdraw()`, there is a critical issue where `onReward()` is called too early:\\n```\\n      // Update rewarder for this user\\n          if (address(rewarder) != address(0)) {\\n      rewarder.onReward(0, msg.sender, msg.sender, pending, user.amount);\\n      }\\n      // Burn baby burn\\n            _burn(msg.sender, _shares);\\n      // User accounting\\n                uint256 userAmount = balanceOf(msg.sender);\\n      // - Underlying (Frontend ONLY)\\n            if (userAmount == 0) {\\n               user.amount = 0;\\n         } else {\\n            user.amount -= r;\\n         }\\n```\\n\\nThe new _amt which will be stored in reward contract's user.amount is vault's user.amount, before decrementing the withdrawn amount. Therefore, the withdrawn amount is still gaining rewards even though it's no longer in the contract. Effectively it is stealing the rewards of others, leading to reward insolvency. In order to exploit this flaw, attacker will deposit a larger amount and immediately withdraw it, except for one wei. When they would like to receive the rewards accrued for others, they will withdraw the remaining wei, which will trigger `onReward()`, which will calculate and send pending awards for the previously withdrawn amount.

Impact:

Recommended mitigation:
Move the `onReward()` call to after user.amount is updated.
Vulnerability: Unsafe transferFrom breaks compatibility with 100s of ERC20 tokens
Severity: medium

Description:
In Ninja vaults, the delegated strategy sends profit tokens to the vault using `depositProfitTokenForUsers()`. The vault transfers the tokens in using:\\n```\\n         // Now pull in the tokens (Should have permission)\\n          // We only want to pull the tokens with accounting\\n                profitToken.transferFrom(strategy, address(this), _amount);\\n          emit ProfitReceivedFromStrategy(_amount);\\n```\\n\\nThe issue is that the code doesn't use the `safeTransferFrom()` utility from SafeERC20. Therefore, profitTokens that don't return a bool in `transferFrom()` will cause a revert which means they are stuck in the strategy. Examples of such tokens are USDT, BNB, among hundreds of other tokens.

Impact:

Recommended mitigation:
Use `safeTransferFrom()` from SafeERC20.sol
Vulnerability: Attacker can force partial withdrawals to fail
Severity: medium

Description:
In Ninja vaults, users call `withdraw()` to take back their deposited tokens. There is bookkeeping on remaining amount:\\n```\\n      uint256 userAmount = balanceOf(msg.sender);\\n         // - Underlying (Frontend ONLY)\\n            if (userAmount == 0) {\\n            user.amount = 0;\\n         } else {\\n         user.amount -= r;\\n      }\\n```\\n\\nIf the withdraw is partial (some tokens are left), user.amount is decremented by r.\\n```\\n      uint256 r = (balance() * _shares) / totalSupply();\\n```\\n\\nAbove, r is calculated as the relative share of the user's _shares of the total balance kept in the vault.\\nWe can see that user.amount is incremented in deposit().\\n```\\n      function deposit(uint256 _amount) public nonReentrant {\\n      …\\n            user.amount += _amount;\\n      …\\n         }\\n```\\n\\nThe issue is that the calculated r can be more than _amount , causing an overflow in `withdraw()` and freezing the withdrawal. All attacker needs to do is send a tiny amount of underlying token directly to the contract, to make the shares go out of sync.

Impact:

Recommended mitigation:
Redesign user structure, taking into account that balance of underlying can be externally manipulated
Vulnerability: Rewards may be stuck due to unchangeable slippage parameter
Severity: medium

Description:
In NyPtvFantomWftmBooSpookyV2StrategyToUsdc.sol, MAX_SLIPPAGE is used to limit slippage in trades of BOO tokens to USDC, for yield:\\n```\\n      function _swapFarmEmissionTokens() internal { IERC20Upgradeable boo = IERC20Upgradeable(BOO);\\n            uint256 booBalance = boo.balanceOf(address(this));\\n      if (booToUsdcPath.length < 2 || booBalance == 0) {\\n         return;\\n      }\\n         boo.safeIncreaseAllowance(SPOOKY_ROUTER, booBalance);\\n             uint256[] memory amounts = \\n      IUniswapV2Router02(SPOOKY_ROUTER).getAmountsOut(booBalance, booToUsdcPath);\\n          uint256 amountOutMin = (amounts[amounts.length - 1] * MAX_SLIPPAGE) / PERCENT_DIVISOR;\\n            IUniswapV2Router02(SPOOKY_ROUTER).swapExactTokensForTokensSupportingFeeOnTransferTokens( booBalance, amountOutMin, booToUsdcPath, address(this), block.timestamp );\\n                }\\n```\\n\\nIf slippage is not satisfied the entire transaction reverts. Since MAX_SLIPPAGE is constant, it is possible that harvesting of the strategy will be stuck, due to operations leading to too high of a slippage. For example, strategy might accumulate a large amount of BOO, or `harvest()` can be sandwich-attacked.

Impact:

Recommended mitigation:
Allow admin to set slippage after some timelock period.
Vulnerability: potential overflow in reward accumulator may freeze functionality
Severity: medium

Description:
Note the above description of `updatePool()` functionality. We can see that accRewardPerShare is only allocated 128 bits in PoolInfo:\\n```\\n      struct PoolInfo {\\n          uint128 accRewardPerShare;\\n            uint64 lastRewardTime;\\n               uint64 allocPoint;\\n```\\n\\nTherefore, even if truncation issues do not occur, it is likely that continuous incrementation of the counter would cause accRewardPerShare to overflow, which would freeze vault functionalities such as withdrawal.

Impact:

Recommended mitigation:
Steal 32 bits from lastRewardTime and 32 bits from allocPoint to make the accumulator have 192 bits, which should be enough for safe calculations.