Skip to main content

Try it Live

Run Transaction examples in the interactive playground

EIP-2930 Transactions

Access list transactions introduced in Berlin hard fork for gas optimization.

Overview

EIP-2930 transactions (Type 1) add access lists to pre-declare which accounts and storage slots will be accessed. This reduces gas costs by avoiding cold access penalties introduced in Berlin.

Type Definition

type EIP2930 = {
  type: Type.EIP2930      // 0x01
  chainId: bigint         // Explicit chain ID
  nonce: bigint
  gasPrice: bigint        // Fixed gas price (like Legacy)
  gasLimit: bigint
  to: AddressType | null
  value: bigint
  data: Uint8Array
  accessList: AccessList  // Pre-declared access
  yParity: number         // 0 or 1 (not v)
  r: Uint8Array           // 32 bytes
  s: Uint8Array           // 32 bytes
}

type AccessListItem = {
  address: AddressType
  storageKeys: readonly HashType[]
}

type AccessList = readonly AccessListItem[]
Source: types.ts:74-90

Creating EIP-2930 Transactions

import * as Transaction from 'tevm/Transaction'

// Without access list (same cost as Legacy)
const basic: Transaction.EIP2930 = {
  type: Transaction.Type.EIP2930,
  chainId: 1n,
  nonce: 0n,
  gasPrice: 20000000000n,
  gasLimit: 21000n,
  to: recipientAddress,
  value: 1000000000000000000n,
  data: new Uint8Array(),
  accessList: [],
  yParity: 0,
  r: signatureR,
  s: signatureS,
}

// With access list for gas savings
const withAccessList: Transaction.EIP2930 = {
  type: Transaction.Type.EIP2930,
  chainId: 1n,
  nonce: 0n,
  gasPrice: 20000000000n,
  gasLimit: 100000n,
  to: contractAddress,
  value: 0n,
  data: encodedCall,
  accessList: [
    {
      address: contractAddress,
      storageKeys: [slot1, slot2, slot3]
    },
    {
      address: anotherContract,
      storageKeys: [slot4]
    }
  ],
  yParity: 0,
  r: signatureR,
  s: signatureS,
}

Access Lists

Purpose

Access lists reduce gas costs by pre-declaring account and storage access:
  • Cold account access: 2600 gas → 100 gas (list) + 2400 gas (access)
  • Cold storage access: 2100 gas → 1900 gas (list) + 100 gas (access)

Structure

type AccessListItem = {
  address: AddressType      // Contract address
  storageKeys: readonly HashType[]  // Storage slot hashes
}

type AccessList = readonly AccessListItem[]

Gas Cost

Adding to access list has upfront cost:
  • Per address: 2400 gas
  • Per storage key: 1900 gas
Break-even point:
  • Accessing address once: no savings (2400 + 2400 = 4800 vs 2600 + 2600 = 5200)
  • Accessing 2+ times: savings

Example

// Contract that reads same storage slot multiple times
const accessList: AccessList = [
  {
    address: tokenContract,
    storageKeys: [
      balanceSlot,  // Read 3 times in transaction
    ]
  }
]

// Gas cost breakdown:
// Without access list: 2100 + 100 + 100 = 2300 gas
// With access list: 1900 + 100 + 100 + 100 = 2200 gas
// Savings: 100 gas per transaction

Building Access Lists

eth_createAccessList RPC

Most nodes provide eth_createAccessList to automatically generate access lists:
// RPC call
const result = await provider.send('eth_createAccessList', [{
  from: senderAddress,
  to: contractAddress,
  data: encodedCall,
}])

// Returns
{
  accessList: [
    {
      address: '0x742d35Cc6634C0532925a3b844Bc9e7595f51e3e',
      storageKeys: [
        '0x0000000000000000000000000000000000000000000000000000000000000001',
        '0x0000000000000000000000000000000000000000000000000000000000000002'
      ]
    }
  ],
  gasUsed: '0x5208'
}

Manual Construction

import * as Transaction from 'tevm/Transaction'

// Calculate storage slot for mapping
function mapSlot(key: AddressType, slot: bigint): HashType {
  return keccak256(concat([
    zeroPadLeft(key, 32),
    zeroPadLeft(toBytes(slot), 32)
  ])) as HashType
}

// Build access list for ERC20 transfer
const accessList: Transaction.AccessList = [
  {
    address: tokenAddress,
    storageKeys: [
      mapSlot(senderAddress, 0n),    // balances[sender]
      mapSlot(recipientAddress, 0n), // balances[recipient]
    ]
  }
]

Methods

All standard transaction methods work with EIP-2930:
import { EIP2930 } from 'tevm/Transaction'

// Serialization
const bytes = EIP2930.serialize(eip2930Tx)
const decoded = EIP2930.deserialize(bytes)

// Hashing
const txHash = EIP2930.hash(eip2930Tx)
const signingHash = EIP2930.getSigningHash(eip2930Tx)

// Signing
const sender = EIP2930.getSender(eip2930Tx)
const isValid = EIP2930.verifySignature(eip2930Tx)

RLP Encoding

EIP-2930 uses typed transaction envelope:
0x01 || rlp([chainId, nonce, gasPrice, gasLimit, to, value, data, accessList, yParity, r, s])
Access list encoding:
accessList = [
  [address1, [storageKey1, storageKey2, ...]],
  [address2, [storageKey3, ...]],
  ...
]
Example:
// Transaction with access list
{
  type: 0x01,
  chainId: 1n,
  // ...
  accessList: [
    { address: addr1, storageKeys: [key1, key2] },
    { address: addr2, storageKeys: [] }
  ],
  // ...
}

// Encodes to:
// [0x01,  // Type byte
//  0xf8, 0x...,  // RLP list
//    ...
//    [  // Access list
//      [addr1_bytes, [key1_bytes, key2_bytes]],
//      [addr2_bytes, []
//    ],
//    yParity, r, s
//  ]
// ]

yParity vs v

EIP-2930 uses yParity (0 or 1) instead of v:
// Legacy
type Legacy = {
  v: bigint  // 27/28 or chainId * 2 + 35 + yParity
}

// EIP-2930
type EIP2930 = {
  chainId: bigint  // Explicit field
  yParity: number  // 0 or 1 only
}
Converting between formats:
// Legacy to EIP-2930
const yParity = Number(legacy.v % 2n)

// EIP-2930 to Legacy (for chain ID 1)
const v = 1n * 2n + 35n + BigInt(eip2930.yParity)

Gas Optimization Strategies

When to Use Access Lists

Access lists are beneficial when:
  1. Accessing same account/storage multiple times
  2. Complex contract interactions
  3. Gas savings > access list overhead

When NOT to Use

Access lists add overhead for:
  1. Simple ETH transfers (no storage access)
  2. Single storage reads
  3. Small transactions

Optimization Example

// Bad: Access list for simple transfer
const wasteful: Transaction.EIP2930 = {
  type: Transaction.Type.EIP2930,
  // ...
  to: recipientAddress,
  value: 1000000000000000000n,
  data: new Uint8Array(),
  accessList: [],  // Overhead with no benefit
  // ...
}

// Good: Access list for contract interaction
const optimized: Transaction.EIP2930 = {
  type: Transaction.Type.EIP2930,
  // ...
  to: defiContract,
  value: 0n,
  data: complexCallData,
  accessList: [
    {
      address: defiContract,
      storageKeys: [slot1, slot2, slot3]  // Read multiple times
    }
  ],
  // ...
}

Comparison with Legacy

FeatureLegacyEIP-2930
Type byteNone (RLP)0x01
Chain IDIn vExplicit
Signaturev/r/syParity/r/s
Gas pricegasPricegasPrice
Access listNoYes
Gas optimizationNoYes

Comparison with EIP-1559

FeatureEIP-2930EIP-1559
Type byte0x010x02
Gas pricingFixedDynamic
Access listYesYes
Base feeNoYes
Priority feeNoYes
EIP-2930 is a stepping stone between Legacy and EIP-1559 - it adds access lists but keeps fixed gas pricing.

When to Use

Use EIP-2930 for:
  • Gas optimization on Berlin+ networks
  • Fixed gas price with access list benefits
  • Networks without EIP-1559
Prefer EIP-1559 for:
  • Modern Ethereum (better gas pricing)
  • Most new applications
Prefer Legacy for:
  • Maximum compatibility
  • Pre-Berlin networks

EIP Reference