msc-smart-contract-auditing/vulnerable-functions-base huggingface dataset:
Vulnerability: Anyone can withdraw user's Velo Deposit NFT after approval is given to depositor
Severity: high

Description:
`Depositor#withdrawFromGauge` is a public function that can be called by anyone which transfers token to `msg.sender`. `withdrawFromGauge` burns the NFT to be withdrawn, which means that `Depositor` must either be approved or be in possession of the NFT. Since it doesn't transfer the NFT to the contract before burning the user must either send the NFT to the `Depositor` or `approve` the `Depositor` in a separate transaction. After the NFT is either transferred or approved, a malicious user could withdraw the NFT for themselves.\\n```\\nfunction withdrawFromGauge(uint256 _NFTId, address[] memory _tokens)  public  {\\n    uint256 amount = depositReceipt.pooledTokens(_NFTId);\\n    depositReceipt.burn(_NFTId);\\n    gauge.getReward(address(this), _tokens);\\n    gauge.withdraw(amount);\\n    //AMMToken adheres to ERC20 spec meaning it reverts on failure, no need to check return\\n    //slither-disable-next-line unchecked-transfer\\n    AMMToken.transfer(msg.sender, amount);\\n}\\n```\\n\\n`Depositor#withdrawFromGauge` allows anyone to call it, burning the NFT and sending `msg.sender` the withdrawn tokens.\\n```\\nfunction burn(uint256 _NFTId) external onlyMinter{\\n    require(_isApprovedOrOwner(msg.sender, _NFTId), "ERC721: caller is not token owner or approved");\\n    delete pooledTokens[_NFTId];\\n    delete relatedDepositor[_NFTId];\\n    _burn(_NFTId);\\n}\\n```\\n\\n`Depositor` calls `DepositReceipt_Base#burn`, which means that it must be either the owner or approved for the NFT. Since `Depositor#withdrawFromGauge` doesn't transfer the NFT from the user, this must happen in a separate transaction. Between the user approval/transfer and them calling `Depositor#withdrawFromGauge` a malicious user could call `Depositor#withdrawFromGauge` first to withdraw the NFT and steal the users funds. This would be very easy to automate with a bot.\\nExample: `User A` deposits 100 underlying into their `Depositor` and is given `Token A` which represents their deposit. After some time they want to redeem `Token A` so they `Approve` their `Depositor` for `Token A`. `User B` sees the approval and quickly calls `Depositor#withdrawFromGauge` to withdraw `Token A`. `User B` is sent the 100 tokens and `Token A` is burned from `User A`.

Impact:
Users attempting to withdraw can have their funds stolen

Recommended mitigation:
Only allow owner of NFT to withdraw it:\\n```\\n    function withdrawFromGauge(uint256 _NFTId, address[] memory _tokens)  public  {\\n+       require(depositReceipt.ownerOf(_NFTId) == msg.sender);\\n        uint256 amount = depositReceipt.pooledTokens(_NFTId);\\n        depositReceipt.burn(_NFTId);\\n        gauge.getReward(address(this), _tokens);\\n        gauge.withdraw(amount);\\n        //AMMToken adheres to ERC20 spec meaning it reverts on failure, no need to check return\\n        //slither-disable-next-line unchecked-transfer\\n        AMMToken.transfer(msg.sender, amount);\\n    }\\n```\\n
Vulnerability: Lyra vault underestimates the collateral value
Severity: medium

Description:
Lyra vault subtracts the withdrawal fee while calculating the collateral value in USD, and it does not match the actual Lyra Pool implementation.\\nThe user's collateral value is estimated using the function `priceCollateralToUSD()` at `Vault_Lyra.sol#L77` as follows.\\n```\\nfunction priceCollateralToUSD(bytes32 _currencyKey, uint256 _amount) public view override returns(uint256){\\n        //The LiquidityPool associated with the LP Token is used for pricing\\n    ILiquidityPoolAvalon LiquidityPool = ILiquidityPoolAvalon(collateralBook.liquidityPoolOf(_currencyKey));\\n    //we have already checked for stale greeks so here we call the basic price function.\\n    uint256 tokenPrice = LiquidityPool.getTokenPrice();\\n    uint256 withdrawalFee = _getWithdrawalFee(LiquidityPool);\\n    uint256 USDValue  = (_amount * tokenPrice) / LOAN_SCALE;\\n    //we remove the Liquidity Pool withdrawalFee\\n    //as there's no way to remove the LP position without paying this.\\n    uint256 USDValueAfterFee = USDValue * (LOAN_SCALE- withdrawalFee)/LOAN_SCALE;\\n    return(USDValueAfterFee);\\n}\\n```\\n\\nSo it is understood that the withdrawal fee is removed to get the reasonable value of the collateral. But according to the Lyra Pool implementation, the token price used for withdrawal is calculated using the function `_getTotalBurnableTokens`. And the function `_getTotalBurnableTokens` is as belows.\\n```\\nfunction _getTotalBurnableTokens()\\n    internal\\n    returns (\\n      uint tokensBurnable,\\n      uint tokenPriceWithFee,\\n      bool stale\\n    )\\n  {\\n    uint burnableLiquidity;\\n    uint tokenPrice;\\n    (tokenPrice, stale, burnableLiquidity) = _getTokenPriceAndStale();\\n\\n    if (optionMarket.getNumLiveBoards() != 0) {\\n      tokenPriceWithFee = tokenPrice.multiplyDecimal(DecimalMath.UNIT - lpParams.withdrawalFee);\\n    } else {\\n      tokenPriceWithFee = tokenPrice;//@audit withdrawalFee is not applied if there are no live borads\\n    }\\n\\n    return (burnableLiquidity.divideDecimal(tokenPriceWithFee), tokenPriceWithFee, stale);\\n  }\\n```\\n\\nFrom the code, it is clear that the withdrawal fee is subtracted only when the related option market has live boards. Because `Vault_Lyra.sol` applies a withdrawal fee all the time to price the collateral, it means the user's collateral is under-valued.

Impact:
User's collaterals are under-valued than reasonable and might get to a liquidatable status sooner than expected. A liquidator can abuse this to get an unfair profit by liquidating the user's collateral with the under-estimated value and withdrawing it from the Lyra pool without paying a withdrawal fee.

Recommended mitigation:
Make sure to apply withdrawal fee consistent to how Lyra pool does.
Vulnerability: priceLiquidity() may not work if PriceFeed.aggregator() is updated
Severity: medium

Description:
priceLiquidity() may not work if PriceFeed.aggregator() is updated\\nIn the constructor of the DepositReceipt_* contract, the value of minAnswer/maxAnswer in priceFeed.aggregator() is obtained and assigned to *MinPrice/*MaxPrice as the maximum/minimum price limit when calling the getOraclePrice function in priceLiquidity, and *MinPrice/*MaxPrice can not change.\\n```\\n        IAccessControlledOffchainAggregator  aggregator = IAccessControlledOffchainAggregator(priceFeed.aggregator());\\n        //fetch the pricefeeds hard limits so we can be aware if these have been reached.\\n        tokenMinPrice = aggregator.minAnswer();\\n        tokenMaxPrice = aggregator.maxAnswer();\\n// rest of code\\n            uint256 oraclePrice = getOraclePrice(priceFeed, tokenMaxPrice, tokenMinPrice);\\n// rest of code\\n    function getOraclePrice(IAggregatorV3 _priceFeed, int192 _maxPrice, int192 _minPrice) public view returns (uint256 ) {\\n        (\\n            /*uint80 roundID*/,\\n            int signedPrice,\\n            /*uint startedAt*/,\\n            uint timeStamp,\\n            /*uint80 answeredInRound*/\\n        ) = _priceFeed.latestRoundData();\\n        //check for Chainlink oracle deviancies, force a revert if any are present. Helps prevent a LUNA like issue\\n        require(signedPrice > 0, "Negative Oracle Price");\\n        require(timeStamp >= block.timestamp - HEARTBEAT_TIME , "Stale pricefeed");\\n        require(signedPrice < _maxPrice, "Upper price bound breached");\\n        require(signedPrice > _minPrice, "Lower price bound breached");\\n```\\n\\nBut in the priceFeed contract, the address of the aggregator can be changed by the owner, which may cause the value of minAnswer/maxAnswer to change, and the price limit in the DepositReceipt_* contract to be invalid, and priceLiquidity() can not work.\\n```\\n  function confirmAggregator(address _aggregator)\\n    external\\n    onlyOwner()\\n  {\\n    require(_aggregator == address(proposedAggregator), "Invalid proposed aggregator");\\n    delete proposedAggregator;\\n    setAggregator(_aggregator);\\n  }\\n\\n\\n  /*\\n   * Internal\\n   */\\n\\n  function setAggregator(address _aggregator)\\n    internal\\n  {\\n    uint16 id = currentPhase.id + 1;\\n    currentPhase = Phase(id, AggregatorV2V3Interface(_aggregator));\\n    phaseAggregators[id] = AggregatorV2V3Interface(_aggregator);\\n  }\\n  // rest of code\\n    function aggregator()\\n    external\\n    view\\n    returns (address)\\n  {\\n    return address(currentPhase.aggregator);\\n  }\\n```\\n

Impact:

Recommended mitigation:
Consider getting latest priceFeed.aggregator().minAnswer()/maxAnswer() in priceLiquidity()
Vulnerability: The calculation of ````totalUSDborrowed```` in ````openLoan()```` is not correct
Severity: high

Description:
The `openLoan()` function wrongly use `isoUSDLoaned` to calculate `totalUSDborrowed`. Attacker can exploit it to bypass security check and loan isoUSD with no enough collateral.\\nvulnerability point\\n```\\nfunction openLoan(\\n    // // rest of code\\n    ) external override whenNotPaused \\n    {\\n    //// rest of code\\n    uint256 colInUSD = priceCollateralToUSD(currencyKey, _colAmount\\n                        + collateralPosted[_collateralAddress][msg.sender]);\\n    uint256 totalUSDborrowed = _USDborrowed \\n        +  (isoUSDLoaned[_collateralAddress][msg.sender] * virtualPrice)/LOAN_SCALE;\\n        // @audit should be isoUSDLoanAndInterest[_collateralAddress][msg.sender]\\n    require(totalUSDborrowed >= ONE_HUNDRED_DOLLARS, "Loan Requested too small");\\n    uint256 borrowMargin = (totalUSDborrowed * minOpeningMargin) / LOAN_SCALE;\\n    require(colInUSD >= borrowMargin, "Minimum margin not met!");\\n\\n    // // rest of code\\n}\\n```\\n\\nAttack example: <1>Attacker normally loans and produces 10000 isoUSD interest <2>Attacker repays principle but left interest <3>Attacker open a new 10000 isoUSD loan without providing collateral

Impact:
Attacker can loan isoUSD with no enough collateral.

Recommended mitigation:
See Vulnerability Detail
Vulnerability: User can steal rewards from other users by withdrawing their Velo Deposit NFTs from other users' depositors
Severity: high

Description:
Rewards from staking AMM tokens accumulate to the depositor used to deposit them. The rewards accumulated by a depositor are passed to the owner when they claim. A malicious user to steal the rewards from other users by manipulating other users depositors. Since any NFT of a DepositReceipt can be withdrawn from any depositor with the same DepositReceipt, a malicious user could mint an NFT on their depositor then withdraw in from another user's depositor. The net effect is that that the victims deposits will effectively be in the attackers depositor and they will collect all the rewards.\\n```\\nfunction withdrawFromGauge(uint256 _NFTId, address[] memory _tokens)  public  {\\n    uint256 amount = depositReceipt.pooledTokens(_NFTId);\\n    depositReceipt.burn(_NFTId);\\n    gauge.getReward(address(this), _tokens);\\n    gauge.withdraw(amount);\\n    //AMMToken adheres to ERC20 spec meaning it reverts on failure, no need to check return\\n    //slither-disable-next-line unchecked-transfer\\n    AMMToken.transfer(msg.sender, amount);\\n}\\n```\\n\\nEvery user must create a `Depositor` using `Templater` to interact with vaults and take loans. `Depositor#withdrawFromGauge` allows any user to withdraw any NFT that was minted by the same `DepositReciept`. This is where the issues arises. Since rewards are accumulated to the `Depositor` in which the underlying is staked a user can deposit to their `Depositor` then withdraw their NFT through the `Depositor` of another user's `Depositor` that uses the same `DepositReciept`. The effect is that the tokens will remained staked to the attackers `Depositor` allowing them to steal all the other user's rewards.\\nExample: `User A` and `User B` both create a `Depositor` for the same `DepositReciept`. Both users deposit 100 tokens into their respective `Depositors`. `User B` now calls `withdrawFromGauge` on `Depositor` A. `User B` gets their 100 tokens back and `Depositor B` still has 100 tokens deposited in it. `User B` cannot steal these tokens but they are now collecting the yield on all 100 tokens via `Depositor B` and `User A` isn't getting any rewards at all because `Depositor` A no longer has any tokens deposited into Velodrome gauge.

Impact:
Malicious user can steal other user's rewards

Recommended mitigation:
Depositors should only be able to burn NFTs that they minted. Change DepositReciept_Base#burn to enforce this:\\n```\\n    function burn(uint256 _NFTId) external onlyMinter{\\n+       //tokens must be burned by the depositor that minted them\\n+       address depositor = relatedDepositor[_NFTId];\\n+       require(depositor == msg.sender, "Wrong depositor");\\n        require(_isApprovedOrOwner(msg.sender, _NFTId), "ERC721: caller is not token owner or approved");\\n        delete pooledTokens[_NFTId];\\n        delete relatedDepositor[_NFTId];\\n        _burn(_NFTId);\\n    }\\n```\\n
Vulnerability: Vault_Base_ERC20#_updateVirtualPrice calculates interest incorrectly if updated frequently
Severity: medium

Description:
Updating the virtual price of an asset happens in discrete increments of 3 minutes. This is done to reduce the chance of DOS loops. The issue is that it updates the time to an incorrect timestamp. It should update to the truncated 3 minute interval but instead updates to the current timestamp. The result is that the interest calculation can be abused to lower effective interest rate.\\n```\\nfunction _updateVirtualPrice(uint256 _currentBlockTime, address _collateralAddress) internal { \\n    (   ,\\n        ,\\n        ,\\n        uint256 interestPer3Min,\\n        uint256 lastUpdateTime,\\n        uint256 virtualPrice,\\n\\n    ) = _getCollateral(_collateralAddress);\\n    uint256 timeDelta = _currentBlockTime - lastUpdateTime;\\n    //exit gracefully if two users call the function for the same collateral in the same 3min period\\n    //@audit increments \\n    uint256 threeMinuteDelta = timeDelta / 180; \\n    if(threeMinuteDelta > 0) {\\n        for (uint256 i = 0; i < threeMinuteDelta; i++ ){\\n        virtualPrice = (virtualPrice * interestPer3Min) / LOAN_SCALE; \\n        }\\n        collateralBook.vaultUpdateVirtualPriceAndTime(_collateralAddress, virtualPrice, _currentBlockTime);\\n    }\\n}\\n```\\n\\n_updateVirtualPrice is used to update the interest calculations for the specified collateral and is always called with block.timestamp. Due to truncation threeMinuteDelta is always rounded down, that is if there has been 1.99 3-minute intervals it will truncate to 1. The issue is that in the collateralBook#vaultUpdateVirtualPriceAndTime subcall the time is updated to block.timestamp (_currentBlockTime).\\nExample: lastUpdateTime = 1000 and block.timestamp (_currentBlockTime) = 1359.\\ntimeDelta = 1359 - 1000 = 359\\nthreeMinuteDelta = 359 / 180 = 1\\nThis updates the interest by only as single increment but pushes the new time forward 359 seconds. When called again it will use 1359 as lastUpdateTime which means that 179 seconds worth of interest have been permanently lost. Users with large loan positions could abuse this to effectively halve their interest accumulation. Given how cheap optimism transactions are it is highly likely this could be exploited profitably with a bot.

Impact:
Interest calculations will be incorrect if they are updated frequently, which can be abused by users with large amounts of debt to halve their accumulated interest

Recommended mitigation:
Before updating the interest time it should first truncate it to the closest 3-minute interval:\\n```\\n    if(threeMinuteDelta > 0) {\\n        for (uint256 i = 0; i < threeMinuteDelta; i++ ){\\n            virtualPrice = (virtualPrice * interestPer3Min) / LOAN_SCALE; \\n        }\\n+       _currentBlockTime = (_currentBlockTime / 180) * 180;\\n        collateralBook.vaultUpdateVirtualPriceAndTime(_collateralAddress, virtualPrice, _currentBlockTime);\\n    }\\n```\\n
Vulnerability: Outstanding loans cannot be closed or liquidated if collateral is paused
Severity: high

Description:
When a collateral is paused by governance, `collateralValid` is set to false. This causes closing and liquidating of loans to be impossible, leading to two issues. The first is that users with exist loans are unable to close their loans to recover their collateral. The second is that since debt is impossible to liquidate the protocol could end up being stuck with a lot of bad debt.\\n```\\nfunction pauseCollateralType(\\n    address _collateralAddress,\\n    bytes32 _currencyKey\\n    ) external collateralExists(_collateralAddress) onlyAdmin {\\n    require(_collateralAddress != address(0)); //this should get caught by the collateralExists check but just to be careful\\n    //checks two inputs to help prevent input mistakes\\n    require( _currencyKey == collateralProps[_collateralAddress].currencyKey, "Mismatched data");\\n    collateralValid[_collateralAddress] = false;\\n    collateralPaused[_collateralAddress] = true;\\n}\\n```\\n\\nWhen a collateral is paused `collateralValid[_collateralAddress]` is set to `false`. For `Vault_Lyra` `Vault_Synths` and `Vault_Velo` this will cause `closeLoan` and `callLiquidation` to revert. This traps existing users and prevents liquidations which will result in bad debt for the protocol

Impact:
Outstanding loans cannot be closed or liquidated, freezing user funds and causing the protocol to take on bad debt

Recommended mitigation:
Allow liquidations and loan closure when collateral is paused
Vulnerability: increaseCollateralAmount : User is not allowed to increase collateral freely.
Severity: medium

Description:
For all the tree type of vault, a user is allowed to increase collateral only if the overall collateral value is higher than the margin value.\\nimo, this restriction may not be needed. anyway user is adding the collateral that could eventually save from liquidation.\\nProtocol will loose advantage due to this restriction.\\nCodes from lyra vault implementation :\\nLine 184\\n```\\n    require(colInUSD >= borrowMargin, "Liquidation margin not met!");\\n```\\n\\nFor synth - Refer here\\nFor velo - Refer here

Impact:
User may not have the collateral all at once, but they can add like an EMI.\\nProtocol will loose the repayment anyway.\\nWhat is no one comes for liquidation - again this could lose.

Recommended mitigation:
Allow user add collateral freely.
Vulnerability: Dangerous assumption on the peg of USDC can lead to manipulations
Severity: medium

Description:
Dangerous assumption on the peg of USDC can lead to manipulations\\nThe volatility of USDC will also affect the price of the other token in the pool since it's priced in USDC (DepositReceipt_USDC.sol#L87, DepositReceipt_USDC.sol#L110) and then compared to its USD price from a Chainlink oracle (DepositReceipt_USDC.sol#L90-L98).\\nThis issue is also applicable to the hard coded peg of sUSD when evaluating the USD price of a Synthetix collateral (Vault_Synths.sol#L76):\\n```\\n/// @return returns the value of the given synth in sUSD which is assumed to be pegged at $1.\\nfunction priceCollateralToUSD(bytes32 _currencyKey, uint256 _amount) public view override returns(uint256){\\n    //As it is a synth use synthetix for pricing\\n    return (synthetixExchangeRates.effectiveValue(_currencyKey, _amount, SUSD_CODE));      \\n}\\n```\\n\\nTogether with isoUSD not having a stability mechanism, these assumptions can lead to different manipulations with the price of isoUSD and the arbitraging opportunities created by the hard peg assumptions (sUSD and USDC will be priced differently on exchanges and on Isomorph).

Impact:
If the price of USDC falls below $1, collateral will be priced higher than expected. This will keep borrowers from being liquidated. And it will probably affect the price of isoUSD since there will be an arbitrage opportunity: the cheaper USDC will be priced higher as collateral on Isomorph. If hte price of USDC raises above $1, borrowers' collateral will be undervalued and some liquidations will be possible that wouldn't have be allowed if the actual price of USDC was used.

Recommended mitigation:
Consider using the Chainlink USDC/USD feed to get the price of USDC and price liquidity using the actual price of USDC. Also, consider converting sUSD prices of Synthetix collaterals to USD to mitigate the discrepancy in prices between external exchanges and Isomorph.
Vulnerability: Wrong constants for time delay
Severity: medium

Description:
This protocol uses several constants for time dealy and some of them are incorrect.\\nIn `isoUSDToken.sol`, `ISOUSD_TIME_DELAY` should be `3 days` instead of 3 seconds.\\n```\\n    uint256 constant ISOUSD_TIME_DELAY = 3; // days;\\n```\\n\\nIn `CollateralBook.sol`, `CHANGE_COLLATERAL_DELAY` should be `2 days` instead of 200 seconds.\\n```\\n    uint256 public constant CHANGE_COLLATERAL_DELAY = 200; //2 days\\n```\\n

Impact:
Admin settings would be updated within a short period of delay so that users wouldn't react properly.

Recommended mitigation:
2 constants should be modified as mentioned above.
Vulnerability: Unnecessary precision loss in `_recipientBalance()`
Severity: medium

Description:
Using `ratePerSecond()` to calculate the `_recipientBalance()` incurs an unnecessary precision loss.\\nThe current formula in `_recipientBalance()` to calculate the vested amount (balance) incurs an unnecessary precision loss, as it includes div before mul:\\n```\\nbalance = elapsedTime_ * (RATE_DECIMALS_MULTIPLIER * tokenAmount_ / duration) / RATE_DECIMALS_MULTIPLIER\\n```\\n\\nThis can be avoided and the improved formula can also save some gas.

Impact:
Precision loss in `_recipientBalance()`.

Recommended mitigation:
Consdier changing to:\\n```\\nbalance = elapsedTime_ * tokenAmount_ / duration\\n```\\n
Vulnerability: The ````Stream```` contract is designed to receive ETH but not implement function for withdrawal
Severity: medium

Description:
The `Stream` contract instances can receive ETH but can not withdraw, ETH occasionally sent by users will be stuck in those contracts.\\nShown as the test case, it can receive ETH normally.\\n```\\ncontract StreamReceiveETHTest is StreamTest {\\n    function setUp() public override {\\n        super.setUp();\\n    }\\n\\n    function test_receiveETH() public {\\n        s = Stream(\\n            factory.createStream(\\n                payer, recipient, STREAM_AMOUNT, address(token), startTime, stopTime\\n            )\\n        );\\n\\n        vm.deal(payer, 10 ether);\\n        vm.prank(payer);\\n        (bool success, ) = address(s).call{value: 1 ether}("");\\n        assertEq(success, true);\\n        assertEq(address(s).balance, 1 ether);\\n    }\\n}\\n```\\n\\nResult\\n```\\nRunning 1 test for test/Stream.t.sol:StreamReceiveETHTest\\n[PASS] test_receiveETH() (gas: 167691)\\nTest result: ok. 1 passed; 0 failed; finished in 1.25ms\\n```\\n

Impact:
See Summary

Recommended mitigation:
Issue The `Stream` contract is designed to receive ETH but not implement function for withdrawal\\nAdd a `rescueETH()` function which is similar with the existing `rescueERC20()`