51813 sc high malicious user can grief victims by staking them across many validators leading to fund freezing

Submitted on Aug 5th 2025 at 22:40:03 UTC by @ihtishamsudo for Attackathon | Plume Network

  • Report ID: #51813

  • Report Type: Smart Contract

  • Report severity: High

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

  • Impacts:

    • Temporary freezing of funds for at least 24 hours

Description

Brief/Intro

The Plume staking system contains a high-severity vulnerability in the stakeOnBehalf function that enables attackers to perform economical griefing attacks against users. By staking minimal amounts on behalf of victims across many validators, attackers can force victims into gas-expensive operations that make their funds practically inaccessible. This creates a temporary fund freeze lasting at least 24 hours and potentially much longer due to prohibitive recovery costs, allowing a 100 PLUME attack to effectively trap thousands of PLUME in victim accounts.

Vulnerability Details

The vulnerability involves a multi-step call flow that allows attackers to manipulate victim's validator arrays.

Attacker will execute stakeOnBehalf of victim with minimal stake amount which is 0.1 Plume as used in deployment scripts.

function stakeOnBehalf(uint16 validatorId, address staker) external payable returns (uint256) {
    //... SNIP
    uint256 stakeAmount = msg.value;
    
    bool isNewStake = _performStakeSetup(staker, validatorId, stakeAmount);

}

_performStakeSetup internally calls PlumeValidatorLogic.addStakerToValidator()

which pushes the validatorId to victim's $.userValidators[user] array:

Once the attacker has populated the victim's $.userValidators[user] array with many validators, all subsequent operations become gas-expensive:

Impacted functions

  • amountCooling() - Iterates over entire validator array to calculate cooling amounts

  • amountWithdrawable() - Iterates over entire validator array to calculate withdrawable amounts

  • getUserCooldowns() - Iterates over entire validator array to return cooldown data

  • withdraw() - Calls _processMaturedCooldowns() which iterates over all user validators

  • unstake() - Can trigger expensive operations when combined with many validators

  • _processMaturedCooldowns() - Processes cooldowns across all validators for a user

Attack path

1

Identify victim and validators

  • Attacker identifies a victim address with existing funds.

  • Attacker queries available validators (typically 1000+ exist).

2

Stake on behalf of victim across many validators

  • For each validator, attacker calls:

  • Each call triggers _performStakeSetup(victimAddress, validatorId, 0.1 ether)addStakerToValidator()$.userValidators[victim].push(validatorId).

  • Victim's validator array grows from 0 to 1000+ entries.

3

Victim attempts normal operations

  • Victim calls view/state functions which must iterate over the very large userValidators array.

  • Gas usage increases dramatically:

    • amountCooling(): from ~7,100 gas → 300,000+ gas (example 43x increase)

    • withdraw(): may exceed block gas limits

    • All view and critical functions become prohibitively expensive

Impact Details

HIGH - Temporary freezing of funds for at least 24 hours. Economical: low attacker cost (example: 100 PLUME) to cause significant disruption. Based on the provided example cost calculations, the attacker's total cost can be very low relative to victim loss/time locked.

References

1. Implement Maximum Validator Limits

Add a per-user max validator limit and check in _performStakeSetup before adding a new validator:

Proof of Concept

Click to expand the full Proof-of-Concept test and output
  • PoC test file: test/StakeOnBehalfDoSTest.t.sol

  • Test output:

Was this helpful?