Skip to content

Contract Loader

Overview

Contract Loaders are powerful actions that can extract ABI information from contract bytecode, even for unverified contracts. Powered by WhatsABI, Tevm's Contract Loader can be used both as runtime actions and as buildtime macros, allowing you to:

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

🧩 Works with Unverified Contracts

Extract function selectors directly from bytecode

🔄 Resolves Proxy Implementations

Automatically detects and follows proxy patterns

📚 Uses Multiple Sources

Combines Sourcify, Etherscan and bytecode analysis

🛠️ Fully Typed API

Complete TypeScript support for parameters and results

Usage

import { loadContract, createMemoryClient, http } from 'tevm'
import { MultiABILoader, EtherscanABILoader, SourcifyABILoader } from 'tevm/whatsabi'
 
const client = createMemoryClient({ 
  fork: { transport: http('https://mainnet.optimism.io') } 
})
 
// loadContract returns a fully typed Contract instance 
const contract = await loadContract(client, { 
  address: '0x00000000006c3852cbEf3e08E8dF289169EdE581', // Seaport contract 
  followProxies: true, 
  // Use multiple loaders to find ABIs from different sources
  loaders: [
    new SourcifyABILoader(),
    new EtherscanABILoader({ apiKey: 'YOUR_ETHERSCAN_KEY' })
  ]
})
 
// Contract is a fully typed Tevm contract instance 
console.log(`Contract address: ${contract.address}`) 
console.log(`Contract has ${contract.abi.length} ABI entries`) 
 
// Access additional properties
console.log(`Human readable ABI: ${contract.humanReadableAbi}`)
console.log(`Deployed bytecode available: ${Boolean(contract.deployedBytecode)}`)
console.log(`Implementation address (if proxy): ${contract.proxyDetails?.[0]?.implementation || 'Not a proxy'}`)
 
// Use the contract for type-safe interactions
const owner = await client.readContract({
  ...contract.read.owner(),
  address: contract.address
})
import { createClient, contractLoaderExtension } from 'tevm'
import { http } from 'viem'
import { BlockscoutABILoader, SourcifyABILoader } from 'tevm/whatsabi'
 
// Configure loaders and other options in the extension 
const client = createClient({ 
  transport: http('https://mainnet.optimism.io') 
}).extend(contractLoaderExtension({ 
  // Default options used for all contract loading 
  followProxies: true, 
  loaders: [ 
    new SourcifyABILoader(), 
    new BlockscoutABILoader({ apiKey: 'YOUR_BLOCKSCOUT_KEY' }) 
  ] 
})) 
 
// Now you can load contracts by just providing the address 
const contract = await client.loadContract({ 
  address: '0x00000000006c3852cbEf3e08E8dF289169EdE581'
}) 
 
// Contract is a Tevm Contract instance 
// Use it with client methods for type-safe interactions 
const balance = await client.readContract({ 
  ...contract.read.balanceOf('0x123...'), 
  address: contract.address 
}) 
 
// Override default options when needed
const anotherContract = await client.loadContract({
  address: '0x456...',
  // Override default options
  followProxies: false
})

Network Imports via Macros

One of the most powerful features of Contract Loader in Tevm is the ability to import contracts from any network at build time using macros.

Creating Contract Macros

First, create a file that exports functions using loadContract to resolve contract data:

import { createClient, createMemoryClient } from "tevm";
import { http } from "viem";
import { mainnet } from "viem/chains";
import { loadContract } from "tevm";
import { EtherscanABILoader, SourcifyABILoader } from "tevm/whatsabi";
 
// For hermetic builds, use a memory client with a fork at specific block
// This ensures deterministic builds with reproducible contract data
const client = createMemoryClient({
  fork: {
    transport: http("https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY"),
    // Pinning a block height ensures your builds are reproducible
    blockNumber: 19000000n,
  },
});
 
// Configure loaders
const loaders = [
  new SourcifyABILoader(),
  new EtherscanABILoader({ apiKey: "YOUR_ETHERSCAN_KEY" }),
];
 
// Using top-level await to pre-load contracts
// Note: This requires Node.js 14.8+ or setting "module": "esnext" in tsconfig
// Directly export the contract instances
export const usdc = await loadContract(client, {
  address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC on Ethereum
  followProxies: true,
  includeBytecode: true,
  includeSourceCode: true,
  loaders,
});
 
export const weth = await loadContract(client, {
  address: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", // WETH on Ethereum
  followProxies: true,
  includeBytecode: true,
  includeSourceCode: true,
  loaders,
});

Using Contract Macros

Then import the contracts with with { type: 'tevm' } attribute:

// Import contracts using macros
import { usdc } from "./contract-macros.js" with { type: "tevm" };
import { weth } from "./contract-macros.js" with { type: "tevm" };
import { createMemoryClient } from "tevm";
 
// Create client for local interaction
const client = createMemoryClient();
 
// Use the contracts with full type safety
const usdcBalance = await client.readContract({
  ...usdc.read.balanceOf("0x123..."),
  address: usdc.address,
});
 
// WETH contract methods are fully typed
const wethDeposit = await client.writeContract({
  ...weth.write.deposit(),
  value: 1000000000000000000n,
});

How Macros Work

1. Build-Time Execution

When your bundler encounters an import with type: 'macro', it executes the imported function during the build process.

2. Contract Resolution

The function uses WhatsABI to fetch and analyze the contract from the blockchain, resolving ABIs and following any proxies.

3. Static Code Generation

The bundler replaces the import with statically generated code that includes the full contract ABI and metadata.

4. Type Generation

TypeScript types are generated for all contract methods, events, and properties, ensuring full type safety.

Benefits of Macros

  • Build-time resolution - No network requests during application runtime
  • Full type safety - Complete TypeScript types for all contract methods
  • Proxy resolution - Automatically resolves and follows proxy implementations
  • Works with unverified contracts - Uses bytecode analysis when sources aren't available
  • IDE integration - Autocompletion and hover documentation for contract methods

How It Works

1. Bytecode Analysis

WhatsABI extracts function selectors from the contract's bytecode by analyzing jump tables and other bytecode patterns. This works for any contract, even if it's not verified on block explorers.

2. Signature Lookup

The extracted function selectors are looked up in signature databases to match known function signatures. This helps identify common functions like transfer, approve, etc.

3. Proxy Detection

WhatsABI checks for common proxy patterns (ERC-1967, Transparent, Beacon, etc.) and can automatically resolve and follow these implementations.

4. On-chain Source Verification

WhatsABI attempts to find verified contract sources from Sourcify, Etherscan, and other sources based on the client's chain ID.

Parameters

The loadContract action accepts the following 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

The action returns a full Tevm Contract instance with the following properties:

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

Available Loaders

You can import various ABI loaders from tevm/whatsabi:

MultiABILoader

Tries multiple ABI loaders until a result is found.

import { MultiABILoader } from "tevm/whatsabi";
 
// Create loader that tries multiple sources in order
const loader = new MultiABILoader([
  new SourcifyABILoader(),
  new EtherscanABILoader({ apiKey: "YOUR_ETHERSCAN_KEY" }),
]);
 
// Use with loadContract
const contract = await loadContract(client, {
  address: "0x123...",
  loaders: [loader],
});

EtherscanABILoader

Loads contract ABIs from Etherscan and similar explorers.

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

SourcifyABILoader

Loads contract ABIs from Sourcify's decentralized repository.

import { SourcifyABILoader } from "tevm/whatsabi";
 
// Default configuration uses public Sourcify endpoint
const sourcify = new SourcifyABILoader();
 
// Custom Sourcify server
const customSourcify = new SourcifyABILoader({
  baseUrl: "https://your-sourcify-server.com",
});

BlockscoutABILoader

Loads contract ABIs from Blockscout explorers.

import { BlockscoutABILoader } from "tevm/whatsabi";
 
// For default Blockscout
const blockscout = new BlockscoutABILoader({
  apiKey: "YOUR_BLOCKSCOUT_KEY", // optional
});
 
// For custom Blockscout instance
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") },
});
 
// Analyze a proxy contract (e.g., a typical ERC-1967 proxy)
const contract = await loadContract(client, {
  address: "0x4f8AD938eBA0CD19155a835f617317a6E788c868",
  followProxies: true,
  loaders: [
    new SourcifyABILoader(),
    new EtherscanABILoader({ apiKey: "YOUR_KEY" }),
  ],
});
 
console.log(`Original address: 0x4f8AD938eBA0CD19155a835f617317a6E788c868`);
console.log(`Implementation address: ${contract.address}`);
console.log(`Detected proxies: ${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") },
});
 
// Analyze an unverified contract to extract its interface
const contract = await loadContract(client, {
  address: "0xUnverifiedContractAddress",
  // No loaders - will use bytecode analysis only
  loaders: [],
});
 
// The resulting ABI will contain entries discovered through bytecode analysis
console.log("Discovered functions:");
contract.abi
  .filter((item) => item.type === "function")
  .forEach((func) => console.log(`- ${func.name || "Unknown"}`));

Creating a Contract Macro

import { createClient } from "tevm";
import { http } from "viem";
import { optimism } from "viem/chains";
import { loadContract } from "tevm";
import { SourcifyABILoader, EtherscanABILoader } from "tevm/whatsabi";
 
// Client for the Optimism network
const client = createClient({
  chain: optimism,
  transport: http("https://mainnet.optimism.io"),
});
 
// Configure loaders
const loaders = [
  new SourcifyABILoader(),
  new EtherscanABILoader({
    apiKey: "YOUR_ETHERSCAN_KEY",
    baseUrl: "https://api-optimistic.etherscan.io/api",
  }),
];
 
// Directly export the contract instance using top-level await
export const aaveV3Pool = await loadContract(client, {
  address: "0x794a61358D6845594F94dc1DB02A252b5b4814aD", // Aave V3 Pool on Optimism
  followProxies: true,
  includeBytecode: true,
  includeSourceCode: true,
  loaders,
});
// Import with tevm attribute
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(),
});
 
// All methods are strongly typed
const reserves = await client.readContract({
  ...aaveV3Pool.read.getReservesList(),
  address: aaveV3Pool.address,
});
 
console.log(`Aave has ${reserves.length} reserves on Optimism`);

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") },
});
 
// Create a multi-loader that tries different sources in order
const multiLoader = new MultiABILoader([
  new SourcifyABILoader(),
  new EtherscanABILoader({ apiKey: "YOUR_ETHERSCAN_KEY" }),
  new BlockscoutABILoader({ apiKey: "YOUR_BLOCKSCOUT_KEY" }),
]);
 
// Load a contract with comprehensive ABI resolution
const contract = await loadContract(client, {
  address: "0xVerifiedContractAddress",
  followProxies: true,
  loaders: [multiLoader],
  onProgress: (phase) => console.log(`Phase: ${phase}`),
});
 
// Use the contract directly with the client
const name = await client.readContract({
  ...contract.read.name(),
  address: contract.address,
});
 
console.log(`Contract name: ${name}`);

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") },
});
 
// Load a contract with source code included
const contract = await loadContract(client, {
  address: "0xVerifiedContractAddress",
  followProxies: true,
  includeBytecode: true,
  includeSourceCode: true,
  loaders: [
    new SourcifyABILoader(),
    new EtherscanABILoader({ apiKey: "YOUR_ETHERSCAN_KEY" }),
  ],
});
 
// Check if source code was retrieved
if (contract.sources) {
  console.log("Source files available:");
  // Log the names of the source files
  Object.keys(contract.sources).forEach((fileName) => {
    console.log(`- ${fileName}`);
 
    // Access the source content
    const sourceContent = contract.sources[fileName].content;
    console.log(`First 100 chars: ${sourceContent.substring(0, 100)}...`);
  });
 
  // Use the source code for integration with tools, compilation, etc.
  // For example, you could write it to a file for local development
  // Or use it for analysis, documentation generation, etc.
}
 
// You can still use the contract for interactions as usual
const balance = await client.readContract({
  ...contract.read.balanceOf("0x123..."),
  address: contract.address,
});

Progress Tracking for Large Contracts

import { loadContract, createMemoryClient } from "tevm";
import { http } from "viem";
import { SourcifyABILoader } from "tevm/whatsabi";
 
// Custom progress tracker
const progressTracker = (phase, ...args) => {
  switch (phase) {
    case "bytecode":
      console.log("Analyzing bytecode...");
      break;
    case "proxy":
      console.log("Checking for proxy patterns...");
      break;
    case "abi":
      console.log("Loading ABI information...");
      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: The source of truth is managed by another team
  • Needing the latest contract implementation: Always stay up-to-date with the latest contract state
  • Dealing with unverified contracts: Extract function selectors from bytecode directly
  • Interacting with proxy contracts: Automatically resolve and follow proxy implementations
  • Building exploratory tools: Analyze and interact with arbitrary contracts
  • Creating SDKs for protocols: Use macros to generate type-safe interfaces at build time

Use Direct Solidity Imports When:

  • You own the contract code: You control the source of truth
  • Need fully hermetic builds: Direct imports ensure deterministic, reproducible builds
  • Working with fixed contract versions: Import exact versions from npm or git submodules
  • Need complete control over ABI generation: Custom ABI transformations or optimizations

Ensuring Hermetic Builds

For production applications that need deterministic builds while using Contract Loader:

  1. Pin block heights: Use createMemoryClient with a specific blockNumber in the fork config:
const client = createMemoryClient({
  fork: {
    transport: http("https://mainnet.infura.io"),
    blockNumber: 19000000n, // Pin to a specific block
  },
});
  1. Generate and commit contracts: Use the CLI to generate static contract files:
tevm generate contract-loader --address 0x123... --output ./src/contracts
  1. Use npm packages: If available, use published contracts from official packages:
npm install @uniswap/v3-core

Feedback Welcome

This API design is currently in the planning phase. If you have suggestions, feature requests, or other feedback about the proposed Contract Loader integration, please share your thoughts in our

GitHub repository

or

Telegram community

.