msc-smart-contract-auditing/vulnerable-functions-base huggingface dataset:
Vulnerability: user can drawDebt that is below dust amount
Severity: medium

Description:
According to the protocol, drawDebt prevents user from drawing below the `quoteDust_` amount. However, a logical error in the code can allow user to draw below dust amount.\\n`_revertOnMinDebt` is used in `drawDebt` to prevent dust loans. As you can see, the protocol wants to take the average of debt in the pool and make it the minimum if there are 10 or more loans. If it is lower than 10 loans, a `quoteDust` is used as the minimum. There is an edge case, whereby there are 10 loans in the pool, and the borrowers repay the loans till there is only 1 unit owed for each loan.(Might revert due to rounding error but it is describing a situation whereby repaying till a low amount of poolDebt can enable this). A new borrower can then `drawDebt` and because `_revertOnMindebt` only goes through the average loan amount check and not the `quoteDust_` amount check, he/she is able to draw loan that is well below the `quoteDust_` amount.\\n```\\n    function _revertOnMinDebt(\\n        LoansState storage loans_,\\n        uint256 poolDebt_,\\n        uint256 borrowerDebt_,\\n        uint256 quoteDust_\\n    ) view {\\n        if (borrowerDebt_ != 0) {\\n            uint256 loansCount = Loans.noOfLoans(loans_);\\n            if (loansCount >= 10) {\\n                if (borrowerDebt_ < _minDebtAmount(poolDebt_, loansCount)) revert AmountLTMinDebt();\\n            } else {\\n                if (borrowerDebt_ < quoteDust_)                            revert DustAmountNotExceeded();\\n            }\\n        }\\n    }\\n```\\n\\n```\\n    function _minDebtAmount(\\n        uint256 debt_,\\n        uint256 loansCount_\\n    ) pure returns (uint256 minDebtAmount_) {\\n        if (loansCount_ != 0) {\\n            minDebtAmount_ = Maths.wdiv(Maths.wdiv(debt_, Maths.wad(loansCount_)), 10**19);\\n        }\\n    }\\n```\\n

Impact:
A minimum loan amount is used to deter dust loans, which can diminish user experience.

Recommended mitigation:
Issue user can drawDebt that is below dust amount\\nRecommend checking that loan amount is more than `quoteDust_` regardless of the loan count.\\n```\\n    function _revertOnMinDebt(\\n        LoansState storage loans_,\\n        uint256 poolDebt_,\\n        uint256 borrowerDebt_,\\n        uint256 quoteDust_\\n    ) view {\\n        if (borrowerDebt_ != 0) {\\n            uint256 loansCount = Loans.noOfLoans(loans_);\\n            if (loansCount >= 10) {\\n                if (borrowerDebt_ < _minDebtAmount(poolDebt_, loansCount)) revert AmountLTMinDebt();\\n            } \\n            if (borrowerDebt_ < quoteDust_) revert DustAmountNotExceeded();\\n            \\n        }\\n    }\\n```\\n
Vulnerability: `moveQuoteToken()` can cause bucket to go bankrupt but it is not reflected in the accounting
Severity: high

Description:
Both `removeQuoteToken()` and `moveQuoteToken()` can be used to completely remove all quote tokens from a bucket. When this happens, if at the same time `bucketCollateral == 0 && lpsRemaining != 0`, then the bucket should be declared bankrupt. This update is done in `removeQuoteToken()` but not in `moveQuoteToken()`.\\n`removeQuoteToken()` has the following check to update bankruptcy time when collateral and quote token remaining is 0, but lps is more than 0. `moveQuoteToken()` is however missing this check. Both this functions has the same effects on the `fromBucket` and the only difference is that `removeQuoteToken()` returns the token to `msg.sender` but `moveQuoteToken()` moves the token to another bucket.\\n```\\nif (removeParams.bucketCollateral == 0 && unscaledRemaining == 0 && lpsRemaining != 0) {\\n emit BucketBankruptcy(params_.index, lpsRemaining);\\n bucket.lps            = 0;\\n bucket.bankruptcyTime = block.timestamp;\\n} else {\\n bucket.lps = lpsRemaining;\\n}\\n```\\n

Impact:
A future depositor to the bucket will get less lps than expected due to depositing in a bucket that is supposedly bankrupt, hence the lps they get will be diluted with the existing ones in the bucket.

Recommended mitigation:
Issue `moveQuoteToken()` can cause bucket to go bankrupt but it is not reflected in the accounting\\nWe should check if a bucket is bankrupt after moving quote tokens.
Vulnerability: Adversary can grief kicker by frontrunning kickAuction call with a large amount of loan
Severity: medium

Description:
Average debt size of the pool is used to calculated MOMP (Most optimistic matching price), which is used to derive NP (neutral price). Higher average debt size will result in lower MOMP and hence lower NP which will make it harder for kicker to earn a reward and more likely that the kicker is penalized. An adversary can manipulate the average debt size of the pool by frontrunning kicker's `kickAuction` call with a large amount of loan.\\nNP (neutral price) is a price that will be used to decide whether to reward a kicker with a bonus or punish the kicker with a penalty. In the event the auction ends with a price higher than NP, kicker will be given a penalty and if the auction ends with a price lower than NP, kicker will be rewarded with a bonus.\\nNP is derived from MOMP (Most optimistic matching price). BI refers to borrower inflator. Quoted from the whitepaper page 17, When a loan is initiated (the first debt or additional debt is drawn, or collateral is removed from the loan), the neutral price is set to the current MOMP times the ratio of the loan's threshold price to the LUP, plus one year's interest. As time passes, the neutral price increases at the same rate as interest. This can be expressed as the following formula for the neutral price as a function of time 𝑡, where 𝑠 is the time the loan is initiated.\\n```\\n NP_t = (1 + rate_s) * MOMP_s * TP_s * \\frac{TP_s}{LUP_s} *  \\frac{BI_s}{BI_t}\\n```\\n\\nTherefore the lower the MOMP, the lower the NP. Lower NP will mean that kicker will be rewarded less and punished more compared to a higher NP. Quoted from the white paper, The MOMP, or “most optimistic matching price,” is the price at which a loan of average size would match with the most favorable lenders on the book. Technically, it is the highest price for which the amount of deposit above it exceeds the average loan debt of the pool. In `_kick` function, MOMP is calculated as this. Notice how total pool debt is divided by number of loans to find the average loan debt size.\\n```\\n        uint256 momp = _priceAt(\\n            Deposits.findIndexOfSum(\\n                deposits_,\\n                Maths.wdiv(poolState_.debt, noOfLoans * 1e18)\\n            )\\n        );\\n```\\n\\nAn adversary can frontrun `kickAuction` by taking a huge loan, causing the price for which the amount of deposit above the undercollaterized loan bucket to have a lower probability of surpassing the average loan debt. The adversary can use the deposits for the buckets above and the total pool debt to figure out how much loan is necessary to grief the kicker significantly by lowering the MOMP and NP.

Impact:
Kickers can be grieved which can disincentivize user from kicking loans that deserve to be liquidated, causing the protocol to not work as desired as undercollaterized loans will not be liquidated.

Recommended mitigation:
Recommend taking the snapshot average loan size of the pool to prevent frontrunning attacks.
Vulnerability: Incorrect MOMP calculation in neutral price calculation
Severity: medium

Description:
When calculating MOMP to find the neutral price of a borrower, borrower's accrued debt is divided by the total number of loans in the pool, but it's total pool's debt that should be divided. The mistake will result in lower neutral prices and more lost bonds to kickers.\\nAs per the whitepaper:\\nMOMP: is the price at which the amount of deposit above it is equal to the average loan size of the pool. MOMP is short for “Most Optimistic Matching Price”, as it's the price at which a loan of average size would match with the most favorable lenders on the book.\\nI.e. MOMP is calculated on the total number of loans of a pool (so that the average loan size could be found).\\nMOMP calculation is implemented correctly when kicking a debt, however it's implementation in the Loans.update function is not correct:\\n```\\nuint256 loansInPool = loans_.loans.length - 1 + auctions_.noOfAuctions;\\nuint256 curMomp     = _priceAt(Deposits.findIndexOfSum(deposits_, Maths.wdiv(borrowerAccruedDebt_, loansInPool * 1e18)));\\n```\\n\\nHere, only borrower's debt (borrowerAccruedDebt_) is divided, not the entire debt of the pool.

Impact:
The miscalculation affects only borrower's neutral price calculation. Since MOMP is calculated on a smaller debt (borrower's debt will almost always be smaller than total pool's debt), the value of MOMP will be smaller than expected, and the neutral price will also be smaller (from the whitepaper: "The NP of a loan is the interest-adjusted MOMP..."). This will cause kickers to lose their bonds more often than expected, as per the whitepaper:\\nIf the liquidation auction yields a value that is over the “Neutral Price,” NP, the kicker forfeits a portion or all of their bond.

Recommended mitigation:
Issue Incorrect MOMP calculation in neutral price calculation\\nConsider using total pool's debt in the MOMP calculation in `Loans.update`.
Vulnerability: Lender force Loan become default
Severity: high

Description:
in `repay()` directly transfer the debt token to Lender, but did not consider that Lender can not accept the token (in contract blacklist), resulting in `repay()` always revert, and finally the Loan can only expire, Loan be default\\nThe only way for the borrower to get the collateral token back is to repay the amount owed via repay(). Currently in the repay() method transfers the debt token directly to the Lender. This has a problem: if the Lender is blacklisted by the debt token now, the debtToken.transferFrom() method will fail and the repay() method will always fail and finally the Loan will default. Example: Assume collateral token = ETH,debt token = USDC, owner = alice 1.alice call request() to loan 2000 usdc , duration = 1 mon 2.bob call clear(): loanID =1 3.bob transfer loan[1].lender = jack by Cooler.approve/transfer\\nNote: jack has been in USDC's blacklist for some reason before or bob in USDC's blacklist for some reason now, it doesn't need transfer 'lender') 4.Sometime before the expiration date, alice call repay(id=1) , it will always revert, Because usdc.transfer(jack) will revert 5.after 1 mon, loan[1] default, jack call defaulted() get collateral token\\n```\\n    function repay (uint256 loanID, uint256 repaid) external {\\n        Loan storage loan = loans[loanID];\\n// rest of code\\n        debt.transferFrom(msg.sender, loan.lender, repaid);   //***<------- lender in debt token's blocklist will revert , example :debt = usdc\\n        collateral.transfer(owner, decollateralized);\\n    }\\n```\\n

Impact:
Lender forced Loan become default for get collateral token, owner lost collateral token

Recommended mitigation:
Instead of transferring the debt token directly, put the debt token into the Cooler.sol and set like: withdrawBalance[lender]+=amount, and provide the method withdraw() for lender to get debtToken back
Vulnerability: `Cooler.roll()` wouldn't work as expected when `newCollateral = 0`.
Severity: medium

Description:
`Cooler.roll()` is used to increase the loan duration by transferring the additional collateral.\\nBut there will be some problems when `newCollateral = 0`.\\n```\\n    function roll (uint256 loanID) external {\\n        Loan storage loan = loans[loanID];\\n        Request memory req = loan.request;\\n\\n        if (block.timestamp > loan.expiry) \\n            revert Default();\\n\\n        if (!loan.rollable)\\n            revert NotRollable();\\n\\n        uint256 newCollateral = collateralFor(loan.amount, req.loanToCollateral) - loan.collateral;\\n        uint256 newDebt = interestFor(loan.amount, req.interest, req.duration);\\n\\n        loan.amount += newDebt;\\n        loan.expiry += req.duration;\\n        loan.collateral += newCollateral;\\n        \\n        collateral.transferFrom(msg.sender, address(this), newCollateral); //@audit 0 amount\\n    }\\n```\\n\\nIn `roll()`, it transfers the `newCollateral` amount of collateral to the contract.\\nAfter the borrower repaid most of the debts, `loan.amount` might be very small and `newCollateral` for the original interest might be 0 because of the rounding issue.\\nThen as we can see from this one, some tokens might revert for 0 amount and `roll()` wouldn't work as expected.

Impact:
There will be 2 impacts.\\nWhen the borrower tries to extend the loan using `roll()`, it will revert with the weird tokens when `newCollateral = 0`.\\nAfter the borrower noticed he couldn't repay anymore(so the lender will default the loan), the borrower can call `roll()` again when `newCollateral = 0`. In this case, the borrower doesn't lose anything but the lender must wait for `req.duration` again to default the loan.

Recommended mitigation:
I think we should handle it differently when `newCollateral = 0`.\\nAccording to impact 2, I think it would be good to revert when `newCollateral = 0`.
Vulnerability: Loan is rollable by default
Severity: medium

Description:
Making the loan rollable by default gives an unfair early advantage to the borrowers.\\nWhen clearing a new loan, the flag of `rollable` is set to true by default:\\n```\\n    loans.push(\\n        Loan(req, req.amount + interest, collat, expiration, true, msg.sender)\\n    );\\n```\\n\\nThis means a borrower can extend the loan anytime before the expiry:\\n```\\n    function roll (uint256 loanID) external {\\n        Loan storage loan = loans[loanID];\\n        Request memory req = loan.request;\\n\\n        if (block.timestamp > loan.expiry) \\n            revert Default();\\n\\n        if (!loan.rollable)\\n            revert NotRollable();\\n```\\n\\nIf the lenders do not intend to allow rollable loans, they should separately toggle the status to prevent that:\\n```\\n    function toggleRoll(uint256 loanID) external returns (bool) {\\n        // rest of code\\n        loan.rollable = !loan.rollable;\\n        // rest of code\\n    }\\n```\\n\\nI believe it gives an unfair advantage to the borrower because they can re-roll the loan before the lender's transaction forbids this action.

Impact:
Lenders who do not want the loans to be used more than once, have to bundle their transactions. Otherwise, it is possible that someone might roll their loan, especially if the capital requirements are not huge because anyone can roll any loan.

Recommended mitigation:
I believe `rollable` should be set to false by default or at least add an extra function parameter to determine the initial value of this status.
Vulnerability: Use safeTransfer/safeTransferFrom consistently instead of transfer/transferFrom
Severity: high

Description:
Use safeTransfer/safeTransferFrom consistently instead of transfer/transferFrom\\n```\\n    function clear (uint256 reqID) external returns (uint256 loanID) {\\n        Request storage req = requests[reqID];\\n\\n        factory.newEvent(reqID, CoolerFactory.Events.Clear);\\n\\n        if (!req.active) \\n            revert Deactivated();\\n        else req.active = false;\\n\\n        uint256 interest = interestFor(req.amount, req.interest, req.duration);\\n        uint256 collat = collateralFor(req.amount, req.loanToCollateral);\\n        uint256 expiration = block.timestamp + req.duration;\\n\\n        loanID = loans.length;\\n        loans.push(\\n            Loan(req, req.amount + interest, collat, expiration, true, msg.sender)\\n        );\\n        debt.transferFrom(msg.sender, owner, req.amount);\\n    }\\n```\\n

Impact:
If the token send fails, it will cause a lot of serious problems. For example, in the clear function, if debt token is ZRX, the lender can clear request without providing any debt token.

Recommended mitigation:
Consider using safeTransfer/safeTransferFrom consistently.
Vulnerability: Fully repaying a loan will result in debt payment being lost
Severity: high

Description:
When a `loan` is fully repaid the `loan` `storage` is deleted. Since `loan` is a `storage` reference to the `loan`, `loan.lender` will return `address(0)` after the `loan` has been deleted. This will result in the `debt` being transferred to `address(0)` instead of the lender. Some ERC20 tokens will revert when being sent to `address(0)` but a large number will simply be sent there and lost forever.\\n```\\nfunction repay (uint256 loanID, uint256 repaid) external {\\n    Loan storage loan = loans[loanID];\\n\\n    if (block.timestamp > loan.expiry) \\n        revert Default();\\n    \\n    uint256 decollateralized = loan.collateral * repaid / loan.amount;\\n\\n    if (repaid == loan.amount) delete loans[loanID];\\n    else {\\n        loan.amount -= repaid;\\n        loan.collateral -= decollateralized;\\n    }\\n\\n    debt.transferFrom(msg.sender, loan.lender, repaid);\\n    collateral.transfer(owner, decollateralized);\\n}\\n```\\n\\nIn `Cooler#repay` the `loan` storage associated with the loanID being repaid is deleted. `loan` is a storage reference so when `loans[loanID]` is deleted so is `loan`. The result is that `loan.lender` is now `address(0)` and the `loan` payment will be sent there instead.

Impact:
Lender's funds are sent to `address(0)`

Recommended mitigation:
Send collateral/debt then delete:\\n```\\n-   if (repaid == loan.amount) delete loans[loanID];\\n+   if (repaid == loan.amount) {\\n+       debt.transferFrom(msg.sender, loan.lender, loan.amount);\\n+       collateral.transfer(owner, loan.collateral);\\n+       delete loans[loanID];\\n+       return;\\n+   }\\n```\\n
Vulnerability: No check if Arbitrum L2 sequencer is down in Chainlink feeds
Severity: medium

Description:
Using Chainlink in L2 chains such as Arbitrum requires to check if the sequencer is down to avoid prices from looking like they are fresh although they are not.\\nThe bug could be leveraged by malicious actors to take advantage of the sequencer downtime.\\n```\\n    function getEthPrice() internal view returns (uint) {\\n        (, int answer,, uint updatedAt,) =\\n            ethUsdPriceFeed.latestRoundData();\\n\\n        if (block.timestamp - updatedAt >= 86400)\\n            revert Errors.StalePrice(address(0), address(ethUsdPriceFeed));\\n\\n        if (answer <= 0)\\n            revert Errors.NegativePrice(address(0), address(ethUsdPriceFeed));\\n\\n        return uint(answer);\\n    }\\n```\\n

Impact:
The impact depends on the usage of the GLP. If it is used as part of the collateral for lenders:\\nUsers can get better borrows if the price is above the actual price\\nUsers can avoid liquidations if the price is under the actual price

Recommended mitigation:
Issue No check if Arbitrum L2 sequencer is down in Chainlink feeds
Vulnerability: USDC deposited to PerpDepository.sol are irretrievable and effectively causes UDX to become undercollateralized
Severity: high

Description:
PerpDepository rebalances negative PNL into USDC holdings. This preserves the delta neutrality of the system by exchanging base to quote. This is problematic though as once it is in the vault as USDC it can never be withdrawn. The effect is that the delta neutral position can never be liquidated but the USDC is inaccessible so UDX is effectively undercollateralized.\\n`_processQuoteMint`, `_rebalanceNegativePnlWithSwap` and `_rebalanceNegativePnlLite` all add USDC collateral to the system. There were originally two ways in which USDC could be removed from the system. The first was positive PNL rebalancing, which has now been deactivated. The second is for the owner to remove the USDC via `withdrawInsurance`.\\n```\\nfunction withdrawInsurance(uint256 amount, address to)\\n    external\\n    nonReentrant\\n    onlyOwner\\n{\\n    if (amount == 0) {\\n        revert ZeroAmount();\\n    }\\n\\n    insuranceDeposited -= amount;\\n\\n    vault.withdraw(insuranceToken(), amount);\\n    IERC20(insuranceToken()).transfer(to, amount);\\n\\n    emit InsuranceWithdrawn(msg.sender, to, amount);\\n}\\n```\\n\\nThe issue is that `withdrawInsurance` cannot actually redeem any USDC. Since insuranceDeposited is a uint256 and is decremented by the withdraw, it is impossible for more USDC to be withdrawn then was originally deposited.\\nThe result is that there is no way for the USDC to ever be redeemed and therefore over time will lead to the system becoming undercollateralized due to its inaccessibility.

Impact:
UDX will become undercollateralized and the ecosystem will spiral out of control

Recommended mitigation:
Allow all USDC now deposited into the insurance fund to be redeemed 1:1
Vulnerability: PerpDepository#getPositionValue uses incorrect value for TWAP interval allowing more than intended funds to be extracted
Severity: high

Description:
PerpDepository#getPositionValue queries the exchange for the mark price to calculate the unrealized PNL. Mark price is defined as the 15 minute TWAP of the market. The issue is that it uses the 15 second TWAP instead of the 15 minute TWAP\\nAs stated in the docs and as implemented in the ClearHouseConfig contract, the mark price is a 15 minute / 900 second TWAP.\\n```\\nfunction getPositionValue() public view returns (uint256) {\\n    uint256 markPrice = getMarkPriceTwap(15);\\n    int256 positionSize = IAccountBalance(clearingHouse.getAccountBalance())\\n        .getTakerPositionSize(address(this), market);\\n    return markPrice.mulWadUp(_abs(positionSize));\\n}\\n\\nfunction getMarkPriceTwap(uint32 twapInterval)\\n    public\\n    view\\n    returns (uint256)\\n{\\n    IExchange exchange = IExchange(clearingHouse.getExchange());\\n    uint256 markPrice = exchange\\n        .getSqrtMarkTwapX96(market, twapInterval)\\n        .formatSqrtPriceX96ToPriceX96()\\n        .formatX96ToX10_18();\\n    return markPrice;\\n}\\n```\\n\\nAs seen in the code above getPositionValue uses 15 as the TWAP interval. This means it is pulling a 15 second TWAP rather than a 15 minute TWAP as intended.

Impact:
The mark price and by extension the position value will frequently be different from true mark price of the market allowing for larger rebalances than should be possible.

Recommended mitigation:
I recommend pulling pulling the TWAP fresh each time from ClearingHouseConfig, because the TWAP can be changed at anytime. If it is desired to make it a constant then it should at least be changed from 15 to 900.
Vulnerability: PerpDepository.netAssetDeposits variable can prevent users to withdraw with underflow error
Severity: medium

Description:
PerpDepository.netAssetDeposits variable can prevent users to withdraw with underflow error\\n```\\n    function _depositAsset(uint256 amount) private {\\n        netAssetDeposits += amount;\\n\\n\\n        IERC20(assetToken).approve(address(vault), amount);\\n        vault.deposit(assetToken, amount);\\n    }\\n```\\n\\n```\\n    function _withdrawAsset(uint256 amount, address to) private {\\n        if (amount > netAssetDeposits) {\\n            revert InsufficientAssetDeposits(netAssetDeposits, amount);\\n        }\\n        netAssetDeposits -= amount;\\n\\n\\n        vault.withdraw(address(assetToken), amount);\\n        IERC20(assetToken).transfer(to, amount);\\n    }\\n```\\n\\nThe problem here is that when user deposits X assets, then he receives Y UXD tokens. And when later he redeems his Y UXD tokens he can receive more or less than X assets. This can lead to situation when netAssetDeposits variable will be seting to negative value which will revert tx.\\nExample. 1.User deposits 1 WETH when it costs 1200$. As result 1200 UXD tokens were minted and netAssetDeposits was set to 1. 2.Price of WETH has decreased and now it costs 1100. 3.User redeem his 1200 UXD tokens and receives from perp protocol 1200/1100=1.09 WETH. But because netAssetDeposits is 1, then transaction will revert inside `_withdrawAsset` function with underflow error.

Impact:
User can't redeem all his UXD tokens.

Recommended mitigation:
As you don't use this variable anywhere else, you can remove it. Otherwise you need to have 2 variables instead: totalDeposited and totalWithdrawn.