From 7ea38556801067d68d68f06b53500c30930c00c2 Mon Sep 17 00:00:00 2001 From: yivlad Date: Wed, 2 Oct 2024 17:29:30 +0200 Subject: [PATCH 1/8] Fetch ERC-1967 proxy implementation in generate CLI command --- packages/cli/src/plugins/blockExplorer.ts | 129 +++++++++++++++++++++- packages/cli/src/plugins/etherscan.ts | 1 + 2 files changed, 125 insertions(+), 5 deletions(-) diff --git a/packages/cli/src/plugins/blockExplorer.ts b/packages/cli/src/plugins/blockExplorer.ts index b305a52f4a..69ad3de851 100644 --- a/packages/cli/src/plugins/blockExplorer.ts +++ b/packages/cli/src/plugins/blockExplorer.ts @@ -1,5 +1,14 @@ import { camelCase } from 'change-case' -import type { Address } from 'viem' +import { + http, + type Address, + createClient, + isAddress, + isAddressEqual, + slice, + zeroAddress, +} from 'viem' +import { getStorageAt } from 'viem/actions' import { z } from 'zod' import type { ContractConfig } from '../config.js' @@ -7,6 +16,64 @@ import { fromZodError } from '../errors.js' import type { Compute } from '../types.js' import { fetch } from './fetch.js' +export const rpcUrls = { + [1]: 'https://cloudflare-eth.com', + + [5]: 'https://rpc.ankr.com/eth_goerli', + + [10]: 'https://mainnet.optimism.io', + + [56]: 'https://rpc.ankr.com/bsc', + + [97]: 'https://data-seed-prebsc-1-s1.bnbchain.org:8545', + + [100]: 'https://rpc.gnosischain.com', + + [128]: 'undefined', + + [137]: 'https://polygon-rpc.com', + + [250]: 'https://rpc.ankr.com/fantom', + + [252]: 'https://rpc.frax.com', + + [256]: 'undefined', + + [420]: 'https://goerli.optimism.io', + + [2522]: 'https://rpc.testnet.frax.com', + + [4002]: 'https://rpc.testnet.fantom.network', + + [8453]: 'https://mainnet.base.org', + + [17000]: 'https://ethereum-holesky-rpc.publicnode.com', + + [42161]: 'https://arb1.arbitrum.io/rpc', + + [42220]: 'https://forno.celo.org', + + [43113]: 'https://api.avax-test.network/ext/bc/C/rpc', + + [43114]: 'https://api.avax.network/ext/bc/C/rpc', + + [44787]: 'https://alfajores-forno.celo-testnet.org', + + [80001]: 'https://rpc.ankr.com/polygon_mumbai', + + [81457]: 'https://rpc.blast.io', + + [84532]: 'https://sepolia.base.org', + + [421613]: 'https://goerli-rollup.arbitrum.io/rpc', + + [421614]: 'https://sepolia-rollup.arbitrum.io/rpc', + + [11155111]: 'https://rpc.sepolia.org', + + [11155420]: 'https://sepolia.optimism.io', +} + export type BlockExplorerConfig = { /** * API key for block explorer. Appended to the request URL as query param `&apikey=${apiKey}`. @@ -38,6 +105,10 @@ export type BlockExplorerConfig = { * Name of source. */ name?: ContractConfig['name'] | undefined + /** + * Chain id to use for fetching on-chain info of contract (e.g. implementation address) + */ + chainId?: number | undefined } const BlockExplorerResponse = z.discriminatedUnion('status', [ @@ -69,6 +140,7 @@ export function blockExplorer(config: BlockExplorerConfig) { return Object.values(address)[0]! }, name = 'Block Explorer', + chainId, } = config return fetch({ @@ -88,13 +160,60 @@ export function blockExplorer(config: BlockExplorerConfig) { if (parsed.data.status === '0') throw new Error(parsed.data.result) return parsed.data.result }, - request({ address }) { + async request({ address }) { if (!address) throw new Error('address is required') + const normalizedAddress = getAddress({ address }) + const makeUrl = (address: Address) => + `${baseUrl}?module=contract&action=getabi&address=${address}${apiKey ? `&apikey=${apiKey}` : ''}` + + const implementationAddress = await getImplementationAddress({ + address: normalizedAddress, + chainId, + }) + if (implementationAddress) { + return { + url: makeUrl(implementationAddress), + } + } + return { - url: `${baseUrl}?module=contract&action=getabi&address=${getAddress({ - address, - })}${apiKey ? `&apikey=${apiKey}` : ''}`, + url: makeUrl(normalizedAddress), } }, }) } + +async function getImplementationAddress({ + address, + chainId, +}: { address: Address; chainId: number | undefined }): Promise< + Address | undefined +> { + if (!chainId) return undefined + + const rpcUrl: string | undefined = (rpcUrls as any)[chainId] + if (!rpcUrl) return undefined + + const client = createClient({ + transport: http(rpcUrl), + }) + + // ERC-1967 Implementation Slot + const implementationSlot = + '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc' + const implementationSlotContent = await getStorageAt(client, { + address, + slot: implementationSlot, + }) + + if (!implementationSlotContent) return undefined + + const implementationAddress = slice(implementationSlotContent, 12) + if ( + !isAddress(implementationAddress) || + isAddressEqual(implementationAddress, zeroAddress) + ) + return undefined + + return implementationAddress +} diff --git a/packages/cli/src/plugins/etherscan.ts b/packages/cli/src/plugins/etherscan.ts index ce96bc3c6b..dc39921479 100644 --- a/packages/cli/src/plugins/etherscan.ts +++ b/packages/cli/src/plugins/etherscan.ts @@ -115,5 +115,6 @@ export function etherscan( return contractAddress }, name: 'Etherscan', + chainId, }) } From b0241a1fa089f77650cc40b53c6085a76144df94 Mon Sep 17 00:00:00 2001 From: yivlad Date: Wed, 2 Oct 2024 17:35:08 +0200 Subject: [PATCH 2/8] format --- packages/cli/src/plugins/blockExplorer.ts | 27 ----------------------- 1 file changed, 27 deletions(-) diff --git a/packages/cli/src/plugins/blockExplorer.ts b/packages/cli/src/plugins/blockExplorer.ts index 69ad3de851..be26e5072c 100644 --- a/packages/cli/src/plugins/blockExplorer.ts +++ b/packages/cli/src/plugins/blockExplorer.ts @@ -18,59 +18,32 @@ import { fetch } from './fetch.js' export const rpcUrls = { [1]: 'https://cloudflare-eth.com', - [5]: 'https://rpc.ankr.com/eth_goerli', - [10]: 'https://mainnet.optimism.io', - [56]: 'https://rpc.ankr.com/bsc', - [97]: 'https://data-seed-prebsc-1-s1.bnbchain.org:8545', - [100]: 'https://rpc.gnosischain.com', - [128]: 'undefined', - [137]: 'https://polygon-rpc.com', - [250]: 'https://rpc.ankr.com/fantom', - [252]: 'https://rpc.frax.com', - [256]: 'undefined', - [420]: 'https://goerli.optimism.io', - [2522]: 'https://rpc.testnet.frax.com', - [4002]: 'https://rpc.testnet.fantom.network', - [8453]: 'https://mainnet.base.org', - [17000]: 'https://ethereum-holesky-rpc.publicnode.com', - [42161]: 'https://arb1.arbitrum.io/rpc', - [42220]: 'https://forno.celo.org', - [43113]: 'https://api.avax-test.network/ext/bc/C/rpc', - [43114]: 'https://api.avax.network/ext/bc/C/rpc', - [44787]: 'https://alfajores-forno.celo-testnet.org', - [80001]: 'https://rpc.ankr.com/polygon_mumbai', - [81457]: 'https://rpc.blast.io', - [84532]: 'https://sepolia.base.org', - [421613]: 'https://goerli-rollup.arbitrum.io/rpc', - [421614]: 'https://sepolia-rollup.arbitrum.io/rpc', - [11155111]: 'https://rpc.sepolia.org', - [11155420]: 'https://sepolia.optimism.io', } From c7bc87a0c54050cbc64fd202dd2dba1c1d5522c7 Mon Sep 17 00:00:00 2001 From: yivlad Date: Wed, 2 Oct 2024 23:25:22 +0200 Subject: [PATCH 3/8] fix undefined as string --- packages/cli/src/plugins/blockExplorer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/plugins/blockExplorer.ts b/packages/cli/src/plugins/blockExplorer.ts index be26e5072c..ed596979d5 100644 --- a/packages/cli/src/plugins/blockExplorer.ts +++ b/packages/cli/src/plugins/blockExplorer.ts @@ -23,11 +23,11 @@ export const rpcUrls = { [56]: 'https://rpc.ankr.com/bsc', [97]: 'https://data-seed-prebsc-1-s1.bnbchain.org:8545', [100]: 'https://rpc.gnosischain.com', - [128]: 'undefined', + [128]: undefined, [137]: 'https://polygon-rpc.com', [250]: 'https://rpc.ankr.com/fantom', [252]: 'https://rpc.frax.com', - [256]: 'undefined', + [256]: undefined, [420]: 'https://goerli.optimism.io', [2522]: 'https://rpc.testnet.frax.com', [4002]: 'https://rpc.testnet.fantom.network', From fc7d0dbe961098310f35b4fe74850926b44f917b Mon Sep 17 00:00:00 2001 From: yivlad Date: Mon, 30 Dec 2024 23:39:53 +0100 Subject: [PATCH 4/8] Add flag --- packages/cli/src/plugins/blockExplorer.ts | 37 +++++++++++++++-------- packages/cli/src/plugins/etherscan.ts | 7 ++++- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/packages/cli/src/plugins/blockExplorer.ts b/packages/cli/src/plugins/blockExplorer.ts index ed596979d5..59917f28f0 100644 --- a/packages/cli/src/plugins/blockExplorer.ts +++ b/packages/cli/src/plugins/blockExplorer.ts @@ -78,11 +78,21 @@ export type BlockExplorerConfig = { * Name of source. */ name?: ContractConfig['name'] | undefined - /** - * Chain id to use for fetching on-chain info of contract (e.g. implementation address) - */ - chainId?: number | undefined -} +} & ( + | { + /** + * Chain id to use for fetching on-chain info of contract (e.g. implementation address) + */ + chainId: number + /** + * Whether to try fetching the implementation address of the contract + */ + tryFetchImplementation: true + } + | { + tryFetchImplementation?: false + } +) const BlockExplorerResponse = z.discriminatedUnion('status', [ z.object({ @@ -113,7 +123,6 @@ export function blockExplorer(config: BlockExplorerConfig) { return Object.values(address)[0]! }, name = 'Block Explorer', - chainId, } = config return fetch({ @@ -139,13 +148,15 @@ export function blockExplorer(config: BlockExplorerConfig) { const makeUrl = (address: Address) => `${baseUrl}?module=contract&action=getabi&address=${address}${apiKey ? `&apikey=${apiKey}` : ''}` - const implementationAddress = await getImplementationAddress({ - address: normalizedAddress, - chainId, - }) - if (implementationAddress) { - return { - url: makeUrl(implementationAddress), + if ('tryFetchImplementation' in config && config.tryFetchImplementation) { + const implementationAddress = await getImplementationAddress({ + address: normalizedAddress, + chainId: config.chainId, + }) + if (implementationAddress) { + return { + url: makeUrl(implementationAddress), + } } } diff --git a/packages/cli/src/plugins/etherscan.ts b/packages/cli/src/plugins/etherscan.ts index dc39921479..56007587c5 100644 --- a/packages/cli/src/plugins/etherscan.ts +++ b/packages/cli/src/plugins/etherscan.ts @@ -83,6 +83,10 @@ export type EtherscanConfig = { * Contracts to fetch ABIs for. */ contracts: Compute, 'abi'>>[] + /** + * Whether to try fetching the implementation address of the contract + */ + tryFetchImplementation?: boolean | undefined } /** @@ -91,7 +95,7 @@ export type EtherscanConfig = { export function etherscan( config: EtherscanConfig, ) { - const { apiKey, cacheDuration, chainId } = config + const { apiKey, cacheDuration, chainId, tryFetchImplementation } = config const contracts = config.contracts.map((x) => ({ ...x, @@ -116,5 +120,6 @@ export function etherscan( }, name: 'Etherscan', chainId, + tryFetchImplementation, }) } From 831f8bbcd84378db2cfde1541a0c1f29181cb18f Mon Sep 17 00:00:00 2001 From: yivlad Date: Mon, 30 Dec 2024 23:41:52 +0100 Subject: [PATCH 5/8] Fix flag name --- packages/cli/src/plugins/blockExplorer.ts | 9 ++++++--- packages/cli/src/plugins/etherscan.ts | 8 ++++---- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/cli/src/plugins/blockExplorer.ts b/packages/cli/src/plugins/blockExplorer.ts index 59917f28f0..de3bb0ee42 100644 --- a/packages/cli/src/plugins/blockExplorer.ts +++ b/packages/cli/src/plugins/blockExplorer.ts @@ -87,10 +87,10 @@ export type BlockExplorerConfig = { /** * Whether to try fetching the implementation address of the contract */ - tryFetchImplementation: true + tryFetchProxyImplementation: true } | { - tryFetchImplementation?: false + tryFetchProxyImplementation?: false } ) @@ -148,7 +148,10 @@ export function blockExplorer(config: BlockExplorerConfig) { const makeUrl = (address: Address) => `${baseUrl}?module=contract&action=getabi&address=${address}${apiKey ? `&apikey=${apiKey}` : ''}` - if ('tryFetchImplementation' in config && config.tryFetchImplementation) { + if ( + 'tryFetchProxyImplementation' in config && + config.tryFetchProxyImplementation + ) { const implementationAddress = await getImplementationAddress({ address: normalizedAddress, chainId: config.chainId, diff --git a/packages/cli/src/plugins/etherscan.ts b/packages/cli/src/plugins/etherscan.ts index 56007587c5..6f062fb1cc 100644 --- a/packages/cli/src/plugins/etherscan.ts +++ b/packages/cli/src/plugins/etherscan.ts @@ -84,9 +84,9 @@ export type EtherscanConfig = { */ contracts: Compute, 'abi'>>[] /** - * Whether to try fetching the implementation address of the contract + * Whether to try fetching proxy implementation address of the contract */ - tryFetchImplementation?: boolean | undefined + tryFetchProxyImplementation?: boolean | undefined } /** @@ -95,7 +95,7 @@ export type EtherscanConfig = { export function etherscan( config: EtherscanConfig, ) { - const { apiKey, cacheDuration, chainId, tryFetchImplementation } = config + const { apiKey, cacheDuration, chainId, tryFetchProxyImplementation } = config const contracts = config.contracts.map((x) => ({ ...x, @@ -120,6 +120,6 @@ export function etherscan( }, name: 'Etherscan', chainId, - tryFetchImplementation, + tryFetchProxyImplementation, }) } From 2f39bbda714f9c1810af650a4b3edeaaab135ea8 Mon Sep 17 00:00:00 2001 From: yivlad Date: Mon, 30 Dec 2024 23:45:50 +0100 Subject: [PATCH 6/8] Add changeset --- .changeset/curly-brooms-explain.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/curly-brooms-explain.md diff --git a/.changeset/curly-brooms-explain.md b/.changeset/curly-brooms-explain.md new file mode 100644 index 0000000000..27a2b82ecb --- /dev/null +++ b/.changeset/curly-brooms-explain.md @@ -0,0 +1,5 @@ +--- +"@wagmi/cli": patch +--- + +Add flag to etherscan and blockExplorer plugins that allow to try to fetch the implementation ABI instead of proxy ABI From 12a22fef3a71aac9f9f33fdab3275ec8e302b963 Mon Sep 17 00:00:00 2001 From: yivlad Date: Fri, 10 Jan 2025 11:04:52 +0100 Subject: [PATCH 7/8] Add tests --- .../__snapshots__/blockExplorer.test.ts.snap | 290 ++++++++++++++ .../__snapshots__/etherscan.test.ts.snap | 368 ++++++++++++++++++ .../cli/src/plugins/blockExplorer.test.ts | 12 + packages/cli/src/plugins/etherscan.test.ts | 11 + packages/cli/test/utils.ts | 32 ++ 5 files changed, 713 insertions(+) diff --git a/packages/cli/src/plugins/__snapshots__/blockExplorer.test.ts.snap b/packages/cli/src/plugins/__snapshots__/blockExplorer.test.ts.snap index 2abd351741..2b5ed28ed9 100644 --- a/packages/cli/src/plugins/__snapshots__/blockExplorer.test.ts.snap +++ b/packages/cli/src/plugins/__snapshots__/blockExplorer.test.ts.snap @@ -734,3 +734,293 @@ exports[`fetches ABI with multichain deployment 1`] = ` }, ] `; + +exports[`fetches implementation ABI when tryFetchProxyImplementation is true 1`] = ` +[ + { + "abi": [ + { + "constant": true, + "inputs": [], + "name": "name", + "outputs": [ + { + "name": "", + "type": "string", + }, + ], + "payable": false, + "stateMutability": "view", + "type": "function", + }, + { + "constant": false, + "inputs": [ + { + "name": "guy", + "type": "address", + }, + { + "name": "wad", + "type": "uint256", + }, + ], + "name": "approve", + "outputs": [ + { + "name": "", + "type": "bool", + }, + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function", + }, + { + "constant": true, + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "name": "", + "type": "uint256", + }, + ], + "payable": false, + "stateMutability": "view", + "type": "function", + }, + { + "constant": false, + "inputs": [ + { + "name": "src", + "type": "address", + }, + { + "name": "dst", + "type": "address", + }, + { + "name": "wad", + "type": "uint256", + }, + ], + "name": "transferFrom", + "outputs": [ + { + "name": "", + "type": "bool", + }, + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function", + }, + { + "constant": false, + "inputs": [ + { + "name": "wad", + "type": "uint256", + }, + ], + "name": "withdraw", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function", + }, + { + "constant": true, + "inputs": [], + "name": "decimals", + "outputs": [ + { + "name": "", + "type": "uint8", + }, + ], + "payable": false, + "stateMutability": "view", + "type": "function", + }, + { + "constant": true, + "inputs": [ + { + "name": "", + "type": "address", + }, + ], + "name": "balanceOf", + "outputs": [ + { + "name": "", + "type": "uint256", + }, + ], + "payable": false, + "stateMutability": "view", + "type": "function", + }, + { + "constant": true, + "inputs": [], + "name": "symbol", + "outputs": [ + { + "name": "", + "type": "string", + }, + ], + "payable": false, + "stateMutability": "view", + "type": "function", + }, + { + "constant": false, + "inputs": [ + { + "name": "dst", + "type": "address", + }, + { + "name": "wad", + "type": "uint256", + }, + ], + "name": "transfer", + "outputs": [ + { + "name": "", + "type": "bool", + }, + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function", + }, + { + "constant": false, + "inputs": [], + "name": "deposit", + "outputs": [], + "payable": true, + "stateMutability": "payable", + "type": "function", + }, + { + "constant": true, + "inputs": [ + { + "name": "", + "type": "address", + }, + { + "name": "", + "type": "address", + }, + ], + "name": "allowance", + "outputs": [ + { + "name": "", + "type": "uint256", + }, + ], + "payable": false, + "stateMutability": "view", + "type": "function", + }, + { + "payable": true, + "stateMutability": "payable", + "type": "fallback", + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "src", + "type": "address", + }, + { + "indexed": true, + "name": "guy", + "type": "address", + }, + { + "indexed": false, + "name": "wad", + "type": "uint256", + }, + ], + "name": "Approval", + "type": "event", + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "src", + "type": "address", + }, + { + "indexed": true, + "name": "dst", + "type": "address", + }, + { + "indexed": false, + "name": "wad", + "type": "uint256", + }, + ], + "name": "Transfer", + "type": "event", + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "dst", + "type": "address", + }, + { + "indexed": false, + "name": "wad", + "type": "uint256", + }, + ], + "name": "Deposit", + "type": "event", + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "src", + "type": "address", + }, + { + "indexed": false, + "name": "wad", + "type": "uint256", + }, + ], + "name": "Withdrawal", + "type": "event", + }, + ], + "address": { + "1": "0xaf0326d92b97df1221759476b072abfd8084f9be", + }, + "name": "WagmiMintExample", + }, +] +`; diff --git a/packages/cli/src/plugins/__snapshots__/etherscan.test.ts.snap b/packages/cli/src/plugins/__snapshots__/etherscan.test.ts.snap index 1c8d532c2c..b9d906e4f2 100644 --- a/packages/cli/src/plugins/__snapshots__/etherscan.test.ts.snap +++ b/packages/cli/src/plugins/__snapshots__/etherscan.test.ts.snap @@ -736,3 +736,371 @@ exports[`fetches ABI with multichain deployment 1`] = ` }, ] `; + +exports[`fetches implementation ABI when tryFetchProxyImplementation is true 1`] = ` +[ + { + "abi": [ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor", + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address", + }, + { + "indexed": true, + "internalType": "address", + "name": "approved", + "type": "address", + }, + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256", + }, + ], + "name": "Approval", + "type": "event", + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address", + }, + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address", + }, + { + "indexed": false, + "internalType": "bool", + "name": "approved", + "type": "bool", + }, + ], + "name": "ApprovalForAll", + "type": "event", + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address", + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address", + }, + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256", + }, + ], + "name": "Transfer", + "type": "event", + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address", + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256", + }, + ], + "name": "approve", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address", + }, + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256", + }, + ], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256", + }, + ], + "name": "getApproved", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address", + }, + ], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address", + }, + { + "internalType": "address", + "name": "operator", + "type": "address", + }, + ], + "name": "isApprovedForAll", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool", + }, + ], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [], + "name": "mint", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string", + }, + ], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256", + }, + ], + "name": "ownerOf", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address", + }, + ], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address", + }, + { + "internalType": "address", + "name": "to", + "type": "address", + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256", + }, + ], + "name": "safeTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address", + }, + { + "internalType": "address", + "name": "to", + "type": "address", + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256", + }, + { + "internalType": "bytes", + "name": "_data", + "type": "bytes", + }, + ], + "name": "safeTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address", + }, + { + "internalType": "bool", + "name": "approved", + "type": "bool", + }, + ], + "name": "setApprovalForAll", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "interfaceId", + "type": "bytes4", + }, + ], + "name": "supportsInterface", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool", + }, + ], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string", + }, + ], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256", + }, + ], + "name": "tokenURI", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string", + }, + ], + "stateMutability": "pure", + "type": "function", + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256", + }, + ], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address", + }, + { + "internalType": "address", + "name": "to", + "type": "address", + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256", + }, + ], + "name": "transferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + }, + ], + "address": { + "1": "0xaf0326d92b97df1221759476b072abfd8084f9be", + }, + "name": "WagmiMintExample", + }, +] +`; diff --git a/packages/cli/src/plugins/blockExplorer.test.ts b/packages/cli/src/plugins/blockExplorer.test.ts index 02d94d80c8..0c618578bb 100644 --- a/packages/cli/src/plugins/blockExplorer.test.ts +++ b/packages/cli/src/plugins/blockExplorer.test.ts @@ -51,3 +51,15 @@ test('fails to fetch for unverified contract', () => { '[Error: Contract source code not verified]', ) }) + +test('fetches implementation ABI when tryFetchProxyImplementation is true', async () => { + expect( + blockExplorer({ + apiKey, + baseUrl, + chainId: 1, + contracts: [{ name: 'WagmiMintExample', address: { 1: address } }], + tryFetchProxyImplementation: true, + }).contracts?.(), + ).resolves.toMatchSnapshot() +}) diff --git a/packages/cli/src/plugins/etherscan.test.ts b/packages/cli/src/plugins/etherscan.test.ts index a9d9f8cfd6..035a5f1978 100644 --- a/packages/cli/src/plugins/etherscan.test.ts +++ b/packages/cli/src/plugins/etherscan.test.ts @@ -75,3 +75,14 @@ test('invalid api key', () => { }).contracts?.(), ).rejects.toThrowErrorMatchingInlineSnapshot('[Error: Invalid API Key]') }) + +test('fetches implementation ABI when tryFetchProxyImplementation is true', async () => { + expect( + etherscan({ + apiKey, + chainId: 1, + contracts: [{ name: 'WagmiMintExample', address: { 1: address } }], + tryFetchProxyImplementation: true, + }).contracts?.(), + ).resolves.toMatchSnapshot() +}) diff --git a/packages/cli/test/utils.ts b/packages/cli/test/utils.ts index a35d5b95b1..f3b9b9fda7 100644 --- a/packages/cli/test/utils.ts +++ b/packages/cli/test/utils.ts @@ -3,6 +3,7 @@ import fixtures from 'fixturez' import { default as fs } from 'fs-extra' import { http, HttpResponse } from 'msw' import * as path from 'pathe' +import { pad } from 'viem' import { vi } from 'vitest' const f = fixtures(__dirname) @@ -168,6 +169,8 @@ export const baseUrl = 'https://api.etherscan.io/api' export const apiKey = 'abc' export const invalidApiKey = 'xyz' export const address = '0xaf0326d92b97df1221759476b072abfd8084f9be' +export const implementationAddress = + '0x43506849d7c04f9138d1a2050bbf3a0c054402dd' export const unverifiedContractAddress = '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e' export const timeoutAddress = '0xecb504d39723b0be0e3a9aa33d646642d1051ee1' @@ -207,6 +210,17 @@ export const handlers = [ '[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"approved","type":"address"},{"indexed":true,"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"operator","type":"address"},{"indexed":false,"internalType":"bool","name":"approved","type":"bool"}],"name":"ApprovalForAll","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":true,"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"Transfer","type":"event"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"approve","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"getApproved","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"operator","type":"address"}],"name":"isApprovedForAll","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"mint","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"ownerOf","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"safeTransferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"bytes","name":"_data","type":"bytes"}],"name":"safeTransferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"operator","type":"address"},{"internalType":"bool","name":"approved","type":"bool"}],"name":"setApprovalForAll","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes4","name":"interfaceId","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"tokenURI","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"transferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"}]', }) + if ( + url.search === + `?module=contract&action=getabi&address=${implementationAddress}&apikey=${apiKey}` + ) + return HttpResponse.json({ + status: '1', + message: 'OK', + result: + '[{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"guy","type":"address"},{"name":"wad","type":"uint256"}],"name":"approve","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"src","type":"address"},{"name":"dst","type":"address"},{"name":"wad","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"wad","type":"uint256"}],"name":"withdraw","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint8"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"dst","type":"address"},{"name":"wad","type":"uint256"}],"name":"transfer","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[],"name":"deposit","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"},{"name":"","type":"address"}],"name":"allowance","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"payable":true,"stateMutability":"payable","type":"fallback"},{"anonymous":false,"inputs":[{"indexed":true,"name":"src","type":"address"},{"indexed":true,"name":"guy","type":"address"},{"indexed":false,"name":"wad","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"src","type":"address"},{"indexed":true,"name":"dst","type":"address"},{"indexed":false,"name":"wad","type":"uint256"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"dst","type":"address"},{"indexed":false,"name":"wad","type":"uint256"}],"name":"Deposit","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"src","type":"address"},{"indexed":false,"name":"wad","type":"uint256"}],"name":"Withdrawal","type":"event"}]', + }) + if ( url.search === `?module=contract&action=getabi&address=${timeoutAddress}&apikey=${apiKey}` @@ -217,4 +231,22 @@ export const handlers = [ throw new Error(`Unhandled request: ${url.search}`) }), + http.post('https://cloudflare-eth.com', async ({ request }) => { + const body = (await request.json()) as any + + if ( + body.method === 'eth_getStorageAt' && + body.params[0] === address && + body.params[1] === + '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc' // ERC-1967 Implementation Slot + ) { + return HttpResponse.json({ + status: '1', + message: 'OK', + result: pad(implementationAddress), + }) + } + + throw new Error(`Unhandled RPC request: ${JSON.stringify(body)}`) + }), ] From 5b93cafbe19b4956822af42d7a7c7a759497af96 Mon Sep 17 00:00:00 2001 From: yivlad Date: Fri, 10 Jan 2025 23:55:57 +0100 Subject: [PATCH 8/8] Add test for implementation address not found --- .../__snapshots__/blockExplorer.test.ts.snap | 372 +++++++++++++++++- .../__snapshots__/etherscan.test.ts.snap | 296 +++++++++++++- .../cli/src/plugins/blockExplorer.test.ts | 13 + packages/cli/src/plugins/etherscan.test.ts | 12 + packages/cli/test/utils.ts | 22 +- 5 files changed, 703 insertions(+), 12 deletions(-) diff --git a/packages/cli/src/plugins/__snapshots__/blockExplorer.test.ts.snap b/packages/cli/src/plugins/__snapshots__/blockExplorer.test.ts.snap index 2b5ed28ed9..a72ec7a72f 100644 --- a/packages/cli/src/plugins/__snapshots__/blockExplorer.test.ts.snap +++ b/packages/cli/src/plugins/__snapshots__/blockExplorer.test.ts.snap @@ -366,6 +366,374 @@ exports[`fetches ABI 1`] = ` ] `; +exports[`fetches ABI when tryFetchProxyImplementation is true but contract is not a proxy 1`] = ` +[ + { + "abi": [ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor", + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address", + }, + { + "indexed": true, + "internalType": "address", + "name": "approved", + "type": "address", + }, + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256", + }, + ], + "name": "Approval", + "type": "event", + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address", + }, + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address", + }, + { + "indexed": false, + "internalType": "bool", + "name": "approved", + "type": "bool", + }, + ], + "name": "ApprovalForAll", + "type": "event", + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address", + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address", + }, + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256", + }, + ], + "name": "Transfer", + "type": "event", + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address", + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256", + }, + ], + "name": "approve", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address", + }, + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256", + }, + ], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256", + }, + ], + "name": "getApproved", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address", + }, + ], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address", + }, + { + "internalType": "address", + "name": "operator", + "type": "address", + }, + ], + "name": "isApprovedForAll", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool", + }, + ], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [], + "name": "mint", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string", + }, + ], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256", + }, + ], + "name": "ownerOf", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address", + }, + ], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address", + }, + { + "internalType": "address", + "name": "to", + "type": "address", + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256", + }, + ], + "name": "safeTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address", + }, + { + "internalType": "address", + "name": "to", + "type": "address", + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256", + }, + { + "internalType": "bytes", + "name": "_data", + "type": "bytes", + }, + ], + "name": "safeTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address", + }, + { + "internalType": "bool", + "name": "approved", + "type": "bool", + }, + ], + "name": "setApprovalForAll", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "interfaceId", + "type": "bytes4", + }, + ], + "name": "supportsInterface", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool", + }, + ], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string", + }, + ], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256", + }, + ], + "name": "tokenURI", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string", + }, + ], + "stateMutability": "pure", + "type": "function", + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256", + }, + ], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address", + }, + { + "internalType": "address", + "name": "to", + "type": "address", + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256", + }, + ], + "name": "transferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + }, + ], + "address": { + "1": "0xaf0326d92b97df1221759476b072abfd8084f9be", + }, + "name": "WagmiMintExample", + }, +] +`; + exports[`fetches ABI with multichain deployment 1`] = ` [ { @@ -1018,9 +1386,9 @@ exports[`fetches implementation ABI when tryFetchProxyImplementation is true 1`] }, ], "address": { - "1": "0xaf0326d92b97df1221759476b072abfd8084f9be", + "1": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", }, - "name": "WagmiMintExample", + "name": "ProxyExample", }, ] `; diff --git a/packages/cli/src/plugins/__snapshots__/etherscan.test.ts.snap b/packages/cli/src/plugins/__snapshots__/etherscan.test.ts.snap index b9d906e4f2..c5b288504c 100644 --- a/packages/cli/src/plugins/__snapshots__/etherscan.test.ts.snap +++ b/packages/cli/src/plugins/__snapshots__/etherscan.test.ts.snap @@ -368,7 +368,7 @@ exports[`fetches ABI 1`] = ` ] `; -exports[`fetches ABI with multichain deployment 1`] = ` +exports[`fetches ABI when tryFetchProxyImplementation is true but contract is not a proxy 1`] = ` [ { "abi": [ @@ -730,14 +730,13 @@ exports[`fetches ABI with multichain deployment 1`] = ` ], "address": { "1": "0xaf0326d92b97df1221759476b072abfd8084f9be", - "10": "0xaf0326d92b97df1221759476b072abfd8084f9be", }, "name": "WagmiMintExample", }, ] `; -exports[`fetches implementation ABI when tryFetchProxyImplementation is true 1`] = ` +exports[`fetches ABI with multichain deployment 1`] = ` [ { "abi": [ @@ -1099,8 +1098,299 @@ exports[`fetches implementation ABI when tryFetchProxyImplementation is true 1`] ], "address": { "1": "0xaf0326d92b97df1221759476b072abfd8084f9be", + "10": "0xaf0326d92b97df1221759476b072abfd8084f9be", }, "name": "WagmiMintExample", }, ] `; + +exports[`fetches implementation ABI when tryFetchProxyImplementation is true 1`] = ` +[ + { + "abi": [ + { + "constant": true, + "inputs": [], + "name": "name", + "outputs": [ + { + "name": "", + "type": "string", + }, + ], + "payable": false, + "stateMutability": "view", + "type": "function", + }, + { + "constant": false, + "inputs": [ + { + "name": "guy", + "type": "address", + }, + { + "name": "wad", + "type": "uint256", + }, + ], + "name": "approve", + "outputs": [ + { + "name": "", + "type": "bool", + }, + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function", + }, + { + "constant": true, + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "name": "", + "type": "uint256", + }, + ], + "payable": false, + "stateMutability": "view", + "type": "function", + }, + { + "constant": false, + "inputs": [ + { + "name": "src", + "type": "address", + }, + { + "name": "dst", + "type": "address", + }, + { + "name": "wad", + "type": "uint256", + }, + ], + "name": "transferFrom", + "outputs": [ + { + "name": "", + "type": "bool", + }, + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function", + }, + { + "constant": false, + "inputs": [ + { + "name": "wad", + "type": "uint256", + }, + ], + "name": "withdraw", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function", + }, + { + "constant": true, + "inputs": [], + "name": "decimals", + "outputs": [ + { + "name": "", + "type": "uint8", + }, + ], + "payable": false, + "stateMutability": "view", + "type": "function", + }, + { + "constant": true, + "inputs": [ + { + "name": "", + "type": "address", + }, + ], + "name": "balanceOf", + "outputs": [ + { + "name": "", + "type": "uint256", + }, + ], + "payable": false, + "stateMutability": "view", + "type": "function", + }, + { + "constant": true, + "inputs": [], + "name": "symbol", + "outputs": [ + { + "name": "", + "type": "string", + }, + ], + "payable": false, + "stateMutability": "view", + "type": "function", + }, + { + "constant": false, + "inputs": [ + { + "name": "dst", + "type": "address", + }, + { + "name": "wad", + "type": "uint256", + }, + ], + "name": "transfer", + "outputs": [ + { + "name": "", + "type": "bool", + }, + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function", + }, + { + "constant": false, + "inputs": [], + "name": "deposit", + "outputs": [], + "payable": true, + "stateMutability": "payable", + "type": "function", + }, + { + "constant": true, + "inputs": [ + { + "name": "", + "type": "address", + }, + { + "name": "", + "type": "address", + }, + ], + "name": "allowance", + "outputs": [ + { + "name": "", + "type": "uint256", + }, + ], + "payable": false, + "stateMutability": "view", + "type": "function", + }, + { + "payable": true, + "stateMutability": "payable", + "type": "fallback", + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "src", + "type": "address", + }, + { + "indexed": true, + "name": "guy", + "type": "address", + }, + { + "indexed": false, + "name": "wad", + "type": "uint256", + }, + ], + "name": "Approval", + "type": "event", + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "src", + "type": "address", + }, + { + "indexed": true, + "name": "dst", + "type": "address", + }, + { + "indexed": false, + "name": "wad", + "type": "uint256", + }, + ], + "name": "Transfer", + "type": "event", + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "dst", + "type": "address", + }, + { + "indexed": false, + "name": "wad", + "type": "uint256", + }, + ], + "name": "Deposit", + "type": "event", + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "src", + "type": "address", + }, + { + "indexed": false, + "name": "wad", + "type": "uint256", + }, + ], + "name": "Withdrawal", + "type": "event", + }, + ], + "address": { + "1": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + }, + "name": "ProxyExample", + }, +] +`; diff --git a/packages/cli/src/plugins/blockExplorer.test.ts b/packages/cli/src/plugins/blockExplorer.test.ts index 0c618578bb..75b97dd253 100644 --- a/packages/cli/src/plugins/blockExplorer.test.ts +++ b/packages/cli/src/plugins/blockExplorer.test.ts @@ -6,6 +6,7 @@ import { apiKey, baseUrl, handlers, + proxyAddress, unverifiedContractAddress, } from '../../test/utils.js' import { blockExplorer } from './blockExplorer.js' @@ -53,6 +54,18 @@ test('fails to fetch for unverified contract', () => { }) test('fetches implementation ABI when tryFetchProxyImplementation is true', async () => { + expect( + blockExplorer({ + apiKey, + baseUrl, + chainId: 1, + contracts: [{ name: 'ProxyExample', address: { 1: proxyAddress } }], + tryFetchProxyImplementation: true, + }).contracts?.(), + ).resolves.toMatchSnapshot() +}) + +test('fetches ABI when tryFetchProxyImplementation is true but contract is not a proxy', async () => { expect( blockExplorer({ apiKey, diff --git a/packages/cli/src/plugins/etherscan.test.ts b/packages/cli/src/plugins/etherscan.test.ts index 035a5f1978..84e6322f4c 100644 --- a/packages/cli/src/plugins/etherscan.test.ts +++ b/packages/cli/src/plugins/etherscan.test.ts @@ -6,6 +6,7 @@ import { apiKey, handlers, invalidApiKey, + proxyAddress, timeoutAddress, unverifiedContractAddress, } from '../../test/utils.js' @@ -77,6 +78,17 @@ test('invalid api key', () => { }) test('fetches implementation ABI when tryFetchProxyImplementation is true', async () => { + expect( + etherscan({ + apiKey, + chainId: 1, + contracts: [{ name: 'ProxyExample', address: { 1: proxyAddress } }], + tryFetchProxyImplementation: true, + }).contracts?.(), + ).resolves.toMatchSnapshot() +}) + +test('fetches ABI when tryFetchProxyImplementation is true but contract is not a proxy', async () => { expect( etherscan({ apiKey, diff --git a/packages/cli/test/utils.ts b/packages/cli/test/utils.ts index f3b9b9fda7..b8f519e852 100644 --- a/packages/cli/test/utils.ts +++ b/packages/cli/test/utils.ts @@ -3,7 +3,7 @@ import fixtures from 'fixturez' import { default as fs } from 'fs-extra' import { http, HttpResponse } from 'msw' import * as path from 'pathe' -import { pad } from 'viem' +import { pad, zeroHash } from 'viem' import { vi } from 'vitest' const f = fixtures(__dirname) @@ -169,6 +169,7 @@ export const baseUrl = 'https://api.etherscan.io/api' export const apiKey = 'abc' export const invalidApiKey = 'xyz' export const address = '0xaf0326d92b97df1221759476b072abfd8084f9be' +export const proxyAddress = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48' export const implementationAddress = '0x43506849d7c04f9138d1a2050bbf3a0c054402dd' export const unverifiedContractAddress = @@ -236,15 +237,22 @@ export const handlers = [ if ( body.method === 'eth_getStorageAt' && - body.params[0] === address && body.params[1] === '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc' // ERC-1967 Implementation Slot ) { - return HttpResponse.json({ - status: '1', - message: 'OK', - result: pad(implementationAddress), - }) + if (body.params[0] === proxyAddress) + return HttpResponse.json({ + status: '1', + message: 'OK', + result: pad(implementationAddress), + }) + + if (body.params[0] === address) + return HttpResponse.json({ + status: '1', + message: 'OK', + result: zeroHash, + }) } throw new Error(`Unhandled RPC request: ${JSON.stringify(body)}`)