msc-smart-contract-auditing/vulnerable-functions-base huggingface dataset:
Vulnerability: AgentRegistryCore - Agent Creation DoS
Severity: medium

Description:
AgentRegistryCore allows anyone to mint an `agentID` for the desired owner address. However, in some cases, it may fall prey to DoS, either deliberately or unintentionally.\\nFor instance, let's assume the Front Running Protection is disabled or the `frontRunningDelay` is 0. It means anyone can directly create an agent without any prior commitment. Thus, anyone can observe pending transactions and try to front run them to mint an `agentID` prior to the victim's restricting it to mint a desired `agentID`.\\nAlso, it may be possible that a malicious actor succeeds in frontrunning a transaction with manipulated data/chainIDs but with the same owner address and `agentID`. There is a good chance that victim still accepts the attacker's transaction as valid, even though its own transaction reverted, due to the fact that the victim is still seeing itself as the owner of that ID.\\nTaking an instance where let's assume the frontrunning protection is enabled. Still, there is a good chance that two users vouch for the same `agentIDs` and commits in the same block, thus getting the same frontrunning delay. Then, it will be a game of luck, whoever creates that agent first will get the ID minted to its address, and the other user's transaction will be reverted wasting the time they have spent on the delay.\\nAs the `agentIDs` can be picked by users, the chances of collisions with an already minted ID will increase over time causing unnecessary reverts for others.\\nAdding to the fact that there is no restriction for owner address, anyone can spam mint any `agentID` to any address for any profitable reason.\\n```\\nfunction createAgent(uint256 agentId, address owner, string calldata metadata, uint256[] calldata chainIds)\\npublic\\n    onlySorted(chainIds)\\n    frontrunProtected(keccak256(abi.encodePacked(agentId, owner, metadata, chainIds)), frontRunningDelay)\\n{\\n    \\_mint(owner, agentId);\\n    \\_beforeAgentUpdate(agentId, metadata, chainIds);\\n    \\_agentUpdate(agentId, metadata, chainIds);\\n    \\_afterAgentUpdate(agentId, metadata, chainIds);\\n}\\n```\\n

Impact:

Recommended mitigation:
Modify function `prepareAgent` to not commit an already registered `agentID`.\\nA better approach could be to allow sequential minting of `agentIDs` using some counters.\\nOnly allow users to mint an `agentID`, either for themselves or for someone they are approved to.
Vulnerability: Lack of checks for rewarding an epoch that has already been rewarded
Severity: medium

Description:
To give rewards to the participating stakers, the Forta system utilizes reward epochs for each `shareId`, i.e. a delegated staking share. Each epoch gets their own reward distribution, and then `StakeAllocator` and `RewardsDistributor` contracts along with the Forta staking shares determine how much the users get.\\nTo actually allocate these rewards, a privileged account with the role `REWARDER_ROLE` calls the `RewardsDistributor.reward()` function with appropriate parameters to store the `amount` a `shareId` gets for that specific `epochNumber`, and then adds the `amount` to the `totalRewardsDistributed` contract variable for tracking. However, there is no check that the `shareId` already received rewards for that `epoch`. The new reward `amount` simply replaces the old reward `amount`, and `totalRewardsDistributed` gets the new `amount` added to it anyway. This causes inconsistencies with accounting in the `totalRewardsDistributed` variable.\\nAlthough `totalRewardsDistributed` is essentially isolated to the `sweep()` function to allow transferring out the reward tokens without taking away those tokens reserved for the reward distribution, this still creates an inconsistency, albeit a minor one in the context of the current system.\\nSimilarly, the `sweep()` function deducts the `totalRewardsDistributed` amount instead of the amount of pending rewards only. In other words, either there should be a different variable that tracks only pending rewards, or the `totalRewardsDistributed` should have token amounts deducted from it when users execute the `claimRewards()` function. Otherwise, after a few epochs there will be a really large `totalRewardsDistributed` amount that might not reflect the real amount of pending reward tokens left on the contract, and the `sweep()` function for the reward token is likely to fail for any amount being transferred out.\\n```\\nfunction reward(\\n    uint8 subjectType,\\n    uint256 subjectId,\\n    uint256 amount,\\n    uint256 epochNumber\\n) external onlyRole(REWARDER\\_ROLE) {\\n    if (subjectType != NODE\\_RUNNER\\_SUBJECT) revert InvalidSubjectType(subjectType);\\n    if (!\\_subjectGateway.isRegistered(subjectType, subjectId)) revert RewardingNonRegisteredSubject(subjectType, subjectId);\\n    uint256 shareId = FortaStakingUtils.subjectToActive(getDelegatorSubjectType(subjectType), subjectId);\\n    \\_rewardsPerEpoch[shareId][epochNumber] = amount;\\n    totalRewardsDistributed += amount;\\n    emit Rewarded(subjectType, subjectId, amount, epochNumber);\\n}\\n```\\n

Impact:

Recommended mitigation:
Implement checks as appropriate to the `reward()` function to ensure correct behavior of `totalRewardsDistributed` tracking. Also, implement necessary changes to the tracking of pending rewards, if necessary.
Vulnerability: Unnecessary code blocks that check the same condition
Severity: low

Description:
In the `RewardsDistributor` there is a function that allows to set delegation fees for a `NodeRunner`. It adjusts the `fees[]` array for that node as appropriate. However, during its checks, it performs the same check twice in a row.\\n```\\nif (fees[1].sinceEpoch != 0) {\\n    if (Accumulators.getCurrentEpochNumber() < fees[1].sinceEpoch + delegationParamsEpochDelay) revert SetDelegationFeeNotReady();\\n}\\nif (fees[1].sinceEpoch != 0) {\\n    fees[0] = fees[1];\\n}\\n```\\n

Impact:

Recommended mitigation:
Consider refactoring this under a single code block.
Vulnerability: Event spam in RewardsDistributor.claimRewards
Severity: low

Description:
The `RewardsDistributor` contract allows users to claim their rewards through the `claimRewards()` function. It does check to see whether or not the user has already claimed the rewards for a specific epoch that they are claiming for, but it does not check to see if the user has any associated rewards at all. This could lead to event `ClaimedRewards` being spammed by malicious users, especially on low gas chains.\\n```\\nfor (uint256 i = 0; i < epochNumbers.length; i++) {\\n    if (\\_claimedRewardsPerEpoch[shareId][epochNumbers[i]][\\_msgSender()]) revert AlreadyClaimed();\\n    \\_claimedRewardsPerEpoch[shareId][epochNumbers[i]][\\_msgSender()] = true;\\n    uint256 epochRewards = \\_availableReward(shareId, isDelegator, epochNumbers[i], \\_msgSender());\\n    SafeERC20.safeTransfer(rewardsToken, \\_msgSender(), epochRewards);\\n    emit ClaimedRewards(subjectType, subjectId, \\_msgSender(), epochNumbers[i], epochRewards);\\n```\\n

Impact:

Recommended mitigation:
Add a check for rewards amounts being greater than 0.
Vulnerability: Lack of a check for the subject's stake for reviewSlashProposalParameters
Severity: low

Description:
In the `SlashingController` contract, the address with the `SLASHING_ARBITER_ROLE` may call the `reviewSlashProposalParameters()` function to adjust the slashing proposal to a new `_subjectId` and `_subjectType`. However, unlike in the `proposeSlash()` function, there is no check for that subject having any stake at all.\\nWhile it may be assumed that the review function will be called by a privileged and knowledgeable actor, this additional check may avoid accidental mistakes.\\n```\\nif (subjectGateway.totalStakeFor(\\_subjectType, \\_subjectId) == 0) revert ZeroAmount("subject stake");\\n```\\n\\n```\\nif (\\_subjectType != proposals[\\_proposalId].subjectType || \\_subjectId != proposals[\\_proposalId].subjectId) {\\n    \\_unfreeze(\\_proposalId);\\n    \\_freeze(\\_subjectType, \\_subjectId);\\n}\\n```\\n

Impact:

Recommended mitigation:
Add a check for the new subject having stake to slash.
Vulnerability: Comment and code inconsistencies
Severity: low

Description:
During the audit a few inconsistencies were found between what the comments say and what the implemented code actually did.\\nSubject Type Agency for Scanner Subjects\\nIn the `SubjectTypeValidator`, the comment says that the `SCANNER_SUBJECT` is of type `DIRECT` agency type, i.e. it can be directly staked on by multiple different stakers. However, we found a difference in the implementation, where the concerned subject is defined as type `MANAGED` agency type, which says that it cannot be staked on directly; instead it's a delegated type and the allocation is supposed to be managed by its manager.\\n```\\n\\* - SCANNER\\_SUBJECT --> DIRECT\\n```\\n\\n```\\n} else if (subjectType == SCANNER\\_SUBJECT) {\\n    return SubjectStakeAgency.MANAGED;\\n```\\n\\nDispatch refers to ERC721 tokens as ERC1155\\nOne of the comments describing the functionality to `link` and `unlink` agents and scanners refers to them as ERC1155 tokens, when in reality they are ERC721.\\n```\\n/\\*\\*\\n \\* @notice Assigns the job of running an agent to a scanner.\\n \\* @dev currently only allowed for DISPATCHER\\_ROLE (Assigner software).\\n \\* @dev emits Link(agentId, scannerId, true) event.\\n \\* @param agentId ERC1155 token id of the agent.\\n \\* @param scannerId ERC1155 token id of the scanner.\\n \\*/\\n```\\n\\nNodeRunnerRegistryCore comment that implies the reverse of what happens\\nA comment describing a helper function that returns address for a given scanner ID describes the opposite behavior. It is the same comment for the function just above that actually does what the comment says.\\n```\\n/// Converts scanner address to uint256 for FortaStaking Token Id.\\nfunction scannerIdToAddress(uint256 scannerId) public pure returns (address) {\\n    return address(uint160(scannerId));\\n}\\n```\\n\\nScannerToNodeRunnerMigration comment that says that no NodeRunner tokens must be owned\\nFor the migration from Scanners to NodeRunners, a comment in the beginning of the file implies that for the system to work correctly, there must be no NodeRunner tokens owned prior to migration. After a conversation with the Forta Foundation team, it appears that this was an early design choice that is no longer relevant.\\n```\\n\\* @param nodeRunnerId If set as 0, a new NodeRunnerRegistry ERC721 will be minted to nodeRunner (but it must not own any prior),\\n```\\n\\n```\\n\\* @param nodeRunnerId If set as 0, a new NodeRunnerRegistry ERC721 will be minted to nodeRunner (but it must not own any prior),\\n```\\n

Impact:

Recommended mitigation:
Verify the operational logic and fix either the concerned comments or defined logic as per the need.
Vulnerability: Oracle's _sanityCheck for prices will not work with slashing
Severity: high

Description:
The `_sanityCheck` is verifying that the new price didn't change significantly:\\n```\\nuint256 maxPrice = curPrice +\\n ((curPrice \\*\\n self.PERIOD\\_PRICE\\_INCREASE\\_LIMIT \\*\\n \\_periodsSinceUpdate) / PERCENTAGE\\_DENOMINATOR);\\n\\nuint256 minPrice = curPrice -\\n ((curPrice \\*\\n self.PERIOD\\_PRICE\\_DECREASE\\_LIMIT \\*\\n \\_periodsSinceUpdate) / PERCENTAGE\\_DENOMINATOR);\\n\\nrequire(\\n \\_newPrice >= minPrice && \\_newPrice <= maxPrice,\\n "OracleUtils: price is insane"\\n```\\n\\nWhile the rewards of staking can be reasonably predicted, the balances may also be changed due to slashing. So any slashing event should reduce the price, and if enough ETH is slashed, the price will drop heavily. The oracle will not be updated because of a sanity check. After that, there will be an arbitrage opportunity, and everyone will be incentivized to withdraw as soon as possible. That process will inevitably devaluate gETH to zero. The severity of this issue is also amplified by the fact that operators have no skin in the game and won't lose anything from slashing.

Impact:

Recommended mitigation:
Make sure that slashing can be adequately processed when updating the price.
Vulnerability: MiniGovernance - fetchUpgradeProposal will always revert
Severity: high

Description:
In the function `fetchUpgradeProposal()`, `newProposal()` is called with a hard coded `duration` of 4 weeks. This means the function will always revert since `newProposal()` checks that the proposal `duration` is not more than the constant `MAX_PROPOSAL_DURATION` of 2 weeks. Effectively, this leaves MiniGovernance non-upgradeable.\\n```\\nGEM.newProposal(proposal.CONTROLLER, 2, proposal.NAME, 4 weeks);\\n```\\n\\n```\\nrequire(\\n duration <= MAX\\_PROPOSAL\\_DURATION,\\n "GeodeUtils: duration exceeds MAX\\_PROPOSAL\\_DURATION"\\n);\\n```\\n

Impact:

Recommended mitigation:
Switch the hard coded proposal duration to 2 weeks.
Vulnerability: Only the GOVERNANCE can initialize the Portal
Severity: medium

Description:
In the Portal's `initialize` function, the `_GOVERNANCE` is passed as a parameter:\\n```\\nfunction initialize(\\n address \\_GOVERNANCE,\\n address \\_gETH,\\n address \\_ORACLE\\_POSITION,\\n address \\_DEFAULT\\_gETH\\_INTERFACE,\\n address \\_DEFAULT\\_DWP,\\n address \\_DEFAULT\\_LP\\_TOKEN,\\n address \\_MINI\\_GOVERNANCE\\_POSITION,\\n uint256 \\_GOVERNANCE\\_TAX,\\n uint256 \\_COMET\\_TAX,\\n uint256 \\_MAX\\_MAINTAINER\\_FEE,\\n uint256 \\_BOOSTRAP\\_PERIOD\\n) public virtual override initializer {\\n \\_\\_ReentrancyGuard\\_init();\\n \\_\\_Pausable\\_init();\\n \\_\\_ERC1155Holder\\_init();\\n \\_\\_UUPSUpgradeable\\_init();\\n\\n GEODE.SENATE = \\_GOVERNANCE;\\n GEODE.GOVERNANCE = \\_GOVERNANCE;\\n GEODE.GOVERNANCE\\_TAX = \\_GOVERNANCE\\_TAX;\\n GEODE.MAX\\_GOVERNANCE\\_TAX = \\_GOVERNANCE\\_TAX;\\n GEODE.SENATE\\_EXPIRY = type(uint256).max;\\n\\n STAKEPOOL.GOVERNANCE = \\_GOVERNANCE;\\n STAKEPOOL.gETH = IgETH(\\_gETH);\\n STAKEPOOL.TELESCOPE.gETH = IgETH(\\_gETH);\\n STAKEPOOL.TELESCOPE.ORACLE\\_POSITION = \\_ORACLE\\_POSITION;\\n STAKEPOOL.TELESCOPE.MONOPOLY\\_THRESHOLD = 20000;\\n\\n updateStakingParams(\\n \\_DEFAULT\\_gETH\\_INTERFACE,\\n \\_DEFAULT\\_DWP,\\n \\_DEFAULT\\_LP\\_TOKEN,\\n \\_MAX\\_MAINTAINER\\_FEE,\\n \\_BOOSTRAP\\_PERIOD,\\n type(uint256).max,\\n type(uint256).max,\\n \\_COMET\\_TAX,\\n 3 days\\n );\\n```\\n\\nBut then it calls the `updateStakingParams` function, which requires the `msg.sender` to be the governance:\\n```\\nfunction updateStakingParams(\\n address \\_DEFAULT\\_gETH\\_INTERFACE,\\n address \\_DEFAULT\\_DWP,\\n address \\_DEFAULT\\_LP\\_TOKEN,\\n uint256 \\_MAX\\_MAINTAINER\\_FEE,\\n uint256 \\_BOOSTRAP\\_PERIOD,\\n uint256 \\_PERIOD\\_PRICE\\_INCREASE\\_LIMIT,\\n uint256 \\_PERIOD\\_PRICE\\_DECREASE\\_LIMIT,\\n uint256 \\_COMET\\_TAX,\\n uint256 \\_BOOST\\_SWITCH\\_LATENCY\\n) public virtual override {\\n require(\\n msg.sender == GEODE.GOVERNANCE,\\n "Portal: sender not GOVERNANCE"\\n );\\n```\\n\\nSo only the future governance can initialize the `Portal`. In the case of the Geode protocol, the governance will be represented by a token contract, making it hard to initialize promptly. Initialization should be done by an actor that is more flexible than governance.

Impact:

Recommended mitigation:
Split the `updateStakingParams` function into public and private ones and use them accordingly.
Vulnerability: The maintainer of the MiniGovernance can block the changeMaintainer function
Severity: medium

Description:
Every entity with an ID has a controller and a maintainer. The controller tends to have more control, and the maintainer is mostly used for operational purposes. So the controller should be able to change the maintainer if that is required. Indeed we see that it is possible in the MiniGovernance too:\\n```\\nfunction changeMaintainer(\\n bytes calldata password,\\n bytes32 newPasswordHash,\\n address newMaintainer\\n)\\n external\\n virtual\\n override\\n onlyPortal\\n whenNotPaused\\n returns (bool success)\\n{\\n require(\\n SELF.PASSWORD\\_HASH == bytes32(0) ||\\n SELF.PASSWORD\\_HASH ==\\n keccak256(abi.encodePacked(SELF.ID, password))\\n );\\n SELF.PASSWORD\\_HASH = newPasswordHash;\\n\\n \\_refreshSenate(newMaintainer);\\n\\n success = true;\\n}\\n```\\n\\nHere the `changeMaintainer` function can only be called by the Portal, and only the controller can initiate that call. But the maintainer can pause the MiniGovernance, which will make this call revert because the `_refreshSenate` function has the `whenNotPaused` modifier. Thus maintainer could intentionally prevent the controller from replacing it by another maintainer.

Impact:

Recommended mitigation:
Make sure that the controller can always change the malicious maintainer.
Vulnerability: Entities are not required to be initiated
Severity: medium

Description:
Every entity (Planet, Comet, Operator) has a 3-step creation process:\\nCreation of the proposal.\\nApproval of the proposal.\\nInitiation of the entity.\\nThe last step is crucial, but it is never explicitly checked that the entity is initialized. The initiation always includes the `initiator` modifier that works with the `"initiated"` slot on DATASTORE:\\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\\nBut this slot is never actually checked when the entities are used. While we did not find any profitable attack vector using uninitiated entities, the code will be upgraded, which may allow for possible attack vectors related to this issue.

Impact:

Recommended mitigation:
Make sure the entities are initiated before they are used.
Vulnerability: The blameOperator can be called for an alienated validator
Severity: medium

Description:
The `blameOperator` function is designed to be called by anyone. If some operator did not signal to exit in time, anyone can blame and imprison this operator.\\n```\\n/\\*\\*\\n \\* @notice allows improsening an Operator if the validator have not been exited until expectedExit\\n \\* @dev anyone can call this function\\n \\* @dev if operator has given enough allowence, they can rotate the validators to avoid being prisoned\\n \\*/\\nfunction blameOperator(\\n StakePool storage self,\\n DataStoreUtils.DataStore storage DATASTORE,\\n bytes calldata pk\\n) external {\\n if (\\n block.timestamp > self.TELESCOPE.\\_validators[pk].expectedExit &&\\n self.TELESCOPE.\\_validators[pk].state != 3\\n ) {\\n OracleUtils.imprison(\\n DATASTORE,\\n self.TELESCOPE.\\_validators[pk].operatorId\\n );\\n }\\n}\\n```\\n\\nThe problem is that it can be called for any state that is not `3` (self.TELESCOPE._validators[pk].state != 3). But it should only be called for active validators whose state equals `2`. So the `blameOperator` can be called an infinite amount of time for alienated or not approved validators. These types of validators cannot switch to state `3`.\\nThe severity of the issue is mitigated by the fact that this function is currently unavailable for users to call. But it is intended to be external once the withdrawal process is in place.

Impact:

Recommended mitigation:
Make sure that you can only blame the operator of an active validator.
Vulnerability: Latency timelocks on certain functions can be bypassed
Severity: medium

Description:
The functions `switchMaintainerFee()` and `switchWithdrawalBoost()` add a latency of typically three days to the current timestamp at which the new value is meant to be valid. However, they don't limit the number of times this value can be changed within the latency period. This allows a malicious maintainer to set their desired value twice and effectively make the change immediately. Let's take the first function as an example. The first call to it sets a value as the `newFee`, moving the old value to `priorFee`, which is effectively the fee in use until the time lock is up. A follow-up call to the function with the same value as a parameter would mean the “new” value overwrites the old `priorFee` while remaining in the queue for the switch.\\n```\\nfunction switchMaintainerFee(\\n DataStoreUtils.DataStore storage DATASTORE,\\n uint256 id,\\n uint256 newFee\\n) external {\\n DATASTORE.writeUintForId(\\n id,\\n "priorFee",\\n DATASTORE.readUintForId(id, "fee")\\n );\\n DATASTORE.writeUintForId(\\n id,\\n "feeSwitch",\\n block.timestamp + FEE\\_SWITCH\\_LATENCY\\n );\\n DATASTORE.writeUintForId(id, "fee", newFee);\\n\\n emit MaintainerFeeSwitched(\\n id,\\n newFee,\\n block.timestamp + FEE\\_SWITCH\\_LATENCY\\n );\\n}\\n```\\n\\n```\\nfunction getMaintainerFee(\\n DataStoreUtils.DataStore storage DATASTORE,\\n uint256 id\\n) internal view returns (uint256 fee) {\\n if (DATASTORE.readUintForId(id, "feeSwitch") > block.timestamp) {\\n return DATASTORE.readUintForId(id, "priorFee");\\n }\\n return DATASTORE.readUintForId(id, "fee");\\n}\\n```\\n

Impact:

Recommended mitigation:
Add a check to make sure only one value can be set between time lock periods.
Vulnerability: MiniGovernance's senate has almost unlimited validity
Severity: medium

Description:
A new senate for the MiniGovernance contract is set in the following line:\\n```\\nGEM.\\_setSenate(newSenate, block.timestamp + SENATE\\_VALIDITY);\\n```\\n\\nThe validity period argument should not include `block.timestamp`, because it is going to be added a bit later in the code:\\n```\\nself.SENATE\\_EXPIRY = block.timestamp + \\_senatePeriod;\\n```\\n\\nSo currently, every senate of MiniGovernance will have much longer validity than it is supposed to.

Impact:

Recommended mitigation:
Pass onlySENATE_VALIDITY in the `_refreshSenate` function.
Vulnerability: Proposed validators not accounted for in the monopoly check.
Severity: medium

Description:
The Geode team introduced a check that makes sure that node operators do not initiate more validators than a threshold called `MONOPOLY_THRESHOLD` allows. It is used on call to `proposeStake(...)` which the operator would call in order to propose new validators. It is worth mentioning that onboarding new validator nodes requires 2 steps: a proposal from the node operator and approval from the planet maintainer. After the first step validators get a status of `proposed`. After the second step validators get the status of `active` and all eth accounting is done. The issue we found is that the `proposed` validators step performs the monopoly check but does not account for previously `proposed` but not `active` validators.\\nAssume that `MONOPOLY_THRESHOLD` is set to 5. The node operator could propose 4 new validators and pass the monopoly check and label those validators as `proposed`. The node operator could then suggest 4 more validators in a separate transaction and since the monopoly check does not check for the `proposed` validators, that would pass as well. Then in `beaconStake` or the step of maintainer approval, there is no monopoly check at all, so 8 validators could be activated at once.\\n```\\nrequire(\\n (DATASTORE.readUintForId(operatorId, "totalActiveValidators") +\\n pubkeys.length) <= self.TELESCOPE.MONOPOLY\\_THRESHOLD,\\n "StakeUtils: IceBear does NOT like monopolies"\\n);\\n```\\n

Impact:

Recommended mitigation:
Include the `(DATASTORE.readUintForId(poolId,DataStoreUtils.getKey(operatorId, "proposedValidators"))` into the require statement, just like in the check for the node operator allowance check.\\n```\\nrequire(\\n (DATASTORE.readUintForId(\\n poolId,\\n DataStoreUtils.getKey(operatorId, "proposedValidators")\\n ) +\\n DATASTORE.readUintForId(\\n poolId,\\n DataStoreUtils.getKey(operatorId, "activeValidators")\\n ) +\\n pubkeys.length) <=\\n operatorAllowance(DATASTORE, poolId, operatorId),\\n "StakeUtils: NOT enough allowance"\\n);\\n```\\n