52960 sc insight incosistent withdrawable amount calculations
Submitted on Aug 14th 2025 at 13:42:08 UTC by @holydevoti0n for Attackathon | Plume Network
Report ID: #52960
Report Type: Smart Contract
Report severity: Insight
Target: https://github.com/immunefi-team/attackathon-plume-network/blob/main/plume/src/facets/StakingFacet.sol
Impacts:
Contract fails to deliver promised returns, but doesn't lose value
Description
Brief/Intro
In StakingFacet, the amountWithdrawable() function returns matured cooldowns that haven't been processed, while totalAmountWithdrawable() only returns processed parked funds, creating inconsistent balance reporting.
Vulnerability Details
The two conflicting functions for the withdrawable returns are totalAmountWithdrawable and amountWithdrawable.
Notice the global calculation returns directly the totalWithdrawable variable:
https://github.com/immunefi-team/attackathon-plume-network/blob/580cc6d61b08a728bd98f11b9a2140b84f41c802/plume/src/facets/StakingFacet.sol#L553-L555
function totalAmountWithdrawable() external view returns (uint256 amount) {
return PlumeStakingStorage.layout().totalWithdrawable;
}Now the amountWithdrawable function considers the matured cooldowns for the user:
https://github.com/immunefi-team/attackathon-plume-network/blob/580cc6d61b08a728bd98f11b9a2140b84f41c802/plume/src/facets/StakingFacet.sol#L520-L522
The global totalWithdrawable state variable is only updated when funds are explicitly moved to parked state via these functions:
https://github.com/immunefi-team/attackathon-plume-network/blob/580cc6d61b08a728bd98f11b9a2140b84f41c802/plume/src/facets/StakingFacet.sol#L731
Problem is cooldowns become withdrawable immediately when they mature (block.timestamp >= cooldownEndTime), but the global state is not updated until:
User calls
withdraw()→ triggers_processMaturedCooldowns()User calls
unstake()→ triggers_processCooldownLogic()User calls
restake()→ triggers_processMaturedCooldowns()
This causes the amount to withdraw to be inconsistent when calling the withdrawable functions.
Example
User stakes 100 PLUME to Validator A
User unstakes 100 PLUME → enters cooldown period
Time passes → cooldown matures
Before the user calls any processing function, current state:
When the user calls totalAmountWithdrawable, the result is 0.
When the same user calls amountWithdrawable, it includes matured cooldown amounts and returns 100.
This discrepancy happens because totalAmountWithdrawable does not account for recently matured cooldowns, so it will always be outdated compared to amountWithdrawable.
Impact Details
Two view functions return different withdrawable amounts for the same user, breaking integrations that rely on consistent balance data.
Recommendation
totalAmountWithdrawable never reflects the actual up-to-date withdrawable amount, it only includes parked amounts. Either update it to reflect the real total withdrawable amount or rename it to something more accurate, such as totalParkedAmount.
Proof of Concept
User stakes 100 PLUME to Validator A
User unstakes 100 PLUME → enters cooldown period
Time passes → cooldown matures
Before the user calls any processing function, current state:
When the user calls totalAmountWithdrawable, the result is 0.
When the same user calls amountWithdrawable, it includes matured cooldown amounts and returns 100.
Was this helpful?