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 Quality halts, 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?