Skip to content

Commit

Permalink
use evm detect proxy
Browse files Browse the repository at this point in the history
  • Loading branch information
gsteenkamp89 committed Feb 9, 2024
1 parent 768f08b commit cd9fae7
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 124 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"@vueuse/head": "^2.0.0",
"autolinker": "^4.0.0",
"bluebird": "^3.7.2",
"evm-proxy-detection": "^1.2.0",
"graphql": "16.6.0",
"graphql-tag": "^2.12.6",
"js-sha256": "^0.10.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import { useDebounceFn } from '@vueuse/core';
import { ContractInteractionTransaction, Network } from '../../types';
import {
AbiResult,
createContractInteractionTransaction,
fetchProxyAndImplementationAbis,
fetchImplementationAddress,
getABIWriteFunctions,
getContractABI,
validateTransaction
} from '../../utils';
import AddressInput from '../Input/Address.vue';
Expand All @@ -28,8 +28,9 @@ const to = ref(props.transaction.to ?? '');
const isToValid = computed(() => {
return to.value === '' || isAddress(to.value);
});
const abi = ref<Extract<AbiResult, { success: true }> | undefined>();
const selectedAbi = ref<string | undefined>();
const abi = ref('');
const implementationAddress = ref('');
const showAbiChoiceModal = ref(false);
const isAbiValid = ref(true);
const value = ref(props.transaction.value ?? '0');
Expand All @@ -44,12 +45,12 @@ const selectedMethod = computed(
const parameters = ref<string[]>([]);
function updateTransaction() {
if (!isValueValid || !isToValid || !isAbiValid || !selectedAbi.value) return;
if (!isValueValid || !isToValid || !isAbiValid || !abi.value) return;
try {
const transaction = createContractInteractionTransaction({
to: to.value,
value: value.value,
abi: selectedAbi.value,
abi: abi.value,
method: selectedMethod.value,
parameters: parameters.value
});
Expand All @@ -75,11 +76,11 @@ function updateMethod(methodName: string) {
}
function updateAbi(newAbi: string) {
selectedAbi.value = newAbi;
abi.value = newAbi;
methods.value = [];
try {
methods.value = getABIWriteFunctions(selectedAbi.value);
methods.value = getABIWriteFunctions(abi.value);
isAbiValid.value = true;
updateMethod(methods.value[0].name);
} catch (error) {
Expand All @@ -91,18 +92,52 @@ function updateAbi(newAbi: string) {
const debouncedUpdateAddress = useDebounceFn(() => {
if (isAddress(to.value)) {
fetchABIs();
fetchABI();
}
}, 300);
async function fetchABIs() {
const result = await fetchProxyAndImplementationAbis(props.network, to.value);
if (result.success) {
abi.value = result;
// defaults to implementation if proxy
abi.value?.isProxy
? updateAbi(abi.value.implementationAbi)
: updateAbi(abi.value.abi);
async function handleUseProxyAbi() {
showAbiChoiceModal.value = false;
try {
const res = await getContractABI(props.network, to.value);
if (res) {
updateAbi(res);
}
} catch (error) {
console.error(error);
}
}
async function handleUseImplementationAbi() {
showAbiChoiceModal.value = false;
try {
if (!implementationAddress.value) {
throw new Error(' No Implementation address');
}
const res = await getContractABI(
props.network,
implementationAddress.value
);
if (res) {
updateAbi(res);
}
} catch (error) {
console.error(error);
}
}
async function fetchABI() {
try {
const res = await fetchImplementationAddress(to.value, props.network);
if (!res) {
handleUseProxyAbi();
return;
}
// if proxy, let user decide which ABI we should fetch
implementationAddress.value = res;
showAbiChoiceModal.value = true;
} catch (error) {
console.error(error);
}
}
Expand Down Expand Up @@ -134,20 +169,9 @@ function updateValue(newValue: string) {
<template #label>{{ $t('safeSnap.value') }}</template>
</UiInput>

<UiSelect
v-if="abi?.success && abi.isProxy"
v-model="selectedAbi"
@change="updateAbi($event)"
>
<template #label>Choose ABI</template>
<option :value="abi.implementationAbi">Write Contract</option>
<option :value="abi.proxyAbi">Write Proxy</option>
</UiSelect>

<UiInput
v-if="!abi?.success"
:error="!isAbiValid && $t('safeSnap.invalidAbi')"
:model-value="selectedAbi"
:model-value="abi"
@update:model-value="updateAbi($event)"
>
<template #label>ABI</template>
Expand All @@ -174,4 +198,22 @@ function updateValue(newValue: string) {
</div>
</div>
</div>

<BaseModal :open="showAbiChoiceModal" @close="showAbiChoiceModal = false">
<template #header>
<h3 class="text-left px-3">Use Implementation ABI?</h3>
</template>
<div class="flex flex-col gap-4 p-3">
<p class="pr-8">
This contract looks like a proxy. Would you like to use the
implementation ABI?
</p>
<div class="flex gap-2 justify-center">
<TuneButton @click="handleUseProxyAbi"> Keep proxy ABI </TuneButton>
<TuneButton @click="handleUseImplementationAbi">
Use Implementation ABI
</TuneButton>
</div>
</div>
</BaseModal>
</template>
91 changes: 1 addition & 90 deletions src/plugins/oSnap/utils/abi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { fetchImplementationAddress } from './getters';
*
* If this is the case, we must parse the value as JSON and verify that it is valid.
*/
export function isArrayParameter(parameter: string): boolean {
export async function isArrayParameter(parameter: string): Promise<boolean> {
return ['tuple', 'array'].includes(parameter);
}

Expand Down Expand Up @@ -74,95 +74,6 @@ export async function getContractABI(
}
}

export type AbiResult =
| {
success: true;
isProxy: true;
proxyAbi: string;
implementationAbi: string;
}
| {
success: true;
isProxy: false;
abi: string;
}
| {
success: false;
error: string;
};

/**
* Attempts to fetch ABIs at a particular address, from a block explorer.
*
* Checks if the contract is a proxy contract, and if so, returns both
*/
export async function fetchProxyAndImplementationAbis(
network: string,
contractAddress: string
): Promise<AbiResult> {
try {
const abi = await getContractABI(network, contractAddress);
if (!abi) {
throw new Error('unable to fetch ABI');
}
if (!isProxyContract(abi)) {
return {
success: true,
isProxy: false,
abi
};
}
const implementationAddress = await fetchImplementationAddress(
abi,
contractAddress,
network
);

if (!implementationAddress) {
throw new Error('Failed to fetch implementation address');
}
const implementationAbi = await getContractABI(
network,
implementationAddress
);
if (!implementationAbi) {
throw new Error('failed to fetch implementation ABI');
}

return {
success: true,
isProxy: true,
proxyAbi: abi,
implementationAbi
};
} catch (error) {
return {
success: false,
error: isErrorWithMessage(error)
? error.message
: "Failed to fetch ABI's for this address"
};
}
}

/**
* Checks an ABI statically to see if it is a proxy contract.
*
* This is very rudimentary, it only checks if the contract has a function called "implementation"
*/
function isProxyContract(abi: string): boolean {
const contract = new Interface(abi);
if (
contract.fragments.find(
fragment =>
fragment.type === 'function' && fragment.name === 'implementation'
)
) {
return true;
}
return false;
}

/**
* Checks if a method is a write function.
*
Expand Down
10 changes: 5 additions & 5 deletions src/plugins/oSnap/utils/getters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { toUtf8Bytes } from '@ethersproject/strings';
import { multicall } from '@snapshot-labs/snapshot.js/src/utils';
import getProvider from '@snapshot-labs/snapshot.js/src/utils/provider';
import memoize from 'lodash/memoize';
import detectProxyTarget from 'evm-proxy-detection';
import {
ERC20_ABI,
GNOSIS_SAFE_TRANSACTION_API_URLS,
Expand Down Expand Up @@ -755,15 +756,14 @@ export function getOracleUiLink(
}

export async function fetchImplementationAddress(
abi: string,
proxyAddress: string,
network: string
): Promise<string | undefined> {
try {
const provider = getProvider(network);
const proxyContract = new Contract(proxyAddress, abi, provider);
return (await proxyContract.implementation()) as string;
} catch {
return undefined;
const requestFunc = ({ method, params }) => provider.send(method, params);
return (await detectProxyTarget(proxyAddress, requestFunc)) ?? undefined;
} catch (error) {
console.error(error);
}
}
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4326,6 +4326,11 @@ events@^3.0.0, events@^3.3.0:
resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==

evm-proxy-detection@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/evm-proxy-detection/-/evm-proxy-detection-1.2.0.tgz#090a0812d09638b0ef2389d2de121704dc21fb86"
integrity sha512-pujpLG5JIiNWLRvjqI7qeT63dAQMMZvO8gaDWMfIsur1GYaJwtqrerXjXjbb0s/P3fIYu9nUJMJTd8/HudR3ow==

evp_bytestokey@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02"
Expand Down

0 comments on commit cd9fae7

Please sign in to comment.