-
Notifications
You must be signed in to change notification settings - Fork 141
feat: detect diamond proxy contract and display it in diamond proxy tab #365
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
base: main
Are you sure you want to change the base?
Changes from all commits
c8c9216
5944ea0
56b9443
2bea43e
e262a5c
34b64c2
be4e596
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -137,6 +137,11 @@ | |
| </div> | ||
| </div> | ||
| </template> | ||
| <template #tab-6-content> | ||
| <div class="functions-contract-container"> | ||
| <DiamondProxy :contract="props.contract" /> | ||
| </div> | ||
| </template> | ||
| </Tabs> | ||
| <ContractBytecode v-else :contract="contract" /> | ||
| </div> | ||
|
|
@@ -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,13 +243,15 @@ 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" }, | ||
| { title: t("contractInfoTabs.read"), hash: isVerified ? "#read" : null }, | ||
| { 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(() => { | |
| <style lang="scss" scoped> | ||
| .functions-contract-container { | ||
| @apply mt-4; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why have the styles below been moved? Existing tabs are broken (most probably because of this change): |
||
| .functions-dropdown-container { | ||
| @apply grid grid-cols-1 gap-4 md:mb-10; | ||
| .function-dropdown-spacer { | ||
| @apply space-y-4; | ||
| .metamask-button-container { | ||
| @apply flex flex-col justify-between sm:flex-row; | ||
| } | ||
| .function-type-title { | ||
| @apply text-xl leading-8 text-neutral-700; | ||
| } | ||
| } | ||
| } | ||
| .proxy-implementation-link { | ||
| @apply mb-4; | ||
| } | ||
| .to-lowercase { | ||
| @apply lowercase; | ||
| } | ||
| } | ||
| </style> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,162 @@ | ||
| <template> | ||
| <div v-if="hasDiamondVerificationInfo" class="flex flex-col gap-2 w-full"> | ||
| <p class="font-semibold">{{ t("contract.abiInteraction.diamondProxySubtitleWrite") }}</p> | ||
| <div class="flex flex-col md:flex-row w-full border rounded-lg"> | ||
| <div class="flex flex-col items-start w-full md:hidden p-2"> | ||
| <button @click="toggleIsMobileDropdownOpen()" class="facet-menu-button"> | ||
| <MenuIcon class="h-4 w-4" aria-hidden="true" /> | ||
| <span>Facet Menu</span> | ||
| </button> | ||
| <ul | ||
| class="flex-col items-start justify-start border-r p-2 w-full" | ||
| :class="{ flex: isMobileDropdownOpen, hidden: !isMobileDropdownOpen }" | ||
| > | ||
| <li class="facet-tab" v-for="item in contract.diamondProxyInfo" :key="item.implementation.address"> | ||
| <button | ||
| type="button" | ||
| class="facet-address-button" | ||
| @click="setTabMobile(item.implementation.address)" | ||
| :class="{ active: currentTabHash === item.implementation.address }" | ||
| > | ||
| <span class="facet-address-primary">{{ `${item.implementation.address.substring(0, 21)}...` }}</span> | ||
| <span class="facet-address-secondary">{{ shortValue(item.implementation.address, 21) }}</span> | ||
| </button> | ||
| </li> | ||
| </ul> | ||
| </div> | ||
| <ul class="hidden md:flex flex-col items-start justify-start border-r p-2"> | ||
| <li class="facet-tab" v-for="item in contract.diamondProxyInfo" :key="item.implementation.address"> | ||
| <button | ||
| type="button" | ||
| class="facet-address-button" | ||
| @click="setTab(item.implementation.address)" | ||
| :class="{ active: currentTabHash === item.implementation.address }" | ||
| > | ||
| <span class="facet-address-primary">{{ `${item.implementation.address.substring(0, 21)}...` }}</span> | ||
| <span class="facet-address-secondary">{{ shortValue(item.implementation.address, 21) }}</span> | ||
| </button> | ||
| </li> | ||
| </ul> | ||
| <div class="w-full p-2"> | ||
| <div v-if="!writeProxyFunctions?.length" class="flex flex-col w-full gap-2"> | ||
| <p class="font-bold">{{ currentTabHash }}</p> | ||
| <Alert class="w-fit" type="notification">{{ t("contract.bytecode.writeMissingMessage") }}</Alert> | ||
| </div> | ||
| <div v-else class="functions-dropdown-container"> | ||
| <div class="function-dropdown-spacer"> | ||
| <div class="metamask-button-container"> | ||
| <span class="function-type-title"> {{ t("contract.abiInteraction.method.writeAsProxy.name") }}</span> | ||
| <ConnectMetamaskButton /> | ||
| </div> | ||
| <p class="font-bold">{{ currentTabHash }}</p> | ||
| <FunctionDropdown | ||
| v-for="(item, index) in writeProxyFunctions" | ||
| :key="item.name" | ||
| type="write" | ||
| :abi-fragment="item" | ||
| :contract-address="contract.address" | ||
| > | ||
| {{ index + 1 }}. {{ item.name }} | ||
| </FunctionDropdown> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </template> | ||
|
|
||
| <script lang="ts" setup> | ||
| import { computed, ref } from "vue"; | ||
| import { useI18n } from "vue-i18n"; | ||
|
|
||
| import { MenuIcon } from "@heroicons/vue/outline"; | ||
|
|
||
| import ConnectMetamaskButton from "../ConnectMetamaskButton.vue"; | ||
| import Alert from "../common/Alert.vue"; | ||
|
|
||
| import FunctionDropdown from "@/components/contract/interaction/FunctionDropdown.vue"; | ||
|
|
||
| import type { Contract } from "@/composables/useAddress"; | ||
| import type { PropType } from "vue"; | ||
|
|
||
| import { shortValue } from "@/utils/formatters"; | ||
|
|
||
| const props = defineProps({ | ||
| contract: { | ||
| type: Object as PropType<Contract>, | ||
| default: () => ({}), | ||
| required: true, | ||
| }, | ||
| }); | ||
|
|
||
| const { t } = useI18n(); | ||
|
|
||
| const writeProxyFunctions = computed(() => { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why does |
||
| return ( | ||
| props.contract?.diamondProxyInfo | ||
| ?.find((item) => item.implementation.address === currentTabHash.value) | ||
| ?.implementation.verificationInfo?.artifacts.abi.filter( | ||
| (item) => | ||
| item.name && | ||
| item.type !== "constructor" && | ||
| (item.stateMutability === "nonpayable" || item.stateMutability === "payable") | ||
| ) || [] | ||
| ); | ||
| }); | ||
|
|
||
| const hasDiamondVerificationInfo = computed(() => { | ||
| return !!props.contract.diamondProxyInfo?.find((info) => info.implementation.verificationInfo); | ||
| }); | ||
|
|
||
| const currentTabHash = ref( | ||
| props.contract.diamondProxyInfo ? props.contract.diamondProxyInfo[0].implementation.address : "" | ||
| ); | ||
|
|
||
| const isMobileDropdownOpen = ref(false); | ||
|
|
||
| const setTab = (address: string) => { | ||
| currentTabHash.value = address; | ||
| }; | ||
|
|
||
| const setTabMobile = (address: string) => { | ||
| currentTabHash.value = address; | ||
| toggleIsMobileDropdownOpen(); | ||
| }; | ||
|
|
||
| const toggleIsMobileDropdownOpen = () => { | ||
| isMobileDropdownOpen.value = !isMobileDropdownOpen.value; | ||
| }; | ||
| </script> | ||
|
|
||
| <style lang="scss" scoped> | ||
| .functions-dropdown-container { | ||
| @apply grid grid-cols-1 gap-4 md:mb-10; | ||
| .function-dropdown-spacer { | ||
| @apply space-y-4; | ||
| .metamask-button-container { | ||
| @apply flex flex-col justify-between sm:flex-row; | ||
| } | ||
| .function-type-title { | ||
| @apply text-xl leading-8 text-neutral-700; | ||
| } | ||
| } | ||
| } | ||
| .facet-tab { | ||
| @apply w-full self-stretch; | ||
| .facet-address-button { | ||
| @apply p-2 rounded-md text-sm bg-opacity-[15%] text-primary-800 transition-colors hover:bg-primary-600 hover:bg-opacity-10 flex flex-col items-start justify-center self-stretch w-full; | ||
| .facet-address-primary { | ||
| @apply font-semibold; | ||
| } | ||
| .facet-address-secondary { | ||
| @apply font-light text-primary-800/70; | ||
| } | ||
| } | ||
| } | ||
| .active { | ||
| @apply bg-primary-600 bg-opacity-10; | ||
| } | ||
| .facet-menu-button { | ||
| @apply p-2 rounded-md text-sm border text-primary-800 transition-colors flex gap-2 items-center justify-start self-stretch w-full; | ||
| } | ||
| </style> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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); | ||
|
|
@@ -86,6 +86,14 @@ export type Contract = Api.Response.Contract & { | |
| verificationInfo: null | ContractVerificationInfo; | ||
| }; | ||
| }; | ||
| diamondProxyInfo: | ||
| | null | ||
| | { | ||
| implementation: { | ||
| address: string; | ||
| verificationInfo: null | ContractVerificationInfo; | ||
| }; | ||
| }[]; | ||
| }; | ||
| export type AddressItem = Account | Contract; | ||
|
|
||
|
|
@@ -121,6 +129,67 @@ export default (context = useContext()) => { | |
| } | ||
| }; | ||
|
|
||
| const getAddressesSafe = async (getAddressesFn: () => Promise<string[]>) => { | ||
| 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<string[] | null> => { | ||
| 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) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This condition seems to always be |
||
| 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<string | null> => { | ||
| const provider = context.getL2Provider(); | ||
| const proxyContract = new EthersContract(address, PROXY_CONTRACT_IMPLEMENTATION_ABI, provider); | ||
|
|
@@ -175,14 +244,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) { | ||
|
|
||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isDiamondProxyhas to be added to theifcondition. Otherwise the line added below is unreachable.