Immunefi Audit Competitions
Plume | Attackathon

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

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 / FunctionOutcome
Staked PLUME principalPermanent freeze — withdraw() always reverts
Accrued native rewardsUnclaimable

Recommendation

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"
);

Rescue mechanism

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

Fail-safe escrow

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

Wrap native token

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

Proof of Concept

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

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

Unstake after some time; wait for cooldown to mature.

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

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