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: Support cloned contracts in the action builder. #294

Draft
wants to merge 4 commits into
base: develop
Choose a base branch
from
Draft
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
3 changes: 1 addition & 2 deletions schema/rich-contracts.json
Original file line number Diff line number Diff line change
Expand Up @@ -125,12 +125,11 @@
"$id": "#/properties/networks",
"type": "object",
"title": "The networks schema",
"description": "A mapping of chains to addresses, for factory created contracts use 0x0 for all chains",
"description": "A mapping of chains to addresses. For factory-created or other cloned contracts use the sha256-hashed bytecode of the contract instead.",
"default": {},
"examples": [
{
"mainnet": "0x0b17cf48420400e1D71F8231d4a8e43B3566BB5B",
"mainnet": "0x0",
"rinkeby": "0x0b17cf48420400e1D71F8231d4a8e43B3566BB5B",
"gnosis": "0x0b17cf48420400e1D71F8231d4a8e43B3566BB5B",
"arbitrum": "0x0b17cf48420400e1D71F8231d4a8e43B3566BB5B",
Expand Down
14 changes: 11 additions & 3 deletions src/Modules/Guilds/Hooks/useProposalCalls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { ZERO_HASH } from 'utils';
import useProposalMetadata from 'hooks/Guilds/useProposalMetadata';
import { useRichContractRegistry } from 'hooks/Guilds/contracts/useRichContractRegistry';
import { ERC20_APPROVE_SIGNATURE } from 'utils';
import { useNetwork } from 'wagmi';
import { useNetwork, useProvider } from 'wagmi';
import { getBigNumberPercentage } from 'utils/bnPercentage';
import { EMPTY_CALL } from 'Modules/Guilds/pages/CreateProposal';

Expand All @@ -27,6 +27,7 @@ const useProposalCalls = (guildId: string, proposalId: string) => {
const { contracts } = useRichContractRegistry();
const { chain } = useNetwork();
const { t } = useTranslation();
const provider = useProvider();

const theme = useTheme();
const [options, setOptions] = useState<Option[]>([]);
Expand Down Expand Up @@ -102,7 +103,8 @@ const useProposalCalls = (guildId: string, proposalId: string) => {
const { decodedCall: decodedApprovalCall } = await decodeCall(
call?.approvalCall,
contracts,
chain?.id
chain?.id,
provider
);
// Avoid spreading unnecesary approvalCall;
const { approvalCall, ...newCall } = call;
Expand Down Expand Up @@ -145,7 +147,12 @@ const useProposalCalls = (guildId: string, proposalId: string) => {
})
);

return bulkDecodeCallsFromOptions(encodedOptions, contracts, chain?.id);
return bulkDecodeCallsFromOptions(
encodedOptions,
contracts,
chain?.id,
provider
);
}
decodeOptions().then(options =>
// Return options putting default against-call last
Expand All @@ -162,6 +169,7 @@ const useProposalCalls = (guildId: string, proposalId: string) => {
theme,
optionLabels,
totalOptionsNum,
provider,
]);

return {
Expand Down
6 changes: 4 additions & 2 deletions src/components/ActionsModal/ActionsModal.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useEffect } from 'react';
import { BigNumber } from 'ethers';
import { BigNumber, utils } from 'ethers';
import { useTranslation } from 'react-i18next';

import { useTypedParams } from 'Modules/Guilds/Hooks/useTypedParams';
Expand Down Expand Up @@ -107,9 +107,11 @@ const ActionModal: React.FC<ActionModalProps> = ({
);
}

const isCloneBytecodeHash = !utils.isAddress(contractId);
return (
<ParamsForm
fn={fn}
contractBytecodeHash={isCloneBytecodeHash ? contractId : null}
defaultValues={data?.args}
onSubmit={args => {
onAddAction({
Expand All @@ -118,7 +120,7 @@ const ActionModal: React.FC<ActionModalProps> = ({
decodedCall: {
callType: SupportedAction.GENERIC_CALL,
from: guildId,
to: contractId,
to: args._clonedContractAddress || contractId,
function: contractInterface.getFunction(selectedFunction),
value: BigNumber.from(0),
args,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { useMemo } from 'react';
import moment, { Moment } from 'moment';
import { RegisterOptions } from 'react-hook-form';

import { RichContractFunctionParam } from 'hooks/Guilds/contracts/useRichContractRegistry';
import { isAddress } from 'utils';
import { AddressInput } from 'components/primitives/Forms/AddressInput';
import { FormElementProps } from 'components/primitives/Forms/types';
Expand All @@ -16,17 +15,17 @@ import { DurationInput } from 'components/primitives/Forms/DurationInput';
import { SwaprPicker } from 'components/SwaprPicker';

interface FormElementRendererProps extends FormElementProps<any> {
param: RichContractFunctionParam;
formElement: string;
}

const FormElementRenderer: React.FC<FormElementRendererProps> = ({
param,
formElement,
value,
onChange,
...remainingProps
}) => {
const FormElement: React.FC<FormElementProps<any>> = useMemo(() => {
switch (param.component) {
switch (formElement) {
case 'address':
return AddressInput;
case 'integer':
Expand All @@ -47,10 +46,10 @@ const FormElementRenderer: React.FC<FormElementRendererProps> = ({
default:
return Input;
}
}, [param]);
}, [formElement]);

const props = useMemo(() => {
switch (param.component) {
switch (formElement) {
case 'date':
return {
isUTC: true,
Expand All @@ -70,7 +69,7 @@ const FormElementRenderer: React.FC<FormElementRendererProps> = ({
default:
return {};
}
}, [param, value, onChange]);
}, [formElement, value, onChange]);

return (
<FormElement
Expand All @@ -87,12 +86,10 @@ type Validations = Omit<
'valueAsNumber' | 'valueAsDate' | 'setValueAs' | 'disabled'
>;

export const getDefaultValidationsByFormElement = (
param: RichContractFunctionParam
) => {
export const getDefaultValidationsByFormElement = (formElement: string) => {
const validations: Validations = { required: 'This field is required.' };

switch (param.component) {
switch (formElement) {
case 'address':
validations.validate = (value: string) =>
!!isAddress(value) || 'Invalid address.';
Expand Down
58 changes: 56 additions & 2 deletions src/components/ActionsModal/components/ParamsForm/ParamsForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { RichContractFunction } from 'hooks/Guilds/contracts/useRichContractRegi
import FormElementRenderer, {
getDefaultValidationsByFormElement,
} from './FormElementRenderer';
import { useProvider } from 'wagmi';
import { enc, SHA256 } from 'crypto-js';

const SubmitButton = styled(ActionsButton).attrs(() => ({
variant: 'primary',
Expand All @@ -21,32 +23,84 @@ const SubmitButton = styled(ActionsButton).attrs(() => ({

interface ParamsFormProps {
fn: RichContractFunction;
contractBytecodeHash?: string;
defaultValues?: Record<string, any>;
onSubmit: (args: Record<string, any>) => void;
}

const ParamsForm: React.FC<ParamsFormProps> = ({
fn,
contractBytecodeHash,
defaultValues,
onSubmit,
}) => {
const { control, handleSubmit } = useForm();

const provider = useProvider();
const getDefaultValidationsForClonedContract = () => {
const validations = getDefaultValidationsByFormElement('address');

if (typeof validations.validate === 'function') {
validations.validate = { default: validations.validate };
}

validations.validate = {
...validations.validate,
isValidClone: async value => {
const btcode = await provider.getCode(value);
if (btcode === '0x') return "Contract doesn't exist.";

const hashedBytecode = `0x${SHA256(btcode).toString(enc.Hex)}`;

return (
hashedBytecode === contractBytecodeHash ||
'Contract is of wrong type.'
);
},
};

return validations;
};

return (
<Wrapper>
<form onSubmit={handleSubmit(onSubmit)}>
{contractBytecodeHash && (
<FormElement key="_clonedContractAddress">
<FormLabel>Contract address to call</FormLabel>
<Controller
name="_clonedContractAddress"
control={control}
defaultValue={''}
rules={getDefaultValidationsForClonedContract()}
render={({ field, fieldState }) => (
<>
<FormElementRenderer
formElement={'address'}
{...field}
isInvalid={fieldState.invalid}
/>
{fieldState.error && (
<FormError>{fieldState.error.message}</FormError>
)}
</>
)}
/>
</FormElement>
)}

{fn.params.map(param => (
<FormElement key={param.name}>
<FormLabel>{param.description}</FormLabel>
<Controller
name={param.name}
control={control}
defaultValue={defaultValues?.[param.name] || param.defaultValue}
rules={getDefaultValidationsByFormElement(param)}
rules={getDefaultValidationsByFormElement(param.component)}
render={({ field, fieldState }) => (
<>
<FormElementRenderer
param={param}
formElement={param.component}
{...field}
isInvalid={fieldState.invalid}
/>
Expand Down
1 change: 1 addition & 0 deletions src/hooks/Guilds/contracts/useDecodedCall.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ jest.mock('./useRichContractRegistry', () => ({

jest.mock('wagmi', () => ({
useNetwork: () => ({ chain: mockChain, chains: [mockChain] }),
useProvider: () => ({ getCode: jest.fn() }),
}));

describe('useDecodedCall', () => {
Expand Down
32 changes: 25 additions & 7 deletions src/hooks/Guilds/contracts/useDecodedCall.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useEffect, useRef, useState } from 'react';
import { utils } from 'ethers';
import { useNetwork } from 'wagmi';
import { useNetwork, useProvider } from 'wagmi';
import {
RichContractData,
useRichContractRegistry,
Expand All @@ -24,6 +24,8 @@ import {
ENS_UPDATE_CONTENT_SIGNATURE,
} from 'utils';
import { lookUpContractWithSourcify } from 'utils/sourcify';
import { Provider } from '@wagmi/core';
import { enc, SHA256 } from 'crypto-js';

const knownSigHashes: Record<string, { callType: SupportedAction; ABI: any }> =
{
Expand Down Expand Up @@ -113,7 +115,8 @@ const getContractFromKnownSighashes = (data: string) => {
export const decodeCall = async (
call: Call,
contracts: RichContractData[],
chainId: number
chainId: number,
provider?: Provider
) => {
let decodedCall: DecodedCall = null;

Expand All @@ -136,11 +139,24 @@ export const decodeCall = async (
}

// Detect using the rich contract data registry.
const matchedRichContractData = contracts?.find(
let matchedRichContractData = contracts?.find(
contract =>
contract.networks[chainId].toLocaleLowerCase() ===
call.to.toLocaleLowerCase()
);

// Try to detect clone contracts
if (!matchedRichContractData && provider) {
const btcode = await provider.getCode(call.to);
const hashedBytecode = `0x${SHA256(btcode).toString(enc.Hex)}`;

matchedRichContractData = contracts?.find(
contract =>
contract.networks[chainId].toLocaleLowerCase() ===
hashedBytecode.toLocaleLowerCase()
);
}

let matchedContract = matchedRichContractData
? getContractInterfaceFromRichContractData(matchedRichContractData)
: getContractFromKnownSighashes(call.data);
Expand Down Expand Up @@ -184,13 +200,14 @@ export const decodeCall = async (
export const bulkDecodeCallsFromOptions = (
options: Option[],
contracts: RichContractData[],
chainId: number
chainId: number,
provider?: Provider
) => {
return Promise.all(
options.map(async option => {
const { actions } = option;
const actionPromisesArray = actions.map(
async action => await decodeCall(action, contracts, chainId)
async action => await decodeCall(action, contracts, chainId, provider)
);
const decodedActions = await Promise.all(actionPromisesArray);
return {
Expand All @@ -213,10 +230,11 @@ export const useDecodedCall = (call: Call) => {
const isCancelled = useRef(false);
const { chain } = useNetwork();
const { contracts } = useRichContractRegistry();
const provider = useProvider();

useEffect(() => {
if (call && !isCancelled.current) {
decodeCall(call, contracts, chain?.id).then(decodedData =>
decodeCall(call, contracts, chain?.id, provider).then(decodedData =>
setDecodedCall(decodedData)
);
} else if (!call) {
Expand All @@ -225,7 +243,7 @@ export const useDecodedCall = (call: Call) => {
return () => {
isCancelled.current = true;
};
}, [call, contracts, chain]);
}, [call, contracts, chain, provider]);

return (
decodedCall || {
Expand Down