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
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.
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.
validatorExitBlockrecords 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:
When
requestDelegationExitwas called while the validator was still treated as active (decrease for completedPeriods + 2).Again in
_delegatebecause the validator is nowEXITED(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
ACTIVEstatus until period end; after the period ends its status becomesEXITED.
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:
Deploy mocks and contracts (ProtocolStakerMock, StargateNFTMock, VTHO token, etc.)
Set validators and statuses to ACTIVE.
Mint NFTs to users representing stake tokens.
user1 delegates (pending -> active after period).
user2 delegates to same validator (pending -> active after period).
user1 calls
requestDelegationExitwhile validator still considered active;_updatePeriodEffectiveStakedecreases future effective stake once.Validator signals exit (still active until period ends).
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,
_delegatedecreases the effective stake again for the previous validator — causing a double-decrease for the same delegator.
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
_delegatewhen the delegation already performed a scheduled decrease, or track whether a decrease was applied for the period), orConvert the provided POC test into a smaller unit test focused on the exact logic path (isolated reproduction). Which would you prefer?
Was this helpful?