56403 bc insight there is a problem in the dpos threshold switch undercounts votes at hayabusa activation
Submitted on Oct 15th 2025 at 16:13:52 UTC by @XDZIBECX for Attackathon | VeChain Hayabusa Upgrade
Report ID: #56403
Report Type: Blockchain/DLT
Report severity: Insight
Target: https://github.com/vechain/thor/compare/master...release/hayabusa
Impacts: A bug in the respective layer 0/1/2 network code that results in unintended smart contract behavior with no concrete funds at direct risk
Description
Brief / Intro
When Hayabusa switches on PoS, the round immediately begins using a stake-weighted 2/3 rule to decide finality. In that same round, COM votes from blocks at or below the activation height are recorded with weight = 0. These early votes are never upgraded later in the round because the justifier keeps only the first-seen vote per signer and never replaces it. As a result, some real stake is not counted toward the weighted threshold. An epoch that truly has ≥ 2/3 by stake can still show not justified/committed until additional post-boundary (properly weighted) votes arrive. This causes a temporary finality delay right at the switch. Blocks continue producing and contracts remain callable; there is no chain split. See PoC and details below.
Vulnerability Details
At the start of a round, the engine constructs a justifier and, if PoS is active, sets a weighted threshold. This happens here:
https://github.com/vechain/thor/blob/b4c914fe573ed6141daa159fa293e9193a96d74f/bft/justifier.go#L83C1-L91C10
Excerpt:
posActive, err := staker.IsPoSActive()
...
if posActive {
totalWeight, err := engine.getTotalWeight(sum)
thresholdWeight := totalWeight * 2 / 3
return newJustifier(parentQuality, checkpoint, 0, thresholdWeight), nil
}In the same round, the engine already uses a stake-weighted 2/3 threshold, so any votes cast at or before the activation height are recorded with weight = 0:
https://github.com/vechain/thor/blob/b4c914fe573ed6141daa159fa293e9193a96d74f/bft/engine.go#L323C3-L348C1
Excerpt:
The justifier keeps only the first vote per signer and doesn’t upgrade a signer’s weight later in the round. Thus if the first-seen vote is zero-weight (because it was at/below the activation height), it remains zero-weight for the rest of the round. When the protocol uses the stake-weighted rule (≥ 2/3 of total stake), these zero-weight early votes undercount the real stake and can prevent justification/commit even when the actual stake participation is ≥ 2/3.
Impact Details
At the Hayabusa PoS activation boundary, the protocol switches to a stake-weighted 2/3 threshold but still counts some early votes in the same round as zero weight. That undercounts real stake, so an epoch that actually has ≥ two-thirds weighted participation can fail to justify/commit until more counted votes arrive. The chain continues producing blocks and contracts are still callable, but finality is temporarily delayed. This can delay unlocks/settlements, extend safety windows, and lag oracle/bridge updates that depend on timely finalization. No direct funds are at risk.
References
https://github.com/vechain/thor/blob/b4c914fe573ed6141daa159fa293e9193a96d74f/bft/justifier.go#L83C1-L91C10
https://github.com/vechain/thor/blob/b4c914fe573ed6141daa159fa293e9193a96d74f/bft/engine.go#L323C3-L348C1
Proof of Concept
A unit test demonstrating the issue (the test shows counted weight under the threshold while actual stake equals the threshold, then later justification occurs only after extra counted weight arrives):
If you want, I can suggest concrete code fixes (e.g., upgrade existing zero-weight first-seen votes when PoS becomes active, or delay using stake-weighted threshold until all signer weights for the round are known) and produce a small patch sketch.
Was this helpful?