Skip to content

Commit

Permalink
Merge branch 'main' into ui/heimdallrs-steps
Browse files Browse the repository at this point in the history
  • Loading branch information
portdeveloper authored Apr 15, 2024
2 parents 7455cef + 388e519 commit 73276ed
Show file tree
Hide file tree
Showing 6 changed files with 128 additions and 59 deletions.
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
24 changes: 3 additions & 21 deletions packages/nextjs/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@ 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 { ChevronLeftIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline";
import { MetaHeader } from "~~/components/MetaHeader";
Expand All @@ -15,15 +13,14 @@ import { AddressInput } 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 @@ -54,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
6 changes: 0 additions & 6 deletions packages/nextjs/scaffold.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,9 @@ export type ScaffoldConfig = {
walletAutoConnect: boolean;
};

const localhost = {
...chains.localhost,
id: 31337,
} as const;

const scaffoldConfig = {
// After adding a new chain here we should also add it to the networks.ts file
targetNetworks: [
localhost,
chains.mainnet,
chains.sepolia,
chains.optimism,
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

0 comments on commit 73276ed

Please sign in to comment.