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

52870 sc low cooldown extension logic may lead to locked funds

Submitted on Aug 13th 2025 at 20:11:57 UTC by @EFCCWEB3 for Attackathon | Plume Network

  • Report ID: #52870

  • Report Type: Smart Contract

  • Report severity: Low

  • 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 _processCooldownLogic function in the StakingFacet contract aggregates multiple unstaking amounts from the same validator into a single cooldown entry and resets the cooldownEndTime to block.timestamp + cooldownInterval for the entire aggregated amount. As a result, subsequent unstakes (before prior cooldowns mature) extend the cooldown for previously unstaked funds. In production this can delay user withdrawals beyond the expected cooldown period (for example, 7 days), increase exposure to slashing risk, and cause user confusion because the documentation does not explicitly mention this per-validator aggregation behavior.

Vulnerability Details

PlumeStaking stores per-user, per-validator cooldown as a single CooldownEntry in PlumeStakingStorage.userValidatorCooldowns[user][validatorId], containing amount and cooldownEndTime. When a user unstakes multiple times from the same validator while a previous cooldown is still active, _processCooldownLogic:

  • Adds the new unstake amount to the existing amount in the same cooldown entry, and

  • Resets cooldownEndTime for the entire aggregated amount to block.timestamp + cooldownInterval.

Thus, previously-unstaked funds whose cooldowns were approaching maturity now get extended to the new timestamp. The docs note cooldownInterval > maxSlashVoteDuration but do not disclose this aggregation & reset behavior.

Relevant code:

When a cooldown is active (block.timestamp < currentCooldownEndTimeInSlot), the function aggregates the new amount and resets cooldownEndTime for the total, thereby extending the cooldown for previously unstaked funds.

Other relevant functions:

Documentation states: “unstaking initiates a cooldown period” with cooldownInterval > maxSlashVoteDuration to protect cooling funds from slashing, but it does not mention that multiple unstakes aggregate and reset the per-validator cooldown entry.

Impact Details

Funds already in a cooldown can be locked longer than the expected cooldownInterval because a subsequent unstake (before maturity) resets the cooldown for the aggregated amount. For example, an additional unstake 1 hour after the first will extend the cooldown by ~1 hour for the earlier funds; repeated unstakes could extend the cooldown by days. This increases temporary exposure to slashing risk and may confuse users expecting earlier availability. Impact class: Temporary freezing of funds for at least 24 hours (not permanently lost).

Proof of Concept

1

Setup State

  • cooldownInterval = 7 days (604,800 seconds)

  • maxSlashVoteDuration < 7 days (per docs)

  • Alice has 100 PLUME staked on Validator 1 (userValidatorStakes[Alice][1].staked = 100)

  • userValidatorCooldowns[Alice][1] is empty (amount = 0, cooldownEndTime = 0)

  • block.timestamp = 1000

2

Assumptions

  • Validator 1 is not slashed (validators[1].slashed = false)

  • userValidators[Alice] = [1]

3

Execution — Alice Unstakes 100 PLUME

Alice calls: unstake(validatorId=1, amount=100)

Validations / actions performed:

  • Validator exists & not slashed → _validateValidatorForUnstaking

  • amount = 100 is valid and within userValidatorStakes[Alice][1].staked = 100

  • _updateUnstakeAmounts(Alice, 1, 100) reduces staked balances accordingly

  • _processCooldownLogic(Alice, 1, 100):

    • currentCooledAmountInSlot = 0

    • currentCooldownEndTimeInSlot = 0

    • No active cooldown → finalNewCooledAmountForSlot = 100

    • newCooldownEndTime = 1000 + 604800 = 605800

  • userValidatorCooldowns[Alice][1] becomes { amount: 100, cooldownEndTime: 605800 }

  • _updateCoolingAmounts(Alice, 1, 100) increments cooled totals

  • Emits: CooldownStarted(Alice, 1, 100, 605800)

4

Alice Unstakes 1 PLUME Before Maturity

  • At block.timestamp = 6060 (1 hour later) Alice calls: unstake(validatorId=1, amount=1) (assuming 1 PLUME was re-staked)

Processing:

  • currentCooledAmountInSlot = 100

  • currentCooldownEndTimeInSlot = 605800

  • Since block.timestamp = 6060 < 605800, cooldown is active

  • _processCooldownLogic:

    • Adds to existing cooldown: finalNewCooledAmountForSlot = 100 + 1 = 101

    • newCooldownEndTime = 6060 + 604800 = 610860

  • _updateCoolingAmounts(Alice, 1, 1) increments cooled totals

  • userValidatorCooldowns[Alice][1] becomes { amount: 101, cooldownEndTime: 610860 }

  • Emits: CooldownStarted(Alice, 1, 1, 610860)

Result:

  • The original 100 PLUME (expected to mature at t = 605800) is now locked until t = 610860 → ~6 days, 23 hours longer.

  • If Alice continues to unstake periodically, the cooldown can be extended further (example: unstaking every hour, or again after 7 days could push maturity further).

References

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

(See code excerpts above for the relevant functions.)

Was this helpful?