Skip to content

feat(decoder): Universal Router execute() + V3/V2 opcodes + V4_SWAP named-skip + real-mainnet fixtures - #171

Merged
0xfandom merged 4 commits into
developfrom
feat/decoder-universal-router-v3-swap-exact-in
May 27, 2026
Merged

feat(decoder): Universal Router execute() + V3/V2 opcodes + V4_SWAP named-skip + real-mainnet fixtures#171
0xfandom merged 4 commits into
developfrom
feat/decoder-universal-router-v3-swap-exact-in

Conversation

@0xfandom

@0xfandom 0xfandom commented May 27, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Uniswap Universal Router is the dominant post-2024 aggregator entry point but the pipeline ignored it on two fronts: the Alchemy toAddress filter never subscribed to its address, and the decoder layer had no handler for execute(bytes commands, bytes[] inputs[, uint256 deadline]).
  • UR is a command-byte VM, not a multicall. Each byte in commands is an opcode (after masking with COMMAND_TYPE_MASK = 0x3f) that consumes one entry from the parallel inputs[] array.
  • This PR ships UR end-to-end across all four AMM swap opcodes (V3_SWAP_EXACT_IN, V3_SWAP_EXACT_OUT, V2_SWAP_EXACT_IN, V2_SWAP_EXACT_OUT). Permit / wrap / sweep / unknown opcodes are silently skipped so the dispatcher stays robust against future on-chain VM additions.
  • The E5 fixture sweep also surfaced that ~80% of current UR V2 traffic uses opcode 0x10 (V4_SWAP) which wraps a Uniswap V4 PoolManager action stream. Full V4 decode is out of scope for the current V2/V3 backrun target, but the opcode is now named explicitly and routed to a logged skip so operators can size the missed-volume from production.
  • Three real-mainnet fixture tests pin the decoder against raw eth_getTransactionByHash payloads to catch encoder-vs-decoder drift (synthetic sol!-encoded tests can hide that class of bug because both sides use the same Rust macro).

Files Changed

File Change
crates/ingestion/src/mempool.rs Adds UR V2 (0x66a9...) and V1.2 (0x3fc9...) to default_router_addresses(). New unit test pins both.
crates/pools/src/router_decoder.rs Adds IUniversalRouter sol! interface, ur_commands opcode constants (V3_IN, V3_OUT, V2_IN, V2_OUT, V4_SWAP), try_universal_router dispatcher, decode_ur_opcode handler covering all four swap variants plus the V4_SWAP named-skip. Fourteen tests (synthetic + 3 real mainnet fixtures + V4_SWAP skip).

Acceptance Criteria

  • execute_0Call::SELECTOR == 0x24856bc3.
  • execute_1Call::SELECTOR == 0x3593564c.
  • UR V2 and V1.2 addresses appear in default_router_addresses().
  • V3_SWAP_EXACT_IN calldata decodes into one Protocol::UniswapV3 swap with the correct (token_in, token_out, fee, amount_in, amount_out_min, recipient, router).
  • V3_SWAP_EXACT_OUT flips the path so (token_in, token_out) are in canonical pool-pair order.
  • V2_SWAP_EXACT_IN decodes address[] paths; intermediate tokens carry into path_extra.
  • V2_SWAP_EXACT_OUT follows V2 path orientation (NOT reversed).
  • V4_SWAP opcode 0x10 is named, logged at debug with target ur_v4_swap_unsupported, and does not error a UR call that mixes it with recognised opcodes.
  • Non-swap opcodes (WRAP_ETH, UNWRAP_WETH, SWEEP, ...) leave only swap records in the output.
  • Length mismatch between commands and inputs returns DecodeError::AbiDecode.
  • Empty / single-element V2 paths return DecodeError::EmptyPath.
  • Three captured-mainnet payloads decode cleanly via the production entry point.
  • cargo clippy -p aether-pools -p aether-ingestion -- -D warnings clean.

Test plan

  • cargo test -p aether-pools --lib router_decoder — 47 pass (14 UR tests including 3 mainnet fixtures + V4_SWAP skip), 0 new failures (1 pre-existing unrelated overflow in decode_bancor_trade_by_source_amount).
  • cargo test -p aether-ingestion --lib mempool — all pass.
  • cargo build --workspace clean.
  • Expected impact on next shadow run: Universal Router pending txs flow through the pipeline for the first time. The ur_v4_swap_unsupported log line additionally lets operators size the V4 follow-up against production volume.

Follow-up scope (NOT in this PR)

Full V4 PoolManager decode is a separate workstream — adds a V4Pool trait implementation, parses (bytes actions, bytes[] params) tuples, models V4 hooks for post-state prediction. Tracked as a downstream task once shadow data quantifies the missed volume.

@vercel

vercel Bot commented May 27, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
aether Ready Ready Preview, Comment May 27, 2026 6:21pm
aether-63xv Ready Ready Preview, Comment May 27, 2026 6:21pm

@0xfandom 0xfandom changed the title feat(decoder): Universal Router execute() + V3_SWAP_EXACT_IN opcode feat(decoder): Universal Router execute() + V3/V2 exact-in/out opcodes May 27, 2026
@0xfandom 0xfandom changed the title feat(decoder): Universal Router execute() + V3/V2 exact-in/out opcodes feat(decoder): Universal Router execute() + V3/V2 exact-in/out opcodes + real-mainnet fixtures May 27, 2026
@0xfandom 0xfandom changed the title feat(decoder): Universal Router execute() + V3/V2 exact-in/out opcodes + real-mainnet fixtures feat(decoder): Universal Router execute() + V3/V2 opcodes + V4_SWAP named-skip + real-mainnet fixtures May 27, 2026
@0xfandom
0xfandom force-pushed the feat/decoder-universal-router-v3-swap-exact-in branch from 13094bf to 5574e46 Compare May 27, 2026 18:17
0xfandom added 4 commits May 27, 2026 23:50
Uniswap Universal Router is the dominant post-2024 entry point for
aggregator traffic but our pipeline ignored it on two fronts: the
Alchemy pending-tx filter never subscribed to the UR address, and the
decoder layer had no handler for execute(bytes, bytes[]) /
execute(bytes, bytes[], uint256). UR is a command-byte VM, not a
multicall — each byte in commands is an opcode that consumes one
entry from the parallel inputs[] array.

Three changes ship together so the wire-up is end-to-end usable:

1. Add Universal Router V2 (0x66a9893c...) and V1.2 (0x3fc91a3a...)
   to the default Alchemy router filter.
2. Declare IUniversalRouter with both execute overloads — sol! emits
   execute_0Call (selector 0x24856bc3) and execute_1Call (selector
   0x3593564c) faithful to mainnet.
3. Add try_universal_router dispatcher and decode_ur_opcode handler.
   This first cut covers V3_SWAP_EXACT_IN (opcode 0x00) only;
   permit / wrap / sweep opcodes are silently skipped and unknown
   opcodes drop without erroring so the dispatcher stays robust
   against future on-chain VM additions. Additional swap opcodes
   (V3_OUT, V2_IN, V2_OUT) land as follow-up PRs.
Extends the Universal Router opcode handler with the remaining three
swap variants so wallet-issued UR calls covering exact-output trades
and V2-only paths are not silently dropped.

- V3_SWAP_EXACT_OUT (0x01): reverses the path (token_out → token_in
  in calldata) before surfacing the canonical (token_in, token_out)
  pair; same exact-out amount-surfacing convention as the V3
  router's standalone exactOutput single / multi-hop family.
- V2_SWAP_EXACT_IN (0x08): decodes address[] path, first hop is
  path[0] → path[1], remaining tokens carry into path_extra.
- V2_SWAP_EXACT_OUT (0x09): same path orientation as V2_IN (NOT
  reversed, unlike V3 exact-out); exact-out amount mapping mirrors
  the V3 branch.

All three reject empty / single-element paths with EmptyPath. A new
mixed V2+V3 chain test exercises multi-opcode dispatch order.
Synthetic sol!-encoded tests can mask encoder-vs-decoder drift
because both sides use the same Rust macro. Pin the decoder
against three real mainnet pending-tx payloads to catch that
class of bug.

Each fixture is the raw eth_getTransactionByHash → input bytes of
a real Universal Router call, embedded as a hex constant:

- 0xfbb854...8996 (UR V2, ops=[V2_SWAP_EXACT_IN, SWEEP]): WETH →
  arbitrary token; asserts V2 surfacing with canonical WETH as
  token_in and zero fee_bps.
- 0x87a841...4d4e33 (UR V1.2, ops=[WRAP_ETH, V3_SWAP_EXACT_OUT,
  UNWRAP_WETH]): asserts the V3 exact-out path flip lands WETH on
  the token_in side and surfaces the 1% (10000 bps) fee tier.
- 0x637397...0b1f (UR V2, ops=[V2_SWAP_EXACT_IN, UNWRAP_WETH]):
  token → WETH; asserts WRAP/UNWRAP opcodes are dropped silently
  and only the swap survives.

All three fixtures use the production decode_pending_many entry
point, so any future drift in the dispatcher, opcode parser, or
sol! ABI shape regresses these tests.
The E5 fixture sweep showed that Universal Router V2 traffic is now
dominated by opcode 0x10 (V4_SWAP), which wraps a Uniswap V4
PoolManager action stream. Roughly four out of every five UR V2
pending txs the captures touched had V4_SWAP at the top level. The
prior catch-all skip dropped them indistinguishably from other
unknown opcodes, hiding their volume in the dashboards.

V4 PoolManager decode is a substantial separate workstream — new
PoolKey shape, hooks-aware pricing, per-action ABI tables — and is
out of scope for the current V2/V3 backrun target. This PR is
deliberately minimal:

- Add a named V4_SWAP = 0x10 constant in ur_commands with a doc
  comment that explains why it is currently un-decoded and what
  full support would entail.
- Route the opcode through an explicit match arm that emits a
  debug-level trace event labelled 'ur_v4_swap_unsupported' so
  operators can size the missed-volume from logs ahead of any
  formal metric migration.
- Unit test that confirms a mixed [V4_SWAP, V3_SWAP_EXACT_IN] call
  silently drops the V4 record and surfaces the V3 swap intact.
@0xfandom
0xfandom force-pushed the feat/decoder-universal-router-v3-swap-exact-in branch from 5574e46 to b8f1737 Compare May 27, 2026 18:21
@0xfandom
0xfandom merged commit 60a8232 into develop May 27, 2026
3 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.

1 participant