57685 sc critical vulnerabilities in the design of the token s staking mechanism resulted in financial harm to users involved in transfer related operations
Submitted on Oct 28th 2025 at 06:47:47 UTC by @queen for Audit Comp | Belong
Impacts: Vulnerabilities in the design of the token's staking mechanism resulted in financial harm to users involved in transfer-related operations.
Description
Brief/Intro
The Staking.sol ERC4626 vault mints and allows transfer of ERC4626 shares while lock metadata (per-deposit stakes) is tracked separately by address. When a user transfers unlocked shares to another address, the recipient receives ERC4626 shares but no corresponding lock entries. As a result the recipient cannot perform normal withdrawals (which require consuming unlocked stakes) and is forced to use the emergency withdrawal path, incurring a penalty. This leads to direct economic loss and breaks expected unlock semantics.
On normal withdrawal, the contract consumes “unlocked shares” from stakes[owner] and reverts if locks aren’t satisfied:
If a user transfers ERC4626 shares (permitted by ERC4626/solady ERC20 implementation), the contract does not migrate the corresponding stakes[from] entries to stakes[to]. The recipient thus owns shares without any lock entries. When they attempt a standard withdrawal, _consumeUnlockedSharesOrRevert(to, ...) finds no unlocked stake entries and reverts. The only available path becomes the emergency withdrawal, which always applies a penalty:
This occurs even if the transferred shares were already fully unlocked at the sender address. Unlock state is tied to stakes[owner] and does not follow share transfers.
Root cause:
ERC4626 shares are transferable.
No transfer hook overrides to block transfers or migrate stakes on transfer/transferFrom.
Withdrawal logic requires lock metadata on the current owner, not on the original depositor.
Impact Details
Forced penalty loss: Users who wait out the minimum stake period and then transfer their unlocked shares to another wallet will be unable to perform a standard withdrawal and will incur the emergency withdrawal penalty (default 10%), causing direct economic loss.
UX/DoS of normal withdrawal: Recipients of transferred shares are effectively denied the standard withdraw path due to missing lock entries, despite holding shares that semantically should be unlocked.
Griefing / integration risk: Any workflow that moves shares between wallets (custodial moves, account abstractions, migrations) may unintentionally convert “unlocked” assets into “penalized-only” assets.
Severity: High for affected users (loss proportional to position size × penalty). The issue undermines the advertised semantics of time-locked staking that unlocks after min period.
Concrete scenario
1
Depositor stakes and waits
User A deposits LONG and waits past minStakePeriod (all shares matured).
2
Transfer of unlocked shares
User A transfers all shares to User B (e.g., moving to a hardware wallet).
3
Normal withdrawal fails for recipient
User B attempts to withdraw; _consumeUnlockedSharesOrRevert(B, …) finds no unlocked entries (there are none), so it reverts.
4
Emergency path imposes penalty
User B must use emergency withdrawal and loses penaltyPercentage of assets.
References (code pointers)
Lock state tracking and deposit hook:
Normal withdraw requires unlocked stakes; otherwise revert:
Emergency withdrawal applies penalty:
Suggested remediations
Do not add any behavior or code beyond what's suggested here — these are remediation patterns only.
Option A — Make shares non-transferable: override ERC20 transfer hooks (transfer / transferFrom) to revert, preserving simple per-address lock semantics.
Option B — Migrate stake metadata on transfers: implement logic in transfer hooks to split/merge/copy stake entries so that lock timestamps follow shares. This must correctly handle partial transfers and gas implications (stake array management, consolidation).
Option C — Change withdrawal semantics: allow consumption of unlocked shares based on on-chain share ownership and original deposit timestamps (more complex; requires design to prevent circumvention).
Consider documenting the exact semantics for integrators and users (e.g., "shares are transferable but lock metadata is per-address" is surprising and dangerous).
Proof of Concept
Run with Foundry:
Place as test/StakingTransferPoC.t.sol
forge test -m test_TransferUnlockedSharesBreaksWithdrawForRecipient