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

1

Permanent freezing of funds

An attacker can permanently freeze all staked funds for the victim address. No on-chain recourse without a protocol upgrade or migration.

2

Unauthenticated abuse

The attack is unauthenticated: no prior deposits, approvals, or victim interaction are required to bloat their userValidators array.

3

Funds locked for core user operations

Locked state affects withdraw, restake, unstake, and reward restake because these functions iterate the bloated array and will OOG (out-of-gas) before making progress.

4

Irrecoverable without protocol change

Because the victim cannot successfully call the functions that would clear or restore their state, funds cannot be recovered without a protocol upgrade or emergency migration.

References

  • Contract source: contracts/facets/StakingFacet.sol (lines implementing stakeOnBehalf and 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

Minimal PoC demonstrating the gas-bomb DoS (expand to view contract and tests)

PoC Solidity contract (saved as PoCStakingFacet.sol in the PoC):

PoC test (saved as poc.test.js under test):

Observations from the PoC:

  • The first test reproduces an out-of-gas revert on withdraw(), demonstrating the permanent DoS.

  • The second test shows normal behavior when the victim’s list is small.

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 stakeOnBehalf for a given staker.

    • Alternatively, restrict stakeOnBehalf to 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?