feat(hindsight): decode on-chain aggregator swaps (ENG-6122)#263
feat(hindsight): decode on-chain aggregator swaps (ENG-6122)#263kayibal wants to merge 4 commits into
Conversation
| ) -> Option<(Address, (Address, U256, Address, U256))> { | ||
| // Prefer externally-owned accounts; pools and routers carry code. | ||
| let mut maker = None; | ||
| for (candidate, trade) in maker_candidates(logs, native, exclude, names) { |
There was a problem hiding this comment.
🤖 Batch settlements decode only one maker. This loop returns the first candidate with a clean two-token net, so a CoW batch that settles multiple retail orders in one tx contributes a single decoded trade — the rest surface as 'Allium only' gaps and the decoder systematically under-counts batch trades. If decoding one maker per batch is intentional for v0, make it explicit (doc or warn!); the batch/CoW path is the focus of this PR.
| /// Addresses with a clean two-token net swap, excluding the zero address, the | ||
| /// excluded addresses, and known registry contracts. Ordered by address for | ||
| /// deterministic selection. | ||
| fn maker_candidates( |
There was a problem hiding this comment.
🤖 Maker selection has no settlement-tied tiebreak. When several non-excluded EOAs each net to a clean two-token swap (two independent makers, or a maker plus an EOA counterparty), the winner is just the first in maker_candidates' address-ordered iteration. The EOA-vs-contract filter disambiguates maker-vs-pool but not maker-vs-maker, so a correct-looking decode can attribute the wrong account's flow. Consider tying the choice to the actual settlement rather than address order.
| fn largest_external_call( | ||
| root: &CallFrame, | ||
| entry_point: Address, | ||
| sender: Address, | ||
| ) -> Option<Address> { | ||
| let mut best: Option<(Address, U256)> = None; | ||
| for child in &root.calls { | ||
| if child.error.is_some() { | ||
| continue; | ||
| } | ||
| let Some(to) = child.to else { continue }; | ||
| if to == entry_point || to == sender { | ||
| continue; | ||
| } | ||
| let value = child.value.unwrap_or_default(); | ||
| if best.is_none_or(|(_, best_value)| value > best_value) { | ||
| best = Some((to, value)); |
There was a problem hiding this comment.
🤖 Aggregator fallback degenerates to 'first child' for ERC-20 routes. largest_external_call ranks the entry point's child calls by native ETH value, but a token→token route through an unknown router carries no native value, so every candidate ties at zero and the first child wins. That attributes the swap to an essentially arbitrary contract; the label is then diffed against Allium, so the mis-attribution reads as a spurious aggregator mismatch rather than a clean 'unknown'. Prefer returning unknown when all candidates tie at zero value.
a3db0f2 to
e6507a6
Compare
f1d1a72 to
f1fcbd8
Compare
Decode settled aggregator swaps for a block and report what each trade put in and took out, with client and aggregator attribution. - Fetch receipts per block with eth_getBlockReceipts and trace matched transactions concurrently (bounded), keeping each block within its block time - Recover native ETH legs from the callTracer trace, since token->ETH and ETH->token swaps deliver ETH without emitting a log - Attribute client vs aggregator: direct swaps settle at the entry point; client-routed swaps (e.g. Relay) resolve the aggregator from the trace, falling back to the contract address when unknown Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a `hindsight verify` subcommand that decodes a block locally and diffs each transaction against Allium's aggregator_trades table as ground truth (allium.rs async Explorer client, verify.rs comparison by token, amount in bps, and aggregator). Split the CLI into `decode` and `verify` subcommands. Extend the decoder to catch filler-initiated intent fills (UniswapX, 1inch limit orders) by matching a known aggregator log signature and resolving the order maker by net flow. Add 1inch v5, the UniswapX reactor, and the Tycho router addresses. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
net_trade picked the largest-by-raw-amount leg when a tracked address netted more than one token, but raw amounts are not comparable across decimals, so it paired unrelated tokens. Decline unless exactly one token nets in and one nets out, so only clean single swaps are decoded.
For CoW the tx sender is the solver settling a whole batch, so tracking it nets unrelated orders. Mark batch settlers (registry::is_batch_settler) and decode them by finding the order maker, like filler-initiated intent fills.
e6507a6 to
8fe771d
Compare
f1fcbd8 to
71e8727
Compare
Decode settled aggregator swaps from chain data, with Allium verification. Includes decoder reliability: decline ambiguous multi-token nets, and decode CoW/batch settlements via the order maker rather than the solver sender. Stacked PR #2/6; base
eng-6121.