60334 sc high unstake permanently reverts when validator exits after delegator exit double decrease of effective stake
Submitted on Nov 21st 2025 at 13:41:46 UTC by @Diavol0 for Audit Comp | Vechain | Stargate Hayabusa
Report ID: #60334
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
Description
High‑level overview
In the Hayabusa Stargate staking protocol, a delegator’s staking position is represented by an NFT. When the user wants to fully exit, the expected flow is:
While the validator is still active, the user calls
requestDelegationExit(tokenId)to signal exit.After the delegation has effectively ended and the validator’s current period has advanced, the user calls
unstake(tokenId)to burn the NFT and withdraw their VET.
Due to the way effective stake accounting is implemented, there is a timing pattern where this flow permanently breaks:
If the user calls
requestDelegationExitwhile the validator is ACTIVE (which already decreases their effective stake for future periods), andLater the validator itself moves to
EXITEDbefore the user callsunstake,
then calling unstake(tokenId) triggers a second decrease of the same token’s effective stake. In the common case where this token was the only delegator (or the main one), this second decrease underflows and the entire unstake call reverts every time.
As a result, the user’s NFT can never be successfully unstaked through the normal interface, effectively freezing their VET principal in the protocol. There is no way for the user to recover the funds without an upgrade or administrative intervention.
This matches Immunefi’s “Permanent freezing of funds” category: user funds are not directly stolen or misallocated to another party, but the contract logic prevents the user from ever withdrawing their own stake under some timing conditions.
Root cause
The bug is in the interaction between:
How effective stake is tracked per validator and period via
_updatePeriodEffectiveStake, andWhen and how that function is called in
requestDelegationExitandunstake.
Relevant code locations:
File:
packages/contracts/contracts/Stargate.sol_updatePeriodEffectiveStake(around lines 993–1015)requestDelegationExit(around lines 523–571)unstake(around lines 231–320)
Effective stake updates
Effective stake is updated with:
Key point:
When
_isIncrease == false, the function unconditionally computescurrentValue - effectiveStakewith checked arithmetic (Solidity 0.8), so ifcurrentValue < effectiveStakeit will underflow and revert.
Where decreases happen
On delegation creation (inside
_delegate), the effective stake is increased:On
requestDelegationExit, regardless of whether the delegation is ACTIVE or PENDING, the function always performs a decrease at the end:When the delegation is ACTIVE and the validator status is
VALIDATOR_STATUS_ACTIVE, this is the only decrease, and it is correct.On
unstake, there is another conditional decrease:This path is meant to cover cases where:
The validator was EXITED, or
The delegation was still PENDING and never actually started,
so that any scheduled effective stake for future periods is cleared.
Problematic combination
The problematic scenario is when both requestDelegationExit and unstake execute their own decreases for the same delegation:
The user’s delegation is ACTIVE when
requestDelegationExit(tokenId)is called.This call immediately performs a decrease via
_updatePeriodEffectiveStake(..., false)at a certain future period (based on the validator’scompletedPeriods).Later, before the user calls
unstake, the validator’s status becomesVALIDATOR_STATUS_EXITED(via the trustedProtocolStakerlogic).When the user now calls
unstake(tokenId), theunstakefunction seescurrentValidatorStatus == VALIDATOR_STATUS_EXITEDand executes another_updatePeriodEffectiveStake(..., false)for a (typically later) future period.
Because after step 2 the effective stake has already been subtracted from the delegatorsEffectiveStake trace for all future lookup periods, the second decrease in step 4 sees currentValue == 0 from upperLookup at the chosen period, and attempts to compute:
This underflow causes the entire unstake transaction to revert every time.
Link to Proof of Concept
https://gist.github.com/6newbie/42877df1c0596306589076b13d3a8ec2
Proof of Concept
Below is a minimal PoC as a Hardhat unit test. It can be added as a new test file: packages/contracts/test/unit/Stargate/UnstakeExploit_C02.test.ts.
Setup
Environment
Network: Hardhat in‑memory network.
Contracts deployed via existing helper
getOrDeployContracts({ forceDeploy: true, config })with a local config (createLocalConfig()), identical to the project’s own unit tests.ProtocolStakerMockis used as the staking protocol backend.
Actors
user: delegator (victim) who will experience frozen funds.validator: a single validator configured as ACTIVE in theProtocolStakerMock.
NFT level configuration
Similar to other unit tests: one level with a fixed
vetAmountRequiredToStakeandscaledRewardFactor, so that the effective stake is non-zero and simple to reason about.
Exploit scenario
User stakes and delegates an NFT to the validator.
The validator completes some periods so that the delegation becomes ACTIVE.
While the validator is still ACTIVE, the user calls
requestDelegationExit(tokenId).This performs one
_updatePeriodEffectiveStake(..., false)for a future period, correctly scheduling the removal of the user’s stake.
The validator’s status is then changed to EXITED, and its
completedPeriodsis increased (e.g. by the protocol).The user now calls
unstake(tokenId):Because the validator status is
EXITED,unstakeexecutes another_updatePeriodEffectiveStake(..., false).Since the first decrease has already reduced the future effective stake to zero, this second decrease underflows and causes the entire transaction to revert.
Any future attempt to
unstake(tokenId)will hit the same logic and revert again, freezing the user’s funds.
PoC code
Was this helpful?