56345 bc insight there is an issue related to strict threshold breaks exact 2 3 and is causing finality freeze
Submitted on Oct 14th 2025 at 18:46:03 UTC by @XDZIBECX for Attackathon | VeChain Hayabusa Upgrade
Report ID: #56345
Report Type: Blockchain/DLT
Report severity: Insight
Target: https://github.com/vechain/thor/compare/master...release/hayabusa
Impacts:
Network not being able to confirm new transactions (total network shutdown)
Description
Brief / Intro
There is an issue in Hayabusa’s BFT finality logic in the Summarize() function: it checks whether enough validators participated to “finalize” an epoch. It should accept at least two-thirds participation, but the code mistakenly requires strictly more than two-thirds.
Concretely:
In PoA (count-based), the code uses
len(votes) > thresholdVotes.In DPoS (weight-based), the code uses
justifiedWeight > thresholdWeight.
Both checks use > where the protocol requires >=. When participation is exactly two-thirds (66.67%), due to integer division producing exact two-thirds thresholds frequently, the epoch is treated as not justified/committed. As a result the Quality metric does not increase, the finalized checkpoint isn’t updated, and finality stalls. This can cause the chain to stop confirming new transactions whenever participation is exactly 2/3.
Vulnerability Details
The bug occurs in Summarize() where both PoA (vote-count) and DPoS (weight-based) branches use >:
This violates the VIP specification which states finality requires at least two-thirds of the total weighted staked VET (see VIP-253): "At least" means using >=. The code instead requires strictly more than two-thirds (>), so exact equality fails.
The threshold is calculated using integer arithmetic as exactly two-thirds:
When participation equals the computed threshold (exactly 2/3), Summarize() sets Justified=false and Committed=false, so Quality does not increase. Downstream, CommitBlock() only writes a new finalized checkpoint when state.Committed == true and state.Quality > 1. Therefore, no checkpoint is finalized and finality can stall. An attacker can withhold ~34% by stake to keep participation pinned at 2/3, indefinitely stalling finality until participation exceeds 2/3 by any epsilon.
Impact Details
Because Summarize() uses > instead of >= for the 2/3 supermajority:
Epochs with exactly two-thirds participation never reach
Committed=true.CommitBlock()never writes a new finalized checkpoint.An attacker (or natural outages) withholding ~34% of stake can keep participation at exactly 2/3, stalling finality indefinitely.
During such a stall, new transactions cannot be finalized; exchanges/wallets treat transfers as unsafe; bridges and downstream protocols likely pause; and fork-choice progress based on
Qualityhalts, causing a network-wide liveness failure.
Proof of Concept
Single minimal test proving the bug in both modes (PoA count and DPoS weight):
Logs of the test run:
```text Running tool: C:\Program Files\Go\bin\go.exe test -timeout 30s -run ^TestFinalityBug_MinimalProof$ github.com/vechain/thor/v2/bft
=== RUN TestFinalityBug_MinimalProof === RUN TestFinalityBug_MinimalProof/PoA_exactly_two_thirds_fails --- PASS: TestFinalityBug_MinimalProof/PoA_exactly_two_thirds_fails (0.00s) === RUN TestFinalityBug_MinimalProof/PoA_two_thirds_plus_one_passes --- PASS: TestFinalityBug_MinimalProof/PoA_two_thirds_plus_one_passes (0.00s) === RUN TestFinalityBug_MinimalProof/DPoS_exactly_two_thirds_weight_fails --- PASS: TestFinalityBug_MinimalProof/DPoS_exactly_two_thirds_weight_fails (0.00s) === RUN TestFinalityBug_MinimalProof/DPoS_two_thirds_plus_one_weight_passes --- PASS: TestFinalityBug_MinimalProof/DPoS_two_thirds_plus_one_weight_passes (0.00s) --- PASS: TestFinalityBug_MinimalProof (0.00s) PASS ok github.com/vechain/thor/v2/bft 0.238s
Was this helpful?