n-grade interface resolver requires a deterministic fallback chain. The pipeline prioritizes data richness, then degrades gracefully through proxy detection, implementation routing, and bytecode reconstruction. Each stage is isolated, cacheable, and independently testable.
Step 1: Verification Status Probe
Start by querying the block explorer API. If the contract is verified, return the ABI immediately. This is the fastest path and should be cached aggressively.
import { createPublicClient, http, type Address } from "viem";
interface VerificationResult {
isVerified: boolean;
abi: any[] | null;
source: "explorer" | "proxy" | "bytecode";
}
async function probeVerification(
client: ReturnType<typeof createPublicClient>,
address: Address,
apiKey: string
): Promise<VerificationResult> {
const response = await fetch(
`https://api.etherscan.io/v2/api?module=contract&action=getsourcecode&address=${address}&apikey=${apiKey}`
);
const data = await response.json();
if (data.result?.[0]?.ABI && data.result[0].ABI !== "Contract source code not verified") {
return {
isVerified: true,
abi: JSON.parse(data.result[0].ABI),
source: "explorer",
};
}
return { isVerified: false, abi: null, source: "explorer" };
}
Step 2: Proxy Detection & Implementation Routing
If verification fails, check for standard proxy patterns. EIP-1967, UUPS, and Beacon proxies store the implementation address in deterministic storage slots. Reading these slots requires handling 32-byte padding correctly.
import { keccak256, toHex, slice } from "viem";
const PROXY_SLOTS = {
EIP1967: "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc",
UUPS: "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc",
Beacon: "0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50",
};
async function extractImplementationAddress(
client: ReturnType<typeof createPublicClient>,
proxyAddress: Address
): Promise<Address | null> {
for (const slot of Object.values(PROXY_SLOTS)) {
const rawStorage = await client.getStorageAt({
address: proxyAddress,
slot: slot as `0x${string}`,
});
if (!rawStorage) continue;
// Storage values are left-padded to 32 bytes. Extract the last 20 bytes.
const implAddress = `0x${rawStorage.slice(-40)}` as Address;
if (implAddress !== "0x0000000000000000000000000000000000000000") {
return implAddress;
}
}
return null;
}
Step 3: Bytecode Reconstruction & Signature Mapping
When no verified source or proxy slot exists, fall back to bytecode analysis. The @shazow/whatsabi library scans opcodes to extract function selectors. These four-byte hashes are then resolved against a signature database to recover human-readable names.
import { whatsabi } from "@shazow/whatsabi";
interface ReconstructedInterface {
abi: any[];
unresolvedSelectors: string[];
}
async function reconstructFromBytecode(
client: ReturnType<typeof createPublicClient>,
targetAddress: Address
): Promise<ReconstructedInterface> {
const autoloadResult = await whatsabi.autoload(targetAddress, {
provider: client,
followProxies: false, // Already handled in Step 2
});
const abi = autoloadResult.abi;
const unresolved = autoloadResult.functions
.filter((f: any) => f.name?.startsWith("0x"))
.map((f: any) => f.name);
return { abi, unresolvedSelectors: unresolved };
}
async function mapSelectorsToSignatures(
selectors: string[],
signatureDbUrl: string
): Promise<Record<string, string>> {
const mapping: Record<string, string> = {};
for (const selector of selectors) {
const res = await fetch(`${signatureDbUrl}/api/v3/signature/${selector}`);
const data = await res.json();
if (data?.result?.[0]?.text_signature) {
mapping[selector] = data.result[0].text_signature;
} else {
mapping[selector] = selector; // Fallback to raw hash
}
}
return mapping;
}
Architecture Decisions & Rationale
- Cascade Over Parallelism: Resolution happens sequentially because each step depends on the previous failure. Parallel execution wastes RPC calls and complicates error handling. The pipeline is designed to short-circuit on success.
- Storage Slot Determinism: EIP-1967 and UUPS share the same slot because they both use
keccak256("eip1967.proxy.implementation"). Beacon proxies use a different slot to avoid collision. Hardcoding these slots is safe because they are standardized; dynamic slot discovery is unnecessary and slower.
- Bytecode Scanning Strategy:
whatsabi uses static analysis of the deployment bytecode to identify PUSH4 + EQ + JUMPI patterns that match function selectors. This is deterministic and doesn't require executing the contract, making it safe for untrusted addresses.
- Signature Database Fallback: Four-byte selectors are collision-resistant but opaque. Public databases (like 4byte.directory or Etherscan's signature API) map known hashes to signatures. Unresolved selectors are preserved as raw hashes, ensuring the interface remains callable even when names are missing.
- LLM Integration Boundary: The resolved ABI is never fed raw to a language model without structure. The model receives a typed function list, input/output schemas, and resolved names. This prevents hallucination and ensures the AI maps natural language queries to deterministic contract surfaces.
Pitfall Guide
1. Assuming Single-Slot Proxy Patterns
Explanation: Developers often check only the EIP-1967 slot. UUPS and Beacon proxies use different storage layouts or delegatecall patterns that bypass standard slots.
Fix: Iterate through all known standard slots. If none match, check for DELEGATECALL opcodes in the proxy's runtime bytecode to infer implementation routing.
2. Ignoring Storage Padding & Alignment
Explanation: EVM storage slots are 32 bytes. Implementation addresses are 20 bytes. Naive slicing (raw.slice(2, 42)) fails because values are left-padded with zeros.
Fix: Always extract the last 40 hex characters (raw.slice(-40)) and validate against the zero address before returning.
3. Caching Resolved ABIs Without Invalidation
Explanation: Proxy contracts can be upgraded. A cached implementation ABI becomes stale, causing transaction failures or incorrect state reads.
Fix: Implement TTL-based caching (e.g., 24 hours) combined with event listening for Upgraded(address) or AdminChanged(address,address) events. Invalidate cache on emission.
4. Over-Reliance on Signature Databases
Explanation: Public signature databases contain outdated, incorrect, or ambiguous mappings. Blindly trusting them introduces false positives.
Fix: Treat database results as hints, not facts. Cross-reference with known standard interfaces (ERC-20, ERC-721, ERC-1155) and flag low-confidence mappings for manual review.
5. Feeding Raw Selectors to LLMs Without Context
Explanation: Language models hallucinate when given opaque hashes. They may invent function names or misinterpret parameter types.
Fix: Always resolve selectors to human-readable names before passing to the model. If resolution fails, pass the raw hash alongside a type inference hint (e.g., fallback_0xa9059cbb(address,uint256)).
6. Blocking Resolution on Network Timeouts
Explanation: RPC providers throttle or drop requests. A single timeout in the pipeline can crash the entire resolution chain.
Fix: Wrap each stage in a timeout wrapper with exponential backoff. If a stage fails, degrade to the next available layer instead of throwing.
7. Skipping EIP-55 Checksum Validation
Explanation: Implementation addresses extracted from storage are raw hex. Feeding them to APIs or SDKs without checksum validation causes silent failures on strict providers.
Fix: Run all extracted addresses through viem's getAddress() or equivalent checksum utility before use.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| High-frequency reads on verified contracts | Explorer-First + Aggressive Caching | Lowest latency, highest fidelity | Minimal (API quota) |
| Upgradeable protocol with frequent deployments | Proxy-Aware Resolution + Event Invalidation | Handles upgrades without stale ABIs | Moderate (storage reads + event indexing) |
| Unverified or legacy contracts | Bytecode Reconstruction + Signature Mapping | Recovers callable interface without source | Higher (opcode scan + DB queries) |
| AI agent requiring natural language mapping | Resolved ABI + Structured Prompt Template | Prevents hallucination, ensures deterministic routing | Low (LLM inference cost) |
| Multi-chain deployment with inconsistent verification | Unified Resolver with Chain-Specific Fallbacks | Maintains consistent interface across networks | Moderate (RPC routing + caching) |
Configuration Template
// resolver.config.ts
import { createPublicClient, http } from "viem";
import { mainnet, optimism, arbitrum } from "viem/chains";
export const resolverConfig = {
chains: [mainnet, optimism, arbitrum],
rpcTimeout: 5000,
cache: {
ttl: 86400, // 24 hours
invalidationEvents: ["Upgraded", "AdminChanged", "ImplementationChanged"],
},
proxy: {
slots: [
"0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc",
"0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50",
],
maxDepth: 3, // Prevent infinite proxy loops
},
signatureDb: {
endpoint: "https://www.4byte.directory/api/v1/signatures/",
fallbackEndpoint: "https://api.etherscan.io/v2/api?module=contract&action=getabi",
timeout: 3000,
},
llm: {
maxTokens: 2048,
temperature: 0.1,
systemPrompt: "You are a contract interface mapper. Map user queries to resolved ABI functions only. Never invent parameters.",
},
};
export const clients = resolverConfig.chains.map((chain) =>
createPublicClient({ chain, transport: http() })
);
Quick Start Guide
- Initialize the Pipeline: Import the resolver configuration and instantiate chain-specific
viem clients. Ensure your RPC provider supports storage and bytecode queries.
- Run the Cascade: Call the resolver with a target address. The pipeline will automatically probe verification, check proxy slots, and fall back to bytecode reconstruction if needed.
- Cache & Invalidate: Store resolved ABIs in a key-value store with the configured TTL. Subscribe to
Upgraded events on known proxy addresses to trigger cache invalidation.
- Integrate with AI Agent: Pass the resolved ABI to your LLM prompt template. Use structured function schemas to map natural language queries to deterministic contract calls.
- Monitor & Log: Track resolution source, latency, and fallback frequency. Adjust TTL and timeout thresholds based on chain-specific RPC performance.