Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: fetch ERC-1967 proxy implementation in generate command #4312

Closed
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/curly-brooms-explain.md
Original file line number Diff line number Diff line change
@@ -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
118 changes: 112 additions & 6 deletions packages/cli/src/plugins/blockExplorer.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,52 @@
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'
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}`.
Expand Down Expand Up @@ -38,7 +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
/**
* Whether to try fetching the implementation address of the contract
*/
tryFetchProxyImplementation: true
}
| {
tryFetchProxyImplementation?: false
}
)

const BlockExplorerResponse = z.discriminatedUnion('status', [
z.object({
Expand Down Expand Up @@ -88,13 +142,65 @@ 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}` : ''}`

if (
'tryFetchProxyImplementation' in config &&
config.tryFetchProxyImplementation
) {
const implementationAddress = await getImplementationAddress({
address: normalizedAddress,
chainId: config.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
}
8 changes: 7 additions & 1 deletion packages/cli/src/plugins/etherscan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ export type EtherscanConfig<chainId extends number> = {
* Contracts to fetch ABIs for.
*/
contracts: Compute<Omit<ContractConfig<ChainId, chainId>, 'abi'>>[]
/**
* Whether to try fetching proxy implementation address of the contract
*/
tryFetchProxyImplementation?: boolean | undefined
}

/**
Expand All @@ -91,7 +95,7 @@ export type EtherscanConfig<chainId extends number> = {
export function etherscan<chainId extends ChainId>(
config: EtherscanConfig<chainId>,
) {
const { apiKey, cacheDuration, chainId } = config
const { apiKey, cacheDuration, chainId, tryFetchProxyImplementation } = config

const contracts = config.contracts.map((x) => ({
...x,
Expand All @@ -115,5 +119,7 @@ export function etherscan<chainId extends ChainId>(
return contractAddress
},
name: 'Etherscan',
chainId,
tryFetchProxyImplementation,
})
}