Meter the bytes each opcode adds to the EIP-7928 Block Access List in an explicit per-transaction counter, and fold that count, at 64 gas/byte, into the transaction's floor accumulator — checked at runtime before the BAL grows. Today an attacker can pack ~1.55 MB into a 60M-gas block: 75% of gas on cold SLOADs (32 BAL bytes per 2,100 gas) + 25% on calldata at 16 gas/byte. On top of EIP-8131's tx-content floor (same rate), block content is capped at block_gas_limit / 64 ≈ 0.89 MB (~42% reduction). Neither EIP alone closes the bypass: 8131 does not price BAL bytes; 8279 reuses 8131's floor. Typical transactions are unaffected: the runtime BAL floor never binds in isolation.
EIP-7623 caps worst-case block size by charging at least 64 gas per non-zero calldata byte. EIP-7981 extended that to access-list entries, and EIP-8131 generalises the floor to a uniform per-byte rule over all tx-content fields, including EIP-7702 authorization tuples and EIP-4844 blob versioned hashes. EIP-7928 introduces a new source of block bytes, the BAL, populated by runtime opcodes. None of those static floors cover it.
The cheapest BAL contributor is a cold SLOAD: 32 bytes for 2,100 gas. Combined with all-non-zero calldata, an attacker pushes intrinsic gas above the calldata floor, pays intrinsic, and gets calldata at 16 gas/byte while loading the rest of the block via SLOAD keys.
At a 60M gas limit:
| Attack | Worst-case block bytes |
|---|---|
| Pure calldata at floor | 0.894 MB (target) |
| Auth-tuple bypass under EIP-8131 alone | 1.067 MB |
Cold-SLOAD bypass under EIP-7928 | 1.548 MB |
SSTORE, address access, CREATE, and successful EIP-7702 delegations give further vectors. This EIP closes them with one uniform mechanism.
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174.
Clients keep two internal, per-transaction counters on the execution environment for the duration of the transaction:
bal_data_bytes — an explicit count of the bytes each opcode contributes to the BAL, andfloor_gas_used — the floor accumulator, derived from the static seed plus the metered bytes.Neither is part of the signed transaction, RLP-encoded, gossiped, or persisted; no new transaction field or type is introduced. floor_gas_used is seeded with the static floor (below) with bal_data_bytes = 0, and both are extended at runtime. No gas is reserved or deducted from the execution budget; the derived floor is checked against tx.gas only to ensure the user can pay it if it ends up binding.
meter_bal_data MUST be called BEFORE the matching BAL insertion or state mutation. An OutOfGasError aborts the operation before any unpaid BAL byte exists. The metered count is a deliberate upper bound on the transaction's actual BAL contribution (see Reverts and Repeated writes to the same slot); over-counting only raises the floor, never lowers it, so the bound below is never violated.
The tx-content portion of the floor (calldata, access-list entries, authorization tuples, blob versioned hashes) is defined by EIP-8131 as tx_floor. This EIP adds the per-auth BAL contribution that arises when an authorization is processed:
tx_env.static_floor holds this value and tx_env.floor_gas_used is initialised to it (with bal_data_bytes = 0) at the start of execution. The per-auth term covers each authorization's worst-case BAL contribution statically, so set_delegation (which runs before the EVM's OOG handler) never meters at runtime.
Per-transaction accounting always adds further BAL entries that no opcode places there: sender, coinbase, and target (each with address plus the relevant balance/nonce changes); recipient balance for value transfers; contract nonce on creation; optional top-level EIP-7702 delegated target. These total at most 184 bytes, fully covered by the TX_BASE / FLOOR_GAS_PER_BYTE = 328 bytes of headroom already in TX_BASE.
Each trigger calls meter_bal_data with the byte count below, before the corresponding BAL entry is added:
| Trigger | Bytes added |
|---|---|
Cold account access (BALANCE, EXT*, CALL*, SELFDESTRUCT beneficiary, CREATE/CREATE2 deployed address) | 20 |
Cold storage access (SLOAD, SSTORE) | 32 |
SSTORE first moving a slot off its pre-transaction value | +32 |
SSTORE returning a slot to its pre-transaction value | −32 (refund) |
CALL with non-zero value to a different account | 32 |
SELFDESTRUCT with non-zero balance to a different beneficiary | 32 |
CREATE / CREATE2 after the collision check (new contract's nonce) | 8 |
CREATE / CREATE2 with non-zero endowment (new contract's balance) | +32 |
Successful CREATE / CREATE2 deploy, just before set_code | len(deployed_code) |
EIP-7702 delegations are covered by the static auth_bytes term. CALLCODE, DELEGATECALL, and STATICCALL transfer no value out of the executing account and therefore add no balance bytes.
The 32 storage-value bytes are metered at most once per slot, tracking whether the slot's current value differs from its pre-transaction value: charged on the first SSTORE that makes it differ, refunded by a later SSTORE that restores it. The refund subtracts from bal_data_bytes and recomputes floor_gas_used = static_floor + bal_data_bytes * FLOOR_GAS_PER_BYTE. This mirrors EIP-7928, which records a single post-value per changed slot and demotes a slot whose post-value equals its pre-value to a no-op write in storage_reads — the slot key remains (covered by the cold-access row above), only the value entry disappears. The condition is the BAL's no-op-write rule, not the EIP-3529 SSTORE gas refund: clearing a non-zero slot to zero earns a gas refund but is still a recorded BAL write, so its value bytes are never refunded.
Transaction validation continues to require tx.gas >= max(intrinsic, static_floor).
System-contract calls (EIP-2935, EIP-4788, EIP-7002, EIP-7251) and withdrawals do NOT consume from a per-transaction floor. Their BAL bytes are absorbed by the existing EIP-7928 block-level buffer bal_items <= block_gas_limit / ITEM_COST.
Raising opcode intrinsic costs would penalise every user, including ones who never bypass. Charging on the floor only bites when execution is too cheap to cover the floor, which is exactly the bypass case.
A pure intrinsic surcharge also fails to cap the block at gas_limit / 64: for any added per-SLOAD cost X, the optimum bypass still yields B = gas_limit / 64 + 24 · gas_limit / (2100 + X) bytes, with the residual only vanishing as X grows large. Floor-side charging gives a closed bound directly.
Every runtime trigger pairs its meter_bal_data call with an execution charge larger than the floor extension itself:
| Trigger | Floor extension | Min execution charge |
|---|---|---|
| Cold account access | 1,280 | 2,600 |
Cold SLOAD | 2,048 | 2,100 |
SSTORE (cold, zero → non-zero) | 4,096 | 22,100 |
CALL with non-zero value | 2,048 | 9,000 |
SELFDESTRUCT with non-zero balance | 2,048 | 5,000 |
CREATE / CREATE2 (nonce only) | 512 | 32,000 |
| Deployed code (per byte) | 64 | 200 |
For every BAL-contributing opcode, floor_extension < execution_gas. Executing one more such opcode raises execution_gas_used by strictly more than it raises floor_gas_used. A transaction whose execution gas already exceeds its static floor therefore stays execution-dominated no matter how many BAL-contributing opcodes it runs: the gap between execution_gas_used and floor_gas_used can only widen. Floor-side OutOfGasError (new_floor > tx.gas inside meter_bal_data) requires the floor accumulator to climb past tx.gas; from an execution-dominated state, execution-side OOG fires first.
The floor only binds when the static seed defined by EIP-8131 (calldata, access-list entries, auths, blob hashes) already pushes static_floor close to tx.gas before any opcode runs. That is precisely the calldata + cold-SLOAD bypass this EIP targets. Transactions whose static seed is small, the common case, cannot reach a binding floor through opcode execution alone.
Pricing every BAL byte at the same FLOOR_GAS_PER_BYTE as non-zero calldata makes the worst case invariant to the attacker's choice: calldata, auths, storage keys, addresses, code, and balances all yield 1/64 bytes per gas at the floor. The optimum collapses to B = gas_limit / 64.
EIP-7928 keeps accessed addresses and storage keys in the BAL even when their call frame reverts. Metering each contribution before the insertion guarantees every BAL byte is accounted for, and an out-of-gas at the metering step aborts before the matching insertion. The meter is otherwise a deliberate upper bound, not an exact mirror of the BAL: changes from reverted frames are never rewound (see Reverts), because rewinding them would entangle the meter with the existing OOG and refund machinery for no benefit — the count feeds nothing but a floor that rarely binds, so over-counting is free.
The one refund is the storage-value byte, and it is safe precisely because it is keyed to the BAL rather than to gas. A slot restored to its pre-transaction value becomes a no-op write under EIP-7928, so its 32 value bytes genuinely leave the BAL; refunding only when bytes provably leave the BAL keeps the meter an upper bound and never under-counts, so the block-size bound holds. This is deliberately narrower than the EIP-3529 SSTORE gas refund, which also fires when clearing a non-zero slot to zero — a case EIP-7928 still records as a write, and which therefore earns no byte refund. Keying the byte refund to the gas refund instead would drop bytes that remain in the BAL and break the bound.
set_delegation runs outside the EVM's exception handler. A runtime meter_bal_data raising OutOfGasError there would propagate uncaught and crash block processing. Folding the worst-case 51 BAL bytes per auth (on top of the 108-byte tuple priced by EIP-8131, for 159 B total) into the static floor lets set_delegation mutate state freely: any transaction whose tx.gas cannot cover it has already been rejected at validation.
The runtime mechanism does not deduct gas from the execution budget or pre-commit any gas. meter_bal_data only increments the explicit byte counter (bal_data_bytes) and the derived accumulator (floor_gas_used), and checks that the floor remains ≤ tx.gas. Execution gas accounting is untouched. At the end, tx.gasUsed = max(execution_gas_used, floor_gas_used); the floor accumulator decides only how much of the upfront-debited tx.gas × gas_price gets kept versus refunded.
max(execution, floor) preservedThe EIP-7623 / EIP-8131 charging shape is unchanged; only the floor side has a runtime tail. Transactions whose execution exceeds the floor pay exactly as today.
Hard fork.
The static floor takes tx_floor from EIP-8131 and adds 51 BAL bytes per authorization, plus runtime extensions of the floor accumulator for BAL bytes that opcodes contribute during execution. The combined floor per auth is (108 + 51) × 64 = 10,176 gas, still below the AUTH_PER_EMPTY_ACCOUNT = 25,000 intrinsic per auth. The minimum tx.gas for a transaction only changes when the floor side already dominated, i.e. for calldata-heavy or BAL-heavy transactions. A bare ETH transfer stays at 21,000.
Mainnet sample (1,500 random blocks, 441,271 transactions, 20,000 of those traced for BAL bytes, May 2026): EIP-8131 alone makes 3.24% of transactions pay more (+3.29% gas). EIP-8279 adds another 0.54 pp and +0.77 pp, for a combined 3.79% / +4.06%. The BAL piece never bites on its own: per-tx BAL bytes average 120-200 B after TX_BASE absorption, under 13 k floor gas, well below typical execution gas. EIP-8279's job is to close the calldata + cold-SLOAD bypass when paired with EIP-8131, not to add a standalone floor.
Wallets and eth_estimateGas MUST compute the new static floor (including the per-auth term) and, when tracing execution, meter the runtime BAL bytes for cold accesses, value-bearing calls, and deployments alongside intrinsic gas.
tx.to = recipient, tx.value = 1 ether, tx.data = b"".
static_floor = TX_BASE = 21,000 gas.tx.gas = 21,000, unchanged.SLOAD bypass attemptPre-EIP optimum at gas_limit = 60M: 937,500 non-zero calldata bytes plus 21,429 cold SLOADs in one transaction.
21,000 + 64 × 937,500 = 60,021,000.SLOAD floor extension: 32 × 64 = 2,048.SLOADs: 60,021,000 + 21,429 × 2,048 ≈ 103.9M, exceeding gas_limit. The transaction fails validation or hits OutOfGasError mid-execution.B = gas_limit / 64 = 937,500 block bytes regardless of the calldata / SLOAD split.SSTORE (zero → non-zero)Single cold SSTORE writing a non-zero value to an empty slot.
32 (key) + 32 (value) = 64 BAL bytes → 4,096 gas folded into floor_gas_used.SSTORE cost: 22,100. max(intrinsic, floor) = intrinsic. The floor accumulator is dominated by execution gas.SSTORE round-tripSlot starts at original value O; the transaction writes X ≠ O then writes O again.
SSTORE (O → X): 32 (cold key) + 32 (value) metered.SSTORE (X → O): slot is back at its pre-transaction value, a no-op write under EIP-7928; the 32 value bytes are refunded.32 (key only). The BAL records the slot in storage_reads, with no storage_changes value entry — the meter matches.Slot starts at O; the transaction writes A, then B, then C (all distinct, C ≠ O).
32 value bytes are metered once, on the O → A transition; A → B and B → C add nothing.32 (key) + 32 (value) = 64, matching the single post-value C the BAL records.CALL with valueCALL with value > 0 to a recipient that reverts.
floor_gas_used) before generic_call.bal_data_bytes nor the floor is rewound.Per transaction, every byte the tx contributes to the block (tx blob + BAL) is accounted for at the floor rate via either the static seed or a runtime extension, with implicit per-tx bytes covered by TX_BASE:
Summing across user transactions:
System-contract and withdrawal contributions are bounded separately by the EIP-7928 block-level ITEM_COST buffer (~1,428 items at a 60M gas limit), sized to absorb existing system contract use.
Accessed addresses and storage keys persist in the BAL across reverts (per EIP-7928); balance, nonce, code, and storage-value changes from reverted frames are discarded. Neither bal_data_bytes nor the derived floor is rewound for discarded entries. The storage-value refund fires only on an explicit SSTORE back to the pre-transaction value on the committed path — value bytes charged inside a frame that later reverts are left in place, not refunded. Safe: the meter over-counts, never under-counts.
The 32 storage-value bytes are metered once per slot — on the first SSTORE that moves it off its pre-transaction value — and refunded if it returns there, matching the single post-value the BAL records per changed slot. Repeated distinct writes (A → B → C) therefore meter 32 value bytes total, not 32 per write, and a net-zero round-trip (O → X → O) meters none. bal_data_bytes can still over-count where the BAL deduplicates further (e.g. value bytes charged in a frame that later reverts, never rewound); the bound holds.
Wallets and eth_estimateGas MUST simulate floor_gas_used alongside intrinsic gas. Without it, estimates for floor-bound transactions are too low and submissions will be rejected at validation.
Copyright and related rights waived via CC0.