Skip to main content

Documentation Index

Fetch the complete documentation index at: https://voltaire.tevm.sh/llms.txt

Use this file to discover all available pages before exploring further.

Common patterns for working with storage keys and EVM state management.

Basic Storage Operations

Track contract storage across multiple contracts:
import * as State from 'tevm/State';
import * as Address from 'tevm/Address';

// Create storage database
const storage = new Map<string, bigint>();

// USDC contract
const usdcAddress = Address("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
const usdcTotalSupply = State.StorageKey(usdcAddress, 0n);
storage.set(State.StorageKey.toString(usdcTotalSupply), 1000000000n);

// DAI contract
const daiAddress = Address("0x6B175474E89094C44Da98b954EedeAC495271d0F");
const daiTotalSupply = State.StorageKey(daiAddress, 0n);
storage.set(State.StorageKey.toString(daiTotalSupply), 5000000000n);

// Query specific slot
const value = storage.get(State.StorageKey.toString(usdcTotalSupply));
console.log(value); // 1000000000n

Computing Mapping Slots

Calculate storage slots for Solidity mappings:
import * as State from 'tevm/State';
import { Keccak256 } from 'tevm/crypto';

function computeMappingSlot(key: Uint8Array, baseSlot: bigint): bigint {
  // Solidity: keccak256(abi.encode(key, baseSlot))
  const encoded = Bytes64();

  // Pad key to 32 bytes
  encoded.set(new Uint8Array(32 - key.length).fill(0), 0);
  encoded.set(key, 32 - key.length);

  // Encode baseSlot as 32 bytes
  const slotBytes = Bytes32();
  const view = new DataView(slotBytes.buffer);
  view.setBigUint64(24, baseSlot, false);
  encoded.set(slotBytes, 32);

  const hash = Keccak256.hash(encoded);
  return BigInt(Hex.fromBytes(hash));
}

// Example: balances[userAddress] in ERC-20
const tokenAddress = Address("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48");
const userAddress = Address("0x742d35Cc6634C0532925a3b844Bc9e7595f51e3e");

const balancesBaseSlot = 1n; // Slot where balances mapping is declared
const userBalanceSlot = computeMappingSlot(userAddress, balancesBaseSlot);
const userBalanceKey = State.StorageKey(tokenAddress, userBalanceSlot);

console.log(`User balance at slot: ${userBalanceSlot}`);

Nested Mappings

Handle nested mappings like mapping(address => mapping(address => uint256)):
// allowances[owner][spender] in ERC-20
const allowancesBaseSlot = 2n;

// First level: keccak256(abi.encode(owner, allowancesBaseSlot))
const ownerSlot = computeMappingSlot(ownerAddress, allowancesBaseSlot);

// Second level: keccak256(abi.encode(spender, ownerSlot))
const allowanceSlot = computeMappingSlot(spenderAddress, ownerSlot);

const allowanceKey = State.StorageKey(tokenAddress, allowanceSlot);
storage.set(State.StorageKey.toString(allowanceKey), 1000000n);

Storage Change Tracking

Track storage modifications:
class StorageTracker {
  private original = new Map<string, bigint>();
  private current = new Map<string, bigint>();

  load(key: BrandedStorageKey, value: bigint): void {
    const keyStr = State.StorageKey.toString(key);
    this.original.set(keyStr, value);
    this.current.set(keyStr, value);
  }

  set(key: BrandedStorageKey, value: bigint): void {
    const keyStr = State.StorageKey.toString(key);
    this.current.set(keyStr, value);
  }

  getChanges(): Array<{ key: BrandedStorageKey; before: bigint; after: bigint }> {
    const changes: Array<{ key: BrandedStorageKey; before: bigint; after: bigint }> = [];

    for (const [keyStr, afterValue] of this.current) {
      const beforeValue = this.original.get(keyStr) ?? 0n;
      if (beforeValue !== afterValue) {
        const key = State.StorageKey(keyStr);
        changes.push({ key, before: beforeValue, after: afterValue });
      }
    }

    return changes;
  }
}

// Usage
const tracker = new StorageTracker();
tracker.load(State.StorageKey(contractAddr, 0n), 100n);
tracker.set(State.StorageKey(contractAddr, 0n), 200n);

const changes = tracker.getChanges();
// [{ key: {...}, before: 100n, after: 200n }]

Multi-Contract State Manager

Manage state across multiple contracts:
class StateManager {
  private storage = new Map<string, bigint>();

  get(address: AddressType, slot: bigint): bigint {
    const key = State.StorageKey(address, slot);
    return this.storage.get(State.StorageKey.toString(key)) ?? 0n;
  }

  set(address: AddressType, slot: bigint, value: bigint): void {
    const key = State.StorageKey(address, slot);
    this.storage.set(State.StorageKey.toString(key), value);
  }

  getContractStorage(address: AddressType): Map<bigint, bigint> {
    const result = new Map<bigint, bigint>();

    for (const [keyStr, value] of this.storage) {
      const key = State.StorageKey(keyStr);
      if (Address.equals(key.address, address)) {
        result.set(key.slot, value);
      }
    }

    return result;
  }

  clear(address: AddressType): void {
    const toDelete: string[] = [];

    for (const keyStr of this.storage.keys()) {
      const key = State.StorageKey(keyStr);
      if (Address.equals(key.address, address)) {
        toDelete.push(keyStr);
      }
    }

    for (const keyStr of toDelete) {
      this.storage.delete(keyStr);
    }
  }
}

Persistent Storage

Save and load state from disk:
import * as fs from 'fs/promises';

async function saveState(
  storage: Map<string, bigint>,
  filePath: string
): Promise<void> {
  const entries = Array(storage.entries()).map(([key, value]) => ({
    key,
    value: value.toString()
  }));

  await fs.writeFile(filePath, JSON.stringify(entries, null, 2));
}

async function loadState(filePath: string): Promise<Map<string, bigint>> {
  const data = await fs.readFile(filePath, 'utf-8');
  const entries = JSON.parse(data);

  const storage = new Map<string, bigint>();
  for (const { key, value } of entries) {
    storage.set(key, BigInt(value));
  }

  return storage;
}

// Usage
const storage = new Map<string, bigint>();
storage.set(State.StorageKey.toString(key1), 1000n);
await saveState(storage, './state.json');

const loaded = await loadState('./state.json');

Storage Diff

Compare storage states:
function diffStorage(
  before: Map<string, bigint>,
  after: Map<string, bigint>
): {
  added: Array<{ key: BrandedStorageKey; value: bigint }>;
  modified: Array<{ key: BrandedStorageKey; before: bigint; after: bigint }>;
  deleted: Array<{ key: BrandedStorageKey; value: bigint }>;
} {
  const added: Array<{ key: BrandedStorageKey; value: bigint }> = [];
  const modified: Array<{ key: BrandedStorageKey; before: bigint; after: bigint }> = [];
  const deleted: Array<{ key: BrandedStorageKey; value: bigint }> = [];

  // Find added and modified
  for (const [keyStr, afterValue] of after) {
    const beforeValue = before.get(keyStr);
    const key = State.StorageKey(keyStr);

    if (beforeValue === undefined) {
      added.push({ key, value: afterValue });
    } else if (beforeValue !== afterValue) {
      modified.push({ key, before: beforeValue, after: afterValue });
    }
  }

  // Find deleted
  for (const [keyStr, beforeValue] of before) {
    if (!after.has(keyStr)) {
      const key = State.StorageKey(keyStr);
      deleted.push({ key, value: beforeValue });
    }
  }

  return { added, modified, deleted };
}

See Also