56454 bc insight gas undercharging threatens hayabusa network upgrade

Submitted on Oct 16th 2025 at 08:25:34 UTC by @Angry_Mustache_Man for Attackathon | VeChain Hayabusa Upgrade

  • Report ID: #56454

  • 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

Vulnerability Details

During the HAYABUSA upgrade, the critical transition of PoA→PoS invokes the native_isEndorsed function. The native_isEndorsed function (in-scope for the attackathon) undercharges gas. See implementation:

https://github.com/vechain/thor/blob/b4c914fe573ed6141daa159fa293e9193a96d74f/builtin/authority_native.go#L105-L137

{"native_isEndorsed", func(env *xenv.Environment) []any {
			var nodeMaster common.Address
			env.ParseArgs(&nodeMaster)

			env.UseGas(thor.SloadGas * 2)
			listed, endorsor, _, _, err := Authority.Native(env.State()).Get(thor.Address(nodeMaster))
			if err != nil {
				panic(err)
			}
			if !listed {
				return []any{false}
			}

			env.UseGas(thor.SloadGas)
			endorsement, err := Params.Native(env.State()).Get(thor.KeyProposerEndorsement)
			if err != nil {
				panic(err)
			}

			// Use staker Transition Period logic
			// to ensure that transitioning validators are marked as endorsed
			env.UseGas(thor.GetBalanceGas)
			isEndorsed, err := Staker.Native(env.State()).TransitionPeriodBalanceCheck(
				env.ForkConfig(),
				env.BlockContext().Number,
				endorsement,
			)(thor.Address(nodeMaster), endorsor)
			if err != nil {
				panic(err)
			}
			return []any{isEndorsed}
		}},
	}

Current gas charges in native_isEndorsed:

Total charged GAS: 1000 gas.

However, TransitionPeriodBalanceCheck (called next) performs additional operations that are not charged here:

https://github.com/vechain/thor/blob/b4c914fe573ed6141daa159fa293e9193a96d74f/builtin/staker/transition.go#L72-L98

The code comment notes: "// before HAYABUSA fork, we only check the account balance". After HAYABUSA, GetValidation is invoked and performs extra storage reads (SLOADs) that are not accounted for in native_isEndorsed.

So the actual gas to be charged after HAYABUSA should be >= 1200 GAS: thor.SloadGas * 2 + thor.SloadGas + thor.GetBalanceGas + (Gas for the GetValidation call)

The Missing Gas Charge is that s.validationService.GetValidation(validator) performs:

1

Storage lookup / SLOADs

GetValidation does a storage lookup that involves one or more SLOAD operations to retrieve validation data.

2

Additional gas (>= 1 SLOAD)

Those SLOADs cost >= 200 gas (>= thor.SloadGas) which is not charged in native_isEndorsed.

3

Result: Undercharge (~>=200 gas)

This >=200 gas is never charged in native_isEndorsed, resulting in >=16.7% gas undercharging (200/1200+).

Every block validation during the HAYABUSA transition calls native_isEndorsed. The missing charge per call creates economic incentives for exploitation: each call saves >=200 gas, causing economic depletion of the protocol and potentially leading to network shutdown.

Impact Details

Proof of Concept

Create a new test file named thor/builtin/authority_native_test.go with the following tests to (a) statically verify that native_isEndorsed does not charge an SLOAD before calling TransitionPeriodBalanceCheck, and (b) measure gas consumed by GetValidation:

Run the tests:

  • go test -v ./builtin -run Test_POC

  • go test -v ./builtin -run Test_GasConsumedAtGetValidation

Expected output for Test_POC
Expected output for Test_GasConsumedAtGetValidation

Was this helpful?