60004 sc high double decrease effective stake bug in unstake
Submitted on Nov 17th 2025 at 14:09:08 UTC by @jo13 for Audit Comp | Vechain | Stargate Hayabusa
Report ID: #60004
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
Description
Brief/Intro
A vulnerability in the Stargate contract’s unstake() function causes the protocol to double-decrease a delegator’s effective stake for a given validator and NFT. In realistic conditions (a user requests delegation exit, the validator later exits, and the user then calls unstake()), this double-decrease causes an arithmetic underflow inside _updatePeriodEffectiveStake, leading to a permanent denial-of-service on unstaking. Affected users are unable to withdraw their staked VET or fully realize their staking rewards, causing permanent freezing of funds and unclaimed yield unless the contract is upgraded or otherwise remediated off-chain.
Vulnerability Details
High-level description
The Stargate contract tracks delegators’ "effective stake" per validator and per period using a checkpointed mapping delegatorsEffectiveStake[validator]. This mapping is updated via the internal function _updatePeriodEffectiveStake, which (simplified):
When _isIncrease == false, this function performs currentValue - effectiveStake without any explicit guard that currentValue >= effectiveStake. In normal flows, the checkpointed value is expected to be large enough, but under certain sequences of calls the protocol applies this decrease twice for the same NFT/validator combination at future periods where the total effective stake is already 0. In Solidity 0.8.x, 0 - effectiveStake reverts with a panic due to arithmetic underflow, causing unstake() to revert.
Call flow and conditions
Two key public functions contribute to the double-decrease:
requestDelegationExit(uint256 _tokenId)unstake(uint256 _tokenId)
requestDelegationExit() path
requestDelegationExit() pathWhen a delegation is ACTIVE, requestDelegationExit signals an exit in the external ProtocolStaker and immediately decreases the delegator's effective stake at a future period:
At this point, the effective stake for that NFT/validator pair is decreased for completedPeriods + 2, which is a future period relative to the current checkpoint.
unstake() path
unstake() pathLater, after the validator has completed additional periods and may have exited, the user calls unstake(_tokenId):
The important point is that unstake() may call _updatePeriodEffectiveStake(..., false) a second time for the same NFT/validator pair at a similar future period (oldCompletedPeriods + 2). Depending on how periods are advanced between requestDelegationExit and unstake, this can target a period where the delegators’ effective stake is already fully drawn down to 0.
In that scenario, inside _updatePeriodEffectiveStake:
currentValue = $.delegatorsEffectiveStake[_validator].upperLookup(_period)returns0.effectiveStake = _calculateEffectiveStake($, _tokenId)is positive.updatedValue = currentValue - effectiveStakebecomes0 - effectiveStake, which reverts with a panic due to underflow.
Because this arithmetic occurs inside unstake(), the entire unstake operation reverts, even though the user followed a valid lifecycle.
Impact Details
In the affected flow, a user stakes VET, delegates their NFT to a validator, requests delegation exit while the delegation is active, waits until the delegation and validator are considered exited, and then calls unstake() to reclaim their principal. Due to the double-decrease of effective stake, _updatePeriodEffectiveStake underflows and causes unstake() to revert every time, resulting in:
Permanent freezing of staked funds and any unclaimed yield for that position.
A broken staking lifecycle where the protocol fails to deliver the promised ability to exit.
A safety/availability issue that can impact any regular delegator following a normal exit flow.
Potential for griefing: attackers can deliberately create conditions that push users into the unrecoverable state.
References
Core contract:
contracts/Stargate.solunstake(uint256 _tokenId)logic and call to_updatePeriodEffectiveStakewhen validator or delegation is exited.requestDelegationExit(uint256 _tokenId)logic and initial decrease of effective stake atcompletedPeriods + 2._updatePeriodEffectiveStake(StargateStorage storage $, address _validator, uint256 _tokenId, uint32 _period, bool _isIncrease)implementation.
Mocks:
contracts/mocks/ProtocolStakerMock.solUsed in tests to simulate validator statuses and completed periods.
Tests:
test/unit/Stargate/Delegation.test.tsNew regression test:
"should revert when unstaking after requesting delegation exit and validator exit".
VeChain Thor Staker implementation (for behavior reference):
builtin/staker.goandbuiltin/staker_native.goin the VeChain Thor repository (confirming behaviors ofGetValidation,GetDelegation, period handling, and exit semantics).
Proof of Concept
A dedicated unit test was added to test/unit/Stargate/Delegation.test.ts to reproduce this behavior using the ProtocolStakerMock:
Run the test:
Observed output:
This confirms that in the described scenario, unstake() always reverts, demonstrating the existence of the vulnerability.
Was this helpful?