50343 sc low cooldown reset vulnerability

Submitted on Jul 23rd 2025 at 20:21:31 UTC by @ciphermalware for Attackathon | Plume Network

  • Report ID: #50343

  • 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 StakingFacet.sol contract has a bug in the _processCooldownLogic function where unstaking more funds from the same validator reuses the cooldown timer for all previous unstaked funds. This keeps users who unstake in repeated transactions locked into their previously unstaked funds longer than expected, effectively extending the withdrawal time interval by as much as the total cooldown period with each additional unstake action.

Vulnerability Details

The issue lies in the _processCooldownLogic function responsible for unstaking by adding funds into a cooldown before they can be withdrawn. Each user-validator pair has a single cooldown entry (userValidatorCooldowns[user][validatorId]). When unstaking, a new end timestamp is computed as block.timestamp + cooldownInterval. If an existing cooldown is present and has already matured (block.timestamp >= currentCooldownEndTimeInSlot), the matured amount is moved to parked and a fresh cooldown is started for the current unstake amount. Otherwise (unmatured cooldown), the code sums the new amount with the existing cooled amount and reschedules the timestamp for the total accumulated amount to the new end time — effectively resetting the cooldown for all previously cooled amounts.

The relevant code excerpt:

if (currentCooledAmountInSlot > 0 && block.timestamp >= currentCooldownEndTimeInSlot) {
            // Previous cooldown for this slot has matured - move to parked and start new cooldown
            _updateParkedAmounts(user, currentCooledAmountInSlot);
            _removeCoolingAmounts(user, validatorId, currentCooledAmountInSlot);
            _updateCoolingAmounts(user, validatorId, amount);
            finalNewCooledAmountForSlot = amount;
        } else {
            // No matured cooldown - add to existing cooldown
            _updateCoolingAmounts(user, validatorId, amount);
            finalNewCooledAmountForSlot = currentCooledAmountInSlot + amount;
        }

        cooldownEntrySlot.amount = finalNewCooledAmountForSlot;
        cooldownEntrySlot.cooldownEndTime = newCooldownEndTime;

        return newCooldownEndTime;

Source: https://github.com/immunefi-team/attackathon-plume-network/blob/main/plume/src/facets/StakingFacet.sol?utm_source=immunefi#L832-L847

Example:

  • User unstakes 1 ETH at timestamp T; cooldown completes at T + 7 days.

  • 3 days later (T + 3 days), the user unstakes another 1 ETH.

  • The total amount becomes 2 ETH but the end timestamp is updated to (T + 3) + 7 = T + 10 days.

  • The first 1 ETH is now locked for 10 days instead of 7.

Suggested fix: when adding to an unmatured cooldown, use the maximum of the existing cooldownEndTime and the newly computed newCooldownEndTime, e.g.:

Impact Details

This vulnerability can temporarily freeze user funds for at least 24 hours and can extend that lock further if the user performs additional unstake actions — effectively adding more days to the withdrawal delay for previously cooled funds.

Proof of Concept

For testing the behavior, Hardhat was used with a simplified contract reproducing the cooldown logic and a test demonstrating the timestamp reset and extension.

Contract added to contracts (StakingFacetSimplified):

Test added to test (Hardhat):

Test run output:

1

Test step

Stake 2 ETH to the validator.

2

Test step

Unstake 1 ETH -> cooldown set for ~T + 7 days.

3

Test step

Advance time by 3 days (cooldown not matured).

4

Test step

Unstake another 1 ETH -> cooled amounts combine and cooldown end time is reset to ~now + 7 days, thus extending the initial batch's lock.

Notes / Recommendation

  • The fix is to avoid decreasing the cooldown end time when adding to an existing unmatured cooldown. Use the max between the existing cooldown end time and the newly calculated one (i.e., do not shorten the existing end time).

  • Keep the logic that moves matured cooled amounts to parked and starts a fresh cooldown, but when accumulating into an unmatured slot, preserve the furthest cooldown end timestamp.

(Links and code references are preserved exactly as provided.)

Was this helpful?