56629 bc insight there is an issue in mapping gas undercharge and is enables 30 extra node work per unit gas

Submitted on Oct 18th 2025 at 16:11:43 UTC by @XDZIBECX for Attackathon | VeChain Hayabusa Upgrade

  • Report ID: #56629

  • Report Type: Blockchain/DLT

  • Report severity: Insight

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

  • Impacts: Increasing network processing node resource consumption by at least 30% without brute force actions, compared to the preceding 24 hours

Description

There is an issue in the generic Mapping storage layer: gas is charged by the encoded/decoded byte-size of values instead of by the storage transition type. This causes two undercharged operations:

  • Clearing a previously non-zero slot to the type's zero value (treated as a hard delete) reports size == 0 and is billed 0 SstoreResetGas.

  • Reading an empty slot (len(raw) == 0) yields size == 0 and is billed 0 SloadGas.

Native builtin code always charges at least one SLOAD/SSTORE reset per operation regardless of the slot contents. The current Mapping behavior allows an attacker to craft transactions that flip entries non-zero→zero and perform empty reads while paying far less gas than intended. This can significantly increase node I/O/CPU per unit gas (the report demonstrates >30% and in the PoC a ~96.15% under-charge).

Brief / Intro

  • The Mapping implementation bills gas based on encoded/decoded byte-size (size) instead of the storage transition type.

  • For empty slots (len(raw) == 0) or when writing a value equal to the type’s zero value, size becomes 0 and the operation can be billed 0 gas.

  • As a result, clearing non-zero slots to zero and empty reads are undercharged compared to native semantics (which always bill at least one SstoreResetGas or SloadGas).

  • An attacker can create many such cheap operations in transactions to increase validator resource consumption and degrade network performance or validator economics.

Vulnerability Details

The core bug is that gas is billed by the encoded/decoded byte-size (size) returned by Mapping's get/set, rather than by the kind of storage transition.

Key code points:

  • Get charges SLOAD as size * SloadGas; if size == 0 (empty slot), the call charges 0 gas:

  • get sets size = toWordSize(len(raw)); for an empty slot len(raw) == 0 → size == 0:

  • Update bills SSTORE reset as size * SstoreResetGas; if set returns size == 0 (writing zero value treated as delete), then billed 0:

  • set treats the type zero value as a hard delete and returns size == 0:

Because of this, when a slot is cleared from non-zero→zero the Mapping code reports size == 0 and charges no SstoreResetGas. Similarly empty reads report size == 0 and are billed 0 SloadGas. This deviates from native semantics and underprices operations that still require node I/O/CPU.

Impact Details

  • The undercharging enables attackers to create transactions containing many cheap state churn operations (non-zero→zero clears and empty reads) that force validators to perform much more I/O/CPU than the gas paid for.

  • The PoC demonstrates that with 20,000 clears and 20,000 empty reads:

    • Expected (native) billing: 104,000,000 gas

    • Mapping billed: 4,000,000 gas

    • Under-charge: 100,000,000 gas (96.15% under-charge)

  • This allows an attacker to sustain blocks dominated by underpriced churn and drive node resource consumption well beyond a 30% increase vs prior-day baseline at the same on-chain gas usage.

  • Consequences: degraded validator economics, increased block processing time, higher DB pressure, and a persistent griefing vector.

Proof of Concept

Test that demonstrates the issue:

Example test output (logs):

References

  • Vulnerability details and code links referenced above are from the target repository: https://github.com/vechain/thor/compare/master...release/hayabusa

Suggested Fix (summary)

  • Bill at least one SloadGas for every read and at least one SstoreResetGas for any storage reset (non-zero → zero), regardless of encoded byte-size.

  • Replace size-based minimum with transition-type based billing (e.g., detect non-zero→zero and empty→non-empty transitions and charge appropriate minimums to match native semantics).

Was this helpful?