msc-smart-contract-auditing/vulnerable-functions-base huggingface dataset:
Vulnerability: Comparison operator used instead of assignment operator
Severity: medium

Description:
```\\nself.\\_validators[\\_pk].state == 2;\\n```\\n\\n```\\nself.\\_validators[\\_pk].state == 3;\\n```\\n

Impact:

Recommended mitigation:
Replace `==` with `=`.
Vulnerability: initiator modifier will not work in the context of one transaction
Severity: low

Description:
Each planet, comet or operator must be initialized after the onboarding proposal is approved. In order to make sure that these entities are not initialized more than once `initiateOperator`, `initiateComet` and `initiatePlanet` have the `initiator` modifier.\\n```\\nfunction initiatePlanet(\\n DataStoreUtils.DataStore storage DATASTORE,\\n uint256[3] memory uintSpecs,\\n address[5] memory addressSpecs,\\n string[2] calldata interfaceSpecs\\n)\\n external\\n initiator(DATASTORE, 5, uintSpecs[0], addressSpecs[1])\\n returns (\\n address miniGovernance,\\n address gInterface,\\n address withdrawalPool\\n )\\n```\\n\\n```\\nfunction initiateComet(\\n DataStoreUtils.DataStore storage DATASTORE,\\n uint256 id,\\n uint256 fee,\\n address maintainer\\n) external initiator(DATASTORE, 6, id, maintainer) {\\n```\\n\\n```\\nfunction initiateOperator(\\n DataStoreUtils.DataStore storage DATASTORE,\\n uint256 id,\\n uint256 fee,\\n address maintainer\\n) external initiator(DATASTORE, 4, id, maintainer) {\\n```\\n\\nInside that modifier, we check that the `initiated` flag is 0 and if so we proceed to initialization. We later update it to the current timestamp.\\n```\\nmodifier initiator(\\n DataStoreUtils.DataStore storage DATASTORE,\\n uint256 \\_TYPE,\\n uint256 \\_id,\\n address \\_maintainer\\n) {\\n require(\\n msg.sender == DATASTORE.readAddressForId(\\_id, "CONTROLLER"),\\n "MaintainerUtils: sender NOT CONTROLLER"\\n );\\n require(\\n DATASTORE.readUintForId(\\_id, "TYPE") == \\_TYPE,\\n "MaintainerUtils: id NOT correct TYPE"\\n );\\n require(\\n DATASTORE.readUintForId(\\_id, "initiated") == 0,\\n "MaintainerUtils: already initiated"\\n );\\n\\n DATASTORE.writeAddressForId(\\_id, "maintainer", \\_maintainer);\\n\\n \\_;\\n\\n DATASTORE.writeUintForId(\\_id, "initiated", block.timestamp);\\n\\n emit IdInitiated(\\_id, \\_TYPE);\\n}\\n```\\n\\nUnfortunately, this does not follow the checks-effects-interractions pattern. If one for example would call `initiatePlanet` again from the body of the modifier, this check will still pass making it susceptible to a reentrancy attack. While we could not find a way to exploit this in the current engagement, given that system is designed to be upgradable this could become a risk in the future. For example, if during the initialization of the planet the maintainer will be allowed to pass a custom interface that could potentially allow reentering.

Impact:

Recommended mitigation:
Bring the line that updated the `initiated` flag to the current timestamp before the `_;`.\\n```\\nDATASTORE.writeUintForId(\\_id, "initiated", block.timestamp);\\n```\\n
Vulnerability: Incorrect accounting for the burned gEth
Severity: low

Description:
Geode Portal records the amount of minted and burned gETH on any given day during the active period of the oracle. One case where some gETH is burned is when the users redeem gETH for ETH. In the burn function we burn the spentGeth - `gEthDonation` but in the accounting code we do not account for `gEthDonation` so the code records more assets burned than was really burned.\\n```\\nDATASTORE.subUintForId(poolId, "surplus", spentSurplus);\\nself.gETH.burn(address(this), poolId, spentGeth - gEthDonation);\\n\\nif (self.TELESCOPE.\\_isOracleActive()) {\\n bytes32 dailyBufferKey = DataStoreUtils.getKey(\\n block.timestamp - (block.timestamp % OracleUtils.ORACLE\\_PERIOD),\\n "burnBuffer"\\n );\\n DATASTORE.addUintForId(poolId, dailyBufferKey, spentGeth);\\n}\\n```\\n

Impact:

Recommended mitigation:
Record the `spentGeth` - gEthDonation instead of just `spentGeth` in the burn buffer.\\n```\\nDATASTORE.addUintForId(poolId, dailyBufferKey, spentGeth);\\n```\\n
Vulnerability: Boost calculation on fetchUnstake should not be using the cumBalance when it is larger than debt.
Severity: low

Description:
The Geode team implemented the 2-step withdrawal mechanism for the staked ETH. First, node operators signal their intent to withdraw the stake, and then the oracle will trigger all of the accounting of rewards, balances, and buybacks if necessary. Buybacks are what we are interested in at this time. Buybacks are performed by checking if the derivative asset is off peg in the Dynamic Withdrawal Pool contract. Once the debt is larger than some ignorable threshold an arbitrage buyback will be executed. A portion of the arbitrage profit will go to the node operator. The issue here is that when simulating the arbitrage swap in the `calculateSwap` call we use the cumulative un-stake balance rather than ETH debt preset in the DWP. In the case where the withdrawal cumulative balance is higher than the debt node operator will receive a higher reward than intended.\\n```\\nuint256 arb = withdrawalPoolById(DATASTORE, poolId)\\n .calculateSwap(0, 1, cumBal);\\n```\\n

Impact:

Recommended mitigation:
Use the `debt` amount of ETH in the boost reward calculation when the cumulative balance is larger than the `debt`.
Vulnerability: DataStore struct not having the _gap for upgrades.
Severity: low

Description:
```\\nDataStoreUtils.DataStore private DATASTORE;\\nGeodeUtils.Universe private GEODE;\\nStakeUtils.StakePool private STAKEPOOL;\\n```\\n\\nIt is worth mentioning that Geode contracts are meant to support the upgradability pattern. Given that information, one should be careful not to overwrite the storage variables by reordering the old ones or adding the new once not at the end of the list of variables when upgrading. The issue comes with the fact that structs seem to give a false sense of security making it feel like they are an isolated set of storage variables that will not override anything else. In reality, struts are just tuples that are expanded in storage sequentially just like all the other storage variables. For that reason, if you have two struct storage variables listed back to back like in the code above, you either need to make sure not to change the order or the number of variables in the structs other than the last one between upgrades or you need to add a `uint256[N] _gap` array of fixed size to reserve some storage slots for the future at the end of each struct. The Geode Finance team is missing the gap in the `DataStrore` struct making it non-upgradable.\\n```\\nstruct DataStore {\\n mapping(uint256 => uint256[]) allIdsByType;\\n mapping(bytes32 => uint256) uintData;\\n mapping(bytes32 => bytes) bytesData;\\n mapping(bytes32 => address) addressData;\\n}\\n```\\n

Impact:

Recommended mitigation:
We suggest that gap is used in DataStore as well. Since it was used for all the other structs we consider it just a typo.
Vulnerability: Handle division by 0
Severity: medium

Description:
There are a few places in the code where division by zero may occur but isn't handled.\\nIf the vault settles at exactly 0 value with 0 remaining strategy token value, there may be an unhandled division by zero trying to divide claims on the settled assets:\\n```\\nint256 settledVaultValue = settlementRate.convertToUnderlying(residualAssetCashBalance)\\n    .add(totalStrategyTokenValueAtSettlement);\\n\\n// If the vault is insolvent (meaning residualAssetCashBalance < 0), it is necessarily\\n// true that totalStrategyTokens == 0 (meaning all tokens were sold in an attempt to\\n// repay the debt). That means settledVaultValue == residualAssetCashBalance, strategyTokenClaim == 0\\n// and assetCashClaim == totalAccountValue. Accounts that are still solvent will be paid from the\\n// reserve, accounts that are insolvent will have a totalAccountValue == 0.\\nstrategyTokenClaim = totalAccountValue.mul(vaultState.totalStrategyTokens.toInt())\\n    .div(settledVaultValue).toUint();\\n\\nassetCashClaim = totalAccountValue.mul(residualAssetCashBalance)\\n    .div(settledVaultValue);\\n```\\n\\nIf a vault account is entirely insolvent and its `vaultShareValue` is zero, there will be an unhandled division by zero during liquidation:\\n```\\nuint256 vaultSharesToLiquidator;\\n{\\n    vaultSharesToLiquidator = vaultAccount.tempCashBalance.toUint()\\n        .mul(vaultConfig.liquidationRate.toUint())\\n        .mul(vaultAccount.vaultShares)\\n        .div(vaultShareValue.toUint())\\n        .div(uint256(Constants.RATE\\_PRECISION));\\n}\\n```\\n\\nIf a vault account's secondary debt is being repaid when there is none, there will be an unhandled division by zero:\\n```\\nVaultSecondaryBorrowStorage storage balance =\\n    LibStorage.getVaultSecondaryBorrow()[vaultConfig.vault][maturity][currencyId];\\nuint256 totalfCashBorrowed = balance.totalfCashBorrowed;\\nuint256 totalAccountDebtShares = balance.totalAccountDebtShares;\\n\\nfCashToLend = debtSharesToRepay.mul(totalfCashBorrowed).div(totalAccountDebtShares).toInt();\\n```\\n\\nWhile these cases may be unlikely today, this code could be reutilized in other circumstances later that could cause reverts and even disrupt operations more frequently.

Impact:

Recommended mitigation:
Handle the cases where the denominator could be zero appropriately.
Vulnerability: Increasing a leveraged position in a vault with secondary borrow currency will revert
Severity: low

Description:
From the client's specifications for the strategy vaults, we know that accounts should be able to increase their leveraged positions before maturity. This property will not hold for the vaults that require borrowing a secondary currency to enter a position. When an account opens its position in such vault for the first time, the `VaultAccountSecondaryDebtShareStorage.maturity` is set to the maturity an account has entered. When the account is trying to increase the debt position, an accounts current maturity will be checked, and since it is not set to 0, as in the case where an account enters the vault for the first time, nor it is smaller than the new maturity passed by an account as in case of a rollover, the code will revert.\\n```\\nif (accountMaturity != 0) {\\n    // Cannot roll to a shorter term maturity\\n    require(accountMaturity < maturity);\\n```\\n

Impact:

Recommended mitigation:
In order to fix this issue, we recommend that `<` is replaced with `<=` so that account can enter the vault maturity the account is already in as well as the future once.
Vulnerability: Vaults are unable to borrow single secondary currency
Severity: low

Description:
As was previously mentioned some strategies `require` borrowing one or two secondary currencies. All secondary currencies have to be whitelisted in the `VaultConfig.secondaryBorrowCurrencies`. Borrow operation on secondary currencies is performed in the `borrowSecondaryCurrencyToVault(...)` function. Due to a `require` statement in that function, vaults will only be able to borrow secondary currencies if both of the currencies are whitelisted in `VaultConfig.secondaryBorrowCurrencies`. Considering that many strategies will have just one secondary currency, this will prevent those strategies from borrowing any secondary assets.\\n```\\nrequire(currencies[0] != 0 && currencies[1] != 0);\\n```\\n

Impact:

Recommended mitigation:
We suggest that the `&&` operator is replaced by the `||` operator. Ideally, an additional check will be performed that will ensure that values in argument arrays `fCashToBorrow`, `maxBorrowRate`, and `minRollLendRate` are passed under the same index as the whitelisted currencies in `VaultConfig.secondaryBorrowCurrencies`.\\n```\\nfunction borrowSecondaryCurrencyToVault(\\n    address account,\\n    uint256 maturity,\\n    uint256[2] calldata fCashToBorrow,\\n    uint32[2] calldata maxBorrowRate,\\n    uint32[2] calldata minRollLendRate\\n) external override returns (uint256[2] memory underlyingTokensTransferred) {\\n```\\n
Vulnerability: An account roll may be impossible if the vault is already at the maximum borrow capacity.
Severity: low

Description:
One of the actions allowed in Notional Strategy Vaults is to roll an account's maturity to a later one by borrowing from a later maturity and repaying that into the debt of the earlier maturity.\\nHowever, this could cause an issue if the vault is at maximum capacity at the time of the roll. When an account performs this type of roll, the new borrow would have to be more than the existing debt simply because it has to at least cover the existing debt and pay for the borrow fees that get added on every new borrow. Since the whole vault was already at max borrow capacity before with the old, smaller borrow, this process would revert at the end after the new borrow as well once the process gets to `VaultAccount.updateAccountfCash` and VaultConfiguration.updateUsedBorrowCapacity:\\n```\\nfunction updateUsedBorrowCapacity(\\n    address vault,\\n    uint16 currencyId,\\n    int256 netfCash\\n) internal returns (int256 totalUsedBorrowCapacity) {\\n    VaultBorrowCapacityStorage storage cap = LibStorage.getVaultBorrowCapacity()[vault][currencyId];\\n\\n    // Update the total used borrow capacity, when borrowing this number will increase (netfCash < 0),\\n    // when lending this number will decrease (netfCash > 0).\\n    totalUsedBorrowCapacity = int256(uint256(cap.totalUsedBorrowCapacity)).sub(netfCash);\\n    if (netfCash < 0) {\\n        // Always allow lending to reduce the total used borrow capacity to satisfy the case when the max borrow\\n        // capacity has been reduced by governance below the totalUsedBorrowCapacity. When borrowing, it cannot\\n        // go past the limit.\\n        require(totalUsedBorrowCapacity <= int256(uint256(cap.maxBorrowCapacity)), "Max Capacity");\\n```\\n\\nThe result is that users won't able to roll while the vault is at max capacity. However, users may exit some part of their position to reduce their borrow, thereby reducing the overall vault borrow capacity, and then could execute the roll. A bigger problem would occur if the vault configuration got updated to massively reduce the borrow capacity, which would force users to exit their position more significantly with likely a much smaller chance at being able to roll.

Impact:

Recommended mitigation:
Document this case so that users can realise that rolling may not always be an option. Perhaps consider adding ways where users can pay a small deposit, like on `enterVault`, to offset the additional difference in borrows and pay for fees so they can remain with essentially the same size position within Notional.
Vulnerability: Rollover might introduce economically impractical deposits of dust into a strategy
Severity: low

Description:
During the rollover of the strategy position into a longer maturity, several things happen:\\nFunds are borrowed from the longer maturity to pay off the debt and fees of the current maturity.\\nStrategy tokens that are associated with the current maturity are moved to the new maturity.\\nAny additional funds provided by the account are deposited into the strategy into a new longer maturity.\\nIn reality, due to the AMM nature of the protocol, the funds borrowed from the new maturity could exceed the debt the account has in the current maturity, resulting in a non-zero `vaultAccount.tempCashBalance`. In that case, those funds will be deposited into the strategy. That would happen even if there are no external funds supplied by the account for the deposit.\\nIt is possible that the dust in the temporary account balance will not cover the gas cost of triggering a full deposit call of the strategy.\\n```\\nuint256 strategyTokensMinted = vaultConfig.deposit(\\n    vaultAccount.account, vaultAccount.tempCashBalance, vaultState.maturity, additionalUnderlyingExternal, vaultData\\n);\\n```\\n

Impact:

Recommended mitigation:
We suggest that additional checks are introduced that would check that on rollover `vaultAccount.tempCashBalance + additionalUnderlyingExternal > 0` or larger than a certain threshold like `minAccountBorrowSize` for example.
Vulnerability: ConvexPositionHandler._claimRewards incorrectly calculates amount of LP tokens to unstake
Severity: high

Description:
`ConvexPositionHandler._claimRewards` is an internal function that harvests Convex reward tokens and takes the generated yield in ETH out of the Curve pool by calculating the difference in LP token price. To do so, it receives the current share price of the curve LP tokens and compares it to the last one stored in the contract during the last rewards claim. The difference in share price is then multiplied by the LP token balance to get the ETH yield via the `yieldEarned` variable:\\n```\\nuint256 currentSharePrice = ethStEthPool.get\\_virtual\\_price();\\nif (currentSharePrice > prevSharePrice) {\\n    // claim any gain on lp token yields\\n    uint256 contractLpTokenBalance = lpToken.balanceOf(address(this));\\n    uint256 totalLpBalance = contractLpTokenBalance +\\n        baseRewardPool.balanceOf(address(this));\\n    uint256 yieldEarned = (currentSharePrice - prevSharePrice) \\*\\n        totalLpBalance;\\n```\\n\\nHowever, to receive this ETH yield, LP tokens need to be unstaked from the Convex pool and then converted via the Curve pool. To do this, the contract introduces lpTokenEarned:\\n```\\nuint256 lpTokenEarned = yieldEarned / NORMALIZATION\\_FACTOR; // 18 decimal from virtual price\\n```\\n\\nThis calculation is incorrect. It uses yieldEarned which is denominated in ETH and simply divides it by the normalization factor to get the correct number of decimals, which still returns back an amount denominated in ETH, whereas an amount denominated in LP tokens should be returned instead.\\nThis could lead to significant accounting issues including losses in the “no-loss” parts of the vault's strategy as 1 LP token is almost always guaranteed to be worth more than 1 ETH. So, when the intention is to withdraw `X` ETH worth of an LP token, withdrawing `X` LP tokens will actually withdraw `Y` ETH worth of an LP token, where `Y>X`. As a result, less than expected ETH will remain in the Convex handler part of the vault, and the ETH yield will go to the Lyra options, which are much riskier. In the event Lyra options don't work out and there is more ETH withdrawn than expected, there is a possibility that this would result in a loss for the vault.

Impact:

Recommended mitigation:
The fix is straightforward and that is to calculate `lpTokenEarned` using the `currentSharePrice` already received from the Curve pool. That way, it is the amount of LP tokens that will be sent to be unwrapped and unstaked from the Convex and Curve pools. This will also take care of the normalization factor. `uint256 `lpTokenEarned` = yieldEarned / currentSharePrice;`
Vulnerability: The WETH tokens are not taken into account in the ConvexTradeExecutor.totalFunds function
Severity: high

Description:
The `totalFunds` function of every executor should include all the funds that belong to the contract:\\n```\\nfunction totalFunds() public view override returns (uint256, uint256) {\\n    return ConvexPositionHandler.positionInWantToken();\\n}\\n```\\n\\nThe `ConvexTradeExecutor` uses this function for calculations:\\n```\\nfunction positionInWantToken()\\n    public\\n    view\\n    override\\n    returns (uint256, uint256)\\n{\\n    (\\n        uint256 stakedLpBalanceInETH,\\n        uint256 lpBalanceInETH,\\n        uint256 ethBalance\\n    ) = \\_getTotalBalancesInETH(true);\\n\\n    return (\\n        stakedLpBalanceInETH + lpBalanceInETH + ethBalance,\\n        block.number\\n    );\\n}\\n```\\n\\n```\\nfunction \\_getTotalBalancesInETH(bool useVirtualPrice)\\n    internal\\n    view\\n    returns (\\n        uint256 stakedLpBalance,\\n        uint256 lpTokenBalance,\\n        uint256 ethBalance\\n    )\\n{\\n    uint256 stakedLpBalanceRaw = baseRewardPool.balanceOf(address(this));\\n    uint256 lpTokenBalanceRaw = lpToken.balanceOf(address(this));\\n\\n    uint256 totalLpBalance = stakedLpBalanceRaw + lpTokenBalanceRaw;\\n\\n    // Here, in order to prevent price manipulation attacks via curve pools,\\n    // When getting total position value -> its calculated based on virtual price\\n    // During withdrawal -> calc\\_withdraw\\_one\\_coin() is used to get an actual estimate of ETH received if we were to remove liquidity\\n    // The following checks account for this\\n    uint256 totalLpBalanceInETH = useVirtualPrice\\n        ? \\_lpTokenValueInETHFromVirtualPrice(totalLpBalance)\\n        : \\_lpTokenValueInETH(totalLpBalance);\\n\\n    lpTokenBalance = useVirtualPrice\\n        ? \\_lpTokenValueInETHFromVirtualPrice(lpTokenBalanceRaw)\\n        : \\_lpTokenValueInETH(lpTokenBalanceRaw);\\n\\n    stakedLpBalance = totalLpBalanceInETH - lpTokenBalance;\\n    ethBalance = address(this).balance;\\n}\\n```\\n\\nThis function includes ETH balance, LP balance, and staked balance. But WETH balance is not included here. WETH tokens are initially transferred to the contract, and before the withdrawal, the contract also stores WETH.

Impact:

Recommended mitigation:
Include WETH balance into the `totalFunds`.
Vulnerability: Harvester.harvest swaps have no slippage parameters
Severity: medium

Description:
As part of the vault strategy, all reward tokens for staking in the Convex ETH-stETH pool are claimed and swapped into ETH. The swaps for these tokens are done with no slippage at the moment, i.e. the expected output amount for all of them is given as 0.\\nIn particular, one reward token that is most susceptible to slippage is LDO, and its swap is implemented through the Uniswap router:\\n```\\nfunction \\_swapLidoForWETH(uint256 amountToSwap) internal {\\n    IUniswapSwapRouter.ExactInputSingleParams\\n        memory params = IUniswapSwapRouter.ExactInputSingleParams({\\n            tokenIn: address(ldo),\\n            tokenOut: address(weth),\\n            fee: UNISWAP\\_FEE,\\n            recipient: address(this),\\n            deadline: block.timestamp,\\n            amountIn: amountToSwap,\\n            amountOutMinimum: 0,\\n            sqrtPriceLimitX96: 0\\n        });\\n    uniswapRouter.exactInputSingle(params);\\n}\\n```\\n\\nThe swap is called with `amountOutMinimum: 0`, meaning that there is no slippage protection in this swap. This could result in a significant loss of yield from this reward as MEV bots could “sandwich” this swap by manipulating the price before this transaction and immediately reversing their action after the transaction, profiting at the expense of our swap. Moreover, the Uniswap pools seem to have low liquidity for the LDO token as opposed to Balancer or Sushiswap, further magnifying slippage issues and susceptibility to frontrunning.\\nThe other two tokens - CVX and CRV - are being swapped through their Curve pools, which have higher liquidity and are less susceptible to slippage. Nonetheless, MEV strategies have been getting more advanced and calling these swaps with 0 as expected output may place these transactions in danger of being frontrun and “sandwiched” as well.\\n```\\nif (cvxBalance > 0) {\\n    cvxeth.exchange(1, 0, cvxBalance, 0, false);\\n}\\n// swap CRV to WETH\\nif (crvBalance > 0) {\\n    crveth.exchange(1, 0, crvBalance, 0, false);\\n}\\n```\\n\\nIn these calls `.exchange` , the last `0` is the `min_dy` argument in the Curve pools swap functions that represents the minimum expected amount of tokens received after the swap, which is `0` in our case.

Impact:

Recommended mitigation:
Introduce some slippage parameters into the swaps.