msc-smart-contract-auditing/vulnerable-functions-base huggingface dataset:
Vulnerability: Broken check in `MysteryBox::fulfillRandomWords()` fails to prevent same request being fulfilled multiple times
Severity: high

Description:
Consider the check which attempts to prevent the same request from being fulfilled multiple times:\\n```\\nif (vrfRequests[_requestId].fulfilled) revert InvalidVrfState();\\n```\\n\\nThe problem is that `vrfRequests[_requestId].fulfilled` is never set to `true` anywhere and `vrfRequests[_requestId]` is deleted at the end of the function.

Impact:
The same request can be fulfilled multiple times which would override the previous randomly generated seed; a malicious provider who was also a mystery box minter could generate new randomness until they got a rare mystery box.

Recommended mitigation:
Set `vrfRequests[_requestId].fulfilled = true`.\\nConsider an optimized version which involves having 2 mappings `activeVrfRequests` and fulfilledVrfRequests:\\nrevert `if(fulfilledVrfRequests[_requestId])`\\nelse set `fulfilledVrfRequests[_requestId] = true`\\nfetch the matching active request into memory from `activeVrfRequests[_requestId]` and continue processing as normal\\nat the end `delete activeVrfRequests[_requestId]`\\nThis only stores forever the `requestId` : `bool` pair in `fulfilledVrfRequests`.\\nConsider a similar approach in `MysteryBox::fulfillBoxAmount()`.
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, ) = address(operatorAddress).call{value: msg.value}("");\\nif (!sent) revert Unauthorized();\\n```\\n\\nIs the same as writing:\\n```\\n(bool sent, bytes memory data) = address(operatorAddress).call{value: msg.value}("");\\nif (!sent) revert Unauthorized();\\n```\\n\\nIn both cases the returned data will have to be copied into memory exposing the contract to gas griefing attacks, even though the returned data is not required at all.

Impact:
Contracts unnecessarily expose themselves 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(), receiver, amount, 0, 0, 0, 0)\\n}\\nif (!sent) revert Unauthorized();\\n```\\n\\nConsider using ExcessivelySafeCall.
Vulnerability: `GovPool::delegateTreasury` does not verify transfer of tokens and NFTs to delegatee leading to potential voting manipulation
Severity: high

Description:
`GovPool::delegateTreasury` transfers ERC20 tokens & specific nfts from DAO treasury to `govUserKeeper`. Based on this transfer, the `tokenBalance` and `nftBalance` of the delegatee is increased. This allows a delegatee to use this delegated voting power to vote in critical proposals.\\nAs the following snippet of `GovPool::delegateTreasury` function shows, there is no verification that the tokens and nfts are actually transferred to the `govUserKeeper`. It is implicitly assumed that a successful transfer is completed and subsequently, the voting power of the delegatee is increased.\\n```\\n  function delegateTreasury(\\n        address delegatee,\\n        uint256 amount,\\n        uint256[] calldata nftIds\\n    ) external override onlyThis {\\n        require(amount > 0 || nftIds.length > 0, "Gov: empty delegation");\\n        require(getExpertStatus(delegatee), "Gov: delegatee is not an expert");\\n\\n        _unlock(delegatee);\\n\\n        if (amount != 0) {\\n            address token = _govUserKeeper.tokenAddress();\\n\\n  >          IERC20(token).transfer(address(_govUserKeeper), amount.from18(token.decimals())); //@audit no check if tokens are actually transferred\\n\\n            _govUserKeeper.delegateTokensTreasury(delegatee, amount);\\n        }\\n\\n        if (nftIds.length != 0) {\\n            IERC721 nft = IERC721(_govUserKeeper.nftAddress());\\n\\n            for (uint256 i; i < nftIds.length; i++) {\\n  >              nft.safeTransferFrom(address(this), address(_govUserKeeper), nftIds[i]); //-n no check if nft's are actually transferred\\n            }\\n\\n            _govUserKeeper.delegateNftsTreasury(delegatee, nftIds);\\n        }\\n\\n        _revoteDelegated(delegatee, VoteType.TreasuryVote);\\n\\n        emit DelegatedTreasury(delegatee, amount, nftIds, true);\\n    }\\n```\\n\\nThis could lead to a dangerous situation where a malicious DAO treasury can increase voting power manifold while actually transferring tokens only once (or even, not transfer at all). This breaks the invariance that the total accounting balances in `govUserKeeper` contract must match the actual token balances in that contract.

Impact:
Since both the ERC20 and ERC721 token implementations are controlled by the DAO, and since we are dealing with upgradeable token contracts, there is a potential rug-pull vector created by the implicit transfer assumption above.

Recommended mitigation:
Since DEXE starts out with a trustless assumption that does not give any special trust privileges to a DAO treasury, it is always prudent to follow the "trust but verify" approach when it comes to non-standard tokens, both ERC20 and ERC721. To that extent, consider adding verification of token & nft balance increase before/after token transfer.
Vulnerability: Voting to change `RewardsInfo::voteRewardsCoefficient` has an unintended side-effect of retrospectively changing voting rewards for active proposals
Severity: medium

Description:
`GovSettings::editSettings` is one of the functions that can be executed via an internal proposal. When this function is called, setting are validated via `GovSettings::_validateProposalSettings`. This function does not check the value of `RewardsInfo::voteRewardsCoefficient` while updating the settings. There is neither a floor nor a cap for this setting.\\nHowever, we've noted that this coefficient amplifies voting rewards as calculated in the `GovPoolRewards::_getInitialVotingRewards` shown below.\\n```\\n    function _getInitialVotingRewards(\\n        IGovPool.ProposalCore storage core,\\n        IGovPool.VoteInfo storage voteInfo\\n    ) internal view returns (uint256) {\\n        (uint256 coreVotes, uint256 coreRawVotes) = voteInfo.isVoteFor\\n            ? (core.votesFor, core.rawVotesFor)\\n            : (core.votesAgainst, core.rawVotesAgainst);\\n\\n        return\\n            coreRawVotes.ratio(core.settings.rewardsInfo.voteRewardsCoefficient, PRECISION).ratio(\\n                voteInfo.totalVoted,\\n                coreVotes\\n            ); //@audit -> initial rewards are calculated proportionate to the vote rewards coefficient\\n    }\\n```\\n\\nThis has the unintended side-effect that for the same proposal, different voters can get paid different rewards based on when the reward was claimed. In the extreme case where `core.settings.rewardsInfo.voteRewardsCoefficient` is voted to 0, note that we have a situation where voters who claimed rewards before the update got paid as promised whereas voters who claimed later got nothing.

Impact:
Updating `rewardsCoefficient` can lead to unfair reward distribution on old proposals. Since voting rewards for a given proposal are communicated upfront, this could lead to a situation where promised rewards to users are not honoured.\\nProof of Concept: N/A

Recommended mitigation:
Consider freezing `voteRewardMultiplier` and the time of proposal creation. A prospective update of this setting via internal voting should not change rewards for old proposals.
Vulnerability: Proposal execution can be DOSed with return bombs when calling untrusted execution contracts
Severity: medium

Description:
`GovPool::execute` does not check for return bombs when executing a low-level call. A return bomb is a large bytes array that expands the memory so much that any attempt to execute the transaction will lead to an `out-of-gas` exception.\\nThis can create potentially risky outcomes for the DAO. One possible outcome is "single sided" execution, ie. "actionsFor" can be executed when voting is successful while "actionsAgainst" can be DOSed when voting fails.\\nA clever proposal creator can design a proposal in such a way that only `actionsFor` can be executed and any attempts to execute `actionsAgainst` will be permanently DOS'ed (refer POC contract). T\\nThis is possible because the `GovPoolExecute::execute` does a low level call on potentially untrusted `executor` assigned to a specific action.\\n```\\n   function execute(\\n        mapping(uint256 => IGovPool.Proposal) storage proposals,\\n        uint256 proposalId\\n    ) external {\\n        // rest of code. // code\\n\\n        for (uint256 i; i < actionsLength; i++) {\\n>            (bool status, bytes memory returnedData) = actions[i].executor.call{\\n                value: actions[i].value\\n            }(actions[i].data); //@audit returnedData could expand memory and cause out-of-gas exception\\n\\n            require(status, returnedData.getRevertMsg());\\n        }\\n   }\\n```\\n

Impact:
Voting actions can be manipulated by a creator causing two potential issues:\\nProposal actions can never be executed even after successful voting\\nOne-sided execution where some actions can be executed while others can be DOSed\\nProof of Concept: Consider the following malicious proposal action executor contract. Note that when the proposal passes (isVotesFor = true), the `vote()` function returns empty bytes and when the proposal fails (isVotesFor = false), the same function returns a huge bytes array, effectively causing an "out-of-gas" exception to any caller.\\n```\\ncontract MaliciousProposalActionExecutor is IProposalValidator{\\n\\n    function validate(IGovPool.ProposalAction[] calldata actions) external view override returns (bool valid){\\n     valid = true;\\n    }\\n\\n    function vote(\\n        uint256 proposalId,\\n        bool isVoteFor,\\n        uint256 voteAmount,\\n        uint256[] calldata voteNftIds\\n    ) external returns(bytes memory result){\\n\\n if(isVoteFor){\\n  // @audit implement actions for successful vote\\n         return ""; // 0 bytes\\n        }\\n else{\\n  // @audit implement actions for failed vote\\n\\n  // Create a large bytes array\\n                assembly{\\n                     revert(0, 1_000_000)\\n              }\\n }\\n\\n   }\\n}\\n```\\n

Recommended mitigation:
Consider using `ExcessivelySafeCall` while calling untrusted contracts to avoid return bombs.
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 status, ) = payable(receiver).call{value: amount}("");\\nrequire(status, "Gov: failed to send eth");\\n```\\n\\nIs the same as writing:\\n```\\n(bool status, bytes memory data ) = payable(receiver).call{value: amount}("");\\nrequire(status, "Gov: failed to send eth");\\n```\\n\\nIn both cases the returned data will have to be copied into memory exposing the contract to gas griefing attacks, even though the returned data is not required at all.

Impact:
Contracts unnecessarily expose themselves to gas griefing attacks.

Recommended mitigation:
Use a low-level call when the returned data is not required, eg:\\n```\\nbool status;\\nassembly {\\n    status := call(gas(), receiver, amount, 0, 0, 0, 0)\\n}\\n```\\n\\nConsider using ExcessivelySafeCall.
Vulnerability: `abi.encodePacked()` should not be used with dynamic types when passing the result to a hash function such as `keccak256()`
Severity: low

Description:
`abi.encodePacked()` should not be used with dynamic types when passing the result to a hash function such as `keccak256()`.\\nUse `abi.encode()` instead which will pad items to 32 bytes, which will prevent hash collisions (e.g. `abi.encodePacked(0x123,0x456)` => `0x123456` => `abi.encodePacked(0x1,0x23456)`, but `abi.encode(0x123,0x456)` => 0x0...1230...456).\\nUnless there is a compelling reason, `abi.encode` should be preferred. If there is only one argument to `abi.encodePacked()` it can often be cast to `bytes()` or `bytes32()` instead. If all arguments are strings and or bytes, `bytes.concat()` should be used instead.\\nProof of Concept:\\n```\\nFile: factory/PoolFactory.sol\\n\\n        return keccak256(abi.encodePacked(deployer, poolName));\\n```\\n\\n```\\nFile: libs/gov/gov-pool/GovPoolOffchain.sol\\n\\n        return keccak256(abi.encodePacked(resultsHash, block.chainid, address(this)));\\n```\\n\\n```\\nFile: user/UserRegistry.sol\\n\\n        _signatureHashes[_documentHash][msg.sender] = keccak256(abi.encodePacked(signature));\\n```\\n

Impact:

Recommended mitigation:
See description.
Vulnerability: A removal signature might be applied to the wrong `fid`.
Severity: medium

Description:
A remove signature is used to remove a key from `fidOwner` using `KeyRegistry.removeFor()`. And the signature is verified in `_verifyRemoveSig()`.\\n```\\n    function _verifyRemoveSig(address fidOwner, bytes memory key, uint256 deadline, bytes memory sig) internal {\\n        _verifySig(\\n            _hashTypedDataV4(\\n                keccak256(abi.encode(REMOVE_TYPEHASH, fidOwner, keccak256(key), _useNonce(fidOwner), deadline))\\n            ),\\n            fidOwner,\\n            deadline,\\n            sig\\n        );\\n    }\\n```\\n\\nBut the signature doesn't specify a `fid` to remove and the below scenario would be possible.\\nAlice is an owner of `fid1` and she created a removal signature to remove a `key` but it's not used yet.\\nFor various reasons, she became an owner of `fid2`.\\n`fid2` has a `key` also but she doesn't want to remove it.\\nBut if anyone calls `removeFor()` with her previous signature, the `key` will be removed from `fid2` unexpectedly.\\nOnce a key is removed, `KeyState` will be changed to `REMOVED` and anyone including the owner can't retrieve it.

Impact:
A key remove signature might be used for an unexpected `fid`.

Recommended mitigation:
The removal signature should contain `fid` also to be invalidated for another `fid`.
Vulnerability: `VoteKickPolicy._endVote()` might revert forever due to underflow
Severity: high

Description:
In `onFlag()`, `targetStakeAtRiskWei[target]` might be less than the total rewards for the flagger/reviewers due to rounding.\\n```\\nFile: contracts\\OperatorTokenomics\\StreamrConfig.sol\\n    /**\\n     * Minimum amount to pay reviewers+flagger\\n     * That is: minimumStakeWei >= (flaggerRewardWei + flagReviewerCount * flagReviewerRewardWei) / slashingFraction\\n     */\\n    function minimumStakeWei() public view returns (uint) {\\n        return (flaggerRewardWei + flagReviewerCount * flagReviewerRewardWei) * 1 ether / slashingFraction;\\n    }\\n```\\n\\nLet's assume `flaggerRewardWei + flagReviewerCount * flagReviewerRewardWei = 100, StreamrConfig.slashingFraction = 0.03e18(3%), minimumStakeWei() = 1000 * 1e18 / 0.03e18 = 10000 / 3 = 3333.`\\nIf we suppose `stakedWei[target] = streamrConfig.minimumStakeWei()`, then `targetStakeAtRiskWei[target]` = 3333 * 0.03e18 / 1e18 = 99.99 = 99.\\nAs a result, `targetStakeAtRiskWei[target]` is less than total rewards(=100), and `_endVote()` will revert during the reward distribution due to underflow.\\nThe above scenario is possible only when there is a rounding during `minimumStakeWei` calculation. So it works properly with the default `slashingFraction = 10%`.

Impact:
The `VoteKickPolicy` wouldn't work as expected and malicious operators won't be kicked forever.

Recommended mitigation:
Always round the `minimumStakeWei()` up.
Vulnerability: Possible overflow in `_payOutFirstInQueue`
Severity: high

Description:
In `_payOutFirstInQueue()`, possible revert during `operatorTokenToDataInverse()`.\\n```\\nuint amountOperatorTokens = moduleCall(address(exchangeRatePolicy), abi.encodeWithSelector(exchangeRatePolicy.operatorTokenToDataInverse.selector, amountDataWei));\\n```\\n\\nIf a delegator calls `undelegate()` with `type(uint256).max`, `operatorTokenToDataInverse()` will revert due to uint overflow and the queue logic will be broken forever.\\n```\\n   function operatorTokenToDataInverse(uint dataWei) external view returns (uint operatorTokenWei) {\\n       return dataWei * this.totalSupply() / valueWithoutEarnings();\\n   }\\n```\\n

Impact:
The queue logic will be broken forever because `_payOutFirstInQueue()` keeps reverting.

Recommended mitigation:
We should cap `amountDataWei` before calling `operatorTokenToDataInverse()`.
Vulnerability: Wrong validation in `DefaultUndelegationPolicy.onUndelegate()`
Severity: high

Description:
In `onUndelegate()`, it checks if the operator owner still holds at least `minimumSelfDelegationFraction` of total supply.\\n```\\n   function onUndelegate(address delegator, uint amount) external {\\n       // limitation only applies to the operator, others can always undelegate\\n       if (delegator != owner) { return; }\\n\\n       uint actualAmount = amount < balanceOf(owner) ? amount : balanceOf(owner); //@audit amount:DATA, balanceOf:Operator\\n       uint balanceAfter = balanceOf(owner) - actualAmount;\\n       uint totalSupplyAfter = totalSupply() - actualAmount;\\n       require(1 ether * balanceAfter >= totalSupplyAfter * streamrConfig.minimumSelfDelegationFraction(), "error_selfDelegationTooLow");\\n   }\\n```\\n\\nBut `amount` means the DATA token `amount` and `balanceOf(owner)` indicates the `Operator` token balance and it's impossible to compare them directly.

Impact:
The operator owner wouldn't be able to undelegate because `onUndelegate()` works unexpectedly.

Recommended mitigation:
`onUndelegate()` should compare amounts after converting to the same token.
Vulnerability: Malicious target can make `_endVote()` revert forever by forceUnstaking/staking again
Severity: high

Description:
In `_endVote()`, we update `forfeitedStakeWei` or `lockedStakeWei[target]` according to the target's staking status.\\n```\\nFile: contracts\\OperatorTokenomics\\SponsorshipPolicies\\VoteKickPolicy.sol\\n    function _endVote(address target) internal {\\n        address flagger = flaggerAddress[target];\\n        bool flaggerIsGone = stakedWei[flagger] == 0;\\n        bool targetIsGone = stakedWei[target] == 0;\\n        uint reviewerCount = reviewers[target].length;\\n        // release stake locks before vote resolution so that slashings and kickings during resolution aren't affected\\n        // if either the flagger or the target has forceUnstaked or been kicked, the lockedStakeWei was moved to forfeitedStakeWei\\n        if (flaggerIsGone) {\\n            forfeitedStakeWei -= flagStakeWei[target];\\n        } else {\\n            lockedStakeWei[flagger] -= flagStakeWei[target];\\n        }\\n        if (targetIsGone) {\\n            forfeitedStakeWei -= targetStakeAtRiskWei[target];\\n        } else {\\n            lockedStakeWei[target] -= targetStakeAtRiskWei[target]; //@audit revert after forceUnstake() => stake() again\\n        }\\n```\\n\\nWe consider the target is still active if he has a positive staking amount. But we don't know if he has unstaked and staked again, so the below scenario would be possible.\\nThe target staked 100 amount and a flagger reported him.\\nIn `onFlag()`, `lockedStakeWei[target]` = targetStakeAtRiskWei[target] = 100.\\nDuring the voting period, the target called `forceUnstake()`. Then `lockedStakeWei[target]` was reset to 0 in `Sponsorship._removeOperator()`.\\nAfter that, he stakes again and `_endVote()` will revert forever at L195 due to underflow.\\nAfter all, he won't be flagged again because the current flagging won't be finalized.\\nFurthermore, malicious operators would manipulate the above state by themselves to earn operator rewards without any risks.

Impact:
Malicious operators can bypass the flagging system by reverting `_endVote()` forever.

Recommended mitigation:
Perform stake unlocks in `_endVote()` without relying on the current staking amounts.
Vulnerability: In `VoteKickPolicy.onFlag()`, `targetStakeAtRiskWei[target]` might be greater than `stakedWei[target]` and `_endVote()` would revert.
Severity: medium

Description:
`targetStakeAtRiskWei[target]` might be greater than `stakedWei[target]` in `onFlag()`.\\n```\\ntargetStakeAtRiskWei[target] = max(stakedWei[target], streamrConfig.minimumStakeWei()) * streamrConfig.slashingFraction() / 1 ether;\\n```\\n\\nFor example,\\nAt the first time, `streamrConfig.minimumStakeWei()` = 100 and an operator(=target) has staked 100.\\n`streamrConfig.minimumStakeWei()` was increased to 2000 after a reconfiguration.\\n`onFlag()` is called for target and `targetStakeAtRiskWei[target]` will be `max(100, 2000) * 10% = 200`.\\nIn `_endVote()`, `slashingWei = _kick(target, slashingWei)` will be 100 because target has staked 100 only.\\nSo it will revert due to underflow during the reward distribution.

Impact:
Operators with small staked funds wouldn't be kicked forever.

Recommended mitigation:
`onFlag()` should check if a target has staked enough funds for rewards and handle separately if not.
Vulnerability: Possible front running of `flag()`
Severity: medium

Description:
The `target` might call `unstake()/forceUnstake()` before a flagger calls `flag()` to avoid a possible fund loss. Also, there would be no slash during the unstaking for `target` when it meets the `penaltyPeriodSeconds` requirement.\\n```\\nFile: contracts\\OperatorTokenomics\\SponsorshipPolicies\\VoteKickPolicy.sol\\n    function onFlag(address target, address flagger) external {\\n        require(flagger != target, "error_cannotFlagSelf");\\n        require(voteStartTimestamp[target] == 0 && block.timestamp > protectionEndTimestamp[target], "error_cannotFlagAgain"); // solhint-disable-line not-rely-on-time\\n        require(stakedWei[flagger] >= minimumStakeOf(flagger), "error_notEnoughStake");\\n        require(stakedWei[target] > 0, "error_flagTargetNotStaked"); //@audit possible front run\\n```\\n

Impact:
A malicious target would bypass the kick policy by front running.

Recommended mitigation:
There is no straightforward mitigation but we could implement a kind of `delayed unstaking` logic for some percent of staking funds.
Vulnerability: In `Operator._transfer()`, `onDelegate()` should be called after updating the token balances
Severity: medium

Description:
In `_transfer()`, `onDelegate()` is called to validate the owner's `minimumSelfDelegationFraction` requirement.\\n```\\nFile: contracts\\OperatorTokenomics\\Operator.sol\\n        // transfer creates a new delegator: check if the delegation policy allows this "delegation"\\n        if (balanceOf(to) == 0) {\\n            if (address(delegationPolicy) != address(0)) {\\n                moduleCall(address(delegationPolicy), abi.encodeWithSelector(delegationPolicy.onDelegate.selector, to)); //@audit\\nshould be called after _transfer()\\n            }\\n        }\\n        super._transfer(from, to, amount);\\n```\\n\\nBut `onDelegate()` is called before updating the token balances and the below scenario would be possible.\\nThe operator owner has 100 shares(required minimum fraction). And there are no undelegation policies.\\nLogically, the owner shouldn't be able to transfer his 100 shares to a new delegator due to the min fraction requirement in `onDelegate()`.\\nBut if the owner calls `transfer(owner, to, 100)`, `balanceOf(owner)` will be 100 in `onDelegation()` and it will pass the requirement because it's called before `super._transfer()`.

Impact:
The operator owner might transfer his shares to other delegators in anticipation of slashing, to avoid slashing.

Recommended mitigation:
`onDelegate()` should be called after `super._transfer()`.