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

Use viem for detecting proxy contracts logic #82

Merged
merged 8 commits into from
Apr 13, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 0 additions & 1 deletion packages/nextjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
32 changes: 9 additions & 23 deletions packages/nextjs/pages/[contractAddress]/[network].tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand All @@ -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";
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -129,7 +115,7 @@ const ContractDetailPage = () => {
}
}
}
}, [contractAddress, network, storedAbi, setMainChainId, setImplementationAddress]);
}, [contractAddress, network, storedAbi, setMainChainId, setImplementationAddress, publicClient]);

return (
<>
Expand Down
25 changes: 3 additions & 22 deletions packages/nextjs/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,23 @@ 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 {
verifiedContract,
addressAbi,
}

type AllowedNetwork = (typeof scaffoldConfig.targetNetworks)[number]["id"];

const tabValues = Object.values(TabName) as TabName[];

const networks = getTargetNetworks();
Expand Down Expand Up @@ -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);
Expand Down
116 changes: 116 additions & 0 deletions packages/nextjs/utils/abi-ninja/proxyContracts.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
};
8 changes: 0 additions & 8 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down
Loading