msc-smart-contract-auditing/vulnerable-functions-base huggingface dataset:
Vulnerability: Fee share calculation is incorrect
Severity: medium

Description:
Fees are given to the feeRecipient by minting them shares. The current share calculation is incorrect and always mints too many shares the fee recipient, giving them more fees than they should get.\\nThe current equation is incorrect and will give too many shares, which is demonstrated in the example below.\\nExample:\\n```\\n_supplied = 100\\n_totalSupply = 100\\n\\n_interest = 10\\nfee = 2\\n```\\n\\nCalculate the fee with the current equation:\\n```\\n_accuredFeeShares = fee * _totalSupply / supplied = 2 * 100 / 100 = 2\\n```\\n\\nThis yields 2 shares. Next calculate the value of the new shares:\\n```\\n2 * 110 / 102 = 2.156\\n```\\n\\nThe value of these shares yields a larger than expected fee. Using a revised equation gives the correct amount of fees:\\n```\\n_accuredFeeShares = (_totalSupply * fee) / (_supplied + _interest - fee) = 2 * 100 / (100 + 10 - 2) = 1.852\\n\\n1.852 * 110 / 101.852 = 2\\n```\\n\\nThis new equation yields the proper fee of 2.

Impact:
Fee recipient is given more fees than intended, which results in less interest for LPs

Recommended mitigation:
Use the modified equation shown above:\\n```\\n    uint fee = _interest * _feeMantissa / 1e18;\\n    // 13. Calculate the accrued fee shares\\n-   _accruedFeeShares = fee * _totalSupply / _supplied; // if supplied is 0, we will have returned at step 7\\n+   _accruedFeeShares = fee * (_totalSupply * fee) / (_supplied + _interest - fee); // if supplied is 0, we will have returned at step 7\\n    // 14. Update the total supply\\n    _currentTotalSupply += _accruedFeeShares;\\n```\\n
Vulnerability: cachedUserRewards variable is never reset, so user can steal all rewards
Severity: high

Description:
cachedUserRewards variable is never reset, so user can steal all rewards\\nWhen user wants to withdraw then `_withdrawUpdateRewardState` function is called. This function updates internal reward state and claims rewards for user if he provided `true` as `claim_` param.\\n```\\n            if (rewardDebtDiff > userRewardDebts[msg.sender][rewardToken.token]) {\\n                userRewardDebts[msg.sender][rewardToken.token] = 0;\\n                cachedUserRewards[msg.sender][rewardToken.token] +=\\n                    rewardDebtDiff -\\n                    userRewardDebts[msg.sender][rewardToken.token];\\n            } else {\\n                userRewardDebts[msg.sender][rewardToken.token] -= rewardDebtDiff;\\n            }\\n```\\n\\nWhen user calls claimRewards, then `cachedUserRewards` variable is added to the rewards he should receive. The problem is that `cachedUserRewards` variable is never reset to 0, once user claimed that amount.\\nBecause of that he can claim multiple times in order to receive all balance of token.

Impact:
User can steal all rewards

Recommended mitigation:
Once user received rewards, reset `cachedUserRewards` variable to 0. This can be done inside `_claimInternalRewards` function.
Vulnerability: Vault can experience long downtime periods
Severity: medium

Description:
The chainlink price could stay up to 24 hours (heartbeat period) outside the boundaries defined by `THRESHOLD` but within the chainlink deviation threshold. Deposits and withdrawals will not be possible during this period of time.\\nThe `_isPoolSafe()` function checks if the balancer pool spot price is within the boundaries defined by `THRESHOLD` respect to the last fetched chainlink price.\\nSince in `_valueCollateral()` the `updateThreshold` should be 24 hours (as in the tests), then the OHM derived oracle price could stay at up to 2% from the on-chain trusted price. The value is 2% because in WstethLiquidityVault.sol#L223:\\n```\\nreturn (amount_ * stethPerWsteth * stethUsd * decimalAdjustment) / (ohmEth * ethUsd * 1e18);\\n```\\n\\n`stethPerWsteth` is mostly stable and changes in `stethUsd` and `ethUsd` will cancel out, so the return value changes will be close to changes in `ohmEth`, so up to 2% from the on-chain trusted price.\\nIf `THRESHOLD` < 2%, say 1% as in the tests, then the Chainlink price can deviate by more than 1% from the pool spot price and less than 2% from the on-chain trusted price fro up to 24 h. During this period withdrawals and deposits will revert.

Impact:
Withdrawals and deposits can be often unavailable for several hours.

Recommended mitigation:
Issue Vault can experience long downtime periods\\n`THRESHOLD` is not fixed and can be changed by the admin, meaning that it can take different values over time.Only a tight range of values around 2% should be allowed to avoid the scenario above.
Vulnerability: SingleSidedLiquidityVault._accumulateInternalRewards will revert with underflow error if rewardToken.lastRewardTime is bigger than current time
Severity: medium

Description:
SingleSidedLiquidityVault._accumulateInternalRewards will revert with underflow error if rewardToken.lastRewardTime is bigger than current time\\n```\\n    function _accumulateInternalRewards() internal view returns (uint256[] memory) {\\n        uint256 numInternalRewardTokens = internalRewardTokens.length;\\n        uint256[] memory accumulatedInternalRewards = new uint256[](numInternalRewardTokens);\\n\\n\\n        for (uint256 i; i < numInternalRewardTokens; ) {\\n            InternalRewardToken memory rewardToken = internalRewardTokens[i];\\n\\n\\n            uint256 totalRewards;\\n            if (totalLP > 0) {\\n                uint256 timeDiff = block.timestamp - rewardToken.lastRewardTime;\\n                totalRewards = (timeDiff * rewardToken.rewardsPerSecond);\\n            }\\n\\n\\n            accumulatedInternalRewards[i] = totalRewards;\\n\\n\\n            unchecked {\\n                ++i;\\n            }\\n        }\\n\\n\\n        return accumulatedInternalRewards;\\n    }\\n```\\n\\nThe line is needed to see is this `uint256 timeDiff = block.timestamp - rewardToken.lastRewardTime`. In case if `rewardToken.lastRewardTime > block.timestamp` than function will revert and ddos functions that use it.\\n```\\n    function addInternalRewardToken(\\n        address token_,\\n        uint256 rewardsPerSecond_,\\n        uint256 startTimestamp_\\n    ) external onlyRole("liquidityvault_admin") {\\n        InternalRewardToken memory newInternalRewardToken = InternalRewardToken({\\n            token: token_,\\n            decimalsAdjustment: 10**ERC20(token_).decimals(),\\n            rewardsPerSecond: rewardsPerSecond_,\\n            lastRewardTime: block.timestamp > startTimestamp_ ? block.timestamp : startTimestamp_,\\n            accumulatedRewardsPerShare: 0\\n        });\\n\\n\\n        internalRewardTokens.push(newInternalRewardToken);\\n    }\\n```\\n\\nIn case if `startTimestamp_` is in the future, then it will be set and cause that problem. lastRewardTime: block.timestamp > `startTimestamp_` ? block.timestamp : `startTimestamp_`.\\nNow till, `startTimestamp_` time, `_accumulateInternalRewards` will not work, so vault will be stopped. And of course, admin can remove that token and everything will be fine. That's why i think this is medium.

Impact:
SingleSidedLiquidityVault will be blocked

Recommended mitigation:
Skip token if it's `lastRewardTime` is in future.
Vulnerability: Protection sellers can bypass withdrawal delay mechanism and avoid losing funds when loans are defaulted by creating withdrawal request in each cycle
Severity: high

Description:
To prevent protection sellers from withdrawing fund immediately when protected lending pools are defaults, there is withdrawal delay mechanism, but it's possible to bypass it by creating withdraw request in each cycle by doing so user can withdraw in each cycle's open state. there is no penalty for users when they do this or there is no check to avoid this.\\nThis is `_requestWithdrawal()` code:\\n```\\n  function _requestWithdrawal(uint256 _sTokenAmount) internal {\\n    uint256 _sTokenBalance = balanceOf(msg.sender);\\n    if (_sTokenAmount > _sTokenBalance) {\\n      revert InsufficientSTokenBalance(msg.sender, _sTokenBalance);\\n    }\\n\\n    /// Get current cycle index for this pool\\n    uint256 _currentCycleIndex = poolCycleManager.getCurrentCycleIndex(\\n      address(this)\\n    );\\n\\n    /// Actual withdrawal is allowed in open period of cycle after next cycle\\n    /// For example: if request is made in at some time in cycle 1,\\n    /// then withdrawal is allowed in open period of cycle 3\\n    uint256 _withdrawalCycleIndex = _currentCycleIndex + 2;\\n\\n    WithdrawalCycleDetail storage withdrawalCycle = withdrawalCycleDetails[\\n      _withdrawalCycleIndex\\n    ];\\n\\n    /// Cache existing requested amount for the cycle for the sender\\n    uint256 _oldRequestAmount = withdrawalCycle.withdrawalRequests[msg.sender];\\n    withdrawalCycle.withdrawalRequests[msg.sender] = _sTokenAmount;\\n\\n    unchecked {\\n      /// Update total requested withdrawal amount for the cycle considering existing requested amount\\n      if (_oldRequestAmount > _sTokenAmount) {\\n        withdrawalCycle.totalSTokenRequested -= (_oldRequestAmount -\\n          _sTokenAmount);\\n      } else {\\n        withdrawalCycle.totalSTokenRequested += (_sTokenAmount -\\n          _oldRequestAmount);\\n      }\\n    }\\n\\n    emit WithdrawalRequested(msg.sender, _sTokenAmount, _withdrawalCycleIndex);\\n  }\\n```\\n\\nAs you can see it doesn't keep track of user current withdrawal requests and user can request withdrawal for all of his balance in each cycle and by doing so user can set `withdrawalCycleDetails[Each Cycle][User]` to user's sToken balance. and whenever user wants to withdraw he only need to wait until the end of the current cycle while he should have waited until next cycle end.

Impact:
protection sellers can request withdraw in each cycle for their full sToken balance and code would allow them to withdraw in each cycle end time because code doesn't track how much of the balance of users is requested for withdrawals in the past.

Recommended mitigation:
To avoid this code should keep track of user balance that is not in withdraw delay and user balance that are requested for withdraw. and to prevent users from requesting withdrawing and not doing it protocol should have some penalties for withdrawals, for example the waiting withdraw balance shouldn't get reward in waiting duration.
Vulnerability: Lending pool state transition will be broken when pool is expired in late state
Severity: high

Description:
Lending pool state transition will be broken when pool is expired in late state\\n```\\n  function _getLendingPoolStatus(address _lendingPoolAddress)\\n    internal\\n    view\\n    returns (LendingPoolStatus)\\n  {\\n    if (!_isReferenceLendingPoolAdded(_lendingPoolAddress)) {\\n      return LendingPoolStatus.NotSupported;\\n    }\\n\\n\\n    ILendingProtocolAdapter _adapter = _getLendingProtocolAdapter(\\n      _lendingPoolAddress\\n    );\\n\\n\\n    if (_adapter.isLendingPoolExpired(_lendingPoolAddress)) {\\n      return LendingPoolStatus.Expired;\\n    }\\n\\n\\n    if (\\n      _adapter.isLendingPoolLateWithinGracePeriod(\\n        _lendingPoolAddress,\\n        Constants.LATE_PAYMENT_GRACE_PERIOD_IN_DAYS\\n      )\\n    ) {\\n      return LendingPoolStatus.LateWithinGracePeriod;\\n    }\\n\\n\\n    if (_adapter.isLendingPoolLate(_lendingPoolAddress)) {\\n      return LendingPoolStatus.Late;\\n    }\\n\\n\\n    return LendingPoolStatus.Active;\\n  }\\n```\\n\\nAs you can see, pool is expired if time of credit line has ended or loan is fully paid.\\nState transition for lending pool is done inside `DefaultStateManager._assessState` function. This function is responsible to lock capital, when state is late and unlock it when it's changed from late to active again.\\nBecause the first state that is checked is `expired` there can be few problems.\\nFirst problem. Suppose that lending pool is in late state. So capital is locked. There are 2 options now: payment was done, so pool becomes active and capital unlocked, payment was not done then pool has defaulted. But in case when state is late, and lending pool expired or loan is fully repaid(so it's also becomes expired), then capital will not be unlocked as there is no such transition Late -> Expired. The state will be changed to Expired and no more actions will be done. Also in this case it's not possible to detect if lending pool expired because of time or because no payment was done.\\nSecond problem. Lending pool is in active state. Last payment should be done some time before `_creditLine.termEndTime()`. Payment was not done, which means that state should be changed to Late and capital should be locked, but state was checked when loan has ended, so it became Expired and again there is no such transition that can detect that capital should be locked in this case. The state will be changed to Expired and no more actions will be done.

Impact:
Depending on situation, capital can be locked forever or protection buyers will not be compensated.

Recommended mitigation:
These are tricky cases, think about transition for lending pool in such cases.
Vulnerability: If unlocked capital in pool falls below minRequiredCapital, then protection can be bought for minimum premium
Severity: medium

Description:
If the unlocked capital in a pool falls below the minRequiredCapital, then protection can be bought for minimum premium\\nIn PremiumCalculator.calculatePremium, we see that if the risk factor "cannot be calculated," it uses the minimum premium.\\n```\\n    if (\\n      RiskFactorCalculator.canCalculateRiskFactor(\\n        _totalCapital,\\n        _leverageRatio,\\n        _poolParameters.leverageRatioFloor,\\n        _poolParameters.leverageRatioCeiling,\\n        _poolParameters.minRequiredCapital\\n      )\\n    ) {\\n      // rest of code\\n    } else {\\n      /// This means that the risk factor cannot be calculated because of either\\n      /// min capital not met or leverage ratio out of range.\\n      /// Hence, the premium is the minimum premium\\n      _isMinPremium = true;\\n    }\\n```\\n\\nIn RiskFactor.canCalculateRiskFactor, we see there are three conditions when this is so:\\n```\\n  function canCalculateRiskFactor(\\n    uint256 _totalCapital,\\n    uint256 _leverageRatio,\\n    uint256 _leverageRatioFloor,\\n    uint256 _leverageRatioCeiling,\\n    uint256 _minRequiredCapital\\n  ) external pure returns (bool _canCalculate) {\\n    if (\\n      _totalCapital < _minRequiredCapital ||\\n      _leverageRatio < _leverageRatioFloor ||\\n      _leverageRatio > _leverageRatioCeiling\\n    ) {\\n      _canCalculate = false;\\n    } else {\\n      _canCalculate = true;\\n    }\\n  }\\n}\\n```\\n\\nIf the leverage ratio is above the ceiling, then protection should be very cheap, and it is correct to use the minimum premium. If the leverage ratio is above the floor, then protection cannot be purchased.\\nHowever, we see that the minimum premium is also used if _totalCapital is below _minRequiredCapital. In this case, protection should be very expensive, but it will instead be very cheap.

Impact:
Buyers can get very cheap protection at a time when it should be expensive.

Recommended mitigation:
Issue If unlocked capital in pool falls below minRequiredCapital, then protection can be bought for minimum premium\\nProhibit protection purchases when capital falls below the minimum required capital
Vulnerability: secondary markets are problematic with how `lockCapital` works
Severity: medium

Description:
Seeing that a pool is about to lock, an attacker can use a flash loan from a secondary market like uniswap to claim the share of a potential unlock of capital later.\\nThe timestamp a pool switches to `Late` can be predicted and an attacker can use this to call `assessState` which is callable by anyone. This will trigger the pool to move from Active/LateWithinGracePeriod to `Late` calling `lockCapital` on the ProtectionPool:\\n```\\nFile: ProtectionPool.sol\\n\\n   /// step 1: Capture protection pool's current investors by creating a snapshot of the token balance by using ERC20Snapshot in SToken\\n   _snapshotId = _snapshot();\\n```\\n\\nThis records who is holding sTokens at this point in time. If the borrower makes a payment and the pool turns back to Active, later the locked funds will be available to claim for the sToken holders at that snapshot:\\n```\\nFile: DefaultStateManager.sol\\n\\n       /// The claimable amount for the given seller is proportional to the seller's share of the total supply at the snapshot\\n       /// claimable amount = (seller's snapshot balance / total supply at snapshot) * locked capital amount\\n       _claimableUnlockedCapital =\\n         (_poolSToken.balanceOfAt(_seller, _snapshotId) *\\n           lockedCapital.amount) /\\n         _poolSToken.totalSupplyAt(_snapshotId);\\n```\\n\\nFrom docs:\\nIf sellers wish to redeem their capital and interest before the lockup period, they might be able to find a buyer of their sToken in a secondary market like Uniswap. Traders in the exchanges can long/short sTokens based on their opinion about the risk exposure associated with sTokens. Since an sToken is a fungible ERC20 token, it is fairly easy to bootstrap the secondary markets for protection sellers.\\nIf there is a uniswap (or similar) pool for this sToken, an attacker could potentially, using a flash loan, trigger the switch to `Late` and since they will be the ones holding the sTokens at the point of locking they will be the ones that can claim the funds at a potential unlock.

Impact:
An attacker can, using a flash loan from a secondary market like uniswap, steal a LPs possible share of unlocked tokens. Only paying taking the risk of the flash loan fee.

Recommended mitigation:
I recommend you make `assessState` only callable by a trusted user. This would remove the attack vector, since you must hold the tokens over a transaction. It would still be possible to use the withdraw bug, but if that is fixed this would remove the possibility to "flash-lock".
Vulnerability: Sandwich attack to accruePremiumAndExpireProtections()
Severity: high

Description:
Let's show how a malicious user, Bob, can launch a sandwich attack to `accruePremiumAndExpireProtections()` and profit.\\nSuppose there are 1,000,000 underlying tokens for the `ProtectionPool`, and `totalSupply = 1,000,000`, therefore the exchange rate is 1/1 share. Suppose Bob has 100,000 shares.\\nSuppose `accruePremiumAndExpireProtections()` is going to be called and add 100,000 to `totalSTokenUnderlying` at L346.\\nBob front-runs `accruePremiumAndExpireProtections()` and calls `deposit()` to deposit 100,000 underlying tokens into the contract. The check for `ProtectionPoolPhase` will pass for an open phase. As a result, there are 1,100,000 underlying tokens, and 1,100,000 shares, the exchange rate is still 1/1 share. Bob now has 200,000 shares.\\n```\\n function deposit(uint256 _underlyingAmount, address _receiver)\\n    external\\n    override\\n    whenNotPaused\\n    nonReentrant\\n  {\\n    _deposit(_underlyingAmount, _receiver);\\n  }\\n\\n  function _deposit(uint256 _underlyingAmount, address _receiver) internal {\\n    /// Verify that the pool is not in OpenToBuyers phase\\n    if (poolInfo.currentPhase == ProtectionPoolPhase.OpenToBuyers) {\\n      revert ProtectionPoolInOpenToBuyersPhase();\\n    }\\n\\n    uint256 _sTokenShares = convertToSToken(_underlyingAmount);\\n    totalSTokenUnderlying += _underlyingAmount;\\n    _safeMint(_receiver, _sTokenShares);\\n    poolInfo.underlyingToken.safeTransferFrom(\\n      msg.sender,\\n      address(this),\\n      _underlyingAmount\\n    );\\n\\n    /// Verify leverage ratio only when total capital/sTokenUnderlying is higher than minimum capital requirement\\n    if (_hasMinRequiredCapital()) {\\n      /// calculate pool's current leverage ratio considering the new deposit\\n      uint256 _leverageRatio = calculateLeverageRatio();\\n\\n      if (_leverageRatio > poolInfo.params.leverageRatioCeiling) {\\n        revert ProtectionPoolLeverageRatioTooHigh(_leverageRatio);\\n      }\\n    }\\n\\n    emit ProtectionSold(_receiver, _underlyingAmount);\\n }\\n```\\n\\nNow accruePremiumAndExpireProtections()gets called and 100,000 is added to `totalSTokenUnderlying` at L346. As a result, we have 1,200,000 underlying tokens with 1,100,000 shares. The exchange rate becomes 12/11 share.\\nBob calls the `withdraw()` function (assume he made a request two cycles back, he could do that since he had 100,000 underlying tokens in the pool) to withdraw 100,000 shares and he will get `100,000*12/11 = 109,090` underlying tokens. So he has a profit of 9,090 underlying tokens by the sandwich attack.

Impact:
A malicious user can launch a sandwich attack to accruePremiumAndExpireProtections()and profit.

Recommended mitigation:
Create a new contract as a temporary place to store the accrued premium, and then deliver it to the `ProtectionPool` over a period of time (delivery period) with some `premiumPerSecond` to lower the incentive of a quick profit by sandwich attack.\\nRestrict the maximum deposit amount for each cycle.\\nRestrict the maximum withdraw amount for each cycle.
Vulnerability: LP tokens are not sent back to withdrawing user
Severity: high

Description:
When users withdraw their assets from `IchiVaultSpell.sol`, the function unwinds their position and sends them back their assets, but it never sends them back the amount they requested to withdraw, leaving the tokens stuck in the Spell contract.\\nWhen a user withdraws from `IchiVaultSpell.sol`, they either call `closePosition()` or `closePositionFarm()`, both of which make an internal call to `withdrawInternal()`.\\nThe following arguments are passed to the function:\\nstrategyId: an index into the `strategies` array, which specifies the Ichi vault in question\\ncollToken: the underlying token, which is withdrawn from Compound\\namountShareWithdraw: the number of underlying tokens to withdraw from Compound\\nborrowToken: the token that was borrowed from Compound to create the position, one of the underlying tokens of the vault\\namountRepay: the amount of the borrow token to repay to Compound\\namountLpWithdraw: the amount of the LP token to withdraw, rather than trade back into borrow tokens\\nIn order to accomplish these goals, the contract does the following...\\nRemoves the LP tokens from the ERC1155 holding them for collateral.\\n```\\ndoTakeCollateral(strategies[strategyId].vault, lpTakeAmt);\\n```\\n\\nCalculates the number of LP tokens to withdraw from the vault.\\n```\\nuint256 amtLPToRemove = vault.balanceOf(address(this)) - amountLpWithdraw;\\nvault.withdraw(amtLPToRemove, address(this));\\n```\\n\\nConverts the non-borrowed token that was withdrawn in the borrowed token (not copying the code in, as it's not relevant to this issue).\\nWithdraw the underlying token from Compound.\\n```\\ndoWithdraw(collToken, amountShareWithdraw);\\n```\\n\\nPay back the borrowed token to Compound.\\n```\\ndoRepay(borrowToken, amountRepay);\\n```\\n\\nValidate that this situation does not put us above the maxLTV for our loans.\\n```\\n_validateMaxLTV(strategyId);\\n```\\n\\nSends the remaining borrow token that weren't paid back and withdrawn underlying tokens to the user.\\n```\\ndoRefund(borrowToken);\\ndoRefund(collToken);\\n```\\n\\nCrucially, the step of sending the remaining LP tokens to the user is skipped, even though the function specifically does the calculations to ensure that `amountLpWithdraw` is held back from being taken out of the vault.

Impact:
Users who close their positions and choose to keep LP tokens (rather than unwinding the position for the constituent tokens) will have their LP tokens stuck permanently in the IchiVaultSpell contract.

Recommended mitigation:
Add an additional line to the `withdrawInternal()` function to refund all LP tokens as well:\\n```\\n  doRefund(borrowToken);\\n  doRefund(collToken);\\n// Add the line below\\n doRefund(address(vault));\\n```\\n