57485 sc medium emergencywithdraw cost more penalty than expected

Submitted on Oct 26th 2025 at 16:09:18 UTC by @ox9527 for Audit Comp | Belongarrow-up-right

  • 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() + 1 time 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

circle-exclamation

Proof of Concept

chevron-rightPoC Test (expand to view)hashtag

```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?