60151 sc high double reduction of effective stake can lead to stuck delegations
Submitted on Nov 19th 2025 at 09:28:35 UTC by @Bizarro for Audit Comp | Vechain | Stargate Hayabusa
Report ID: #60151
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
Double reduction of the effective stake of a validator can lead to users not being able to withdraw their staked amount.
Vulnerability Details
In the _delegation and _unstake functions, the effective stake of the validator is reduced for the next period when the validator has exited their position in the Staker contract. The problem arises when a user, who has already exited their delegation in the same or before period as the validator's exit, proceeds to unstake or delegate to another validator. This sequence of actions causes the contract to reduce the effective stake twice for the same user's stake.
User unstake / re-delegate causes double reduction
A user who previously requested delegation exit now calls unstake (or delegates elsewhere). Because the validator status is now exited, the contract logic reduces the effective stake again for the next period. This results in an additional decrease, incorrectly bringing effective stake to 0e18 in this example. Remaining users then cannot unstake because the system believes there is no effective stake left โ their funds become stuck.
Below are the relevant contract excerpts showing where the effective stake is decreased.
requestDelegationExit snippet (decreases effective stake at @1):
function requestDelegationExit(
uint256 _tokenId
) external whenNotPaused onlyTokenOwner(_tokenId) nonReentrant {
StargateStorage storage $ = _getStargateStorage();
uint256 delegationId = $.delegationIdByTokenId[_tokenId];
if (delegationId == 0) {
revert DelegationNotFound(_tokenId);
}
Delegation memory delegation = _getDelegationDetails($, _tokenId);
if (delegation.status == DelegationStatus.PENDING) {
// if the delegation is pending, we can exit it immediately
// by withdrawing the VET from the protocol
$.protocolStakerContract.withdrawDelegation(delegationId);
emit DelegationWithdrawn(
_tokenId,
delegation.validator,
delegationId,
delegation.stake,
$.stargateNFTContract.getTokenLevel(_tokenId)
);
// and reset the mappings in storage regarding this delegation
_resetDelegationDetails($, _tokenId);
} else if (delegation.status == DelegationStatus.ACTIVE) {
// If the delegation is active, we need to signal the exit to the protocol and wait for the end of the period
// We do not allow the user to request an exit multiple times
if (delegation.endPeriod != type(uint32).max) {
revert DelegationExitAlreadyRequested();
}
$.protocolStakerContract.signalDelegationExit(delegationId);
} else {
revert InvalidDelegationStatus(_tokenId, delegation.status);
}
// decrease the effective stake
// Get the latest completed period of the validator
(, , , uint32 completedPeriods) = $.protocolStakerContract.getValidationPeriodDetails(
delegation.validator
);
(, uint32 exitBlock) = $.protocolStakerContract.getDelegationPeriodDetails(delegationId);
// decrease the effective stake
@1> _updatePeriodEffectiveStake($, delegation.validator, _tokenId, completedPeriods + 2, false);
emit DelegationExitRequested(_tokenId, delegation.validator, delegationId, exitBlock);
}unstake snippet (may decrease effective stake again at @3 when validator is exited โ check at @2):
Steps summarized:
Step-1: A validator exits by calling
signalExit.Step-2: A user calls
requestDelegationExit, which decreases theeffectiveStakefor the next period (@1).Step-3: In the next period, validator status becomes
VALIDATOR_STATUS_EXITED. The same user callsunstake. Because of the check at@2,effectiveStakeis decreased again (@3).Step-4: The double reduction leads to incorrect total stake accounting and can cause further unstake operations to fail.
Impact Details
Due to the double reduction of the effective stake, users legitimately staked with a validator that has exited may be unable to unstake their funds. This results in funds becoming permanently stuck in the protocol.
References
https://github.com/immunefi-team/audit-comp-vechain-stargate-hayabusa/blob/e9c0bc9b0f24dc0c44de273181d9a99aaf2c31b0/packages/contracts/contracts/Stargate.sol#L276
https://github.com/immunefi-team/audit-comp-vechain-stargate-hayabusa/blob/e9c0bc9b0f24dc0c44de273181d9a99aaf2c31b0/packages/contracts/contracts/Stargate.sol#L407
Proof of Concept
Add this console.log to track the new effective values: https://github.com/immunefi-team/audit-comp-vechain-stargate-hayabusa/blob/e9c0bc9b0f24dc0c44de273181d9a99aaf2c31b0/packages/contracts/contracts/Stargate.sol#L1010
Add this test to test/unit/Stargate/Rewards.test.ts and run yarn contracts:test:unit:
Expected console/test output demonstrates the double accounting (updatedValue decreased twice for the same stake):
Mitigation / Notes
(Left as-is โ no changes/additions to remediation beyond the original report content per import rules.)
Was this helpful?