57485 sc medium emergencywithdraw cost more penalty than expected
Submitted on Oct 26th 2025 at 16:09:18 UTC by @ox9527 for Audit Comp | Belong
Report ID: #57485
Report Type: Smart Contract
Report severity: Medium
Target: https://github.com/immunefi-team/audit-comp-belong/blob/main/contracts/v2/periphery/Staking.sol
Impacts: Permanent freezing of funds
Description
Brief/Intro
The penalty cost in emergencyWithdraw / emergencyRedeem depends on the user’s withdrawal amount. Each deposit must individually wait for its own unlock period, and there is no method for a user to unlock already locked shares.
Assume the following scenario:
Bob deposits 5e18 for the first time.
After
staking.minStakePeriod() + 1time has passed, Bob deposits another 5e18.At this point, the first 5e18 is already unlocked, while the second deposit remains locked.
When Bob performs emergencyWithdraw(10e18), the penalty is calculated based on the total withdrawal amount, resulting in a 1e18 penalty instead of the expected 0.5e18.
Vulnerability Details
The problematic function in Staking.sol:
This implementation does not consider whether individual stakes are unlocked or locked; it removes arbitrary shares across the user's stakes when withdrawing, which mixes locked and unlocked stakes and causes the penalty calculation for emergency withdraws to be based on the total withdrawal amount rather than only the locked portion.
Impact Details
User funds can be permanently frozen or incur larger-than-expected penalties due to locked shares being incorrectly considered when removing shares for emergency withdrawals.
Proof of Concept
PoC Test (expand to view)
```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13;
import {Test} from "forge-std/Test.sol"; import {Staking} from "../contracts/v2/periphery/Staking.sol"; import "node_modules/@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "node_modules/@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "forge-std/console2.sol";
contract Long is ERC20 { constructor() ERC20("LONG Mock", "Long") { _mint(msg.sender, 1_000_000_000_000 * 10 ** decimals()); }
} contract StakingTest is Test {
}
[PASS] test_POC_3() (gas: 395902) Logs: bob get assets: 9000000000000000000
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 6.84ms (2.07ms CPU time)
Ran 1 test suite in 142.64ms (6.84ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
Was this helpful?