53051 sc high unconsented stakeonbehalf enables third party gas griefing dos by bloating uservalidators breaking withdraw claimall
Submitted on Aug 14th 2025 at 18:10:58 UTC by @tansegv for Attackathon | Plume Network
Report ID: #53051
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
Theft of gas
Unbounded gas consumption
Description
Because anyone can stakeOnBehalf(staker) and permanently append distinct validators to a victim’s userValidators, and many public flows iterate that array, an attacker can force the victim’s operations (withdrawals, claims, restakes) to consume unbounded gas and revert, freezing access to funds.
stakeOnBehalf lets any external account fund a stake for someone else. The first stake with a validator appends that validator ID to the victim’s $.userValidators. Core user actions (withdraw, claimAll, reward calculations, cooldown processing) loop over this entire list. A malicious actor can “spray” the victim across many validators with tiny stakes (≥ minStake), inflating loop work until calls OOG/revert, causing a liveness DoS of user funds.
Vulnerability Details
Unconsented association & list growth
// StakingFacet.sol (L428)
function stakeOnBehalf(uint16 validatorId, address staker) external payable returns (uint256) {
...
_performStakeSetup(staker, validatorId, msg.value); // no consent check
}// PlumeValidatorLogic.sol (L58-L66)
function addStakerToValidator(..., address staker, uint16 validatorId) internal {
if (!$.userHasStakedWithValidator[staker][validatorId]) {
$.userValidators[staker].push(validatorId); // grows victim's list
$.userHasStakedWithValidator[staker][validatorId] = true;
}
...
}Hot paths iterate
$.userValidators[user]Cooldown & withdraw:
// StakingFacet.sol (L855)
uint16[] memory userAssociatedValidators = $.userValidators[user];
for (uint256 i = 0; i < userAssociatedValidators.length; i++) { ... }
// called by withdraw() (L395) and restakeRewards() (L451)Rewards & claims:
// RewardsFacet.sol (L537)
uint16[] memory validatorIds = $.userValidators[user];
for (uint256 i = 0; i < validatorIds.length; i++) { ... } // used by claimAll()Pruning is hard to trigger Removal from
$.userValidatorshappens only if no active stake, no active cooldown, and no pending rewards for that validator (PlumeValidatorLogic.solL113–L141). An attacker who keeps tiny stakes outstanding (and never unstakes) effectively pins those entries.Asymmetry Attacker cost per validator =
minStakeAmount(configurable; can be very small). Victim pays ongoing extra gas (O(N)) forever. With enough validators, the loops exceed the block gas limit → revert.
Impact Details
Primary impact: Liveness DoS of user funds — the victim cannot successfully:
withdraw()parked native (after cooldown processing),claimAll()rewards,restakeRewards(...), whenever looped processing runs out of gas due to inflateduserValidators.
Scope: Per-user targeted; no privileged roles required.
Persistence: Lasts as long as attacker’s tiny stakes remain ≥
minStakeAmount. Victim could try to unwind validator-by-validator, but that’s expensive and time-consuming (cooldowns, per-validator interactions), while the DoS persists during each attempt.Economics: If
minStakeAmountis small, an attacker can cheaply create hundreds of entries. Even with moderateminStakeAmount, this is a griefing vector (attacker spends once; victim pays gas tax forever).
Proof of Concept
Was this helpful?