msc-smart-contract-auditing/vulnerable-functions-base huggingface dataset:
Vulnerability: StakingContract - PubKey Length Checks Not Always Enforced
Severity: medium

Description:
`addValidators` checks that the provided `bytes pubKey` is a multiple of the expected pubkey length while functions like `setWithdrawer` do not enforce similar length checks. This is an inconsistency that should be avoided.\\n`addValidators` enforcing input length checks\\n```\\nfunction addValidators(\\n uint256 \\_operatorIndex,\\n uint256 \\_keyCount,\\n bytes calldata \\_publicKeys,\\n bytes calldata \\_signatures\\n) external onlyActiveOperator(\\_operatorIndex) {\\n if (\\_keyCount == 0) {\\n revert InvalidArgument();\\n }\\n\\n if (\\_publicKeys.length % PUBLIC\\_KEY\\_LENGTH != 0 || \\_publicKeys.length / PUBLIC\\_KEY\\_LENGTH != \\_keyCount) {\\n revert InvalidPublicKeys();\\n }\\n```\\n\\n`setWithdrawer` accepting any length for a `pubKey`. Note that `_getPubKeyRoot` will take any input provided and concat it the zero bytes.\\n```\\n/// @notice Set withdrawer for public key\\n/// @dev Only callable by current public key withdrawer\\n/// @param \\_publicKey Public key to change withdrawer\\n/// @param \\_newWithdrawer New withdrawer address\\nfunction setWithdrawer(bytes calldata \\_publicKey, address \\_newWithdrawer) external {\\n if (!StakingContractStorageLib.getWithdrawerCustomizationEnabled()) {\\n revert Forbidden();\\n }\\n \\_checkAddress(\\_newWithdrawer);\\n bytes32 pubkeyRoot = \\_getPubKeyRoot(\\_publicKey);\\n StakingContractStorageLib.WithdrawersSlot storage withdrawers = StakingContractStorageLib.getWithdrawers();\\n\\n if (withdrawers.value[pubkeyRoot] != msg.sender) {\\n revert Unauthorized();\\n }\\n\\n emit ChangedWithdrawer(\\_publicKey, \\_newWithdrawer);\\n\\n withdrawers.value[pubkeyRoot] = \\_newWithdrawer;\\n}\\n```\\n\\n```\\nfunction \\_getPubKeyRoot(bytes memory \\_publicKey) internal pure returns (bytes32) {\\n return sha256(abi.encodePacked(\\_publicKey, bytes16(0)));\\n}\\n```\\n\\nsimilarly, the withdraw family of functions does not enforce a pubkey length either. However, it is unlikely that someone finds a pubkey that matches a root for the attackers address.\\n```\\n/// @notice Withdraw the Execution Layer Fee for a given validator public key\\n/// @dev Funds are sent to the withdrawer account\\n/// @param \\_publicKey Validator to withdraw Execution Layer Fees from\\nfunction withdrawELFee(bytes calldata \\_publicKey) external {\\n \\_onlyWithdrawerOrAdmin(\\_publicKey);\\n \\_deployAndWithdraw(\\_publicKey, EXECUTION\\_LAYER\\_SALT\\_PREFIX, StakingContractStorageLib.getELDispatcher());\\n}\\n```\\n\\nNevertheless, the methods should be hardened so as not to give a malicious actor the freedom to use an unexpected input size for the `pubKey` argument.

Impact:

Recommended mitigation:
Enforce pubkey length checks when accepting a single pubkey as bytes similar to the batch functions that check for a multiple of ´PUBLIC_KEY_LENGTH´. Alternatively, declare the function argument as `bytes48` (however, in this case inputs may be auto-padded to fit the expected length, pot. covering situations that otherwise would throw an error)
Vulnerability: Unpredictable Behavior Due to Admin Front Running or General Bad Timing
Severity: medium

Description:
In a number of cases, administrators of contracts can update or upgrade things in the system without warning. This has the potential to violate a security goal of the system.\\nSpecifically, privileged roles could use front running to make malicious changes just ahead of incoming transactions, or purely accidental negative effects could occur due to the unfortunate timing of changes.\\nSome instances of this are more important than others, but in general, users of the system should have assurances about the behavior of the action they're about to take.\\nUpgradeable TU proxy\\nFee changes take effect immediately\\n```\\n/// @notice Change the Operator fee\\n/// @param \\_operatorFee Fee in Basis Point\\nfunction setOperatorFee(uint256 \\_operatorFee) external onlyAdmin {\\n if (\\_operatorFee > StakingContractStorageLib.getOperatorCommissionLimit()) {\\n revert InvalidFee();\\n }\\n StakingContractStorageLib.setOperatorFee(\\_operatorFee);\\n emit ChangedOperatorFee(\\_operatorFee);\\n}\\n```\\n\\n```\\n/// @notice Change the Global fee\\n/// @param \\_globalFee Fee in Basis Point\\nfunction setGlobalFee(uint256 \\_globalFee) external onlyAdmin {\\n if (\\_globalFee > StakingContractStorageLib.getGlobalCommissionLimit()) {\\n revert InvalidFee();\\n }\\n StakingContractStorageLib.setGlobalFee(\\_globalFee);\\n emit ChangedGlobalFee(\\_globalFee);\\n}\\n```\\n

Impact:

Recommended mitigation:
The underlying issue is that users of the system can't be sure what the behavior of a function call will be, and this is because the behavior can change at any time.\\nWe recommend giving the user advance notice of changes with a time lock. For example, make all upgrades require two steps with a mandatory time window between them. The first step merely broadcasts to users that a particular change is coming, and the second step commits that change after a suitable waiting period.
Vulnerability: Potentially Uninitialized Implementations
Severity: medium

Description:
Most contracts in the system are meant to be used with a proxy pattern. First, the implementations are deployed, and then proxies are deployed that delegatecall into the respective implementations following an initialization call (hardhat, with same transaction). However, the implementations are initialized explicitly nor are they protected from other actors claiming/initializing them. This allows anyone to call initialization functions on implementations for use with phishing attacks (i.e. contract implementation addresses are typically listed on the official project website as valid contracts) which may affect the reputation of the system.\\nNone of the implementations allow unprotected delegatecalls or selfdesturcts. lowering the severity of this finding.\\n```\\nfunction initialize\\_1(\\n address \\_admin,\\n address \\_treasury,\\n address \\_depositContract,\\n address \\_elDispatcher,\\n address \\_clDispatcher,\\n address \\_feeRecipientImplementation,\\n uint256 \\_globalFee,\\n uint256 \\_operatorFee,\\n uint256 globalCommissionLimitBPS,\\n uint256 operatorCommissionLimitBPS\\n) external init(1) {\\n```\\n\\n```\\n/// @notice Initializes the receiver\\n/// @param \\_dispatcher Address that will handle the fee dispatching\\n/// @param \\_publicKeyRoot Public Key root assigned to this receiver\\nfunction init(address \\_dispatcher, bytes32 \\_publicKeyRoot) external {\\n if (initialized) {\\n revert AlreadyInitialized();\\n }\\n initialized = true;\\n dispatcher = IFeeDispatcher(\\_dispatcher);\\n publicKeyRoot = \\_publicKeyRoot;\\n stakingContract = msg.sender; // The staking contract always calls init\\n}\\n```\\n\\n```\\n/// @param \\_publicKeyRoot Public Key root assigned to this receiver\\nfunction init(address \\_dispatcher, bytes32 \\_publicKeyRoot) external {\\n if (initialized) {\\n revert AlreadyInitialized();\\n }\\n initialized = true;\\n dispatcher = IFeeDispatcher(\\_dispatcher);\\n publicKeyRoot = \\_publicKeyRoot;\\n}\\n```\\n

Impact:

Recommended mitigation:
Petrify contracts in the constructor and disallow other actors from claiming/initializing the implementations.
Vulnerability: Operator May DoS the Withdrawal or Make It More Expensive
Severity: medium

Description:
While collecting fees, the operator may:\\ncause DoS for the funds/rewards withdrawal by reverting the call, thus reverting the whole transaction. By doing this, it won't be receiving any rewards, but so the treasury and withdrawer.\\nmake the withdrawal more expensive by sending a huge chunk of `returndata`. As the `returndata` is copied into memory in the caller's context, it will add an extra gas overhead for the withdrawer making it more expensive.\\nor mint gas token\\n```\\nif (operatorFee > 0) {\\n (status, data) = operator.call{value: operatorFee}("");\\n if (status == false) {\\n revert FeeRecipientReceiveError(data);\\n }\\n}\\n```\\n

Impact:

Recommended mitigation:
A possible solution could be to make a low-level call in an inline assembly block, restricting the `returndata` to a couple of bytes, and instead of reverting on the failed call, emit an event, flagging the call that failed.
Vulnerability: ConsensusLayerFeeDispatcher/ExecutionLayerFeeDispatcher - Should Hardcode autoPetrify With Highest Initializable Version Instead of User Provided Argument
Severity: low

Description:
The version to auto-initialize is not hardcoded with the constructor. On deployment, the deployer may accidentally use the wrong version, allowing anyone to call `initialize` on the contract.\\n```\\n/// @notice Constructor method allowing us to prevent calls to initCLFR by setting the appropriate version\\nconstructor(uint256 \\_version) {\\n VERSION\\_SLOT.setUint256(\\_version);\\n}\\n```\\n\\n```\\n/// @notice Constructor method allowing us to prevent calls to initCLFR by setting the appropriate version\\nconstructor(uint256 \\_version) {\\n VERSION\\_SLOT.setUint256(\\_version);\\n}\\n\\n/// @notice Initialize the contract by storing the staking contract and the public key in storage\\n/// @param \\_stakingContract Address of the Staking Contract\\nfunction initELD(address \\_stakingContract) external init(1) {\\n STAKING\\_CONTRACT\\_ADDRESS\\_SLOT.setAddress(\\_stakingContract);\\n}\\n```\\n

Impact:

Recommended mitigation:
Similar to the `init(1)` modifier, it is suggested to track the highest version as a `const int` with the contract and auto-initialize to the highest version in the constructor instead of taking the highest version as a deployment argument.
Vulnerability: StakingContract - Misleading Comment
Severity: low

Description:
The comment notes that the expected caller is `admin` while the modifier checks that `msg.sender` is an active operator.\\n```\\n/// @notice Ensures that the caller is the admin\\nmodifier onlyActiveOperator(uint256 \\_operatorIndex) {\\n \\_onlyActiveOperator(\\_operatorIndex);\\n \\_;\\n}\\n```\\n

Impact:

Recommended mitigation:
Rectify the comment to accurately describe the intention of the method/modifier.
Vulnerability: Impractical Checks for Global/Operator Fees and the Commission Limits
Severity: low

Description:
The contract initialization sets up the global and operator fees and also their commission limits. However, the checks just make sure that the fees or commission limit is up to 100% which is not a very practical check. Any unusual value, for instance, if set to 100% will mean the whole rewards/funds will be non-exempted and taxed as global fees, which we believe will never be a case practically.\\n```\\nif (\\_globalFee > BASIS\\_POINTS) {\\n revert InvalidFee();\\n}\\nStakingContractStorageLib.setGlobalFee(\\_globalFee);\\nif (\\_operatorFee > BASIS\\_POINTS) {\\n revert InvalidFee();\\n}\\nStakingContractStorageLib.setOperatorFee(\\_operatorFee);\\n```\\n\\n```\\nfunction initialize\\_2(uint256 globalCommissionLimitBPS, uint256 operatorCommissionLimitBPS) public init(2) {\\n if (globalCommissionLimitBPS > BASIS\\_POINTS) {\\n revert InvalidFee();\\n }\\n StakingContractStorageLib.setGlobalCommissionLimit(globalCommissionLimitBPS);\\n if (operatorCommissionLimitBPS > BASIS\\_POINTS) {\\n revert InvalidFee();\\n }\\n StakingContractStorageLib.setOperatorCommissionLimit(operatorCommissionLimitBPS);\\n}\\n```\\n\\n```\\nfunction setGlobalFee(uint256 \\_globalFee) external onlyAdmin {\\n if (\\_globalFee > StakingContractStorageLib.getGlobalCommissionLimit()) {\\n revert InvalidFee();\\n }\\n StakingContractStorageLib.setGlobalFee(\\_globalFee);\\n emit ChangedGlobalFee(\\_globalFee);\\n}\\n```\\n\\n```\\nfunction setOperatorFee(uint256 \\_operatorFee) external onlyAdmin {\\n if (\\_operatorFee > StakingContractStorageLib.getOperatorCommissionLimit()) {\\n revert InvalidFee();\\n }\\n StakingContractStorageLib.setOperatorFee(\\_operatorFee);\\n emit ChangedOperatorFee(\\_operatorFee);\\n}\\n```\\n

Impact:

Recommended mitigation:
The fees should be checked with a more practical limit. For instance, checking against a min - max limit, like 20% - 40%.
Vulnerability: Contracts Should Inherit From Their Interfaces
Severity: low

Description:
The following contracts should enforce correct interface implementation by inheriting from the interface declarations.\\n```\\n/// @title Ethereum Staking Contract\\n/// @author Kiln\\n/// @notice You can use this contract to store validator keys and have users fund them and trigger deposits.\\ncontract StakingContract {\\n using StakingContractStorageLib for bytes32;\\n```\\n\\n```\\ninterface IStakingContractFeeDetails {\\n function getWithdrawerFromPublicKeyRoot(bytes32 \\_publicKeyRoot) external view returns (address);\\n\\n function getTreasury() external view returns (address);\\n\\n function getOperatorFeeRecipient(bytes32 pubKeyRoot) external view returns (address);\\n\\n function getGlobalFee() external view returns (uint256);\\n\\n function getOperatorFee() external view returns (uint256);\\n\\n function getExitRequestedFromRoot(bytes32 \\_publicKeyRoot) external view returns (bool);\\n\\n function getWithdrawnFromPublicKeyRoot(bytes32 \\_publicKeyRoot) external view returns (bool);\\n\\n function toggleWithdrawnFromPublicKeyRoot(bytes32 \\_publicKeyRoot) external;\\n}\\n```\\n\\n```\\ninterface IFeeRecipient {\\n function init(address \\_dispatcher, bytes32 \\_publicKeyRoot) external;\\n\\n function withdraw() external;\\n}\\n```\\n

Impact:

Recommended mitigation:
Inherit from interface.
Vulnerability: Misleading Error Statements
Severity: low

Description:
The contracts define custom errors to revert transactions on failed operations or invalid input, however, they convey little to no information, making it difficult for the off-chain monitoring tools to track relevant updates.\\n```\\nerror Forbidden();\\nerror InvalidFee();\\nerror Deactivated();\\nerror NoOperators();\\nerror InvalidCall();\\nerror Unauthorized();\\nerror DepositFailure();\\nerror DepositsStopped();\\nerror InvalidArgument();\\nerror UnsortedIndexes();\\nerror InvalidPublicKeys();\\nerror InvalidSignatures();\\nerror InvalidWithdrawer();\\nerror InvalidZeroAddress();\\nerror AlreadyInitialized();\\nerror InvalidDepositValue();\\nerror NotEnoughValidators();\\nerror InvalidValidatorCount();\\nerror DuplicateValidatorKey(bytes);\\nerror FundedValidatorDeletionAttempt();\\nerror OperatorLimitTooHigh(uint256 limit, uint256 keyCount);\\nerror MaximumOperatorCountAlreadyReached();\\nerror LastEditAfterSnapshot();\\nerror PublicKeyNotInContract();\\n```\\n\\nFor instance, the `init` modifier is used to initialize the contracts with the current Version. The Version initialization ensures that the provided version must be an increment of the previous version, if not, it reverts with an error as `AlreadyInitialized()`. However, the error doesn't convey an appropriate message correctly, as any version other than the expected version will signify that the version has already been initialized.\\n```\\nmodifier init(uint256 \\_version) {\\n if (\\_version != VERSION\\_SLOT.getUint256() + 1) {\\n revert AlreadyInitialized();\\n }\\n```\\n\\n```\\nmodifier init(uint256 \\_version) {\\n if (\\_version != VERSION\\_SLOT.getUint256() + 1) {\\n revert AlreadyInitialized();\\n }\\n```\\n\\n```\\nmodifier init(uint256 \\_version) {\\n if (\\_version != StakingContractStorageLib.getVersion() + 1) {\\n revert AlreadyInitialized();\\n }\\n```\\n

Impact:

Recommended mitigation:
Use a more meaningful statement with enough information to track off-chain for all the custom errors in every contract in scope. For instance, add the current and supplied versions as indexed parameters, like: IncorrectVersionInitialization(current version, supplied version);\\nAlso, the function can be simplified as\\n```\\n function initELD(address \\_stakingContract) external init(VERSION\\_SLOT.getUint256() + 1) {\\n STAKING\\_CONTRACT\\_ADDRESS\\_SLOT.setAddress(\\_stakingContract);\\n }\\n```\\n
Vulnerability: Architectural Pattern of Internal and External Functions Increases Attack Surface
Severity: low

Description:
There is an architectural pattern throughout the code of functions being defined in two places: an external wrapper (name) that verifies authorization and validates parameters, and an internal function (_name) that contains the implementation logic. This pattern separates concerns and avoids redundancy in the case that more than one external function reuses the same internal logic.\\nFor example, `VotingTokenLockupPlans.setupVoting` calls an internal function `_setupVoting` and sets the `holder` parameter to `msg.sender`.\\n```\\nfunction setupVoting(uint256 planId) external nonReentrant returns (address votingVault) {\\n votingVault = \\_setupVoting(msg.sender, planId);\\n```\\n\\n```\\nfunction \\_setupVoting(address holder, uint256 planId) internal returns (address) {\\n require(ownerOf(planId) == holder, '!owner');\\n```\\n\\nIn this case, however, there is no case in which `holder` should not be set to `msg.sender`. Because the internal function doesn't enforce this, it's theoretically possible that if another internal (or derived) function were compromised then it could call `_setupVoting` with `holder` set to `ownerOf(planId)`, even if `msg.sender` isn't the owner. This increases the attack surface through providing unneeded flexibility.\\nOther Examples\\n```\\nfunction segmentPlan(\\n uint256 planId,\\n uint256[] memory segmentAmounts\\n) external nonReentrant returns (uint256[] memory newPlanIds) {\\n newPlanIds = new uint256[](segmentAmounts.length);\\n for (uint256 i; i < segmentAmounts.length; i++) {\\n uint256 newPlanId = \\_segmentPlan(msg.sender, planId, segmentAmounts[i]);\\n```\\n\\n```\\nfunction \\_segmentPlan(address holder, uint256 planId, uint256 segmentAmount) internal returns (uint256 newPlanId) {\\n require(ownerOf(planId) == holder, '!owner');\\n```\\n\\n```\\nfunction revokePlans(uint256[] memory planIds) external nonReentrant {\\n for (uint256 i; i < planIds.length; i++) {\\n \\_revokePlan(msg.sender, planIds[i]);\\n```\\n\\n```\\nfunction \\_revokePlan(address vestingAdmin, uint256 planId) internal {\\n Plan memory plan = plans[planId];\\n require(vestingAdmin == plan.vestingAdmin, '!vestingAdmin');\\n```\\n

Impact:

Recommended mitigation:
Resolution\\nFixed as of commit `f4299cdba5e863c9ca2d69a3a7dd554ac34af292`.\\nTo reduce the attack surface, consider hard coding parameters such as `holder` to `msg.sender` in internal functions when extra flexibility isn't needed.
Vulnerability: Revoking Vesting Will Trigger a Taxable Event
Severity: low

Description:
Resolution\\nFixed as of commit `f4299cdba5e863c9ca2d69a3a7dd554ac34af292`.\\nFrom the previous conversations with the Hedgey team, we identified that users should be in control of when taxable events happen. For that reason, one could redeem a plan in the past. Unfortunately, the recipient of the vesting plan can not always be in control of the redemption process. If for one reason or another the administrator of the vesting plan decides to revoke it, any vested funds will be sent to the vesting plan holder, triggering the taxable event and burning the NFT.\\n```\\nfunction \\_revokePlan(address vestingAdmin, uint256 planId) internal {\\n Plan memory plan = plans[planId];\\n require(vestingAdmin == plan.vestingAdmin, '!vestingAdmin');\\n (uint256 balance, uint256 remainder, ) = planBalanceOf(planId, block.timestamp, block.timestamp);\\n require(remainder > 0, '!Remainder');\\n address holder = ownerOf(planId);\\n delete plans[planId];\\n \\_burn(planId);\\n TransferHelper.withdrawTokens(plan.token, vestingAdmin, remainder);\\n TransferHelper.withdrawTokens(plan.token, holder, balance);\\n emit PlanRevoked(planId, balance, remainder);\\n}\\n```\\n\\n```\\nfunction \\_revokePlan(address vestingAdmin, uint256 planId) internal {\\n Plan memory plan = plans[planId];\\n require(vestingAdmin == plan.vestingAdmin, '!vestingAdmin');\\n (uint256 balance, uint256 remainder, ) = planBalanceOf(planId, block.timestamp, block.timestamp);\\n require(remainder > 0, '!Remainder');\\n address holder = ownerOf(planId);\\n delete plans[planId];\\n \\_burn(planId);\\n address vault = votingVaults[planId];\\n if (vault == address(0)) {\\n TransferHelper.withdrawTokens(plan.token, vestingAdmin, remainder);\\n TransferHelper.withdrawTokens(plan.token, holder, balance);\\n } else {\\n delete votingVaults[planId];\\n VotingVault(vault).withdrawTokens(vestingAdmin, remainder);\\n VotingVault(vault).withdrawTokens(holder, balance);\\n }\\n emit PlanRevoked(planId, balance, remainder);\\n}\\n```\\n

Impact:

Recommended mitigation:
One potential workaround is to only withdraw the unvested portion to the vesting admin while keeping the vested part in the contract. That being said `amount` and `rate` variables would need to be updated in order not to allow any additional vesting for the given plan. This way plan holders will not be entitled to more funds but will be able to redeem them at the time they choose.
Vulnerability: Use of selfdestruct Deprecated in VotingVault
Severity: low

Description:
The `VotingVault.withdrawTokens` function invokes the `selfdestruct` operation when the vault is empty so that it can't be used again.\\nThe use ofselfdestruct has been deprecated and a breaking change in its future behavior is expected.\\n```\\nfunction withdrawTokens(address to, uint256 amount) external onlyController {\\n TransferHelper.withdrawTokens(token, to, amount);\\n if (IERC20(token).balanceOf(address(this)) == 0) selfdestruct;\\n}\\n```\\n

Impact:

Recommended mitigation:
Remove the line that invokes `selfdestruct` and consider changing internal state so that future calls to `delegateTokens` always revert.
Vulnerability: Balance of msg.sender Is Used Instead of the from Address
Severity: low

Description:
The `TransferHelper` library has methods that allow transferring tokens directly or on behalf of a different wallet that previously approved the transfer. Those functions also check the sender balance before conducting the transfer. In the second case, where the transfer happens on behalf of someone the code is checking not the actual token spender balance, but the `msg.sender` balance instead.\\n```\\nfunction transferTokens(\\n address token,\\n address from,\\n address to,\\n uint256 amount\\n) internal {\\n uint256 priorBalance = IERC20(token).balanceOf(address(to));\\n require(IERC20(token).balanceOf(msg.sender) >= amount, 'THL01');\\n```\\n

Impact:

Recommended mitigation:
Use the `from` parameter instead of `msg.sender`.