From 028d808fc6143be0cac70a454426fd2b6e712939 Mon Sep 17 00:00:00 2001 From: Vasyl Ivanchuk Date: Tue, 21 Jan 2025 19:43:15 +0200 Subject: [PATCH] fix: calldata encoding fails for tuple params --- .../app/src/composables/useTransactionData.ts | 34 ++-- packages/app/src/utils/helpers.ts | 56 +++++- .../composables/useTransactionData.spec.ts | 4 + packages/app/tests/utils/helpers.spec.ts | 163 ++++++++++++++++++ 4 files changed, 238 insertions(+), 19 deletions(-) diff --git a/packages/app/src/composables/useTransactionData.ts b/packages/app/src/composables/useTransactionData.ts index 341eb482e2..8cbf4121da 100644 --- a/packages/app/src/composables/useTransactionData.ts +++ b/packages/app/src/composables/useTransactionData.ts @@ -1,6 +1,6 @@ import { ref } from "vue"; -import { AbiCoder, Interface } from "ethers"; +import { Interface } from "ethers"; import useAddress from "./useAddress"; import useContext from "./useContext"; @@ -10,22 +10,26 @@ import type { AbiFragment } from "./useAddress"; import type { InputType } from "./useEventLog"; import type { Address } from "@/types"; -const defaultAbiCoder: AbiCoder = AbiCoder.defaultAbiCoder(); +import { decodeInputData } from "@/utils/helpers"; +export type MethodData = { + name: string; + inputs: InputData[]; +}; + +export type InputData = { + name: string; + type: InputType | string; + value: string; + inputs: InputData[]; + encodedValue: string; +}; export type TransactionData = { calldata: string; contractAddress: Address | null; value: string; sighash: string; - method?: { - name: string; - inputs: { - name: string; - type: InputType; - value: string; - encodedValue: string; - }[]; - }; + method?: MethodData; }; export function decodeDataWithABI( @@ -33,7 +37,6 @@ export function decodeDataWithABI( abi: AbiFragment[] ): TransactionData["method"] | undefined { const contractInterface = new Interface(abi); - try { const decodedData = contractInterface.parseTransaction({ data: transactionData.calldata, @@ -41,12 +44,7 @@ export function decodeDataWithABI( })!; return { name: decodedData.name, - inputs: decodedData.fragment.inputs.map((input) => ({ - name: input.name, - type: input.type as InputType, - value: decodedData.args[input.name]?.toString(), - encodedValue: defaultAbiCoder.encode([input.type], [decodedData.args[input.name]]).split("0x")[1], - })), + inputs: decodedData.fragment.inputs.flatMap((input) => decodeInputData(input, decodedData.args[input.name])), }; } catch { return undefined; diff --git a/packages/app/src/utils/helpers.ts b/packages/app/src/utils/helpers.ts index fe8bc94e0d..b3a9c6a282 100644 --- a/packages/app/src/utils/helpers.ts +++ b/packages/app/src/utils/helpers.ts @@ -1,5 +1,5 @@ import { format } from "date-fns"; -import { Interface } from "ethers"; +import { AbiCoder, Interface } from "ethers"; import { utils } from "zksync-ethers"; import { DEPLOYER_CONTRACT_ADDRESS } from "./constants"; @@ -8,9 +8,13 @@ import type { DecodingType } from "@/components/transactions/infoTable/HashViewe import type { AbiFragment } from "@/composables/useAddress"; import type { InputType, TransactionEvent, TransactionLogEntry } from "@/composables/useEventLog"; import type { TokenTransfer } from "@/composables/useTransaction"; +import type { InputData } from "@/composables/useTransactionData"; +import type { ParamType, Result } from "ethers"; const { BOOTLOADER_FORMAL_ADDRESS } = utils; +export const DefaultAbiCoder: AbiCoder = AbiCoder.defaultAbiCoder(); + export function utcStringFromUnixTimestamp(timestamp: number) { const isoDate = new Date(+`${timestamp}000`).toISOString(); return format(new Date(isoDate.slice(0, -1)), "yyyy-MM-dd HH:mm 'UTC'"); @@ -116,6 +120,56 @@ export function decodeLogWithABI(log: TransactionLogEntry, abi: AbiFragment[]): } } +export function decodeInputData(input: ParamType, args: Result): InputData[] { + if (input.isArray()) { + return decodeArrayInputData(input, args); + } + + if (input.isTuple()) { + return decodeTupleInputData(input, args); + } + + return [ + { + name: input.name, + type: input.type as InputType, + value: args.toString(), + encodedValue: DefaultAbiCoder.encode([input.type], [args]).split("0x")[1], + inputs: [], + }, + ]; +} + +function decodeArrayInputData(input: ParamType, args: Result): InputData[] { + const inputs = args.flatMap((arg) => decodeInputData(input.arrayChildren!, arg)); + + return [ + { + name: input.name, + type: `${inputs[0]?.type ? `${inputs[0]?.type}[]` : input.type}`, + value: `[${inputs.map((input) => input.value).join(",")}]`, + inputs: inputs, + encodedValue: `[${inputs.map((input) => input.encodedValue).join(",")}]`, + }, + ]; +} + +function decodeTupleInputData(input: ParamType, args: Result): InputData[] { + const inputs = input.components!.flatMap((component: ParamType, index: number) => + decodeInputData(component, args[index]) + ); + + return [ + { + name: input.name, + type: `tuple(${inputs.map((input) => input.type).join(",")})`, + value: `(${inputs.map((input) => input.value).join(",")})`, + inputs, + encodedValue: `(${inputs.map((input) => input.encodedValue).join(",")})`, + }, + ]; +} + export function sortTokenTransfers(transfers: TokenTransfer[]): TokenTransfer[] { return [...transfers].sort((_, t) => { if (t.to === BOOTLOADER_FORMAL_ADDRESS || t.from === BOOTLOADER_FORMAL_ADDRESS) { diff --git a/packages/app/tests/composables/useTransactionData.spec.ts b/packages/app/tests/composables/useTransactionData.spec.ts index 0d6b921bb1..cfc625a85a 100644 --- a/packages/app/tests/composables/useTransactionData.spec.ts +++ b/packages/app/tests/composables/useTransactionData.spec.ts @@ -53,12 +53,14 @@ const transactionDataDecodedMethod = { inputs: [ { name: "recipient", + inputs: [], type: "address", value: "0xa1cf087DB965Ab02Fb3CFaCe1f5c63935815f044", encodedValue: "000000000000000000000000a1cf087db965ab02fb3cface1f5c63935815f044", }, { name: "amount", + inputs: [], type: "uint256", value: "1", encodedValue: "0000000000000000000000000000000000000000000000000000000000000001", @@ -175,12 +177,14 @@ describe("useTransactionData:", () => { inputs: [ { encodedValue: "000000000000000000000000a1cf087db965ab02fb3cface1f5c63935815f044", + inputs: [], name: "recipient", type: "address", value: "0xa1cf087DB965Ab02Fb3CFaCe1f5c63935815f044", }, { encodedValue: "0000000000000000000000000000000000000000000000000000000000000001", + inputs: [], name: "amount", type: "uint256", value: "1", diff --git a/packages/app/tests/utils/helpers.spec.ts b/packages/app/tests/utils/helpers.spec.ts index 6ad49b6614..63d5919ef3 100644 --- a/packages/app/tests/utils/helpers.spec.ts +++ b/packages/app/tests/utils/helpers.spec.ts @@ -1,17 +1,20 @@ import { describe, expect, it } from "vitest"; import { format } from "date-fns"; +import { ParamType } from "ethers"; import { utils } from "zksync-ethers"; import ExecuteTx from "@/../mock/transactions/Execute.json"; import type { InputType } from "@/composables/useEventLog"; import type { TokenTransfer } from "@/composables/useTransaction"; +import type { Result } from "ethers"; import { arrayHalfDivider, camelCaseFromSnakeCase, contractInputTypeToHumanType, + decodeInputData, getRawFunctionType, getRequiredArrayLength, getTypeFromEvent, @@ -178,4 +181,164 @@ describe("helpers:", () => { expect(truncateNumber("0.02", 5)).toEqual("0.02"); }); }); + describe("decodeInputData:", () => { + it("decodes a simple input type", () => { + const input = ParamType.from({ + name: "value", + type: "uint256", + baseType: "scalar", + isArray: () => true, + isTuple: () => false, + }); + const args = 42; + + const result = decodeInputData(input, args as unknown as Result); + + expect(result).toEqual([ + { + name: "value", + type: "uint256", + value: "42", + encodedValue: expect.any(String), + inputs: [], + }, + ]); + }); + + it("decodes an array input type", () => { + const input = ParamType.from({ + name: "values", + baseType: "array", + type: "uint256[]", + arrayChildren: { name: "value", type: "uint256" }, + }); + const args = [42, 43]; + + const result = decodeInputData(input, args as unknown as Result); + + expect(result).toEqual([ + { + name: "values", + type: "uint256[]", + value: "[42,43]", + inputs: [ + { + name: "", + type: "uint256", + value: "42", + encodedValue: "000000000000000000000000000000000000000000000000000000000000002a", + inputs: [], + }, + { + name: "", + type: "uint256", + value: "43", + encodedValue: "000000000000000000000000000000000000000000000000000000000000002b", + inputs: [], + }, + ], + encodedValue: + "[000000000000000000000000000000000000000000000000000000000000002a,000000000000000000000000000000000000000000000000000000000000002b]", + }, + ]); + }); + + it("decodes a tuple input type", () => { + const input = ParamType.from({ + name: "tupleValue", + type: "tuple", + baseType: "tuple", + components: [ + { name: "value1", type: "uint256", baseType: "scalar" }, + { name: "value2", type: "string", baseType: "scalar" }, + ], + }); + const args = ["42", "test"]; + + const result = decodeInputData(input, args as unknown as Result); + expect(result).toEqual([ + { + name: "tupleValue", + type: "tuple(uint256,string)", + value: "(42,test)", + inputs: [ + { + name: "value1", + type: "uint256", + value: "42", + encodedValue: "000000000000000000000000000000000000000000000000000000000000002a", + inputs: [], + }, + { + name: "value2", + type: "string", + value: "test", + encodedValue: + "000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000047465737400000000000000000000000000000000000000000000000000000000", + inputs: [], + }, + ], + encodedValue: + "(000000000000000000000000000000000000000000000000000000000000002a,000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000047465737400000000000000000000000000000000000000000000000000000000)", + }, + ]); + }); + + it("decodes a tuple with an array", () => { + const input = ParamType.from({ + name: "tupleValue", + type: "tuple", + baseType: "tuple", + components: [ + { name: "value1", type: "uint256", baseType: "scalar" }, + { name: "value2", type: "uint256[]", baseType: "array", arrayChildren: { name: "value", type: "uint256" } }, + ], + }); + const args = [42, [43, 44]]; + + const result = decodeInputData(input, args as unknown as Result); + + expect(result).toEqual([ + { + name: "tupleValue", + type: "tuple(uint256,uint256[])", + value: "(42,[43,44])", + inputs: [ + { + name: "value1", + type: "uint256", + value: "42", + encodedValue: "000000000000000000000000000000000000000000000000000000000000002a", + inputs: [], + }, + { + name: "value2", + type: "uint256[]", + value: "[43,44]", + inputs: [ + { + name: "", + type: "uint256", + value: "43", + encodedValue: "000000000000000000000000000000000000000000000000000000000000002b", + inputs: [], + }, + { + name: "", + type: "uint256", + value: "44", + encodedValue: "000000000000000000000000000000000000000000000000000000000000002c", + inputs: [], + }, + ], + encodedValue: + "[000000000000000000000000000000000000000000000000000000000000002b,000000000000000000000000000000000000000000000000000000000000002c]", + }, + ], + encodedValue: + "(000000000000000000000000000000000000000000000000000000000000002a,[000000000000000000000000000000000000000000000000000000000000002b,000000000000000000000000000000000000000000000000000000000000002c])", + }, + ]); + }); + }); });