msc-smart-contract-auditing/vulnerable-functions-base huggingface dataset:
Vulnerability: Limit orders can be used to get a free look into the future
Severity: high

Description:
Users can continually update their orders to get a free look into prices in future blocks\\nOrder execution relies on signed archived prices from off-chain oracles, where each price is stored along with the block range it applies to, and limit orders are only allowed to execute with oracle prices where the block is greater than the block in which the order was last updated. Since prices are required to be future prices, there is a time gap between when the last signed price was archived, and the new price for the next block is stored in the archive, and the order keeper is able to fetch it and submit an execution for it in the next block.\\nThe example given by the sponsor in discord was:\\n```\\nthe oracle process:\\n\\n1. the oracle node checks the latest price from reference exchanges and stores it with the oracle node's timestamp, e.g. time: 1000\\n2. the oracle node checks the latest block of the blockchain, e.g. block 100, it stores this with the oracle node's timestamp as well\\n3. the oracle node signs minOracleBlockNumber: 100, maxOracleBlockNumber: 100, timestamp: 1000, price: <price>\\n4. the next time the loop runs is at time 1001, if the latest block of the blockchain is block 105, e.g. if 5 blocks were produced in that one second, then the oracle would sign\\nminOracleBlockNumber: 101, maxOracleBlockNumber: 105, timestamp: 1001, price: <price>\\n```\\n

Impact:
If a user has a pending exit order that was submitted a block N, and the user sees that the price at block N+1 will be more favorable, they can update their exit order, changing the amount by +/- 1 wei, and have the order execution delayed until the next block, at which point they can decided again whether the price and or impact is favorable, and whether to exit. In the sponsor's example, if the order was submitted at block 101, they have until block 105 to decide whether to update their order, since the order execution keeper won't be able to do the execution until block 106. There is a gas cost for doing such updates, but if the position is large enough, or the price is gapping enough, it is worth while to do this, especially if someone comes up with an automated service that does this on your behalf.\\nThe more favorable price for the attacker is at the expense of the other side of the trade, and is a loss of capital for them.

Recommended mitigation:
Require a delay between when the order was last increased/submitted, and when an update is allowed, similar to REQUEST_EXPIRATION_BLOCK_AGE for the cancellation of market orders
Vulnerability: Multiplication after Division error leading to larger precision loss
Severity: medium

Description:
There are couple of instance of using result of a division for multiplication while can cause larger precision loss.\\n```\\nFile: MarketUtils.sol\\n\\n   cache.fundingUsd = (cache.sizeOfLargerSide / Precision.FLOAT_PRECISION) * cache.durationInSeconds * cache.fundingFactorPerSecond;\\n\\n   if (result.longsPayShorts) {\\n         cache.fundingUsdForLongCollateral = cache.fundingUsd * cache.oi.longOpenInterestWithLongCollateral / cache.oi.longOpenInterest;\\n         cache.fundingUsdForShortCollateral = cache.fundingUsd * cache.oi.longOpenInterestWithShortCollateral / cache.oi.longOpenInterest;\\n     } else {\\n         cache.fundingUsdForLongCollateral = cache.fundingUsd * cache.oi.shortOpenInterestWithLongCollateral / cache.oi.shortOpenInterest;\\n         cache.fundingUsdForShortCollateral = cache.fundingUsd * cache.oi.shortOpenInterestWithShortCollateral / cache.oi.shortOpenInterest;\\n     }\\n```\\n\\nLink to Code\\nIn above case, value of `cache.fundingUsd` is calculated by first dividing `cache.sizeOfLargerSide` with `Precision.FLOAT_PRECISION` which is `10**30`. Then the resultant is multiplied further. This result in larger Loss of precision.\\nLater the same `cache.fundingUsd` is used to calculate `cache.fundingUsdForLongCollateral` and `cache.fundingUsdForShortCollateral` by multiplying further which makes the precision error even more big.\\nSame issue is there in calculating `cache.positionPnlUsd` in `PositionUtils`.\\n```\\nFile: PositionUtils.sol\\n\\n    if (position.isLong()) {\\n            cache.sizeDeltaInTokens = Calc.roundUpDivision(position.sizeInTokens() * sizeDeltaUsd, position.sizeInUsd());\\n        } else {\\n            cache.sizeDeltaInTokens = position.sizeInTokens() * sizeDeltaUsd / position.sizeInUsd();\\n        }\\n    }\\n\\n    cache.positionPnlUsd = cache.totalPositionPnl * cache.sizeDeltaInTokens.toInt256() / position.sizeInTokens().toInt256();\\n```\\n\\nLink to Code

Impact:
Precision Loss in accounting.

Recommended mitigation:
First Multiply all the numerators and then divide it by the product of all the denominator.
Vulnerability: when execute deposit fails, cancel deposit will be called which means that execution fee for keeper will be little for executing the cancellation depending on where the executeDeposit fails
Severity: medium

Description:
When execute deposit fails, the deposit will be automatically cancelled. However, since executeDeposit has taken up a portion of the execution fee, execution fee left for cancellation might be little and keeper will lose out on execution fee.\\nIn `executeDeposit` when an error is thrown, `_handleDepositError` is called.\\n```\\n            _handleDepositError(\\n                key,\\n                startingGas,\\n                reasonBytes\\n            );\\n```\\n\\nNotice that in `_handleDepositError` that `cancelDeposit` is called which will pay execution fee to the keeper. However, since the failure can have failed at the late stage of executeDeposit, the execution fee left for the cancellation will be little for the keeper.\\n```\\n    function _handleDepositError(\\n        bytes32 key,\\n        uint256 startingGas,\\n        bytes memory reasonBytes\\n    ) internal {\\n        (string memory reason, /* bool hasRevertMessage */) = ErrorUtils.getRevertMessage(reasonBytes);\\n\\n\\n        bytes4 errorSelector = ErrorUtils.getErrorSelectorFromData(reasonBytes);\\n\\n\\n        if (OracleUtils.isEmptyPriceError(errorSelector)) {\\n            ErrorUtils.revertWithCustomError(reasonBytes);\\n        }\\n\\n\\n        DepositUtils.cancelDeposit(\\n            dataStore,\\n            eventEmitter,\\n            depositVault,\\n            key,\\n            msg.sender,\\n            startingGas,\\n            reason,\\n            reasonBytes\\n        );\\n    }\\n}\\n```\\n\\nNote: This also applies to failed `executeWithdrawal`.

Impact:
Keeper will lose out on execution fee in the event of a failed deposit.

Recommended mitigation:
Recommend increasing the minimum required execution fee to account for failed deposit and refund the excess to the user when a deposit succeeds.
Vulnerability: Adversary can sandwich oracle updates to exploit vault
Severity: high

Description:
BLVaultLido added a mechanism to siphon off all wstETH obtained from mismatched pool and oracle prices. This was implemented to fix the problem that the vault could be manipulated to the attackers gain. This mitigation however does not fully address the issue and the same issue is still exploitable by sandwiching oracle update.\\nBLVaultLido.sol#L232-L240\\n```\\n    uint256 wstethOhmPrice = manager.getTknOhmPrice();\\n    uint256 expectedWstethAmountOut = (ohmAmountOut * wstethOhmPrice) / _OHM_DECIMALS;\\n\\n    // Take any arbs relative to the oracle price for the Treasury and return the rest to the owner\\n    uint256 wstethToReturn = wstethAmountOut > expectedWstethAmountOut\\n        ? expectedWstethAmountOut\\n        : wstethAmountOut;\\n    if (wstethAmountOut > wstethToReturn)\\n        wsteth.safeTransfer(TRSRY(), wstethAmountOut - wstethToReturn);\\n```\\n\\nIn the above lines we can see that the current oracle price is used to calculate the expected amount of wstETH to return to the user. In theory this should prevent the attack but an attacker can side step this sandwiching the oracle update.\\nExample:\\nThe POC is very similar to before except now it's composed of two transactions sandwiching the oracle update. Chainlink oracles have a tolerance threshold of 0.5% before updating so we will use that as our example value. The current price is assumed to be 0.995 wstETH/OHM. The oracle price (which is about to be updated) is currently 1:1\\n```\\nTransaction 1:\\n\\nBalances before attack (0.995:1)\\nLiquidity: 79.8 OHM 80.2 wstETH\\nAdversary: 20 wstETH\\n\\nSwap OHM so that pool price matches pre-update oracle price:\\nLiquidity: 80 OHM 80 wstETH\\nAdversary: -0.2 OHM 20.2 wstETH\\n\\nBalances after adversary has deposited to the pool:\\nLiquidity: 100 OHM 100 wstETH\\nAdversary: -0.2 OHM 0.2 wstETH\\n\\nBalances after adversary sells wstETH for OHM (0.5% movement in price):\\nLiquidity: 99.748 OHM 100.252 wstETH\\nAdversary: 0.052 OHM -0.052 wstETH\\n\\nSandwiched Oracle Update:\\n\\nOracle updates price of wstETH to 0.995 OHM. Since the attacker already sold wstETH to balance \\nthe pool to the post-update price they will be able to withdraw the full amount of wstETH.\\n\\nTransaction 2:\\n\\nBalances after adversary removes their liquidity:\\nLiquidity: 79.798 OHM 80.202 wstETH\\nAdversary: 0.052 OHM 19.998 wstETH\\n\\nBalances after selling profited OHM:\\nLiquidity: 79.849 OHM 80.152 wstETH\\nAdversary: 20.05 wstETH\\n```\\n\\nAs shown above it's still profitable to exploit the vault by sandwiching the oracle updates. With each oracle update the pool can be repeatedly attacked causing large losses.

Impact:
Vault will be attacked repeatedly for large losses

Recommended mitigation:
To prevent this I would recommend locking the user into the vault for some minimum amount of time (i.e. 24 hours)
Vulnerability: minTokenAmounts_ is useless in new configuration and doesn't provide any real slippage protection
Severity: high

Description:
BLVaultLido#withdraw skims off extra stETH from the user that results from oracle arb. The problem with this is that minTokenAmounts_ no longer provides any slippage protection because it only ensures that enough is received from the liquidity pool but never enforces how much is received by the user.\\nBLVaultLido.sol#L224-L247\\n```\\n    _exitBalancerPool(lpAmount_, minTokenAmounts_);\\n\\n    // Calculate OHM and wstETH amounts received\\n    uint256 ohmAmountOut = ohm.balanceOf(address(this)) - ohmBefore;\\n    uint256 wstethAmountOut = wsteth.balanceOf(address(this)) - wstethBefore;\\n\\n    // Calculate oracle expected wstETH received amount\\n    // getTknOhmPrice returns the amount of wstETH per 1 OHM based on the oracle price\\n    uint256 wstethOhmPrice = manager.getTknOhmPrice();\\n    uint256 expectedWstethAmountOut = (ohmAmountOut * wstethOhmPrice) / _OHM_DECIMALS;\\n\\n    // Take any arbs relative to the oracle price for the Treasury and return the rest to the owner\\n    uint256 wstethToReturn = wstethAmountOut > expectedWstethAmountOut\\n        ? expectedWstethAmountOut\\n        : wstethAmountOut;\\n    if (wstethAmountOut > wstethToReturn)\\n        wsteth.safeTransfer(TRSRY(), wstethAmountOut - wstethToReturn);\\n\\n    // Burn OHM\\n    ohm.increaseAllowance(MINTR(), ohmAmountOut);\\n    manager.burnOhmFromVault(ohmAmountOut);\\n\\n    // Return wstETH to owner\\n    wsteth.safeTransfer(msg.sender, wstethToReturn);\\n```\\n\\nminTokenAmounts_ only applies to the removal of liquidity. Since wstETH is skimmed off to the treasury the user no longer has any way to protect themselves from slippage. As shown in my other submission, oracle slop can lead to loss of funds due to this skimming.

Impact:
Users cannot protect themselves from oracle slop/wstETH skimming

Recommended mitigation:
Allow the user to specify the amount of wstETH they receive AFTER the arb is skimmed.
Vulnerability: Adversary can stake LP directly for the vault then withdraw to break lp accounting in BLVaultManagerLido
Severity: high

Description:
The AuraRewardPool allows users to stake directly for other users. In this case the malicious user could stake LP directly for their vault then call withdraw on their vault. This would cause the LP tracking to break on BLVaultManagerLido. The result is that some users would now be permanently trapped because their vault would revert when trying to withdraw.\\nBaseRewardPool.sol#L196-L207\\n```\\nfunction stakeFor(address _for, uint256 _amount)\\n    public\\n    returns(bool)\\n{\\n    _processStake(_amount, _for);\\n\\n    //take away from sender\\n    stakingToken.safeTransferFrom(msg.sender, address(this), _amount);\\n    emit Staked(_for, _amount);\\n    \\n    return true;\\n}\\n```\\n\\nAuraRewardPool allows users to stake directly for another address with them receiving the staked tokens.\\nBLVaultLido.sol#L218-L224\\n```\\n    manager.decreaseTotalLp(lpAmount_);\\n\\n    // Unstake from Aura\\n    auraRewardPool().withdrawAndUnwrap(lpAmount_, claim_);\\n\\n    // Exit Balancer pool\\n    _exitBalancerPool(lpAmount_, minTokenAmounts_);\\n```\\n\\nOnce the LP has been stake the adversary can immediately withdraw it from their vault. This calls decreaseTotalLP on BLVaultManagerLido which now permanently break the LP account.\\nBLVaultManagerLido.sol#L277-L280\\n```\\nfunction decreaseTotalLp(uint256 amount_) external override onlyWhileActive onlyVault {\\n    if (amount_ > totalLp) revert BLManagerLido_InvalidLpAmount();\\n    totalLp -= amount_;\\n}\\n```\\n\\nIf the amount_ is ever greater than totalLP it will cause decreaseTotalLP to revert. By withdrawing LP that was never deposited to a vault, it permanently breaks other users from being able to withdraw.\\nExample: User A deposits wstETH to their vault which yields 50 LP. User B creates a vault then stake 50 LP and withdraws it from his vault. The manager now thinks there is 0 LP in vaults. When User A tries to withdraw their LP it will revert when it calls manger.decreaseTotalLp. User A is now permanently trapped in the vault.

Impact:
LP accounting is broken and users are permanently trapped.

Recommended mitigation:
Individual vaults should track how much they have deposited and shouldn't be allowed to withdraw more than deposited.
Vulnerability: Users can abuse discrepancies between oracle and true asset price to mint more OHM than needed and profit from it
Severity: high

Description:
All chainlink oracles have a deviation threshold between the current price of the asset and the on-chain price for that asset. The more oracles used for determining the price the larger the total discrepancy can be. These can be combined and exploited to mint more OHM than expected and profit.\\nBLVaultLido.sol#L156-L171\\n```\\n    uint256 ohmWstethPrice = manager.getOhmTknPrice();\\n    uint256 ohmMintAmount = (amount_ * ohmWstethPrice) / _WSTETH_DECIMALS;\\n\\n    // Block scope to avoid stack too deep\\n    {\\n        // Cache OHM-wstETH BPT before\\n        uint256 bptBefore = liquidityPool.balanceOf(address(this));\\n\\n        // Transfer in wstETH\\n        wsteth.safeTransferFrom(msg.sender, address(this), amount_);\\n\\n        // Mint OHM\\n        manager.mintOhmToVault(ohmMintAmount);\\n\\n        // Join Balancer pool\\n        _joinBalancerPool(ohmMintAmount, amount_, minLpAmount_);\\n```\\n\\nThe amount of OHM to mint and deposit is determined by the calculated price from the on-chain oracle prices.\\nBLVaultLido.sol#L355-L364\\n```\\n    uint256[] memory maxAmountsIn = new uint256[](2);\\n    maxAmountsIn[0] = ohmAmount_;\\n    maxAmountsIn[1] = wstethAmount_;\\n\\n    JoinPoolRequest memory joinPoolRequest = JoinPoolRequest({\\n        assets: assets,\\n        maxAmountsIn: maxAmountsIn,\\n        userData: abi.encode(1, maxAmountsIn, minLpAmount_),\\n        fromInternalBalance: false\\n    });\\n```\\n\\nTo make the issue worse, _joinBalancerPool use 1 for the join type. This is the EXACT_TOKENS_IN_FOR_BPT_OUT method of joining. What this means is that the join will guaranteed use all input tokens. If the current pool isn't balanced in the same way then the join request will effectively swap one token so that the input tokens match the current pool. Now if the ratio is off then too much OHM will be minted and will effectively traded for wstETH. This allows the user to withdraw at a profit once the oracle has been updated the discrepancy is gone.

Impact:
Users can always time oracles so that they enter at an advantageous price and the deficit is paid by Olympus with minted OHM

Recommended mitigation:
The vault needs to have withdraw and/or deposit fees to make attacks like this unprofitable.
Vulnerability: stETH/ETH chainlink oracle has too long of heartbeat and deviation threshold which can cause loss of funds
Severity: medium

Description:
getTknOhmPrice uses the stETH/ETH chainlink oracle to calculate the current price of the OHM token. This token valuation is used to determine the amount of stETH to skim from the user resulting from oracle arb. This is problematic since stETH/ETH has a 24 hour heartbeat and a 2% deviation threshold. This deviation in price could easily cause loss of funds to the user.\\nBLVaultManagerLido.sol#L458-L473\\n```\\nfunction getTknOhmPrice() public view override returns (uint256) {\\n    // Get stETH per wstETH (18 Decimals)\\n    uint256 stethPerWsteth = IWsteth(pairToken).stEthPerToken();\\n\\n    // Get ETH per OHM (18 Decimals)\\n    uint256 ethPerOhm = _validatePrice(ohmEthPriceFeed.feed, ohmEthPriceFeed.updateThreshold);\\n\\n    // Get stETH per ETH (18 Decimals)\\n    uint256 stethPerEth = _validatePrice(\\n        stethEthPriceFeed.feed,\\n        stethEthPriceFeed.updateThreshold\\n    );\\n\\n    // Calculate wstETH per OHM (18 decimals)\\n    return (ethPerOhm * 1e36) / (stethPerWsteth * stethPerEth);\\n}\\n```\\n\\ngetTknOhmPrice uses the stETH/ETH oracle to determine the price which as stated above has a 24 hour hearbeat and 2% deviation threshold, this means that the price can move up to 2% or 24 hours before a price update is triggered. The result is that the on-chain price could be much different than the true stETH price.\\nBLVaultLido.sol#L232-L240\\n```\\n    uint256 wstethOhmPrice = manager.getTknOhmPrice();\\n    uint256 expectedWstethAmountOut = (ohmAmountOut * wstethOhmPrice) / _OHM_DECIMALS;\\n\\n    // Take any arbs relative to the oracle price for the Treasury and return the rest to the owner\\n    uint256 wstethToReturn = wstethAmountOut > expectedWstethAmountOut\\n        ? expectedWstethAmountOut\\n        : wstethAmountOut;\\n    if (wstethAmountOut > wstethToReturn)\\n        wsteth.safeTransfer(TRSRY(), wstethAmountOut - wstethToReturn);\\n        \\n```\\n\\nThis price is used when determining how much stETH to send back to the user. Since the oracle can be up to 2% different from the true price, the user can unfairly lose part of their funds.

Impact:
User will be unfairly penalized due large variance between on-chain price and asset price

Recommended mitigation:
Use the stETH/USD oracle instead because it has a 1-hour heartbeat and a 1% deviation threshold.
Vulnerability: Normal users could be inadvertently grieved by the withdrawn ratios check
Severity: medium

Description:
The contract check on the withdrawn ratios of OHM and wstETH against the current oracle price could run into grieving naive users by taking any wstETH shifted imbalance as a fee to the treasury even though these users have not gamed the system.\\nHere is a typical scenario, assuming the pool has been initiated with total LP equal to sqrt(100_000 * 1_000) = 10_000. (Note: OHM: $15, wstETH: $1500 with the pool pricing match up with manager.getOhmTknPrice() or manager.getTknOhmPrice(), i.e. 100 OHM to 1 wstETH or 0.01 wstETH to 1 OHM. The pool token balances in each step below may be calculated via the Constant Product Simulation after each swap and stake.)\\n```\\nOHM token balance: 100_000\\nwstETH token balance: 1_000\\nTotal LP: 10_000\\n```\\n\\nA series of swap activities results in the pool shifted more of the LP into wstETH.\\nOHM token balance: 90_909.1 wstETH token balance: 1_100 Total LP: 10_000\\nBob calls `deposit()` by providing 11 wstETH where 1100 OHM is minted with 1100 - 90909.1 * 0.01 = 190.91 unused OHM burned. (Note: Bob successfully stakes with 909.09 OHM and 11 wstETH and proportionately receives 100 LP.)\\nOHM token balance: 91_818.19 wstETH token balance: 1_111 Total LP: 10_100 User's LP: 100\\nBob changes his mind instantly and proceeds to call `withdraw()` to remove all of his LP. He receives the originally staked 909.09 OHM and 11 wstETH. All OHM is burned but he is only entitled to receive 909.09 / 100 = 9.09 wstETH since the system takes any arbs relative to the oracle price for the Treasury and returns the rest to the owner.\\nOHM token balance: 90_909.1 wstETH token balance: 1_100 Total LP: 10_000 User's LP: 0

Impact:
Bob suffers a loss of 11 - 9.09 = 1.91 wstETH (~ 17.36% loss), and the system is ready to trap the next user given the currently imbalanced pool still shifted more of the LP into wstETH.

Recommended mitigation:
Consider implementing a snapshot of the entry record of OHM and wstETH and compare that with the proportionate exit record. Slash only the differential for treasury solely on dissuading large attempts to shift the pool around, and in this case it should be 0 wstETH since the originally staked wstETH is no greater than expectedWstethAmountOut.
Vulnerability: Periphery#_swapPTsForTarget won't work correctly if PT is mature but redeem is restricted
Severity: medium

Description:
Periphery#_swapPTsForTarget doesn't properly account for mature PTs that have their redemption restricted\\nPeriphery.sol#L531-L551\\n```\\nfunction _swapPTsForTarget(\\n    address adapter,\\n    uint256 maturity,\\n    uint256 ptBal,\\n    PermitData calldata permit\\n) internal returns (uint256 tBal) {\\n    _transferFrom(permit, divider.pt(adapter, maturity), ptBal);\\n\\n    if (divider.mscale(adapter, maturity) > 0) {\\n        tBal = divider.redeem(adapter, maturity, ptBal); <- @audit-issue always tries to redeem even if restricted\\n    } else {\\n        tBal = _balancerSwap(\\n            divider.pt(adapter, maturity),\\n            Adapter(adapter).target(),\\n            ptBal,\\n            BalancerPool(spaceFactory.pools(adapter, maturity)).getPoolId(),\\n            0,\\n            payable(address(this))\\n        );\\n    }\\n}\\n```\\n\\nAdapters can have their redeem restricted meaning the even when they are mature they can't be redeemed. In the scenario that it is restricted Periphery#_swapPTsForTarget simply won't work.

Impact:
Redemption will fail when redeem is restricted because it tries to redeem instead of swapping

Recommended mitigation:
Use the same structure as _removeLiquidity:\\n```\\n    if (divider.mscale(adapter, maturity) > 0) {\\n        if (uint256(Adapter(adapter).level()).redeemRestricted()) {\\n            ptBal = _ptBal;\\n        } else {\\n            // 2. Redeem PTs for Target\\n            tBal += divider.redeem(adapter, maturity, _ptBal);\\n        }\\n```\\n