msc-smart-contract-auditing/vulnerable-functions-base huggingface dataset:
Vulnerability: when using fee-on-transfer tokens in VaultV3, capacity is limited below underlyingCap
Severity: low

Description:
Vault V3 documentation states it accounts properly for fee-on-transfer tokens. It calculates actual transferred amount as below:\\n```\\n      uint256 _pool = balance();\\n           if (_pool + _amount > underlyingCap) {\\n      revert NYProfitTakingVault__UnderlyingCapReached(underlyingCap);\\n            }\\n      uint256 _before = underlying.balanceOf(address(this));\\n            underlying.safeTransferFrom(msg.sender, address(this), _amount);\\n               uint256 _after = underlying.balanceOf(address(this));\\n                   _amount = _after - _before;\\n```\\n\\nA small issue is that underlyingCap is compared to the _amount before correction for actual transferred amount. Therefore, it cannot actually be reached, and limits the maximum capacity of the vault to underlyingCap minus a factor of the fee %.

Impact:

Recommended mitigation:
Move the underlyingCap check to below the effective _amount calculation
Vulnerability: Redundant checks in Vault V3
Severity: low

Description:
`depositProfitTokenForUsers()` and `withdrawProfit()` contain the following check:\\n```\\n    if (block.timestamp <= lastProfitTime) {\\n       revert NYProfitTakingVault__ProfitTimeOutOfBounds();\\n          }\\n```\\n\\nHowever, lastProfitTime is only ever set to block.timestamp. Therefore, it can never be larger than block.timestamp.

Impact:

Recommended mitigation:
It would be best in terms of gas costs and logical clarity to change the comparison to !=
Vulnerability: createUniswapRangeOrder() charges manager instead of pool
Severity: high

Description:
_createUniswapRangeOrder() can be called either from manager flow, with createUniswapRangeOrder(), or pool-induced from hedgeDelta(). The issue is that the function assumes the sender is the parentLiquidityPool, for example:\\n```\\n        if (inversed && balance < amountDesired) {\\n             // collat = 0\\n        uint256 transferAmount = amountDesired - balance;\\n         uint256 parentPoolBalance = \\n             ILiquidityPool(parentLiquidityPool).getBalance(address(token0));\\n        if (parentPoolBalance < transferAmount) { revert \\n            CustomErrors.WithdrawExceedsLiquidity(); \\n        }\\n        SafeTransferLib.safeTransferFrom(address(token0), msg.sender, \\n         address(this), transferAmount);\\n            } \\n```\\n\\nBalance check is done on pool, but money is transferred from sender. It will cause the order to use manager's funds.\\n```\\n    function createUniswapRangeOrder(\\n         RangeOrderParams calldata params,\\n             uint256 amountDesired\\n              ) external {\\n           require(!_inActivePosition(), "RangeOrder: active position");\\n         _onlyManager();\\n    bool inversed = collateralAsset == address(token0);\\n    _createUniswapRangeOrder(params, amountDesired, inversed);\\n    }\\n```\\n

Impact:

Recommended mitigation:
Ensure safeTransfer from uses parentLiquidityPool as source.
Vulnerability: hedgeDelta() priceToUse is calculated wrong, which causes bad hedges
Severity: high

Description:
When _delta parameter is negative for `hedgeDelta()`, priceToUse will be the minimum between quotePrice and underlyingPrice.\\n```\\n    // buy wETH\\n    // lowest price is best price when buying\\n        uint256 priceToUse = quotePrice < underlyingPrice ? quotePrice : \\n            underlyingPrice;\\n    RangeOrderDirection direction = inversed ? RangeOrderDirection.ABOVE \\n        : RangeOrderDirection.BELOW;\\n    RangeOrderParams memory rangeOrder = \\n        _getTicksAndMeanPriceFromWei(priceToUse, direction);\\n```\\n\\nThis works fine when direction is BELOW, because the calculated lowerTick and upperTick from _getTicksAndMeanPriceFromWei are guaranteed to be lower than current price.\\n```\\n    int24 lowerTick = direction == RangeOrderDirection.ABOVE ? \\n         nearestTick + tickSpacing : nearestTick - (2 * tickSpacing);\\n     int24 tickUpper = direction ==RangeOrderDirection.ABOVE ? lowerTick + \\n        tickSpacing : nearestTick - tickSpacing;\\n```\\n\\nTherefore, the fulfill condition is not true and we mint from the correct base. However, when direction is ABOVE, it is possible that the oracle supplied price (underlyingPrice) is low enough in comparison to pool price, that the fulfill condition is already active. In that case, the contract tries to mint from the wrong asset which will cause the wrong tokens to be sent in. In effect, the contract is not hedging. A similar situation occurs when _delta parameter is greater than zero.

Impact:

Recommended mitigation:
Verify the calculated priceToUse is on the same side as pool-calculated tick price.
Vulnerability: multiplication overflow in getPoolPrice() likely
Severity: medium

Description:
`getPoolPrice()` is used in hedgeDelta to get the price directly from Uniswap v3 pool:\\n```\\n    function getPoolPrice() public view returns (uint256 price, uint256 \\n         inversed){\\n            (uint160 sqrtPriceX96, , , , , , ) = pool.slot0();\\n        uint256 p = uint256(sqrtPriceX96) * uint256(sqrtPriceX96) * (10 \\n        ** token0.decimals());\\n     // token0/token1 in 1e18 format\\n          price = p / (2 ** 192);\\n              inversed = 1e36 / price;\\n         }\\n```\\n\\nThe issue is that calculation of p is likely to overflow. sqrtPriceX96 has 96 bits for decimals, 10** `token0.decimals()` will have 60 bits when decimals is 18, therefore there is only (256 - 2 * 96 - 60) / 2 = 2 bits for non-decimal part of sqrtPriceX96.

Impact:

Recommended mitigation:
Consider converting the sqrtPrice to a 60x18 format and performing arithmetic operations using the PRBMathUD60x18 library.
Vulnerability: Hedging won't work if token1.decimals() < token0.decimals()
Severity: medium

Description:
`tickToToken0PriceInverted()` performs some arithmetic calculations. It's called by `_getTicksAndMeanPriceFromWei()`, which is called by `hedgeDelta()`. This line can overflow:\\n```\\n    uint256 intermediate = inWei.div(10**(token1.decimals() -\\n         token0.decimals()));\\n```\\n\\nAlso, this line would revert even if the above calculation was done correctly:\\n```\\n    meanPrice = OptionsCompute.convertFromDecimals(meanPrice, \\n         token0.decimals(), token1.decimals());\\n```\\n\\n```\\n    function convertFromDecimals(uint256 value, uint8 decimalsA, uint8 decimalsB) internal pure\\n        returns (uint256) {\\n    if (decimalsA > decimalsB) {\\n          revert();\\n        }\\n        …\\n```\\n\\nThe impact is that when `token1.decimals()` < `token0.decimals()`, the contract's main function is unusable.

Impact:

Recommended mitigation:
Refactor the calculation to support different decimals combinations. Additionally, add more comprehensive tests to detect similar issues in the future.
Vulnerability: Overflow danger in _sqrtPriceX96ToUint
Severity: medium

Description:
_sqrtPriceX96ToUint will only work when the non-fractional component of sqrtPriceX96 takes up to 32 bits. This represents a price ratio of 18446744073709551616. With different token digits it is not unlikely that this ratio will be crossed which will make hedgeDelta() revert.\\n```\\n    function _sqrtPriceX96ToUint(uint160 sqrtPriceX96) private pure returns (uint256)\\n    {\\n        uint256 numerator1 = uint256(sqrtPriceX96) * \\n         uint256(sqrtPriceX96);\\n    return FullMath.mulDiv(numerator1, 1, 1 << 192);\\n         }\\n```\\n

Impact:

Recommended mitigation:
Perform the multiplication after converting the numbers to 60x18 variables
Vulnerability: Insufficient dust checks
Severity: low

Description:
In `hedgeDelta()`, there is a dust check in the case of sell wETH order:\\n```\\n        // sell wETH\\n             uint256 wethBalance = inversed ? amount1Current : amount0Current;\\n        if (wethBalance < minAmount) return 0;\\n```\\n\\nHowever, the actual used amount is _delta\\n```\\n             uint256 deltaToUse = _delta > int256(wethBalance) ? wethBalance : \\n           uint256(_delta);\\n        _createUniswapRangeOrder(rangeOrder, deltaToUse, inversed);\\n```\\n\\nThe check should be applied on deltaToUse rather than wethBalance because it will be the minimum of wethBalance and _delta. Additionally, there is no corresponding check for minting with collateral in case _delta is negative.

Impact:

Recommended mitigation:
Correct current dust checks and add them also in the if clause.
Vulnerability: Linear vesting users may not receive vested amount
Severity: high

Description:
TokenTransmuter supports two types of transmutations, linear and instant. In linear, allocated amount is released across time until fully vested, while in instant the entire amount is released immediately. transmuteLinear() checks that there is enough output tokens left in the contract before accepting transfer of input tokens.\\n```\\n       require(IERC20(outputTokenAddress).balanceOf(address(this)) >= \\n           (totalAllocatedOutputToken - totalReleasedOutputToken), \\n       "INSUFFICIENT_OUTPUT_TOKEN");\\n            IERC20(inputTokenAddress).transferFrom(msg.sender, address(0), \\n       _inputTokenAmount);\\n```\\n\\nHowever, `transmuteInstant()` lacks any remaining balance checks, and will operate as long as the function has enough output tokens to satisfy the request.\\n```\\n        IERC20(inputTokenAddress).transferFrom(msg.sender, address(0), \\n             _inputTokenAmount);\\n        SafeERC20.safeTransfer(IERC20(outputTokenAddress), msg.sender, \\n             allocation);\\n        emit OutputTokenInstantReleased(msg.sender, allocation, \\n             outputTokenAddress);\\n```\\n\\nAs a result, it is not ensured that tokens that have been reserved for linear distribution will be available when users request to claim them. An attacker may empty the output balance with a large instant transmute and steal future vested tokens of users.

Impact:

Recommended mitigation:
In transmuteInstant, add a check similar to the one in transmuteLinear. It will ensure allocations are kept faithfully.
Vulnerability: Multiplier implementation causes limited functionality
Severity: low

Description:
linearMultiplier and instantMultiplier are used to calculate output token amount from input token amount in transmute functions.\\n```\\n    uint256 allocation = (_inputTokenAmount * linearMultiplier) / \\n        tokenDecimalDivider;\\n    …\\n    uint256 allocation = (_inputTokenAmount * instantMultiplier) / \\n        tokenDecimalDivider;\\n```\\n\\nThe issue is that they are uint256 variables and can only multiply _inputTokenAmount, not divide it. It results in limited functionality of the protocol as vesting pairs where output tokens are valued more than input tokens cannot be used.

Impact:

Recommended mitigation:
Add a boolean state variable which will describe whether to multiply or divide by the multiplier.
Vulnerability: Requested oracle versions, which have expired, must return this oracle version as invalid, but they return it as a normal version with previous version's price instead
Severity: high

Description:
Each market action requests a new oracle version which must be commited by the keepers. However, if keepers are unable to commit requested version's price (for example, no price is available at the time interval, network or keepers are down), then after a certain timeout this oracle version will be commited as invalid, using the previous valid version's price.\\nThe issue is that when this expired oracle version is used by the market (using oracle.at), the version returned will be valid (valid = true), because oracle returns version as invalid only if `price = 0`, but the `commit` function sets the previous version's price for these, thus it's not 0.\\nThis leads to market using invalid versions as if they're valid, keeping the orders (instead of invalidating them), which is a broken core functionality and a security risk for the protocol.\\nWhen requested oracle version is commited, but is expired (commited after a certain timeout), the price of the previous valid version is set to this expired oracle version:\\n```\\nfunction _commitRequested(OracleVersion memory version) private returns (bool) {\\n    if (block.timestamp <= (next() + timeout)) {\\n        if (!version.valid) revert KeeperOracleInvalidPriceError();\\n        _prices[version.timestamp] = version.price;\\n    } else {\\n        // @audit previous valid version's price is set for expired version\\n        _prices[version.timestamp] = _prices[_global.latestVersion]; \\n    }\\n    _global.latestIndex++;\\n    return true;\\n}\\n```\\n\\nLater, `Market._processOrderGlobal` reads the oracle version using the `oracle.at`, invalidating the order if the version is invalid:\\n```\\nfunction _processOrderGlobal(\\n    Context memory context,\\n    SettlementContext memory settlementContext,\\n    uint256 newOrderId,\\n    Order memory newOrder\\n) private {\\n    OracleVersion memory oracleVersion = oracle.at(newOrder.timestamp);\\n\\n    context.pending.global.sub(newOrder);\\n    if (!oracleVersion.valid) newOrder.invalidate();\\n```\\n\\nHowever, expired oracle version will return `valid = true`, because this flag is only set to `false` if price = 0:\\n```\\nfunction at(uint256 timestamp) public view returns (OracleVersion memory oracleVersion) {\\n    (oracleVersion.timestamp, oracleVersion.price) = (timestamp, _prices[timestamp]);\\n    oracleVersion.valid = !oracleVersion.price.isZero(); // @audit <<< valid = false only if price = 0\\n}\\n```\\n\\nThis means that `_processOrderGlobal` will treat this expired oracle version as valid and won't invalidate the order.

Impact:
Market uses invalid (expired) oracle versions as if they're valid, keeping the orders (instead of invalidating them), which is a broken core functionality and a security risk for the protocol.

Recommended mitigation:
Add validity map along with the price map to `KeeperOracle` when recording commited price.
Vulnerability: _loadContext() uses the wrong pendingGlobal.
Severity: medium

Description:
`StrategyLib._loadContext()` is using the incorrect `pendingGlobal`, causing `currentPosition`, `minPosition`, and `maxPosition` to be incorrect, leading to incorrect rebalance operation.\\nIn `StrategyLib._loadContext()`, there is a need to compute `currentPosition`, `minPosition`, and `maxPosition`. The code as follows:\\n```\\n    function _loadContext(\\n        Registration memory registration\\n    ) private view returns (MarketStrategyContext memory marketContext) {\\n// rest of code\\n        // current position\\n      Order memory pendingGlobal = registration.market.pendings(address(this));\\n        marketContext.currentPosition = registration.market.position();\\n        marketContext.currentPosition.update(pendingGlobal);\\n        marketContext.minPosition = marketContext.currentAccountPosition.maker\\n            .unsafeSub(marketContext.currentPosition.maker\\n                .unsafeSub(marketContext.currentPosition.skew().abs()).min(marketContext.closable));\\n        marketContext.maxPosition = marketContext.currentAccountPosition.maker\\n            .add(marketContext.riskParameter.makerLimit.unsafeSub(marketContext.currentPosition.maker));\\n    }\\n```\\n\\nThe code above `pendingGlobal = registration.market.pendings(address(this));` is wrong It takes the address(this)'s `pendingLocal`. The correct approach is to use `pendingGlobal = registration.market.pending();`.

Impact:
Since `pendingGlobal` is wrong, `currentPosition`, `minPosition` and `maxPosition` are all wrong. affects subsequent rebalance calculations, such as `target.position` etc. rebalance does not work properly

Recommended mitigation:
```\\n    function _loadContext(\\n        Registration memory registration\\n    ) private view returns (MarketStrategyContext memory marketContext) {\\n// rest of code\\n        // current position\\n// Remove the line below\\n       Order memory pendingGlobal = registration.market.pendings(address(this));\\n// Add the line below\\n       Order memory pendingGlobal = registration.market.pending();\\n        marketContext.currentPosition = registration.market.position();\\n        marketContext.currentPosition.update(pendingGlobal);\\n        marketContext.minPosition = marketContext.currentAccountPosition.maker\\n            .unsafeSub(marketContext.currentPosition.maker\\n                .unsafeSub(marketContext.currentPosition.skew().abs()).min(marketContext.closable));\\n        marketContext.maxPosition = marketContext.currentAccountPosition.maker\\n            .add(marketContext.riskParameter.makerLimit.unsafeSub(marketContext.currentPosition.maker));\\n    }\\n```\\n
Vulnerability: Liquidator can set up referrals for other users
Severity: medium

Description:
If a user has met the liquidation criteria and currently has no referrer then a malicious liquidator can specify a referrer in the liquidation order. making it impossible for subsequent users to set up the referrer they want.\\nCurrently, there are 2 conditions to set up a referrer\\nthe order cannot be empty （Non-empty orders require authorization unless they are liquidation orders）\\nthere can't be another referrer already\\n```\\n    function _loadUpdateContext(\\n        Context memory context,\\n        address account,\\n        address referrer\\n    ) private view returns (UpdateContext memory updateContext) {\\n// rest of code\\n        updateContext.referrer = referrers[account][context.local.currentId];\\n        updateContext.referralFee = IMarketFactory(address(factory())).referralFee(referrer);\\n    }\\n\\n    function _processReferrer(\\n        UpdateContext memory updateContext,\\n        Order memory newOrder,\\n        address referrer\\n    ) private pure {\\n      if (newOrder.makerReferral.isZero() && newOrder.takerReferral.isZero()) return;\\n        if (updateContext.referrer == address(0)) updateContext.referrer = referrer;\\n        if (updateContext.referrer == referrer) return;\\n\\n        revert MarketInvalidReferrerError();\\n    }\\n\\n\\n    function _storeUpdateContext(Context memory context, UpdateContext memory updateContext, address account) private {\\n// rest of code\\n        referrers[account][context.local.currentId] = updateContext.referrer;\\n    }\\n```\\n\\nHowever, if the user does not have a referrer, the liquidation order is able to meet both of these restrictions\\nThis allows the liquidator to set up referrals for other users.\\nWhen the user subsequently tries to set up a referrer, it will fail.

Impact:
If a user is set up as a referrer by a liquidated order in advance, the user cannot be set up as anyone else.

Recommended mitigation:
Restrictions on Liquidation Orders Cannot Set a referrer\\n```\\n    function _processReferrer(\\n        UpdateContext memory updateContext,\\n        Order memory newOrder,\\n        address referrer\\n    ) private pure {\\n// Add the line below\\n       if (newOrder.protected() && referrer != address(0)) revert MarketInvalidReferrerError;\\n        if (newOrder.makerReferral.isZero() && newOrder.takerReferral.isZero()) return;\\n        if (updateContext.referrer == address(0)) updateContext.referrer = referrer;\\n        if (updateContext.referrer == referrer) return;\\n\\n        revert MarketInvalidReferrerError();\\n    }\\n```\\n
Vulnerability: Liquidity provider fees can be stolen from any pair
Severity: high

Description:
An attacker can steal the liquidiy providers fees by transfering liquidity tokens to the pair and then withdrawing fees on behalf of the pair itself.\\nThis is possible because of two reasons:\\nTransfering liquidity tokens to the pair itself doesn't update the fee tracking variables:\\n```\\nif (to != address(this)) {\\n    _updateFeeRewards(to);\\n}\\n```\\n\\nwhich results in the variable `feesPerTokenPaid[address(pair)]` of the pair being equal to 0.\\nThe function withdrawFees() is a permissionless function that allows to withdraw fees on behalf of any address, including the pair itself.\\nBy combining this two quirks of the codebase an attacker can steal all of the currently pending liquidity provider fees by doing the following:\\nAdd liquidity to a pair, which will mint the attacker some liquidity tokens\\nTransfer the liquidity tokens to the pair directly\\nCall withdrawFees() by passing the address of the pair. Because `feesPerTokenPaid[address(pair)]` is 0 this will collect fees on behalf of the pair even if it shouldn't. The function will transfer an amount `x` of WETH from the pair to the pair itself and will lower the `_pendingLiquidityFee` variable by that same amount\\nBecause the variable `_pendingLiquidityFee` has been lowered by `x` the pool will assume someone transferred `x` WETH to it\\nAt this point the attacker can take advantage of this however he likes, but for the sake of the example let's suppose he calls swap() to swap `x` ETH into tokens that will be transferred to his wallet\\nThe attacker burns the liquidity transferred at point `2` to recover his funds\\nPOC\\n

Impact:
Liquidity provider fees can be stolen from any pair.

Recommended mitigation:
In withdrawFees(pair) add a require statement to prevent fees being withdrawn on behalf of the pool.\\n```\\nrequire(to != address(this));\\n```\\n
Vulnerability: The router is not compatible with fee on transfers tokens
Severity: medium

Description:
The router is not compatible with fee on transfers tokens.\\nLet's take as example the removeLiquidity function:\\n```\\naddress pair = GoatV1Factory(FACTORY).getPool(token);\\n\\nIERC20(pair).safeTransferFrom(msg.sender, pair, liquidity); //-> 1. Transfers liquidity tokens to the pair\\n(amountWeth, amountToken) = GoatV1Pair(pair).burn(to); //-> 2. Burns the liquidity tokens and sends WETH and TOKEN to the recipient\\nif (amountWeth < wethMin) { //-> 3. Ensures enough WETH has been transferred\\n    revert GoatErrors.InsufficientWethAmount();\\n}\\nif (amountToken < tokenMin) { //4. Ensures enough TOKEN has been transferred\\n    revert GoatErrors.InsufficientTokenAmount();\\n}\\n```\\n\\nIt does the following:\\nTransfers liquidity tokens `to` the pair.\\nBurns the liquidity tokens and sends WETH and TOKEN `to` the recipient `to`.\\nEnsures enough WETH has been transferred.\\nEnsures enough TOKEN has been transferred.\\nAt point `4` the router doesn't account for the fee paid to transfer TOKEN. The recipient didn't actually receive `amountToken`, but slightly less because a fee has been charged.\\nAnother interesting example is the removeLiquidityETH which first burns the liquidity and transfers the tokens to the router itself, and then from the router the tokens are transferred to the recipient. This will charge double the fees.\\nThis is just two examples to highlight the fact that these kind of tokens are not supported, but the other functions in the router have similar issues that can cause all sorts of trouble including reverts and loss of funds.

Impact:
The router is not compatible with fee on transfers tokens.

Recommended mitigation:
Add functionality to the router to support fee on transfer tokens, a good example of where this is correctly implememented is the Uniswap Router02.