Overclocking Blocks with Gas Refunds

Overclocking Blocks with Gas Refunds

TL;DR: Storage‐clearing refunds are valuable but can inflate block gas figures - keep them for users, but don’t count them against block gas used. EIP-7778 restores accurate on-chain gas accounting.

Ethereum’s gas mechanism aims to reflect computational resource consumption accurately. To encourage users to keep the state clean and manageable, Ethereum grants gas refunds when storage slots are reset to zero. While effective in reducing state bloat, these refunds currently complicate gas accounting, making it appear that blocks consume fewer resources than they actually do.

How Gas Refunds Work Today

Transactions that clear storage slots via SSTORE operations, resetting a slot to zero, qualify for gas refunds (see EIP-3529). For instance, a transaction consuming 45 million gas with a 20% refund effectively costs the user:

tx.gas_used = 45,000,000 - (0.20 × 45,000,000) = 36,000,000 gas

Similarly, another transaction consuming 9 million gas with a 20% refund results in:

tx.gas_used = 9,000,000 - (0.20 × 9,000,000) = 7,200,000 gas

Importantly, the gas refund of one transaction can be consumed by another transaction.

What’s Problematic About the Current Mechanism?

While gas refunds effectively incentivize storage cleanup, their current implementation reduces the total gas usage counted against the block gas limit. As a result, the block’s gas usage understates the actual computational effort performed. Crucially, long-term incentives against state bloat should not distort short-term feedback controller mechanisms such as EIP-1559’s basefee.

To clearly understand the impact of gas refunds on block gas accounting, let’s formalize this more systematically.

Formal Model of Gas Smuggling

Definitions

  • Block Gas Limit: G_0G0
  • Refund Ratio: r = 0.2r=0.2 (20%)
  • Minimum Gas Required per Transaction: G_{\text{min}} = 21,000Gmin=21,000

Gas Refund at Step ii

The gas refund at each iterative step ii (starting from i=1i=1) is given by:

R_i = G_0 \times r^i
Ri=G0×ri

For example:

  • Step 1: R_1 = G_0 \times rR1=G0×r
  • Step 2: R_2 = G_0 \times r^2R2=G0×r2, etc.

Stopping Condition

We continue generating transactions that trigger more refunds until the refund at step nn falls below the minimum required gas:

R_n < G_{\text{min}}
Rn<Gmin

Isolating nn:

G_0 \times r^n < G_{\text{min}}
G0×rn<Gmin

Solving for nn:

n > \frac{\log\left(\frac{G_{\text{min}}}{G_0}\right)}{\log(r)}
n>log(GminG0)log(r)

Thus, the stopping step number is:

n = \left\lfloor \frac{\log\left(\frac{G_{\text{min}}}{G_0}\right)}{\log(r)} \right\rfloor + 1
n=⎢⎢⎢⎢log(GminG0)log(r)⎥⎥⎥⎥+1

(\lfloor x \rfloorx denotes the floor function, giving the largest integer ≤ x.)

Total Computational Gas

Summing all computational gas used up to step n-1n1:

G_{\text{total}} = G_0 + G_0 r + G_0 r^2 + \dots + G_0 r^{n-1}
Gtotal=G0+G0r+G0r2++G0rn1

Factoring out G_0G0:

G_{\text{total}} = G_0 (1 + r + r^2 + \dots + r^{n-1})
Gtotal=G0(1+r+r2++rn1)

Using the geometric series formula:

1 + r + r^2 + \dots + r^{n-1} = \frac{1 - r^{n}}{1 - r}
1+r+r2++rn1=1rn1r

We get:

G_{\text{total}} = G_0 \times \frac{1 - r^{n}}{1 - r}
Gtotal=G0×1rn1r

Substituting our values:

  • G_0 = 45,000,000G0=45,000,000
  • r = 0.2r=0.2
  • G_{\text{min}} = 21,000Gmin=21,000

Compute nn:

n = \left\lfloor \frac{\log\left(\frac{21,000}{45,000,000}\right)}{\log(0.2)} \right\rfloor + 1 = \lfloor 4.766 \rfloor + 1 = 5
n=⎢⎢⎢⎢log(21,00045,000,000)log(0.2)⎥⎥⎥⎥+1=4.766+1=5

Compute Total Gas:

G_{\text{total}} = 45,000,000 \times \frac{1 - (0.2)^5}{1 - 0.2} = 56,232,000
Gtotal=45,000,000×1(0.2)510.2=56,232,000

This demonstrates a ~25% increase from the initial gas limit, highlighting how significantly current refunds distort block gas accounting.

Example Contract

This is how such contracts might look like:

// SPDX-License-Identifier: MITpragma solidity ^0.8.0;contract Refundooor {mapping(uint256 => uint256) private storageSlots;/// @notice Pre‑fill `maxSlots` keys starting at `startKey`function chargeStorage(uint256 startKey, uint256 maxSlots) external {for (uint256 i = 0; i < maxSlots; i++) {storageSlots[startKey + i] = startKey + i + 1;}}/// @notice Clears exactly MAX_LOOPS slots unconditionally (no checks)fallback() external {assembly {let base := storageSlots.slotmstore(0x20, base)// MAX_LOOPS * ~5,109 gas/iter ≈ 45 000 000 gaslet MAX_LOOPS := 8805for { let i := 0 } lt(i, MAX_LOOPS) { i := add(i, 1) } {// compute storage slot for key = imstore(0x0, i)let s := keccak256(0x0, 0x40)// unconditionally write zerosstore(s, 0)}}}}
  • We first charge the contract by writing to empty storage slots.
  • Second, we call the contract’s fallback, setting 8805 storage slots to zero.

Check out this sample transaction on Holesky which maxes out the number of SSTORE operations.

Eventually, the “smuggled” gas scales linearly with the block gas limit. At 100M gas, a block can come with 125M gas of actual work.


Proposed Change: Separate Transaction Refunds from Block Gas Accounting

EIP-7778 proposes retaining user-level refunds to incentivize efficient storage management but removing these refunds from block-level gas accounting. This ensures block gas usage accurately reflects actual resource consumption.

The benefits of implementing the EIP are:

  • Improved Predictability: Actual work for blocks stays below the intended limit.
  • Increased Network Stability: Reduced DoS risks regardless of the worst-case scenario.
  • Preserved User Incentives: Users maintain their motivation for state cleanup.

EIP-7778 cleanly separates user incentives from block-wide resource constraints, aligning block gas usage more closely with the actual work performed.


Source
Disclaimer: The content above is only the author's opinion which does not represent any position of Followin, and is not intended as, and shall not be understood or construed as, investment advice from Followin.
Like
Add to Favorites
Comments