From c83616ad33aa06054342a3bf72bcb51c09ee0ada Mon Sep 17 00:00:00 2001 From: jxom Date: Fri, 7 Apr 2023 11:38:39 +1000 Subject: [PATCH] fix: skip invalid logs (#326) * fix: skip invalid logs * chore: format * chore: changeset --- .changeset/spotty-chefs-glow.md | 5 + contracts/src/ERC20InvalidTransferEvent.sol | 12 + src/_test/index.ts | 1 + src/_test/utils.ts | 15 +- src/actions/public/getFilterChanges.test.ts | 93 ++++ src/actions/public/getFilterChanges.ts | 36 +- src/actions/public/getFilterLogs.test.ts | 93 ++++ src/actions/public/getFilterLogs.ts | 33 +- src/actions/public/getLogs.test.ts | 85 ++++ src/actions/public/getLogs.ts | 28 +- src/actions/public/multicall.test.ts | 16 +- src/errors/abi.test.ts | 52 ++- src/errors/abi.ts | 67 ++- src/errors/index.ts | 1 + src/utils/abi/decodeAbiParameters.test.ts | 18 + src/utils/abi/decodeAbiParameters.ts | 10 +- src/utils/abi/decodeEventLog.test.ts | 95 +++++ src/utils/abi/decodeEventLog.ts | 24 +- src/utils/abi/formatAbiItem.test.ts | 446 ++++++++++++-------- src/utils/abi/formatAbiItem.ts | 12 +- src/utils/abi/index.test.ts | 1 + src/utils/abi/index.ts | 2 +- src/utils/index.test.ts | 1 + src/utils/index.ts | 1 + 24 files changed, 902 insertions(+), 245 deletions(-) create mode 100644 .changeset/spotty-chefs-glow.md create mode 100644 contracts/src/ERC20InvalidTransferEvent.sol diff --git a/.changeset/spotty-chefs-glow.md b/.changeset/spotty-chefs-glow.md new file mode 100644 index 0000000000..39368a9bfe --- /dev/null +++ b/.changeset/spotty-chefs-glow.md @@ -0,0 +1,5 @@ +--- +"viem": patch +--- + +Fixed an issue where filtered logs that do not conform to the provided ABI would cause `getLogs`, `getFilterLogs` or `getFilterChanges` to throw – these logs are now skipped. See [#323](https://github.com/wagmi-dev/viem/issues/323#issuecomment-1499654052) for more info. diff --git a/contracts/src/ERC20InvalidTransferEvent.sol b/contracts/src/ERC20InvalidTransferEvent.sol new file mode 100644 index 0000000000..370ee5fd36 --- /dev/null +++ b/contracts/src/ERC20InvalidTransferEvent.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity ^0.8.13; + +contract ERC20InvalidTransferEvent { + // Non-conforming `to` parameter (not indexed). + event Transfer(address indexed from, address to, uint256 value); + + function transfer(address recipient, uint256 amount) public { + emit Transfer(msg.sender, recipient, amount); + return; + } +} diff --git a/src/_test/index.ts b/src/_test/index.ts index 6959b36aab..8ff1f749e3 100644 --- a/src/_test/index.ts +++ b/src/_test/index.ts @@ -22,6 +22,7 @@ export { deploy, deployBAYC, deployEnsAvatarTokenUri, + deployErc20InvalidTransferEvent, publicClient, testClient, walletClient, diff --git a/src/_test/utils.ts b/src/_test/utils.ts index 617af795c9..63da440c0c 100644 --- a/src/_test/utils.ts +++ b/src/_test/utils.ts @@ -1,6 +1,7 @@ /* c8 ignore start */ import type { Abi } from 'abitype' import ensAvatarTokenUri from '../../contracts/out/EnsAvatarTokenUri.sol/EnsAvatarTokenUri.json' +import erc20InvalidTransferEvent from '../../contracts/out/ERC20InvalidTransferEvent.sol/ERC20InvalidTransferEvent.json' import errorsExample from '../../contracts/out/ErrorsExample.sol/ErrorsExample.json' import { deployContract, @@ -26,7 +27,11 @@ import { RpcError } from '../types/eip1193' import { rpc } from '../utils' import { baycContractConfig, ensRegistryConfig } from './abis' import { accounts, address, localWsUrl } from './constants' -import { ensAvatarTokenUriABI, errorsExampleABI } from './generated' +import { + ensAvatarTokenUriABI, + erc20InvalidTransferEventABI, + errorsExampleABI, +} from './generated' import type { RequestListener } from 'http' import { createServer } from 'http' @@ -190,6 +195,14 @@ export async function deployEnsAvatarTokenUri() { }) } +export async function deployErc20InvalidTransferEvent() { + return deploy({ + abi: erc20InvalidTransferEventABI, + bytecode: erc20InvalidTransferEvent.bytecode.object as Hex, + account: accounts[0].address, + }) +} + export async function setBlockNumber(blockNumber: bigint) { await reset(testClient, { blockNumber, diff --git a/src/actions/public/getFilterChanges.test.ts b/src/actions/public/getFilterChanges.test.ts index 2801abd162..75f9c2b85d 100644 --- a/src/actions/public/getFilterChanges.test.ts +++ b/src/actions/public/getFilterChanges.test.ts @@ -3,12 +3,14 @@ import { afterAll, assertType, beforeAll, describe, expect, test } from 'vitest' import { accounts, address, + deployErc20InvalidTransferEvent, initialBlockNumber, publicClient, testClient, walletClient, usdcContractConfig, } from '../../_test' +import { erc20InvalidTransferEventABI } from '../../_test/generated' import { impersonateAccount, @@ -47,6 +49,27 @@ const event = { name: 'Transfer', type: 'event', }, + invalid: { + inputs: [ + { + indexed: true, + name: 'from', + type: 'address', + }, + { + indexed: false, + name: 'to', + type: 'address', + }, + { + indexed: false, + name: 'value', + type: 'uint256', + }, + ], + name: 'Transfer', + type: 'event', + }, unnamed: { inputs: [ { @@ -870,3 +893,73 @@ describe('events', () => { ]) }) }) + +describe('skip invalid logs', () => { + test('indexed params mismatch', async () => { + const { contractAddress } = await deployErc20InvalidTransferEvent() + + const filter = await createEventFilter(publicClient, { + event: event.default, + }) + + await writeContract(walletClient, { + ...usdcContractConfig, + functionName: 'transfer', + args: [accounts[0].address, 1n], + account: address.vitalik, + }) + await writeContract(walletClient, { + abi: erc20InvalidTransferEventABI, + address: contractAddress!, + functionName: 'transfer', + args: [accounts[0].address, 1n], + account: address.vitalik, + }) + await writeContract(walletClient, { + abi: erc20InvalidTransferEventABI, + address: contractAddress!, + functionName: 'transfer', + args: [accounts[1].address, 1n], + account: address.vitalik, + }) + await mine(testClient, { blocks: 1 }) + + const logs = await getFilterChanges(publicClient, { filter }) + assertType(logs) + expect(logs.length).toBe(1) + }) + + test('non-indexed params mismatch', async () => { + const { contractAddress } = await deployErc20InvalidTransferEvent() + + const filter = await createEventFilter(publicClient, { + event: event.invalid, + }) + + await writeContract(walletClient, { + ...usdcContractConfig, + functionName: 'transfer', + args: [accounts[0].address, 1n], + account: address.vitalik, + }) + await writeContract(walletClient, { + abi: erc20InvalidTransferEventABI, + address: contractAddress!, + functionName: 'transfer', + args: [accounts[0].address, 1n], + account: address.vitalik, + }) + await writeContract(walletClient, { + abi: erc20InvalidTransferEventABI, + address: contractAddress!, + functionName: 'transfer', + args: [accounts[1].address, 1n], + account: address.vitalik, + }) + await mine(testClient, { blocks: 1 }) + + const logs = await getFilterChanges(publicClient, { filter }) + assertType(logs) + expect(logs.length).toBe(2) + }) +}) diff --git a/src/actions/public/getFilterChanges.ts b/src/actions/public/getFilterChanges.ts index c7d140016d..9bfa713838 100644 --- a/src/actions/public/getFilterChanges.ts +++ b/src/actions/public/getFilterChanges.ts @@ -47,16 +47,28 @@ export async function getFilterChanges< method: 'eth_getFilterChanges', params: [filter.id], }) - return logs.map((log) => { - if (typeof log === 'string') return log - const { eventName, args } = - 'abi' in filter && filter.abi - ? decodeEventLog({ - abi: filter.abi, - data: log.data, - topics: log.topics as any, - }) - : { eventName: undefined, args: undefined } - return formatLog(log, { args, eventName }) - }) as GetFilterChangesReturnType + return logs + .map((log) => { + if (typeof log === 'string') return log + try { + const { eventName, args } = + 'abi' in filter && filter.abi + ? decodeEventLog({ + abi: filter.abi, + data: log.data, + topics: log.topics as any, + }) + : { eventName: undefined, args: undefined } + return formatLog(log, { args, eventName }) + } catch { + // Skip log if there is an error decoding (e.g. indexed/non-indexed params mismatch). + return + } + }) + .filter(Boolean) as GetFilterChangesReturnType< + TFilterType, + TAbiEvent, + TAbi, + TEventName + > } diff --git a/src/actions/public/getFilterLogs.test.ts b/src/actions/public/getFilterLogs.test.ts index c38508a0ef..537d1a6684 100644 --- a/src/actions/public/getFilterLogs.test.ts +++ b/src/actions/public/getFilterLogs.test.ts @@ -3,12 +3,14 @@ import { afterAll, assertType, beforeAll, describe, expect, test } from 'vitest' import { accounts, address, + deployErc20InvalidTransferEvent, initialBlockNumber, publicClient, testClient, usdcContractConfig, walletClient, } from '../../_test' +import { erc20InvalidTransferEventABI } from '../../_test/generated' import { impersonateAccount, @@ -45,6 +47,27 @@ const event = { name: 'Transfer', type: 'event', }, + invalid: { + inputs: [ + { + indexed: true, + name: 'from', + type: 'address', + }, + { + indexed: false, + name: 'to', + type: 'address', + }, + { + indexed: false, + name: 'value', + type: 'uint256', + }, + ], + name: 'Transfer', + type: 'event', + }, unnamed: { inputs: [ { @@ -746,3 +769,73 @@ describe('raw events', () => { ]) }) }) + +describe('skip invalid logs', () => { + test('indexed params mismatch', async () => { + const { contractAddress } = await deployErc20InvalidTransferEvent() + + const filter = await createEventFilter(publicClient, { + event: event.default, + }) + + await writeContract(walletClient, { + ...usdcContractConfig, + functionName: 'transfer', + args: [accounts[0].address, 1n], + account: address.vitalik, + }) + await writeContract(walletClient, { + abi: erc20InvalidTransferEventABI, + address: contractAddress!, + functionName: 'transfer', + args: [accounts[0].address, 1n], + account: address.vitalik, + }) + await writeContract(walletClient, { + abi: erc20InvalidTransferEventABI, + address: contractAddress!, + functionName: 'transfer', + args: [accounts[1].address, 1n], + account: address.vitalik, + }) + await mine(testClient, { blocks: 1 }) + + const logs = await getFilterLogs(publicClient, { filter }) + assertType(logs) + expect(logs.length).toBe(1) + }) + + test('non-indexed params mismatch', async () => { + const { contractAddress } = await deployErc20InvalidTransferEvent() + + const filter = await createEventFilter(publicClient, { + event: event.invalid, + }) + + await writeContract(walletClient, { + ...usdcContractConfig, + functionName: 'transfer', + args: [accounts[0].address, 1n], + account: address.vitalik, + }) + await writeContract(walletClient, { + abi: erc20InvalidTransferEventABI, + address: contractAddress!, + functionName: 'transfer', + args: [accounts[0].address, 1n], + account: address.vitalik, + }) + await writeContract(walletClient, { + abi: erc20InvalidTransferEventABI, + address: contractAddress!, + functionName: 'transfer', + args: [accounts[1].address, 1n], + account: address.vitalik, + }) + await mine(testClient, { blocks: 1 }) + + const logs = await getFilterLogs(publicClient, { filter }) + assertType(logs) + expect(logs.length).toBe(2) + }) +}) diff --git a/src/actions/public/getFilterLogs.ts b/src/actions/public/getFilterLogs.ts index 7b2f58d812..0ddf3e9e87 100644 --- a/src/actions/public/getFilterLogs.ts +++ b/src/actions/public/getFilterLogs.ts @@ -31,15 +31,26 @@ export async function getFilterLogs< method: 'eth_getFilterLogs', params: [filter.id], }) - return logs.map((log) => { - const { eventName, args } = - 'abi' in filter && filter.abi - ? decodeEventLog({ - abi: filter.abi, - data: log.data, - topics: log.topics as any, - }) - : { eventName: undefined, args: undefined } - return formatLog(log, { args, eventName }) - }) as unknown as GetFilterLogsReturnType + return logs + .map((log) => { + try { + const { eventName, args } = + 'abi' in filter && filter.abi + ? decodeEventLog({ + abi: filter.abi, + data: log.data, + topics: log.topics as any, + }) + : { eventName: undefined, args: undefined } + return formatLog(log, { args, eventName }) + } catch { + // Skip log if there is an error decoding (e.g. indexed/non-indexed params mismatch). + return + } + }) + .filter(Boolean) as unknown as GetFilterLogsReturnType< + TAbiEvent, + TAbi, + TEventName + > } diff --git a/src/actions/public/getLogs.test.ts b/src/actions/public/getLogs.test.ts index 165943f28b..9a994ef734 100644 --- a/src/actions/public/getLogs.test.ts +++ b/src/actions/public/getLogs.test.ts @@ -3,12 +3,14 @@ import { afterAll, assertType, beforeAll, describe, expect, test } from 'vitest' import { accounts, address, + deployErc20InvalidTransferEvent, initialBlockNumber, publicClient, testClient, usdcContractConfig, walletClient, } from '../../_test' +import { erc20InvalidTransferEventABI } from '../../_test/generated' import { impersonateAccount, mine, @@ -43,6 +45,27 @@ const event = { name: 'Transfer', type: 'event', }, + invalid: { + inputs: [ + { + indexed: true, + name: 'from', + type: 'address', + }, + { + indexed: false, + name: 'to', + type: 'address', + }, + { + indexed: false, + name: 'value', + type: 'uint256', + }, + ], + name: 'Transfer', + type: 'event', + }, unnamed: { inputs: [ { @@ -430,3 +453,65 @@ describe('events', () => { ]) }) }) + +describe('skip invalid logs', () => { + test('indexed params mismatch', async () => { + const { contractAddress } = await deployErc20InvalidTransferEvent() + + await writeContract(walletClient, { + ...usdcContractConfig, + functionName: 'transfer', + args: [accounts[0].address, 1n], + account: address.vitalik, + }) + await writeContract(walletClient, { + abi: erc20InvalidTransferEventABI, + address: contractAddress!, + functionName: 'transfer', + args: [accounts[0].address, 1n], + account: address.vitalik, + }) + await writeContract(walletClient, { + abi: erc20InvalidTransferEventABI, + address: contractAddress!, + functionName: 'transfer', + args: [accounts[1].address, 1n], + account: address.vitalik, + }) + await mine(testClient, { blocks: 1 }) + + const logs = await getLogs(publicClient, { event: event.default }) + assertType(logs) + expect(logs.length).toBe(1) + }) + + test('non-indexed params mismatch', async () => { + const { contractAddress } = await deployErc20InvalidTransferEvent() + + await writeContract(walletClient, { + ...usdcContractConfig, + functionName: 'transfer', + args: [accounts[0].address, 1n], + account: address.vitalik, + }) + await writeContract(walletClient, { + abi: erc20InvalidTransferEventABI, + address: contractAddress!, + functionName: 'transfer', + args: [accounts[0].address, 1n], + account: address.vitalik, + }) + await writeContract(walletClient, { + abi: erc20InvalidTransferEventABI, + address: contractAddress!, + functionName: 'transfer', + args: [accounts[1].address, 1n], + account: address.vitalik, + }) + await mine(testClient, { blocks: 1 }) + + const logs = await getLogs(publicClient, { event: event.invalid }) + assertType(logs) + expect(logs.length).toBe(2) + }) +}) diff --git a/src/actions/public/getLogs.ts b/src/actions/public/getLogs.ts index f95a798e5b..fcc9949274 100644 --- a/src/actions/public/getLogs.ts +++ b/src/actions/public/getLogs.ts @@ -98,14 +98,22 @@ export async function getLogs< ], }) } - return logs.map((log) => { - const { eventName, args } = event - ? decodeEventLog({ - abi: [event], - data: log.data, - topics: log.topics as any, - }) - : { eventName: undefined, args: undefined } - return formatLog(log, { args, eventName }) - }) as unknown as GetLogsReturnType + + return logs + .map((log) => { + try { + const { eventName, args } = event + ? decodeEventLog({ + abi: [event], + data: log.data, + topics: log.topics as any, + }) + : { eventName: undefined, args: undefined } + return formatLog(log, { args, eventName }) + } catch { + // Skip log if there is an error decoding (e.g. indexed/non-indexed params mismatch). + return + } + }) + .filter(Boolean) as unknown as GetLogsReturnType } diff --git a/src/actions/public/multicall.test.ts b/src/actions/public/multicall.test.ts index 1081aa03ed..833f8e4a83 100644 --- a/src/actions/public/multicall.test.ts +++ b/src/actions/public/multicall.test.ts @@ -46,7 +46,7 @@ test('default', async () => { "status": "success", }, { - "result": 231481998553n, + "result": 231481998547n, "status": "success", }, { @@ -81,7 +81,7 @@ test('args: allowFailure', async () => { ).toMatchInlineSnapshot(` [ 41119586940119550n, - 231481998553n, + 231481998547n, 10000n, ] `) @@ -115,7 +115,7 @@ test('args: multicallAddress', async () => { "status": "success", }, { - "result": 231481998553n, + "result": 231481998547n, "status": "success", }, { @@ -168,7 +168,7 @@ describe('errors', async () => { "status": "failure", }, { - "result": 231481998553n, + "result": 231481998547n, "status": "success", }, { @@ -222,7 +222,7 @@ describe('errors', async () => { "status": "failure", }, { - "result": 231481998553n, + "result": 231481998547n, "status": "success", }, { @@ -276,7 +276,7 @@ describe('errors', async () => { "status": "failure", }, { - "result": 231481998553n, + "result": 231481998547n, "status": "success", }, { @@ -321,11 +321,11 @@ describe('errors', async () => { ).toMatchInlineSnapshot(` [ { - "result": 231481998553n, + "result": 231481998547n, "status": "success", }, { - "result": 231481998553n, + "result": 231481998547n, "status": "success", }, { diff --git a/src/errors/abi.test.ts b/src/errors/abi.test.ts index 93194afe39..963ee6c020 100644 --- a/src/errors/abi.test.ts +++ b/src/errors/abi.test.ts @@ -1,9 +1,11 @@ import { describe, expect, test } from 'vitest' import { AbiDecodingDataSizeInvalidError, + AbiDecodingDataSizeTooSmallError, AbiEncodingArrayLengthMismatchError, AbiEncodingLengthMismatchError, AbiEventSignatureEmptyTopicsError, + DecodeLogDataMismatch, DecodeLogTopicsMismatch, InvalidAbiDecodingTypeError, InvalidAbiEncodingTypeError, @@ -11,10 +13,34 @@ import { } from './abi' test('AbiDecodingDataSizeInvalidError', () => { - expect(new AbiDecodingDataSizeInvalidError(69)).toMatchInlineSnapshot(` - [AbiDecodingDataSizeInvalidError: Data size of 69 bytes is invalid. + expect( + new AbiDecodingDataSizeInvalidError({ data: '0x1234', size: 2 }), + ).toMatchInlineSnapshot(` + [AbiDecodingDataSizeInvalidError: Data size of 2 bytes is invalid. Size must be in increments of 32 bytes (size % 32 === 0). + Data: 0x1234 (2 bytes) + + Version: viem@1.0.2] + `) +}) + +test('AbiDecodingDataSizeTooSmallError', () => { + expect( + new AbiDecodingDataSizeTooSmallError({ + data: '0x1234', + params: [ + { name: 'a', type: 'uint256' }, + { name: 'b', type: 'uint256' }, + ], + size: 2, + }), + ).toMatchInlineSnapshot(` + [AbiDecodingDataSizeTooSmallError: Data size of 2 bytes is too small for given parameters. + + Params: (uint256 a, uint256 b) + Data: 0x1234 (2 bytes) + Version: viem@1.0.2] `) }) @@ -73,6 +99,28 @@ test('AbiEventSignatureEmptyTopicsError', () => { `) }) +test('DecodeLogDataMismatch', () => { + expect( + new DecodeLogDataMismatch({ + data: '0x1234', + params: [ + { name: 'a', type: 'uint256' }, + { name: 'b', type: 'uint256' }, + ], + size: 2, + }), + ).toMatchInlineSnapshot(` + [DecodeLogDataMismatch: Data size of 2 bytes is too small for non-indexed event parameters. + + This error is usually caused if the ABI event has too many non-indexed event parameters for the emitted log. + + Params: (uint256 a, uint256 b) + Data: 0x1234 (2 bytes) + + Version: viem@1.0.2] + `) +}) + describe('DecodeLogTopicsMismatch', () => { test('default', () => { expect( diff --git a/src/errors/abi.ts b/src/errors/abi.ts index 593b26c6b8..b7b7561066 100644 --- a/src/errors/abi.ts +++ b/src/errors/abi.ts @@ -1,6 +1,6 @@ import type { AbiParameter } from 'abitype' import type { AbiItem, Hex } from '../types' -import { formatAbiItem, size } from '../utils' +import { formatAbiItem, formatAbiParams, size } from '../utils' import { BaseError } from './base' export class AbiConstructorNotFoundError extends BaseError { @@ -35,16 +35,47 @@ export class AbiConstructorParamsNotFoundError extends BaseError { export class AbiDecodingDataSizeInvalidError extends BaseError { name = 'AbiDecodingDataSizeInvalidError' - constructor(size: number) { + constructor({ data, size }: { data: Hex; size: number }) { super( [ `Data size of ${size} bytes is invalid.`, 'Size must be in increments of 32 bytes (size % 32 === 0).', ].join('\n'), + { metaMessages: [`Data: ${data} (${size} bytes)`] }, ) } } +export class AbiDecodingDataSizeTooSmallError extends BaseError { + name = 'AbiDecodingDataSizeTooSmallError' + + data: Hex + params: readonly AbiParameter[] + size: number + + constructor({ + data, + params, + size, + }: { data: Hex; params: readonly AbiParameter[]; size: number }) { + super( + [`Data size of ${size} bytes is too small for given parameters.`].join( + '\n', + ), + { + metaMessages: [ + `Params: (${formatAbiParams(params, { includeName: true })})`, + `Data: ${data} (${size} bytes)`, + ], + }, + ) + + this.data = data + this.params = params + this.size = size + } +} + export class AbiDecodingZeroDataError extends BaseError { name = 'AbiDecodingZeroDataError' constructor() { @@ -240,6 +271,38 @@ export class BytesSizeMismatchError extends BaseError { } } +export class DecodeLogDataMismatch extends BaseError { + name = 'DecodeLogDataMismatch' + + data: Hex + params: readonly AbiParameter[] + size: number + + constructor({ + data, + params, + size, + }: { data: Hex; params: readonly AbiParameter[]; size: number }) { + super( + [ + `Data size of ${size} bytes is too small for non-indexed event parameters.`, + ].join('\n'), + { + metaMessages: [ + 'This error is usually caused if the ABI event has too many non-indexed event parameters for the emitted log.', + '', + `Params: (${formatAbiParams(params, { includeName: true })})`, + `Data: ${data} (${size} bytes)`, + ], + }, + ) + + this.data = data + this.params = params + this.size = size + } +} + export class DecodeLogTopicsMismatch extends BaseError { name = 'DecodeLogTopicsMismatch' constructor({ diff --git a/src/errors/index.ts b/src/errors/index.ts index 77a346e94a..5a14ac6375 100644 --- a/src/errors/index.ts +++ b/src/errors/index.ts @@ -2,6 +2,7 @@ export { AbiConstructorNotFoundError, AbiConstructorParamsNotFoundError, AbiDecodingDataSizeInvalidError, + AbiDecodingDataSizeTooSmallError, AbiDecodingZeroDataError, AbiEncodingArrayLengthMismatchError, AbiEncodingBytesSizeMismatchError, diff --git a/src/utils/abi/decodeAbiParameters.test.ts b/src/utils/abi/decodeAbiParameters.test.ts index 8c7206f9c0..836e14a8e1 100644 --- a/src/utils/abi/decodeAbiParameters.test.ts +++ b/src/utils/abi/decodeAbiParameters.test.ts @@ -1964,6 +1964,24 @@ test('invalid size', () => { "Data size of 35 bytes is invalid. Size must be in increments of 32 bytes (size % 32 === 0). + Data: 0x0000000000000000000000000000000000000000000000000000000000010fabababab (35 bytes) + + Version: viem@1.0.2" + `) +}) + +test('data size too small', () => { + expect(() => + decodeAbiParameters( + [{ type: 'uint256' }, { type: 'uint256' }], + '0x0000000000000000000000000000000000000000000000000000000000010f2c', + ), + ).toThrowErrorMatchingInlineSnapshot(` + "Data size of 32 bytes is too small for given parameters. + + Params: (uint256, uint256) + Data: 0x0000000000000000000000000000000000000000000000000000000000010f2c (32 bytes) + Version: viem@1.0.2" `) }) diff --git a/src/utils/abi/decodeAbiParameters.ts b/src/utils/abi/decodeAbiParameters.ts index b88d66ec93..e185081340 100644 --- a/src/utils/abi/decodeAbiParameters.ts +++ b/src/utils/abi/decodeAbiParameters.ts @@ -7,6 +7,7 @@ import type { import { AbiDecodingDataSizeInvalidError, + AbiDecodingDataSizeTooSmallError, AbiDecodingZeroDataError, InvalidAbiDecodingTypeError, } from '../../errors' @@ -29,7 +30,7 @@ export function decodeAbiParameters< >(params: Narrow, data: Hex): DecodeAbiParametersReturnType { if (data === '0x' && params.length > 0) throw new AbiDecodingZeroDataError() if (size(data) % 32 !== 0) - throw new AbiDecodingDataSizeInvalidError(size(data)) + throw new AbiDecodingDataSizeInvalidError({ data, size: size(data) }) return decodeParams({ data, params: params as readonly AbiParameter[], @@ -48,6 +49,13 @@ function decodeParams({ let position = 0 for (let i = 0; i < params.length; i++) { + if (position >= size(data)) + throw new AbiDecodingDataSizeTooSmallError({ + data, + params, + size: size(data), + }) + const param = params[i] const { consumed, value } = decodeParam({ data, param, position }) decodedValues.push(value) diff --git a/src/utils/abi/decodeEventLog.test.ts b/src/utils/abi/decodeEventLog.test.ts index 2ea34452e9..6e9b736d1b 100644 --- a/src/utils/abi/decodeEventLog.test.ts +++ b/src/utils/abi/decodeEventLog.test.ts @@ -490,6 +490,57 @@ describe('GitHub repros', () => { ) }) }) + + describe('https://github.com/wagmi-dev/viem/issues/323', () => { + test('data + params mismatch', () => { + expect(() => + decodeEventLog({ + abi: [ + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'from', + type: 'address', + }, + { + indexed: false, + internalType: 'address', + name: 'to', + type: 'address', + }, + { + indexed: false, + internalType: 'uint256', + name: 'id', + type: 'uint256', + }, + ], + name: 'Transfer', + type: 'event', + }, + ], + data: '0x0000000000000000000000000000000000000000000000000000000023c34600', + topics: [ + '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', + '0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266', + '0x00000000000000000000000070e8a65d014918798ba424110d5df658cde1cc58', + ], + }), + ).toThrowErrorMatchingInlineSnapshot(` + "Data size of 32 bytes is too small for non-indexed event parameters. + + This error is usually caused if the ABI event has too many non-indexed event parameters for the emitted log. + + Params: (address to, uint256 id) + Data: 0x0000000000000000000000000000000000000000000000000000000023c34600 (32 bytes) + + Version: viem@1.0.2" + `) + }) + }) }) test("errors: event doesn't exist", () => { @@ -548,3 +599,47 @@ test('errors: no topics', () => { Version: viem@1.0.2" `) }) + +test("errors: invalid data size", () => { + expect(() => + decodeEventLog({ + abi: [ + { + inputs: [ + { + indexed: true, + name: 'from', + type: 'address', + }, + { + indexed: true, + name: 'to', + type: 'address', + }, + { + indexed: false, + name: 'tokenId', + type: 'uint256', + }, + ], + name: 'Transfer', + type: 'event', + }, + ], + data: '0x1', + eventName: 'Transfer', + topics: [ + '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', + '0x000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045', + '0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266', + ], + }), + ).toThrowErrorMatchingInlineSnapshot(` + "Data size of 1 bytes is invalid. + Size must be in increments of 32 bytes (size % 32 === 0). + + Data: 0x1 (1 bytes) + + Version: viem@1.0.2" + `) +}) diff --git a/src/utils/abi/decodeEventLog.ts b/src/utils/abi/decodeEventLog.ts index 9a7a28ad47..aa2ee33283 100644 --- a/src/utils/abi/decodeEventLog.ts +++ b/src/utils/abi/decodeEventLog.ts @@ -1,5 +1,6 @@ import type { Abi, AbiParameter, Narrow } from 'abitype' import { + AbiDecodingDataSizeTooSmallError, AbiEventSignatureEmptyTopicsError, AbiEventSignatureNotFoundError, DecodeLogTopicsMismatch, @@ -13,6 +14,7 @@ import type { import { getEventSelector } from '../hash' import { decodeAbiParameters } from './decodeAbiParameters' import { formatAbiItem } from './formatAbiItem' +import { DecodeLogDataMismatch } from '../../errors/abi' export type DecodeEventLogParameters< TAbi extends Abi | readonly unknown[] = Abi, @@ -90,14 +92,24 @@ export function decodeEventLog< // Decode data (non-indexed args). if (data && data !== '0x') { const params = inputs.filter((x) => !('indexed' in x && x.indexed)) - const decodedData = decodeAbiParameters(params, data) - if (decodedData) { - if (isUnnamed) args = [...args, ...decodedData] - else { - for (let i = 0; i < params.length; i++) { - args[params[i].name!] = decodedData[i] + try { + const decodedData = decodeAbiParameters(params, data) + if (decodedData) { + if (isUnnamed) args = [...args, ...decodedData] + else { + for (let i = 0; i < params.length; i++) { + args[params[i].name!] = decodedData[i] + } } } + } catch (err) { + if (err instanceof AbiDecodingDataSizeTooSmallError) + throw new DecodeLogDataMismatch({ + data: err.data, + params: err.params, + size: err.size, + }) + throw err } } diff --git a/src/utils/abi/formatAbiItem.test.ts b/src/utils/abi/formatAbiItem.test.ts index 704ed67721..56b53f6a12 100644 --- a/src/utils/abi/formatAbiItem.test.ts +++ b/src/utils/abi/formatAbiItem.test.ts @@ -1,46 +1,32 @@ -import { expect, test } from 'vitest' +import { describe, expect, test } from 'vitest' -import { formatAbiItem } from './formatAbiItem' +import { formatAbiItem, formatAbiParams } from './formatAbiItem' -test('foo()', () => { - expect( - // @ts-expect-error - formatAbiItem({ - name: 'foo', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }), - ).toEqual('foo()') - expect( - formatAbiItem({ - inputs: [], - name: 'foo', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }), - ).toEqual('foo()') -}) - -test('foo(uint256)', () => { - expect( - formatAbiItem({ - inputs: [ - { - name: 'a', - type: 'uint256', - }, - ], - name: 'foo', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }), - ).toEqual('foo(uint256)') - expect( - formatAbiItem( - { +describe('formatAbiItem', () => { + test('foo()', () => { + expect( + // @ts-expect-error + formatAbiItem({ + name: 'foo', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }), + ).toEqual('foo()') + expect( + formatAbiItem({ + inputs: [], + name: 'foo', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }), + ).toEqual('foo()') + }) + + test('foo(uint256)', () => { + expect( + formatAbiItem({ inputs: [ { name: 'a', @@ -51,56 +37,30 @@ test('foo(uint256)', () => { outputs: [], stateMutability: 'nonpayable', type: 'function', - }, - { includeName: true }, - ), - ).toEqual('foo(uint256 a)') -}) - -test('getVoter((uint256,bool,address,uint256),string[],bytes)', () => { - expect( - formatAbiItem({ - inputs: [ + }), + ).toEqual('foo(uint256)') + expect( + formatAbiItem( { - components: [ + inputs: [ { - name: 'weight', - type: 'uint256', - }, - { - name: 'voted', - type: 'bool', - }, - { - name: 'delegate', - type: 'address', - }, - { - name: 'vote', + name: 'a', type: 'uint256', }, ], - name: 'voter', - type: 'tuple', - }, - { name: 'foo', - type: 'string[]', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', }, - { - name: 'bar', - type: 'bytes', - }, - ], - name: 'getVoter', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }), - ).toEqual('getVoter((uint256,bool,address,uint256),string[],bytes)') - expect( - formatAbiItem( - { + { includeName: true }, + ), + ).toEqual('foo(uint256 a)') + }) + + test('getVoter((uint256,bool,address,uint256),string[],bytes)', () => { + expect( + formatAbiItem({ inputs: [ { components: [ @@ -137,58 +97,58 @@ test('getVoter((uint256,bool,address,uint256),string[],bytes)', () => { outputs: [], stateMutability: 'nonpayable', type: 'function', - }, - { includeName: true }, - ), - ).toEqual( - 'getVoter((uint256 weight, bool voted, address delegate, uint256 vote), string[] foo, bytes bar)', - ) -}) - -test('VoterEvent((uint256,bool,address,uint256),string[],bytes)', () => { - expect( - formatAbiItem({ - inputs: [ + }), + ).toEqual('getVoter((uint256,bool,address,uint256),string[],bytes)') + expect( + formatAbiItem( { - components: [ - { - name: 'weight', - type: 'uint256', - }, + inputs: [ { - name: 'voted', - type: 'bool', + components: [ + { + name: 'weight', + type: 'uint256', + }, + { + name: 'voted', + type: 'bool', + }, + { + name: 'delegate', + type: 'address', + }, + { + name: 'vote', + type: 'uint256', + }, + ], + name: 'voter', + type: 'tuple', }, { - name: 'delegate', - type: 'address', + name: 'foo', + type: 'string[]', }, { - name: 'vote', - type: 'uint256', + name: 'bar', + type: 'bytes', }, ], - name: 'voter', - type: 'tuple', - }, - { - name: 'foo', - type: 'string[]', + name: 'getVoter', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', }, - { - name: 'bar', - type: 'bytes', - }, - ], - name: 'VoterEvent', - outputs: [], - stateMutability: 'nonpayable', - type: 'event', - }), - ).toEqual('VoterEvent((uint256,bool,address,uint256),string[],bytes)') - expect( - formatAbiItem( - { + { includeName: true }, + ), + ).toEqual( + 'getVoter((uint256 weight, bool voted, address delegate, uint256 vote), string[] foo, bytes bar)', + ) + }) + + test('VoterEvent((uint256,bool,address,uint256),string[],bytes)', () => { + expect( + formatAbiItem({ inputs: [ { components: [ @@ -225,73 +185,189 @@ test('VoterEvent((uint256,bool,address,uint256),string[],bytes)', () => { outputs: [], stateMutability: 'nonpayable', type: 'event', - }, - { includeName: true }, - ), - ).toEqual( - 'VoterEvent((uint256 weight, bool voted, address delegate, uint256 vote), string[] foo, bytes bar)', - ) -}) - -test('VoterError((uint256,bool,address,uint256),string[],bytes)', () => { - expect( - formatAbiItem({ - inputs: [ + }), + ).toEqual('VoterEvent((uint256,bool,address,uint256),string[],bytes)') + expect( + formatAbiItem( { - components: [ - { - name: 'weight', - type: 'uint256', - }, + inputs: [ { - name: 'voted', - type: 'bool', + components: [ + { + name: 'weight', + type: 'uint256', + }, + { + name: 'voted', + type: 'bool', + }, + { + name: 'delegate', + type: 'address', + }, + { + name: 'vote', + type: 'uint256', + }, + ], + name: 'voter', + type: 'tuple', }, { - name: 'delegate', - type: 'address', + name: 'foo', + type: 'string[]', }, { - name: 'vote', - type: 'uint256', + name: 'bar', + type: 'bytes', }, ], - name: 'voter', - type: 'tuple', + name: 'VoterEvent', + outputs: [], + stateMutability: 'nonpayable', + type: 'event', }, - { - name: 'foo', - type: 'string[]', - }, - { - name: 'bar', - type: 'bytes', - }, - ], - name: 'VoterError', - outputs: [], - stateMutability: 'nonpayable', - type: 'error', - }), - ).toEqual('VoterError((uint256,bool,address,uint256),string[],bytes)') + { includeName: true }, + ), + ).toEqual( + 'VoterEvent((uint256 weight, bool voted, address delegate, uint256 vote), string[] foo, bytes bar)', + ) + }) + + test('VoterError((uint256,bool,address,uint256),string[],bytes)', () => { + expect( + formatAbiItem({ + inputs: [ + { + components: [ + { + name: 'weight', + type: 'uint256', + }, + { + name: 'voted', + type: 'bool', + }, + { + name: 'delegate', + type: 'address', + }, + { + name: 'vote', + type: 'uint256', + }, + ], + name: 'voter', + type: 'tuple', + }, + { + name: 'foo', + type: 'string[]', + }, + { + name: 'bar', + type: 'bytes', + }, + ], + name: 'VoterError', + outputs: [], + stateMutability: 'nonpayable', + type: 'error', + }), + ).toEqual('VoterError((uint256,bool,address,uint256),string[],bytes)') + }) + + test('error: invalid type', () => { + expect(() => + formatAbiItem({ + inputs: [ + { + name: 'proposalNames', + type: 'bytes32[]', + }, + ], + stateMutability: 'nonpayable', + type: 'constructor', + }), + ).toThrowErrorMatchingInlineSnapshot(` + "\\"constructor\\" is not a valid definition type. + Valid types: \\"function\\", \\"event\\", \\"error\\" + + Version: viem@1.0.2" + `) + }) }) -test('error: invalid type', () => { - expect(() => - formatAbiItem({ - inputs: [ - { - name: 'proposalNames', - type: 'bytes32[]', - }, - ], - stateMutability: 'nonpayable', - type: 'constructor', - }), - ).toThrowErrorMatchingInlineSnapshot(` - "\\"constructor\\" is not a valid definition type. - Valid types: \\"function\\", \\"event\\", \\"error\\" +describe('formatAbiParams', () => { + test('default', () => { + expect(formatAbiParams([{ name: 'a', type: 'uint256' }])).toMatchInlineSnapshot('"uint256"') + expect(formatAbiParams([ + { + components: [ + { + name: 'weight', + type: 'uint256', + }, + { + name: 'voted', + type: 'bool', + }, + { + name: 'delegate', + type: 'address', + }, + { + name: 'vote', + type: 'uint256', + }, + ], + name: 'voter', + type: 'tuple', + }, + { + name: 'foo', + type: 'string[]', + }, + { + name: 'bar', + type: 'bytes', + }, + ])).toMatchInlineSnapshot('"(uint256,bool,address,uint256),string[],bytes"') + }) - Version: viem@1.0.2" - `) -}) + test('includeName', () => { + expect(formatAbiParams([{ name: 'a', type: 'uint256' }], { includeName: true })).toMatchInlineSnapshot('"uint256 a"') + expect(formatAbiParams([ + { + components: [ + { + name: 'weight', + type: 'uint256', + }, + { + name: 'voted', + type: 'bool', + }, + { + name: 'delegate', + type: 'address', + }, + { + name: 'vote', + type: 'uint256', + }, + ], + name: 'voter', + type: 'tuple', + }, + { + name: 'foo', + type: 'string[]', + }, + { + name: 'bar', + type: 'bytes', + }, + ], { includeName: true })).toMatchInlineSnapshot('"(uint256 weight, bool voted, address delegate, uint256 vote), string[] foo, bytes bar"') + }) +}) \ No newline at end of file diff --git a/src/utils/abi/formatAbiItem.ts b/src/utils/abi/formatAbiItem.ts index d0891d438f..cc3941b9f2 100644 --- a/src/utils/abi/formatAbiItem.ts +++ b/src/utils/abi/formatAbiItem.ts @@ -14,25 +14,25 @@ export function formatAbiItem( ) throw new InvalidDefinitionTypeError(abiItem.type) - return `${abiItem.name}(${getParams(abiItem.inputs, { includeName })})` + return `${abiItem.name}(${formatAbiParams(abiItem.inputs, { includeName })})` } -function getParams( +export function formatAbiParams( params: readonly AbiParameter[] | undefined, - { includeName }: { includeName: boolean }, + { includeName = false }: { includeName?: boolean } = {}, ): string { if (!params) return '' return params - .map((param) => getParam(param, { includeName })) + .map((param) => formatAbiParam(param, { includeName })) .join(includeName ? ', ' : ',') } -function getParam( +function formatAbiParam( param: AbiParameter, { includeName }: { includeName: boolean }, ): string { if (param.type.startsWith('tuple')) { - return `(${getParams( + return `(${formatAbiParams( (param as unknown as { components: AbiParameter[] }).components, { includeName }, )})${param.type.slice('tuple'.length)}` diff --git a/src/utils/abi/index.test.ts b/src/utils/abi/index.test.ts index 29a97ddf9b..9d3d90f2c5 100644 --- a/src/utils/abi/index.test.ts +++ b/src/utils/abi/index.test.ts @@ -19,6 +19,7 @@ test('exports utils', () => { "encodePacked": [Function], "formatAbiItem": [Function], "formatAbiItemWithArgs": [Function], + "formatAbiParams": [Function], "getAbiItem": [Function], "parseAbi": [Function], "parseAbiItem": [Function], diff --git a/src/utils/abi/index.ts b/src/utils/abi/index.ts index 2dbfa0a54c..158054d5bd 100644 --- a/src/utils/abi/index.ts +++ b/src/utils/abi/index.ts @@ -57,7 +57,7 @@ export { encodePacked } from './encodePacked' export { formatAbiItemWithArgs } from './formatAbiItemWithArgs' -export { formatAbiItem } from './formatAbiItem' +export { formatAbiItem, formatAbiParams } from './formatAbiItem' export type { GetAbiItemParameters } from './getAbiItem' export { getAbiItem } from './getAbiItem' diff --git a/src/utils/index.test.ts b/src/utils/index.test.ts index 2c55eb7044..338a91edfd 100644 --- a/src/utils/index.test.ts +++ b/src/utils/index.test.ts @@ -49,6 +49,7 @@ test('exports utils', () => { "format": [Function], "formatAbiItem": [Function], "formatAbiItemWithArgs": [Function], + "formatAbiParams": [Function], "formatBlock": [Function], "formatEther": [Function], "formatGwei": [Function], diff --git a/src/utils/index.ts b/src/utils/index.ts index 48cf80445f..fb1144548e 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -34,6 +34,7 @@ export { encodePacked, formatAbiItemWithArgs, formatAbiItem, + formatAbiParams, getAbiItem, parseAbi, parseAbiItem,