55925 bc insight underpriced supply queries enable cheap cpu dos

Submitted on Oct 7th 2025 at 20:42:31 UTC by @spongebob for Attackathon | VeChain Hayabusa Upgrade

  • Report ID: #55925

  • Report Type: Blockchain/DLT

  • Report severity: Insight

  • Target: https://github.com/vechain/thor/compare/master...release/hayabusa

  • Impacts:

    • Temporary freezing of network transactions by delaying one block by 500% or more of the average block time of the preceding 24 hours beyond standard difficulty adjustments

Description

Underpriced gas in native_totalSupply lets callers trigger three storage decodes and heavy CalcEnergy math while only paying a single SloadGas (200). The native wrapper charges once at https://github.com/vechain/thor/blob/3ad5e1805c778a070e27d0d0293f335a256c235e/builtin/energy_native.go#L22-L29

Yet TotalSupply() pulls initial-supply, growth-stop-time, and issued via State.DecodeStorage plus re-does big-int growth math:

  • https://github.com/vechain/thor/blob/3ad5e1805c778a070e27d0d0293f335a256c235e/builtin/energy/energy.go#L48-L133

  • https://github.com/vechain/thor/blob/3ad5e1805c778a070e27d0d0293f335a256c235e/builtin/energy/energy.go#L229-L239

Example of one of the storage decodes:

func (e *Energy) GetEnergyGrowthStopTime() (uint64, error) {
	if e.stopTime != 0 {
		return e.stopTime, nil
	}

	var time uint64
	if err := e.state.DecodeStorage(e.addr, growthStopTimeKey, func(raw []byte) error {
		if len(raw) == 0 {
			return nil
		}
		return rlp.DecodeBytes(raw, &time)

The underlying account storage decode is implemented here:

  • https://github.com/vechain/thor/blob/3ad5e1805c778a070e27d0d0293f335a256c235e/state/account.go#L44-L77

After the first call the trie data is cached, but every subsequent call still re-decodes the RLP payloads and multiplies big integers.

An attacker can loop Energy.totalSupply() thousands of times in one clause, consuming most of a block’s CPU while buying only ≈200 gas per iteration.

native_totalBurned has the same pattern. It bills one SloadGas in: https://github.com/vechain/thor/blob/3ad5e1805c778a070e27d0d0293f335a256c235e/builtin/energy_native.go#L30-L36

Yet TotalBurned() RLP-decodes the aggregated add/sub counters and performs big-int arithmetic. That extra work is effectively free, giving another cheap tight loop primitive (lower impact than totalSupply, but still mis-priced).

Severity rationale: This can let an attacker delay block production by cheaply forcing validators into heavy CPU work.

Recommendation

Charge for each underlying storage fetch (for example, 3 * thor.SloadGas for totalSupply) and add a fixed compute premium to cover the big-int math in both getters.

Proof of Concept

Was this helpful?