60027 sc high stuck funds for the later delegators due to an edge case led to double decreasing effective stakes

Submitted on Nov 17th 2025 at 17:37:44 UTC by @rzizah for Audit Comp | Vechain | Stargate Hayabusaarrow-up-right

  • Report ID: #60027

  • Report Type: Smart Contract

  • Report severity: High

  • Target: https://github.com/immunefi-team/audit-comp-vechain-stargate-hayabusa/tree/main/packages/contracts/contracts/Stargate.sol

  • Impacts:

    • Permanent freezing of funds

    • Permanent freezing of unclaimed yield

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

Description

Brief/Intro

1

In the Stargate contract, to exit a delegation while the validator status is active the requestDelegationExit is called which invokes _updatePeriodEffectiveStake to decrease effective stakes.

2

A validator that has signaled exit remains in an active state until the period ends. The scenario:

  • A delegator of an exiting validator calls requestDelegationExit, decreasing effective stakes.

  • The period ends and the validator reaches EXITED state.

  • The delegator can re-delegate by calling delegate.

  • In delegate, the code checks whether the current validator status is EXITED and (in that case) decreases effective stakes again.

Result: the effective stakes are decreased twice for the same delegator, causing later delegators to have stuck funds (can't claim rewards, can't unstake). There is no emergency rescue; stakes and rewards can be permanently stuck.

Vulnerability Details

Context: the protocol staker is mocked in tests. Validators can have multiple statuses (Doesn't exist, Queued, Active, Went offline, SignaledExit, Exited). Focus on SignaledExit:

  • In SignaledExit, the validator is treated as active until the period ends. validatorExitBlock records the exit request block.

  • In Stargate, validators that have signaled exit are treated as active, so a delegator wanting to re-delegate must call requestDelegationExit, which removes the delegation from the validator's effective stakes.

Code reference where requestDelegationExit decreases effective stake in a future period:

Later, after the period ends and the validator becomes EXITED, when the delegator calls delegate to a new validator the code again decreases the effective stake of the previous (now-exited) validator:

This yields a double decrease:

  1. When requestDelegationExit was called while the validator was still treated as active (decrease for completedPeriods + 2).

  2. Again in _delegate because the validator is now EXITED (decrease for oldCompletedPeriods + 2).

The function _updatePeriodEffectiveStake computes and writes the updated effective stake using subtraction if decreasing:

delegatorsEffectiveStake is updated by pushing the new value. The double decrease can cause an underflow or otherwise corrupt expected values used during reward calculations.

The claim logic calls _claimableRewardsForPeriod (via _claimableRewards -> _claimRewards) during delegate and unstake. If delegatorsEffectiveStake gets corrupted (e.g., underflow/overflow), claims can revert, leaving delegations stuck.

References in code where claim is called inside unstake and delegate:

  • Claim inside unstake: https://github.com/vechain/stargate-contracts/blob/877f294a132bf3fd9b51821c5f58b9f9e91c60c1/packages/contracts/contracts/Stargate.sol#L303-L304

  • Claim inside delegate: https://github.com/vechain/stargate-contracts/blob/877f294a132bf3fd9b51821c5f58b9f9e91c60c1/packages/contracts/contracts/Stargate.sol#L438-L439

Decrease effective stakes in delegate: https://github.com/vechain/stargate-contracts/blob/877f294a132bf3fd9b51821c5f58b9f9e91c60c1/packages/contracts/contracts/Stargate.sol#L407-L413

Decrease effective stakes in requestDelegationExit: https://github.com/vechain/stargate-contracts/blob/877f294a132bf3fd9b51821c5f58b9f9e91c60c1/packages/contracts/contracts/Stargate.sol#L567-L569

Impact Details

  • Permanent freezing of delegator funds: delegators cannot redelegate nor unstake; funds stuck.

  • Permanent freezing of delegator rewards: rewards cannot be claimed due to arithmetic underflow/overflow in reward calculations, causing reverts.

Proofs & Tests

The report includes small tests showing protocol staker behavior and a full POC integration test demonstrating the bug.

Notes on the protocol staker behavior:

  • Delegation mapping persists after withdrawal (delegation ID still points to original validator data).

  • A validator that has signaled exit remains in ACTIVE status until period end; after the period ends its status becomes EXITED.

Example Go test confirming mapping persistence:

Sample logs from the Go test show mapping persists after withdrawal:

Example Go test confirming validator exit-state logging:

Logs:

POC (Integration test)

The report provides a Hardhat test to reproduce the issue. Create a local.ts file under packages/config/ with the provided config (kept unchanged).

Create test/integration/DelegationExitBug.test.ts with the provided content. The test walks through:

1
  • Deploy mocks and contracts (ProtocolStakerMock, StargateNFTMock, VTHO token, etc.)

  • Set validators and statuses to ACTIVE.

  • Mint NFTs to users representing stake tokens.

2
  • user1 delegates (pending -> active after period).

  • user2 delegates to same validator (pending -> active after period).

  • user1 calls requestDelegationExit while validator still considered active; _updatePeriodEffectiveStake decreases future effective stake once.

  • Validator signals exit (still active until period ends).

3
  • Advance period so user1's delegation becomes EXITED; validator status becomes EXITED.

  • user1 then re-delegates to a different validator. Because the previous validator is now EXITED, _delegate decreases the effective stake again for the previous validator — causing a double-decrease for the same delegator.

4
  • user2 tries to unstake. The claim/unstake code runs and encounters an arithmetic overflow/underflow due to the corrupted effective stake values, causing revert and leaving user2 stuck.

Test code (full test included as provided in the original report — keep exactly as-is when running):

Run the test with:

Example log output from the test run (shows double decrease and revert at unstake):

Test result: failing unstake due to arithmetic overflow caused by double decrease.

References

  • Stargate.sol (requestDelegationExit decrease): https://github.com/vechain/stargate-contracts/blob/877f294a132bf3fd9b51821c5f58b9f9e91c60c1/packages/contracts/contracts/Stargate.sol#L567-L569

  • Stargate.sol (delegate decrease on exited validator): https://github.com/vechain/stargate-contracts/blob/877f294a132bf3fd9b51821c5f58b9f9e91c60c1/packages/contracts/contracts/Stargate.sol#L396-L414

  • _updatePeriodEffectiveStake implementation: https://github.com/vechain/stargate-contracts/blob/877f294a132bf3fd9b51821c5f58b9f9e91c60c1/packages/contracts/contracts/Stargate.sol#L1044-L1064

  • Claim inside unstake: https://github.com/vechain/stargate-contracts/blob/877f294a132bf3fd9b51821c5f58b9f9e91c60c1/packages/contracts/contracts/Stargate.sol#L303-L304

  • Claim inside delegate: https://github.com/vechain/stargate-contracts/blob/877f294a132bf3fd9b51821c5f58b9f9e91c60c1/packages/contracts/contracts/Stargate.sol#L438-L439


If you want, I can:

  • Propose a minimal code patch to prevent the double-decrease (e.g., avoid decreasing in _delegate when the delegation already performed a scheduled decrease, or track whether a decrease was applied for the period), or

  • Convert the provided POC test into a smaller unit test focused on the exact logic path (isolated reproduction). Which would you prefer?

Was this helpful?