msc-smart-contract-auditing/vulnerable-functions-base huggingface dataset:
Vulnerability: `StrategyPassiveManagerUniswap` gives ERC20 token allowances to `unirouter` but doesn't remove allowances when `unirouter` is updated
Severity: medium

Description:
`StrategyPassiveManagerUniswap` gives ERC20 token allowances to unirouter:\\n```\\nfunction _giveAllowances() private {\\n    IERC20Metadata(lpToken0).forceApprove(unirouter, type(uint256).max);\\n    IERC20Metadata(lpToken1).forceApprove(unirouter, type(uint256).max);\\n}\\n```\\n\\n`unirouter` is inherited from `StratFeeManagerInitializable` which has an external function `setUnirouter` which allows `unirouter` to be changed:\\n```\\n function setUnirouter(address _unirouter) external onlyOwner {\\n    unirouter = _unirouter;\\n    emit SetUnirouter(_unirouter);\\n}\\n```\\n\\nThe allowances can only be removed by calling `StrategyPassiveManagerUniswap::panic` however `unirouter` can be changed any time via the `setUnirouter` function.\\nThis allows the contract to enter a state where `unirouter` is updated via `setUnirouter` but the ERC20 token approvals given to the old `unirouter` are not removed.

Impact:
The old `unirouter` contract will continue to have ERC20 token approvals for `StratFeeManagerInitializable` so it can continue to spend the protocol's tokens when this is not the protocol's intention as the protocol has changed `unirouter`.

Recommended mitigation:
1) Make `StratFeeManagerInitializable::setUnirouter` `virtual` such that it can be overridden by child contracts. 2) `StrategyPassiveManagerUniswap` should override `setUnirouter` to remove all allowances before calling the parent function to update `unirouter`.
Vulnerability: Owner of `StrategyPassiveManagerUniswap` can rug-pull users' deposited tokens by manipulating `onlyCalmPeriods` parameters
Severity: low

Description:
While `StrategyPassiveManagerUniswap` does have some permissioned roles, one of the attack paths we were asked to check was that the permissioned roles could not rug-pull the users' deposited tokens. There is a way that the owner of the `StrategyPassiveManagerUniswap` contract could accomplish this by modifying key parameters to reduce the effectiveness of the `_onlyCalmPeriods` check. This appears to be how a similar protocol Gamma was exploited.\\nProof of Concept:\\nOwner calls `StrategyPassiveManagerUniswap::setDeviation` to increase the maximum allowed deviations to large numbers or alternatively `setTwapInterval` to decrease the twap interval rendering it ineffective\\nOwner takes a flash loan and uses it to manipulate `pool.slot0` to a high value\\nOwner calls `BeefyVaultConcLiq::deposit` to perform a deposit; the shares are calculated thus:\\n```\\n// @audit `price` is derived from `pool.slot0`\\nshares = _amount1 + (_amount0 * price / PRECISION);\\n```\\n\\nAs `price` is derived from `pool.slot0` which has been inflated, the owner will receive many more shares than they normally would\\nOwner unwinds the flash loan returning `pool.slot0` back to its normal value\\nOwner calls `BeefyVaultConcLiq::withdraw` to receive many more tokens than they should be able to due to the inflated share count they received from the deposit

Impact:
Owner of `StrategyPassiveManagerUniswap` can rug-pull users' deposited tokens.

Recommended mitigation:
Beefy already intends to have all owner functions behind a timelocked multi-sig and if these transactions are attempted the suspicious parameters would be an obvious signal that a future attack is coming. Because of this the probability of this attack being effectively executed is low though it is still possible.\\nOne way to further mitigate this attack would be to have a minimum required twap interval and maximum required deviation amounts such that the owner couldn't change these parameters to values which would enable this attack.
Vulnerability: `_onlyCalmPeriods` does not consider MIN/MAX ticks, which can DOS deposit, withdraw and harvest in edge cases
Severity: low

Description:
In Uniswap V3 liquidity providers can only provide liquidity between price ranges `[1.0001^{MIN_ TICK};1.0001^{MAX_TICK})`. Therefore these are the min and max prices.\\n```\\n    function _onlyCalmPeriods() private view {\\n        int24 tick = currentTick();\\n        int56 twapTick = twap();\\n\\n        if(\\n            twapTick - maxTickDeviationNegative > tick  ||\\n            twapTick + maxTickDeviationPositive < tick) revert NotCalm();\\n    }\\n```\\n\\nIf `twapTick - maxTickDeviationNegative < MIN_TICK`, this function would revert even if `tick` has been the same for years. This can DOS deposits, withdrawals and harvests when they should be allowed for as long as the state holds.

Impact:

Recommended mitigation:
Consider changing the current implementation to:\\n```\\n// Add the line below\\n   const int56 MIN_TICK = // Remove the line below\\n887272;\\n// Add the line below\\n   const int56 MAX_TICK = 887272;\\n    function _onlyCalmPeriods() private view {\\n        int24 tick = currentTick();\\n        int56 twapTick = twap();\\n\\n// Add the line below\\n       int56 minCalmTick = max(twapTick // Remove the line below\\n maxTickDeviationNegative, MIN_TICK);\\n// Add the line below\\n       int56 maxCalmTick = min(twapTick // Remove the line below\\n maxTickDeviationPositive, MAX_TICK);\\n\\n        if(\\n// Remove the line below\\n           twapTick // Remove the line below\\n maxTickDeviationNegative > tick  ||\\n// Remove the line below\\n           twapTick // Add the line below\\n maxTickDeviationPositive < tick) revert NotCalm();\\n// Add the line below\\n           minCalmTick > tick  ||\\n// Add the line below\\n           maxCalmTick < tick) revert NotCalm();\\n    }\\n```\\n
Vulnerability: Withdraw can return zero tokens while burning a positive amount of shares
Severity: low

Description:
Invariant fuzzing found an edge-case where a user could burn an amount of shares > 0 but receive zero output tokens. The cause appears to be a rounding down to zero precision loss for small `_shares` value in `BeefyVaultConcLiq::withdraw` L220-221:\\n```\\nuint256 _amount0 = (_bal0 * _shares) / _totalSupply;\\nuint256 _amount1 = (_bal1 * _shares) / _totalSupply;\\n```\\n

Impact:
Protocol can enter a state where a user burns their shares but receives zero output tokens in return.\\nProof of Concept: Invariant fuzz testing suite supplied at the conclusion of the audit.

Recommended mitigation:
Change the slippage check to also revert if no output tokens are returned:\\n```\\nif (_amount0 < _minAmount0 || _amount1 < _minAmount1 ||\\n   (_amount0 == 0 && _amount1 == 0)) revert TooMuchSlippage();\\n```\\n
Vulnerability: `SwellLib.BOT` can subtly rug-pull withdrawals by setting `_processedRate = 0` when calling `swEXIT::processWithdrawals`
Severity: medium

Description:
When users create a withdrawal request, their `swETH` is burned then the current exchange rate `rateWhenCreated` is fetched from swETH::swETHToETHRate:\\n```\\nuint256 rateWhenCreated = AccessControlManager.swETH().swETHToETHRate();\\n```\\n\\nHowever `SwellLib.BOT` can pass an arbitrary value for `_processedRate` when calling swEXIT::processWithdrawals:\\n```\\nfunction processWithdrawals(\\n  uint256 _lastTokenIdToProcess,\\n  uint256 _processedRate\\n) external override checkRole(SwellLib.BOT) {\\n```\\n\\nThe final rate used is the lesser of `rateWhenCreated` and _processedRate:\\n```\\nuint256 finalRate = _processedRate > rateWhenCreated\\n  ? rateWhenCreated\\n  : _processedRate;\\n```\\n\\nThis final rate is multiplied by the requested withdrawal amount to determine the actual amount sent to the user requesting a withdrawal:\\n```\\nuint256 requestExitedETH = wrap(amount).mul(wrap(finalRate)).unwrap();\\n```\\n\\nHence `SwellLib.BOT` can subtly rug-pull all withdrawals by setting `_processedRate = 0` when calling `swEXIT::processWithdrawals`.

Impact:

Recommended mitigation:
Two possible mitigations:\\nChange `swEXIT::processWithdrawals` to always fetch the current rate from `swETH::swETHToETHRate`\\nOnly allow `swEXIT::processWithdrawals` to be called by the `RepricingOracle` contract which calls it correctly.
Vulnerability: Check for staleness of data when fetching Proof of Reserves via Chainlink `Swell ETH PoR` Oracle
Severity: low

Description:
`RepricingOracle::_assertRepricingSnapshotValidity` uses the `Swell ETH PoR` Chainlink Proof Of Reserves Oracle to fetch an off-chain data source for Swell's current reserves.\\nThe Oracle `Swell ETH PoR` is listed on Chainlink's website as having a heartbeat of `86400` seconds (check the "Show More Details" box in the top-right corner of the table), however no staleness check is implemented by RepricingOracle:\\n```\\n// @audit no staleness check\\n(, int256 externallyReportedV3Balance, , , ) = AggregatorV3Interface(\\n  ExternalV3ReservesPoROracle\\n).latestRoundData();\\n```\\n

Impact:
If the `Swell ETH PoR` Chainlink Proof Of Reserves Oracle has stopped functioning correctly, `RepricingOracle::_assertRepricingSnapshotValidity` will continue processing with stale reserve data as if it were fresh.

Recommended mitigation:
Implement a staleness check and if the Oracle is stale, either revert or skip using it as the code currently does if the oracle is not set.\\nFor multi-chain deployments ensure that a correct staleness check is used for each feed as the same feed can have different heartbeats on different chains.\\nConsider adding an off-chain bot that periodically checks if the Oracle has become stale and if it has, raises an internal alert for the team to investigate.
Vulnerability: Precision loss in `swETH::_deposit` from unnecessary hidden division before multiplication
Severity: low

Description:
`swETH::_deposit` L170 contains a hidden unnecessary division before multiplication as the call to `_ethToSwETHRate` performs a division which then gets multiplied by msg.value:\\n```\\nuint256 swETHAmount = wrap(msg.value).mul(_ethToSwETHRate()).unwrap();\\n// @audit expanding this out\\n// wrap(msg.value).mul(_ethToSwETHRate()).unwrap();\\n// wrap(msg.value).mul(wrap(1 ether).div(_swETHToETHRate())).unwrap();\\n```\\n\\nThis issue has not been introduced in the new changes but is in the mainnet code.

Impact:
Slightly less `swETH` will be minted to depositors. While the amount by which individual depositors are short-changed is individually small, the effect is cumulative and increases as depositors and deposit size increase.\\nProof of Concept: This stand-alone stateless fuzz test can be run inside Foundry to prove this as well as provided hard-coded test cases:\\n```\\n// SPDX-License-Identifier: MIT\\npragma solidity ^0.8.23;\\n\\nimport {UD60x18, wrap} from "@prb/math/src/UD60x18.sol";\\n\\nimport "forge-std/Test.sol";\\n\\n// run from base project directory with:\\n// (fuzz test) forge test --match-test FuzzMint -vvv\\n// (hardcoded) forge test --match-test HardcodedMint -vvv\\ncontract MintTest is Test {\\n\\n    uint256 private constant SWETH_ETH_RATE = 1050754209601187151; //as of 2024-02-15\\n\\n    function _mintOriginal(uint256 inputAmount) private pure returns(uint256) {\\n        // hidden division before multiplication\\n        // wrap(inputAmount).mul(_ethToSwETHRate()).unwrap();\\n        // wrap(inputAmount).mul(wrap(1 ether).div(_swETHToETHRate())).unwrap()\\n\\n        return wrap(inputAmount).mul(wrap(1 ether).div(wrap(SWETH_ETH_RATE))).unwrap();\\n    }\\n\\n    function _mintFixed(uint256 inputAmount) private pure returns(uint256) {\\n        // refactor to perform multiplication before division\\n        // wrap(inputAmount).mul(wrap(1 ether)).div(_swETHToETHRate()).unwrap();\\n\\n        return wrap(inputAmount).mul(wrap(1 ether)).div(wrap(SWETH_ETH_RATE)).unwrap();\\n    }\\n\\n    function test_FuzzMint(uint256 inputAmount) public pure {\\n        uint256 resultOriginal = _mintOriginal(inputAmount);\\n        uint256 resultFixed    = _mintFixed(inputAmount);\\n\\n        assert(resultOriginal == resultFixed);\\n    }\\n\\n    function test_HardcodedMint() public {\\n        // found by fuzzer\\n        console.log(_mintFixed(3656923177187149889) - _mintOriginal(3656923177187149889)); // 1\\n\\n        // 100 eth\\n        console.log(_mintFixed(100 ether) - _mintOriginal(100 ether)); // 21\\n\\n        // 1000 eth\\n        console.log(_mintFixed(1000 ether) - _mintOriginal(1000 ether)); // 215\\n\\n        // 10000 eth\\n        console.log(_mintFixed(10000 ether) - _mintOriginal(10000 ether)); // 2159\\n    }\\n}\\n```\\n

Recommended mitigation:
Refactor to perform multiplication before division:\\n```\\nuint256 swETHAmount = wrap(msg.value).mul(wrap(1 ether)).div(_swETHToETHRate()).unwrap();\\n```\\n
Vulnerability: Attacker can abuse `RewardsDistributor::triggerRoot` to block reward claims and unpause a paused state
Severity: medium

Description:
Consider the code of RewardsDistributor::triggerRoot:\\n```\\n    function triggerRoot() external {\\n        bytes32 rootCandidateAValue = rootCandidateA.value;\\n        if (rootCandidateAValue != rootCandidateB.value || rootCandidateAValue == bytes32(0)) revert RootCandidatesInvalid();\\n        root = Root({value: rootCandidateAValue, lastUpdatedAt: block.timestamp});\\n        emit RootChanged(msg.sender, rootCandidateAValue);\\n    }\\n```\\n\\nThis function:\\ncan be called by anyone\\nif it succeeds, sets `root.value` to `rootCandidateA.value` and `root.lastUpdatedAt` to `block.timestamp`\\ndoesn't reset `rootCandidateA` or `rootCandidateB`, so it can be called over and over again to continually update `root.lastUpdatedAt` or to set `root.value` to `rootCandidateA.value`.

Impact:
An attacker can abuse this function in 2 ways:\\nby calling it repeatedly an attacker can continually increase `root.lastUpdatedAt` to trigger the claim delay revert in `RewardsDistributor::claimAll` effectively blocking reward claims\\nby calling it after reward claims have been paused, an attacker can effectively unpause the paused state since `root.value` is over-written with the valid value from `rootCandidateA.value` and claim pausing works by setting `root.value == zeroRoot`.

Recommended mitigation:
Two possible options:\\nMake `RewardsDistributor::triggerRoot` a permissioned function such that an attacker can't call it\\nChange `RewardsDistributor::triggerRoot` to reset `rootCandidateA.value = zeroRoot` such that it can't be successfully called repeatedly.
Vulnerability: `RewardsDistributor` doesn't correctly handle deposits of fee-on-transfer incentive tokens
Severity: medium

Description:
`the kenneth` stated in telegram that Fee-On-Transfer tokens are fine to use as incentive tokens with `RewardsDistributor`, however when receiving Fee-On-Transfer tokens and storing the reward amount the accounting does not account for the fee deducted from the transfer amount in-transit, for example:\\n```\\nfunction _depositLPIncentive(\\n    StoredReward memory reward,\\n    uint256 amount,\\n    uint256 periodReceived\\n) private {\\n    IERC20(reward.token).safeTransferFrom(\\n        msg.sender,\\n        address(this),\\n        amount\\n    );\\n\\n    // @audit stored `amount` here will be incorrect since it doesn't account for\\n    // the actual amount received after the transfer fee was deducted in-transit\\n    _storeReward(periodReceived, reward, amount);\\n}\\n```\\n

Impact:
The actual reward calculation is done off-chain and is outside the audit scope nor do we have visibility of that code. But events emitted by `RewardsDistributor` and the stored incentive token deposits in `RewardsDistributor::periodRewards` use incorrect amounts for Fee-On-Transfer incentive token deposits.

Recommended mitigation:
In `RewardsDistributor::_depositLPIncentive` & depositVoteIncentive:\\nread the `before` transfer token balance of `RewardsDistributor` contract\\nperform the token transfer\\nread the `after` transfer token balance of `RewardsDistributor` contract\\ncalculate the difference between the `after` and `before` balances to get the true amount that was received by the `RewardsDistributor` contract accounting for the fee that was deducted in-transit\\nuse the true received amount to generate events and write the received incentive token amounts to `RewardsDistributor::periodRewards`.\\nAlso note that `RewardsDistributor::periodRewards` is never read in the contract, only written to. If it is not used by off-chain processing then consider removing it.
Vulnerability: Use low level `call()` to prevent gas griefing attacks when returned data not required
Severity: low

Description:
Using `call()` when the returned data is not required unnecessarily exposes to gas griefing attacks from huge returned data payload. For example:\\n```\\n(bool sent, ) = _to.call{value: _amount}("");\\nrequire(sent);\\n```\\n\\nIs the same as writing:\\n```\\n(bool sent, bytes memory data) = _to.call{value: _amount}("");\\nrequire(sent);\\n```\\n\\nIn both cases the returned data will be copied into memory exposing the contract to gas griefing attacks, even though the returned data is not used at all.

Impact:
Contract unnecessarily exposed to gas griefing attacks.

Recommended mitigation:
Use a low-level call when the returned data is not required, eg:\\n```\\nbool sent;\\nassembly {\\n    sent := call(gas(), _to, _amount, 0, 0, 0, 0)\\n}\\nif (!sent) revert FailedToSendEther();\\n```\\n
Vulnerability: No precision scaling or minimum received amount check when subtracting `relayerFeeAmount` can revert due to underflow or return less tokens to user than specified
Severity: medium

Description:
`PorticoFinish::payOut` L376 attempts to subtract the `relayerFeeAmount` from the final post-bridge and post-swap token balance:\\n```\\nfinalUserAmount = finalToken.balanceOf(address(this)) - relayerFeeAmount;\\n```\\n\\nThere is no precision scaling to ensure that PorticoFinish's token contract balance and `relayerFeeAmount` are in the same decimal precision; if the `relayerFeeAmount` has 18 decimal places but the token is USDC with only 6 decimal places, this can easily revert due to underflow resulting in the bridged tokens being stuck.\\nAn excessively high `relayerFeeAmount` could also significantly reduce the amount of post-bridge and post-swap tokens received as there is no check on the minimum amount of tokens the user will receive after deducting `relayerFeeAmount`. This current configuration is an example of the "MinTokensOut For Intermediate, Not Final Amount" vulnerability class; as the minimum received tokens check is before the deduction of `relayerFeeAmount` a user will always receive less tokens than their specified minimum if `relayerFeeAmount > 0`.

Impact:
Bridged tokens stuck or user receives less tokens than their specified minimum.

Recommended mitigation:
Ensure that token balance and `relayerFeeAmount` have the same decimal precision before combining them. Alternatively check for underflow and don't charge a fee if this would be the case. Consider enforcing the user-specified minimum output token check again when deducting `relayerFeeAmount`, and if this would fail then decrease `relayerFeeAmount` such that the user at least receives their minimum specified token amount.\\nAnother option is to check that even if it doesn't underflow, that the remaining amount after subtracting `relayerFeeAmount` is a high percentage of the bridged amount; this would prevent a scenario where `relayerFeeAmount` takes a large part of the bridged amount, effectively capping `relayerFeeAmount` to a tiny % of the post-bridge and post-swap funds. This scenario can still result in the user receiving less tokens than their specified minimum however.\\nFrom the point of view of the smart contract, it should protect itself against the possibility of the token amount and `relayerFeeAmount` being in different decimals or that `relayerFeeAmount` would be too high, similar to how for example L376 inside `payOut` doesn't trust the bridge reported amount and checks the actual token balance.
Vulnerability: Use low level `call()` to prevent gas griefing attacks when returned data not required
Severity: low

Description:
Using `call()` when the returned data is not required unnecessarily exposes to gas griefing attacks from huge returned data payload. For example:\\n```\\n(bool sentToUser, ) = recipient.call{ value: finalUserAmount }("");\\nrequire(sentToUser, "Failed to send Ether");\\n```\\n\\nIs the same as writing:\\n```\\n(bool sentToUser, bytes memory data) = recipient.call{ value: finalUserAmount }("");\\nrequire(sentToUser, "Failed to send Ether");\\n```\\n\\nIn both cases the returned data will be copied into memory exposing the contract to gas griefing attacks, even though the returned data is not used at all.

Impact:
Contract unnecessarily exposed to gas griefing attacks.

Recommended mitigation:
Use a low-level call when the returned data is not required, eg:\\n```\\nbool sent;\\nassembly {\\n    sent := call(gas(), recipient, finalUserAmount, 0, 0, 0, 0)\\n}\\nif (!sent) revert Unauthorized();\\n```\\n\\nConsider using ExcessivelySafeCall.
Vulnerability: Both reserves should be checked in `LibWell::getWellPriceFromTwaReserves`
Severity: low

Description:
```\\nfunction getWellPriceFromTwaReserves(address well) internal view returns (uint256 price) {\\n    AppStorage storage s = LibAppStorage.diamondStorage();\\n    // s.twaReserve[well] should be set prior to this function being called.\\n    // 'price' is in terms of reserve0:reserve1.\\n    if (s.twaReserves[well].reserve0 == 0) {\\n        price = 0;\\n    } else {\\n        price = s.twaReserves[well].reserve0.mul(1e18).div(s.twaReserves[well].reserve1);\\n    }\\n}\\n```\\n\\nCurrently, `LibWell::getWellPriceFromTwaReserves` sets the price to zero if the time-weighted average reserves of the zeroth reserve (for Wells, Bean) is zero. Given the implementation of `LibWell::setTwaReservesForWell`, and that a Pump failure will return an empty reserves array, it does not appear possible to encounter the case where one reserve can be zero without the other except for perhaps an exploit or migration scenario. Therefore, whilst unlikely, it is best to but best to ensure both reserves are non-zero to avoid a potential division by zero `reserve1` when calculating the price as a revert here would result in DoS of `SeasonFacet::gm`.\\n```\\nfunction setTwaReservesForWell(address well, uint256[] memory twaReserves) internal {\\n    AppStorage storage s = LibAppStorage.diamondStorage();\\n    // if the length of twaReserves is 0, then return 0.\\n    // the length of twaReserves should never be 1, but\\n    // is added for safety.\\n    if (twaReserves.length < 1) {\\n        delete s.twaReserves[well].reserve0;\\n        delete s.twaReserves[well].reserve1;\\n    } else {\\n        // safeCast not needed as the reserves are uint128 in the wells.\\n        s.twaReserves[well].reserve0 = uint128(twaReserves[0]);\\n        s.twaReserves[well].reserve1 = uint128(twaReserves[1]);\\n    }\\n}\\n```\\n\\nAdditionally, to correctly implement the check identified by the comment in `LibWell::setTwaReservesForWell`, the time-weighted average reserves in storage should be reset if the array length is less-than or equal-to 1.

Impact:

Recommended mitigation:
```\\n// LibWell::getWellPriceFromTwaReserves`\\n// Remove the line below\\n if (s.twaReserves[well].reserve0 == 0) {\\n// Add the line below\\n if (s.twaReserves[well].reserve0 == 0 || s.twaReserves[well].reserve1 == 0) {\\n        price = 0;\\n} else {\\n\\n// LibWell::setTwaReservesForWell\\n// Remove the line below\\n if (twaReserves.length < 1) {\\n// Add the line below\\n if (twaReserves.length <= 1) {\\n    delete s.twaReserves[well].reserve0;\\n    delete s.twaReserves[well].reserve1;\\n} else {\\n```\\n