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

feat: detect diamond proxy contract and display it in diamond proxy tab #365

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down
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",
},
];
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ const contract: Contract = {
totalTransactions: 0,
balances: {},
proxyInfo: null,
diamondProxyInfo: null,
};

describe("ContractBytecode", () => {
Expand Down
Loading
Loading