Skip to content

Commit

Permalink
feat: detect diamond proxy contract and display it in diamond proxy tab
Browse files Browse the repository at this point in the history
  • Loading branch information
tx-nikola committed Dec 20, 2024
1 parent 83560f0 commit c8c9216
Show file tree
Hide file tree
Showing 5 changed files with 262 additions and 21 deletions.
26 changes: 8 additions & 18 deletions packages/app/src/components/contract/ContractInfoTab.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand All @@ -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";
Expand Down Expand Up @@ -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 [];
Expand All @@ -270,23 +278,5 @@ const tabs = computed(() => {
<style lang="scss" scoped>
.functions-contract-container {
@apply mt-4;
.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>
162 changes: 162 additions & 0 deletions packages/app/src/components/contract/ContractInfoTabDiamondProxy.vue
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(() => {
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>
75 changes: 73 additions & 2 deletions packages/app/src/composables/useAddress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -118,6 +126,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) {
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);
Expand Down Expand Up @@ -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) {
Expand Down
4 changes: 3 additions & 1 deletion packages/app/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,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",
Expand Down Expand Up @@ -700,7 +701,8 @@
"read": "Read",
"write": "Write",
"readAsProxy": "Read as Proxy",
"writeAsProxy": "Write as Proxy"
"writeAsProxy": "Write as Proxy",
"diamondProxy": "Diamond Proxy"
},
"debuggerTool": {
"title": "zkEVM Debugger",
Expand Down
16 changes: 16 additions & 0 deletions packages/app/src/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
];

0 comments on commit c8c9216

Please sign in to comment.