57423 sc medium unbounded gas consumption in emergency redemption enables low cost dos against staking vault users

Submitted on Oct 26th 2025 at 04:14:32 UTC by @InquisitorScythe for Audit Comp | Belongarrow-up-right

  • Report ID: #57423

  • Report Type: Smart Contract

  • Report severity: Medium

  • Target: https://github.com/immunefi-team/audit-comp-belong/blob/main/contracts/v2/periphery/Staking.sol

  • Impacts:

    • Unbounded gas consumption

Description

Brief / Intro

The Staking contract allows users to deposit tokens and redeem them, including via emergency flows. However, the contract's design permits any user to create an unbounded number of stake entries for any receiver. This results in emergency redemption/withdrawal operations for the victim becoming O(n) in gas cost, where n is the number of stake entries. An attacker can exploit this by creating thousands of small stake entries for a victim, making emergency exits infeasible due to gas limits, and thus locking the victim's funds with negligible cost.

Vulnerability Details

  • The contract inherits ERC4626 and exposes deposit/mint functions that allow any caller to specify the receiver address.

  • In the Staking contract, each deposit for a receiver appends a new Stake entry to stakes[receiver]:

    function _deposit(address by, address to, uint256 assets, uint256 shares) internal override {
        super._deposit(by, to, assets, shares);
        stakes[to].push(Stake({shares: shares, timestamp: block.timestamp}));
    }

    Source: https://github.com/belongnet/checkin-contracts/blob/22d92a3af433a1cf4d0aa758f872c887b2f33db8/contracts/v2/periphery/Staking.sol#L242-L246

  • Emergency flows (emergencyRedeem, emergencyWithdraw) call _emergencyWithdraw, which invokes _removeAnySharesFor. This function iterates over all stake entries for the victim, performing swap-and-pop removals and multiple SSTORE operations per entry:

    function _removeAnySharesFor(address staker, uint256 shares) internal {
        Stake[] storage userStakes = stakes[staker];
        uint256 remaining = shares;
        for (uint256 i; i < userStakes.length && remaining > 0;) {
            uint256 stakeShares = userStakes[i].shares;
            if (stakeShares <= remaining) {
                remaining -= stakeShares;
                userStakes[i] = userStakes[userStakes.length - 1];
                userStakes.pop();
            } else {
                userStakes[i].shares = stakeShares - remaining;
                remaining = 0;
                unchecked { ++i; }
            }
        }
    }

    Source: https://github.com/belongnet/checkin-contracts/blob/22d92a3af433a1cf4d0aa758f872c887b2f33db8/contracts/v2/periphery/Staking.sol#L296-L315

  • The attacker can repeatedly call mint(1, victim) or deposit(1, victim) to create thousands of stake entries for the victim at minimal cost (1 token per entry when A/S ≈ 1).

  • When the victim attempts to redeem or withdraw in an emergency, the contract must process all entries, causing the transaction to require O(n) gas. With enough entries, this exceeds the block gas limit, making the operation impossible in a single transaction.

Impact Details

  • Denial-of-Service (DoS): The victim is unable to perform emergency redemption or withdrawal in a single transaction, effectively locking their funds.

  • Low Attack Cost: The attacker only needs to spend a small amount of tokens (e.g., 2,000 tokens to create 2,000 entries) to lock a much larger victim balance (e.g., 100,000 tokens).

  • Gas Consumption: PoC results show baseline emergencyRedeem gas at ~130,000, but after attack, gas rises to ~24,000,000, approaching the Ethereum block gas limit.

References

  • https://github.com/belongnet/checkin-contracts/blob/22d92a3af433a1cf4d0aa758f872c887b2f33db8/contracts/v2/periphery/Staking.sol#L242-L246

  • https://github.com/belongnet/checkin-contracts/blob/22d92a3af433a1cf4d0aa758f872c887b2f33db8/contracts/v2/periphery/Staking.sol#L296-L315

Proof of Concept

Summary of the PoC test procedure:

1

Deploy and setup

  • Deploy a mock LONG token and the Staking contract.

  • Mint tokens to victim and attacker and approve staking.

  • Victim deposits a large amount (creating one large stake entry).

2

Baseline measurement

  • Estimate gas for victim calling emergencyRedeem for their shares before the attack.

3

Attack

  • Attacker repeatedly mints many 1-share entries for the victim by calling mint(1, victim) in batches, creating thousands of small stake entries credited to the victim.

  • Track attack cost in assets and attacker balance.

4

Post-attack measurement and assertion

  • Estimate gas for victim emergencyRedeem again after the attack.

  • Compare baseline vs after-attack gas estimate (expect large increase or OOG).

Full PoC test file (use yarn test test/v2/platform/StakingEmergencyDoS.poc.test.ts):

Example output (from PoC run):

Notes for Remediation (informational)

  • The root cause is unbounded growth of per-user stake entries coupled with linear-time emergency processing. Possible mitigations include:

    • Consolidate stakes for the same receiver (e.g., merge new deposits into the last stake entry when certain conditions hold).

    • Use a data structure or accounting design that avoids O(n) per-user emergency operations (e.g., aggregate balances per user instead of per-deposit entries).

    • Restrict who may specify arbitrary receivers on deposits, or add rate-limiting/anti-spam measures for crediting other accounts.

    • Introduce gas-bounded withdrawal patterns (e.g., paginated emergency withdrawals) or enable the contract owner to perform state-compacting operations.

(Do not consider the above as prescriptive code; they are high-level mitigation directions based on the observed issue.)

Was this helpful?