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:
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?