Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Contract Loader

Contract Loaders are actions that extract ABI information from contract bytecode (including unverified contracts). Powered by WhatsABI, available as runtime actions and buildtime macros:

  • Discover function selectors from bytecode
  • Look up function signatures from selectors
  • Resolve proxy contracts automatically
  • Access verified ABIs from Sourcify, Etherscan, and other sources

Features:

  • Works with unverified contracts (extracts selectors from bytecode)
  • Resolves proxy implementations (detects/follows proxy patterns)
  • Uses multiple sources (Sourcify, Etherscan, bytecode analysis)
  • Fully typed API

Usage

Tree-shakable API
import { loadContract, createMemoryClient, http } from 'tevm'
import { MultiABILoader, EtherscanABILoader, SourcifyABILoader } from 'tevm/whatsabi'
 
const client = createMemoryClient({
  fork: { transport: http('https://mainnet.optimism.io') }
})
 
const contract = await loadContract(client, {
  address: '0x00000000006c3852cbEf3e08E8dF289169EdE581', // Seaport
  followProxies: true,
  loaders: [
    new SourcifyABILoader(),
    new EtherscanABILoader({ apiKey: 'YOUR_ETHERSCAN_KEY' })
  ]
})
 
console.log(`Address: ${contract.address}`)
console.log(`ABI entries: ${contract.abi.length}`)
console.log(`Human readable ABI: ${contract.humanReadableAbi}`)
console.log(`Deployed bytecode available: ${Boolean(contract.deployedBytecode)}`)
console.log(`Implementation (if proxy): ${contract.proxyDetails?.[0]?.implementation || 'Not a proxy'}`)
 
const owner = await client.readContract({
  ...contract.read.owner(),
  address: contract.address
})

Network Imports via Macros

Import contracts from any network at build time.

Creating Contract Macros

Create a file exporting contracts via loadContract:

import { createClient, createMemoryClient } from "tevm";
import { http } from "viem";
import { mainnet } from "viem/chains";
import { loadContract } from "tevm";
import { EtherscanABILoader, SourcifyABILoader } from "tevm/whatsabi";
 
// Pin block height for hermetic builds
const client = createMemoryClient({
  fork: {
    transport: http("https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY"),
    blockNumber: 19000000n,
  },
});
 
const loaders = [
  new SourcifyABILoader(),
  new EtherscanABILoader({ apiKey: "YOUR_ETHERSCAN_KEY" }),
];
 
export const usdc = await loadContract(client, {
  address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
  followProxies: true,
  includeBytecode: true,
  includeSourceCode: true,
  loaders,
});
 
export const weth = await loadContract(client, {
  address: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
  followProxies: true,
  includeBytecode: true,
  includeSourceCode: true,
  loaders,
});

Using Contract Macros

Import with with { type: 'tevm' }:

import { usdc } from "./contract-macros.js" with { type: "tevm" };
import { weth } from "./contract-macros.js" with { type: "tevm" };
import { createMemoryClient } from "tevm";
 
const client = createMemoryClient();
 
const usdcBalance = await client.readContract({
  ...usdc.read.balanceOf("0x123..."),
  address: usdc.address,
});
 
const wethDeposit = await client.writeContract({
  ...weth.write.deposit(),
  value: 1000000000000000000n,
});

How Macros Work

  1. Build-time execution: Bundlers execute type: 'macro' imports during build.
  2. Contract resolution: WhatsABI fetches and analyzes the contract, resolving ABIs and proxies.
  3. Static code generation: The import is replaced with statically generated ABI/metadata.
  4. Type generation: TypeScript types for methods, events, properties.

Benefits

  • Build-time resolution (no runtime network requests)
  • Full TypeScript type safety
  • Proxy resolution
  • Works with unverified contracts via bytecode analysis
  • IDE autocomplete and hover documentation

How It Works

  1. Bytecode analysis: WhatsABI extracts function selectors by analyzing jump tables and bytecode patterns. Works for unverified contracts.
  2. Signature lookup: Selectors are looked up in signature databases (matches transfer, approve, etc.).
  3. Proxy detection: Detects ERC-1967, Transparent, Beacon patterns; resolves and follows implementations.
  4. On-chain source verification: Attempts to fetch verified sources from Sourcify, Etherscan, etc. based on chain ID.

Parameters

ParameterTypeDescription
addressAddressRequired. The contract address to analyze
followProxiesbooleanWhether to automatically follow proxy contracts. Default: false
includeBytecodebooleanWhether to include contract bytecode in the returned contract. Default: false
includeSourceCodebooleanWhether to include contract source code as SolcInputSources. Default: false
loadersABILoader[]Array of ABI loaders to use for resolving contract ABIs. See Available Loaders
enableExperimentalMetadatabooleanWhether to include experimental metadata like event topics. Default: false
signatureLookupSignatureLookup | falseCustom signature lookup or false to disable. Default: uses WhatsABI's default lookup
onProgress(phase: string, ...args: any[]) => voidProgress callback
onError(phase: string, error: Error) => boolean | voidError callback

Return Value

Returns a Tevm Contract instance:

PropertyTypeDescription
abiAbiThe resolved ABI from bytecode or verified sources
addressAddressThe contract address (may differ if proxies followed)
readObjectType-safe read methods for view/pure functions
writeObjectType-safe write methods for state-changing functions
eventsObjectType-safe event filters for subscription
withAddressFunctionCreate a new instance with a different address
abiLoadedFrom{name: string, url?: string}Where the ABI was loaded from
proxyDetailsArray<{name: string, implementation?: Address, selector?: string}>If proxy, details about the implementation
sourcesSolcInputSourcesIf includeSourceCode is true, contract source code files

Available Loaders

Import from tevm/whatsabi:

MultiABILoader

Tries multiple loaders until one succeeds.

import { MultiABILoader } from "tevm/whatsabi";
 
const loader = new MultiABILoader([
  new SourcifyABILoader(),
  new EtherscanABILoader({ apiKey: "YOUR_ETHERSCAN_KEY" }),
]);
 
const contract = await loadContract(client, {
  address: "0x123...",
  loaders: [loader],
});

EtherscanABILoader

Loads ABIs from Etherscan and similar explorers.

import { EtherscanABILoader } from "tevm/whatsabi";
 
const etherscan = new EtherscanABILoader({ apiKey: "YOUR_ETHERSCAN_KEY" });
 
const polygonscan = new EtherscanABILoader({
  apiKey: "YOUR_POLYGONSCAN_KEY",
  baseUrl: "https://api.polygonscan.com/api",
});

SourcifyABILoader

Loads ABIs from Sourcify's decentralized repository.

import { SourcifyABILoader } from "tevm/whatsabi";
 
const sourcify = new SourcifyABILoader();
 
const customSourcify = new SourcifyABILoader({
  baseUrl: "https://your-sourcify-server.com",
});

BlockscoutABILoader

Loads ABIs from Blockscout explorers.

import { BlockscoutABILoader } from "tevm/whatsabi";
 
const blockscout = new BlockscoutABILoader({ apiKey: "YOUR_BLOCKSCOUT_KEY" });
 
const customBlockscout = new BlockscoutABILoader({
  baseUrl: "https://your-blockscout-instance.com",
});

Examples

Resolving a Proxy Contract

import { loadContract, createMemoryClient } from "tevm";
import { http } from "viem";
import { SourcifyABILoader, EtherscanABILoader } from "tevm/whatsabi";
 
const client = createMemoryClient({
  fork: { transport: http("https://mainnet.infura.io/v3/YOUR-KEY") },
});
 
const contract = await loadContract(client, {
  address: "0x4f8AD938eBA0CD19155a835f617317a6E788c868",
  followProxies: true,
  loaders: [
    new SourcifyABILoader(),
    new EtherscanABILoader({ apiKey: "YOUR_KEY" }),
  ],
});
 
console.log(`Original: 0x4f8AD938eBA0CD19155a835f617317a6E788c868`);
console.log(`Implementation: ${contract.address}`);
console.log(`Proxies detected: ${contract.proxyDetails.length}`);

Working with Unverified Contracts

import { loadContract, createMemoryClient } from "tevm";
import { http } from "viem";
 
const client = createMemoryClient({
  fork: { transport: http("https://mainnet.infura.io/v3/YOUR-KEY") },
});
 
const contract = await loadContract(client, {
  address: "0xUnverifiedContractAddress",
  loaders: [], // bytecode analysis only
});
 
contract.abi
  .filter((item) => item.type === "function")
  .forEach((func) => console.log(`- ${func.name || "Unknown"}`));

Creating a Contract Macro

// aave-macro.js
import { createClient } from "tevm";
import { http } from "viem";
import { optimism } from "viem/chains";
import { loadContract } from "tevm";
import { SourcifyABILoader, EtherscanABILoader } from "tevm/whatsabi";
 
const client = createClient({
  chain: optimism,
  transport: http("https://mainnet.optimism.io"),
});
 
const loaders = [
  new SourcifyABILoader(),
  new EtherscanABILoader({
    apiKey: "YOUR_ETHERSCAN_KEY",
    baseUrl: "https://api-optimistic.etherscan.io/api",
  }),
];
 
export const aaveV3Pool = await loadContract(client, {
  address: "0x794a61358D6845594F94dc1DB02A252b5b4814aD", // Aave V3 Pool on Optimism
  followProxies: true,
  includeBytecode: true,
  includeSourceCode: true,
  loaders,
});
// app.js
import { aaveV3Pool } from "./aave-macro.js" with { type: "tevm" };
import { createPublicClient, http } from "viem";
import { optimism } from "viem/chains";
 
const client = createPublicClient({ chain: optimism, transport: http() });
 
const reserves = await client.readContract({
  ...aaveV3Pool.read.getReservesList(),
  address: aaveV3Pool.address,
});
 
console.log(`Aave reserves on Optimism: ${reserves.length}`);

Using Multiple Loaders

import { loadContract, createMemoryClient } from "tevm";
import { http } from "viem";
import {
  MultiABILoader,
  SourcifyABILoader,
  EtherscanABILoader,
  BlockscoutABILoader,
} from "tevm/whatsabi";
 
const client = createMemoryClient({
  fork: { transport: http("https://mainnet.infura.io/v3/YOUR-KEY") },
});
 
const multiLoader = new MultiABILoader([
  new SourcifyABILoader(),
  new EtherscanABILoader({ apiKey: "YOUR_ETHERSCAN_KEY" }),
  new BlockscoutABILoader({ apiKey: "YOUR_BLOCKSCOUT_KEY" }),
]);
 
const contract = await loadContract(client, {
  address: "0xVerifiedContractAddress",
  followProxies: true,
  loaders: [multiLoader],
  onProgress: (phase) => console.log(`Phase: ${phase}`),
});
 
const name = await client.readContract({
  ...contract.read.name(),
  address: contract.address,
});

Working with Source Code

import { loadContract, createMemoryClient } from "tevm";
import { http } from "viem";
import { SourcifyABILoader, EtherscanABILoader } from "tevm/whatsabi";
 
const client = createMemoryClient({
  fork: { transport: http("https://mainnet.infura.io/v3/YOUR-KEY") },
});
 
const contract = await loadContract(client, {
  address: "0xVerifiedContractAddress",
  followProxies: true,
  includeBytecode: true,
  includeSourceCode: true,
  loaders: [
    new SourcifyABILoader(),
    new EtherscanABILoader({ apiKey: "YOUR_ETHERSCAN_KEY" }),
  ],
});
 
if (contract.sources) {
  Object.keys(contract.sources).forEach((fileName) => {
    console.log(`- ${fileName}`);
    const sourceContent = contract.sources[fileName].content;
    console.log(`First 100 chars: ${sourceContent.substring(0, 100)}...`);
  });
}
 
const balance = await client.readContract({
  ...contract.read.balanceOf("0x123..."),
  address: contract.address,
});

Progress Tracking

import { loadContract, createMemoryClient } from "tevm";
import { http } from "viem";
import { SourcifyABILoader } from "tevm/whatsabi";
 
const progressTracker = (phase, ...args) => {
  switch (phase) {
    case "bytecode": console.log("Analyzing bytecode..."); break;
    case "proxy": console.log("Checking proxy patterns..."); break;
    case "abi": console.log("Loading ABI..."); break;
  }
};
 
const contract = await loadContract(client, {
  address: "0xLargeContractAddress",
  followProxies: true,
  loaders: [new SourcifyABILoader()],
  onProgress: progressTracker,
});

When to Use Contract Loader vs Direct Solidity Imports

Use Contract Loader when:
  • Working with third-party contracts (source of truth managed elsewhere)
  • Needing latest contract implementation
  • Dealing with unverified contracts (extract from bytecode)
  • Interacting with proxy contracts (auto-resolve implementations)
  • Building exploratory tools
  • Creating SDKs for protocols (macros generate type-safe interfaces at build time)
Use Direct Solidity Imports when:
  • You own the contract code
  • Need fully hermetic, deterministic builds
  • Working with fixed contract versions
  • Need complete control over ABI generation

Ensuring Hermetic Builds

  1. Pin block heights: Use createMemoryClient with blockNumber:
const client = createMemoryClient({
  fork: {
    transport: http("https://mainnet.infura.io"),
    blockNumber: 19000000n,
  },
});
  1. Generate and commit contracts:
tevm generate contract-loader --address 0x123... --output ./src/contracts
  1. Use published npm packages when available:
npm install @uniswap/v3-core