msc-smart-contract-auditing/vulnerable-functions-base huggingface dataset:
Vulnerability: FundRateArbitrage is vulnerable to inflation attacks
Severity: medium

Description:
When index is calculated, it is figured by dividing the net value of the contract (including USDC held) by the current supply of earnUSDC. Through deposit and donation this ratio can be inflated. Then when others deposit, their deposit can be taken almost completely via rounding.\\nFundingRateArbitrage.sol#L98-L104\\n```\\nfunction getIndex() public view returns (uint256) {\\n    if (totalEarnUSDCBalance == 0) {\\n        return 1e18;\\n    } else {\\n        return SignedDecimalMath.decimalDiv(getNetValue(), totalEarnUSDCBalance);\\n    }\\n}\\n```\\n\\nIndex is calculated is by dividing the net value of the contract (including USDC held) by the current supply of totalEarnUSDCBalance. This can be inflated via donation. Assume the user deposits 1 share then donates 100,000e6 USDC. The exchange ratio is now 100,000e18 which causes issues during deposits.\\nFundingRateArbitrage.sol#L258-L275\\n```\\nfunction deposit(uint256 amount) external {\\n    require(amount != 0, "deposit amount is zero");\\n    uint256 feeAmount = amount.decimalMul(depositFeeRate);\\n    if (feeAmount > 0) {\\n        amount -= feeAmount;\\n        IERC20(usdc).transferFrom(msg.sender, owner(), feeAmount);\\n    }\\n    uint256 earnUSDCAmount = amount.decimalDiv(getIndex());\\n    IERC20(usdc).transferFrom(msg.sender, address(this), amount);\\n    JOJODealer(jojoDealer).deposit(0, amount, msg.sender);\\n    earnUSDCBalance[msg.sender] += earnUSDCAmount;\\n    jusdOutside[msg.sender] += amount;\\n    totalEarnUSDCBalance += earnUSDCAmount;\\n    require(getNetValue() <= maxNetValue, "net value exceed limitation");\\n    uint256 quota = maxUsdcQuota[msg.sender] == 0 ? defaultUsdcQuota : maxUsdcQuota[msg.sender];\\n    require(earnUSDCBalance[msg.sender].decimalMul(getIndex()) <= quota, "usdc amount bigger than quota");\\n    emit DepositToHedging(msg.sender, amount, feeAmount, earnUSDCAmount);\\n}\\n```\\n\\nNotice earnUSDCAmount is amount / index. With the inflated index that would mean that any deposit under 100,000e6 will get zero shares, making it exactly like the standard ERC4626 inflation attack.

Impact:
Subsequent user deposits can be stolen

Recommended mitigation:
Use a virtual offset as suggested by OZ for their ERC4626 contracts
Vulnerability: _isExternalLendingUnhealthy() using stale factors
Severity: medium

Description:
In `checkRebalance()` -> _isExternalLendingUnhealthy() -> getTargetExternalLendingAmount(factors) using stale `factors` will lead to inaccurate `targetAmount`, which in turn will cause `checkRebalance()` that should have been rebalance to not execute.\\nrebalancingBot uses `checkRebalance()` to return the `currencyIds []` that need to be `rebalance`.\\ncall order : `checkRebalance()` -> `_isExternalLendingUnhealthy()` -> `ExternalLending.getTargetExternalLendingAmount(factors)`\\n```\\n    function _isExternalLendingUnhealthy(\\n        uint16 currencyId,\\n        IPrimeCashHoldingsOracle oracle,\\n        PrimeRate memory pr\\n    ) internal view returns (bool isExternalLendingUnhealthy, OracleData memory oracleData, uint256 targetAmount) {\\n// rest of code\\n\\n      PrimeCashFactors memory factors = PrimeCashExchangeRate.getPrimeCashFactors(currencyId);\\n        Token memory underlyingToken = TokenHandler.getUnderlyingToken(currencyId);\\n\\n        targetAmount = ExternalLending.getTargetExternalLendingAmount(\\n            underlyingToken, factors, rebalancingTargetData, oracleData, pr\\n        );\\n```\\n\\nA very important logic is to get `targetAmount`. The calculation of this value depends on `factors`. But currently used is PrimeCashFactors memory `factors` = PrimeCashExchangeRate.getPrimeCashFactors(currencyId);. This is not the latest. It has not been aggregated yet. The correct one should be `( /* */,factors) = PrimeCashExchangeRate.getPrimeCashRateView();`.

Impact:
Due to the incorrect `targetAmount`, it may cause the `currencyId` that should have been re-executed `Rebalance` to not execute `rebalance`, increasing the risk of the protocol.

Recommended mitigation:
```\\n    function _isExternalLendingUnhealthy(\\n        uint16 currencyId,\\n        IPrimeCashHoldingsOracle oracle,\\n        PrimeRate memory pr\\n    ) internal view returns (bool isExternalLendingUnhealthy, OracleData memory oracleData, uint256 targetAmount) {\\n// rest of code\\n\\n// Remove the line below\\n       PrimeCashFactors memory factors = PrimeCashExchangeRate.getPrimeCashFactors(currencyId);\\n// Add the line below\\n       ( /* */,PrimeCashFactors memory factors) = PrimeCashExchangeRate.getPrimeCashRateView();\\n        Token memory underlyingToken = TokenHandler.getUnderlyingToken(currencyId);\\n\\n        targetAmount = ExternalLending.getTargetExternalLendingAmount(\\n            underlyingToken, factors, rebalancingTargetData, oracleData, pr\\n        );\\n```\\n
Vulnerability: recover() using the standard transfer may not be able to retrieve some tokens
Severity: medium

Description:
in `SecondaryRewarder.recover()` Using the standard `IERC20.transfer()` If `REWARD_TOKEN` is like `USDT`, it will not be able to transfer out, because this kind of `token` does not return `bool` This will cause it to always `revert`\\n`SecondaryRewarder.recover()` use for\\nAllows the Notional owner to recover any tokens sent to the address or any reward tokens remaining on the contract in excess of the total rewards emitted.\\n```\\n    function recover(address token, uint256 amount) external onlyOwner {\\n        if (Constants.ETH_ADDRESS == token) {\\n            (bool status,) = msg.sender.call{value: amount}("");\\n            require(status);\\n        } else {\\n          IERC20(token).transfer(msg.sender, amount);\\n        }\\n    }\\n```\\n\\nUsing the standard `IERC20.transfer()` method to execute the transfer A `token` of a type similar to `USDT` has no return value This will cause the execution of the transfer to always fail

Impact:
If `REWARD_TOKEN` is like `USDT`, it will not be able to transfer out.

Recommended mitigation:
```\\n    function recover(address token, uint256 amount) external onlyOwner {\\n        if (Constants.ETH_ADDRESS == token) {\\n            (bool status,) = msg.sender.call{value: amount}("");\\n            require(status);\\n        } else {\\n// Remove the line below\\n          IERC20(token).transfer(msg.sender, amount);\\n// Add the line below\\n          GenericToken.safeTransferOut(token,msg.sender,amount);\\n        }\\n    }\\n```\\n
Vulnerability: getOracleData() maxExternalDeposit not accurate
Severity: medium

Description:
in `getOracleData()` The calculation of `maxExternalDeposit` lacks consideration for `reserve.accruedToTreasury`. This leads to `maxExternalDeposit` being too large, causing `Treasury.rebalance()` to fail.\\nin `getOracleData()`\\n```\\n    function getOracleData() external view override returns (OracleData memory oracleData) {\\n// rest of code\\n        (/* */, uint256 supplyCap) = IPoolDataProvider(POOL_DATA_PROVIDER).getReserveCaps(underlying);\\n        // Supply caps are returned as whole token values\\n        supplyCap = supplyCap * UNDERLYING_PRECISION;\\n        uint256 aTokenSupply = IPoolDataProvider(POOL_DATA_PROVIDER).getATokenTotalSupply(underlying);\\n\\n        // If supply cap is zero, that means there is no cap on the pool\\n        if (supplyCap == 0) {\\n            oracleData.maxExternalDeposit = type(uint256).max;\\n        } else if (supplyCap <= aTokenSupply) {\\n            oracleData.maxExternalDeposit = 0;\\n        } else {\\n            // underflow checked as consequence of if / else statement\\n          oracleData.maxExternalDeposit = supplyCap - aTokenSupply;\\n        }\\n```\\n\\nHowever, AAVE's restrictions are as follows: ValidationLogic.sol#L81-L88\\n```\\n    require(\\n      supplyCap == 0 ||\\n        ((IAToken(reserveCache.aTokenAddress).scaledTotalSupply() +\\n          uint256(reserve.accruedToTreasury)).rayMul(reserveCache.nextLiquidityIndex) + amount) <=\\n        supplyCap * (10 ** reserveCache.reserveConfiguration.getDecimals()),\\n      Errors.SUPPLY_CAP_EXCEEDED\\n    );\\n  }\\n```\\n\\nThe current implementation lacks subtraction of `uint256(reserve.accruedToTreasury)).rayMul(reserveCache.nextLiquidityIndex)`.

Impact:
An overly large `maxExternalDeposit` may cause `rebalance()` to be unable to execute.

Recommended mitigation:
subtract `uint256(reserve.accruedToTreasury)).rayMul(reserveCache.nextLiquidityIndex)`
Vulnerability: `wfCashERC4626`
Severity: medium

Description:
The `wfCash` vault is credited less prime cash than the `wfCash` it mints to the depositor when its underlying asset is a fee-on-transfer token. This leads to the vault being insolvent because it has issued more shares than can be redeemed.\\n```\\n    if (maxFCash < fCashAmount) {\\n        // NOTE: lending at zero\\n        uint256 fCashAmountExternal = fCashAmount * precision / uint256(Constants.INTERNAL_TOKEN_PRECISION);\\n        require(fCashAmountExternal <= depositAmountExternal);\\n\\n        // NOTE: Residual (depositAmountExternal - fCashAmountExternal) will be transferred\\n        // back to the account\\n        NotionalV2.depositUnderlyingToken{value: msgValue}(address(this), currencyId, fCashAmountExternal);\\n    } else if (isETH || hasTransferFee || getCashBalance() > 0) {\\n```\\n\\n```\\n        } else {\\n            // In the case of deposits, we use a balance before and after check\\n            // to ensure that we record the proper balance change.\\n            actualTransferExternal = GenericToken.safeTransferIn(\\n                underlying.tokenAddress, account, underlyingExternalDeposit\\n            ).toInt();\\n        }\\n\\n        netPrimeSupplyChange = _postTransferPrimeCashUpdate(\\n            account, currencyId, actualTransferExternal, underlying, primeRate\\n        );\\n```\\n\\n```\\n    // Mints ERC20 tokens for the receiver\\n    _mint(receiver, fCashAmount);\\n```\\n\\nIn the case of lending at 0% interest, `fCashAmount` is equal to `depositAmount` but at 1e8 precision.\\nTo simplify the example, let us assume that there are no other depositors. When the sole depositor redeems all their `wfCash` shares at maturity, they will be unable to redeem all their shares because the `wfCash` vault does not hold enough prime cash.

Impact:
Although the example used to display the vulnerability is for the case of lending at 0% interest, the issue exists for minting any amount of shares.\\nThe `wfCashERC4626` vault will become insolvent and unable to buy back all shares. The larger the total amount deposited, the larger the deficit. The deficit is equal to the transfer fee. Given a total deposit amount of 100M USDT and a transfer fee of 2% (assuming a transfer fee was set and enabled for USDT), 2M USDT will be the deficit.\\nThe last depositors to redeem their shares will be shouldering the loss.

Recommended mitigation:
Consider adding the following:\\nA flag in `wfCashERC4626` that signals that the vault's asset is a fee-on-transfer token.\\nIn `wfCashERC4626._previewMint()` and `wfCashERC46262._previewDeposit`, all calculations related to `assets` should account for the transfer fee of the token.
Vulnerability: `StakingRewardsManager::topUp(...)` Misallocates Funds to `StakingRewards` Contracts
Severity: high

Description:
The `StakingRewardsManager::topUp(...)` contract exhibits an issue where the specified `StakingRewards` contracts are not topped up at the correct indices, resulting in an incorrect distribution to different contracts.\\nThe `StakingRewardsManager::topUp(...)` function is designed to top up multiple `StakingRewards` contracts simultaneously by taking the indices of the contract's addresses in the `StakingRewardsManager::stakingContracts` array. However, the flaw lies in the distribution process:\\n```\\n    function topUp(\\n        address source,\\n        uint256[] memory indices\\n    ) external onlyRole(EXECUTOR_ROLE) {\\n        for (uint i = 0; i < indices.length; i++) {\\n            // get staking contract and config\\n            StakingRewards staking = stakingContracts[i];\\n            StakingConfig memory config = stakingConfigs[staking];\\n\\n            // will revert if block.timestamp <= periodFinish\\n            staking.setRewardsDuration(config.rewardsDuration);\\n\\n            // pull tokens from owner of this contract to fund the staking contract\\n            rewardToken.transferFrom(\\n                source,\\n                address(staking),\\n                config.rewardAmount\\n            );\\n\\n            // start periods\\n            staking.notifyRewardAmount(config.rewardAmount);\\n\\n            emit ToppedUp(staking, config);\\n        }\\n    }\\n```\\n\\nGitHub: [254-278]\\nThe rewards are not appropriately distributed to the `StakingRewards` contracts at the specified indices. Instead, they are transferred to the contracts at the loop indices. For instance, if intending to top up contracts at indices `[1, 2]`, the actual top-up occurs at indices `[0, 1]`.

Impact:
The consequence of this vulnerability is that rewards will be distributed to the incorrect staking contract, leading to potential misallocation and unintended outcomes

Recommended mitigation:
It is recommended to do the following changes:\\n```\\n    function topUp(\\n        address source,\\n        uint256[] memory indices\\n    ) external onlyRole(EXECUTOR_ROLE) {\\n        for (uint i = 0; i < indices.length; i// Add the line below\\n// Add the line below\\n) {\\n            // get staking contract and config\\n// Remove the line below\\n            StakingRewards staking = stakingContracts[i];\\n// Add the line below\\n           StakingRewards staking = stakingContracts[indices[i]];\\n            StakingConfig memory config = stakingConfigs[staking];\\n\\n            // will revert if block.timestamp <= periodFinish\\n            staking.setRewardsDuration(config.rewardsDuration);\\n\\n            // pull tokens from owner of this contract to fund the staking contract\\n            rewardToken.transferFrom(\\n                source,\\n                address(staking),\\n                config.rewardAmount\\n            );\\n\\n            // start periods\\n            staking.notifyRewardAmount(config.rewardAmount);\\n\\n            emit ToppedUp(staking, config);\\n        }\\n    }\\n```\\n
Vulnerability: When migrating the owner users will lose their rewards
Severity: medium

Description:
When a user migrates the owner due to a lost private key, the rewards belonging to the previous owner remain recorded in their account and cannot be claimed, resulting in the loss of user rewards.\\nAccording to the documentation, `migrateUser()` is used when a user loses their private key to migrate the old vesting owner to a new owner.\\n```\\n    /**\\n     * @notice Migrate owner of vesting. Used when user lost his private key\\n     * @dev Only admin can migrate users vesting\\n     * @param categoryId Category id\\n     * @param vestingId Vesting id\\n     * @param prevUser previous user address\\n     * @param newUser new user address\\n     */\\n```\\n\\nIn this function, the protocol calls `migrateVestingLock()` to obtain a new ID.\\n```\\n    if (lockupId != 0) {\\n            newLockupId = veTRUF.migrateVestingLock(prevUser, newUser, lockupId - 1) + 1;\\n            lockupIds[categoryId][vestingId][newUser] = newLockupId;\\n            delete lockupIds[categoryId][vestingId][prevUser];\\n\\n            newVesting.locked = prevVesting.locked;\\n        }\\n```\\n\\nHowever, in the `migrateVestingLock()` function, the protocol calls `stakingRewards.withdraw()` to withdraw the user's stake, burning points. In the `withdraw()` function, the protocol first calls `updateReward()` to update the user's rewards and records them in the user's account.\\n```\\n    function withdraw(address user, uint256 amount) public updateReward(user) onlyOperator {\\n        if (amount == 0) {\\n            revert ZeroAmount();\\n        }\\n        _totalSupply -= amount;\\n        _balances[user] -= amount;\\n        emit Withdrawn(user, amount);\\n    }\\n```\\n\\nHowever, `stakingRewards.withdraw()` is called with the old owner as a parameter, meaning that the rewards will be updated on the old account.\\n```\\n  uint256 points = oldLockup.points;\\n        stakingRewards.withdraw(oldUser, points);\\n        _burn(oldUser, points);\\n```\\n\\nAs mentioned earlier, the old owner has lost their private key and cannot claim the rewards, resulting in the loss of these rewards.

Impact:
The user's rewards are lost

Recommended mitigation:
When migrating the owner, the rewards belonging to the previous owner should be transferred to the new owner.
Vulnerability: Ended locks can be extended
Severity: medium

Description:
When a lock period ends, it can be extended. If the new extension 'end' is earlier than the current block.timestamp, the user will have a lock that can be unstaked at any time."\\nWhen the lock period ends, the owner of the expired lock can extend it to set a new lock end that is earlier than the current block.timestamp. By doing so, the lock owner can create a lock that is unstakeable at any time.\\nThis is doable because there are no checks in the extendLock function that checks whether the lock is already ended or not.\\nPoC:\\n```\\nfunction test_ExtendLock_AlreadyEnded() external {\\n        uint256 amount = 100e18;\\n        uint256 duration = 5 days;\\n\\n        _stake(amount, duration, alice, alice);\\n\\n        // 5 days later, lock is ended for Alice\\n        skip(5 days + 1);\\n\\n        (,, uint128 _ends,,) = veTRUF.lockups(alice, 0);\\n\\n        // Alice's lock is indeed ended\\n        assertTrue(_ends < block.timestamp, "lock is ended");\\n\\n        // 36 days passed  \\n        skip(36 days);\\n\\n        // Alice extends her already finished lock 30 more days\\n        vm.prank(alice);\\n        veTRUF.extendLock(0, 30 days);\\n\\n        (,,_ends,,) = veTRUF.lockups(alice, 0);\\n\\n        // Alice's lock can be easily unlocked right away\\n        assertTrue(_ends < block.timestamp, "lock is ended");\\n\\n        // Alice unstakes her lock, basically alice can unstake her lock anytime she likes\\n        vm.prank(alice);\\n        veTRUF.unstake(0);\\n    }\\n```\\n

Impact:
The owner of the lock will achieve points that he can unlock anytime. This is clearly a gaming of the system and shouldn't be acceptable behaviour. A locker having a "lock" that can be unstaked anytime will be unfair for the other lockers. Considering this, I'll label this as high.

Recommended mitigation:
Do not let extension of locks that are already ended.
Vulnerability: Incorrect ProtocolOwnedLiquidityOhm calculation due to inclusion of other user's reserves
Severity: high

Description:
ProtocolOwnedLiquidityOhm for Bunni can include the liquidity deposited by other users which is not protocol owned\\nThe protocol owned liquidity in Bunni is calculated as the sum of reserves of all the BunniTokens\\n```\\n    function getProtocolOwnedLiquidityOhm() external view override returns (uint256) {\\n\\n        uint256 len = bunniTokens.length;\\n        uint256 total;\\n        for (uint256 i; i < len; ) {\\n            TokenData storage tokenData = bunniTokens[i];\\n            BunniLens lens = tokenData.lens;\\n            BunniKey memory key = _getBunniKey(tokenData.token);\\n\\n        // rest of code// rest of code// rest of code\\n\\n            total += _getOhmReserves(key, lens);\\n            unchecked {\\n                ++i;\\n            }\\n        }\\n\\n\\n        return total;\\n    }\\n```\\n\\nThe deposit function of Bunni allows any user to add liquidity to a token. Hence the returned reserve will contain amounts other than the reserves that actually belong to the protocol\\n```\\n    // @audit callable by any user\\n    function deposit(\\n        DepositParams calldata params\\n    )\\n        external\\n        payable\\n        virtual\\n        override\\n        checkDeadline(params.deadline)\\n        returns (uint256 shares, uint128 addedLiquidity, uint256 amount0, uint256 amount1)\\n    {\\n    }\\n```\\n

Impact:
Incorrect assumption of the protocol owned liquidity and hence the supply. An attacker can inflate the liquidity reserves The wider system relies on the supply calculation to be correct in order to perform actions of economical impact

Recommended mitigation:
Guard the deposit function in BunniHub or compute the liquidity using shares belonging to the protocol
Vulnerability: Balancer LP valuation methodologies use the incorrect supply metric
Severity: medium

Description:
In various Balancer LP valuations, totalSupply() is used to determine the total LP supply. However this is not the appropriate method for determining the supply. Instead getActualSupply should be used instead. Depending on the which pool implementation and how much LP is deployed, the valuation can be much too high or too low. Since the RBS pricing is dependent on this metric. It could lead to RBS being deployed at incorrect prices.\\nAuraBalancerSupply.sol#L345-L362\\n```\\nuint256 balTotalSupply = pool.balancerPool.totalSupply();\\nuint256[] memory balances = new uint256[](_vaultTokens.length);\\n// Calculate the proportion of the pool balances owned by the polManager\\nif (balTotalSupply != 0) {\\n    // Calculate the amount of OHM in the pool owned by the polManager\\n    // We have to iterate through the tokens array to find the index of OHM\\n    uint256 tokenLen = _vaultTokens.length;\\n    for (uint256 i; i < tokenLen; ) {\\n        uint256 balance = _vaultBalances[i];\\n        uint256 polBalance = (balance * balBalance) / balTotalSupply;\\n\\n\\n        balances[i] = polBalance;\\n\\n\\n        unchecked {\\n            ++i;\\n        }\\n    }\\n}\\n```\\n\\nTo value each LP token the contract divides the valuation of the pool by the total supply of LP. This in itself is correct, however the totalSupply method for a variety of Balancer pools doesn't accurately reflect the true LP supply. If we take a look at a few Balancer pools we can quickly see the issue:\\nThis pool shows a max supply of 2,596,148,429,273,858 whereas the actual supply is 6454.48. In this case the LP token would be significantly undervalued. If a sizable portion of the reserves are deployed in an affected pool the backing per OHM would appear to the RBS system to be much lower than it really is. As a result it can cause the RBS to deploy its funding incorrectly, potentially selling/buying at a large loss to the protocol.

Impact:
Pool LP can be grossly under/over valued

Recommended mitigation:
Use a try-catch block to always query getActualSupply on each pool to make sure supported pools use the correct metric.