Repository: ponder-sh/ponder (ts)

Issue:
Indexer crashes when calling `readContract` during event handler
### Version

0.10.16

### Current behavior

When calling `context.client.readContract` in an event handler, the indexer crashes with `TypeError: Cannot convert undefined or null to object`

### Expected behavior

`context.client.readContract` succesfully calls the RPC to read contract data

### Steps to reproduce

Index an event with no args (for instance `event Paused()`) and then make a `context.client.readContract` call inside the indexing function.

### Link to repository

_No response_

### Anything else?

In debugging, the relevant code seems to be https://github.com/ponder-sh/ponder/blob/f325c381143c888a380b3ee2be4d2db4ed23a3b8/packages/core/src/indexing/profile.ts#L106 as `event.event.args` is undefined


*edit*: Fixed affected version (0.10.16 not 0.10.6). Currently on 0.10.9 which is working; discovered this when upgrading to latest.

Pull request description:
fix prediction for logs and traces with no args
Closes #1683 

Handles abi items such as:
```solidity
event Paused();
function execute() external;
```

Patch:
diff --git a/.changeset/twenty-coats-sit.md b/.changeset/twenty-coats-sit.md
new file mode 100644
index 00000000..76a970be
--- /dev/null
+++ b/.changeset/twenty-coats-sit.md
@@ -0,0 +1,5 @@
+---
+"ponder": patch
+---
+
+Fixed a bug affecting logs and traces with no args that would cause the error `TypeError: Cannot convert undefined or null to object`.
diff --git a/packages/core/src/indexing-store/profile.ts b/packages/core/src/indexing-store/profile.ts
index b69f8f8d..ac39fb79 100644
--- a/packages/core/src/indexing-store/profile.ts
+++ b/packages/core/src/indexing-store/profile.ts
@@ -132,18 +132,26 @@ export const recordProfilePattern = (
       }
 
       case "log": {
-        let hasMatch = false;
-        for (const argKey of Object.keys(event.event.args)) {
-          const argValue = event.event.args[argKey];
-
-          if (typeof argValue !== "object" && eq(argValue, value)) {
-            result[js] = ["args", argKey];
-            hasMatch = true;
-            break;
+        // Note: explicitly skip profiling args if they are an array
+        if (
+          event.event.args !== undefined &&
+          Array.isArray(event.event.args) === false
+        ) {
+          let hasMatch = false;
+          for (const argKey of Object.keys(event.event.args)) {
+            const argValue = (event.event.args as { [key: string]: unknown })[
+              argKey
+            ] as string | bigint | number | boolean;
+
+            if (typeof argValue !== "object" && eq(argValue, value)) {
+              result[js] = ["args", argKey];
+              hasMatch = true;
+              break;
+            }
           }
-        }
 
-        if (hasMatch) continue;
+          if (hasMatch) continue;
+        }
 
         if (eq(event.event.log.address, value)) {
           result[js] = ["log", "address"];
@@ -211,13 +219,40 @@ export const recordProfilePattern = (
 
       case "trace": {
         let hasMatch = false;
-        for (const argKey of Object.keys(event.event.args)) {
-          const argValue = event.event.args[argKey];
 
-          if (typeof argValue !== "object" && eq(argValue, value)) {
-            result[js] = ["args", argKey];
-            hasMatch = true;
-            break;
+        // Note: explicitly skip profiling args if they are an array
+        if (
+          event.event.args !== undefined &&
+          Array.isArray(event.event.args) === false
+        ) {
+          for (const argKey of Object.keys(event.event.args)) {
+            const argValue = (event.event.args as { [key: string]: unknown })[
+              argKey
+            ] as string | bigint | number | boolean;
+
+            if (typeof argValue !== "object" && eq(argValue, value)) {
+              result[js] = ["args", argKey];
+              hasMatch = true;
+              break;
+            }
+          }
+        }
+
+        // Note: explicitly skip profiling result if it is an array
+        if (
+          event.event.result !== undefined &&
+          Array.isArray(event.event.result) === false
+        ) {
+          for (const argKey of Object.keys(event.event.result)) {
+            const argValue = (event.event.result as { [key: string]: unknown })[
+              argKey
+            ] as string | bigint | number | boolean;
+
+            if (typeof argValue !== "object" && eq(argValue, value)) {
+              result[js] = ["result", argKey];
+              hasMatch = true;
+              break;
+            }
           }
         }
 
diff --git a/packages/core/src/indexing/index.ts b/packages/core/src/indexing/index.ts
index 32e53fe8..c7c2acae 100644
--- a/packages/core/src/indexing/index.ts
+++ b/packages/core/src/indexing/index.ts
@@ -341,7 +341,7 @@ export const toErrorMeta = (
 
     case "log": {
       return [
-        `Event arguments:\n${prettyPrint(event?.event?.args)}`,
+        `Event arguments:\n${prettyPrint(Array.isArray(event.event?.args) ? undefined : event.event?.args)}`,
         logText(event?.event?.log),
         transactionText(event?.event?.transaction),
         blockText(event?.event?.block),
@@ -350,7 +350,7 @@ export const toErrorMeta = (
 
     case "trace": {
       return [
-        `Call trace arguments:\n${prettyPrint(event?.event?.args)}`,
+        `Call trace arguments:\n${prettyPrint(Array.isArray(event.event?.args) ? undefined : event.event?.args)}`,
         traceText(event?.event?.trace),
         transactionText(event?.event?.transaction),
         blockText(event?.event?.block),
diff --git a/packages/core/src/indexing/profile.ts b/packages/core/src/indexing/profile.ts
index e7dc33ad..66f184a9 100644
--- a/packages/core/src/indexing/profile.ts
+++ b/packages/core/src/indexing/profile.ts
@@ -102,18 +102,27 @@ export const recordProfilePattern = ({
     }
 
     case "log": {
-      let hasMatch = false;
-      for (const argKey of Object.keys(event.event.args)) {
-        const argValue = event.event.args[argKey];
+      // Note: explicitly skip profiling args if they are an array
+      if (
+        event.event.args !== undefined &&
+        Array.isArray(event.event.args) === false
+      ) {
+        let hasMatch = false;
+
+        for (const argKey of Object.keys(event.event.args)) {
+          const argValue = (event.event.args as { [key: string]: unknown })[
+            argKey
+          ] as string | bigint | number | boolean;
 
-        if (typeof argValue !== "object" && eq(argValue, args.address)) {
-          resultAddress = { type: "derived", value: ["args", argKey] };
-          hasMatch = true;
-          break;
+          if (typeof argValue !== "object" && eq(argValue, args.address)) {
+            resultAddress = { type: "derived", value: ["args", argKey] };
+            hasMatch = true;
+            break;
+          }
         }
-      }
 
-      if (hasMatch) break;
+        if (hasMatch) break;
+      }
 
       if (eq(event.event.log.address, args.address)) {
         resultAddress = { type: "derived", value: ["log", "address"] };
@@ -154,13 +163,40 @@ export const recordProfilePattern = ({
 
     case "trace": {
       let hasMatch = false;
-      for (const argKey of Object.keys(event.event.args)) {
-        const argValue = event.event.args[argKey];
 
-        if (typeof argValue !== "object" && eq(argValue, args.address)) {
-          resultAddress = { type: "derived", value: ["args", argKey] };
-          hasMatch = true;
-          break;
+      // Note: explicitly skip profiling args if they are an array
+      if (
+        event.event.args !== undefined &&
+        Array.isArray(event.event.args) === false
+      ) {
+        for (const argKey of Object.keys(event.event.args)) {
+          const argValue = (event.event.args as { [key: string]: unknown })[
+            argKey
+          ] as string | bigint | number | boolean;
+
+          if (typeof argValue !== "object" && eq(argValue, args.address)) {
+            resultAddress = { type: "derived", value: ["args", argKey] };
+            hasMatch = true;
+            break;
+          }
+        }
+      }
+
+      // Note: explicitly skip profiling result if it is an array
+      if (
+        event.event.result !== undefined &&
+        Array.isArray(event.event.result) === false
+      ) {
+        for (const argKey of Object.keys(event.event.result)) {
+          const argValue = (event.event.result as { [key: string]: unknown })[
+            argKey
+          ] as string | bigint | number | boolean;
+
+          if (typeof argValue !== "object" && eq(argValue, args.address)) {
+            resultAddress = { type: "derived", value: ["result", argKey] };
+            hasMatch = true;
+            break;
+          }
         }
       }
 
@@ -372,18 +408,27 @@ export const recordProfilePattern = ({
       }
 
       case "log": {
-        let hasMatch = false;
-        for (const argKey of Object.keys(event.event.args)) {
-          const argValue = event.event.args[argKey];
-
-          if (typeof argValue !== "object" && eq(argValue, arg)) {
-            resultArgs.push({ type: "derived", value: ["args", argKey] });
-            hasMatch = true;
-            break;
+        // Note: explicitly skip profiling args if they are an array
+        if (
+          event.event.args !== undefined &&
+          Array.isArray(event.event.args) === false
+        ) {
+          let hasMatch = false;
+
+          for (const argKey of Object.keys(event.event.args)) {
+            const argValue = (event.event.args as { [key: string]: unknown })[
+              argKey
+            ] as string | bigint | number | boolean;
+
+            if (typeof argValue !== "object" && eq(argValue, arg)) {
+              resultArgs.push({ type: "derived", value: ["args", argKey] });
+              hasMatch = true;
+              break;
+            }
           }
-        }
 
-        if (hasMatch) continue;
+          if (hasMatch) continue;
+        }
 
         if (eq(event.event.log.address, arg)) {
           resultArgs.push({ type: "derived", value: ["log", "address"] });
@@ -454,13 +499,40 @@ export const recordProfilePattern = ({
 
       case "trace": {
         let hasMatch = false;
-        for (const argKey of Object.keys(event.event.args)) {
-          const argValue = event.event.args[argKey];
 
-          if (typeof argValue !== "object" && eq(argValue, arg)) {
-            resultArgs.push({ type: "derived", value: ["args", argKey] });
-            hasMatch = true;
-            break;
+        // Note: explicitly skip profiling args if they are an array
+        if (
+          event.event.args !== undefined &&
+          Array.isArray(event.event.args) === false
+        ) {
+          for (const argKey of Object.keys(event.event.args)) {
+            const argValue = (event.event.args as { [key: string]: unknown })[
+              argKey
+            ] as string | bigint | number | boolean;
+
+            if (typeof argValue !== "object" && eq(argValue, arg)) {
+              resultArgs.push({ type: "derived", value: ["args", argKey] });
+              hasMatch = true;
+              break;
+            }
+          }
+        }
+
+        // Note: explicitly skip profiling result if it is an array
+        if (
+          event.event.result !== undefined &&
+          Array.isArray(event.event.result) === false
+        ) {
+          for (const argKey of Object.keys(event.event.result)) {
+            const argValue = (event.event.result as { [key: string]: unknown })[
+              argKey
+            ] as string | bigint | number | boolean;
+
+            if (typeof argValue !== "object" && eq(argValue, arg)) {
+              resultArgs.push({ type: "derived", value: ["result", argKey] });
+              hasMatch = true;
+              break;
+            }
           }
         }
 
diff --git a/packages/core/src/internal/types.ts b/packages/core/src/internal/types.ts
index 8066a537..f0b9f0cd 100644
--- a/packages/core/src/internal/types.ts
+++ b/packages/core/src/internal/types.ts
@@ -441,7 +441,7 @@ export type LogEvent = {
 
   event: {
     id: string;
-    args: any;
+    args: { [key: string]: unknown } | readonly unknown[] | undefined;
     log: Log;
     block: Block;
     transaction: Transaction;
@@ -507,8 +507,8 @@ export type TraceEvent = {
 
   event: {
     id: string;
-    args: any;
-    result: any;
+    args: { [key: string]: unknown } | readonly unknown[] | undefined;
+    result: { [key: string]: unknown } | readonly unknown[] | undefined;
     trace: Trace;
     block: Block;
     transaction: Transaction;


Test patch:
diff --git a/packages/core/src/indexing-store/profile.test.ts b/packages/core/src/indexing-store/profile.test.ts
index 126dd32f..918f8333 100644
--- a/packages/core/src/indexing-store/profile.test.ts
+++ b/packages/core/src/indexing-store/profile.test.ts
@@ -1,5 +1,5 @@
 import { onchainTable } from "@/drizzle/onchain.js";
-import type { BlockEvent, LogEvent } from "@/internal/types.js";
+import type { BlockEvent, LogEvent, TraceEvent } from "@/internal/types.js";
 import { ZERO_CHECKPOINT_STRING } from "@/utils/checkpoint.js";
 import type { Column, Table } from "drizzle-orm";
 import { zeroAddress } from "viem";
@@ -40,6 +40,156 @@ test("recordProfilePattern() no pattern", () => {
   expect(pattern).toBeUndefined();
 });
 
+test("recordProfilePattern() with undefined log event args", () => {
+  const event = {
+    type: "log",
+    chainId: 1,
+    checkpoint: ZERO_CHECKPOINT_STRING,
+    name: "",
+    event: {
+      id: ZERO_CHECKPOINT_STRING,
+      args: undefined,
+      log: {} as LogEvent["event"]["log"],
+      transaction: {} as LogEvent["event"]["transaction"],
+      block: {} as BlockEvent["event"]["block"],
+    },
+  } satisfies LogEvent;
+
+  const schema = {
+    account: onchainTable("account", (p) => ({
+      address: p.hex().primaryKey(),
+      balance: p.bigint().notNull(),
+    })),
+  };
+
+  const primaryKeyCache = new Map<Table, [string, Column][]>();
+
+  primaryKeyCache.set(schema.account, [["address", schema.account.address]]);
+
+  const pattern = recordProfilePattern(
+    event,
+    schema.account,
+    { address: zeroAddress },
+    [],
+    primaryKeyCache,
+  );
+
+  expect(pattern).toBeUndefined();
+});
+
+test("recordProfilePattern() with undefined trace event args", () => {
+  const event = {
+    type: "trace",
+    chainId: 1,
+    checkpoint: ZERO_CHECKPOINT_STRING,
+    name: "",
+    event: {
+      id: ZERO_CHECKPOINT_STRING,
+      args: undefined,
+      result: undefined,
+      trace: {} as TraceEvent["event"]["trace"],
+      transaction: {} as TraceEvent["event"]["transaction"],
+      block: {} as BlockEvent["event"]["block"],
+    },
+  } satisfies TraceEvent;
+
+  const schema = {
+    account: onchainTable("account", (p) => ({
+      address: p.hex().primaryKey(),
+      balance: p.bigint().notNull(),
+    })),
+  };
+
+  const primaryKeyCache = new Map<Table, [string, Column][]>();
+
+  primaryKeyCache.set(schema.account, [["address", schema.account.address]]);
+
+  const pattern = recordProfilePattern(
+    event,
+    schema.account,
+    { address: zeroAddress },
+    [],
+    primaryKeyCache,
+  );
+
+  expect(pattern).toBeUndefined();
+});
+
+test("recordProfilePattern() with array log event args", () => {
+  const event = {
+    type: "log",
+    chainId: 1,
+    checkpoint: ZERO_CHECKPOINT_STRING,
+    name: "",
+    event: {
+      id: ZERO_CHECKPOINT_STRING,
+      args: [],
+      log: {} as LogEvent["event"]["log"],
+      transaction: {} as LogEvent["event"]["transaction"],
+      block: {} as BlockEvent["event"]["block"],
+    },
+  } satisfies LogEvent;
+
+  const schema = {
+    account: onchainTable("account", (p) => ({
+      address: p.hex().primaryKey(),
+      balance: p.bigint().notNull(),
+    })),
+  };
+
+  const primaryKeyCache = new Map<Table, [string, Column][]>();
+
+  primaryKeyCache.set(schema.account, [["address", schema.account.address]]);
+
+  const pattern = recordProfilePattern(
+    event,
+    schema.account,
+    { address: zeroAddress },
+    [],
+    primaryKeyCache,
+  );
+
+  expect(pattern).toBeUndefined();
+});
+
+test("recordProfilePattern() with array trace event args", () => {
+  const event = {
+    type: "trace",
+    chainId: 1,
+    checkpoint: ZERO_CHECKPOINT_STRING,
+    name: "",
+    event: {
+      id: ZERO_CHECKPOINT_STRING,
+      args: [],
+      result: [],
+      trace: {} as TraceEvent["event"]["trace"],
+      transaction: {} as TraceEvent["event"]["transaction"],
+      block: {} as BlockEvent["event"]["block"],
+    },
+  } satisfies TraceEvent;
+
+  const schema = {
+    account: onchainTable("account", (p) => ({
+      address: p.hex().primaryKey(),
+      balance: p.bigint().notNull(),
+    })),
+  };
+
+  const primaryKeyCache = new Map<Table, [string, Column][]>();
+
+  primaryKeyCache.set(schema.account, [["address", schema.account.address]]);
+
+  const pattern = recordProfilePattern(
+    event,
+    schema.account,
+    { address: zeroAddress },
+    [],
+    primaryKeyCache,
+  );
+
+  expect(pattern).toBeUndefined();
+});
+
 test("recordProfilePattern() chainId", () => {
   const event = {
     type: "block",
diff --git a/packages/core/src/indexing/profile.test.ts b/packages/core/src/indexing/profile.test.ts
index 835813fb..e986a22f 100644
--- a/packages/core/src/indexing/profile.test.ts
+++ b/packages/core/src/indexing/profile.test.ts
@@ -1,11 +1,413 @@
 import { ALICE } from "@/_test/constants.js";
 import { erc20ABI } from "@/_test/generated.js";
-import type { BlockEvent, LogEvent } from "@/internal/types.js";
+import type { BlockEvent, LogEvent, TraceEvent } from "@/internal/types.js";
 import { ZERO_CHECKPOINT_STRING } from "@/utils/checkpoint.js";
 import { getAbiItem, zeroAddress } from "viem";
 import { expect, test } from "vitest";
 import { recordProfilePattern, recoverProfilePattern } from "./profile.js";
 
+test("recordProfilePattern() with undefined log event args", () => {
+  const event = {
+    type: "log",
+    chainId: 1,
+    checkpoint: ZERO_CHECKPOINT_STRING,
+    name: "",
+    event: {
+      id: ZERO_CHECKPOINT_STRING,
+      args: undefined,
+      log: {} as LogEvent["event"]["log"],
+      transaction: {} as LogEvent["event"]["transaction"],
+      block: { number: 5n } as BlockEvent["event"]["block"],
+    },
+  } satisfies LogEvent;
+
+  const pattern = recordProfilePattern({
+    event,
+    args: {
+      address: zeroAddress,
+      abi: [getAbiItem({ abi: erc20ABI, name: "balanceOf" })],
+      functionName: "balanceOf",
+      args: [ALICE],
+    },
+    hints: [],
+  });
+
+  expect(pattern).toMatchInlineSnapshot(`
+    {
+      "hasConstant": true,
+      "pattern": {
+        "abi": [
+          {
+            "inputs": [
+              {
+                "internalType": "address",
+                "name": "",
+                "type": "address",
+              },
+            ],
+            "name": "balanceOf",
+            "outputs": [
+              {
+                "internalType": "uint256",
+                "name": "",
+                "type": "uint256",
+              },
+            ],
+            "stateMutability": "view",
+            "type": "function",
+          },
+        ],
+        "address": {
+          "type": "constant",
+          "value": "0x0000000000000000000000000000000000000000",
+        },
+        "args": [
+          {
+            "type": "constant",
+            "value": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
+          },
+        ],
+        "functionName": "balanceOf",
+      },
+    }
+  `);
+
+  expect(recoverProfilePattern(pattern!.pattern, event)).toMatchInlineSnapshot(`
+    {
+      "abi": [
+        {
+          "inputs": [
+            {
+              "internalType": "address",
+              "name": "",
+              "type": "address",
+            },
+          ],
+          "name": "balanceOf",
+          "outputs": [
+            {
+              "internalType": "uint256",
+              "name": "",
+              "type": "uint256",
+            },
+          ],
+          "stateMutability": "view",
+          "type": "function",
+        },
+      ],
+      "address": "0x0000000000000000000000000000000000000000",
+      "args": [
+        "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
+      ],
+      "blockNumber": 5n,
+      "chainId": 1,
+      "functionName": "balanceOf",
+    }
+  `);
+});
+
+test("recordProfilePattern() with undefined trace event args", () => {
+  const event = {
+    type: "trace",
+    chainId: 1,
+    checkpoint: ZERO_CHECKPOINT_STRING,
+    name: "",
+    event: {
+      id: ZERO_CHECKPOINT_STRING,
+      args: undefined,
+      result: undefined,
+      trace: {} as TraceEvent["event"]["trace"],
+      transaction: {} as TraceEvent["event"]["transaction"],
+      block: { number: 5n } as BlockEvent["event"]["block"],
+    },
+  } satisfies TraceEvent;
+
+  const pattern = recordProfilePattern({
+    event,
+    args: {
+      address: zeroAddress,
+      abi: [getAbiItem({ abi: erc20ABI, name: "balanceOf" })],
+      functionName: "balanceOf",
+      args: [ALICE],
+    },
+    hints: [],
+  });
+
+  expect(pattern).toMatchInlineSnapshot(`
+    {
+      "hasConstant": true,
+      "pattern": {
+        "abi": [
+          {
+            "inputs": [
+              {
+                "internalType": "address",
+                "name": "",
+                "type": "address",
+              },
+            ],
+            "name": "balanceOf",
+            "outputs": [
+              {
+                "internalType": "uint256",
+                "name": "",
+                "type": "uint256",
+              },
+            ],
+            "stateMutability": "view",
+            "type": "function",
+          },
+        ],
+        "address": {
+          "type": "constant",
+          "value": "0x0000000000000000000000000000000000000000",
+        },
+        "args": [
+          {
+            "type": "constant",
+            "value": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
+          },
+        ],
+        "functionName": "balanceOf",
+      },
+    }
+  `);
+
+  expect(recoverProfilePattern(pattern!.pattern, event)).toMatchInlineSnapshot(`
+    {
+      "abi": [
+        {
+          "inputs": [
+            {
+              "internalType": "address",
+              "name": "",
+              "type": "address",
+            },
+          ],
+          "name": "balanceOf",
+          "outputs": [
+            {
+              "internalType": "uint256",
+              "name": "",
+              "type": "uint256",
+            },
+          ],
+          "stateMutability": "view",
+          "type": "function",
+        },
+      ],
+      "address": "0x0000000000000000000000000000000000000000",
+      "args": [
+        "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
+      ],
+      "blockNumber": 5n,
+      "chainId": 1,
+      "functionName": "balanceOf",
+    }
+  `);
+});
+
+test("recordProfilePattern() with array log event args", () => {
+  const event = {
+    type: "log",
+    chainId: 1,
+    checkpoint: ZERO_CHECKPOINT_STRING,
+    name: "",
+    event: {
+      id: ZERO_CHECKPOINT_STRING,
+      args: [],
+      log: {} as LogEvent["event"]["log"],
+      transaction: {} as LogEvent["event"]["transaction"],
+      block: { number: 5n } as BlockEvent["event"]["block"],
+    },
+  } satisfies LogEvent;
+
+  const pattern = recordProfilePattern({
+    event,
+    args: {
+      address: zeroAddress,
+      abi: [getAbiItem({ abi: erc20ABI, name: "balanceOf" })],
+      functionName: "balanceOf",
+      args: [ALICE],
+    },
+    hints: [],
+  });
+
+  expect(pattern).toMatchInlineSnapshot(`
+    {
+      "hasConstant": true,
+      "pattern": {
+        "abi": [
+          {
+            "inputs": [
+              {
+                "internalType": "address",
+                "name": "",
+                "type": "address",
+              },
+            ],
+            "name": "balanceOf",
+            "outputs": [
+              {
+                "internalType": "uint256",
+                "name": "",
+                "type": "uint256",
+              },
+            ],
+            "stateMutability": "view",
+            "type": "function",
+          },
+        ],
+        "address": {
+          "type": "constant",
+          "value": "0x0000000000000000000000000000000000000000",
+        },
+        "args": [
+          {
+            "type": "constant",
+            "value": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
+          },
+        ],
+        "functionName": "balanceOf",
+      },
+    }
+  `);
+
+  expect(recoverProfilePattern(pattern!.pattern, event)).toMatchInlineSnapshot(`
+    {
+      "abi": [
+        {
+          "inputs": [
+            {
+              "internalType": "address",
+              "name": "",
+              "type": "address",
+            },
+          ],
+          "name": "balanceOf",
+          "outputs": [
+            {
+              "internalType": "uint256",
+              "name": "",
+              "type": "uint256",
+            },
+          ],
+          "stateMutability": "view",
+          "type": "function",
+        },
+      ],
+      "address": "0x0000000000000000000000000000000000000000",
+      "args": [
+        "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
+      ],
+      "blockNumber": 5n,
+      "chainId": 1,
+      "functionName": "balanceOf",
+    }
+  `);
+});
+
+test("recordProfilePattern() with array trace event args", () => {
+  const event = {
+    type: "trace",
+    chainId: 1,
+    checkpoint: ZERO_CHECKPOINT_STRING,
+    name: "",
+    event: {
+      id: ZERO_CHECKPOINT_STRING,
+      args: [],
+      result: [],
+      trace: {} as TraceEvent["event"]["trace"],
+      transaction: {} as TraceEvent["event"]["transaction"],
+      block: { number: 5n } as BlockEvent["event"]["block"],
+    },
+  } satisfies TraceEvent;
+
+  const pattern = recordProfilePattern({
+    event,
+    args: {
+      address: zeroAddress,
+      abi: [getAbiItem({ abi: erc20ABI, name: "balanceOf" })],
+      functionName: "balanceOf",
+      args: [ALICE],
+    },
+    hints: [],
+  });
+
+  expect(pattern).toMatchInlineSnapshot(`
+    {
+      "hasConstant": true,
+      "pattern": {
+        "abi": [
+          {
+            "inputs": [
+              {
+                "internalType": "address",
+                "name": "",
+                "type": "address",
+              },
+            ],
+            "name": "balanceOf",
+            "outputs": [
+              {
+                "internalType": "uint256",
+                "name": "",
+                "type": "uint256",
+              },
+            ],
+            "stateMutability": "view",
+            "type": "function",
+          },
+        ],
+        "address": {
+          "type": "constant",
+          "value": "0x0000000000000000000000000000000000000000",
+        },
+        "args": [
+          {
+            "type": "constant",
+            "value": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
+          },
+        ],
+        "functionName": "balanceOf",
+      },
+    }
+  `);
+
+  expect(recoverProfilePattern(pattern!.pattern, event)).toMatchInlineSnapshot(`
+    {
+      "abi": [
+        {
+          "inputs": [
+            {
+              "internalType": "address",
+              "name": "",
+              "type": "address",
+            },
+          ],
+          "name": "balanceOf",
+          "outputs": [
+            {
+              "internalType": "uint256",
+              "name": "",
+              "type": "uint256",
+            },
+          ],
+          "stateMutability": "view",
+          "type": "function",
+        },
+      ],
+      "address": "0x0000000000000000000000000000000000000000",
+      "args": [
+        "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
+      ],
+      "blockNumber": 5n,
+      "chainId": 1,
+      "functionName": "balanceOf",
+    }
+  `);
+});
+
 test("recordProfilePattern() address", () => {
   const event = {
     type: "log",