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:
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
The action returns a full Tevm Contract instance with the following properties:
Property | Type | Description |
---|---|---|
abi | Abi | The resolved ABI from bytecode or verified sources |
address | Address | The contract address (may be different if proxies were 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 | Method to create a new instance with a different address |
abiLoadedFrom | {name: string, url?: string} | Information about where the ABI was loaded from |
proxyDetails | Array<{name: string, implementation?: Address, selector?: string}> | If the contract is a proxy, details about the proxy implementation |
sources | SolcInputSources | If 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:
- Pin block heights: Use
createMemoryClient
with a specificblockNumber
in the fork config:
const client = createMemoryClient({
fork: {
transport: http("https://mainnet.infura.io"),
blockNumber: 19000000n, // Pin to a specific block
},
});
- Generate and commit contracts: Use the CLI to generate static contract files:
tevm generate contract-loader --address 0x123... --output ./src/contracts
- 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
.