Skip to content

Commit

Permalink
fix: calldata encoding fails for tuple params (#333)
Browse files Browse the repository at this point in the history
# What ❔

This PR implements support for encoding non-primitive input types. With
this update, all input data, including tuples, arrays, and combinations
of various types, will be correctly encoded.

## Why ❔

In the current version of the block explorer, users encounter an error
when input data includes complex objects or arrays.

For show we will use:
https://explorer.zksync.io/tx/0xe39dbc98b41bb22620b78e3a3848ab34982310f9564f27d1e809cfdd461fa54e

Example without fix: 

![image](https://github.com/user-attachments/assets/ece3ffcc-ff03-4eb1-9dd2-d380a4655695)

Example with fix: 

![image](https://github.com/user-attachments/assets/549059ae-a304-4fb7-9a69-bd3753cc9972)

fixes #321

## Checklist

<!-- Check your PR fulfills the following items. -->
<!-- For draft PRs check the boxes as you complete them. -->

- [+ ] PR title corresponds to the body of PR (we generate changelog
entries from PRs).
- [+ ] Tests for the changes have been added / updated.
- [ ] Documentation comments have been added / updated.

Co-authored-by: Vasyl Ivanchuk <[email protected]>
  • Loading branch information
kiriyaga-txfusion and vasyl-ivanchuk authored Jan 21, 2025
1 parent 5576811 commit 237bdf1
Show file tree
Hide file tree
Showing 4 changed files with 238 additions and 19 deletions.
34 changes: 16 additions & 18 deletions packages/app/src/composables/useTransactionData.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -10,43 +10,41 @@ 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(
transactionData: { calldata: TransactionData["calldata"]; value: TransactionData["value"] },
abi: AbiFragment[]
): TransactionData["method"] | undefined {
const contractInterface = new Interface(abi);

try {
const decodedData = contractInterface.parseTransaction({
data: transactionData.calldata,
value: transactionData.value,
})!;
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;
Expand Down
56 changes: 55 additions & 1 deletion packages/app/src/utils/helpers.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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'");
Expand Down Expand Up @@ -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) {
Expand Down
4 changes: 4 additions & 0 deletions packages/app/tests/composables/useTransactionData.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
163 changes: 163 additions & 0 deletions packages/app/tests/utils/helpers.spec.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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])",
},
]);
});
});
});

0 comments on commit 237bdf1

Please sign in to comment.