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-182If 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
Staked PLUME principal
Permanent freeze — withdraw() always reverts
Accrued native rewards
Unclaimable
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"
);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.
Was this helpful?