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
- Build-time execution: Bundlers execute
type: 'macro'imports during build. - Contract resolution: WhatsABI fetches and analyzes the contract, resolving ABIs and proxies.
- Static code generation: The import is replaced with statically generated ABI/metadata.
- 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
- Bytecode analysis: WhatsABI extracts function selectors by analyzing jump tables and bytecode patterns. Works for unverified contracts.
- Signature lookup: Selectors are looked up in signature databases (matches
transfer,approve, etc.). - Proxy detection: Detects ERC-1967, Transparent, Beacon patterns; resolves and follows implementations.
- On-chain source verification: Attempts to fetch verified sources from Sourcify, Etherscan, etc. based on chain ID.
Parameters
| Parameter | Type | Description |
|---|---|---|
address | Address | Required. The contract address to analyze |
followProxies | boolean | Whether to automatically follow proxy contracts. Default: false |
includeBytecode | boolean | Whether to include contract bytecode in the returned contract. Default: false |
includeSourceCode | boolean | Whether to include contract source code as SolcInputSources. Default: false |
loaders | ABILoader[] | Array of ABI loaders to use for resolving contract ABIs. See Available Loaders |
enableExperimentalMetadata | boolean | Whether to include experimental metadata like event topics. Default: false |
signatureLookup | SignatureLookup | false | Custom signature lookup or false to disable. Default: uses WhatsABI's default lookup |
onProgress | (phase: string, ...args: any[]) => void | Progress callback |
onError | (phase: string, error: Error) => boolean | void | Error callback |
Return Value
Returns a Tevm Contract instance:
| Property | Type | Description |
|---|---|---|
abi | Abi | The resolved ABI from bytecode or verified sources |
address | Address | The contract address (may differ if proxies followed) |
read | Object | Type-safe read methods for view/pure functions |
write | Object | Type-safe write methods for state-changing functions |
events | Object | Type-safe event filters for subscription |
withAddress | Function | Create a new instance with a different address |
abiLoadedFrom | {name: string, url?: string} | Where the ABI was loaded from |
proxyDetails | Array<{name: string, implementation?: Address, selector?: string}> | If proxy, details about the implementation |
sources | SolcInputSources | If 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)
- You own the contract code
- Need fully hermetic, deterministic builds
- Working with fixed contract versions
- Need complete control over ABI generation
Ensuring Hermetic Builds
- Pin block heights: Use
createMemoryClientwithblockNumber:
const client = createMemoryClient({
fork: {
transport: http("https://mainnet.infura.io"),
blockNumber: 19000000n,
},
});- Generate and commit contracts:
tevm generate contract-loader --address 0x123... --output ./src/contracts- Use published npm packages when available:
npm install @uniswap/v3-core
