51860 sc high missing access control in stakeonbehalf lets anyone bloat another user s validator list leading to permanent fund lock via gas exhaustion dos
Submitted on Aug 6th 2025 at 10:01:07 UTC by @manvi for Attackathon | Plume Network
Report ID: #51860
Report Type: Smart Contract
Report severity: High
Target: https://github.com/immunefi-team/attackathon-plume-network/blob/main/plume/src/facets/StakingFacet.sol
Impacts:
Permanent freezing of funds
Unbounded gas consumption
Description
Brief/Intro
The stakeOnBehalf(uint16 validatorId, address staker) function in StakingFacet.sol lacks any access control, allowing an attacker to repeatedly push tiny stakes on behalf of an arbitrary victim address. Each call appends a new entry to the victim’s internal userValidators[] array; later, any call by the victim to withdraw, restake, or unstake loops over that now-bloated array until it runs out of gas and reverts—permanently locking the victim’s funds on mainnet.
Vulnerability Details
The function stakeOnBehalf(uint16 validatorId, address staker) is missing authorization.
function stakeOnBehalf(uint16 validatorId, address staker) external payable returns (uint256) {
// no check that msg.sender == staker or is approved
uint256 stakeAmount = msg.value;
_performStakeSetup(staker, validatorId, stakeAmount);
emit Staked(staker, validatorId, stakeAmount, 0, 0, stakeAmount);
emit StakedOnBehalf(msg.sender, staker, validatorId, stakeAmount);
return stakeAmount;
}Any external account can call this and credit any staker address with a new stake. There is no signature requirement or whitelist restricting who may stake on behalf of whom.
Unbounded growth of the victim’s validator list:
Inside _performStakeSetup, every new “on-behalf” stake calls:
PlumeValidatorLogic.addStakerToValidator($, staker, validatorId);
This function appends validatorId to the dynamic array userValidators[staker] whenever it’s not already present. An attacker can repeat calls (using distinct validatorId values) to make userValidators[victim] arbitrarily large, at a very low cost for entry.
Core user operations traverse all entries in userValidators[msg.sender]. Once ids.length multiplied by per-iteration gas exceeds the block limit, every call to withdraw(), restake(), unstake() etc will run out of gas before doing any state change and the victim can never clear or shrink that array.
Impact Details
References
Contract source:
contracts/facets/StakingFacet.sol(lines implementingstakeOnBehalfand the loops in_processMaturedCooldowns/withdraw)Diamond Standard (EIP-2535): https://eips.ethereum.org/EIPS/eip-2535
Gas-bomb DoS discussions: https://blog.openzeppelin.com/stop-using-solidity-dynamic-arrays/
Proof of Concept
Mitigation / Suggested Fixes (not added by reporter)
Require authorization on
stakeOnBehalf:Allow only the staker or an approved actor (e.g., via signature-based permit, allowance, or an explicit approval mapping) to call
stakeOnBehalffor a given staker.Alternatively, restrict
stakeOnBehalfto whitelisted contracts or roles that are explicitly trusted.
Avoid unbounded user-supplied dynamic arrays as the sole source of truth for iterating critical operations. Consider:
Storing mappings for O(1) checks and operations and avoid iterating the entire user-provided list on critical user flows.
Using pagination/limits or off-chain indexing with on-chain checkpoints to prevent single-call iteration over unbounded arrays.
Add defensive checks to withdraw/restake/unstake flows to avoid gas exhaustion (e.g., process only a bounded number of entries per transaction and allow continuation).
Consider mechanisms to allow victims to prune malicious entries (e.g., a function that lets a user remove entries after proving ownership or via coordinated governance recovery) — though this may require careful access control design.
Note: Do not add any changes to the PoC or repository beyond the above recommendations in this report.
Was this helpful?