For the complete documentation index, see llms.txt. This page is also available as Markdown.

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 Hayabusa

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