53056 sc low native withdraw to msg sender only non payable contract stakers cannot withdraw permanent funds lock
Submitted on Aug 14th 2025 at 18:24:58 UTC by @tansegv for Attackathon | Plume Network
Report ID: #53056
Report Type: Smart Contract
Report severity: Low
Target: https://github.com/immunefi-team/attackathon-plume-network/blob/main/plume/src/facets/StakingFacet.sol
Impacts:
Permanent freezing of funds
Description
withdraw() hardcodes the payout address to msg.sender and requires the native transfer to succeed. Contract wallets without a payable receive/fallback cannot accept native PLUME, causing withdraw() to revert and making matured funds unreachable.
The staking system pays out matured “parked” PLUME by sending native value to msg.sender. For contract-based stakers that lack a payable entry point, this transfer reverts, so withdraw() reverts as well. As a result, these users cannot withdraw matured funds at all.
Vulnerability Details
Hardcoded payout target: payout is always
msg.sender(the caller). NowithdrawTo(...)is offered.Raw native send with
require:
// StakingFacet.sol
function withdraw() external {
...
uint256 amountToWithdraw = $.stakeInfo[user].parked;
...
_removeParkedAmounts(user, amountToWithdraw);
_cleanupValidatorRelationships(user);
emit Withdrawn(user, amountToWithdraw);
(bool success,) = user.call{ value: amountToWithdraw }("");
if (!success) {
revert NativeTransferFailed();
}
}Why this fails for many contracts: When a contract without a payable
receive()or payablefallback()receives native value, the call reverts. Because the function requires success, the entirewithdraw()reverts, making the funds effectively inaccessible to that account.
Impact Details
Impact: Permanent freezing of funds for affected users (contract accounts lacking a payable entry point). They can accumulate parked balances but can’t withdraw them.
Blast radius: Any smart-account / contract wallet that doesn’t implement a payable receiver (or that is upgraded to drop payable receive) will be unable to exit to native.
Persistence: Indefinite until protocol adds an alternative withdrawal path (e.g., withdraw-to or wrapped token payout). State changes before the failing transfer are reverted, so accounting stays consistent—but users remain blocked from accessing their funds.
Proof of Concept
Attempt withdraw from the non-payable contract
From NonPayableStaker, call withdraw().
Internally:
(bool success,) = msg.sender.call{value: amount}("")→ reverts because there is no payable receive.The function reverts with
NativeTransferFailed(). Funds remain parked and cannot be withdrawn by this staker.
(Note: In Foundry you can vm.deal(nonPayable, X) to fund it without a payable receiver, then run the above flow and assert a revert on withdraw().)
Was this helpful?