msc-smart-contract-auditing/vulnerable-functions-base huggingface dataset:
Vulnerability: Calls to liquidate don't write down totalBorrows which breaks exchange rate
Severity: high

Description:
When a pool is liquidated, the `totalBorrows` storage slot for the token in question should be decremented by `debtToCover` in order to keep the exchange rate of the corresponding `pToken` correct.\\nWhen users call `liquidate` to `liquidate` a pool, they specify the amount of debt they want to cover. In the end this is used to write down the borrow amount of the pool in question:\\n```\\nrecord.amount = borrows - debtToCover;\\n```\\n\\nHowever, the `totalBorrows` of the token isn't written down as well (like it should be). The `finishLiquidation` method correctly writes down the `totalBorrows` state.

Impact:
When a user calls `liquidate` to `liquidate` a pool, the exchange rate of the token (from its pToken) remains high (because the `totalBorrows` for the token isn't decremented). The result is that users that have deposited this ERC20 token are receiving a higher rate of interest than they should. Because this interest is not being covered by anyone the end result is that the last withdrawer from the vault will not be able to redeem their pTokens because there isn't enough of the underlying ERC20 token available. The longer the period over which interest accrues, the greater the incentive for LPs to withdraw early.

Recommended mitigation:
The `liquidate` method should include the following line to write down the total borrow amount of the debt token being liquidated:\\n```\\ninfo.totalBorrows = info.totalBorrows - debtToCover;\\n```\\n
Vulnerability: possible precision loss in D3VaultLiquidation.finishLiquidation() function when calculating realDebt because of division before multiplication
Severity: medium

Description:
finishLiquidation() divides before multiplying when calculating realDebt.\\n```\\nuint256 realDebt = borrows.div(record.interestIndex == 0 ? 1e18 : record.interestIndex).mul(info.borrowIndex);\\n```\\n\\nThere will be precision loss when calculating the realDebt because solidity truncates values when dividing and dividing before multiplying causes precision loss.\\nValues that suffered from precision loss will be updated here\\n```\\n info.totalBorrows = info.totalBorrows - realDebt;\\n```\\n

Impact:
Issue possible precision loss in D3VaultLiquidation.finishLiquidation() function when calculating realDebt because of division before multiplication\\nValues that suffered from precision loss will be updated here\\n```\\n info.totalBorrows = info.totalBorrows - realDebt;\\n```\\n

Recommended mitigation:
don't divide before multiplying
Vulnerability: `D3VaultFunding.userWithdraw()` doen not have mindTokenAmount
Severity: medium

Description:
`D3VaultFunding.userWithdraw()` doen not have mindTokenAmount, and use `_getExchangeRate` directly.This is vulnerable to a sandwich attack.\\nAs we can see, `D3VaultFunding.userWithdraw()` doen not have mindTokenAmount, and use `_getExchangeRate` directly.\\n```\\nfunction userWithdraw(address to, address user, address token, uint256 dTokenAmount) external nonReentrant allowedToken(token) returns(uint256 amount) {\\n        accrueInterest(token);\\n        AssetInfo storage info = assetInfo[token];\\n        require(dTokenAmount <= IDToken(info.dToken).balanceOf(msg.sender), Errors.DTOKEN_BALANCE_NOT_ENOUGH);\\n\\n        amount = dTokenAmount.mul(_getExchangeRate(token));//@audit does not check amount value\\n        IDToken(info.dToken).burn(msg.sender, dTokenAmount);\\n        IERC20(token).safeTransfer(to, amount);\\n        info.balance = info.balance - amount;\\n\\n        // used for calculate user withdraw amount\\n        // this function could be called from d3Proxy, so we need "user" param\\n        // In the meantime, some users may hope to use this function directly,\\n        // to prevent these users fill "user" param with wrong addresses,\\n        // we use "msg.sender" param to check.\\n        emit UserWithdraw(msg.sender, user, token, amount);\\n    }\\n```\\n\\nAnd the `_getExchangeRate()` result is about `cash` , `info.totalBorrows`, info.totalReserves,info.withdrawnReserves,dTokenSupply,This is vulnerable to a sandwich attack leading to huge slippage\\n```\\nfunction _getExchangeRate(address token) internal view returns (uint256) {\\n        AssetInfo storage info = assetInfo[token];\\n        uint256 cash = getCash(token);\\n        uint256 dTokenSupply = IERC20(info.dToken).totalSupply();\\n        if (dTokenSupply == 0) { return 1e18; }\\n        return (cash + info.totalBorrows - (info.totalReserves - info.withdrawnReserves)).div(dTokenSupply);\\n    } \\n```\\n

Impact:
This is vulnerable to a sandwich attack.

Recommended mitigation:
Add `mindTokenAmount` parameter for `userWithdraw()` function and check if `amount < mindTokenAmount`
Vulnerability: D3Oracle will return the wrong price if the Chainlink aggregator returns price outside min/max range
Severity: medium

Description:
Chainlink oracles have a min and max price that they return. If the price goes below the minimum price the oracle will not return the correct price but only the min price. Same goes for the other extremity.\\nBoth `getPrice()` and `getOriginalPrice()` only check `price > 0` not are they within the correct range\\n```\\n(uint80 roundID, int256 price,, uint256 updatedAt, uint80 answeredInRound) = priceFeed.latestRoundData();\\nrequire(price > 0, "Chainlink: Incorrect Price");\\nrequire(block.timestamp - updatedAt < priceSources[token].heartBeat, "Chainlink: Stale Price");\\nrequire(answeredInRound >= roundID, "Chainlink: Stale Price");\\n```\\n

Impact:
The wrong price may be returned in the event of a market crash. The functions with the issue are used in `D3VaultFunding.sol`, `D3VaultLiquidation.sol` and `D3UserQuota.sol`

Recommended mitigation:
Check the latest answer against reasonable limits and/or revert in case you get a bad price\\n```\\n require(price >= minAnswer && price <= maxAnswer, "invalid price");\\n```\\n
Vulnerability: parseAllPrice not support the tokens whose decimal is greater than 18
Severity: medium

Description:
`parseAllPrice` not support the token decimal is greater than 18, such as NEAR with 24 decimal. Since `buyToken / sellToken` is dependent on `parseAllPrice`, so users can't trade tokens larger than 18 decimal, but DODOv3 is intended to be compatible with all standard ERC20, which is not expected.\\n```\\n        // fix price decimal\\n        if (tokenDecimal != 18) {\\n            uint256 fixDecimal = 18 - tokenDecimal;\\n            bidDownPrice = bidDownPrice / (10 ** fixDecimal);\\n            bidUpPrice = bidUpPrice / (10 ** fixDecimal);\\n            askDownPrice = askDownPrice * (10 ** fixDecimal);\\n            askUpPrice = askUpPrice * (10 ** fixDecimal);\\n        }\\n```\\n\\nIf `tokenDecimal > 18`, `18 - tokenDecimal` will revert

Impact:
DODOv3 is not compatible the tokens whose decimal is greater than 18, users can't trade them.

Recommended mitigation:
Fix decimal to 36 instead of 18
Vulnerability: Wrong assignment of `cumulativeBid` for RangeOrder state in getRangeOrderState function
Severity: medium

Description:
Wrong assignment of `cumulativeBid` for RangeOrder state\\nIn `D3Trading`, the `getRangeOrderState` function is returning RangeOrder (get swap status for internal swap) which is assinging wrong toTokenMMInfo.cumulativeBid which suppose to be `cumulativeBid` not `cumulativeAsk`\\nThe error lies in the assignment of `roState.toTokenMMInfo.cumulativeBid`. Instead of assigning `tokenCumMap[toToken].cumulativeAsk`, it should be assigning `tokenCumMap[toToken].cumulativeBid`.\\n```\\nFile: D3Trading.sol\\n        roState.toTokenMMInfo.cumulativeBid =\\n            allFlag  (toTokenIndex) & 1 == 0 ? 0 : tokenCumMap[toToken].cumulativeAsk;\\n```\\n\\nThis wrong assignment value definitely will mess up accounting balance, resulting unknown state will occure, which is not expected by the protocol\\nFor one case, this `getRangeOrderState` is being used in `querySellTokens` & `queryBuyTokens` which may later called from `sellToken` and `buyToken`. The issue is when calling `_contructTokenState` which can be reverted from `PMMRangeOrder` when buy or sell token\\n```\\nFile: PMMRangeOrder.sol\\n        // B\\n        tokenState.B = askOrNot ? tokenState.B0 - tokenMMInfo.cumulativeAsk : tokenState.B0 - tokenMMInfo.cumulativeBid;\\n```\\n\\nWhen the `tokenMMInfo.cumulativeBid` (which was wrongly assign from cumulativeAsk) is bigger than `tokenState.B0`, this will revert

Impact:
This wrong assignment value definitely will mess up accounting balance, resulting unknown state will occure, which is not expected by the protocol. For example reverting state showing a case above.

Recommended mitigation:
Fix the error to\\n```\\nFile: D3Trading.sol\\n        roState.toTokenMMInfo.cumulativeBid =\\n// Remove the line below\\n// Remove the line below\\n:             allFlag  (toTokenIndex) & 1 == 0 ? 0 : tokenCumMap[toToken].cumulativeAsk;\\n// Add the line below\\n// Add the line below\\n:             allFlag  (toTokenIndex) & 1 == 0 ? 0 : tokenCumMap[toToken].cumulativeBid;\\n```\\n
Vulnerability: D3VaultFunding#checkBadDebtAfterAccrue is inaccurate and can lead to further damage to both LP's and MM
Severity: medium

Description:
D3VaultFunding#checkBadDebtAfterAccrue makes the incorrect assumption that a collateral ratio of less than 1e18 means that the pool has bad debt. Due to how collateral and debt weight affect the collateral ratio calculation a pool can have a collateral ratio less than 1e18 will still maintaining debt that is profitable to liquidate. The result of this is that the after this threshold has been passed, a pool can no longer be liquidate by anyone which can lead to continued losses that harm both the LPs and the MM being liquidated.\\nD3VaultFunding.sol#L382-L386\\n```\\n        if (balance >= borrows) {\\n            collateral += min(balance - borrows, info.maxCollateralAmount).mul(info.collateralWeight).mul(price);\\n        } else {\\n            debt += (borrows - balance).mul(info.debtWeight).mul(price);\\n        }\\n```\\n\\nWhen calculating the collateral and debt values, the value of the collateral is adjusted by the collateralWeight and debtWeight respectively. This can lead to a position in which the collateral ratio is less than 1e18, which incorrectly signals the pool has bad debt via the checkBadDebtAfterAccrue check.\\nExample:\\n```\\nAssume a pool has the following balances and debts:\\n\\nToken A - 100 borrows 125 balance\\nToken B - 100 borrows 80 balance\\n\\nPrice A = 1\\ncollateralWeightA = 0.8\\n\\nPrice B = 1\\ndebtWeightB = 1.2\\n\\ncollateral = 25 * 1 * 0.8 = 20\\ndebt = 20 * 1 * 1.2 = 24\\n\\ncollateralRatio = 20/24 = 0.83\\n```\\n\\nThe problem here is that there is no bad debt at all and it is still profitable to liquidate this pool, even with a discount:\\n```\\nExcessCollateral = 125 - 100 = 25\\n\\n25 * 1 * 0.95 [DISCOUNT] = 23.75\\n\\nExcessDebt = 100 - 80 = 20\\n\\n20 * 1 = 20\\n```\\n\\nThe issue with this is that once this check has been triggered, no other market participants besides DODO can liquidate this position. This creates a significant inefficiency in the market that can easily to real bad debt being created for the pool. This bad debt is harmful to both the pool MM, who could have been liquidated with remaining collateral, and also the vault LPs who directly pay for the bad debt.

Impact:
Unnecessary loss of funds to LPs and MMs

Recommended mitigation:
The methodology of the bad debt check should be changed to remove collateral and debt weights to accurately indicate the presence of bad debt.
Vulnerability: D3UserQuote#getUserQuote queries incorrect token for exchangeRate leading to inaccurate quota calculations
Severity: medium

Description:
A small typo in the valuation loop of D3UserQuote#getUserQuote uses the wrong variable leading to and incorrect quota being returned. The purpose of a quota is to mitigate risk of positions being too large. This incorrect assumption can dramatically underestimate the quota leading to oversized (and overrisk) positions.\\nD3UserQuota.sol#L75-L84\\n```\\n    for (uint256 i = 0; i < tokenList.length; i++) {\\n        address _token = tokenList[i];\\n        (address assetDToken,,,,,,,,,,) = d3Vault.getAssetInfo(_token);\\n        uint256 tokenBalance = IERC20(assetDToken).balanceOf(user);\\n        if (tokenBalance > 0) {\\n            tokenBalance = tokenBalance.mul(d3Vault.getExchangeRate(token)); <- @audit-issue queries token instead of _token\\n            (uint256 tokenPrice, uint8 priceDecimal) = ID3Oracle(d3Vault._ORACLE_()).getOriginalPrice(_token);\\n            usedQuota = usedQuota + tokenBalance * tokenPrice / 10 ** (priceDecimal+tokenDecimals);\\n        }\\n    }\\n```\\n\\nD3UserQuota.sol#L80 incorrectly uses token rather than _token as it should. This returns the wrong exchange rate which can dramatically alter the perceived token balance as well as the calculated quota.

Impact:
Quota is calculated incorrectly leading to overly risky positions, which in turn can cause loss to the system

Recommended mitigation:
Change variable from token to _token:\\n```\\n-           tokenBalance = tokenBalance.mul(d3Vault.getExchangeRate(token));\\n+           tokenBalance = tokenBalance.mul(d3Vault.getExchangeRate(_token));\\n```\\n
Vulnerability: When swapping 18-decimal token to 8-decimal token , user could buy decimal-18-token with 0 amount of decimal-8-token
Severity: medium

Description:
Here is the poc:\\n```\\nuint256 payFromToken = d3Proxy.buyTokens(\\n            address(d3MM),\\n            user1,\\n            address(token1),\\n            address(token2),\\n            10000000,\\n            0,\\n            abi.encode(swapData),\\n            block.timestamp + 1000\\n        );\\nassertEq(payFromToken, 0);\\n```\\n

Impact:
It may cause unexpected loss\\nTool Used\\nManual Review

Recommended mitigation:
In buyToken() of D3Trading.sol, add this rule:\\n```\\nif(payFromAmount == 0) { // value too small\\n            payFromAmount = 1;\\n        }\\n```\\n
Vulnerability: ArrakisV2Router#addLiquidityPermit2 will strand ETH
Severity: high

Description:
Inside ArrakisV2Router#addLiquidityPermit2, `isToken0Weth` is set incorrectly leading to the wrong amount of ETH being refunded to the user\\nArrakisV2Router.sol#L278-L298\\n```\\n    bool isToken0Weth;\\n    _permit2Add(params_, amount0, amount1, token0, token1);\\n\\n    _addLiquidity(\\n        params_.addData.vault,\\n        amount0,\\n        amount1,\\n        sharesReceived,\\n        params_.addData.gauge,\\n        params_.addData.receiver,\\n        token0,\\n        token1\\n    );\\n\\n    if (msg.value > 0) {\\n        if (isToken0Weth && msg.value > amount0) {\\n            payable(msg.sender).sendValue(msg.value - amount0);\\n        } else if (!isToken0Weth && msg.value > amount1) {\\n            payable(msg.sender).sendValue(msg.value - amount1);\\n        }\\n    }\\n```\\n\\nAbove we see that excess msg.value is returned to the user at the end of the function. This uses the value of `isToken0Weth` to determine the amount to send back to the user. The issue is that `isToken0Weth` is set incorrectly and will lead to ETH being stranded in the contract. `isToken0Weth` is never set, it will always be `false`. This means that when WETH actually is token0 the incorrect amount of ETH will be sent back to the user.\\nThis same issue can also be used to steal the ETH left in the contract by a malicious user. To make matters worse, the attacker can manipulate the underlying pools to increase the amount of ETH left in the contract so they can steal even more.

Impact:
ETH will be stranded in contract and stolen

Recommended mitigation:
Move `isToken0Weth` and set it correctly:\\n```\\n-   bool isToken0Weth;\\n    _permit2Add(params_, amount0, amount1, token0, token1);\\n\\n    _addLiquidity(\\n        params_.addData.vault,\\n        amount0,\\n        amount1,\\n        sharesReceived,\\n        params_.addData.gauge,\\n        params_.addData.receiver,\\n        token0,\\n        token1\\n    );\\n\\n    if (msg.value > 0) {\\n+       bool isToken0Weth = _isToken0Weth(address(token0), address(token1));\\n        if (isToken0Weth && msg.value > amount0) {\\n            payable(msg.sender).sendValue(msg.value - amount0);\\n        } else if (!isToken0Weth && msg.value > amount1) {\\n            payable(msg.sender).sendValue(msg.value - amount1);\\n        }\\n    }\\n```\\n
Vulnerability: Then getAmountsForDelta function at Underlying.sol is implemented incorrectly
Severity: medium

Description:
The function `getAmountsForDelta()` at the `Underlying.sol` contract is used to compute the quantity of `token0` and `token1` to add to the position given a delta of liquidity. These quantities depend on the delta of liquidity, the current tick and the ticks of the range boundaries. Actually, `getAmountsForDelta()` uses the sqrt prices instead of the ticks, but they are equivalent since each tick represents a sqrt price.\\nThere exists 3 cases:\\nThe current tick is outside the range from the left, this means only `token0` should be added.\\nThe current tick is within the range, this means both `token0` and `token1` should be added.\\nThe current tick is outside the range from the right, this means only `token1` should be added.\\nThe issue on the implementation is on the first case, which is coded as follows:\\n```\\nif (sqrtRatioX96 <= sqrtRatioAX96) {\\n      amount0 = SafeCast.toUint256(\\n          SqrtPriceMath.getAmount0Delta(\\n               sqrtRatioAX96,\\n               sqrtRatioBX96,\\n               liquidity\\n          )\\n      );\\n} \\n```\\n\\nThe implementation says that if the current price is equal to the price of the lower tick, it means that it is outside of the range and hence only `token0` should be added to the position.\\nBut for the UniswapV3 implementation, the current price must be lower in order to consider it outside:\\n```\\nif (_slot0.tick < params.tickLower) {\\n   // current tick is below the passed range; liquidity can only become in range by crossing from left to\\n   // right, when we'll need _more_ token0 (it's becoming more valuable) so user must provide it\\n   amount0 = SqrtPriceMath.getAmount0Delta(\\n          TickMath.getSqrtRatioAtTick(params.tickLower),\\n          TickMath.getSqrtRatioAtTick(params.tickUpper),\\n          params.liquidityDelta\\n    );\\n}\\n```\\n\\nReference

Impact:
When the current price is equal to the left boundary of the range, the uniswap pool will request both `token0` and `token1`, but arrakis will only request from the user `token0` so the pool will lose some `token1` if it has enough to cover it.

Recommended mitigation:
Change from:\\n```\\n// @audit-issue Change <= to <.\\nif (sqrtRatioX96 <= sqrtRatioAX96) {\\n     amount0 = SafeCast.toUint256(\\n        SqrtPriceMath.getAmount0Delta(\\n           sqrtRatioAX96,\\n           sqrtRatioBX96,\\n           liquidity\\n         )\\n     );\\n}\\n```\\n\\nto:\\n```\\nif (sqrtRatioX96 < sqrtRatioAX96) {\\n     amount0 = SafeCast.toUint256(\\n        SqrtPriceMath.getAmount0Delta(\\n           sqrtRatioAX96,\\n           sqrtRatioBX96,\\n           liquidity\\n         )\\n     );\\n}\\n```\\n
Vulnerability: outdated variable is not effective to check price feed timeliness
Severity: medium

Description:
In ChainlinkOraclePivot, it uses one `outdated` variable to check if the two price feeds are `outdated`. However, this is not effective because the price feeds have different update frequencies.\\nLet's have an example:\\nIn Polygon mainnet, ChainlinkOraclePivot uses two Chainlink price feeds: MATIC/ETH and ETH/USD.\\nWe can see that\\nIn function `_getLatestRoundData`, both price feeds use the same `outdated` variable.\\nIf we set the `outdated` variable to 27s, the priceFeedA will revert most of the time since it is too short for the 86400s heartbeat.\\nIf we set the `outdated` variable to 86400s, the priceFeedB can have a very `outdated` value without revert.\\n```\\n        try priceFeedA.latestRoundData() returns (\\n            uint80,\\n            int256 price,\\n            uint256,\\n            uint256 updatedAt,\\n            uint80\\n        ) {\\n            require(\\n                block.timestamp - updatedAt <= outdated, // solhint-disable-line not-rely-on-time\\n                "ChainLinkOracle: priceFeedA outdated."\\n            );\\n\\n            priceA = SafeCast.toUint256(price);\\n        } catch {\\n            revert("ChainLinkOracle: price feed A call failed.");\\n        }\\n\\n        try priceFeedB.latestRoundData() returns (\\n            uint80,\\n            int256 price,\\n            uint256,\\n            uint256 updatedAt,\\n            uint80\\n        ) {\\n            require(\\n                block.timestamp - updatedAt <= outdated, // solhint-disable-line not-rely-on-time\\n                "ChainLinkOracle: priceFeedB outdated."\\n            );\\n\\n            priceB = SafeCast.toUint256(price);\\n        } catch {\\n            revert("ChainLinkOracle: price feed B call failed.");\\n        }\\n```\\n

Impact:
The `outdated` variable is not effective to check the timeliness of prices. It can allow stale prices in one price feed or always revert in another price feed.

Recommended mitigation:
Having two `outdated` values for each price feed A and B.
Vulnerability: Update to `managerFeeBPS` applied to pending tokens yet to be claimed
Severity: medium

Description:
A manager (malicious or not) can update the `managerFeeBPS` by calling `ArrakisV2.setManagerFeeBPS()`. The newly-updated `managerFeeBPS` will be retroactively applied to the pending fees yet to be claimed by the `ArrakisV2` contract.\\nWhenever UniV3 fees are collected (via `burn()` or rebalance()), the manager fees are applied to the received pending tokens.\\n```\\nfunction _applyFees(uint256 fee0_, uint256 fee1_) internal {\\n    uint16 mManagerFeeBPS = managerFeeBPS;\\n    managerBalance0 += (fee0_ * mManagerFeeBPS) / hundredPercent;\\n    managerBalance1 += (fee1_ * mManagerFeeBPS) / hundredPercent;\\n}\\n```\\n\\nSince the manager can update the `managerFeeBPS` whenever, this calculation can be altered to take up to 100% of the pending fees in favor of the manager.\\n```\\nfunction setManagerFeeBPS(uint16 managerFeeBPS_) external onlyManager {\\n    require(managerFeeBPS_ <= 10000, "MFO");\\n    managerFeeBPS = managerFeeBPS_;\\n    emit LogSetManagerFeeBPS(managerFeeBPS_);\\n}\\n```\\n

Impact:
Manager's ability to intentionally or accidently steal pending fees owed to stakers

Recommended mitigation:
Fees should be collected at the start of execution within the `setManagerFeeBPS()` function. This effectively checkpoints the fees properly, prior to updating the `managerFeeBPS` variable.
Vulnerability: Wrong calculation of `tickCumulatives` due to hardcoded pool fees
Severity: high

Description:
Wrong calculation of `tickCumulatives` due to hardcoded pool fees\\nReal Wagmi is using a hardcoded `500` fee to calculate the `amountOut` to check for slippage and revert if it was to high, or got less funds back than expected.\\n```\\n IUniswapV3Pool(underlyingTrustedPools[500].poolAddress)\\n```\\n\\nThere are several problems with the hardcoding of the `500` as the fee.\\nNot all tokens have `500` fee pools\\nThe swapping takes place in pools that don't have a `500` fee\\nThe `500` pool fee is not the optimal to fetch the `tickCumulatives` due to low volume\\nSpecially as they are deploying in so many secondary chains like Kava, this will be a big problem pretty much in every transaction over there.\\nIf any of those scenarios is given, `tickCumulatives` will be incorrectly calculated and it will set an incorrect slippage return.

Impact:
Incorrect slippage calculation will increase the risk of `rebalanceAll()` rebalance getting rekt.

Recommended mitigation:
Consider allowing the fees as an input and consider not even picking low TVL pools with no transations