diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index 362de183..83609f16 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -21,7 +21,6 @@ "@uniswap/v2-sdk": "^3.0.1", "blo": "^1.0.1", "daisyui": "^4.4.19", - "evm-proxy-detection": "^1.2.0", "next": "13.3.4", "next-plausible": "^3.12.0", "nextjs-progressbar": "^0.0.16", diff --git a/packages/nextjs/pages/[contractAddress]/[network].tsx b/packages/nextjs/pages/[contractAddress]/[network].tsx index 22a19267..0f3240f1 100644 --- a/packages/nextjs/pages/[contractAddress]/[network].tsx +++ b/packages/nextjs/pages/[contractAddress]/[network].tsx @@ -1,18 +1,17 @@ import { useEffect, useState } from "react"; import Link from "next/link"; import { useRouter } from "next/router"; -import { JsonRpcProvider } from "@ethersproject/providers"; -import detectProxyTarget from "evm-proxy-detection"; import { ParsedUrlQuery } from "querystring"; -import { Abi, extractChain, isAddress } from "viem"; +import { Abi, isAddress } from "viem"; import * as chains from "viem/chains"; +import { usePublicClient } from "wagmi"; import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; import { MetaHeader } from "~~/components/MetaHeader"; import { MiniHeader } from "~~/components/MiniHeader"; import { ContractUI } from "~~/components/scaffold-eth"; -import scaffoldConfig from "~~/scaffold.config"; import { useAbiNinjaState } from "~~/services/store/store"; import { fetchContractABIFromAnyABI, fetchContractABIFromEtherscan } from "~~/utils/abi"; +import { detectProxyTarget } from "~~/utils/abi-ninja/proxyContracts"; interface ParsedQueryContractDetailsPage extends ParsedUrlQuery { contractAddress: string; @@ -24,8 +23,6 @@ type ContractData = { address: string; }; -type AllowedNetwork = (typeof scaffoldConfig.targetNetworks)[number]["id"]; - const ContractDetailPage = () => { const router = useRouter(); const { contractAddress, network } = router.query as ParsedQueryContractDetailsPage; @@ -45,6 +42,10 @@ const ContractDetailPage = () => { setImplementationAddress: state.setImplementationAddress, })); + const publicClient = usePublicClient({ + chainId: parseInt(network), + }); + const getNetworkName = (chainId: number) => { const chain = Object.values(chains).find(chain => chain.id === chainId); return chain ? chain.name : "Unknown Network"; @@ -79,22 +80,7 @@ const ContractDetailPage = () => { } try { - const chain = extractChain({ - id: parseInt(network) as AllowedNetwork, - chains: Object.values(scaffoldConfig.targetNetworks), - }); - // @ts-expect-error this might be present or might not be - const alchmeyRPCURL = chain.rpcUrls?.alchemy?.http[0]; - let implementationAddress = undefined; - if (alchmeyRPCURL) { - const alchemyProvider = new JsonRpcProvider( - `${alchmeyRPCURL}/${scaffoldConfig.alchemyApiKey}`, - parseInt(network), - ); - const requestFunc = ({ method, params }: { method: string; params: any }) => - alchemyProvider.send(method, params); - implementationAddress = await detectProxyTarget(contractAddress, requestFunc); - } + const implementationAddress = await detectProxyTarget(contractAddress, publicClient); if (implementationAddress) { setImplementationAddress(implementationAddress); @@ -129,7 +115,7 @@ const ContractDetailPage = () => { } } } - }, [contractAddress, network, storedAbi, setMainChainId, setImplementationAddress]); + }, [contractAddress, network, storedAbi, setMainChainId, setImplementationAddress, publicClient]); return ( <> diff --git a/packages/nextjs/pages/index.tsx b/packages/nextjs/pages/index.tsx index 4d12c041..c6fac2d4 100644 --- a/packages/nextjs/pages/index.tsx +++ b/packages/nextjs/pages/index.tsx @@ -2,18 +2,16 @@ import { useEffect, useState } from "react"; import Image from "next/image"; import Link from "next/link"; import { useRouter } from "next/router"; -import { JsonRpcProvider } from "@ethersproject/providers"; -import detectProxyTarget from "evm-proxy-detection"; import type { NextPage } from "next"; -import { Address, extractChain, isAddress } from "viem"; +import { Address, isAddress } from "viem"; import { usePublicClient } from "wagmi"; import { MetaHeader } from "~~/components/MetaHeader"; import { MiniFooter } from "~~/components/MiniFooter"; import { NetworksDropdown } from "~~/components/NetworksDropdown"; import { AddressInput, InputBase } from "~~/components/scaffold-eth"; -import scaffoldConfig from "~~/scaffold.config"; import { useAbiNinjaState } from "~~/services/store/store"; import { fetchContractABIFromAnyABI, fetchContractABIFromEtherscan, parseAndCorrectJSON } from "~~/utils/abi"; +import { detectProxyTarget } from "~~/utils/abi-ninja/proxyContracts"; import { getTargetNetworks, notification } from "~~/utils/scaffold-eth"; enum TabName { @@ -21,8 +19,6 @@ enum TabName { addressAbi, } -type AllowedNetwork = (typeof scaffoldConfig.targetNetworks)[number]["id"]; - const tabValues = Object.values(TabName) as TabName[]; const networks = getTargetNetworks(); @@ -55,22 +51,7 @@ const Home: NextPage = () => { const fetchContractAbi = async () => { setIsFetchingAbi(true); try { - const chain = extractChain({ - id: parseInt(network) as AllowedNetwork, - chains: Object.values(scaffoldConfig.targetNetworks), - }); - // @ts-expect-error this might be present or might not be - const alchmeyRPCURL = chain.rpcUrls?.alchemy?.http[0]; - let implementationAddress = undefined; - if (alchmeyRPCURL) { - const alchemyProvider = new JsonRpcProvider( - `${alchmeyRPCURL}/${scaffoldConfig.alchemyApiKey}`, - parseInt(network), - ); - const requestFunc = ({ method, params }: { method: string; params: any }) => - alchemyProvider.send(method, params); - implementationAddress = await detectProxyTarget(verifiedContractAddress, requestFunc); - } + const implementationAddress = await detectProxyTarget(verifiedContractAddress, publicClient); if (implementationAddress) { setImplementationAddress(implementationAddress); diff --git a/packages/nextjs/utils/abi-ninja/proxyContracts.ts b/packages/nextjs/utils/abi-ninja/proxyContracts.ts new file mode 100644 index 00000000..3d66f06c --- /dev/null +++ b/packages/nextjs/utils/abi-ninja/proxyContracts.ts @@ -0,0 +1,116 @@ +import { PublicClient } from "wagmi"; + +const EIP_1967_LOGIC_SLOT = "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc" as const; +const EIP_1967_BEACON_SLOT = "0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50" as const; +// const OPEN_ZEPPELIN_IMPLEMENTATION_SLOT = "0x7050c9e0f4ca769c69bd3a8ef740bc37934f8e2c036e5a723fd8ee048ed3f8c3"; +const EIP_1822_LOGIC_SLOT = "0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7" as const; +const EIP_1167_BEACON_METHODS = [ + "0x5c60da1b00000000000000000000000000000000000000000000000000000000", + "0xda52571600000000000000000000000000000000000000000000000000000000", +] as const; +const EIP_897_INTERFACE = ["0x5c60da1b00000000000000000000000000000000000000000000000000000000"] as const; +const GNOSIS_SAFE_PROXY_INTERFACE = ["0xa619486e00000000000000000000000000000000000000000000000000000000"] as const; +const COMPTROLLER_PROXY_INTERFACE = ["0xbb82aa5e00000000000000000000000000000000000000000000000000000000"] as const; + +const readAddress = (value: string | undefined) => { + if (typeof value !== "string" || value === "0x") { + throw new Error(`Invalid address value: ${value}`); + } + const address = value.length === 66 ? "0x" + value.slice(-40) : value; + const zeroAddress = "0x" + "0".repeat(40); + if (address === zeroAddress) { + throw new Error("Empty address"); + } + return address; +}; + +const EIP_1167_BYTECODE_PREFIX = "0x363d3d373d3d3d363d"; +const EIP_1167_BYTECODE_SUFFIX = "57fd5bf3"; + +export const parse1167Bytecode = (bytecode: unknown): string => { + if (typeof bytecode !== "string" || !bytecode.startsWith(EIP_1167_BYTECODE_PREFIX)) { + throw new Error("Not an EIP-1167 bytecode"); + } + + // detect length of address (20 bytes non-optimized, 0 < N < 20 bytes for vanity addresses) + const pushNHex = bytecode.substring(EIP_1167_BYTECODE_PREFIX.length, EIP_1167_BYTECODE_PREFIX.length + 2); + // push1 ... push20 use opcodes 0x60 ... 0x73 + const addressLength = parseInt(pushNHex, 16) - 0x5f; + + if (addressLength < 1 || addressLength > 20) { + throw new Error("Not an EIP-1167 bytecode"); + } + + const addressFromBytecode = bytecode.substring( + EIP_1167_BYTECODE_PREFIX.length + 2, + EIP_1167_BYTECODE_PREFIX.length + 2 + addressLength * 2, // address length is in bytes, 2 hex chars make up 1 byte + ); + + const SUFFIX_OFFSET_FROM_ADDRESS_END = 22; + if ( + !bytecode + .substring(EIP_1167_BYTECODE_PREFIX.length + 2 + addressLength * 2 + SUFFIX_OFFSET_FROM_ADDRESS_END) + .startsWith(EIP_1167_BYTECODE_SUFFIX) + ) { + throw new Error("Not an EIP-1167 bytecode"); + } + + // padStart is needed for vanity addresses + return `0x${addressFromBytecode.padStart(40, "0")}`; +}; + +export const detectProxyTarget = async (proxyAddress: string, client: PublicClient) => { + const detectUsingBytecode = async () => { + const bytecode = await client.getBytecode({ address: proxyAddress }); + return parse1167Bytecode(bytecode); + }; + + const detectUsingEIP1967LogicSlot = async () => { + const logicAddress = await client.getStorageAt({ address: proxyAddress, slot: EIP_1967_LOGIC_SLOT }); + return readAddress(logicAddress); + }; + + const detectUsingEIP1967BeaconSlot = async () => { + const beaconAddress = await client.getStorageAt({ address: proxyAddress, slot: EIP_1967_BEACON_SLOT }); + const resolvedBeaconAddress = readAddress(beaconAddress); + for (const method of EIP_1167_BEACON_METHODS) { + try { + const data = await client.call({ data: method as `0x${string}`, to: resolvedBeaconAddress }); + return readAddress(data.data); + } catch { + // Ignore individual beacon method call failures + } + } + throw new Error("Beacon method calls failed"); + }; + + const detectionMethods = [detectUsingBytecode, detectUsingEIP1967LogicSlot, detectUsingEIP1967BeaconSlot]; + + try { + return await Promise.any(detectionMethods.map(method => method())); + } catch (primaryError) { + const detectUsingEIP1822LogicSlot = async () => { + const logicAddress = await client.getStorageAt({ address: proxyAddress, slot: EIP_1822_LOGIC_SLOT }); + return readAddress(logicAddress); + }; + + const detectUsingInterfaceCalls = async (data: `0x${string}`) => { + const { data: resultData } = await client.call({ data, to: proxyAddress }); + return readAddress(resultData); + }; + + const nextDetectionMethods = [ + detectUsingEIP1822LogicSlot, + () => detectUsingInterfaceCalls(EIP_897_INTERFACE[0]), + () => detectUsingInterfaceCalls(GNOSIS_SAFE_PROXY_INTERFACE[0]), + () => detectUsingInterfaceCalls(COMPTROLLER_PROXY_INTERFACE[0]), + ]; + + try { + return await Promise.any(nextDetectionMethods.map(method => method())); + } catch (finalError) { + console.error("All detection methods failed:", finalError); + return null; + } + } +}; diff --git a/yarn.lock b/yarn.lock index 7c5dbd4f..7407a364 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1370,7 +1370,6 @@ __metadata: eslint-config-next: ^13.1.6 eslint-config-prettier: ^8.5.0 eslint-plugin-prettier: ^4.2.1 - evm-proxy-detection: ^1.2.0 next: 13.3.4 next-plausible: ^3.12.0 nextjs-progressbar: ^0.0.16 @@ -4877,13 +4876,6 @@ __metadata: languageName: node linkType: hard -"evm-proxy-detection@npm:^1.2.0": - version: 1.2.0 - resolution: "evm-proxy-detection@npm:1.2.0" - checksum: d9996cbcd22eadd0b1209116d1f5c90ebf063edfe08c71c2f92372040dc004248ab280887a2f6d968d3e4963f40909f7f4cece584992e8e1f6ea136d6e18f2ee - languageName: node - linkType: hard - "execa@npm:^6.1.0": version: 6.1.0 resolution: "execa@npm:6.1.0"