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

1

User stakes 100 PLUME to Validator A

2

User unstakes 100 PLUME → enters cooldown period

3

Time passes → cooldown matures

4

Before the user calls any processing function, current state:

5

When the user calls totalAmountWithdrawable, the result is 0.

6

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

1

User stakes 100 PLUME to Validator A

2

User unstakes 100 PLUME → enters cooldown period

3

Time passes → cooldown matures

4

Before the user calls any processing function, current state:

5

When the user calls totalAmountWithdrawable, the result is 0.

6

When the same user calls amountWithdrawable, it includes matured cooldown amounts and returns 100.

Was this helpful?