59951 sc high in special cases delegatorseffectivestake may decrease twice and cause staked funds to become locked
Submitted on Nov 17th 2025 at 06:29:45 UTC by @shaflow1 for Audit Comp | Vechain | Stargate Hayabusa
Report ID: #59951
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
Brief / Intro
The checks for whether _updatePeriodEffectiveStake should be called in the delegate and unstake functions are insufficient. This can cause an NFT's unstaking to potentially call _updatePeriodEffectiveStake twice and lead to failures in other NFTs' withdrawals due to insufficient delegatorsEffectiveStake, resulting in funds being locked.
Vulnerability Details
Relevant code (excerpt):
// if the delegation is pending or the validator is exited or unknown
// decrease the effective stake of the previous validator
if (
currentValidatorStatus == VALIDATOR_STATUS_EXITED ||
delegation.status == DelegationStatus.PENDING
) {
// get the completed periods of the previous validator
(, , , uint32 oldCompletedPeriods) = $
.protocolStakerContract
.getValidationPeriodDetails(delegation.validator);
// decrease the effective stake of the previous validator
_updatePeriodEffectiveStake(
$,
delegation.validator,
_tokenId,
oldCompletedPeriods + 2,
false // decrease
);
}The _updatePeriodEffectiveStake function is called to decrease delegatorsEffectiveStake in:
unstakeanddelegatewhencurrentValidatorStatus == VALIDATOR_STATUS_EXITED || delegation.status == DelegationStatus.PENDINGrequestDelegationExit(in other code paths) — so in other cases it is not invoked inunstake/delegate
The condition currentValidatorStatus == VALIDATOR_STATUS_EXITED || delegation.status == DelegationStatus.PENDING does not account for the case where a delegator already called requestDelegationExit (which already decreased delegatorsEffectiveStake) and afterward the validator transitions to exited status. That sequence can cause _updatePeriodEffectiveStake to be invoked again in unstake/delegate because currentValidatorStatus == VALIDATOR_STATUS_EXITED, resulting in a double decrease.
Sequence that causes the double decrease (summary)
A delegator calls
requestDelegationExit→ this calls_updatePeriodEffectiveStaketo decreasedelegatorsEffectiveStake.The validator later exits (validator status becomes
EXITED).The delegator then calls
unstakeordelegate. BecausecurrentValidatorStatus == VALIDATOR_STATUS_EXITED, the code path inunstake/delegateagain calls_updatePeriodEffectiveStake, decreasingdelegatorsEffectiveStakea second time.The double decrease can reduce
delegatorsEffectiveStakebelow what remaining delegators rely on, causing subsequent exits/unstakes to fail and locking funds.
Impact Details
This special-case double decrease can cause delegatorsEffectiveStake to be excessively reduced, preventing some delegators from withdrawing their funds. An attacker might exploit this to lock funds or cause denial-of-withdrawals among honest delegators.
Reference
https://github.com/immunefi-team/audit-comp-vechain-stargate-hayabusa/blob/e9c0bc9b0f24dc0c44de273181d9a99aaf2c31b0/packages/contracts/contracts/Stargate.sol#L267
Proof of Concept
Add this test to the end of packages/contracts/test/unit/Stargate/Delegation.test.ts to reproduce the issue:
Test explanation:
Two delegator NFTs are active and delegated to the same validator.
NFT1 calls
requestDelegationExit(which calls_updatePeriodEffectiveStaketo reduce effective stake) but does not yet unstake.The validator exits afterwards.
NFT1 calls
unstake; due to validator status beingEXITED,_updatePeriodEffectiveStakeis called again during unstake, causing a double decrease.The double decrease can make the effective stake zero, causing NFT2's unstake to revert.
The PoC logs (when running the test) show that during NFT1's unstake the delegatorsEffectiveStake is reduced twice.
Recommended mitigation (summary)
Ensure
_updatePeriodEffectiveStakeis not called a second time for a delegation that already had its effective stake decreased duringrequestDelegationExit.Add and check a flag/state to indicate whether the effective stake has already been decreased for that delegation/period (or refine the condition to avoid double-decrement when delegator previously requested exit).
Carefully audit all code paths that call
_updatePeriodEffectiveStaketo ensure each delegation/period is decreased exactly once.
Was this helpful?