diff --git a/packages/app/src/components/contract/ContractBytecode.stories.ts b/packages/app/src/components/contract/ContractBytecode.stories.ts index e5f544b74..365605a1c 100644 --- a/packages/app/src/components/contract/ContractBytecode.stories.ts +++ b/packages/app/src/components/contract/ContractBytecode.stories.ts @@ -78,6 +78,7 @@ const contract: Contract = { balances: {}, totalTransactions: 0, proxyInfo: null, + diamondProxyInfo: null, } as Contract; export const Verified = Template.bind({}) as unknown as { args: Args }; diff --git a/packages/app/src/components/contract/ContractInfoTab.vue b/packages/app/src/components/contract/ContractInfoTab.vue index f35b7786b..bff1e3a5b 100644 --- a/packages/app/src/components/contract/ContractInfoTab.vue +++ b/packages/app/src/components/contract/ContractInfoTab.vue @@ -137,6 +137,11 @@ + @@ -151,6 +156,7 @@ import Alert from "@/components/common/Alert.vue"; import HashLabel from "@/components/common/HashLabel.vue"; import Tabs from "@/components/common/Tabs.vue"; import ContractBytecode from "@/components/contract/ContractBytecode.vue"; +import DiamondProxy from "@/components/contract/ContractInfoTabDiamondProxy.vue"; import FunctionDropdown from "@/components/contract/interaction/FunctionDropdown.vue"; import type { Contract } from "@/composables/useAddress"; @@ -237,6 +243,7 @@ const readProxyFunctions = computed(() => { const tabs = computed(() => { const isVerified = !!props.contract?.verificationInfo; const isProxy = !!props.contract?.proxyInfo; + const isDiamondProxy = !!props.contract?.diamondProxyInfo; if (isVerified || isProxy) { return [ { title: t("contractInfoTabs.contract"), hash: "#contract-info" }, @@ -244,6 +251,7 @@ const tabs = computed(() => { { title: t("contractInfoTabs.write"), hash: isVerified ? "#write" : null }, { title: t("contractInfoTabs.readAsProxy"), hash: isProxy ? "#read-proxy" : null }, { title: t("contractInfoTabs.writeAsProxy"), hash: isProxy ? "#write-proxy" : null }, + { title: t("contractInfoTabs.diamondProxy"), hash: isDiamondProxy ? "#diamon-proxy" : null }, ]; } return []; @@ -270,23 +278,5 @@ const tabs = computed(() => { diff --git a/packages/app/src/components/contract/ContractInfoTabDiamondProxy.vue b/packages/app/src/components/contract/ContractInfoTabDiamondProxy.vue new file mode 100644 index 000000000..684c34af1 --- /dev/null +++ b/packages/app/src/components/contract/ContractInfoTabDiamondProxy.vue @@ -0,0 +1,162 @@ + + + + + diff --git a/packages/app/src/composables/useAddress.ts b/packages/app/src/composables/useAddress.ts index c2836ca3f..49c06ecef 100644 --- a/packages/app/src/composables/useAddress.ts +++ b/packages/app/src/composables/useAddress.ts @@ -5,7 +5,7 @@ import { $fetch, FetchError } from "ohmyfetch"; import useContext from "./useContext"; -import { PROXY_CONTRACT_IMPLEMENTATION_ABI } from "@/utils/constants"; +import { DIAMOND_CONTRACT_IMPLEMENTATION_ABI, PROXY_CONTRACT_IMPLEMENTATION_ABI } from "@/utils/constants"; import { numberToHexString } from "@/utils/formatters"; const oneBigInt = BigInt(1); @@ -83,6 +83,14 @@ export type Contract = Api.Response.Contract & { verificationInfo: null | ContractVerificationInfo; }; }; + diamondProxyInfo: + | null + | { + implementation: { + address: string; + verificationInfo: null | ContractVerificationInfo; + }; + }[]; }; export type AddressItem = Account | Contract; @@ -118,6 +126,67 @@ export default (context = useContext()) => { } }; + const getAddressesSafe = async (getAddressesFn: () => Promise) => { + try { + const addresses = await getAddressesFn(); + if (!addresses.every(isAddress) || addresses.some((address) => address === ZeroAddress)) { + return null; + } + return addresses; + } catch (e) { + return null; + } + }; + + const getDiamondProxyImplementation = async (address: string): Promise => { + const provider = context.getL2Provider(); + + const EIP2535_DIAMOND_IMPLEMENTATION_SLOT = numberToHexString( + BigInt(keccak256(toUtf8Bytes("diamond.standard.diamond.storage"))) + BigInt(2) + ); + + const eip2535Diamond = await provider.getStorage(address, EIP2535_DIAMOND_IMPLEMENTATION_SLOT); + + if (eip2535Diamond) { + const diamondContract = new EthersContract(address, DIAMOND_CONTRACT_IMPLEMENTATION_ABI, provider); + const facetAddresses = await getAddressesSafe(() => diamondContract.facetAddresses()); + return facetAddresses?.length ? facetAddresses : null; + } + + return null; + }; + + const getDiamondProxyInfo = async (address: string) => { + try { + const implementationAddresses = await getDiamondProxyImplementation(address); + + if (!implementationAddresses) { + return null; + } + + // Prepare an array of promises for every address string in the array + const mapContractVerificationInfo = async (address: string) => { + const contractVerificationInfo = await getContractVerificationInfo(address); + return { + implementation: { + address, + verificationInfo: contractVerificationInfo, + }, + }; + }; + + const implementationPromiseArr = implementationAddresses.map((address) => { + return mapContractVerificationInfo(address); + }); + + const implementationVerificationInfoArr = await Promise.all(implementationPromiseArr); + + return implementationVerificationInfoArr; + } catch (e) { + return null; + } + }; + const getProxyImplementation = async (address: string): Promise => { const provider = context.getL2Provider(); const proxyContract = new EthersContract(address, PROXY_CONTRACT_IMPLEMENTATION_ABI, provider); @@ -172,14 +241,16 @@ export default (context = useContext()) => { if (response.type === "account") { item.value = response; } else if (response.type === "contract") { - const [verificationInfo, proxyInfo] = await Promise.all([ + const [verificationInfo, proxyInfo, diamondProxyInfo] = await Promise.all([ getContractVerificationInfo(response.address), getContractProxyInfo(response.address), + getDiamondProxyInfo(response.address), ]); item.value = { ...response, verificationInfo, proxyInfo, + diamondProxyInfo, }; } } catch (error: unknown) { diff --git a/packages/app/src/locales/en.json b/packages/app/src/locales/en.json index 289060c7f..a818f8df3 100644 --- a/packages/app/src/locales/en.json +++ b/packages/app/src/locales/en.json @@ -391,6 +391,7 @@ "contractNotVerified": "Is not verified", "verifyImplementationMessage": "Please verify the implementation contract in order to Read/Write the contract as Proxy.", "proxyCautionMessage": "Please note that the proxy identification process is based on analysis of popular proxy standards and might not be always accurate. Proceed with caution when interacting with any smart contract.", + "diamondProxySubtitleWrite": "Facet Contracts (Write)", "method": { "read": { "name": "Read", @@ -701,7 +702,8 @@ "read": "Read", "write": "Write", "readAsProxy": "Read as Proxy", - "writeAsProxy": "Write as Proxy" + "writeAsProxy": "Write as Proxy", + "diamondProxy": "Diamond Proxy" }, "debuggerTool": { "title": "zkEVM Debugger", diff --git a/packages/app/src/utils/constants.ts b/packages/app/src/utils/constants.ts index dc2acab62..63396d17f 100644 --- a/packages/app/src/utils/constants.ts +++ b/packages/app/src/utils/constants.ts @@ -14,3 +14,19 @@ export const PROXY_CONTRACT_IMPLEMENTATION_ABI = [ type: "function", }, ]; + +export const DIAMOND_CONTRACT_IMPLEMENTATION_ABI = [ + { + inputs: [], + name: "facetAddresses", + outputs: [ + { + internalType: "address[]", + name: "", + type: "address[]", + }, + ], + stateMutability: "view", + type: "function", + }, +]; diff --git a/packages/app/tests/components/contract/ContractBytecode.spec.ts b/packages/app/tests/components/contract/ContractBytecode.spec.ts index f16b28f1a..17610cbfa 100644 --- a/packages/app/tests/components/contract/ContractBytecode.spec.ts +++ b/packages/app/tests/components/contract/ContractBytecode.spec.ts @@ -73,6 +73,7 @@ const contract: Contract = { totalTransactions: 0, balances: {}, proxyInfo: null, + diamondProxyInfo: null, }; describe("ContractBytecode", () => { diff --git a/packages/app/tests/composables/useAddress.spec.ts b/packages/app/tests/composables/useAddress.spec.ts index a7d7a53e1..4fa728ff1 100644 --- a/packages/app/tests/composables/useAddress.spec.ts +++ b/packages/app/tests/composables/useAddress.spec.ts @@ -110,6 +110,7 @@ describe("useAddresses", () => { }, }, }, + diamondProxyInfo: null, }); }); @@ -126,6 +127,7 @@ describe("useAddresses", () => { type: "contract", verificationInfo: null, proxyInfo: null, + diamondProxyInfo: null, }); }); @@ -149,6 +151,7 @@ describe("useAddresses", () => { }, }, }, + diamondProxyInfo: null, }); }); @@ -179,6 +182,7 @@ describe("useAddresses", () => { }, }, }, + diamondProxyInfo: null, }); }); @@ -208,6 +212,7 @@ describe("useAddresses", () => { }, }, }, + diamondProxyInfo: null, }); }); @@ -238,6 +243,7 @@ describe("useAddresses", () => { }, }, }, + diamondProxyInfo: null, }); }); @@ -260,6 +266,7 @@ describe("useAddresses", () => { artifacts: { abi: "abi" }, }, proxyInfo: null, + diamondProxyInfo: null, }); }); });