EIP-7971 - Hard Limits for Transient Storage

Created 2025-06-12
Status Draft
Category Core
Type Standards Track
Authors
Requires

Abstract

This EIP proposes to reduce the gas costs for transient storage operations (TLOAD and TSTORE) by implementing constant pricing. To prevent denial-of-service attacks through excessive memory allocation, a transaction-global limit on transient storage slots is introduced. This approach provides lower costs for common use cases while maintaining security against resource exhaustion attacks.

Motivation

EIP-1153 introduced transient storage with gas costs equivalent to warm storage operations (100 gas). The current pricing model presents several limitations:

  1. Reentrancy Protection Cost: At 100 gas per operation, implementing reentrancy locks by default remains expensive enough to discourage universal adoption at the language level, leaving contracts vulnerable to one of the most common attack vectors.
  2. Underutilization: The high cost is still punishing to developers who wish to use transient storage for other legitimate use cases such as temporary approvals, callback metadata, and cross-frame communication within transactions.
  3. Pricing Inconsistency: Transient storage fundamentally requires fewer resources than persistent storage (no disk I/O, no state root updates), yet is priced identically to warm storage operations.

This EIP addresses these issues by implementing constant, lower pricing for transient storage operations while introducing a transaction-global limit to prevent denial-of-service attacks. It also makes the cost of warm storage cheaper. This provides lower costs and enables broader adoption of transient storage, while also providing hard resource limits for clients.

Specification

Parameters

This EIP introduces the following parameters:

Constant Value Description
GAS_TLOAD 5 Gas cost of TLOAD
GAS_TSTORE 12 Gas cost of TSTORE
MAX_TRANSIENT_SLOTS 131072 The maximum number of transient slots allowed in a single transaction
GAS_TSTORE_ALLOCATE 24 Cost of additional allocated slots

Gas Cost Changes

  1. The gas cost for TLOAD (opcode 0x5c) is reduced from GAS_WARM_ACCESS (100) to GAS_TLOAD.
  2. The base gas cost for TSTORE (opcode 0x5d) is reduced from GAS_WARM_ACCESS (100) to GAS_TSTORE.

Transaction-Global Transient Storage Limit

A transaction-global counter tracks the number of unique transient storage slots written across all contracts during transaction execution:

  1. At the beginning of each transaction, initialize a counter transient_slots_used to 0.
  2. When TSTORE is executed:
  3. If the slot has not been written to during this transaction (across any contract), increment transient_slots_used.
  4. If transient_slots_used exceeds MAX_TRANSIENT_SLOTS, the transaction MUST exceptionally halt.
  5. The counter persists across all message calls in the transaction.
  6. The counter is reset to 0 at the end of the transaction.

Implementation Note

Implementations MUST track transient storage allocated across all contracts. A slot is considered unique based on the tuple (contract_address, storage_key). Writing to the same slot multiple times within a transaction does not generally increment the counter after the first write (unless the slot gets deallocated with a revert).

Rationale

Constant Pricing with Hard Limit

This EIP implements constant pricing with a hard limit for several reasons:

  1. A hard limit provides guarantees on the total resource consumption which are easier to reason about for clients, rather than needing to perform a calculation as a function of current gas limits to find out what total memory consumption could be.
  2. Common use cases (e.g., reentrancy locks using 1-2 slots) are not penalized for worst case resource usage (like in DOS attacks).
  3. Clients can safely pre-reserve all memory which could be used by transient storage up-front at the beginning of a transaction.

Gas Cost Selection

TSTORE currently has a fixed cost. However, writing to fresh slots requires memory allocation, which is more expensive than writing to an existing slot. Therefore, we may consider introducing charging more for the first slot allocation through the parameter GAS_TSTORE_ALLOCATE. However, we would also need to introduce a mechanism to check for the first slot allocation versus subsequent allocations.

Benchmarking

This proposal does not yet have finalized numbers. To achieve this, we require benchmarks on the transient memory operations, which are currently in development. Once we collect that data, we will set the final numbers. We will also use this to understand whether the difference in performance justifies pricing new slot allocations differently.

We should note that warm storage loads from cache are expected to have similar performance characteristics to transient storage reads. Therefore, the final parameters should be consistent with EIP-8038.

<– TODO –>

Hard Limit Selection

MAX_TRANSIENT_SLOTS of 131072 allows:

Design Alternatives Considered

  1. Per-Contract Limits: Increased complexity in reasoning about resource consumption based on the shape of the call stack.
  2. Superlinear Pricing: Adds complexity, and still punishes "common" (non-DOS) use cases.
  3. No Limit: May allow memory-based DOS attack if transaction-level gas limits change, or if pricing changes in the future.

The benefit of a hard limit is that the resource consumption is bounded predictably even in the presence of other parameter changes in the protocol.

Backwards Compatibility

No known issues, besides the gas cost of existing operations being cheaper.

Test Cases

TBD

Reference Implementation

This reference implementation includes the mechanism to price TSTORE depending on whether the slot was previously allocated.

# Pseudo-code for transaction execution with global transient storage limit

GAS_TLOAD = 5
GAS_TSTORE = 12
GAS_TSTORE_ALLOCATE = 24
MAX_TRANSIENT_SLOTS = 131072

class TransactionContext:
    def __init__(self):
        self.transient_storage = {}  # (address, key) -> value
        self.unique_slots = set()    # set of (address, key) tuples
        self.transient_slots_used = 0

    def tload(self, address: Address, key: Bytes32) -> Bytes32:
        # Charge gas
        self.charge_gas(GAS_TLOAD)

        # Return value or zero
        return self.transient_storage.get((address, key), Bytes32(0))

    def tstore(self, address: Address, key: Bytes32, value: Bytes32):
        # Charge gas
        self.charge_gas(GAS_TSTORE)

        # Check if this is a new unique slot
        slot_id = (address, key)
        if slot_id not in self.unique_slots:
            self.charge_gas(GAS_TSTORE_ALLOCATE)

            self.unique_slots.add(slot_id)

            # Check limit
            if len(self.unique_slots) > MAX_TRANSIENT_SLOTS:
                raise ExceptionalHalt("Transient storage limit exceeded")

        # Store value
        self.transient_storage[slot_id] = value

Security Considerations

With MAX_TRANSIENT_SLOTS = 131072, maximum memory allocation is bounded to 8 MB per transaction (131072 * 64 bytes). Compared to limits under current pricing (100 gas), a 60M gas transaction can allocate up to 600,000 slots (38.4 MB). This EIP reduces the maximum allocated amount by 79%.

Copyright

Copyright and related rights waived via CC0.