Skip to main content

Try it Live

Run AccessList examples in the interactive playground

Usage Patterns

Common patterns and best practices for working with AccessList.

Building Access Lists

Incremental Construction

// Start empty and build up
let list = AccessList.create();

// Add contracts one by one
list = list.withAddress(routerAddress);
list = list.withAddress(factoryAddress);
list = list.withAddress(tokenAddress);

// Add storage keys
list = list.withStorageKey(tokenAddress, balanceSlot);
list = list.withStorageKey(tokenAddress, allowanceSlot);

// Optimize once at end
list = list.deduplicate();

Batch Construction

// From known items
const items: Item[] = [
  { address: router, storageKeys: [] },
  { address: token0, storageKeys: [balanceSlot, allowanceSlot] },
  { address: token1, storageKeys: [balanceSlot] }
];

const list = AccessList(items).deduplicate();

Conditional Building

function buildAccessList(config: Config): BrandedAccessList {
  let list = AccessList.create();

  // Required contracts
  list = list.withAddress(config.router);

  // Optional contracts
  if (config.needsFactory) {
    list = list.withAddress(config.factory);
  }

  if (config.needsPriceOracle) {
    list = list.withAddress(config.oracle);
    list = list.withStorageKey(config.oracle, config.priceSlot);
  }

  return list.deduplicate();
}

Builder Pattern

class AccessListBuilder {
  private list: BrandedAccessList = AccessList.create();

  addAddress(address: AddressType): this {
    this.list = this.list.withAddress(address);
    return this;
  }

  addStorageKey(address: AddressType, key: HashType): this {
    this.list = this.list.withStorageKey(address, key);
    return this;
  }

  addKeys(address: AddressType, keys: HashType[]): this {
    for (const key of keys) {
      this.list = this.list.withStorageKey(address, key);
    }
    return this;
  }

  build(): BrandedAccessList {
    return this.list.deduplicate();
  }
}

// Usage
const list = new AccessListBuilder()
  .addAddress(router)
  .addKeys(token, [balanceSlot, allowanceSlot])
  .build();

DeFi Patterns

Uniswap V2 Swap

function buildSwapAccessList(
  router: AddressType,
  pair: AddressType,
  token0: AddressType,
  token1: AddressType,
  user: AddressType
): BrandedAccessList {
  const balanceSlot0 = calculateBalanceSlot(token0, user);
  const balanceSlot1 = calculateBalanceSlot(token1, user);
  const allowanceSlot0 = calculateAllowanceSlot(token0, user, router);
  const reserve0Slot = Hash('0x08');  // reserves slot
  const reserve1Slot = Hash('0x08');

  let list = AccessList.create();

  // Router
  list = list.withAddress(router);

  // Pair contract
  list = list.withAddress(pair);
  list = list.withStorageKey(pair, reserve0Slot);

  // Token0
  list = list.withStorageKey(token0, balanceSlot0);
  list = list.withStorageKey(token0, allowanceSlot0);

  // Token1
  list = list.withStorageKey(token1, balanceSlot1);

  return list.deduplicate();
}

Aave Borrow

function buildBorrowAccessList(
  pool: AddressType,
  asset: AddressType,
  user: AddressType
): BrandedAccessList {
  // Calculate storage slots
  const userConfig = calculateUserConfigSlot(pool, user);
  const reserve = calculateReserveSlot(pool, asset);
  const debt = calculateDebtSlot(asset, user);
  const balance = calculateBalanceSlot(asset, user);

  return AccessList.create()
    .withAddress(pool)
    .withStorageKey(pool, userConfig)
    .withStorageKey(pool, reserve)
    .withStorageKey(asset, debt)
    .withStorageKey(asset, balance)
    .deduplicate();
}

ERC-20 Batch Transfer

function buildBatchTransferAccessList(
  token: AddressType,
  sender: AddressType,
  recipients: AddressType[]
): BrandedAccessList {
  let list = AccessList.create();

  // Token contract
  list = list.withAddress(token);

  // Sender balance
  const senderSlot = calculateBalanceSlot(token, sender);
  list = list.withStorageKey(token, senderSlot);

  // Recipient balances
  for (const recipient of recipients) {
    const recipientSlot = calculateBalanceSlot(token, recipient);
    list = list.withStorageKey(token, recipientSlot);
  }

  return list.deduplicate();
}

Transaction Integration

Before Sending

async function sendWithAccessList(
  tx: Transaction
): Promise<TransactionReceipt> {
  // Build access list
  let list = buildAccessListForTransaction(tx);

  // Optimize
  list = list.deduplicate();

  // Check if beneficial
  if (!list.hasSavings()) {
    console.log('Skipping unprofitable access list');
    list = AccessList.create();
  }

  // Encode and send
  return await provider.sendTransaction({
    ...tx,
    type: 2,  // EIP-1559
    accessList: list
  });
}

Auto-detection

async function detectAccessList(
  tx: Transaction
): Promise<BrandedAccessList> {
  // Use eth_createAccessList RPC
  const detected = await provider.send('eth_createAccessList', [
    {
      from: tx.from,
      to: tx.to,
      data: tx.data,
      gas: tx.gasLimit
    }
  ]);

  // Convert to our format
  const list = AccessList(detected.accessList);

  // Validate and optimize
  AccessList.assertValid(list);
  return list.deduplicate();
}

Gas Estimation

async function estimateWithAccessList(
  tx: Transaction
): Promise<{ withList: bigint; without: bigint; beneficial: boolean }> {
  // Build access list
  const list = buildAccessListForTransaction(tx);

  // Estimate with access list
  const withList = await provider.estimateGas({
    ...tx,
    accessList: list
  });

  // Estimate without
  const without = await provider.estimateGas({
    ...tx,
    accessList: []
  });

  return {
    withList,
    without,
    beneficial: withList < without
  };
}

Optimization Patterns

Merge Multiple Lists

function combineAccessLists(
  ...txs: Transaction[]
): BrandedAccessList {
  const lists = txs
    .map(tx => tx.accessList)
    .filter(list => list && !list.isEmpty());

  if (lists.length === 0) {
    return AccessList.create();
  }

  return AccessList.merge(...lists);
}

Deduplicate Strategy

// Strategy 1: Deduplicate at end
function strategy1(): BrandedAccessList {
  let list = AccessList.create();
  for (const addr of addresses) {
    list = list.withAddress(addr);
  }
  return list.deduplicate();  // Once at end
}

// Strategy 2: Check before adding
function strategy2(): BrandedAccessList {
  let list = AccessList.create();
  for (const addr of addresses) {
    if (!list.includesAddress(addr)) {
      list = list.withAddress(addr);
    }
  }
  return list;  // No deduplication needed
}

// Strategy 1 faster for many items
// Strategy 2 faster for few items

Size Limits

function limitAccessListSize(
  list: BrandedAccessList,
  maxGas: bigint
): BrandedAccessList {
  const cost = list.gasCost();

  if (cost <= maxGas) {
    return list;
  }

  // Sort by value (most keys first)
  const sorted = [...list].sort(
    (a, b) => b.storageKeys.length - a.storageKeys.length
  );

  // Build up to budget
  let result = AccessList.create();
  for (const item of sorted) {
    const withItem = result.withAddress(item.address);

    for (const key of item.storageKeys) {
      const withKey = withItem.withStorageKey(item.address, key);

      if (withKey.gasCost() <= maxGas) {
        result = withKey;
      } else {
        break;
      }
    }

    if (result.gasCost() >= maxGas) {
      break;
    }
  }

  return result;
}

Analysis Patterns

Complete Analysis

function analyzeAccessList(list: BrandedAccessList) {
  const addresses = list.addressCount();
  const keys = list.storageKeyCount();
  const cost = list.gasCost();
  const savings = list.gasSavings();
  const net = savings - cost;

  return {
    addresses,
    keys,
    avgKeysPerAddress: addresses > 0 ? keys / addresses : 0,
    cost,
    savings,
    net,
    beneficial: net > 0n,
    costPerAddress: addresses > 0 ? cost / BigInt(addresses) : 0n,
    costPerKey: keys > 0 ? cost / BigInt(keys) : 0n
  };
}

Comparison

function compareAccessLists(
  lists: BrandedAccessList[]
): BrandedAccessList {
  const analyses = lists.map(list => ({
    list,
    ...analyzeAccessList(list)
  }));

  // Sort by net benefit
  analyses.sort((a, b) => {
    const diff = a.net - b.net;
    return diff > 0n ? -1 : diff < 0n ? 1 : 0;
  });

  return analyses[0].list;
}

Debug Output

function debugAccessList(list: BrandedAccessList): void {
  console.log('Access List Summary:');
  console.log(`  Addresses: ${list.addressCount()}`);
  console.log(`  Storage Keys: ${list.storageKeyCount()}`);
  console.log(`  Gas Cost: ${list.gasCost()}`);
  console.log(`  Gas Savings: ${list.gasSavings()}`);
  console.log(`  Net: ${list.gasSavings() - list.gasCost()}`);
  console.log(`  Beneficial: ${list.hasSavings()}`);

  console.log('\nDetailed Items:');
  for (const item of list) {
    console.log(`  ${item.address.toHex()}:`);
    console.log(`    Keys: ${item.storageKeys.length}`);
    for (const key of item.storageKeys) {
      console.log(`      ${key.toHex()}`);
    }
  }
}

Error Handling

Safe Construction

function safeConstruction(data: unknown): BrandedAccessList {
  try {
    // Try to construct
    if (data instanceof Uint8Array) {
      const list = AccessList(data);
      AccessList.assertValid(list);
      return list;
    }

    if (Array.isArray(data)) {
      const list = AccessList(data);
      AccessList.assertValid(list);
      return list;
    }

    throw new Error('Invalid data type');
  } catch (err) {
    console.error('Failed to construct access list:', err);
    return AccessList.create();
  }
}

Fallback Strategies

function buildWithFallback(
  primary: () => BrandedAccessList,
  fallback: () => BrandedAccessList
): BrandedAccessList {
  try {
    const list = primary();
    AccessList.assertValid(list);
    return list.deduplicate();
  } catch (err) {
    console.warn('Primary strategy failed, using fallback:', err);
    try {
      const list = fallback();
      AccessList.assertValid(list);
      return list.deduplicate();
    } catch {
      return AccessList.create();
    }
  }
}

Testing Patterns

Test Fixtures

const fixtures = {
  empty: AccessList.create(),

  singleAddress: AccessList([
    { address: testAddr1, storageKeys: [] }
  ]),

  withKeys: AccessList([
    { address: testAddr1, storageKeys: [testKey1, testKey2] }
  ]),

  multipleAddresses: AccessList([
    { address: testAddr1, storageKeys: [testKey1] },
    { address: testAddr2, storageKeys: [testKey2] }
  ])
};

Invariant Checks

function checkInvariants(list: BrandedAccessList): void {
  // Valid structure
  AccessList.assertValid(list);

  // Deduplicated
  const deduped = list.deduplicate();
  assert.equal(deduped.length, list.length, 'Should be deduplicated');

  // Consistent counts
  const manual = list.reduce(
    (sum, item) => sum + item.storageKeys.length,
    0
  );
  assert.equal(list.storageKeyCount(), manual, 'Key count mismatch');
}

See Also