msc-smart-contract-auditing/vulnerable-functions-base huggingface dataset:
Vulnerability: Users can be liquidated prematurely because calculation understates value of underlying position
Severity: high

Description:
When the value of the underlying asset is calculated in `getPositionRisk()`, it uses the `underlyingAmount`, which is the amount of tokens initially deposited, without any adjustment for the interest earned. This can result in users being liquidated early, because the system undervalues their assets.\\nA position is considered liquidatable if it meets the following criteria:\\n```\\n((borrowsValue - collateralValue) / underlyingValue) >= underlyingLiqThreshold\\n```\\n\\nThe value of the underlying tokens is a major factor in this calculation. However, the calculation of the underlying value is performed with the following function call:\\n```\\nuint256 cv = oracle.getUnderlyingValue(\\n    pos.underlyingToken,\\n    pos.underlyingAmount\\n);\\n```\\n\\nIf we trace it back, we can see that `pos.underlyingAmount` is set when `lend()` is called (ie when underlying assets are deposited). This is the only place in the code where this value is moved upward, and it is only increased by the amount deposited. It is never moved up to account for the interest payments made on the deposit, which can materially change the value.

Impact:
Users can be liquidated prematurely because the value of their underlying assets are calculated incorrectly.

Recommended mitigation:
Value of the underlying assets should be derived from the vault shares and value, rather than being stored directly.
Vulnerability: BlueBerryBank#withdrawLend will cause underlying token accounting error if soft/hard vault has withdraw fee
Severity: high

Description:
Soft/hard vaults can have a withdraw fee. This takes a certain percentage from the user when they withdraw. The way that the token accounting works in BlueBerryBank#withdrawLend, it will only remove the amount returned by the hard/soft vault from pos.underlying amount. If there is a withdraw fee, underlying amount will not be decrease properly and the user will be left with phantom collateral that they can still use.\\n```\\n    // Cut withdraw fee if it is in withdrawVaultFee Window (2 months)\\n    if (\\n        block.timestamp <\\n        config.withdrawVaultFeeWindowStartTime() +\\n            config.withdrawVaultFeeWindow()\\n    ) {\\n        uint256 fee = (withdrawAmount * config.withdrawVaultFee()) /\\n            DENOMINATOR;\\n        uToken.safeTransfer(config.treasury(), fee);\\n        withdrawAmount -= fee;\\n    }\\n```\\n\\nBoth SoftVault and HardVault implement a withdraw fee. Here we see that withdrawAmount (the return value) is decreased by the fee amount.\\n```\\n    uint256 wAmount;\\n    if (address(ISoftVault(bank.softVault).uToken()) == token) {\\n        ISoftVault(bank.softVault).approve(\\n            bank.softVault,\\n            type(uint256).max\\n        );\\n        wAmount = ISoftVault(bank.softVault).withdraw(shareAmount);\\n    } else {\\n        wAmount = IHardVault(bank.hardVault).withdraw(token, shareAmount);\\n    }\\n\\n    wAmount = wAmount > pos.underlyingAmount\\n        ? pos.underlyingAmount\\n        : wAmount;\\n\\n    pos.underlyingVaultShare -= shareAmount;\\n    pos.underlyingAmount -= wAmount;\\n    bank.totalLend -= wAmount;\\n```\\n\\nThe return value is stored as `wAmount` which is then subtracted from `pos.underlyingAmount` the issue is that the withdraw fee has now caused a token accounting error for `pos`. We see that the fee paid to the hard/soft vault is NOT properly removed from `pos.underlyingAmount`. This leaves the user with phantom underlying which doesn't actually exist but that the user can use to take out loans.\\nExmaple: For simplicity let's say that 1 share = 1 underlying and the soft/hard vault has a fee of 5%. Imagine a user deposits 100 underlying to receive 100 shares. Now the user withdraws their 100 shares while the hard/soft vault has a withdraw. This burns 100 shares and softVault/hardVault.withdraw returns 95 (100 - 5). During the token accounting pos.underlyingVaultShares are decreased to 0 but pos.underlyingAmount is still equal to 5 (100 - 95).\\n```\\n  uint256 cv = oracle.getUnderlyingValue(\\n      pos.underlyingToken,\\n      pos.underlyingAmount\\n  );\\n```\\n\\nThis accounting error is highly problematic because collateralValue uses pos.underlyingAmount to determine the value of collateral for liquidation purposes. This allows the user to take on more debt than they should.

Impact:
User is left with collateral that isn't real but that can be used to take out a loan

Recommended mitigation:
`HardVault/SoftVault#withdraw` should also return the fee paid to the vault, so that it can be accounted for.
Vulnerability: IchiLpOracle is extemely easy to manipulate due to how IchiVault calculates underlying token balances
Severity: high

Description:
`IchiVault#getTotalAmounts` 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. Given that the protocol specializes in leverage, the effects of this manipulation would compound to make malicious uses even easier.\\nICHIVault.sol\\n```\\nfunction _amountsForLiquidity(\\n    int24 tickLower,\\n    int24 tickUpper,\\n    uint128 liquidity\\n) internal view returns (uint256, uint256) {\\n    (uint160 sqrtRatioX96, , , , , , ) = IUniswapV3Pool(pool).slot0();\\n    return\\n        UV3Math.getAmountsForLiquidity(\\n            sqrtRatioX96,\\n            UV3Math.getSqrtRatioAtTick(tickLower),\\n            UV3Math.getSqrtRatioAtTick(tickUpper),\\n            liquidity\\n        );\\n}\\n```\\n\\n`IchiVault#getTotalAmounts` uses the `UniV3Pool.slot0` to determine the number of tokens it has in it's position. slot0 is the most recent data point and can easily be manipulated.\\n`IchiLPOracle` directly uses the token values returned by `vault#getTotalAmounts`. 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.

Impact:
Ichi LP value can be manipulated to cause loss of funds for the protocol and other users

Recommended mitigation:
Token balances should be calculated inside the oracle instead of getting them from the `IchiVault`. To determine the liquidity, use a TWAP instead of `slot0`.
Vulnerability: IchiLpOracle returns inflated price due to invalid calculation
Severity: medium

Description:
`IchiLpOracle` returns inflated price due to invalid calculation\\nIf you run the tests, then you can see that IchiLpOracle returns inflated price for the ICHI_USDC vault\\n```\\nSTATICCALL IchiLpOracle.getPrice(token=0xFCFE742e19790Dd67a627875ef8b45F17DB1DaC6) => (1101189125194558706411110851447)\\n```\\n\\nAs the documentation says, the token price should be in USD with 18 decimals of precision. The price returned here is `1101189125194_558706411110851447` This is 1.1 trillion USD when considering the 18 decimals.\\nThe test uses real values except for mocking ichi and usdc price, which are returned by the mock with correct decimals (1e18 and 1e6)

Impact:
`IchiLpOracle` price is used in `_validateMaxLTV` (collToken is the vault). Therefore the collateral value is inflated and users can open bigger positions than their collateral would normally allow.

Recommended mitigation:
Issue IchiLpOracle returns inflated price due to invalid calculation\\nFix the LP token price calculation. The problem is that you multiply totalReserve with extra 1e18 (return (totalReserve * 1e18) / totalSupply;).
Vulnerability: Complete debt size is not paid off for fee on transfer tokens, but users aren't warned
Severity: medium

Description:
The protocol seems to be intentionally catering to fee on transfer tokens by measuring token balances before and after transfers to determine the value received. However, the mechanism to pay the full debt will not succeed in paying off the debt if it is used with a fee on transfer token.\\nThe protocol is clearly designed to ensure it is compatible with fee on transfer tokens. For example, all functions that receive tokens check the balance before and after, and calculate the difference between these values to measure tokens received:\\n```\\nfunction doERC20TransferIn(address token, uint256 amountCall)\\n    internal\\n    returns (uint256)\\n{\\n    uint256 balanceBefore = IERC20Upgradeable(token).balanceOf(\\n        address(this)\\n    );\\n    IERC20Upgradeable(token).safeTransferFrom(\\n        msg.sender,\\n        address(this),\\n        amountCall\\n    );\\n    uint256 balanceAfter = IERC20Upgradeable(token).balanceOf(\\n        address(this)\\n    );\\n    return balanceAfter - balanceBefore;\\n}\\n```\\n\\nThere is another feature of the protocol, which is that when loans are being repaid, the protocol gives the option of passing `type(uint256).max` to pay your debt in full:\\n```\\nif (amountCall == type(uint256).max) {\\n    amountCall = oldDebt;\\n}\\n```\\n\\nHowever, these two features are not compatible. If a user paying off fee on transfer tokens passes in `type(uint256).max` to pay their debt in full, the full amount of their debt will be calculated. But when that amount is transferred to the contract, the amount that the result increases will be slightly less. As a result, the user will retain some balance that is not paid off.

Impact:
The feature to allow loans to be paid in full will silently fail when used with fee on transfer tokens, which may trick users into thinking they have completely paid off their loans, and accidentally maintaining a balance.

Recommended mitigation:
I understand that it would be difficult to implement a mechanism to pay fee on transfer tokens off in full. That adds a lot of complexity that is somewhat fragile.\\nThe issue here is that the failure is silent, so that users request to pay off their loan in full, get confirmation, and may not realize that the loan still has an outstanding balance with interest accruing.\\nTo solve this, there should be a confirmation that any user who passes `type(uint256).max` has paid off their debt in full. Otherwise, the function should revert, so that users paying fee on transfer tokens know that they cannot use the "pay in full" feature and must specify the correct amount to get their outstanding balance down to zero.
Vulnerability: HardVault never deposits assets to Compound
Severity: medium

Description:
While the protocol states that all underlying assets are deposited to their Compound fork to earn interest, it appears this action never happens in `HardVault.sol`.\\nThe documentation and comments seem to make clear that all assets deposited to `HardVault.sol` should be deposited to Compound to earn yield:\\n```\\n/**\\n    * @notice Deposit underlying assets on Compound and issue share token\\n    * @param amount Underlying token amount to deposit\\n    * @return shareAmount cToken amount\\n    */\\nfunction deposit(address token, uint256 amount) { // rest of code }\\n\\n/**\\n    * @notice Withdraw underlying assets from Compound\\n    * @param shareAmount Amount of cTokens to redeem\\n    * @return withdrawAmount Amount of underlying assets withdrawn\\n    */\\nfunction withdraw(address token, uint256 shareAmount) { // rest of code }\\n```\\n\\nHowever, if we examine the code in these functions, there is no movement of the assets to Compound. Instead, they sit in the Hard Vault and doesn't earn any yield.

Impact:
Users who may expect to be earning yield on their underlying tokens will not be.

Recommended mitigation:
Either add the functionality to the Hard Vault to have the assets pulled from the ERC1155 and deposited to the Compound fork, or change the comments and docs to be clear that such underlying assets will not be receiving any yield.
Vulnerability: BasicSpell.doCutRewardsFee uses depositFee instead of withdraw fee
Severity: medium

Description:
BasicSpell.doCutRewardsFee uses depositFee instead of withdraw fee\\n```\\n    function doCutRewardsFee(address token) internal {\\n        if (bank.config().treasury() == address(0)) revert NO_TREASURY_SET();\\n\\n\\n        uint256 balance = IERC20Upgradeable(token).balanceOf(address(this));\\n        if (balance > 0) {\\n            uint256 fee = (balance * bank.config().depositFee()) / DENOMINATOR;\\n            IERC20Upgradeable(token).safeTransfer(\\n                bank.config().treasury(),\\n                fee\\n            );\\n\\n\\n            balance -= fee;\\n            IERC20Upgradeable(token).safeTransfer(bank.EXECUTOR(), balance);\\n        }\\n    }\\n```\\n\\nThis function is called in order to get fee from ICHI rewards, collected by farming. But currently it takes `bank.config().depositFee()` instead of `bank.config().withdrawFee()`.

Impact:
Wrong fee amount is taken.

Recommended mitigation:
Issue BasicSpell.doCutRewardsFee uses depositFee instead of withdraw fee\\nTake withdraw fee from rewards.
Vulnerability: onlyEOAEx modifier that ensures call is from EOA might not hold true in the future
Severity: medium

Description:
modifier `onlyEOAEx` is used to ensure calls are only made from EOA. However, EIP 3074 suggests that using `onlyEOAEx` modifier to ensure calls are only from EOA might not hold true.\\nFor `onlyEOAEx`, `tx.origin` is used to ensure that the caller is from an EOA and not a smart contract.\\n```\\n    modifier onlyEOAEx() {\\n        if (!allowContractCalls && !whitelistedContracts[msg.sender]) {\\n            if (msg.sender != tx.origin) revert NOT_EOA(msg.sender);\\n        }\\n        _;\\n    }\\n```\\n\\nHowever, according to EIP 3074,\\nThis EIP introduces two EVM instructions AUTH and AUTHCALL. The first sets a context variable authorized based on an ECDSA signature. The second sends a call as the authorized account. This essentially delegates control of the externally owned account (EOA) to a smart contract.\\nTherefore, using tx.origin to ensure msg.sender is an EOA will not hold true in the event EIP 3074 goes through.

Impact:
Using modifier `onlyEOAEx` to ensure calls are made only from EOA will not hold true in the event EIP 3074 goes through.

Recommended mitigation:
```\\n    modifier onlyEOAEx() {\\n        if (!allowContractCalls && !whitelistedContracts[msg.sender]) {\\n            if (isContract(msg.sender)) revert NOT_EOA(msg.sender);\\n        }\\n        _;\\n    }\\n```\\n
Vulnerability: when issuer set new winner by calling setTierWinner() code should reset invoice and supporting documents for that tier
Severity: medium

Description:
if invoice or supporting documents are required to receive the winning prize then tier winner should provide them. bounty issuer or oracle would set invoice and supporting document status of a tier by calling `setInvoiceComplete()` and `setSupportingDocumentsComplete()`. bounty issuer can set tier winners by calling `setTierWinner()` but code won't reset the status of the invoice and supporting documents when tier winner changes. a malicious winner can bypass invoice and supporting document check by this issue.\\nif bounty issuer set invoice and supporting documents as required for the bounty winners in the tiered bounty, then tier winner should provide those and bounty issuer or off-chain oracle would set the status of the invoice and documents for that tier. but if issuer wants to change a tier winner and calls `setTierWinner()` code would changes the tier winner but won't reset the status of the invoice and supporting documents for the new winner. This is the `setTierWinner()` code in OpenQV1 and TieredBountyCore:\\n```\\n    function setTierWinner(\\n        string calldata _bountyId,\\n        uint256 _tier,\\n        string calldata _winner\\n    ) external {\\n        IBounty bounty = getBounty(_bountyId);\\n        require(msg.sender == bounty.issuer(), Errors.CALLER_NOT_ISSUER);\\n        bounty.setTierWinner(_winner, _tier);\\n\\n        emit TierWinnerSelected(\\n            address(bounty),\\n            bounty.getTierWinners(),\\n            new bytes(0),\\n            VERSION_1\\n        );\\n    }\\n\\n    function setTierWinner(string memory _winner, uint256 _tier)\\n        external\\n        onlyOpenQ\\n    {\\n        tierWinners[_tier] = _winner;\\n    }\\n```\\n\\nAs you can see code only sets the `tierWinner[tier]` and won't reset `invoiceComplete[tier]` or `supportingDocumentsComplete[tier]` to false. This would cause an issue when issuer wants to change the tier winner. these are the steps that makes the issue:\\nUserA creates tiered Bounty1 and set invoice and supporting documents as required for winners to claim their funds.\\nUserA would set User1 as winner of tier 1 and User1 completed the invoice and oracle would set `invoiceComplete[1]` = true.\\nUserA would change tier winner to User2 because User1 didn't complete supporting documents phase. now User2 is winner of tier 1 and `invoiceComplete[1]` is true and User2 only required to complete supporting documents and User2 would receive the win prize without completing the invoice phase.

Impact:
malicious winner can bypass invoice and supporting document check when they are required if he is replace as another person to be winner of a tier.

Recommended mitigation:
set status of the `invoiceComplete[tier]` or `supportingDocumentsComplete[tier]` to false in `setTierWinner()` function.
Vulnerability: Attackers can call UToken.redeem() and drain the funds in assetManager
Severity: medium

Description:
Attackers can call `UToken.redeem()` and drain the funds in assetManager, taking advantage of the following vulnerability: due to round error, it is possible that uTokenAmount = 0; the redeem()function does not check whether `uTokenAmount = 0` and will redeem the amount of `underlyingAmount` even when zero uTokens are burned.\\nConsider the following attack scenario:\\nSuppose `exchangeRate = 1000 WAD`, that is each utoken exchanges for 1000 underlying tokens.\\nAttacker B calls `redeem(0, 999)`, then the else-part of the following code will get executed:\\n```\\n if (amountIn > 0) {\\n            // We calculate the exchange rate and the amount of underlying to be redeemed:\\n            // uTokenAmount = amountIn\\n            // underlyingAmount = amountIn x exchangeRateCurrent\\n            uTokenAmount = amountIn;\\n            underlyingAmount = (amountIn * exchangeRate) / WAD;\\n        } else {\\n            // We get the current exchange rate and calculate the amount to be redeemed:\\n            // uTokenAmount = amountOut / exchangeRate\\n            // underlyingAmount = amountOut\\n            uTokenAmount = (amountOut * WAD) / exchangeRate;\\n            underlyingAmount = amountOut;\\n        }\\n```\\n\\nwe have `uTokenAmount = 999*WAD/1000WAD = 0`, and `underlyingAmount = 999`.\\nSince `redeeem()` does not check whether `uTokenAmount = 0` and the function will proceed. When finished, the attacker will get 999 underlying tokens, but burned no utokens. He stole 999 underlying tokens.\\nThe attacker can accomplish draining the `assetManger` by writing a malicious contract/function using a loop to run `redeem(0, exchangeRate/WAD-1)` multiple times (as long as not running of gas) and will be able to steal more funds in one SINGLE transaction. Running this transaction a few times will drain `assetManager` easily. This attack will be successful when `exchangeRate/WAD-1 > 0`. Here we need to consider that `exchangeRate` might change due to the decreasing of `totalReeemable`. So in each iteration, when we call `redeem(0, exchangeRate/WAD-1)`, the second argument is recalculated.

Impact:
An attacker can keep calling redeem() and drain the funds in `assetManager`.

Recommended mitigation:
Issue Attackers can call UToken.redeem() and drain the funds in assetManager\\nRevise `redeem()` so that it will revert when `uTokenAmount = 0`.
Vulnerability: Challenger can override the 7 day finalization period
Severity: medium

Description:
All withdrawals are finalized after a 7 days window (finalization period). After this duration transaction are confirmed and user can surely withdraw their balance. But due to lack of check, challenger can delete a l2Output which is older than 7 days meaning withdrawals will stop working for even confirmed transaction\\nProposer has proposed L2 output for a _l2BlockNumber which creates entries on l2Outputs using the proposeL2Output. Assume this creates a new l2Output at index X\\n```\\nl2Outputs.push(\\n            Types.OutputProposal({\\n                outputRoot: _outputRoot,\\n                timestamp: uint128(block.timestamp),\\n                l2BlockNumber: uint128(_l2BlockNumber)\\n            })\\n        );\\n```\\n\\nproveWithdrawalTransaction has been called for user linked to this l2Output\\nFinalization period(7 day) is over after proposal and Users is ready to call `finalizeWithdrawalTransaction` to withdraw their funds\\nSince confirmation is done, User A is sure that he will be able to withdraw and thinks to do it after coming back from his holidays\\nChallenger tries to delete the index X (Step 1), ideally it should not be allowed as already confirmed. But since there is no such timeline check so the l2Output gets deleted\\n```\\nfunction deleteL2Outputs(uint256 _l2OutputIndex) external {\\n        require(\\n            msg.sender == CHALLENGER,\\n            "L2OutputOracle: only the challenger address can delete outputs"\\n        );\\n\\n        // Make sure we're not *increasing* the length of the array.\\n        require(\\n            _l2OutputIndex < l2Outputs.length,\\n            "L2OutputOracle: cannot delete outputs after the latest output index"\\n        );\\n\\n        uint256 prevNextL2OutputIndex = nextOutputIndex();\\n\\n        // Use assembly to delete the array elements because Solidity doesn't allow it.\\n        assembly {\\n            sstore(l2Outputs.slot, _l2OutputIndex)\\n        }\\n\\n        emit OutputsDeleted(prevNextL2OutputIndex, _l2OutputIndex);\\n    }\\n```\\n\\nUser comes back and now tries to withdraw but the withdraw fails since the l2Output index X does not exist anymore. This is incorrect and nullifies the network guarantee.\\nNote: In case of a separate output root could be proven then user withdrawal will permanently stuck. Ideally if such anomaly could not be caught within finalization period then user should be allowed to withdraw

Impact:
Withdrawal will fail for confirmed transaction

Recommended mitigation:
Add below check in\\n```\\nrequire(getL2Output(_l2OutputIndex).timestamp<=FINALIZATION_PERIOD_SECONDS, "Output already confirmed");\\n```\\n