57008 sc critical emergencywithdraw function malfunction due to missing validation in removeanysharesfor
Submitted on Oct 22nd 2025 at 15:38:01 UTC by @Happy_Hunter for Audit Comp | Belong
Report ID: #57008
Report Type: Smart Contract
Report severity: Critical
Target: https://github.com/immunefi-team/audit-comp-belong/blob/main/contracts/v2/periphery/Staking.sol
Impacts:
Contract fails to deliver promised returns, but doesn't lose value
Description
Brief/Intro
The Staking contract's emergencyWithdraw() function does not work properly when share transfers occur because the _removeAnySharesFor() function is missing the critical validation check:
if (remaining != 0) revert MinStakePeriodNotMet()
When users transfer their sLONG shares to another address, the stake records remain in the original owner's account, creating orphaned entries. The emergencyWithdraw() function then succeeds even when called on an address with no stake records, because _removeAnySharesFor() silently returns without validating whether it successfully removed the required shares. This causes the function to process withdrawals it should reject, leaving permanent orphaned stake records that corrupt state data used by third-party integrations.
Vulnerability Details
The Staking contract has two internal functions for managing stake records:
_consumeUnlockedSharesOrRevert()- Used in normal withdrawals (SAFE):
_removeAnySharesFor()- Used in emergency withdrawals (VULNERABLE):
The _removeAnySharesFor() function is missing the validation check if (remaining != 0) revert. This means emergency withdrawals succeed even when the owner has insufficient or zero stake records, completely bypassing the lock mechanism.
Share Transfer
Alice transfers all 1000 sLONG shares to account_02 (also owned by her):
balanceOf(account_01)= 0 (updated by ERC20 transfer)balanceOf(account_02)= 1000 (updated by ERC20 transfer)stakes[account_01][0]={shares: 1000, timestamp: T}(NOT updated - remains orphaned)stakes[account_02]=[](empty - no stake records transferred)
emergencyWithdraw Execution
Alice calls emergencyWithdraw(1000, account_01, account_02) from account_01:
_removeAnySharesFor(account_02, 1000)attempts to remove stake records from account_02Loop through
stakes[account_02](empty array) - loop doesn't executeremaining = 1000(unchanged)Missing validation: function returns without checking
if (remaining != 0) revert MinStakePeriodNotMet()_burn(account_02, 1000)succeeds (account_02 has the share balance)Alice receives 900 LONG (1000 - 10% penalty)
Corrupt State
The withdrawal succeeds despite account_02 having no stake records to validate:
stakes[account_01]remains with orphaned record{shares: 1000, timestamp: T}balanceOf(account_01)= 0 (no actual shares)Third-party integrations reading
stakes[account_01]see 1000 locked shares that don't exist
Impact Details
The emergencyWithdraw() function fails to properly validate stake ownership due to the missing if (remaining != 0) revert check in _removeAnySharesFor(). When shares are transferred between addresses, stake records remain orphaned in the original owner's account while the new owner has the actual share balance but no stake entries. The _removeAnySharesFor() function silently succeeds even when it cannot find any stake records to remove, allowing emergencyWithdraw() to process withdrawals for addresses that have no locked stakes. This breaks the function's intended behavior of only allowing emergency withdrawals for properly staked positions and creates permanent orphaned stake records that cannot be cleaned up.
If there is any third-party integration that uses the stakes variable to track staked positions, locked amounts, or time-lock status, this flaw is very serious as it provides completely incorrect data where addresses show locked stakes they don't own while actual share holders show no stake records at all.
References
contracts/v2/periphery/Staking.solSolady ERC4626: https://github.com/Vectorized/solady/blob/main/src/tokens/ERC4626.sol
Recommendation
Add the missing validation to _removeAnySharesFor() by mirroring the check in _consumeUnlockedSharesOrRevert():
This enforces that emergency withdrawals can only proceed when the required shares are actually removed from the stake records, preserving the intended lock semantics and preventing orphaned stake entries.
Link to Proof of Concept
https://gist.github.com/SproutGoodHub/4b50713fe9e5396a279b064bd5116c01
Proof of Concept
Test Code
Running the Test
Test Output
Was this helpful?