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 | Belong
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)ordeposit(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
emergencyRedeemgas 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:
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?