Skip to content
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) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isDiamondProxy has to be added to the if condition. Otherwise the line added below is unreachable.

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;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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):
Current version on prod
image

This version locally
image

.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(() => {

@vasyl-ivanchuk vasyl-ivanchuk Mar 25, 2025

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does Diamond Proxy tab support only write functions?

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 @@ -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;

Expand Down Expand Up @@ -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) {

@vasyl-ivanchuk vasyl-ivanchuk Mar 25, 2025

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This condition seems to always be true, as it returns 0x0000000000000000000000000000000000000000000000000000000000000000 even for a random slot. I think the check for the slot can be omitted, AFAIU the Diamond Proxy spec doesn't define a particular storage slot, so it can differ. Considering this, checking for facetAddresses is probably enough.

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 @@ -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) {
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 @@ -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",
Expand Down Expand Up @@ -714,7 +715,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 @@ -15,3 +15,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