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