For the complete documentation index, see llms.txt. This page is also available as Markdown.

52573 sc high unconsented stakeonbehalf enables unbounded gas consumption via uservalidators growth causing dos at scale in claimall withdraw

Submitted on Aug 11th 2025 at 17:55:42 UTC by @wylis for Attackathon | Plume Network

  • Report ID: #52573

  • Report Type: Smart Contract

  • Report severity: High

  • Target: https://github.com/immunefi-team/attackathon-plume-network/blob/main/plume/src/facets/StakingFacet.sol

  • Impacts: Unbounded gas consumption

Description

Inro

Anyone can call stakeOnBehalf() with ≥ minStakeAmount, which forcibly appends validatorId to userValidators[] without the user’s consent. Core user flows iterate this array, so the attacker can arbitrarily increase a victim’s gas costs. In our test, pushing a user from 1 → 15 validators raised claimAll() gas from 68,613 to 395,829 (~5.77×). At higher validator counts the same paths can hit block gas limits, turning this from a cost-imposition into a functional DoS.

Vulnerability Details

Root cause: stakeOnBehalf records stake for an arbitrary user and adds that validator to userValidators[user] with no beneficiary approval. Gas-critical code paths iterate that array:

  • RewardsFacet

    • claimAll(): for each reward token, calls an internal that loops userValidators[msg.sender] to settle rewards across all validators.

    • claim(): Single-token version, but still loops over validators for that token.

  • StakingFacet

    • withdraw(): calls _processMaturedCooldowns() which loops userValidators[user] to move matured cooldown balances.

    • restakeRewards(): also triggers _processMaturedCooldowns(user).

    • amountWithdrawable(): uses _calculateTotalWithdrawableAmount() which loops userValidators[user] to sum matured cooldowns.

Complexity:

  • claimAll() ≈ O(#rewardTokens × #userValidators)

  • Other API ≈ O(#userValidators)

Attacker control: Attacker repeats stakeOnBehalf() for each validator ID (enumerated via getValidatorsList()), paying only minStakeAmount each time. This permanently bloats userValidators[] until the victim performs gas-heavy clean-up.

Economics: Grief cost upper bound = minStakeAmount × validatorCount (attacker capital stays staked as the victim’s balance). With low minStakeAmount or many validators, this is practical.

Impact Details

  • Primary (defensible): High — Unbounded gas consumption The attacker can arbitrarily increase userValidators[]. Gas scales linearly across multiple core functions.

  • Potential escalation (evidence-dependent): High — Temporary freezing of funds (≥ 24h) APIs (e.g. claimAll()) could exceed block gas limits at high parameters (e.g., 50–100 validators). The victim could not complete normal flows.

  • Not claimed (impact tied to slashing): Though it does not expose attacker-controlled loss of a victim’s existing funds, it should be noted that victim's gifted funds are exposed to slashing and not necessarily available to them.

https://gist.github.com/wylis-hodor/a183e7d1b051c1a2cd30b89178f88fee

Proof of Concept

1

Setup

  • Deploy/initialize as in project tests (e.g. 15 validators; 1 reward token).

  • Record baseline gas for the victim calling claimAll() once (~68,613).

2

Attack

  • Attacker enumerates validators with getValidatorsList().

  • For each ID, attacker calls stakeOnBehalf(vid, victim) sending exactly minStakeAmount.

  • Confirm validators.getUserValidators(victim).length increased to the validator count.

3

Impact

  • Victim calls claimAll().

  • Observe gas increase with e.g. 15 validators (5.8×). Gas increased to ~395,829.

Note: Attack scales with validator count toward approaching/exceeding block gas limits.

References

  • Target file: https://github.com/immunefi-team/attackathon-plume-network/blob/main/plume/src/facets/StakingFacet.sol

  • PoC gist: https://gist.github.com/wylis-hodor/a183e7d1b051c1a2cd30b89178f88fee

Was this helpful?