57813 sc critical transfer recipients will pay unwarranted emergency withdrawal penalties for share positions they legitimately own

Submitted on Oct 29th 2025 at 01:41:03 UTC by @Lavender88 for Audit Comp | Belongarrow-up-right

  • Report ID: #57813

  • Report Type: Smart Contract

  • Report severity: Critical

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

  • Impacts: Griefing (e.g. no profit motive for an attacker, but damage to the users or the protocol)

triangle-exclamation

Description

Brief / Intro

The missing stake tracking updates on ERC20 transfers will cause a 10% penalty loss for share recipients: they cannot use normal withdrawal even after waiting the full minStakePeriod because the stakes[] array remains empty for transferred shares.

Vulnerability Details

In Staking.sol, the ERC20 transfer() function inherited from Solady's base implementation does not update the stakes[] mapping. When shares are transferred:

  • balanceOf[] correctly updates (sender decreases, recipient increases)

  • stakes[] does NOT update (recipient remains with empty array)

This desynchronization causes _consumeUnlockedSharesOrRevert() to fail for transfer recipients even after the lock period expires, forcing them into the emergency withdrawal path with its 10% penalty.

Preconditions / Reproduction Steps

1

1. Create a locked stake for Alice

Alice calls deposit() to create a stake entry in stakes[A] with locked shares.

2

2. Transfer shares to recipient

Alice calls transfer() to send shares to Bob, who has an empty stakes[B] array.

3

3. Ensure non-zero lock period

minStakePeriod is set to a non-zero value (e.g., default 1 day).

Attack Path (example scenario)

Scenario: Innocent Transfer Recipient (Bob) Gets Griefed

  • Alice deposits 1000 LONG tokens via deposit(), receiving 1000 shares stakes[alice] = [{shares: 1000, timestamp: T}] balanceOf[alice] = 1000

  • Alice transfers 1000 shares to Bob via standard ERC20 transfer() balanceOf[alice] = 0 balanceOf[bob] = 1000 stakes[alice] = unchanged [{shares: 1000, timestamp: T}] stakes[bob] = [] (empty)

  • Bob waits full lock period (1 day passes) and calls redeem(1000, bob, bob) Execution reaches _consumeUnlockedSharesOrRevert(bob, 1000) which loops through stakes[bob] (empty) => remaining = 1000 => REVERTS with MinStakePeriodNotMet().

  • Bob is forced to call emergencyRedeem(1000, bob, bob) which uses _removeAnySharesFor() and does not check stakes[] properly. Bob pays 10% penalty: 100 LONG sent to treasury, receives 900 LONG.

Result: Bob loses 100 LONG (10% of his position) despite legitimately holding shares for the required duration.

Impact Details

Transfer recipients suffer an unwarranted 10% loss when attempting to withdraw shares they legitimately own. Examples of affected recipients:

  • DEX purchasers

  • Gift/transfer recipients

  • Liquidity provision reward recipients

This is griefing: damage to users without profit motive for the attacker. Treasury receives unintended penalty funds.

Mitigation

Two suggested mitigations (as proposed by the reporter):

  • Option 1: Update stakes[] on ERC20 transfers to keep stake metadata in sync with balanceOf[].

  • Option 2: Disable transfers entirely (preventing transfer-based desynchronization).

Proof of Concept

References

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

Was this helpful?