fix: validate bridged ABI and malformed logs

This commit is contained in:
golem
2026-08-21 03:19:22 -06:00
parent ba393bbbc3
commit 9ce8843279
4 changed files with 79 additions and 18 deletions
+11
View File
@@ -135,6 +135,17 @@ describe("loadDashboardSnapshot", () => {
expect(snapshot.diagnostics).toEqual([expect.objectContaining({ blockNumber: 4n, logIndex: 2, message: expect.any(String) })]);
});
it("diagnoses proxy logs with malformed topics or payload data", async () => {
// Catches payload-shape validation skipping malformed proxy logs before the diagnostic boundary.
const reader = new ReaderDouble();
const valid = depositedLog(4n, 1, owner, 7n);
reader.logs = [valid, { ...valid, logIndex: 2, topics: "not-topics" }, { ...valid, logIndex: 3, data: "not-hex" }, { ...valid, logIndex: 4, data: undefined }];
const snapshot = await loadDashboardSnapshot(reader, manifest);
expect(snapshot.activity).toHaveLength(1);
expect(snapshot.diagnostics.map((diagnostic) => diagnostic.logIndex)).toEqual([2, 3, 4]);
});
it("preserves a rejected contract read instead of substituting zero", async () => {
// Catches disconnected RPC reads being silently rendered as zero balances.
const reader = new ReaderDouble(); reader.error = new Error("node read failed");
+10 -5
View File
@@ -106,9 +106,10 @@ function decodeActivity(logs: readonly unknown[], proxy: Address): { activity: r
const activity: Activity[] = [];
const diagnostics: DecodeDiagnostic[] = [];
for (const log of logs) {
if (!isLog(log) || !sameAddress(log.address, proxy)) continue;
if (!isLogIdentity(log) || !sameAddress(log.address, proxy)) continue;
const identity = { blockNumber: log.blockNumber, logIndex: log.logIndex, transactionHash: log.transactionHash };
try {
if (!isLogPayload(log)) throw new Error("proxy event payload is malformed");
const decoded = decodeEventLog({ abi: bankV1Abi, data: log.data, topics: log.topics as [Hex, ...Hex[]] });
const next = decodedActivity(decoded.eventName, decoded.args, identity);
if (next) activity.push(next);
@@ -121,14 +122,18 @@ function decodeActivity(logs: readonly unknown[], proxy: Address): { activity: r
return { activity, diagnostics };
}
function isLog(value: unknown): value is Readonly<{ address: Address; blockNumber: bigint; logIndex: number; transactionHash: Hex; data: Hex; topics: readonly Hex[] }> {
function isLogIdentity(value: unknown): value is Readonly<{ address: Address; blockNumber: bigint; logIndex: number; transactionHash: Hex; data?: unknown; topics?: unknown }> {
return typeof value === "object" && value !== null
&& typeof (value as { address?: unknown }).address === "string" && isAddress((value as { address: string }).address)
&& typeof (value as { blockNumber?: unknown }).blockNumber === "bigint"
&& typeof (value as { logIndex?: unknown }).logIndex === "number"
&& typeof (value as { transactionHash?: unknown }).transactionHash === "string"
&& typeof (value as { data?: unknown }).data === "string"
&& Array.isArray((value as { topics?: unknown }).topics);
&& typeof (value as { transactionHash?: unknown }).transactionHash === "string";
}
function isLogPayload(value: Readonly<{ data?: unknown; topics?: unknown }>): value is Readonly<{ data: Hex; topics: readonly Hex[] }> {
return typeof value.data === "string" && /^0x[0-9a-fA-F]*$/.test(value.data)
&& Array.isArray(value.topics) && value.topics.length > 0
&& value.topics.every((topic) => typeof topic === "string" && /^0x[0-9a-fA-F]*$/.test(topic));
}
function decodedActivity(eventName: string, args: unknown, identity: Readonly<{ blockNumber: bigint; logIndex: number; transactionHash: Hex }>): Activity | undefined {