Skip to content

fix: AbiEvent.decode mis-indexes args when an indexed input isn't a declaration-order prefix - #436

Merged
jxom merged 2 commits into
wevm:mainfrom
gomesalexandre:fix_abievent_decode_indexed_order
Sep 1, 2026
Merged

fix: AbiEvent.decode mis-indexes args when an indexed input isn't a declaration-order prefix#436
jxom merged 2 commits into
wevm:mainfrom
gomesalexandre:fix_abievent_decode_indexed_order

Conversation

@gomesalexandre

Copy link
Copy Markdown
Contributor

AbiEvent.decode mis-indexes args whenever a non-indexed input appears before an indexed one in an event's declaration order — the indexed-topic loop keyed writes off the loop index within the indexed subset, not the input's original declaration index.

The bug

src/core/AbiEvent.ts. The partition loop already tracked original declaration index for the non-indexed side (nonIndexedOriginalIndex), for exactly the reason this bug exists — but had no equivalent for the indexed side:

for (let i = 0; i < indexedInputs.length; i++) {
  const param = indexedInputs[i]!
  args[isUnnamed ? i : param.name || i] = (...)   // `i` is the indexed-SUBSET index

For a canonical event where indexed inputs are a declaration-order prefix (ERC-20 Transfer, Approval, etc.) the indexed-subset index and the declaration index are identical, so this was invisible. It breaks the moment they diverge.

Repro (real, run against the published behavior before this fix)

event Foo(uint256, bool indexed)
  ox    : [true, 273n]        <- wrong order
  viem  : [273n, true]
  ethers: 273n true

event Foo(uint256 indexed a, bool, address indexed)
  ox    : { "1": true, "a": 2730n }                                    <- indexed address is GONE
  viem  : [2730n, true, "0x00CCCCCCccCcCccCCcccCCcccCcCcCccCcCcCCcc"]
  ethers: 2730n true 0x00CCCC...   a=2730n

event Transfer(address indexed from, address indexed to, uint256 value)  (canonical, sanity control)
  ox    : { from: "0x...", to: "0x...", value: 1000n }                 <- correct, unaffected

The second case is the dangerous one: three inputs decoded to two values, silently. Mechanism: indexedInputs = [a (decl. 0), address (decl. 2)]; the loop's i=1 write for address collides with the non-indexed loop's write for bool (decl. index 1) — the address is overwritten with no error raised.

Fix

Added indexedOriginalIndex, mirroring nonIndexedOriginalIndex, and keyed both the indexed-topic writes and the non-indexed array-mode fill by declaration index — matching what the non-indexed named-mode branch already did, and what viem's decodeEventLog (utils/abi/decodeEventLog.js) does.

Tests

Two new cases in src/core/_test/AbiEvent.test.ts reproducing both failure modes above (array-mode reordering, mixed-mode silent drop). Verified genuine red-before/green-after by stashing just the source fix and rerunning: both new tests fail on unfixed code — the silent-drop case shows the "2" key (the address) entirely absent from the received snapshot — while all 67 pre-existing tests (including the canonical-prefix cases and the anonymous-event case) still pass unaffected.

# unfixed:  2 failed | 67 passed (69)
# fixed:    69 passed (69)

Also ran the two direct consumer test files (AbiItem.test.ts, Log.test.ts): 95/95 passing. pnpm check:types (tsc -b) and pnpm check (vp check --fix) both clean — 0 errors (2 pre-existing unrelated warnings in the docs site, untouched by this diff).

Scope

Real-world exposure is unquantified — canonical ERC-20/721 events decode correctly today and are unaffected by this bug or this fix. It only fires for non-canonical/hand-written ABIs where a non-indexed param isn't after all the indexed ones. Framing this as a correctness bug with unmeasured frequency, not "everyone's Transfer decoding is broken" (it isn't).

Related, out of scope

While checking this file for the same bug pattern elsewhere, I found AbiEvent.encode's object-mode indexed-arg lookup (indexedInputs.map((x, i) => args[x.name ?? i]), around line 1237) has the same subset-index-vs-declaration-index confusion — for an unnamed/mixed event, it reads a caller's args object by indexed-subset index rather than declaration index. I confirmed this is real with a standalone repro: AbiEvent.encode on event Foo(uint256, bool indexed, address indexed) given {1: true, 2: '0x1234...'} (the natural shape, matching what decode's own fixed output now produces) throws trying to encode the bool value as an address, because it read the wrong key. Leaving this out of this PR since it's a separate function with its own test surface and deserves its own focused fix — happy to open a follow-up if useful.

Confirmed unclaimed before starting: no open or closed issues/PRs on AbiEvent decode unnamed, decodeEventLog unnamed, AbiEvent unnamed indexed, unnamed, or indexed touch this. Only other open PRs on this repo are unrelated (#433 changesets bot, and two of my own in different files: #434, #435).

…eclaration-order prefix

`decode`'s indexed-topic loop keyed writes off the loop index within the
indexed-subset array, not the input's original declaration index. Whenever a
non-indexed input appeared before an indexed one, this caused:

- array-mode (unnamed events): args returned in indexed-subset order instead
  of declaration order (e.g. `event Foo(uint256, bool indexed)` returned
  `[bool, uint256]` instead of `[uint256, bool]`).
- mixed-mode (some named inputs): an indexed value silently overwritten by
  an unrelated non-indexed value sharing the same numeric key, with no error
  raised (e.g. `event Foo(uint256 indexed a, bool, address indexed)` dropped
  the address entirely - 3 inputs decoded to 2 values).

The non-indexed side of the same function already tracked original
declaration index (`nonIndexedOriginalIndex`) for exactly this reason; the
indexed side had no equivalent. This adds `indexedOriginalIndex` and keys
both the indexed-topic writes and the non-indexed array-mode fill by
declaration index, matching what the non-indexed named-mode branch and
viem's `decodeEventLog` already do.

Canonical events (ERC-20 Transfer, Approval, etc.) are unaffected - their
indexed inputs are already a declaration-order prefix, which is exactly the
shape the four pre-existing snapshot tests exercised, so the bug was
invisible to them.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Sep 1, 2026

Copy link
Copy Markdown

@gomesalexandre is attempting to deploy a commit to the Wevm Team on Vercel.

A member of the Team first needs to authorize it.

@gomesalexandre
gomesalexandre marked this pull request as ready for review September 1, 2026 15:33
@pkg-pr-new

pkg-pr-new Bot commented Sep 1, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/ox@436

commit: a708038

@jxom
jxom merged commit 3f894f8 into wevm:main Sep 1, 2026
11 of 13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants