Skip to content

Commit

Permalink
deep decode proposal actions
Browse files Browse the repository at this point in the history
  • Loading branch information
dan13ram committed Sep 29, 2023
1 parent 826eb58 commit 7151223
Show file tree
Hide file tree
Showing 6 changed files with 369 additions and 76 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { useState } from 'react';
import { isValidNetwork } from '@daohaus/keychain-utils';
import { MolochV3Proposal } from '@daohaus/moloch-v3-data';
import { DecodedMultiTX, isActionError } from '@daohaus/tx-builder';
import {
DeepDecodedMultiTX as DecodedMultiTX,
isActionError,
} from '@daohaus/tx-builder';
import {
AddressDisplay,
Bold,
Expand Down Expand Up @@ -73,7 +76,7 @@ export const ProposalActionData = ({
<MainContainer>
<DisplayContainer>
<TitleContainer>
<ParMd>Transaction Call Data</ParMd>
<ParMd>Decoded Transaction Data</ParMd>

{!actionData && (
<LoadingContainer>
Expand Down Expand Up @@ -166,6 +169,16 @@ export const ProposalActionData = ({
</div>
);
})}
{action.decodedActions?.length ? (
<ProposalActionData
daoChain={daoChain}
daoId={daoId}
proposal={proposal}
proposalActionConfig={proposalActionConfig}
actionData={action.decodedActions}
decodeError={decodeError}
/>
) : null}
</div>
);
})}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ import {
TXLego,
} from '@daohaus/utils';
import {
DecodedMultiTX,
decodeProposalActions,
DeepDecodedMultiTX as DecodedMultiTX,
deepDecodeProposalActions as decodeProposalActions,
isActionError,
} from '@daohaus/tx-builder';
import { ValidNetwork, isValidNetwork } from '@daohaus/keychain-utils';
Expand Down Expand Up @@ -61,12 +61,11 @@ export const ProposalDetailsContainer = ({
const fetchPropActions = async (
chainId: ValidNetwork,
actionData: string,
actionMeta?: MulticallAction[]
_actionMeta?: MulticallAction[]
) => {
const proposalActions = await decodeProposalActions({
chainId,
actionData,
actionsMeta: actionMeta,
});
if (shouldUpdate) {
setActionData(proposalActions);
Expand Down
275 changes: 275 additions & 0 deletions libs/tx-builder/src/utils/deepDecoding.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
import { decodeFunctionData, fromHex, getAbiItem } from 'viem';
import { ArgType, ENCODED_0X0_DATA } from '@daohaus/utils';
import {
ABI_EXPLORER_KEYS,
HAUS_NETWORK_DATA,
HAUS_RPC,
Keychain,
ValidNetwork,
} from '@daohaus/keychain-utils';

import { MetaTransaction, OperationType, decodeMulti } from 'ethers-multisend';
import { whatsabi, loaders } from '@shazow/whatsabi';
import { providers } from 'ethers';

import { fetchABI, getCode } from './abi';
import { ActionError } from './decoding';
const { MultiABILoader, SourcifyABILoader } = loaders;

export type DeepDecodedAction = {
to: string;
operation: OperationType;
name: string;
value: string;
params: {
name: string;
type: string;
value: ArgType;
}[];
decodedActions?: DeepDecodedMultiTX;
};

export type DeepDecodedMultiTX = (DeepDecodedAction | ActionError)[];

class EtherscanABILoader implements loaders.ABILoader {
chainId: ValidNetwork;
rpcs: Keychain;
explorerKeys: Keychain;

constructor(options: {
chainId: ValidNetwork;
rpcs: Keychain;
explorerKeys: Keychain;
}) {
this.chainId = options.chainId;
this.rpcs = options.rpcs;
this.explorerKeys = options.explorerKeys;
}

async loadABI(address: string): Promise<any[]> {
const abi = await fetchABI({
chainId: this.chainId,
contractAddress: address,
rpcs: this.rpcs,
explorerKeys: this.explorerKeys,
});
if (!abi || !abi?.length) {
throw new Error('No ABI found for this contract');
}
return abi;
}
}

type Options = {
chainId: ValidNetwork;
rpcs: Keychain;
explorerKeys: Keychain;
loader: loaders.ABILoader;
};

export const deepDecodeProposalActions = async ({
chainId,
actionData,
rpcs = HAUS_RPC,
explorerKeys = ABI_EXPLORER_KEYS,
}: {
chainId: ValidNetwork;
actionData: string;
rpcs?: Keychain;
explorerKeys?: Keychain;
}): Promise<(DeepDecodedAction | ActionError)[]> => {
const abiLoader = new MultiABILoader([
new EtherscanABILoader({
chainId,
rpcs,
explorerKeys,
}),
new SourcifyABILoader(),
]);

const options = {
chainId,
rpcs,
explorerKeys,
loader: abiLoader,
};

return decodeMultiCall(options, actionData as `0x${string}`);
};

const decodeMultiCall = async (
options: Options,
data: `0x${string}`
): Promise<DeepDecodedMultiTX> => {
const proposalActions: MetaTransaction[] = decodeMulti(data);

const decodedProposalActions = await Promise.all(
proposalActions.map(async (action) => {
try {
return await decodeAction(options, action);
} catch (e) {
return {
error: true,
message: (e as Error).message,
data,
};
}
})
);

return decodedProposalActions;
};

type FunctionDataWithInputsReturnType = {
functionName: string;
inputs: {
name: string;
type: string;
value: string;
}[];
};

const decodeValue = (value: unknown): string => {
if (typeof value === 'string') {
if (value.startsWith('0x')) {
return fromHex(value as `0x${string}`, 'bigint').toString();
}
return value;
}
if (typeof value === 'number' || typeof value === 'bigint') {
return BigInt(value).toString();
}
return '0';
};

const decodeFunctionDataWithInputs = (options: {
abi: any[];
data: `0x${string}`;
}): FunctionDataWithInputsReturnType => {
const result = decodeFunctionData(options);

const functionDetails = getAbiItem({
abi: options.abi,
name: result.functionName,
});

const inputs = functionDetails['inputs'] || [];

const inputsWithValues = (inputs as any[]).map((input, index) => ({
name: input.name,
type: input.type,
value: result.args?.[index]?.toString() || '0x',
}));

return {
functionName: result.functionName,
inputs: inputsWithValues,
};
};

const decodeAction = async (
options: Options,
action: MetaTransaction
): Promise<DeepDecodedAction | ActionError> => {
const { data, to, value, operation } = action;
const { chainId, rpcs, loader } = options;
if (
!data ||
data === '0x' ||
!data.startsWith('0x') ||
data === ENCODED_0X0_DATA ||
(await getCode({ chainId, contractAddress: action.to, rpcs })) === '0x'
) {
return {
to: to,
operation: operation || OperationType.Call,
name: `${HAUS_NETWORK_DATA[chainId]?.symbol} Transfer`,
value: decodeValue(value),
params: [],
decodedActions: [],
};
}

const { abi } = await whatsabi.autoload(to, {
provider: new providers.JsonRpcProvider(rpcs[chainId]),
followProxies: true,
abiLoader: loader,
});

if (!abi || !abi?.length) {
throw new Error('No ABI found for this contract');
}

const result = decodeFunctionDataWithInputs({
abi,
data: data as `0x${string}`,
});

if (!result) {
throw new Error('Could not decode action');
}

if (result.functionName === 'multiSend' && result.inputs.length === 1) {
const decodedActions = await decodeMultiCall(
options,
data as `0x${string}`
);

return {
to: to,
operation: OperationType.DelegateCall,
name: result.functionName,
value: decodeValue(value),
params: result.inputs,
decodedActions,
};
}

if (result.functionName.toLowerCase().includes('exec')) {
const inputTo = result.inputs.find((input) => input.type === 'address');
const inputData = result.inputs.find((input) => input.type === 'bytes');
const inputValue = result.inputs.find((input) => input.type === 'uint256');
const inputOperation = result.inputs.find(
(input) => input.type === 'uint8'
);

if (!inputTo || !inputData) {
return {
to: to,
operation: operation || OperationType.Call,
name: result.functionName,
value: decodeValue(value),
params: result.inputs,
decodedActions: [],
};
}

const decodedData = await decodeAction(options, {
to: inputTo.value as `0x${string}`,
data: inputData.value as `0x${string}`,
value: decodeValue(inputValue?.value),
operation:
decodeValue(inputOperation?.value) === '1'
? OperationType.DelegateCall
: OperationType.Call,
});

return {
to: to,
operation: operation || OperationType.Call,
name: result.functionName,
value: decodeValue(value),
params: result.inputs,
decodedActions: [decodedData],
};
}

return {
to: to,
operation: operation || OperationType.Call,
name: result.functionName,
value: decodeValue(value),
params: result.inputs,
decodedActions: [],
};
};
1 change: 1 addition & 0 deletions libs/tx-builder/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ export * from './search';
export * from './abi';
export * from './cache';
export * from './decoding';
export * from './deepDecoding';
export * from './ethersFallback';
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"@radix-ui/react-toast": "^1.1.1",
"@radix-ui/react-tooltip": "^1.0.2",
"@safe-global/safe-apps-sdk": "^8.0.0",
"@shazow/whatsabi": "^0.8.5",
"@svgr/webpack": "^6.5.1",
"@types/react-table": "^7.7.12",
"@wagmi/connectors": "^2.6.6",
Expand All @@ -49,6 +50,7 @@
"deep-eql": "^4.1.1",
"ethereum-blockies-base64": "^1.0.2",
"ethers": "^5.7.2",
"ethers-multisend": "^2.4.0",
"graphql": "16.3.0",
"graphql-request": "^4.2.0",
"localforage": "^1.10.0",
Expand Down
Loading

0 comments on commit 7151223

Please sign in to comment.