51201 sc low contracts without payable entry points cannot withdraw nor claim rewards

Submitted on Jul 31st 2025 at 22:03:36 UTC by @jovi for Attackathon | Plume Network

  • Report ID: #51201

  • Report Type: Smart Contract

  • Report severity: Low

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

  • Impacts:

    • Permanent freezing of funds

    • Theft of unclaimed yield

Description

Brief / Intro

A missing “is-payable” check on stake admission means any non-payable contract can become a staker. Later, every native-coin transfer to that address reverts, freezing both yield and unstake withdraw claims.

Vulnerability Details

Both reward and principal payouts are executed with raw native transfers:

(bool success, ) = user.call{value: amountToWithdraw}("");
if (!success) revert NativeTransferFailed();   // StakingFacet.sol#L416-419
(bool success, ) = recipient.call{value: amount}("");
if (!success) revert PlumeTransferFailed(recipient, amount); // PlumeStakingRewardTreasury.sol#L179-182

If the staker address is a smart contract without a payable receive() or fallback(), these calls always revert. The protocol nevertheless lets such contracts open stakes, so:

  • Rewards accumulate but can never be claimed.

  • Unstaked principal moves to the parked/withdrawable pool but withdraw() reverts.

The user’s rewards and entire stake end up locked permanently.

Impact

Asset / Function
Outcome

Staked PLUME principal

Permanent freezewithdraw() always reverts

Accrued native rewards

Unclaimable

Recommendation

1

Admission guard

Introduce an admission guard when someone opts to stake. Make sure to also have reentrancy protection so that the caller cannot exploit this mechanism. The 1 wei call ensures the staker is able to receive native amounts.

require(msg.value > 0, "Msg.value must be bigger than 1");
require(
    staker.code.length == 0 || address(staker).call{value:1}(""),
    "NON_PAYABLE_STAKER"
);
2

Rescue mechanism

Allow one-time designation of an alternate payable address if a native transfer fails.

3

Fail-safe escrow

Wrap calls in try/catch; credit failed amounts to an escrow users can sweep with an EOA.

4

Wrap native token

Consider wrapping the native token so that users receive back an ERC20 version of it.

Proof of Concept

1

Deploy a dummy contract without receive()/fallback().

2

Stake PLUME to any validator, using the dummy contract as staker.

3

Unstake after some time; wait for cooldown to mature.

4

Call withdraw() → tx reverts with NativeTransferFailed; principal remains trapped.

5

(Optional) Call claimRewards() before unstaking; observe identical revert for rewards.

Was this helpful?