UNPKG

@openzeppelin/contracts-ui-builder-adapter-evm

Version:
466 lines (403 loc) 14.5 kB
/** * Proxy Contract Detection Utilities * * Automatically detects proxy contracts and resolves implementation addresses * for UUPS, Transparent, Beacon, and other proxy patterns. */ import { createPublicClient, http, keccak256, parseAbi, toHex } from 'viem'; import { logger } from '@openzeppelin/contracts-ui-builder-utils'; import { resolveRpcUrl } from '../configuration'; import { AbiItem, TypedEvmNetworkConfig } from '../types'; export interface ProxyDetectionResult { isProxy: boolean; proxyType: 'uups' | 'transparent' | 'beacon' | 'diamond' | 'minimal' | 'unknown' | null; confidence: 'high' | 'medium' | 'low'; indicators: string[]; } /** * Analyzes an ABI to determine if it belongs to a proxy contract */ export function detectProxyFromAbi(abi: AbiItem[]): ProxyDetectionResult { const functions = abi.filter((item) => item.type === 'function'); const events = abi.filter((item) => item.type === 'event'); const errors = abi.filter((item) => item.type === 'error'); const indicators: string[] = []; let proxyType: ProxyDetectionResult['proxyType'] = null; let confidence: ProxyDetectionResult['confidence'] = 'low'; // Check for UUPS proxy indicators const hasUpgradeEvent = events.some((e) => e.name === 'Upgraded'); const hasImplementationFunction = functions.some((f) => f.name === 'implementation'); const hasUUPSErrors = errors.some((e) => e.name?.includes('ERC1967')); const hasUpgradeToFunction = functions.some( (f) => f.name === 'upgradeToAndCall' || f.name === 'upgradeTo' ); if (hasUpgradeEvent || hasUUPSErrors) { indicators.push('ERC1967 upgrade pattern detected'); proxyType = 'uups'; confidence = 'high'; } if (hasImplementationFunction) { indicators.push('implementation() function found'); if (proxyType === 'uups') { confidence = 'high'; } else { proxyType = 'transparent'; confidence = 'medium'; } } if (hasUpgradeToFunction && proxyType === 'uups') { indicators.push('UUPS upgrade functions found'); confidence = 'high'; } // Check for Transparent proxy indicators const hasAdminFunction = functions.some((f) => f.name === 'admin'); const hasProxyAdminErrors = errors.some((e) => e.name?.includes('ProxyDenied')); const hasChangeAdminFunction = functions.some((f) => f.name === 'changeAdmin'); if (hasAdminFunction || hasProxyAdminErrors || hasChangeAdminFunction) { indicators.push('Transparent proxy admin pattern detected'); if (proxyType === null) { proxyType = 'transparent'; confidence = 'medium'; } } // Check for Beacon proxy indicators const hasBeaconFunction = functions.some((f) => f.name === 'beacon'); const hasBeaconUpgrade = events.some((e) => e.name === 'BeaconUpgraded'); if (hasBeaconFunction || hasBeaconUpgrade) { indicators.push('Beacon proxy pattern detected'); proxyType = 'beacon'; confidence = 'high'; } // Check for Diamond proxy indicators const hasDiamondCut = functions.some((f) => f.name === 'diamondCut'); const hasFacets = functions.some((f) => f.name === 'facets'); const hasFacetFunctionSelectors = functions.some((f) => f.name === 'facetFunctionSelectors'); if (hasDiamondCut || (hasFacets && hasFacetFunctionSelectors)) { indicators.push('Diamond (EIP-2535) proxy pattern detected'); proxyType = 'diamond'; confidence = 'high'; } // General proxy indicators const hasFallback = abi.some((item) => item.type === 'fallback'); const hasProxyConstructor = abi.some( (item) => item.type === 'constructor' && item.inputs?.some( (input: AbiItem) => input.name === 'implementation' || input.name === '_logic' || input.name === '_data' ) ); if (hasFallback) { indicators.push('Fallback function present'); } if (hasProxyConstructor) { indicators.push('Proxy-style constructor detected'); } // Minimal proxy (EIP-1167) detection const hasMinimalFunctions = functions.length <= 1; // Usually no functions except maybe implementation() const hasNoEvents = events.length === 0; if (hasMinimalFunctions && hasNoEvents && hasFallback && proxyType === null) { indicators.push('Minimal proxy pattern detected'); proxyType = 'minimal'; confidence = 'medium'; } // Final proxy determination const isProxy = proxyType !== null || (hasFallback && hasMinimalFunctions && (hasProxyConstructor || functions.length === 0)); if (isProxy && proxyType === null) { proxyType = 'unknown'; indicators.push('Generic proxy pattern detected'); confidence = 'low'; } return { isProxy, proxyType, confidence, indicators, }; } /** * Attempts to resolve the implementation address for a proxy contract */ export async function getImplementationAddress( proxyAddress: string, networkConfig: TypedEvmNetworkConfig, proxyType: string ): Promise<string | null> { logger.info( 'getImplementationAddress', `Resolving implementation for ${proxyType} proxy: ${proxyAddress}` ); try { switch (proxyType) { case 'uups': case 'transparent': { // Try modern EIP-1967 slot first const eip1967Impl = await getEIP1967Implementation(proxyAddress, networkConfig); if (eip1967Impl) return eip1967Impl; // Fall back to legacy OZ Unstructured Storage slot used by older proxies const legacyImpl = await getLegacyOZImplementation(proxyAddress, networkConfig); if (legacyImpl) return legacyImpl; return null; } case 'beacon': return await getBeaconImplementation(proxyAddress, networkConfig); case 'diamond': // Diamond proxies don't have a single implementation // Would need to handle facets separately logger.info('getImplementationAddress', 'Diamond proxies not fully supported yet'); return null; case 'minimal': return await getMinimalProxyImplementation(proxyAddress, networkConfig); default: // Try common methods for unknown proxy types return await tryCommonImplementationMethods(proxyAddress, networkConfig); } } catch (error) { logger.warn('getImplementationAddress', `Failed to resolve implementation: ${error}`); return null; } } /** * Attempts to resolve the admin address for a proxy contract * Tries EIP-1967 admin slot first, then legacy OZ slot */ export async function getAdminAddress( proxyAddress: string, networkConfig: TypedEvmNetworkConfig ): Promise<string | null> { try { const eip1967Admin = await getEIP1967Admin(proxyAddress, networkConfig); if (eip1967Admin) return eip1967Admin; const legacyAdmin = await getLegacyOZAdmin(proxyAddress, networkConfig); if (legacyAdmin) return legacyAdmin; return null; } catch (error) { logger.warn('getAdminAddress', `Failed to resolve admin: ${error}`); return null; } } /** * Reads implementation address from EIP-1967 storage slot */ async function getEIP1967Implementation( proxyAddress: string, networkConfig: TypedEvmNetworkConfig ): Promise<string | null> { // EIP-1967 implementation storage slot const implementationSlot = '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc'; return await readStorageSlot(proxyAddress, implementationSlot, networkConfig); } /** * Reads admin address from EIP-1967 admin storage slot * Slot: bytes32(uint256(keccak256('eip1967.proxy.admin')) - 1) */ async function getEIP1967Admin( proxyAddress: string, networkConfig: TypedEvmNetworkConfig ): Promise<string | null> { const adminSlot = '0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103'; return await readStorageSlot(proxyAddress, adminSlot, networkConfig); } /** * Reads admin address from legacy OpenZeppelin Unstructured Storage slot * Slot: keccak256("org.zeppelinos.proxy.admin") */ async function getLegacyOZAdmin( proxyAddress: string, networkConfig: TypedEvmNetworkConfig ): Promise<string | null> { try { const slot = keccak256(toHex('org.zeppelinos.proxy.admin')); logger.info('getLegacyOZAdmin', `Trying legacy OZ admin slot: ${slot}`); return await readStorageSlot(proxyAddress, slot, networkConfig); } catch (error) { logger.warn('getLegacyOZAdmin', `Failed computing or reading legacy admin slot: ${error}`); return null; } } /** * Reads implementation address from legacy OpenZeppelin Unstructured Storage slot * Slot: keccak256("org.zeppelinos.proxy.implementation") */ async function getLegacyOZImplementation( proxyAddress: string, networkConfig: TypedEvmNetworkConfig ): Promise<string | null> { try { // Compute slot deterministically at runtime to avoid hardcoding const slot = keccak256(toHex('org.zeppelinos.proxy.implementation')); logger.info('getLegacyOZImplementation', `Trying legacy OZ slot: ${slot}`); return await readStorageSlot(proxyAddress, slot, networkConfig); } catch (error) { logger.warn('getLegacyOZImplementation', `Failed computing or reading legacy slot: ${error}`); return null; } } /** * Resolves implementation through beacon proxy pattern */ async function getBeaconImplementation( proxyAddress: string, networkConfig: TypedEvmNetworkConfig ): Promise<string | null> { // EIP-1967 beacon storage slot const beaconSlot = '0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50'; const beaconAddress = await readStorageSlot(proxyAddress, beaconSlot, networkConfig); if (!beaconAddress) { return null; } // Call implementation() on the beacon contract return await callContractFunction(beaconAddress, 'implementation()', [], networkConfig); } /** * Extracts implementation from minimal proxy bytecode */ async function getMinimalProxyImplementation( proxyAddress: string, networkConfig: TypedEvmNetworkConfig ): Promise<string | null> { try { // Get the contract bytecode const bytecode = await getContractBytecode(proxyAddress, networkConfig); if (!bytecode || bytecode.length < 42) { return null; } // Minimal proxy (EIP-1167) has a specific bytecode pattern // 0x363d3d373d3d3d363d73{implementation}5af43d82803e903d91602b57fd5bf3 if ( bytecode.startsWith('0x363d3d373d3d3d363d73') && bytecode.includes('5af43d82803e903d91602b57fd5bf3') ) { // Extract the 20-byte implementation address const implementationHex = bytecode.slice(22, 62); // Skip prefix, take 20 bytes (40 hex chars) return '0x' + implementationHex; } return null; } catch (error) { logger.warn('getMinimalProxyImplementation', `Error reading bytecode: ${error}`); return null; } } /** * Tries common proxy implementation methods */ async function tryCommonImplementationMethods( proxyAddress: string, networkConfig: TypedEvmNetworkConfig ): Promise<string | null> { const commonMethods = [ 'implementation()', 'getImplementation()', '_implementation()', 'target()', ]; for (const method of commonMethods) { try { const result = await callContractFunction(proxyAddress, method, [], networkConfig); if (result && result !== '0x0000000000000000000000000000000000000000') { logger.info( 'tryCommonImplementationMethods', `Found implementation via ${method}: ${result}` ); return result; } } catch { // Continue to next method continue; } } // Try EIP-1967 storage as last resort return await getEIP1967Implementation(proxyAddress, networkConfig); } /** * Creates a viem public client for the given network configuration */ function createViemClient(networkConfig: TypedEvmNetworkConfig) { // Honor user/app RPC overrides const rpcUrl = resolveRpcUrl(networkConfig); return createPublicClient({ transport: http(rpcUrl), }); } /** * Reads a storage slot from a contract using viem */ async function readStorageSlot( address: string, slot: string, networkConfig: TypedEvmNetworkConfig ): Promise<string | null> { try { const client = createViemClient(networkConfig); const storageValue = await client.getStorageAt({ address: address as `0x${string}`, slot: slot as `0x${string}`, }); // Convert from 32-byte storage format to 20-byte address if ( storageValue && storageValue !== '0x0000000000000000000000000000000000000000000000000000000000000000' ) { logger.info('readStorageSlot', `Found non-zero value at slot ${slot}: ${storageValue}`); const implAddress = '0x' + storageValue.slice(-40); // Last 20 bytes return implAddress; } return null; } catch (error) { logger.warn('readStorageSlot', `Failed to read storage slot ${slot}: ${error}`); return null; } } /** * Calls a function on a contract using viem's readContract * Supports functions with parameters and proper return value decoding */ async function callContractFunction( address: string, signature: string, params: unknown[], networkConfig: TypedEvmNetworkConfig ): Promise<string | null> { try { const client = createViemClient(networkConfig); // Parse the function signature to get proper ABI format const abi = parseAbi([signature]); const func = abi[0] as { name: string; type: 'function' }; // Use viem's readContract for cleaner, more robust contract calls const result = await client.readContract({ address: address as `0x${string}`, abi, functionName: func.name, args: params as readonly unknown[], }); // For proxy functions, we expect an address return value const addressResult = result as string; if (addressResult && addressResult !== '0x0000000000000000000000000000000000000000') { return addressResult; } return null; } catch (error) { logger.warn('callContractFunction', `Failed to call ${signature}: ${error}`); return null; } } /** * Gets contract bytecode using viem */ async function getContractBytecode( address: string, networkConfig: TypedEvmNetworkConfig ): Promise<string | null> { try { const client = createViemClient(networkConfig); const bytecode = await client.getCode({ address: address as `0x${string}`, }); return bytecode || null; } catch (error) { logger.warn('getContractBytecode', `Failed to get bytecode: ${error}`); return null; } }