55632 bc critical delegation submitted in the same period before a validator exit will be permanently frozen
Submitted on Oct 3rd 2025 at 06:29:50 UTC by @Haxatron for Attackathon | VeChain Hayabusa Upgrade
Report ID: #55632
Report Type: Blockchain/DLT
Report severity: Critical
Target: https://github.com/vechain/thor/tree/release/hayabusa
Impacts:
Permanent freezing of funds (fix requires hardfork)
Description
Brief/Intro
Any delegation that is submitted in the same period before a validator exit will be permanently frozen.
Vulnerability Details
Any delegation that is submitted in the same period of a validator exit will be permanently frozen due to the following edge case. Consider the following scenario:
Delegation added in Period 5
AddDelegation is called by a delegator in Period 5; its FirstIteration will be CurrentIteration + 1 so, Period 6.
https://github.com/vechain/thor/blob/release/hayabusa/builtin/staker/staker.go#L534-L543
current, err := val.CurrentIteration(currentBlock)
if err != nil {
return nil, err
}
delegationID, err := s.delegationService.Add(validator, current+1, stake, multiplier)
if err != nil {
logger.Info("failed to add delegation", "validator", validator, "error", err)
return nil, err
}Validator signals exit in Period 5
SignalExit is called by the validator in Period 5; its CompletedPeriods will be set to 5 periods.
https://github.com/vechain/thor/blob/release/hayabusa/builtin/staker/validation/service.go#L165-L185
func (s *Service) SignalExit(validator thor.Address, currentBlock uint32, minBlock uint32, maxTry int) error {
...
current, err := validation.CurrentIteration(currentBlock)
if err != nil {
return err
}
validation.ExitBlock = &exitBlock
// validator is going to exit after current iteration
validation.CompletedPeriods = current
return s.repo.updateValidation(validator, validation)
}Exit processed at start of Period 6
On Period 6, the validator's exit is processed. This causes the aggregation.Pending to be set to 0 during the start of Period 6 when housekeeping is performed.
https://github.com/vechain/thor/blob/release/hayabusa/builtin/staker/aggregation/aggregation.go#L82-L95
func (a *Aggregation) exit() *globalstats.Exit {
// Return these values to modify contract totals
exit := globalstats.Exit{
ExitedTVL: a.Locked.Clone(),
QueuedDecrease: a.Pending.VET,
}
// Reset the aggregation
a.Exiting = &stakes.WeightedStake{}
a.Locked = &stakes.WeightedStake{}
a.Pending = &stakes.WeightedStake{}
return &exit
}Delegator withdraws after exit — stuck due to underflow
When the delegator tries to withdraw via WithdrawDelegation after the exit, since the CompletedPeriods is set to 5 then CurrentIteration will return 5. Because the delegation's FirstIteration is set to 6, started will be false.
https://github.com/vechain/thor/blob/release/hayabusa/builtin/staker/delegation/delegation.go#L28-L38
func (d *Delegation) Started(val *validation.Validation, currentBlock uint32) (bool, error) {
if val.Status == validation.StatusQueued || val.Status == validation.StatusUnknown {
return false, nil // Delegation cannot start if the validation is not active
}
currentStakingPeriod, err := val.CurrentIteration(currentBlock)
if err != nil {
return false, err
}
return currentStakingPeriod >= d.FirstIteration, nil
}Because started is false, the withdraw path attempts to decrement aggregation.Pending via aggregationService.SubPendingVet, but aggregation.Pending was already reset to 0 during exit. This causes an underflow and the transaction reverts.
https://github.com/vechain/thor/blob/release/hayabusa/builtin/staker/staker.go#L679-L688
func (s *Staker) WithdrawDelegation(
delegationID *big.Int,
currentBlock uint32,
) (uint64, error) {
...
if !started {
weightedStake := stakes.NewWeightedStakeWithMultiplier(withdrawableStake, del.Multiplier)
if err = s.aggregationService.SubPendingVet(del.Validation, weightedStake); err != nil {
return 0, err
}
if err = s.globalStatsService.RemoveQueued(withdrawableStake); err != nil {
return 0, err
}
} else {
...
}Impact Details
Any delegation that is submitted in the same period before a validator exit will be permanently frozen (any fix will require a hardfork).
Proof of Concept
Was this helpful?