msc-smart-contract-auditing/vulnerable-functions-base huggingface dataset:
Vulnerability: Slashing during `LSTCalculatorBase.sol` deployment can show bad apr for months
Severity: medium

Description:
The contract `LSTCalculatorBase.sol` has some functions to calculate the rough APR expected from a liquid staking token. The contract is first deployed, and the first snapshot is taken after `APR_FILTER_INIT_INTERVAL_IN_SEC`, which is 9 days. It then calculates the APR between the deployment and this first snapshot, and uses that to initialize the APR value. It uses the function `calculateAnnualizedChangeMinZero` to do this calculation.\\nThe issue is that the function `calculateAnnualizedChangeMinZero` has a floor of 0. So if the backing of the LST decreases over that 9 days due to a slashing event in that interval, this function will return 0, and the initial APR and `baseApr` will be set to 0.\\nThe calculator is designed to update the APR at regular intervals of 3 days. However, the new apr is given a weight of 10% and the older apr is given a weight of 90% as seen below.\\n```\\nreturn ((priorValue * (1e18 - alpha)) + (currentValue * alpha)) / 1e18;\\n```\\n\\nAnd alpha is hardcoded to 0.1. So if the initial APR starts at 0 due to a slashing event in the initial 9 day period, a large number of updates will be required to bring the APR up to the correct value.\\nAssuming the correct APR of 6%, and an initial APR of 0%, we can calculate that it takes upto 28 updates to reflect close the correct APR. This transaltes to 84 days. So the wrong APR cann be shown for upto 3 months. Tha protocol uses these APR values to justify the allocation to the various protocols. Thus a wrong APR for months would mean the protocol would sub optimally allocate funds for months, losing potential yield.

Impact:
The protocol can underperform for months due to slashing events messing up APR calculations close to deployment date.

Recommended mitigation:
It is recommended to initialize the APR with a specified value, rather than calculate it over the initial 9 days. 9 day window is not good enough to get an accurate APR, and can be easily manipulated by a slashing event.
Vulnerability: curve admin can drain pool via reentrancy (equal to execute emergency withdraw and rug tokenmak fund by third party)
Severity: medium

Description:
A few curve liquidity is pool is well in-scope:\\n```\\nCurve Pools\\n\\nCurve stETH/ETH: 0x06325440D014e39736583c165C2963BA99fAf14E\\nCurve stETH/ETH ng: 0x21E27a5E5513D6e65C4f830167390997aA84843a\\nCurve stETH/ETH concentrated: 0x828b154032950C8ff7CF8085D841723Db2696056\\nCurve stETH/frxETH: 0x4d9f9D15101EEC665F77210cB999639f760F831E\\nCurve rETH/ETH: 0x6c38cE8984a890F5e46e6dF6117C26b3F1EcfC9C\\nCurve rETH/wstETH: 0x447Ddd4960d9fdBF6af9a790560d0AF76795CB08\\nCurve rETH/frxETH: 0xbA6c373992AD8ec1f7520E5878E5540Eb36DeBf1\\nCurve cbETH/ETH: 0x5b6C539b224014A09B3388e51CaAA8e354c959C8\\nCurve cbETH/frxETH: 0x548E063CE6F3BaC31457E4f5b4e2345286274257\\nCurve frxETH/ETH: 0xf43211935C781D5ca1a41d2041F397B8A7366C7A\\nCurve swETH/frxETH: 0xe49AdDc2D1A131c6b8145F0EBa1C946B7198e0BA\\n```\\n\\none of the pool is 0x21E27a5E5513D6e65C4f830167390997aA84843a\\nAdmin of curve pools can easily drain curve pools via reentrancy or via the `withdraw_admin_fees` function.\\n```\\n@external\\ndef withdraw_admin_fees():\\n    receiver: address = Factory(self.factory).get_fee_receiver(self)\\n\\n    amount: uint256 = self.admin_balances[0]\\n    if amount != 0:\\n        raw_call(receiver, b"", value=amount)\\n\\n    amount = self.admin_balances[1]\\n    if amount != 0:\\n        assert ERC20(self.coins[1]).transfer(receiver, amount, default_return_value=True)\\n\\n    self.admin_balances = empty(uint256[N_COINS])\\n```\\n\\nif admin of the curve can set a receiver to a malicious smart contract and reenter withdraw_admin_fees a 1000 times to drain the pool even the admin_balances is small\\nthe line of code\\n```\\nraw_call(receiver, b"", value=amount)\\n```\\n\\ntrigger the reentrancy\\nThis is a problem because as stated by the tokemak team:\\nIn case of external protocol integrations, are the risks of external contracts pausing or executing an emergency withdrawal acceptable? If not, Watsons will submit issues related to these situations that can harm your protocol's functionality.\\nPausing or emergency withdrawals are not acceptable for Tokemak.\\nAs you can see above, pausing or emergency withdrawals are not acceptable, and this is possible for cuve pools so this is a valid issue according to the protocol and according to the read me

Impact:
curve admins can drain pool via reentrancy

Recommended mitigation:
N/A
Vulnerability: At claimDefaulted, the lender may not receive the token because the Unclaimed token is not processed
Severity: high

Description:
```\\nfunction claimDefaulted(uint256 loanID_) external returns (uint256, uint256, uint256) {\\n  Loan memory loan = loans[loanID_];\\n  delete loans[loanID_];\\n```\\n\\nLoan data is deletead in `claimDefaulted` function. `loan.unclaimed` is not checked before data deletead. So, if `claimDefaulted` is called while there are unclaimed tokens, the lender will not be able to get the unclaimed tokens.

Impact:
Lender cannot get unclaimed token.

Recommended mitigation:
Process unclaimed tokens before deleting loan data.\\n```\\nfunction claimDefaulted(uint256 loanID_) external returns (uint256, uint256, uint256) {\\n// Add the line below\\n   claimRepaid(loanID_)\\n    Loan memory loan = loans[loanID_];\\n    delete loans[loanID_];\\n```\\n
Vulnerability: `emergency_shutdown` role is not enough for emergency shutdown.
Severity: medium

Description:
Let's examine the function emergencyShutdown():\\n```\\nfunction emergencyShutdown() external onlyRole("emergency_shutdown") {\\n    active = false;\\n\\n    // If necessary, defund sDAI.\\n    uint256 sdaiBalance = sdai.balanceOf(address(this));\\n    if (sdaiBalance != 0) defund(sdai, sdaiBalance);\\n\\n    // If necessary, defund DAI.\\n    uint256 daiBalance = dai.balanceOf(address(this));\\n    if (daiBalance != 0) defund(dai, daiBalance);\\n\\n    emit Deactivated();\\n}\\n```\\n\\nThis has the modifier `onlyRole("emergency_shutdown")`. However, this also calls function `defund()`, which has the modifier `onlyRole("cooler_overseer")`\\n```\\nfunction defund(ERC20 token_, uint256 amount_) public onlyRole("cooler_overseer") {\\n```\\n\\nTherefore, the role `emergency_shutdown` will not have the ability to shutdown the protocol, unless it also has the overseer role.\\nTo get a coded PoC, make the following modifications to the test case:\\n```\\n//rolesAdmin.grantRole("cooler_overseer", overseer);\\nrolesAdmin.grantRole("emergency_shutdown", overseer);\\n```\\n\\nRun the following test command (to just run a single test test_emergencyShutdown()):\\n```\\nforge test --match-test test_emergencyShutdown\\n```\\n\\nThe test will fail with the `ROLES_RequireRole()` error.

Impact:
`emergency_shutdown` role cannot emergency shutdown the protocol

Recommended mitigation:
There are two ways to mitigate this issue:\\nSeparate the logic for emergency shutdown and defunding. i.e. do not defund when emergency shutdown, but rather defund separately after shutdown.\\nMove the defunding logic to a separate internal function, so that emergency shutdown function can directly call defunding without going through a modifier.
Vulnerability: Stable BPT valuation is incorrect and can be exploited to cause protocol insolvency
Severity: high

Description:
StableBPTOracle.sol#L48-L53\\n```\\n    uint256 minPrice = base.getPrice(tokens[0]);\\n    for(uint256 i = 1; i != length; ++i) {\\n        uint256 price = base.getPrice(tokens[i]);\\n        minPrice = (price < minPrice) ? price : minPrice;\\n    }\\n    return minPrice.mulWadDown(pool.getRate());\\n```\\n\\nThe above block is used to calculate the price. Finding the min price of all assets in the pool then multiplying by the current rate of the pool. This is nearly identical to how stable curve LP is priced. Balancer pools are a bit different and this methodology is incorrect for them. Lets look at a current mainnet pool to see the problem. Take the wstETH/aETHc pool. Currently getRate() = 1.006. The lowest price is aETHc at 2,073.23. This values the LP at 2,085.66. The issue is that the LPs actual value is 1,870.67 (nearly 12% overvalued) which can be checked here.\\nOvervaluing the LP as such can cause protocol insolvency as the borrower can overborrow against the LP, leaving the protocol with bad debt.

Impact:
Protocol insolvency due to overborrowing

Recommended mitigation:
Stable BPT oracles need to use a new pricing methodology
Vulnerability: CurveTricryptoOracle#getPrice contains math error that causes LP to be priced completely wrong
Severity: high

Description:
CurveTricryptoOracle.sol#L57-L62\\n```\\n            (lpPrice(\\n                virtualPrice,\\n                base.getPrice(tokens[1]),\\n                ethPrice,\\n                base.getPrice(tokens[0])\\n            ) * 1e18) / ethPrice;\\n```\\n\\nAfter the LP price has been calculated in USD it is mistakenly divided by the price of ETH causing the contract to return the LP price in terms of ETH rather than USD. This leads to LP that is massively undervalued causing positions which are actually heavily over collateralized to be liquidated.

Impact:
Healthy positions are liquidated due to incorrect LP pricing

Recommended mitigation:
Don't divide the price by the price of ETH
Vulnerability: CVX/AURA distribution calculation is incorrect and will lead to loss of rewards at the end of each cliff
Severity: high

Description:
WAuraPools.sol#L233-L248\\n```\\n    if (cliff < totalCliffs) {\\n        /// e.g. (new) reduction = (500 - 100) * 2.5 + 700 = 1700;\\n        /// e.g. (new) reduction = (500 - 250) * 2.5 + 700 = 1325;\\n        /// e.g. (new) reduction = (500 - 400) * 2.5 + 700 = 950;\\n        uint256 reduction = ((totalCliffs - cliff) * 5) / 2 + 700;\\n        /// e.g. (new) amount = 1e19 * 1700 / 500 =  34e18;\\n        /// e.g. (new) amount = 1e19 * 1325 / 500 =  26.5e18;\\n        /// e.g. (new) amount = 1e19 * 950 / 500  =  19e17;\\n        mintAmount = (mintRequestAmount * reduction) / totalCliffs;\\n\\n        /// e.g. amtTillMax = 5e25 - 1e25 = 4e25\\n        uint256 amtTillMax = emissionMaxSupply - emissionsMinted;\\n        if (mintAmount > amtTillMax) {\\n            mintAmount = amtTillMax;\\n        }\\n    }\\n```\\n\\nThe above code is used to calculate the amount of AURA owed to the user. This calculation is perfectly accurate if the AURA hasn't been minted yet. The problem is that each time a user withdraws, AURA is claimed for ALL vault participants. This means that the rewards will be realized for a majority of users before they themselves withdraw. Since the emissions decrease with each cliff, there will be loss of funds at the end of each cliff.\\nExample: Assume for simplicity there are only 2 cliffs. User A deposits LP to WAuraPools. After some time User B deposits as well. Before the end of the first cliff User A withdraw. This claims all tokens owed to both users A and B which is now sitting in the contract. Assume both users are owed 10 tokens. Now User B waits for the second cliff to end before withdrawing. When calculating his rewards it will give him no rewards since all cliffs have ended. The issue is that the 10 tokens they are owed is already sitting in the contract waiting to be claimed.

Impact:
All users will lose rewards at the end of each cliff due to miscalculation

Recommended mitigation:
I would recommend a hybrid approach. When rewards are claimed upon withdrawal, the reward per token should be cached to prevent loss of tokens that have already been received by the contract. Only unminted AURA should be handled this way.
Vulnerability: Protocol fee from Market.sol is locked
Severity: high

Description:
Here is `MarketFactory#fund` function:\\n```\\n  function fund(IMarket market) external {\\n        if (!instances(IInstance(address(market)))) revert FactoryNotInstanceError();\\n      market.claimFee();\\n    }\\n```\\n\\nThis is `Market#claimFee` function:\\n```\\n    function claimFee() external {\\n        Global memory newGlobal = _global.read();\\n\\n        if (_claimFee(address(factory()), newGlobal.protocolFee)) newGlobal.protocolFee = UFixed6Lib.ZERO;\\n        // rest of code\\n    }\\n```\\n\\nThis is the internal `_claimFee` function:\\n```\\n    function _claimFee(address receiver, UFixed6 fee) private returns (bool) {\\n        if (msg.sender != receiver) return false;\\n\\n        token.push(receiver, UFixed18Lib.from(fee));\\n        emit FeeClaimed(receiver, fee);\\n        return true;\\n    }\\n```\\n\\nAs we can see, when `MarketFactory#fund` is called, Market#claimFee gets called which will send the protocolFee to msg.sender(MarketFacttory). When you check through the MarketFactory contract, there is no place where another address(such as protocol multisig, treasury or an EOA) is approved to spend MarketFactory's funds, and also, there is no function in the contract that can be used to transfer MarketFactory's funds. This causes locking of the protocol fees.

Impact:
Protocol fees cannot be withdrawn

Recommended mitigation:
Consider adding a `withdraw` function that protocol can use to get the protocolFee out of the contract. You can have the `withdraw` function transfer the MarketFactory balance to the treasury or something.
Vulnerability: PythOracle:if price.expo is less than 0, wrong prices will be recorded
Severity: high

Description:
Here is PythOracle#_recordPrice function:\\n```\\n    function _recordPrice(uint256 oracleVersion, PythStructs.Price memory price) private {\\n        _prices[oracleVersion] = Fixed6Lib.from(price.price).mul(\\n            Fixed6Lib.from(SafeCast.toInt256(10 ** SafeCast.toUint256(price.expo > 0 ? price.expo : -price.expo)))\\n        );\\n        _publishTimes[oracleVersion] = price.publishTime;\\n    }\\n```\\n\\nIf price is 5e-5 for example, it will be recorded as 5e5 If price is 5e-6, it will be recorded as 5e6.\\nAs we can see, there is a massive deviation in recorded price from actual price whenever price's exponent is negative

Impact:
Wrong prices will be recorded. For example, If priceA is 5e-5, and priceB is 5e-6. But due to the wrong conversion,\\nThere is a massive change in price(5e5 against 5e-5)\\nwe know that priceA is ten times larger than priceB, but priceA will be recorded as ten times smaller than priceB. Unfortunately, current payoff functions may not be able to take care of these discrepancies

Recommended mitigation:
In PythOracle.sol, `_prices` mapping should not be `mapping(uint256 => Fixed6) private _prices;` Instead, it should be `mapping(uint256 => Price) private _prices;`, where Price is a struct that stores the price and expo:\\n```\\nstruct Price{\\n    Fixed6 price,\\n    int256 expo\\n}\\n```\\n\\nThis way, the price exponents will be preserved, and can be used to scale the prices correctly wherever it is used.
Vulnerability: Market: DoS when stuffed with pending protected positions
Severity: medium

Description:
In `_invariant`, there is a limit on the number of pending position updates. But for `protected` position updates, `_invariant` returns early and does not trigger this check.\\n```\\n    function _invariant(\\n        Context memory context,\\n        address account,\\n        Order memory newOrder,\\n        Fixed6 collateral,\\n        bool protected\\n    ) private view {\\n        // rest of code.\\n\\n        if (protected) return; // The following invariants do not apply to protected position updates (liquidations)\\n        // rest of code.\\n            if (\\n            context.global.currentId > context.global.latestId + context.marketParameter.maxPendingGlobal ||\\n            context.local.currentId > context.local.latestId + context.marketParameter.maxPendingLocal\\n        ) revert MarketExceedsPendingIdLimitError();\\n        // rest of code.\\n    }\\n```\\n\\nAfter the `_invariant` check, the postion updates will be added into pending position queues.\\n```\\n        _invariant(context, account, newOrder, collateral, protected);\\n\\n        // store\\n        _pendingPosition[context.global.currentId].store(context.currentPosition.global);\\n        _pendingPositions[account][context.local.currentId].store(context.currentPosition.local);\\n```\\n\\nWhen the protocol enters next oracle version, the global pending queue `_pendingPosition` will be settled in a loop.\\n```\\n    function _settle(Context memory context, address account) private {\\n        // rest of code.\\n        // settle\\n        while (\\n            context.global.currentId != context.global.latestId &&\\n            (nextPosition = _pendingPosition[context.global.latestId + 1].read()).ready(context.latestVersion)\\n        ) _processPositionGlobal(context, context.global.latestId + 1, nextPosition);\\n```\\n\\nThe OOG revert happens if there are too many pending position updates.\\nThis revert will happend on every `update` calls because they all need to settle this `_pendingPosition` before `update`.\\n```\\n    function update(\\n        address account,\\n        UFixed6 newMaker,\\n        UFixed6 newLong,\\n        UFixed6 newShort,\\n        Fixed6 collateral,\\n        bool protect\\n    ) external nonReentrant whenNotPaused {\\n        Context memory context = _loadContext(account);\\n        _settle(context, account);\\n        _update(context, account, newMaker, newLong, newShort, collateral, protect);\\n        _saveContext(context, account);\\n    }\\n```\\n

Impact:
The protocol will be fully unfunctional and funds will be locked. There will be no recover to this DoS.\\nA malicious user can tigger this intentionally at very low cost. Alternatively, this can occur during a volatile market period when there are massive liquidations.

Recommended mitigation:
Either or both,\\nLimit the number of pending protected position updates can be queued in `_invariant`.\\nLimit the number of global pending protected postions can be settled in `_settle`.
Vulnerability: update() wrong privilege control
Severity: medium

Description:
in `OracleFactory.update()` will call `oracle.update()`\\n```\\ncontract OracleFactory is IOracleFactory, Factory {\\n// rest of code\\n    function update(bytes32 id, IOracleProviderFactory factory) external onlyOwner {\\n        if (!factories[factory]) revert OracleFactoryNotRegisteredError();\\n        if (oracles[id] == IOracleProvider(address(0))) revert OracleFactoryNotCreatedError();\\n\\n        IOracleProvider oracleProvider = factory.oracles(id);\\n        if (oracleProvider == IOracleProvider(address(0))) revert OracleFactoryInvalidIdError();\\n\\n        IOracle oracle = IOracle(address(oracles[id]));\\n      oracle.update(oracleProvider);\\n    }\\n```\\n\\nBut `oracle.update()` permission is needed for `OracleFactory.owner()` and not `OracleFactory` itself.\\n```\\n  function update(IOracleProvider newProvider) external onlyOwner {\\n        _updateCurrent(newProvider);\\n        _updateLatest(newProvider.latest());\\n    }\\n\\n    modifier onlyOwner {\\n      if (msg.sender != factory().owner()) revert InstanceNotOwnerError(msg.sender);\\n        _;\\n    }\\n```\\n\\nThis results in `OracleFactory` not being able to do `update()`. Suggest changing the limit of `oracle.update()` to `factory()`.

Impact:
`OracleFactory.update()` unable to add `IOracleProvider`

Recommended mitigation:
```\\ncontract Oracle is IOracle, Instance {\\n// rest of code\\n\\n-   function update(IOracleProvider newProvider) external onlyOwner {\\n+   function update(IOracleProvider newProvider) external {\\n+       require(msg.sender == factory(),"invalid sender");\\n        _updateCurrent(newProvider);\\n        _updateLatest(newProvider.latest());\\n    }\\n```\\n
Vulnerability: `_accumulateFunding()` maker will get the wrong amount of funding fee.
Severity: medium

Description:
The formula that calculates the amount of funding in `Version#_accumulateFunding()` on the maker side is incorrect. This leads to an incorrect distribution of funding between the minor and the maker's side.\\n```\\n// Redirect net portion of minor's side to maker\\nif (fromPosition.long.gt(fromPosition.short)) {\\n    fundingValues.fundingMaker = fundingValues.fundingShort.mul(Fixed6Lib.from(fromPosition.skew().abs()));\\n    fundingValues.fundingShort = fundingValues.fundingShort.sub(fundingValues.fundingMaker);\\n}\\nif (fromPosition.short.gt(fromPosition.long)) {\\n    fundingValues.fundingMaker = fundingValues.fundingLong.mul(Fixed6Lib.from(fromPosition.skew().abs()));\\n    fundingValues.fundingLong = fundingValues.fundingLong.sub(fundingValues.fundingMaker);\\n}\\n```\\n\\nPoC\\nGiven:\\nlong/major: 1000\\nshort/minor: 1\\nmaker: 1\\nThen:\\nskew(): 999/1000\\nfundingMaker: 0.999 of the funding\\nfundingShort: 0.001 of the funding\\nWhile the maker only matches for `1` of the major part and contributes to half of the total short side, it takes the entire funding.

Impact:

Recommended mitigation:
The correct formula to calculate the amount of funding belonging to the maker side should be:\\n```\\nfundingMakerRatio = min(maker, major - minor) / min(major, minor + maker)\\nfundingMaker = fundingMakerRatio * fundingMinor\\n```\\n
Vulnerability: CurveTricryptoOracle incorrectly assumes that WETH is always the last token in the pool which leads to bad LP pricing
Severity: high

Description:
CurveTricryptoOracle.sol#L53-L63\\n```\\n    if (tokens.length == 3) {\\n        /// tokens[2] is WETH\\n        uint256 ethPrice = base.getPrice(tokens[2]);\\n        return\\n            (lpPrice(\\n                virtualPrice,\\n                base.getPrice(tokens[1]),\\n                ethPrice,\\n                base.getPrice(tokens[0])\\n            ) * 1e18) / ethPrice;\\n    }\\n```\\n\\nWhen calculating LP prices, CurveTricryptoOracle#getPrice always assumes that WETH is the second token in the pool. This isn't the case which will cause the LP to be massively overvalued.\\nThere are 6 tricrypto pools currently deployed on mainnet. Half of these pools have an asset other than WETH as token[2]:\\n```\\n    0x4ebdf703948ddcea3b11f675b4d1fba9d2414a14 - CRV\\n    0x5426178799ee0a0181a89b4f57efddfab49941ec - INV\\n    0x2889302a794da87fbf1d6db415c1492194663d13 - wstETH\\n```\\n

Impact:
LP will be massively overvalued leading to overborrowing and protocol insolvency

Recommended mitigation:
There is no need to assume that WETH is the last token. Simply pull the price for each asset and input it into lpPrice.
Vulnerability: Mainnet oracles are incompatible with wstETH causing many popular yields strategies to be broken
Severity: medium

Description:
ChainlinkAdapterOracle.sol#L111-L125\\n```\\n    uint256 decimals = registry.decimals(token, USD);\\n    (\\n        uint80 roundID,\\n        int256 answer,\\n        ,\\n        uint256 updatedAt,\\n        uint80 answeredInRound\\n    ) = registry.latestRoundData(token, USD);\\n    if (updatedAt < block.timestamp - maxDelayTime)\\n        revert Errors.PRICE_OUTDATED(token_);\\n    if (answer <= 0) revert Errors.PRICE_NEGATIVE(token_);\\n    if (answeredInRound < roundID) revert Errors.PRICE_OUTDATED(token_);\\n\\n    return\\n        (answer.toUint256() * Constants.PRICE_PRECISION) / 10 ** decimals;\\n```\\n\\nChainlinkAdapterOracle only supports single asset price data. This makes it completely incompatible with wstETH because chainlink doesn't have a wstETH oracle on mainnet. Additionally Band protocol doesn't offer a wstETH oracle either. This only leaves Uniswap oracles which are highly dangerous given their low liquidity.

Impact:
Mainnet oracles are incompatible with wstETH causing many popular yields strategies to be broken

Recommended mitigation:
Create a special bypass specifically for wstETH utilizing the stETH oracle and it's current exchange rate.