diff --git a/tools/CLAUDE.md b/tools/CLAUDE.md index a67f2479..4f1f2e01 100644 --- a/tools/CLAUDE.md +++ b/tools/CLAUDE.md @@ -63,8 +63,12 @@ Four subcommands via `cargo run -p hindsight --release --`: - **`verify`** — Diff decoded trades against Allium's `aggregator_trades` ground truth. Requires `ALLIUM_API_KEY` and `ALLIUM_QUERY_ID`. - **`monitor`** — Live mode: drives an in-process `fynd-core` solver block-by-block, solving - each settled trade at top-of-block (N-1) and again at back-of-block (N), where the top route - is also re-executed to measure slippage. Emits JSONL and exposes Prometheus metrics. + each trade with known terms — settled or reverted — at top-of-block (N-1) and again at + back-of-block (N), where the top route is also re-executed to measure slippage. A settled + trade is judged against what it settled for; a decodable reverted Relay swap is judged against + its on-chain `min_amount_out` floor at both states, to quantify how many reverts a sequencer + filling against fresher state could avoid. Emits one JSONL stream (settled and reverted trades + told apart by a `status` field) and exposes Prometheus metrics. - **`report`** — Offline: render a self-contained HTML report from a `monitor` run's comparison JSONL (`--comparisons-dir`). No chain or network access. diff --git a/tools/hindsight/CLAUDE.md b/tools/hindsight/CLAUDE.md index 51fae3bc..20f0bcac 100644 --- a/tools/hindsight/CLAUDE.md +++ b/tools/hindsight/CLAUDE.md @@ -26,13 +26,16 @@ name needs `--registry`. `ALLIUM_API_KEY` and `ALLIUM_QUERY_ID`. - **`monitor`** — Live mode: drives an in-process `fynd-core` solver block-by-block. For each - block it decodes settled trades, solves each order at top-of-block (state N-1), then measures - twice at back-of-block (state N): the top route is re-executed via `fynd_core::replay_route` - to measure slippage between quote time and execution time, and the order is solved fresh to - show what routing at the block's end state would deliver. Emits `RangeComparison` JSONL - records. Exposes a Prometheus metrics endpoint (`--metrics-port`). `--max-lag-blocks` (default - 100, ~20 min on mainnet) bounds how far it may fall behind chain head before rebuilding the - solver. + block it decodes every trade — settled and reverted alike — solves each one with known terms at + top-of-block (state N-1), then measures twice at back-of-block (state N): the top route is + re-executed via `fynd_core::replay_route` to measure slippage between quote time and execution + time, and the trade is solved fresh to show what routing at the block's end state would + deliver. A settled trade is judged against what it settled for; a reverted trade whose calldata + parsed into a `SwapIntent` is judged against whether the quote would have cleared the trader's + on-chain floor (`min_amount_out`) instead — told apart by one `status` field, not by separate + record types. Emits one `RangeComparison` JSONL stream (`comparisons-*.jsonl`) and exposes a + Prometheus metrics endpoint (`--metrics-port`). `--max-lag-blocks` (default 100, ~20 min on + mainnet) bounds how far it may fall behind chain head before rebuilding the solver. - **`report`** — Offline: read the `comparisons-YYYY-MM-DD.jsonl` files a `monitor` run wrote (`--comparisons-dir`) and render a single self-contained HTML file (`-o`, defaults to @@ -58,17 +61,17 @@ Match → trace → decode → veto → record. | File/dir | Purpose | |---|---| -| `matching.rs` | Receipt-only filter: is this transaction a solver trade at all, plus match-time vetoes | +| `matching.rs` | Receipt-only filter: is this transaction a solver trade at all — settled or reverted, distinguished by `MatchedSolverTrade::reverted`. A settled match is filtered by tx.to/solver logs plus match-time vetoes; a reverted match is by entry point alone (gated by `REVERT_DECODED_VENUES`) — a revert emits no logs, so this is the only signal available | | `decode.rs` | The `TradeDecoder` trait, the matched entity → decoders mapping, `DecodeContext`, `TraderFlow` | | `netting_decoders.rs` | Netting toolkit (`sender_flow`, `venue_flow`) plus the `SenderNetting` decoder | | `transfer_ledger.rs` | Builds a transfer ledger from logs and native ETH flows | | `veto.rs` | The shared `Veto` type, plus post-decode vetoes of non-comparable shapes (NFT purchases, mis-paired wrap trades) | | `registry.rs` | Per-chain address book, loaded from TOML (see below) | | `sandwich.rs` | Flags trades bracketed by a front/back attacker pair (see the design spec) | -| `venues/` | Per-venue `TradeDecoder` impls (Relay, MetaMask, Rabby), listed in `venues::decoders_for` | -| `solvers/` | Per-solver knowledge: embedded quotes, match-time vetoes, attribution | +| `venues/` | Per-venue `TradeDecoder` impls (Relay, MetaMask, Rabby), listed in `venues::decoders_for`. Relay has two, tried in order: `RelayCalldata` (calldata-primary, see below) then `RelayNetting` (the fallback) | +| `solvers/` | Per-solver knowledge: embedded quotes, match-time vetoes, attribution, swap intents (`fly.rs`'s packed-calldata parser, `kyberswap.rs`'s ABI-decoded `swap` params, `zeroex.rs`'s ABI-decoded `AllowanceHolder.exec`/`Settler.execute`) recovered from a solver frame's own calldata via `trace::find_solver_frame` — for settled trades that frame settled, for reverted trades the frame that tried — plus each solver's slippage-floor marker (`is_slippage_floor`, e.g. Fly's `InsufficientAmountOut()` selector, KyberSwap's "Return amount is not enough" reason, `zeroex.rs`'s `TooMuchSlippage(address,uint256,uint256)` selector) and the declared output recipient (`output_recipient`) that lets `RelayCalldata` anchor the settled amount | | `intents/` | Intent-role decoders (solver-sent, trader-not-sender): `cow.rs` reads CoW's `Trade` event, `netting.rs` is the generic net-flow finder, `decoders_for` lists them | -| `trace.rs` | Transaction trace fetching and processing | +| `trace.rs` | Transaction trace fetching and processing: `find_solver_frame` (the frame that settled the swap, or — when nothing settled — the frame that tried) and revert-cause classification (the generic subtree walk, out-of-gas, deepest-revert-reason; the slippage-floor marker itself is asked of `solvers::`, never hardcoded here) | `src/verify/` contains the Allium integration: - `allium.rs` — Allium API client for the `verify` subcommand @@ -93,10 +96,10 @@ their solver in calldata — `solver_aliases`). | File | Purpose | |---|---| -| `mod.rs` | `SteppingSolver` trait; `resolve_block_range` — solve all trades at top, advance, then re-execute each top route and solve each trade fresh at back | -| `compare.rs` | `Verdict` / `Deltas` / `Slippage` — bps diff, win/loss/coverage-miss classification, quote-vs-re-execution slippage | +| `mod.rs` | `SteppingSolver` trait; `resolve_block_range` — one wave over every trade (settled and reverted alike): solve those with known terms at top, advance, then re-execute each top route and solve fresh at back. A trade with unknown terms is recorded but not solved | +| `compare.rs` | `Verdict` / `Deltas` / `Slippage` — bps diff, win/loss/coverage-miss classification, quote-vs-re-execution slippage; `floor_judgment` — fillable/margin judgment against `min_amount_out`, used for both settled and reverted trades when a floor is known | | `monitor.rs` | Production `monitor` subcommand: in-process solver, block subscription, JSONL emission | -| `jsonl.rs` | Append-only JSONL writer used by `monitor` | +| `jsonl.rs` | `write_comparisons` — the one append-only JSONL stream (`comparisons-*.jsonl`) `monitor` writes, via the rotating-by-day `RotatingWriter`; every trade's record carries `tx_hash`, `sender` (the netted trader for a settled trade, the transaction sender for a reverted one — segments venue-filler fills like Relay's rotating filler without an on-chain lookup), a flat `status` (`settled`/`reverted`), and, for a revert, `cause`/`cause_detail` | ### Report (`src/report/`) @@ -105,7 +108,7 @@ self-contained HTML file. | File | Purpose | |---|---| -| `mod.rs` | `ReportArgs`; reads every `.jsonl` in the dir, skipping malformed lines | +| `mod.rs` | `ReportArgs`; reads every `comparisons-*.jsonl` in the dir, skipping malformed lines, then filters to `status == "settled"` before aggregating — savings and win-rate only mean something for a trade that delivered an output | | `record.rs` | The subset of the JSONL record the report deserializes; round-trip tested against `jsonl::write_comparisons` | | `aggregate.rs` | Pure aggregations over the records (verdicts, coverage, savings, per-group, movers) | | `html.rs` | Renders the aggregates to a self-contained HTML file (inline CSS, `
` bars, no assets) | @@ -130,12 +133,64 @@ positive-only USD histogram (`hindsight_positive_slippage_usd`) whose sum is the hypothetical revenue. Absent when the top was unsolved or the re-execution failed (e.g. a pool vanished at N). +### Calldata-first Relay decoding + +`RelayCalldata` reads `token_in`/`token_out`/`amount_in` from the settling solver frame's own +`SwapIntent` and recovers the settled `amount_out` as the gross amount of `token_out` received by +the output recipient the same calldata declares — the one field calldata can never carry. Two +guards protect the recipient-receipt query: the recovered output must clear the intent's +`min_amount_out` floor, and any declared quote must sit within `plausible_quote`'s band of it; +either failure falls through to `RelayNetting`. The solver frame's `amount_in` needs no fee +adjustment — Relay pays its input-side fee to the collector *before* forwarding into the solver +call, so it is already the post-fee figure `amount_in` is defined to be — and the recipient's +receipt is the gross output before any output-side fee, so neither amount needs adjusting; both +fees are still recorded via `venue_fee_in`/`venue_fee_out` for transparency. See +`.claude/plans/calldata-first-decoding.md` for the empirics: on a 315-transaction Base sample, +coverage rises from 60.0% (netting alone) to 91.4% (calldata-first union), with zero divergences +across the 165 trades both paths could decode. + +### Revert model + +A reverted trade is still a trade — one that did not fill — so it is modeled by `DecodedTrade`, +told apart from a settled one by its `status` field (`TradeStatus::Settled` or +`TradeStatus::Reverted { cause }`), not by a parallel type. Reverted Relay swaps are matched by +entry point alone (no logs to match on), decoded from their trace for the settling solver — via +the same `trace::find_solver_frame` a settled trade uses, whose strict-then-tolerant walk falls +back to "the frame that tried" when nothing settled — and, when that solver's calldata supports +it (Fly, KyberSwap), the trader's terms (`token_in`/`token_out`/`amount_in`/`min_amount_out`), +which land in the same fields a settled trade's terms do. `RevertCause` classifies why the +transaction failed: `SlippageFloor` (a registered solver's `is_slippage_floor` marker — Fly's +`InsufficientAmountOut()` selector, KyberSwap's "Return amount is not enough" reason, 0x's +`TooMuchSlippage(address,uint256,uint256)` selector or a routed-through liquidity source's +"return too low" — the avoidable class), `OutOfGas`, or `Other` (the deepest reverted frame's +error/revert reason, with the frame's raw output selector appended when the tracer decoded no +string reason, e.g. `"execution reverted (0x12345678)"` — several RPC providers never populate +a decoded reason at all, so the selector is what stays classifiable offline). + +A reverted trade with known terms is re-solved at top-of-block (N-1) and back-of-block (N) +through the same wave a settled trade goes through (`resolve_block_range`), then judged by the +same `StateResult` every trade gets: `fillable` (quote ≥ `min_amount_out`) and `margin_bps` +(signed bps of the quote against the floor), quantifying how many reverts a sequencer filling +against fresher state could avoid. A settled trade gets this judgment too, whenever a floor is +known — it cleared the floor by construction, but the margin is still informative. A trade with +unknown terms (a reverted trade whose solver calldata did not parse) is recorded but not solved — +only counted, so parser coverage stays measurable — and every trade, settled or reverted, writes +to the one `comparisons-YYYY-MM-DD.jsonl` stream; there is no separate revert stream or type. + ### Key types -- `DecodedTrade` — decoded on-chain trade; amounts are venue-fee-adjusted so re-solve compares - like-for-like. Carries `sandwich` evidence when a bracket pair was found. -- `RangeComparison` — a trade solved at top and back, including gas-netted settled output and - the top route's `Slippage` between the two states (from its re-execution at back). +- `DecodedTrade` — a decoded on-chain trade, settled or reverted, told apart by `status: + TradeStatus`. A settled trade always carries `token_in`/`token_out`/`amount_in`/`amount_out` + (amounts venue-fee-adjusted so re-solve compares like-for-like) and, when a bracket pair was + found, `sandwich` evidence; a reverted trade carries `token_in`/`token_out`/`amount_in` only + when its solver frame's calldata parsed, and never `amount_out` — nothing was delivered. Both + carry `min_amount_out`, `declared_quote`, and `quote_timestamp` when known (the calldata-declared + terms copied off the settling solver's `SwapIntent`). +- `RangeComparison` — a trade solved at top and back — settled or reverted, told apart by + `status` — including gas-netted settled output (settled only), the top route's `Slippage` + between the two states, and, per state, the `fillable`/`margin_bps` judgment against + `min_amount_out` when a floor is known. `top`/`back`/`verdict`/`slippage` are absent when the + trade's terms were unknown. - `Outcome` — `Solved`, `Partial`, or `Unsolvable`. - `SolvedAmount` — a solved state's amounts plus `algorithm` (which worker pool won the quote) and `solved_route` (the full `fynd_core::types::Route`, kept in memory to replay at back-of-block). @@ -183,9 +238,10 @@ It surfaces three ways: - **Solver** (a router Fynd competes with): one line in the address book's `[solvers]` section is enough for matching, attribution, gas isolation, and metric labels. Optional code: a - `SolverKnowledge` impl in `solvers/` (registered in `solvers::IMPLEMENTATIONS`) with an - `embedded_quote` method if its calldata declares an off-chain quote, or a `solver_veto` method - if some of its orders are not same-chain swaps. + `SolverKnowledge` impl in `solvers/` (registered in `solvers::IMPLEMENTATIONS`) with a + `solver_veto` method if some of its orders are not same-chain swaps, or a `swap_intent` method + if a trade's terms (tokens, amounts, on-chain floor, and — when the calldata declares one — + the solver's off-chain quote) can be recovered from the settling solver frame's own calldata. - **Venue** (a platform users enter through): a `[venues.]` address-book section plus a `TradeDecoder` in `venues/`, registered in the one `venues::decoders_for` arm (its `mod` declaration is the only other line). Most venues are sender netting + fee back-out — call diff --git a/tools/hindsight/README.md b/tools/hindsight/README.md index 4fe31cb7..b10de519 100644 --- a/tools/hindsight/README.md +++ b/tools/hindsight/README.md @@ -52,14 +52,14 @@ All take `--chain` (selects the address book) and `--registry` / │ │ direct solver → [ SenderNetting ] │ batch settler / solver → [ CowSettlement, IntentNetting ] - │ venue relay → [ RelayNetting ] + │ venue relay → [ RelayCalldata, RelayNetting ] │ venue metamask → [ MetaMaskNetting ] │ TraderFlow ▼ - ┌─────────────────┐ embedded_quote ┌─────────────────┐ + ┌─────────────────┐ swap_intent ┌─────────────────┐ │ post-processing │ ───────────────▶ │ SolverKnowledge │ └────────┬────────┘ └─────────────────┘ - │ veto → venue attribution → solver attribution → gas → quote → sandwich scan + │ veto → venue attribution → solver attribution → gas → intent → sandwich scan ▼ DecodedTrade ``` @@ -85,7 +85,7 @@ trait TradeDecoder

{ match role { Sender => vec![Box::new(SenderNetting)], Intent => intents::decoders_for(), // [CowSettlement, IntentNetting] - Venue(name) => venues::decoders_for(name), // e.g. "relay" → [RelayNetting] + Venue(name) => venues::decoders_for(name), // e.g. "relay" → [RelayCalldata, RelayNetting] } ``` @@ -117,16 +117,18 @@ match role { ▼ ▼ ▼ ▼ venues::decoders_for(name) intents::decoders_for() SenderNetting intents::decoders_for() │ └──── intents/ ────┘ netting_dec.rs └──── intents/ ────┘ - ┌──────────┴───────────┐ - ▼ ▼ - [RelayNetting] [MetaMaskNetting] - (venues/relay.rs) (venues/metamask.rs) + ┌──────────┴─────────────────┐ + ▼ ▼ + [RelayCalldata, RelayNetting] [MetaMaskNetting] + (venues/relay.rs) (venues/metamask.rs) direct call vs a solver-settled intent order — SAME solver, DIFFERENT decoder: 0x called directly → Sender → [ SenderNetting ] (your own tx, your gas) 0x settling your intent order → Intent → intents::decoders_for() (a solver settles for you) an intent source with a richer signal gets its own decoder ahead of the netting fallback — - CoW reads its Trade event (intents/cow.rs), then IntentNetting catches the rest. + CoW reads its Trade event (intents/cow.rs), then IntentNetting catches the rest. Relay is the + same shape: RelayCalldata reads the settling solver's own calldata (SwapIntent) plus a + recipient-anchored ledger query for the settled output, ahead of RelayNetting. implement a new decoder where the entity that carries the flow lives: ├─ new venue → venues/.rs + arm in venues::decoders_for("") @@ -157,16 +159,27 @@ the swap. `MetaMaskNetting` backs the fee out to 991. ### Solver knowledge (`solvers/`) -What a solver's transactions reveal beyond its address — a calldata quote (KyberSwap's -`clientData`, ParaSwap's word layout, 0x's positive-slippage action), a match-time veto (LiFi's -bridge orders), or the integrator tag a frontend records in the solver's event (LiFi's Diamond). -Every method defaults to "nothing to add", so most solvers are a single address-book line with no -code; those with code are registered in `solvers::IMPLEMENTATIONS`. +What a solver's transactions reveal beyond its address — a calldata-recovered swap intent +(KyberSwap's ABI decode plus its `clientData` quote, ParaSwap's word layout, Fly's packed +layout), a match-time veto (LiFi's bridge orders), or the integrator tag a frontend records in +the solver's event (LiFi's Diamond). Every method defaults to "nothing to add", so most solvers +are a single address-book line with no code; those with code are registered in +`solvers::IMPLEMENTATIONS`. ```rust trait SolverKnowledge { - /// The solver's off-chain quote declared in its calldata, when it embeds one. - fn embedded_quote(&self, input: &[u8], amount_in: U256) -> Option { None } + /// The trader's swap terms (token in/out, amounts, the on-chain min_amount_out floor, and — + /// when the calldata declares one — the solver's off-chain quote), when the solver frame's + /// own calldata carries them — how a reverted trade's floor is recovered (a revert emits no + /// logs to net a settled amount from). `amount_in_hint` is the decoded flow's input amount, + /// when known (absent for a reverted trade); scan-based extractors (ParaSwap) need it to + /// locate fields by value rather than by ABI offset. + fn swap_intent(&self, input: &[u8], amount_in_hint: Option) -> Option { None } + + /// The address this solver's calldata declares as the output recipient — how a + /// calldata-primary decode (RelayCalldata) learns whose receipt to read the settled amount + /// from, since calldata alone never carries a settled amount. + fn output_recipient(&self, input: &[u8]) -> Option

{ None } /// The veto this solver's logs place on a matched transaction that is not a swap. fn solver_veto(&self, logs: &[Log]) -> Option { None } @@ -212,8 +225,8 @@ collectors are re-verified on every chain a venue is added on. | You want to… | Touch | Without it | |---|---|---| -| Track a new solver | One line in the address book's `[solvers]` section. No code — trades sent straight to the router then match on the entry point and decode like any other; the quote and veto rows below are optional extras | Trades sent directly to the solver's router never match, so they never appear in the output; trades a known venue routed through it still decode, but the solver is recorded as "unknown" | -| Read a solver's quote from its calldata | A `SolverKnowledge` impl in `solvers/`, registered in `solvers::IMPLEMENTATIONS` | Records for that solver carry no quote | +| Track a new solver | One line in the address book's `[solvers]` section. No code — trades sent straight to the router then match on the entry point and decode like any other; the intent and veto rows below are optional extras | Trades sent directly to the solver's router never match, so they never appear in the output; trades a known venue routed through it still decode, but the solver is recorded as "unknown" | +| Recover a solver's swap terms (tokens, amounts, on-chain floor, and — when its calldata declares one — its off-chain quote) | A `swap_intent` method on its `SolverKnowledge` impl, dispatched with the settling solver frame's own input | A settled trade's record carries no `min_amount_out` / `declared_quote` / `quote_timestamp`; a revert is still recorded (venue, solver, cause) but with no `SwapIntent`, so it cannot be resolved or judged for avoidance — only counted as parser coverage | | Skip a solver's non-swap orders | A `solver_veto` method on its `SolverKnowledge` impl | Those orders decode as trades that never happened, with absurd rates | | Add a venue | A `[venues.]` section in the address book, a `TradeDecoder` in `venues/`, one arm in `venues::decoders_for` | The venue's trades are missed: with no entry-point match they only surface when a known solver logs inside them, and intent decoding then excludes the trader | | Extend what Hindsight knows about a venue | That venue's module in `venues/` — never anywhere else | Decoding degrades silently | @@ -228,20 +241,27 @@ collectors are re-verified on every chain a venue is added on. ``` tycho stream ──▶ in-process Fynd solver, held at block N-1 │ - decode block N │ re-solve every settled trade → top-of-block result + decode block N │ re-solve every trade with known terms → top-of-block result ▼ advance solver to N - │ re-solve every trade again → back-of-block result + │ re-solve every trade again → back-of-block result ▼ RangeComparison ──▶ JSONL records + Prometheus metrics ``` +A settled and a reverted trade go through the same wave: `RangeComparison` carries a `status` +field (`settled` or `reverted`, with a cause) rather than being two record types. A trade with +unknown terms — a reverted swap whose solver calldata did not parse — is recorded but not solved. + Top-of-block (state N-1) is the optimistic comparison — Fynd sees the pools before the block's -own swaps moved them; back-of-block (state N) is the pessimistic one. The headline verdict is -top-of-block: `Win`, `Loss`, `CoverageMiss` (Fynd filled under half the settled size), -`Unsolvable`, or `Sandwiched` (the settled output was moved by MEV; excluded from savings -aggregates). Watchdogs rebuild the solver when the tycho feed dies or the monitor falls too -far behind chain head (`--max-lag-blocks`). +own swaps moved them; back-of-block (state N) is the pessimistic one. For a settled trade, the +headline verdict is top-of-block: `Win`, `Loss`, `CoverageMiss` (Fynd filled under half the +settled size), `Unsolvable`, or `Sandwiched` (the settled output was moved by MEV; excluded from +savings aggregates). A reverted trade has no settled output to score a verdict against, but each +state still carries a `fillable`/`margin_bps` judgment against the trader's on-chain floor +(`min_amount_out`) — how many reverts a sequencer filling against fresher state could avoid. +Watchdogs rebuild the solver when the tycho feed dies or the monitor falls too far behind chain +head (`--max-lag-blocks`). Each solved state also records which algorithm won the quote and the route it took, rendered as `USDT -[uniswap_v2]-> DAI -[vm:balancer]-> WETH` (a split fans into ` + `-joined legs, each with its diff --git a/tools/hindsight/src/decoder/decode.rs b/tools/hindsight/src/decoder/decode.rs index 46d1e156..cd0aeb82 100644 --- a/tools/hindsight/src/decoder/decode.rs +++ b/tools/hindsight/src/decoder/decode.rs @@ -18,6 +18,7 @@ use alloy::{ network::AnyTransactionReceipt, primitives::{Address, U256}, providers::Provider, + rpc::types::trace::geth::CallFrame, }; use async_trait::async_trait; @@ -129,6 +130,10 @@ pub(crate) struct DecodeContext<'a, P> { /// The transaction's root calldata. Venues declare their solver in it; some solvers embed /// their quote. pub input: &'a [u8], + /// The transaction's root trace frame. A decoder that must find the settling solver's own + /// call (its calldata, its declared output recipient) walks this itself rather than netting + /// the ledger — e.g. a packed calldata layout (Fly) only decodes inside its own frame. + pub root: &'a CallFrame, /// The matched venue's address-book section (entry points, fee collectors, solver aliases), /// set when the transaction entered through a venue so venue decoders never look themselves /// up by name. `None` for direct and intent transactions. @@ -214,7 +219,7 @@ mod tests { use alloy::{providers::RootProvider, rpc::client::RpcClient, transports::mock::Asserter}; use super::*; - use crate::decoder::test_utils::{addr, receipt, swap, tx_hash}; + use crate::decoder::test_utils::{addr, frame, receipt, swap, tx_hash}; /// Always declines. struct Declines; @@ -267,6 +272,7 @@ mod tests { let mut code_cache = HashMap::new(); let receipt = receipt(tx_hash(1), addr(1), Some(addr(2)), vec![]); let transfer_ledger = TransferLedger::from_transaction(&[], &[]); + let root = frame("CALL", addr(1), addr(2), 0); let mut ctx = DecodeContext { provider: &provider, registry: ®istry, @@ -275,6 +281,7 @@ mod tests { entry_point: addr(2), transfer_ledger: &transfer_ledger, input: &[], + root: &root, venue: None, }; try_decoders(decoders, &mut ctx).await diff --git a/tools/hindsight/src/decoder/fixtures/trace_revert_fly_slippage_0xcba01d0c.json b/tools/hindsight/src/decoder/fixtures/trace_revert_fly_slippage_0xcba01d0c.json new file mode 100644 index 00000000..41344431 --- /dev/null +++ b/tools/hindsight/src/decoder/fixtures/trace_revert_fly_slippage_0xcba01d0c.json @@ -0,0 +1 @@ +{"from": "0xf70da97812cb96acdf810712aa562db8dfa3dbef", "gas": "0x11b15b", "gasUsed": "0x50c55", "to": "0xb92fe925dc43a0ecde6c8b1a2709c170ec4fff4f", "input": "0xcd6e13f7000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000030000000000000000000000000020f6ee51340adeed01a59b0e65cb3703f3dc860c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006f05b59d3b200000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000020446ec278a000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001bd018e0040b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f0000000000000000000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda0291300000000000000000000000000000000000000000000000006f05b59d3b20000e00114e00119e0011ea4a9ebda8d3a6595cf15b1d46ea0e440a9810c2b99a3e889c6b3b46f7ff0e5e1f0012300be558cc8c9aae9b6a3b4324923bea142611cbc9ece60da6178ee6f23cf1220f43e7e1a0ab105a2d26a31c60cf4c6bf310d7be4e0e2b91203eb8efad1d2feab3b1c00e06a69e7e3e038652385e038f6f7ebf027104200000000000000000000000000000000000006c006f05b59d3b200000201250e0139013900040a0b0000000000000000000000000000000000000000000000000000000000d05af3107a400003014a0e0139013903016bf513ef4cf80180fffffffffffffffffffffffffffffffff8000c0139013901017d0301810500000301810301830301810301940301810301136a6951db3bb99f76a8ccb9f8a8399b613666298b020070020070070500200500000142014a00000700200172017d0000000000019601b601b606002001ca01cd000003000001cd01d4000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000001a49bb43718000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000001000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda029130000000000000000000000000000000000000000000000000000000000000001000000000000000000000000f70da97812cb96acdf810712aa562db8dfa3dbef0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000210000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "output": "0xe52970aa", "error": "execution reverted", "calls": [{"from": "0xb92fe925dc43a0ecde6c8b1a2709c170ec4fff4f", "gas": "0x10b240", "gasUsed": "0x44ef2", "to": "0x20f6ee51340adeed01a59b0e65cb3703f3dc860c", "input": "0x46ec278a000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001bd018e0040b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f0000000000000000000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda0291300000000000000000000000000000000000000000000000006f05b59d3b20000e00114e00119e0011ea4a9ebda8d3a6595cf15b1d46ea0e440a9810c2b99a3e889c6b3b46f7ff0e5e1f0012300be558cc8c9aae9b6a3b4324923bea142611cbc9ece60da6178ee6f23cf1220f43e7e1a0ab105a2d26a31c60cf4c6bf310d7be4e0e2b91203eb8efad1d2feab3b1c00e06a69e7e3e038652385e038f6f7ebf027104200000000000000000000000000000000000006c006f05b59d3b200000201250e0139013900040a0b0000000000000000000000000000000000000000000000000000000000d05af3107a400003014a0e0139013903016bf513ef4cf80180fffffffffffffffffffffffffffffffff8000c0139013901017d0301810500000301810301830301810301940301810301136a6951db3bb99f76a8ccb9f8a8399b613666298b020070020070070500200500000142014a00000700200172017d0000000000019601b601b606002001ca01cd000003000001cd01d40000000000", "output": "0xe52970aa", "error": "execution reverted", "calls": [{"from": "0x20f6ee51340adeed01a59b0e65cb3703f3dc860c", "gas": "0x105a72", "gasUsed": "0xbb8", "to": "0x0000000000000000000000000000000000000001", "input": "0x3c4eb8a32d205a531db02ad0a29d8ad5c32c2c6d66cbe3afa61218604fca1b0b000000000000000000000000000000000000000000000000000000000000001cbe558cc8c9aae9b6a3b4324923bea142611cbc9ece60da6178ee6f23cf1220f43e7e1a0ab105a2d26a31c60cf4c6bf310d7be4e0e2b91203eb8efad1d2feab3b", "output": "0x00000000000000000000000028de4113921bd79388b0ef80a259f871977442e9", "type": "STATICCALL"}, {"from": "0x20f6ee51340adeed01a59b0e65cb3703f3dc860c", "gas": "0x1039c8", "gasUsed": "0x2616", "to": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "input": "0x70a08231000000000000000000000000b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f", "output": "0x0000000000000000000000000000000000000000000000000000000000000000", "calls": [{"from": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "gas": "0xfdd5d", "gasUsed": "0x9f9", "to": "0x2ce6311ddae708829bc0784c967b7d77d19fd779", "input": "0x70a08231000000000000000000000000b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f", "output": "0x0000000000000000000000000000000000000000000000000000000000000000", "value": "0x0", "type": "DELEGATECALL"}], "type": "STATICCALL"}, {"from": "0x20f6ee51340adeed01a59b0e65cb3703f3dc860c", "gas": "0x101300", "gasUsed": "0xcb2", "to": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "input": "0x70a0823100000000000000000000000020f6ee51340adeed01a59b0e65cb3703f3dc860c", "output": "0x0000000000000000000000000000000000000000000000000000000000101b00", "calls": [{"from": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "gas": "0xfd02f", "gasUsed": "0x9f9", "to": "0x2ce6311ddae708829bc0784c967b7d77d19fd779", "input": "0x70a0823100000000000000000000000020f6ee51340adeed01a59b0e65cb3703f3dc860c", "output": "0x0000000000000000000000000000000000000000000000000000000000101b00", "value": "0x0", "type": "DELEGATECALL"}], "type": "STATICCALL"}, {"from": "0x20f6ee51340adeed01a59b0e65cb3703f3dc860c", "gas": "0xfd97d", "gasUsed": "0x1a2e", "to": "0x4200000000000000000000000000000000000006", "input": "0xd0e30db0", "value": "0x6f05b59d3b20000", "type": "CALL"}, {"from": "0x20f6ee51340adeed01a59b0e65cb3703f3dc860c", "gas": "0xfb4c7", "gasUsed": "0x6145", "to": "0x4200000000000000000000000000000000000006", "input": "0xa9059cbb00000000000000000000000009ad820aac5779683b481c4674208a4e1b024afa00000000000000000000000000000000000000000000000006f05b59d3b20000", "output": "0x0000000000000000000000000000000000000000000000000000000000000001", "value": "0x0", "type": "CALL"}, {"from": "0x20f6ee51340adeed01a59b0e65cb3703f3dc860c", "gas": "0xf4860", "gasUsed": "0x2f655", "to": "0x09ad820aac5779683b481c4674208a4e1b024afa", "input": "0x158f6894000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001bd018e0040b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f4200000000000000000000000000000000000006833589fcd6edb6e08f4c7c32d4f71b54bda0291300000000000000000000000000000000000000000000000006f05b59d3b20000e00114e00119e0011ea4a9ebda8d3a6595cf15b1d46ea0e440a9810c2b99a3e889c6b3b46f7ff0e5e1f0012300be558cc8c9aae9b6a3b4324923bea142611cbc9ece60da6178ee6f23cf1220f43e7e1a0ab105a2d26a31c60cf4c6bf310d7be4e0e2b91203eb8efad1d2feab3b1c00e06a69e7e3e038652385e038f6f7ebf027104200000000000000000000000000000000000006c006f05b59d3b200000201250e0139013900040a0b0000000000000000000000000000000000000000000000000000000000d05af3107a400003014a0e0139013903016bf513ef4cf80180fffffffffffffffffffffffffffffffff8000c0139013901017d0301810500000301810301830301810301940301810301136a6951db3bb99f76a8ccb9f8a8399b613666298b020070020070070500200500000142014a00000700200172017d0000000000019601b601b606002001ca01cd000003000001cd01d40000", "output": "0x0000000000000000000000000000000000000000000000000000000000000000", "calls": [{"from": "0x09ad820aac5779683b481c4674208a4e1b024afa", "gas": "0xef583", "gasUsed": "0x239f", "to": "0x4200000000000000000000000000000000000006", "input": "0x2e1a7d4d00000000000000000000000000000000000000000000000006f05b59d3b20000", "calls": [{"from": "0x4200000000000000000000000000000000000006", "gas": "0x8fc", "gasUsed": "0x37", "to": "0x09ad820aac5779683b481c4674208a4e1b024afa", "input": "0x", "value": "0x6f05b59d3b20000", "type": "CALL"}], "value": "0x0", "type": "CALL"}, {"from": "0x09ad820aac5779683b481c4674208a4e1b024afa", "gas": "0xe96d8", "gasUsed": "0x26657", "to": "0x6a6951db3bb99f76a8ccb9f8a8399b613666298b", "input": "0xf513ef4c00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000001388000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000ffffffffffffffffffffffffffffffff000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000006a69e7e3", "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "calls": [{"from": "0x6a6951db3bb99f76a8ccb9f8a8399b613666298b", "gas": "0xe49f8", "gasUsed": "0x2535d", "to": "0x57dafd63aa581657b0a76db413f371e17a1b0cbe", "input": "0xf513ef4c00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000001388000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000ffffffffffffffffffffffffffffffff000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000006a69e7e3", "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "calls": [{"from": "0x6a6951db3bb99f76a8ccb9f8a8399b613666298b", "gas": "0xde542", "gasUsed": "0xac1", "to": "0x2a24ecb1962cdbaed8f41c587751550bacea063d", "input": "0xb8b5ce81", "value": "0x0", "type": "CALL"}, {"from": "0x6a6951db3bb99f76a8ccb9f8a8399b613666298b", "gas": "0xdd8ce", "gasUsed": "0x41e7", "to": "0x2a24ecb1962cdbaed8f41c587751550bacea063d", "input": "0x24675a0900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000138800000000000000000000000000000000ffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000000000000001", "output": "0x0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "calls": [{"from": "0x2a24ecb1962cdbaed8f41c587751550bacea063d", "gas": "0xd950f", "gasUsed": "0xa4a", "to": "0x4ad6120c42d5e1bf8ec56f37c437da7b8621c582", "input": "0x9ab9bb6c", "output": "0x34046260d00000000000000000000000000000000000fb880fb8902eab32eab6", "type": "STATICCALL"}, {"from": "0x2a24ecb1962cdbaed8f41c587751550bacea063d", "gas": "0xd8939", "gasUsed": "0xa60", "to": "0x4ad6120c42d5e1bf8ec56f37c437da7b8621c582", "input": "0xa1dc9031", "output": "0x000000000000000000000000000000000000000004932c480ebf3a672aca6a8d", "type": "STATICCALL"}], "type": "STATICCALL"}, {"from": "0x6a6951db3bb99f76a8ccb9f8a8399b613666298b", "gas": "0xd87bf", "gasUsed": "0x59d4", "to": "0x7c976801c8589d924ce93bbcce6966d34edf91bf", "input": "0xc3f909d4", "output": "0x00000000000000000000000000000000000000000000000000005af3107a400000000000000000000000000000000000000000000000000000000000000000010000000000000000000000004200000000000000000000000000000000000006000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda0291300000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000030bf17711657bfb5d6645aad39984d5ccb086ec0000000000000000000000003484f248f7b0dfe1f826b145e9d582a6e034ccb400000000000000000000000000000000000000000000000006f05b59d3b2000000000000000000000000000000000000000000000000000000005af3107a4000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "calls": [{"from": "0x7c976801c8589d924ce93bbcce6966d34edf91bf", "gas": "0xd3f45", "gasUsed": "0x46cb", "to": "0xf1466addf635bf0ba0a0c4bb9fecaa03b0945510", "input": "0xc3f909d4", "output": "0x00000000000000000000000000000000000000000000000000005af3107a400000000000000000000000000000000000000000000000000000000000000000010000000000000000000000004200000000000000000000000000000000000006000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda0291300000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000030bf17711657bfb5d6645aad39984d5ccb086ec0000000000000000000000003484f248f7b0dfe1f826b145e9d582a6e034ccb400000000000000000000000000000000000000000000000006f05b59d3b2000000000000000000000000000000000000000000000000000000005af3107a4000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "value": "0x0", "type": "DELEGATECALL"}], "type": "STATICCALL"}, {"from": "0x6a6951db3bb99f76a8ccb9f8a8399b613666298b", "gas": "0xd1e12", "gasUsed": "0xb92", "to": "0x3484f248f7b0dfe1f826b145e9d582a6e034ccb4", "input": "0xe146af470000000000000000000000000000000000000000000000000000010000000001000000000000000000000000000000000000000000000000000000000000138800000000000000000000000000000000ffffffffffffffffffffffffffffffff", "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "type": "STATICCALL"}, {"from": "0x6a6951db3bb99f76a8ccb9f8a8399b613666298b", "gas": "0xd0eae", "gasUsed": "0xf3e", "to": "0x4200000000000000000000000000000000000006", "input": "0xd0e30db0", "value": "0x0", "type": "CALL"}, {"from": "0x6a6951db3bb99f76a8ccb9f8a8399b613666298b", "gas": "0xcfddb", "gasUsed": "0x1193", "to": "0x4200000000000000000000000000000000000006", "input": "0x095ea7b30000000000000000000000007c976801c8589d924ce93bbcce6966d34edf91bf0000000000000000000000000000000000000000000000000000000000000000", "output": "0x0000000000000000000000000000000000000000000000000000000000000001", "value": "0x0", "type": "CALL"}, {"from": "0x6a6951db3bb99f76a8ccb9f8a8399b613666298b", "gas": "0xceac1", "gasUsed": "0xc62", "to": "0x7c976801c8589d924ce93bbcce6966d34edf91bf", "input": "0x506dcce400000000000000000000000009ad820aac5779683b481c4674208a4e1b024afa0000000000000000000000006a6951db3bb99f76a8ccb9f8a8399b613666298b", "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "calls": [{"from": "0x7c976801c8589d924ce93bbcce6966d34edf91bf", "gas": "0xcb603", "gasUsed": "0xb26", "to": "0xf1466addf635bf0ba0a0c4bb9fecaa03b0945510", "input": "0x506dcce400000000000000000000000009ad820aac5779683b481c4674208a4e1b024afa0000000000000000000000006a6951db3bb99f76a8ccb9f8a8399b613666298b", "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "value": "0x0", "type": "DELEGATECALL"}], "type": "STATICCALL"}, {"from": "0x6a6951db3bb99f76a8ccb9f8a8399b613666298b", "gas": "0xcdb7a", "gasUsed": "0x6fbd", "to": "0x7c976801c8589d924ce93bbcce6966d34edf91bf", "input": "0xf513ef4c00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000001388000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000ffffffffffffffffffffffffffffffff000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000006a69e6b9", "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "calls": [{"from": "0x7c976801c8589d924ce93bbcce6966d34edf91bf", "gas": "0xca6d6", "gasUsed": "0x6e57", "to": "0xf1466addf635bf0ba0a0c4bb9fecaa03b0945510", "input": "0xf513ef4c00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000001388000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000ffffffffffffffffffffffffffffffff000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000006a69e6b9", "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "calls": [{"from": "0x7c976801c8589d924ce93bbcce6966d34edf91bf", "gas": "0xc55b3", "gasUsed": "0x31c", "to": "0x3484f248f7b0dfe1f826b145e9d582a6e034ccb4", "input": "0xa558898f00000000000000000000000000000000000000000000000000000100000000010000000000000000000000000000000000000000000000000000000000001388", "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "value": "0x0", "type": "CALL"}, {"from": "0x7c976801c8589d924ce93bbcce6966d34edf91bf", "gas": "0xc2df7", "gasUsed": "0x153", "to": "0x3484f248f7b0dfe1f826b145e9d582a6e034ccb4", "input": "0x2e286683", "output": "0x0000000000000000000000000000000000000000000000000000000000000000", "type": "STATICCALL"}, {"from": "0x7c976801c8589d924ce93bbcce6966d34edf91bf", "gas": "0xc20cf", "gasUsed": "0x923", "to": "0x030bf17711657bfb5d6645aad39984d5ccb086ec", "input": "0x2e286683", "output": "0x0000000000000000000000000000000000000000000000000000000000000000", "type": "STATICCALL"}, {"from": "0x7c976801c8589d924ce93bbcce6966d34edf91bf", "gas": "0xc1537", "gasUsed": "0xb7f", "to": "0x4ad6120c42d5e1bf8ec56f37c437da7b8621c582", "input": "0x558f514800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "value": "0x0", "type": "CALL"}], "value": "0x0", "type": "DELEGATECALL"}], "value": "0x0", "type": "CALL"}, {"from": "0x6a6951db3bb99f76a8ccb9f8a8399b613666298b", "gas": "0xc57b2", "gasUsed": "0x1f25", "to": "0x01dda2b73ede58c840d73379710a18cbc5fe290e", "input": "0x4d35daf300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "calls": [{"from": "0x01dda2b73ede58c840d73379710a18cbc5fe290e", "gas": "0xc09cc", "gasUsed": "0x123", "to": "0x947c13afe52b8676144664e12487544718089267", "input": "0x8d64be7e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "output": "0x0000000000000000000000000000000000000000000000000000000000000000", "type": "STATICCALL"}], "value": "0x0", "type": "CALL"}, {"from": "0x6a6951db3bb99f76a8ccb9f8a8399b613666298b", "gas": "0xc3797", "gasUsed": "0x1c9", "to": "0x4200000000000000000000000000000000000006", "input": "0x70a082310000000000000000000000006a6951db3bb99f76a8ccb9f8a8399b613666298b", "output": "0x0000000000000000000000000000000000000000000000000000000000000000", "type": "STATICCALL"}, {"from": "0x6a6951db3bb99f76a8ccb9f8a8399b613666298b", "gas": "0xc33ff", "gasUsed": "0xcb2", "to": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "input": "0x70a082310000000000000000000000006a6951db3bb99f76a8ccb9f8a8399b613666298b", "output": "0x0000000000000000000000000000000000000000000000000000000000000000", "calls": [{"from": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "gas": "0xc00aa", "gasUsed": "0x9f9", "to": "0x2ce6311ddae708829bc0784c967b7d77d19fd779", "input": "0x70a082310000000000000000000000006a6951db3bb99f76a8ccb9f8a8399b613666298b", "output": "0x0000000000000000000000000000000000000000000000000000000000000000", "value": "0x0", "type": "DELEGATECALL"}], "type": "STATICCALL"}, {"from": "0x6a6951db3bb99f76a8ccb9f8a8399b613666298b", "gas": "0xc0c4d", "gasUsed": "0x37", "to": "0x09ad820aac5779683b481c4674208a4e1b024afa", "input": "0x", "value": "0x6f05b59d3b20000", "type": "CALL"}, {"from": "0x6a6951db3bb99f76a8ccb9f8a8399b613666298b", "gas": "0xc08c1", "gasUsed": "0x4154", "to": "0x2a24ecb1962cdbaed8f41c587751550bacea063d", "input": "0xec3a5314", "calls": [{"from": "0x2a24ecb1962cdbaed8f41c587751550bacea063d", "gas": "0xbcb5c", "gasUsed": "0x901", "to": "0x49b1a6b424c19803fa64a5b40baa7f0428e258b3", "input": "0x18160ddd", "output": "0x000000000000000000000000000000000000000000005987575d2b7d9d3cecb0", "type": "STATICCALL"}, {"from": "0x2a24ecb1962cdbaed8f41c587751550bacea063d", "gas": "0xbc0bc", "gasUsed": "0x27a", "to": "0x4ad6120c42d5e1bf8ec56f37c437da7b8621c582", "input": "0x9ab9bb6c", "output": "0x34046260d00000000000000000000000000000000000fb880fb8902eab32eab6", "type": "STATICCALL"}, {"from": "0x2a24ecb1962cdbaed8f41c587751550bacea063d", "gas": "0xbbc9f", "gasUsed": "0x290", "to": "0x4ad6120c42d5e1bf8ec56f37c437da7b8621c582", "input": "0xa1dc9031", "output": "0x000000000000000000000000000000000000000004932c480ebf3a672aca6a8d", "type": "STATICCALL"}], "value": "0x0", "type": "CALL"}], "value": "0x6f05b59d3b20000", "type": "DELEGATECALL"}], "value": "0x6f05b59d3b20000", "type": "CALL"}, {"from": "0x09ad820aac5779683b481c4674208a4e1b024afa", "gas": "0xc34b7", "gasUsed": "0xcb2", "to": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "input": "0x70a0823100000000000000000000000009ad820aac5779683b481c4674208a4e1b024afa", "output": "0x0000000000000000000000000000000000000000000000000000000000000000", "calls": [{"from": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "gas": "0xc015f", "gasUsed": "0x9f9", "to": "0x2ce6311ddae708829bc0784c967b7d77d19fd779", "input": "0x70a0823100000000000000000000000009ad820aac5779683b481c4674208a4e1b024afa", "output": "0x0000000000000000000000000000000000000000000000000000000000000000", "value": "0x0", "type": "DELEGATECALL"}], "type": "STATICCALL"}], "value": "0x0", "type": "CALL"}, {"from": "0x20f6ee51340adeed01a59b0e65cb3703f3dc860c", "gas": "0xc5c8c", "gasUsed": "0x4e2", "to": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "input": "0x70a0823100000000000000000000000020f6ee51340adeed01a59b0e65cb3703f3dc860c", "output": "0x0000000000000000000000000000000000000000000000000000000000101b00", "calls": [{"from": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "gas": "0xc2895", "gasUsed": "0x229", "to": "0x2ce6311ddae708829bc0784c967b7d77d19fd779", "input": "0x70a0823100000000000000000000000020f6ee51340adeed01a59b0e65cb3703f3dc860c", "output": "0x0000000000000000000000000000000000000000000000000000000000101b00", "value": "0x0", "type": "DELEGATECALL"}], "type": "STATICCALL"}, {"from": "0x20f6ee51340adeed01a59b0e65cb3703f3dc860c", "gas": "0xc557f", "gasUsed": "0x1cc3", "to": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "input": "0xa9059cbb000000000000000000000000b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f0000000000000000000000000000000000000000000000000000000000000000", "output": "0x0000000000000000000000000000000000000000000000000000000000000001", "calls": [{"from": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "gas": "0xc21a1", "gasUsed": "0x1a07", "to": "0x2ce6311ddae708829bc0784c967b7d77d19fd779", "input": "0xa9059cbb000000000000000000000000b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f0000000000000000000000000000000000000000000000000000000000000000", "output": "0x0000000000000000000000000000000000000000000000000000000000000001", "value": "0x0", "type": "DELEGATECALL"}], "value": "0x0", "type": "CALL"}, {"from": "0x20f6ee51340adeed01a59b0e65cb3703f3dc860c", "gas": "0xc376a", "gasUsed": "0x4e2", "to": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "input": "0x70a08231000000000000000000000000b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f", "output": "0x0000000000000000000000000000000000000000000000000000000000000000", "calls": [{"from": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "gas": "0xc0407", "gasUsed": "0x229", "to": "0x2ce6311ddae708829bc0784c967b7d77d19fd779", "input": "0x70a08231000000000000000000000000b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f", "output": "0x0000000000000000000000000000000000000000000000000000000000000000", "value": "0x0", "type": "DELEGATECALL"}], "type": "STATICCALL"}], "value": "0x6f05b59d3b20000", "type": "CALL"}], "value": "0x6f05b59d3b20000", "type": "CALL"} \ No newline at end of file diff --git a/tools/hindsight/src/decoder/fixtures/trace_revert_kyber_slippage_0xd3b7ffae.json b/tools/hindsight/src/decoder/fixtures/trace_revert_kyber_slippage_0xd3b7ffae.json new file mode 100644 index 00000000..5bb23133 --- /dev/null +++ b/tools/hindsight/src/decoder/fixtures/trace_revert_kyber_slippage_0xd3b7ffae.json @@ -0,0 +1 @@ +{"from": "0xad298577cc1040cce487922efc3f21a483226185", "gas": "0x2d075d", "gasUsed": "0x2225b9", "to": "0xccc88a9d1b4ed6b0eaba998850414b24f1c315be", "input": "0xf9e4bab400000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000ad298577cc1040cce487922efc3f21a483226185000000000000000000000000ad298577cc1040cce487922efc3f21a4832261850000000000000000000000000000000000000000000000000000000000001b2000000000000000000000000000000000000000000000000000000000000000010000000000000000000000002ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000033d426dbfc6b100000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000016c000000000000000000000000000000000000000000000000000000000000018400000000000000000000000002ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec220000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000044095ea7b30000000000000000000000006131b5fae19ea4f9d964eac0408e4408b66337b500000000000000000000000000000000000000000000000000033d426dbfc6b1000000000000000000000000000000000000000000000000000000000000000000000000000000006131b5fae19ea4f9d964eac0408e4408b66337b50000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000001484e21fd0e900000000000000000000000000000000000000000000000000000000000000200000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f996000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000114000000000000000000000000000000000000000000000000000000000000013800000000000000000000000000000000000000000000000000000000000001080000000000000000000033d426dbfc6b1000000000000000000033d426dbfc6b1000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000004139dcd51bdd8e38e88f73a8bc3c044f57ba405359be607eabc7614e10bc7ad0ca7d2a4e5d188a5f1a20d056e2ad0bc1c33994d23e82c109bc4808e47dc9ddc38f1c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f80000000000000000000000000b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000001800000000000000000000313cbe842fcc10000000000000000000366b8f33c90a0000000000000000000033d426dbfc6b100000000000000000003b5cdfe7f410a000000000000000000000000000000000000003e3f13f80000000f42400000000000000000000000000000004f82e73edb06d29ff62c91ec8f5ff06571bdeb290000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006a69eb3a0000000000000000000000000000000000000000000000000000000000000f60000000000000000000000000000000000000000000000000000000000000000161f598cd0000000000000000cd568949398283ba8608ed411f3706462e6e0e56000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000002200000000000000000000000000000000000000000000000000000000000000a800000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000d400000000000000000000000002ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec228000000000000000000000003658aa60000000000000000000033d426dbfc6b100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000033d426dbfc6b13b9d6e0900000000000000015455c918e405a2831fbff8595c0aae35ee3db9d100000000000000000000000000000000000000000000000000000000000000800000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f9960000000000000000000000000000000000000000000000000000000000000040000000000000000000000000a8e4c55d6daf4d768aeba2378c1ad94c112ef48a0000000000000000000000000000000000000000000000000000000100ad139d000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda0291380000000000000000000000000000001000000000000000000000000001e2a6e000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000001e2a6e4f2d31ea00000000000100025455c918e405a2831fbff8595c0aae35ee3db9d100000000000000000000000000000000000000000000000000000000000000800000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f9960000000000000000000000000000000000000000000000000000000000000420000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000002226446ebe8170000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001600000000000000000000000000000000000000000000000000000000000000260000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e03b9d6e0900000000000000005455c918e405a2831fbff8595c0aae35ee3db9d139072a4500000000000000001b70046376a3db8b129409f5643ac9474a3be813000000000000000000000000000000000000000000000000000000000000004000000000000000000000000067a11022b7b6ed66f81233f6c8ed6e48f78265300000000000000000000000000000000000000000000000000000000100ad139d0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e03b9d6e0900000000000000005455c918e405a2831fbff8595c0aae35ee3db9d100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000f8593b97ae3ddf084da845cf7ddf591e3d52106d00000000000000000000000000000000000000000000000000000001004a9780000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000120d607af9f0000000000010000f329f5647432ec3195669550fa91c44168b374c60000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000055555522005bcae1c2424d474bfd5ed477749e3e00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000001ca00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000001e2a6e8bc041a80000000000000102d1877a31a73c7cb31c02b9e7d7c336531562b21e00000000000000000000000000000000000000000000000000000000000000800000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f9960000000000000000000000000000000000000000000000000000000000000240000000000000000000000000111111125421ca6dc452d289314280a0f8842a650000000000000000000000000000000000000000000001440000000000000000000000000000000000000000111111125421ca6dc452d289314280a0f8842a65000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000001849fda64bd00000000000000000000000000000000000000000000000000000021904b489e0000000000000000000000007d94baf661d5ed8ad30d7241d1a50f3883083ef70000000000000000000000007d94baf661d5ed8ad30d7241d1a50f3883083ef7000000000000000000000000acfe6019ed1a7dc6f7b508c02d1b04ec88cc21bf000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda0291300000000000000000000000000000000000000000000000002226446ebe8170000000000000000000000000000000000000000000000000000000000001e2a6e00000000000000000000000021904b489e006a69e6a85f87778827f7d113f996cdd5612f0edb4368976605be47033f7cce41ab78c22445ada8432ae1c3ffbf719b8847bf7e61303fe5eb77007bd5fa6350a95e9499484ab42e0b0297edb7fd4100000000000000000000000000000000000000000000000000000000001e2a6e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000acfe6019ed1a7dc6f7b508c02d1b04ec88cc21bf800000000000000000000023d604e36f00000000000000000222d06c17689a600000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000222d06c17689a603b9d6e0900000000000000035455c918e405a2831fbff8595c0aae35ee3db9d100000000000000000000000000000000000000000000000000000000000000800000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f9960000000000000000000000000000000000000000000000000000000000000040000000000000000000000000ed39c843ca9bc596f5db20b8ebb8199345e7d643000000000000000000000000ffb57e36d5b8bf4597abf0bbcfe764cfe8b1211200000000000000000000000042000000000000000000000000000000000000068000000000000000000000003e3f13f800000000000000000003b5cdfe7f410a0000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000003b5cdfe7f410a0000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee80000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f00000000000000000000000000000000000000000000000000033d426dbfc6b10000000000000000000000000000000000000000000000000003ac4e7e8319fc0000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000000010000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f996000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000033d426dbfc6b100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a97b22536f75726365223a2272656c6179222c22416d6f756e74496e555344223a22312e393832333635222c22416d6f756e744f7574555344223a22312e393932373931222c22416d6f756e744f7574223a2231303434333231323732383037363839222c22526f7574654944223a2231383031303631396e6c564b753739443a343166653161636147304a4d7971742d222c2254696d657374616d70223a313738353332353139347d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c4a6bd8c960000000000000000000000000000000000000000000000000000016913e6da47000000000000000000000000f70da97812cb96acdf810712aa562db8dfa3dbef0000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000002119656d0c698d3daae33a49d0d7aed8305f627c573a77f5f5fd71f6098b48a518010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c4a6bd8c960000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ad298577cc1040cce487922efc3f21a4832261850000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000002119656d0c698d3daae33a49d0d7aed8305f627c573a77f5f5fd71f6098b48a518000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002119656d0c698d3daae33a49d0d7aed8305f627c573a77f5f5fd71f6098b48a5180000000000000000000000000000000000000000000000000000000000000000815a84b8906f17df5f5f77a375c726f5038dea7d0d94a33eaad3d896c0d65691", "output": "0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001b52657475726e20616d6f756e74206973206e6f7420656e6f7567680000000000", "error": "execution reverted", "revertReason": "Return amount is not enough", "calls": [{"from": "0xccc88a9d1b4ed6b0eaba998850414b24f1c315be", "gas": "0x2b31fd", "gasUsed": "0xa4d8", "to": "0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22", "input": "0x23b872dd000000000000000000000000ad298577cc1040cce487922efc3f21a483226185000000000000000000000000b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f00000000000000000000000000000000000000000000000000033d426dbfc6b1", "output": "0x0000000000000000000000000000000000000000000000000000000000000001", "calls": [{"from": "0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22", "gas": "0x2a723b", "gasUsed": "0x916a", "to": "0x07a71b9b835c9eba242836704d17da0953324e1f", "input": "0x23b872dd000000000000000000000000ad298577cc1040cce487922efc3f21a483226185000000000000000000000000b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f00000000000000000000000000000000000000000000000000033d426dbfc6b1", "output": "0x0000000000000000000000000000000000000000000000000000000000000001", "value": "0x0", "type": "DELEGATECALL"}], "value": "0x0", "type": "CALL"}, {"from": "0xccc88a9d1b4ed6b0eaba998850414b24f1c315be", "gas": "0x2a6612", "gasUsed": "0x203073", "to": "0xb92fe925dc43a0ecde6c8b1a2709c170ec4fff4f", "input": "0xcd6e13f70000000000000000000000000000000000000000000000000000000000000080000000000000000000000000ad298577cc1040cce487922efc3f21a483226185000000000000000000000000ad298577cc1040cce487922efc3f21a4832261850000000000000000000000000000000000000000000000000000000000001a6000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000016c000000000000000000000000000000000000000000000000000000000000018400000000000000000000000002ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec220000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000044095ea7b30000000000000000000000006131b5fae19ea4f9d964eac0408e4408b66337b500000000000000000000000000000000000000000000000000033d426dbfc6b1000000000000000000000000000000000000000000000000000000000000000000000000000000006131b5fae19ea4f9d964eac0408e4408b66337b50000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000001484e21fd0e900000000000000000000000000000000000000000000000000000000000000200000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f996000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000114000000000000000000000000000000000000000000000000000000000000013800000000000000000000000000000000000000000000000000000000000001080000000000000000000033d426dbfc6b1000000000000000000033d426dbfc6b1000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000004139dcd51bdd8e38e88f73a8bc3c044f57ba405359be607eabc7614e10bc7ad0ca7d2a4e5d188a5f1a20d056e2ad0bc1c33994d23e82c109bc4808e47dc9ddc38f1c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f80000000000000000000000000b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000001800000000000000000000313cbe842fcc10000000000000000000366b8f33c90a0000000000000000000033d426dbfc6b100000000000000000003b5cdfe7f410a000000000000000000000000000000000000003e3f13f80000000f42400000000000000000000000000000004f82e73edb06d29ff62c91ec8f5ff06571bdeb290000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006a69eb3a0000000000000000000000000000000000000000000000000000000000000f60000000000000000000000000000000000000000000000000000000000000000161f598cd0000000000000000cd568949398283ba8608ed411f3706462e6e0e56000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000002200000000000000000000000000000000000000000000000000000000000000a800000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000d400000000000000000000000002ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec228000000000000000000000003658aa60000000000000000000033d426dbfc6b100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000033d426dbfc6b13b9d6e0900000000000000015455c918e405a2831fbff8595c0aae35ee3db9d100000000000000000000000000000000000000000000000000000000000000800000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f9960000000000000000000000000000000000000000000000000000000000000040000000000000000000000000a8e4c55d6daf4d768aeba2378c1ad94c112ef48a0000000000000000000000000000000000000000000000000000000100ad139d000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda0291380000000000000000000000000000001000000000000000000000000001e2a6e000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000001e2a6e4f2d31ea00000000000100025455c918e405a2831fbff8595c0aae35ee3db9d100000000000000000000000000000000000000000000000000000000000000800000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f9960000000000000000000000000000000000000000000000000000000000000420000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000002226446ebe8170000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001600000000000000000000000000000000000000000000000000000000000000260000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e03b9d6e0900000000000000005455c918e405a2831fbff8595c0aae35ee3db9d139072a4500000000000000001b70046376a3db8b129409f5643ac9474a3be813000000000000000000000000000000000000000000000000000000000000004000000000000000000000000067a11022b7b6ed66f81233f6c8ed6e48f78265300000000000000000000000000000000000000000000000000000000100ad139d0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e03b9d6e0900000000000000005455c918e405a2831fbff8595c0aae35ee3db9d100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000f8593b97ae3ddf084da845cf7ddf591e3d52106d00000000000000000000000000000000000000000000000000000001004a9780000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000120d607af9f0000000000010000f329f5647432ec3195669550fa91c44168b374c60000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000055555522005bcae1c2424d474bfd5ed477749e3e00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000001ca00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000001e2a6e8bc041a80000000000000102d1877a31a73c7cb31c02b9e7d7c336531562b21e00000000000000000000000000000000000000000000000000000000000000800000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f9960000000000000000000000000000000000000000000000000000000000000240000000000000000000000000111111125421ca6dc452d289314280a0f8842a650000000000000000000000000000000000000000000001440000000000000000000000000000000000000000111111125421ca6dc452d289314280a0f8842a65000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000001849fda64bd00000000000000000000000000000000000000000000000000000021904b489e0000000000000000000000007d94baf661d5ed8ad30d7241d1a50f3883083ef70000000000000000000000007d94baf661d5ed8ad30d7241d1a50f3883083ef7000000000000000000000000acfe6019ed1a7dc6f7b508c02d1b04ec88cc21bf000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda0291300000000000000000000000000000000000000000000000002226446ebe8170000000000000000000000000000000000000000000000000000000000001e2a6e00000000000000000000000021904b489e006a69e6a85f87778827f7d113f996cdd5612f0edb4368976605be47033f7cce41ab78c22445ada8432ae1c3ffbf719b8847bf7e61303fe5eb77007bd5fa6350a95e9499484ab42e0b0297edb7fd4100000000000000000000000000000000000000000000000000000000001e2a6e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000acfe6019ed1a7dc6f7b508c02d1b04ec88cc21bf800000000000000000000023d604e36f00000000000000000222d06c17689a600000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000222d06c17689a603b9d6e0900000000000000035455c918e405a2831fbff8595c0aae35ee3db9d100000000000000000000000000000000000000000000000000000000000000800000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f9960000000000000000000000000000000000000000000000000000000000000040000000000000000000000000ed39c843ca9bc596f5db20b8ebb8199345e7d643000000000000000000000000ffb57e36d5b8bf4597abf0bbcfe764cfe8b1211200000000000000000000000042000000000000000000000000000000000000068000000000000000000000003e3f13f800000000000000000003b5cdfe7f410a0000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000003b5cdfe7f410a0000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee80000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f00000000000000000000000000000000000000000000000000033d426dbfc6b10000000000000000000000000000000000000000000000000003ac4e7e8319fc0000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000000010000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f996000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000033d426dbfc6b100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a97b22536f75726365223a2272656c6179222c22416d6f756e74496e555344223a22312e393832333635222c22416d6f756e744f7574555344223a22312e393932373931222c22416d6f756e744f7574223a2231303434333231323732383037363839222c22526f7574654944223a2231383031303631396e6c564b753739443a343166653161636147304a4d7971742d222c2254696d657374616d70223a313738353332353139347d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c4a6bd8c960000000000000000000000000000000000000000000000000000016913e6da47000000000000000000000000f70da97812cb96acdf810712aa562db8dfa3dbef0000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000002119656d0c698d3daae33a49d0d7aed8305f627c573a77f5f5fd71f6098b48a518010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c4a6bd8c960000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ad298577cc1040cce487922efc3f21a4832261850000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000002119656d0c698d3daae33a49d0d7aed8305f627c573a77f5f5fd71f6098b48a518000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002119656d0c698d3daae33a49d0d7aed8305f627c573a77f5f5fd71f6098b48a5180000000000000000000000000000000000000000000000000000000000000000", "output": "0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001b52657475726e20616d6f756e74206973206e6f7420656e6f7567680000000000", "error": "execution reverted", "revertReason": "Return amount is not enough", "calls": [{"from": "0xb92fe925dc43a0ecde6c8b1a2709c170ec4fff4f", "gas": "0x295b86", "gasUsed": "0x1f2a", "to": "0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22", "input": "0x095ea7b30000000000000000000000006131b5fae19ea4f9d964eac0408e4408b66337b500000000000000000000000000000000000000000000000000033d426dbfc6b1", "output": "0x0000000000000000000000000000000000000000000000000000000000000001", "calls": [{"from": "0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22", "gas": "0x28b471", "gasUsed": "0x1d56", "to": "0x07a71b9b835c9eba242836704d17da0953324e1f", "input": "0x095ea7b30000000000000000000000006131b5fae19ea4f9d964eac0408e4408b66337b500000000000000000000000000000000000000000000000000033d426dbfc6b1", "output": "0x0000000000000000000000000000000000000000000000000000000000000001", "value": "0x0", "type": "DELEGATECALL"}], "value": "0x0", "type": "CALL"}, {"from": "0xb92fe925dc43a0ecde6c8b1a2709c170ec4fff4f", "gas": "0x291e88", "gasUsed": "0x1f8dfb", "to": "0x6131b5fae19ea4f9d964eac0408e4408b66337b5", "input": "0xe21fd0e900000000000000000000000000000000000000000000000000000000000000200000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f996000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000114000000000000000000000000000000000000000000000000000000000000013800000000000000000000000000000000000000000000000000000000000001080000000000000000000033d426dbfc6b1000000000000000000033d426dbfc6b1000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000004139dcd51bdd8e38e88f73a8bc3c044f57ba405359be607eabc7614e10bc7ad0ca7d2a4e5d188a5f1a20d056e2ad0bc1c33994d23e82c109bc4808e47dc9ddc38f1c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f80000000000000000000000000b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000001800000000000000000000313cbe842fcc10000000000000000000366b8f33c90a0000000000000000000033d426dbfc6b100000000000000000003b5cdfe7f410a000000000000000000000000000000000000003e3f13f80000000f42400000000000000000000000000000004f82e73edb06d29ff62c91ec8f5ff06571bdeb290000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006a69eb3a0000000000000000000000000000000000000000000000000000000000000f60000000000000000000000000000000000000000000000000000000000000000161f598cd0000000000000000cd568949398283ba8608ed411f3706462e6e0e56000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000002200000000000000000000000000000000000000000000000000000000000000a800000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000d400000000000000000000000002ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec228000000000000000000000003658aa60000000000000000000033d426dbfc6b100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000033d426dbfc6b13b9d6e0900000000000000015455c918e405a2831fbff8595c0aae35ee3db9d100000000000000000000000000000000000000000000000000000000000000800000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f9960000000000000000000000000000000000000000000000000000000000000040000000000000000000000000a8e4c55d6daf4d768aeba2378c1ad94c112ef48a0000000000000000000000000000000000000000000000000000000100ad139d000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda0291380000000000000000000000000000001000000000000000000000000001e2a6e000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000001e2a6e4f2d31ea00000000000100025455c918e405a2831fbff8595c0aae35ee3db9d100000000000000000000000000000000000000000000000000000000000000800000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f9960000000000000000000000000000000000000000000000000000000000000420000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000002226446ebe8170000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001600000000000000000000000000000000000000000000000000000000000000260000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e03b9d6e0900000000000000005455c918e405a2831fbff8595c0aae35ee3db9d139072a4500000000000000001b70046376a3db8b129409f5643ac9474a3be813000000000000000000000000000000000000000000000000000000000000004000000000000000000000000067a11022b7b6ed66f81233f6c8ed6e48f78265300000000000000000000000000000000000000000000000000000000100ad139d0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e03b9d6e0900000000000000005455c918e405a2831fbff8595c0aae35ee3db9d100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000f8593b97ae3ddf084da845cf7ddf591e3d52106d00000000000000000000000000000000000000000000000000000001004a9780000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000120d607af9f0000000000010000f329f5647432ec3195669550fa91c44168b374c60000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000055555522005bcae1c2424d474bfd5ed477749e3e00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000001ca00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000001e2a6e8bc041a80000000000000102d1877a31a73c7cb31c02b9e7d7c336531562b21e00000000000000000000000000000000000000000000000000000000000000800000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f9960000000000000000000000000000000000000000000000000000000000000240000000000000000000000000111111125421ca6dc452d289314280a0f8842a650000000000000000000000000000000000000000000001440000000000000000000000000000000000000000111111125421ca6dc452d289314280a0f8842a65000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000001849fda64bd00000000000000000000000000000000000000000000000000000021904b489e0000000000000000000000007d94baf661d5ed8ad30d7241d1a50f3883083ef70000000000000000000000007d94baf661d5ed8ad30d7241d1a50f3883083ef7000000000000000000000000acfe6019ed1a7dc6f7b508c02d1b04ec88cc21bf000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda0291300000000000000000000000000000000000000000000000002226446ebe8170000000000000000000000000000000000000000000000000000000000001e2a6e00000000000000000000000021904b489e006a69e6a85f87778827f7d113f996cdd5612f0edb4368976605be47033f7cce41ab78c22445ada8432ae1c3ffbf719b8847bf7e61303fe5eb77007bd5fa6350a95e9499484ab42e0b0297edb7fd4100000000000000000000000000000000000000000000000000000000001e2a6e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000acfe6019ed1a7dc6f7b508c02d1b04ec88cc21bf800000000000000000000023d604e36f00000000000000000222d06c17689a600000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000222d06c17689a603b9d6e0900000000000000035455c918e405a2831fbff8595c0aae35ee3db9d100000000000000000000000000000000000000000000000000000000000000800000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f9960000000000000000000000000000000000000000000000000000000000000040000000000000000000000000ed39c843ca9bc596f5db20b8ebb8199345e7d643000000000000000000000000ffb57e36d5b8bf4597abf0bbcfe764cfe8b1211200000000000000000000000042000000000000000000000000000000000000068000000000000000000000003e3f13f800000000000000000003b5cdfe7f410a0000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000003b5cdfe7f410a0000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee80000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f00000000000000000000000000000000000000000000000000033d426dbfc6b10000000000000000000000000000000000000000000000000003ac4e7e8319fc0000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000000010000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f996000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000033d426dbfc6b100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a97b22536f75726365223a2272656c6179222c22416d6f756e74496e555344223a22312e393832333635222c22416d6f756e744f7574555344223a22312e393932373931222c22416d6f756e744f7574223a2231303434333231323732383037363839222c22526f7574654944223a2231383031303631396e6c564b753739443a343166653161636147304a4d7971742d222c2254696d657374616d70223a313738353332353139347d0000000000000000000000000000000000000000000000", "output": "0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001b52657475726e20616d6f756e74206973206e6f7420656e6f7567680000000000", "error": "execution reverted", "revertReason": "Return amount is not enough", "calls": [{"from": "0x6131b5fae19ea4f9d964eac0408e4408b66337b5", "gas": "0x28635a", "gasUsed": "0x2af8", "to": "0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22", "input": "0x23b872dd000000000000000000000000b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f0000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f99600000000000000000000000000000000000000000000000000033d426dbfc6b1", "output": "0x0000000000000000000000000000000000000000000000000000000000000001", "calls": [{"from": "0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22", "gas": "0x27c020", "gasUsed": "0x291e", "to": "0x07a71b9b835c9eba242836704d17da0953324e1f", "input": "0x23b872dd000000000000000000000000b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f0000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f99600000000000000000000000000000000000000000000000000033d426dbfc6b1", "output": "0x0000000000000000000000000000000000000000000000000000000000000001", "value": "0x0", "type": "DELEGATECALL"}], "value": "0x0", "type": "CALL"}, {"from": "0x6131b5fae19ea4f9d964eac0408e4408b66337b5", "gas": "0x282eb0", "gasUsed": "0xbe5", "to": "0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22", "input": "0x70a082310000000000000000000000006131b5fae19ea4f9d964eac0408e4408b66337b5", "output": "0x00000000000000000000000000000000000000000000000000000216f52eb703", "calls": [{"from": "0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22", "gas": "0x278c3d", "gasUsed": "0x9ff", "to": "0x07a71b9b835c9eba242836704d17da0953324e1f", "input": "0x70a082310000000000000000000000006131b5fae19ea4f9d964eac0408e4408b66337b5", "output": "0x00000000000000000000000000000000000000000000000000000216f52eb703", "value": "0x0", "type": "DELEGATECALL"}], "type": "STATICCALL"}, {"from": "0x6131b5fae19ea4f9d964eac0408e4408b66337b5", "gas": "0x27f1b7", "gasUsed": "0x1efed5", "to": "0x8f10b468b06c6fd214b65f87778827f7d113f996", "input": "0xd9c4535700000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000001080000000000000000000033d426dbfc6b1000000000000000000033d426dbfc6b1000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000004139dcd51bdd8e38e88f73a8bc3c044f57ba405359be607eabc7614e10bc7ad0ca7d2a4e5d188a5f1a20d056e2ad0bc1c33994d23e82c109bc4808e47dc9ddc38f1c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f80000000000000000000000000b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000001800000000000000000000313cbe842fcc10000000000000000000366b8f33c90a0000000000000000000033d426dbfc6b100000000000000000003b5cdfe7f410a000000000000000000000000000000000000003e3f13f80000000f42400000000000000000000000000000004f82e73edb06d29ff62c91ec8f5ff06571bdeb290000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006a69eb3a0000000000000000000000000000000000000000000000000000000000000f60000000000000000000000000000000000000000000000000000000000000000161f598cd0000000000000000cd568949398283ba8608ed411f3706462e6e0e56000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000002200000000000000000000000000000000000000000000000000000000000000a800000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000d400000000000000000000000002ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec228000000000000000000000003658aa60000000000000000000033d426dbfc6b100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000033d426dbfc6b13b9d6e0900000000000000015455c918e405a2831fbff8595c0aae35ee3db9d100000000000000000000000000000000000000000000000000000000000000800000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f9960000000000000000000000000000000000000000000000000000000000000040000000000000000000000000a8e4c55d6daf4d768aeba2378c1ad94c112ef48a0000000000000000000000000000000000000000000000000000000100ad139d000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda0291380000000000000000000000000000001000000000000000000000000001e2a6e000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000001e2a6e4f2d31ea00000000000100025455c918e405a2831fbff8595c0aae35ee3db9d100000000000000000000000000000000000000000000000000000000000000800000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f9960000000000000000000000000000000000000000000000000000000000000420000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000002226446ebe8170000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001600000000000000000000000000000000000000000000000000000000000000260000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e03b9d6e0900000000000000005455c918e405a2831fbff8595c0aae35ee3db9d139072a4500000000000000001b70046376a3db8b129409f5643ac9474a3be813000000000000000000000000000000000000000000000000000000000000004000000000000000000000000067a11022b7b6ed66f81233f6c8ed6e48f78265300000000000000000000000000000000000000000000000000000000100ad139d0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e03b9d6e0900000000000000005455c918e405a2831fbff8595c0aae35ee3db9d100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000f8593b97ae3ddf084da845cf7ddf591e3d52106d00000000000000000000000000000000000000000000000000000001004a9780000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000120d607af9f0000000000010000f329f5647432ec3195669550fa91c44168b374c60000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000055555522005bcae1c2424d474bfd5ed477749e3e00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000001ca00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000001e2a6e8bc041a80000000000000102d1877a31a73c7cb31c02b9e7d7c336531562b21e00000000000000000000000000000000000000000000000000000000000000800000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f9960000000000000000000000000000000000000000000000000000000000000240000000000000000000000000111111125421ca6dc452d289314280a0f8842a650000000000000000000000000000000000000000000001440000000000000000000000000000000000000000111111125421ca6dc452d289314280a0f8842a65000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000001849fda64bd00000000000000000000000000000000000000000000000000000021904b489e0000000000000000000000007d94baf661d5ed8ad30d7241d1a50f3883083ef70000000000000000000000007d94baf661d5ed8ad30d7241d1a50f3883083ef7000000000000000000000000acfe6019ed1a7dc6f7b508c02d1b04ec88cc21bf000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda0291300000000000000000000000000000000000000000000000002226446ebe8170000000000000000000000000000000000000000000000000000000000001e2a6e00000000000000000000000021904b489e006a69e6a85f87778827f7d113f996cdd5612f0edb4368976605be47033f7cce41ab78c22445ada8432ae1c3ffbf719b8847bf7e61303fe5eb77007bd5fa6350a95e9499484ab42e0b0297edb7fd4100000000000000000000000000000000000000000000000000000000001e2a6e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000acfe6019ed1a7dc6f7b508c02d1b04ec88cc21bf800000000000000000000023d604e36f00000000000000000222d06c17689a600000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000222d06c17689a603b9d6e0900000000000000035455c918e405a2831fbff8595c0aae35ee3db9d100000000000000000000000000000000000000000000000000000000000000800000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f9960000000000000000000000000000000000000000000000000000000000000040000000000000000000000000ed39c843ca9bc596f5db20b8ebb8199345e7d643000000000000000000000000ffb57e36d5b8bf4597abf0bbcfe764cfe8b1211200000000000000000000000042000000000000000000000000000000000000068000000000000000000000003e3f13f800000000000000000003b5cdfe7f410a0000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000003b5cdfe7f410a0000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee8000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "calls": [{"from": "0x8f10b468b06c6fd214b65f87778827f7d113f996", "gas": "0x27367b", "gasUsed": "0x1ee126", "to": "0x440c1a9786d3c6f09da46b6d42320e0cf2f336a7", "input": "0xd9c4535700000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000001080000000000000000000033d426dbfc6b1000000000000000000033d426dbfc6b1000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000004139dcd51bdd8e38e88f73a8bc3c044f57ba405359be607eabc7614e10bc7ad0ca7d2a4e5d188a5f1a20d056e2ad0bc1c33994d23e82c109bc4808e47dc9ddc38f1c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f80000000000000000000000000b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000001800000000000000000000313cbe842fcc10000000000000000000366b8f33c90a0000000000000000000033d426dbfc6b100000000000000000003b5cdfe7f410a000000000000000000000000000000000000003e3f13f80000000f42400000000000000000000000000000004f82e73edb06d29ff62c91ec8f5ff06571bdeb290000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006a69eb3a0000000000000000000000000000000000000000000000000000000000000f60000000000000000000000000000000000000000000000000000000000000000161f598cd0000000000000000cd568949398283ba8608ed411f3706462e6e0e56000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000002200000000000000000000000000000000000000000000000000000000000000a800000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000d400000000000000000000000002ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec228000000000000000000000003658aa60000000000000000000033d426dbfc6b100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000033d426dbfc6b13b9d6e0900000000000000015455c918e405a2831fbff8595c0aae35ee3db9d100000000000000000000000000000000000000000000000000000000000000800000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f9960000000000000000000000000000000000000000000000000000000000000040000000000000000000000000a8e4c55d6daf4d768aeba2378c1ad94c112ef48a0000000000000000000000000000000000000000000000000000000100ad139d000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda0291380000000000000000000000000000001000000000000000000000000001e2a6e000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000001e2a6e4f2d31ea00000000000100025455c918e405a2831fbff8595c0aae35ee3db9d100000000000000000000000000000000000000000000000000000000000000800000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f9960000000000000000000000000000000000000000000000000000000000000420000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000002226446ebe8170000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001600000000000000000000000000000000000000000000000000000000000000260000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e03b9d6e0900000000000000005455c918e405a2831fbff8595c0aae35ee3db9d139072a4500000000000000001b70046376a3db8b129409f5643ac9474a3be813000000000000000000000000000000000000000000000000000000000000004000000000000000000000000067a11022b7b6ed66f81233f6c8ed6e48f78265300000000000000000000000000000000000000000000000000000000100ad139d0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e03b9d6e0900000000000000005455c918e405a2831fbff8595c0aae35ee3db9d100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000f8593b97ae3ddf084da845cf7ddf591e3d52106d00000000000000000000000000000000000000000000000000000001004a9780000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000120d607af9f0000000000010000f329f5647432ec3195669550fa91c44168b374c60000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000055555522005bcae1c2424d474bfd5ed477749e3e00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000001ca00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000001e2a6e8bc041a80000000000000102d1877a31a73c7cb31c02b9e7d7c336531562b21e00000000000000000000000000000000000000000000000000000000000000800000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f9960000000000000000000000000000000000000000000000000000000000000240000000000000000000000000111111125421ca6dc452d289314280a0f8842a650000000000000000000000000000000000000000000001440000000000000000000000000000000000000000111111125421ca6dc452d289314280a0f8842a65000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000001849fda64bd00000000000000000000000000000000000000000000000000000021904b489e0000000000000000000000007d94baf661d5ed8ad30d7241d1a50f3883083ef70000000000000000000000007d94baf661d5ed8ad30d7241d1a50f3883083ef7000000000000000000000000acfe6019ed1a7dc6f7b508c02d1b04ec88cc21bf000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda0291300000000000000000000000000000000000000000000000002226446ebe8170000000000000000000000000000000000000000000000000000000000001e2a6e00000000000000000000000021904b489e006a69e6a85f87778827f7d113f996cdd5612f0edb4368976605be47033f7cce41ab78c22445ada8432ae1c3ffbf719b8847bf7e61303fe5eb77007bd5fa6350a95e9499484ab42e0b0297edb7fd4100000000000000000000000000000000000000000000000000000000001e2a6e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000acfe6019ed1a7dc6f7b508c02d1b04ec88cc21bf800000000000000000000023d604e36f00000000000000000222d06c17689a600000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000222d06c17689a603b9d6e0900000000000000035455c918e405a2831fbff8595c0aae35ee3db9d100000000000000000000000000000000000000000000000000000000000000800000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f9960000000000000000000000000000000000000000000000000000000000000040000000000000000000000000ed39c843ca9bc596f5db20b8ebb8199345e7d643000000000000000000000000ffb57e36d5b8bf4597abf0bbcfe764cfe8b1211200000000000000000000000042000000000000000000000000000000000000068000000000000000000000003e3f13f800000000000000000003b5cdfe7f410a0000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000003b5cdfe7f410a0000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee8000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "calls": [{"from": "0x8f10b468b06c6fd214b65f87778827f7d113f996", "gas": "0x2689e8", "gasUsed": "0xbb8", "to": "0x0000000000000000000000000000000000000001", "input": "0x9ae6f6bf9de10741eeab29670ad5e983b25cdec2ae128caec440404be53b25fc000000000000000000000000000000000000000000000000000000000000001c39dcd51bdd8e38e88f73a8bc3c044f57ba405359be607eabc7614e10bc7ad0ca7d2a4e5d188a5f1a20d056e2ad0bc1c33994d23e82c109bc4808e47dc9ddc38f", "output": "0x000000000000000000000000030016f75e627a42bdf96f31cdf843cb24002109", "type": "STATICCALL"}, {"from": "0x8f10b468b06c6fd214b65f87778827f7d113f996", "gas": "0x26717c", "gasUsed": "0x415", "to": "0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22", "input": "0x70a082310000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f996", "output": "0x00000000000000000000000000000000000000000000000000033d426dbfc6b2", "calls": [{"from": "0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22", "gas": "0x25d5fe", "gasUsed": "0x22f", "to": "0x07a71b9b835c9eba242836704d17da0953324e1f", "input": "0x70a082310000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f996", "output": "0x00000000000000000000000000000000000000000000000000033d426dbfc6b2", "value": "0x0", "type": "DELEGATECALL"}], "type": "STATICCALL"}, {"from": "0x8f10b468b06c6fd214b65f87778827f7d113f996", "gas": "0x26597f", "gasUsed": "0x15275", "to": "0x5455c918e405a2831fbff8595c0aae35ee3db9d1", "input": "0x3b9d6e0900000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000033d426dbfc6b10000000000000000000000002ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda029130000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f9960000000000000000000000000000000000000000000000000000000000000040000000000000000000000000a8e4c55d6daf4d768aeba2378c1ad94c112ef48a0000000000000000000000000000000000000000000000000000000100ad139d", "output": "0x00000000000000000000000000000000000000000000000000000000001e2a55", "calls": [{"from": "0x8f10b468b06c6fd214b65f87778827f7d113f996", "gas": "0x25b1b5", "gasUsed": "0x139f4", "to": "0xa8e4c55d6daf4d768aeba2378c1ad94c112ef48a", "input": "0x128acb080000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f996000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000033d426dbfc6b10000000000000000000000000000000000000000000000000000000100ad139d00000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000200000000000000000000000002ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22", "output": "0x00000000000000000000000000000000000000000000000000033d426dbfc6b1ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe1d5ab", "calls": [{"from": "0xa8e4c55d6daf4d768aeba2378c1ad94c112ef48a", "gas": "0x249862", "gasUsed": "0x5ba7", "to": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "input": "0xa9059cbb0000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f99600000000000000000000000000000000000000000000000000000000001e2a55", "output": "0x0000000000000000000000000000000000000000000000000000000000000001", "calls": [{"from": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "gas": "0x23ea7a", "gasUsed": "0x3f87", "to": "0x2ce6311ddae708829bc0784c967b7d77d19fd779", "input": "0xa9059cbb0000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f99600000000000000000000000000000000000000000000000000000000001e2a55", "output": "0x0000000000000000000000000000000000000000000000000000000000000001", "value": "0x0", "type": "DELEGATECALL"}], "value": "0x0", "type": "CALL"}, {"from": "0xa8e4c55d6daf4d768aeba2378c1ad94c112ef48a", "gas": "0x243ac3", "gasUsed": "0xbe5", "to": "0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22", "input": "0x70a08231000000000000000000000000a8e4c55d6daf4d768aeba2378c1ad94c112ef48a", "output": "0x00000000000000000000000000000000000000000000000014ba7a0ad6404936", "calls": [{"from": "0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22", "gas": "0x23a81f", "gasUsed": "0x9ff", "to": "0x07a71b9b835c9eba242836704d17da0953324e1f", "input": "0x70a08231000000000000000000000000a8e4c55d6daf4d768aeba2378c1ad94c112ef48a", "output": "0x00000000000000000000000000000000000000000000000014ba7a0ad6404936", "value": "0x0", "type": "DELEGATECALL"}], "type": "STATICCALL"}, {"from": "0xa8e4c55d6daf4d768aeba2378c1ad94c112ef48a", "gas": "0x242c16", "gasUsed": "0x3237", "to": "0x8f10b468b06c6fd214b65f87778827f7d113f996", "input": "0xfa461e3300000000000000000000000000000000000000000000000000033d426dbfc6b1ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe1d5ab000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000200000000000000000000000002ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22", "calls": [{"from": "0x8f10b468b06c6fd214b65f87778827f7d113f996", "gas": "0x2395f4", "gasUsed": "0x2ba5", "to": "0x440c1a9786d3c6f09da46b6d42320e0cf2f336a7", "input": "0xfa461e3300000000000000000000000000000000000000000000000000033d426dbfc6b1ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe1d5ab000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000200000000000000000000000002ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22", "calls": [{"from": "0x8f10b468b06c6fd214b65f87778827f7d113f996", "gas": "0x22f9bd", "gasUsed": "0x1d44", "to": "0xcd568949398283ba8608ed411f3706462e6e0e56", "input": "0x61f598cd00000000000000000000000000000000000000000000000000033d426dbfc6b1ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe1d5ab000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000200000000000000000000000002ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22", "calls": [{"from": "0x8f10b468b06c6fd214b65f87778827f7d113f996", "gas": "0x226a4c", "gasUsed": "0x193b", "to": "0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22", "input": "0xa9059cbb000000000000000000000000a8e4c55d6daf4d768aeba2378c1ad94c112ef48a00000000000000000000000000000000000000000000000000033d426dbfc6b1", "output": "0x0000000000000000000000000000000000000000000000000000000000000001", "calls": [{"from": "0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22", "gas": "0x21dee7", "gasUsed": "0x1752", "to": "0x07a71b9b835c9eba242836704d17da0953324e1f", "input": "0xa9059cbb000000000000000000000000a8e4c55d6daf4d768aeba2378c1ad94c112ef48a00000000000000000000000000000000000000000000000000033d426dbfc6b1", "output": "0x0000000000000000000000000000000000000000000000000000000000000001", "value": "0x0", "type": "DELEGATECALL"}], "value": "0x0", "type": "CALL"}], "value": "0x0", "type": "DELEGATECALL"}], "value": "0x0", "type": "DELEGATECALL"}], "value": "0x0", "type": "CALL"}, {"from": "0xa8e4c55d6daf4d768aeba2378c1ad94c112ef48a", "gas": "0x23f82f", "gasUsed": "0x415", "to": "0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22", "input": "0x70a08231000000000000000000000000a8e4c55d6daf4d768aeba2378c1ad94c112ef48a", "output": "0x00000000000000000000000000000000000000000000000014bdb74d44000fe7", "calls": [{"from": "0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22", "gas": "0x236696", "gasUsed": "0x22f", "to": "0x07a71b9b835c9eba242836704d17da0953324e1f", "input": "0x70a08231000000000000000000000000a8e4c55d6daf4d768aeba2378c1ad94c112ef48a", "output": "0x00000000000000000000000000000000000000000000000014bdb74d44000fe7", "value": "0x0", "type": "DELEGATECALL"}], "type": "STATICCALL"}], "value": "0x0", "type": "CALL"}], "value": "0x0", "type": "DELEGATECALL"}, {"from": "0x8f10b468b06c6fd214b65f87778827f7d113f996", "gas": "0x24feea", "gasUsed": "0x4e2", "to": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "input": "0x70a082310000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f996", "output": "0x00000000000000000000000000000000000000000000000000000000001e2a56", "calls": [{"from": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "gas": "0x246869", "gasUsed": "0x229", "to": "0x2ce6311ddae708829bc0784c967b7d77d19fd779", "input": "0x70a082310000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f996", "output": "0x00000000000000000000000000000000000000000000000000000000001e2a56", "value": "0x0", "type": "DELEGATECALL"}], "type": "STATICCALL"}, {"from": "0x8f10b468b06c6fd214b65f87778827f7d113f996", "gas": "0x24e1a6", "gasUsed": "0x7415", "to": "0x1b70046376a3db8b129409f5643ac9474a3be813", "input": "0x39072a4500000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000001e2a55000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda02913000000000000000000000000acfe6019ed1a7dc6f7b508c02d1b04ec88cc21bf0000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f996000000000000000000000000000000000000000000000000000000000000004000000000000000000000000067a11022b7b6ed66f81233f6c8ed6e48f78265300000000000000000000000000000000000000000000000000000000100ad139d0000000000000000000000000000000000000000000000000000000000000000", "output": "0x0000000000000000000000000000000000000000000000000223ff9e9e0e4b96", "calls": [{"from": "0x1b70046376a3db8b129409f5643ac9474a3be813", "gas": "0x243ff2", "gasUsed": "0xa88", "to": "0x67a11022b7b6ed66f81233f6c8ed6e48f7826530", "input": "0x3850c7bd", "output": "0x00000000000000000000000000000000000444c4987f6fc374f1f75d6d1e1219000000000000000000000000000000000000000000000000000000000003d3dd000000000000000000000000000000000000000000000000000000000000013a0000000000000000000000000000000000000000000000000000000000000168000000000000000000000000000000000000000000000000000000000000016800000000000000000000000000000000000000000000000000000000000000660000000000000000000000000000000000000000000000000000000000000001", "type": "STATICCALL"}, {"from": "0x1b70046376a3db8b129409f5643ac9474a3be813", "gas": "0x243287", "gasUsed": "0x97c", "to": "0x67a11022b7b6ed66f81233f6c8ed6e48f7826530", "input": "0x1a686502", "output": "0x00000000000000000000000000000000000000000000000010dfca188c042f37", "type": "STATICCALL"}, {"from": "0x1b70046376a3db8b129409f5643ac9474a3be813", "gas": "0x242790", "gasUsed": "0x117", "to": "0x67a11022b7b6ed66f81233f6c8ed6e48f7826530", "input": "0xd0c93a7c", "output": "0x000000000000000000000000000000000000000000000000000000000000003c", "type": "STATICCALL"}, {"from": "0x1b70046376a3db8b129409f5643ac9474a3be813", "gas": "0x2424d3", "gasUsed": "0xfb", "to": "0x67a11022b7b6ed66f81233f6c8ed6e48f7826530", "input": "0xddca3f43", "output": "0x0000000000000000000000000000000000000000000000000000000000000bb8", "type": "STATICCALL"}, {"from": "0x1b70046376a3db8b129409f5643ac9474a3be813", "gas": "0x241d62", "gasUsed": "0x9dd", "to": "0x67a11022b7b6ed66f81233f6c8ed6e48f7826530", "input": "0x5339c2960000000000000000000000000000000000000000000000000000000000000010", "output": "0x0012810034000001c0001c0c523f9be945e57fea4dfa7f7eb6edfe379f30dcc0", "type": "STATICCALL"}], "value": "0x0", "type": "CALL"}, {"from": "0x8f10b468b06c6fd214b65f87778827f7d113f996", "gas": "0x246824", "gasUsed": "0x217ae", "to": "0x8f10b468b06c6fd214b65f87778827f7d113f996", "input": "0x049fd03500000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000223ff9e9e0e4b960000000000000000000000000000000000000000000000000222628218c862c8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001e2a55000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda02913000000000000000000000000acfe6019ed1a7dc6f7b508c02d1b04ec88cc21bf0000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f996000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e03b9d6e0900000000000000005455c918e405a2831fbff8595c0aae35ee3db9d100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000f8593b97ae3ddf084da845cf7ddf591e3d52106d00000000000000000000000000000000000000000000000000000001004a97800000000000000000000000000000000000000000000000000000000000000000", "output": "0x00000000000000000000000000000000000000000000000002204d54b65533fe", "error": "execution reverted", "calls": [{"from": "0x8f10b468b06c6fd214b65f87778827f7d113f996", "gas": "0x23d0d0", "gasUsed": "0x21183", "to": "0x440c1a9786d3c6f09da46b6d42320e0cf2f336a7", "input": "0x049fd03500000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000223ff9e9e0e4b960000000000000000000000000000000000000000000000000222628218c862c8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001e2a55000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda02913000000000000000000000000acfe6019ed1a7dc6f7b508c02d1b04ec88cc21bf0000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f996000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e03b9d6e0900000000000000005455c918e405a2831fbff8595c0aae35ee3db9d100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000f8593b97ae3ddf084da845cf7ddf591e3d52106d00000000000000000000000000000000000000000000000000000001004a97800000000000000000000000000000000000000000000000000000000000000000", "output": "0x00000000000000000000000000000000000000000000000002204d54b65533fe", "error": "execution reverted", "calls": [{"from": "0x8f10b468b06c6fd214b65f87778827f7d113f996", "gas": "0x233d07", "gasUsed": "0x20c90", "to": "0x5455c918e405a2831fbff8595c0aae35ee3db9d1", "input": "0x3b9d6e0900000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000001e2a55000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda02913000000000000000000000000acfe6019ed1a7dc6f7b508c02d1b04ec88cc21bf0000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f9960000000000000000000000000000000000000000000000000000000000000040000000000000000000000000f8593b97ae3ddf084da845cf7ddf591e3d52106d00000000000000000000000000000000000000000000000000000001004a9780", "output": "0x00000000000000000000000000000000000000000000000002204d54b65533fe", "calls": [{"from": "0x8f10b468b06c6fd214b65f87778827f7d113f996", "gas": "0x22a1af", "gasUsed": "0x1f40f", "to": "0xf8593b97ae3ddf084da845cf7ddf591e3d52106d", "input": "0x128acb080000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f996000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000001e2a5500000000000000000000000000000000000000000000000000000001004a978000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda02913", "output": "0x00000000000000000000000000000000000000000000000000000000001e2a55fffffffffffffffffffffffffffffffffffffffffffffffffddfb2ab49aacc02", "calls": [{"from": "0xf8593b97ae3ddf084da845cf7ddf591e3d52106d", "gas": "0x220ce3", "gasUsed": "0x1e978", "to": "0xc770898522d2a9c8da7a10d63989b6b58305b665", "input": "0x128acb080000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f996000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000001e2a5500000000000000000000000000000000000000000000000000000001004a978000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda02913", "output": "0x00000000000000000000000000000000000000000000000000000000001e2a55fffffffffffffffffffffffffffffffffffffffffffffffffddfb2ab49aacc02", "calls": [{"from": "0xf8593b97ae3ddf084da845cf7ddf591e3d52106d", "gas": "0x214845", "gasUsed": "0x490e", "to": "0xf8f2eb4940cfe7d13603dddd87f123820fc061ef", "input": "0x35458dcc000000000000000000000000f8593b97ae3ddf084da845cf7ddf591e3d52106d", "output": "0x0000000000000000000000000000000000000000000000000000000000000a8c", "calls": [{"from": "0xf8f2eb4940cfe7d13603dddd87f123820fc061ef", "gas": "0x30d40", "gasUsed": "0x31f0", "to": "0x87d8f999bba9343e8099552426775b51c338e8cb", "input": "0xb88c9148000000000000000000000000f8593b97ae3ddf084da845cf7ddf591e3d52106d", "output": "0x0000000000000000000000000000000000000000000000000000000000000a8c", "calls": [{"from": "0x87d8f999bba9343e8099552426775b51c338e8cb", "gas": "0x2e4f7", "gasUsed": "0x34d", "to": "0xf8593b97ae3ddf084da845cf7ddf591e3d52106d", "input": "0x3850c7bd", "output": "0x000000000000000000000000000000000004412dc5f9055970615862cf483b04000000000000000000000000000000000000000000000000000000000003d39b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000", "calls": [{"from": "0xf8593b97ae3ddf084da845cf7ddf591e3d52106d", "gas": "0x2d8e4", "gasUsed": "0x289", "to": "0xc770898522d2a9c8da7a10d63989b6b58305b665", "input": "0x3850c7bd", "output": "0x000000000000000000000000000000000004412dc5f9055970615862cf483b04000000000000000000000000000000000000000000000000000000000003d39b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000", "value": "0x0", "type": "DELEGATECALL"}], "type": "STATICCALL"}], "type": "STATICCALL"}], "type": "STATICCALL"}, {"from": "0xf8593b97ae3ddf084da845cf7ddf591e3d52106d", "gas": "0x20d1f0", "gasUsed": "0x434d", "to": "0xf8f2eb4940cfe7d13603dddd87f123820fc061ef", "input": "0x48cf7a43000000000000000000000000f8593b97ae3ddf084da845cf7ddf591e3d52106d", "output": "0x00000000000000000000000000000000000000000000000000000000000186a0", "calls": [{"from": "0xf8f2eb4940cfe7d13603dddd87f123820fc061ef", "gas": "0x20429c", "gasUsed": "0xa72", "to": "0x16613524e02ad97edfef371bc883f2f5d6c480a5", "input": "0xb9a09fd5000000000000000000000000f8593b97ae3ddf084da845cf7ddf591e3d52106d", "output": "0x000000000000000000000000b5d2766888a0f33175f1be83f94e1c9d62569196", "type": "STATICCALL"}, {"from": "0xf8f2eb4940cfe7d13603dddd87f123820fc061ef", "gas": "0x20369f", "gasUsed": "0xa7a", "to": "0x16613524e02ad97edfef371bc883f2f5d6c480a5", "input": "0x1703e5f9000000000000000000000000b5d2766888a0f33175f1be83f94e1c9d62569196", "output": "0x0000000000000000000000000000000000000000000000000000000000000001", "type": "STATICCALL"}, {"from": "0xf8f2eb4940cfe7d13603dddd87f123820fc061ef", "gas": "0x30d40", "gasUsed": "0xa1b", "to": "0xc2cc3256434afbc36bb5e815e1bb2151310a1a0b", "input": "0xb88c9148000000000000000000000000f8593b97ae3ddf084da845cf7ddf591e3d52106d", "output": "0x00000000000000000000000000000000000000000000000000000000000186a0", "type": "STATICCALL"}], "type": "STATICCALL"}, {"from": "0xf8593b97ae3ddf084da845cf7ddf591e3d52106d", "gas": "0x203221", "gasUsed": "0x30c4", "to": "0xacfe6019ed1a7dc6f7b508c02d1b04ec88cc21bf", "input": "0xa9059cbb0000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f99600000000000000000000000000000000000000000000000002204d54b65533fe", "output": "0x0000000000000000000000000000000000000000000000000000000000000001", "value": "0x0", "type": "CALL"}, {"from": "0xf8593b97ae3ddf084da845cf7ddf591e3d52106d", "gas": "0x1ff6a6", "gasUsed": "0xcb2", "to": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "input": "0x70a08231000000000000000000000000f8593b97ae3ddf084da845cf7ddf591e3d52106d", "output": "0x0000000000000000000000000000000000000000000000000000000022952101", "calls": [{"from": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "gas": "0x1f7446", "gasUsed": "0x9f9", "to": "0x2ce6311ddae708829bc0784c967b7d77d19fd779", "input": "0x70a08231000000000000000000000000f8593b97ae3ddf084da845cf7ddf591e3d52106d", "output": "0x0000000000000000000000000000000000000000000000000000000022952101", "value": "0x0", "type": "DELEGATECALL"}], "type": "STATICCALL"}, {"from": "0xf8593b97ae3ddf084da845cf7ddf591e3d52106d", "gas": "0x1fe72f", "gasUsed": "0x2f1b", "to": "0x8f10b468b06c6fd214b65f87778827f7d113f996", "input": "0xfa461e3300000000000000000000000000000000000000000000000000000000001e2a55fffffffffffffffffffffffffffffffffffffffffffffffffddfb2ab49aacc0200000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda02913", "calls": [{"from": "0x8f10b468b06c6fd214b65f87778827f7d113f996", "gas": "0x1f6221", "gasUsed": "0x2889", "to": "0x440c1a9786d3c6f09da46b6d42320e0cf2f336a7", "input": "0xfa461e3300000000000000000000000000000000000000000000000000000000001e2a55fffffffffffffffffffffffffffffffffffffffffffffffffddfb2ab49aacc0200000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda02913", "calls": [{"from": "0x8f10b468b06c6fd214b65f87778827f7d113f996", "gas": "0x1ee056", "gasUsed": "0x23ec", "to": "0xcd568949398283ba8608ed411f3706462e6e0e56", "input": "0x61f598cd00000000000000000000000000000000000000000000000000000000001e2a55fffffffffffffffffffffffffffffffffffffffffffffffffddfb2ab49aacc0200000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda02913", "calls": [{"from": "0x8f10b468b06c6fd214b65f87778827f7d113f996", "gas": "0x1e614b", "gasUsed": "0x1fe3", "to": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "input": "0xa9059cbb000000000000000000000000f8593b97ae3ddf084da845cf7ddf591e3d52106d00000000000000000000000000000000000000000000000000000000001e2a55", "output": "0x0000000000000000000000000000000000000000000000000000000000000001", "calls": [{"from": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "gas": "0x1de53e", "gasUsed": "0x1d27", "to": "0x2ce6311ddae708829bc0784c967b7d77d19fd779", "input": "0xa9059cbb000000000000000000000000f8593b97ae3ddf084da845cf7ddf591e3d52106d00000000000000000000000000000000000000000000000000000000001e2a55", "output": "0x0000000000000000000000000000000000000000000000000000000000000001", "value": "0x0", "type": "DELEGATECALL"}], "value": "0x0", "type": "CALL"}], "value": "0x0", "type": "DELEGATECALL"}], "value": "0x0", "type": "DELEGATECALL"}], "value": "0x0", "type": "CALL"}, {"from": "0xf8593b97ae3ddf084da845cf7ddf591e3d52106d", "gas": "0x1fb5f5", "gasUsed": "0x4e2", "to": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "input": "0x70a08231000000000000000000000000f8593b97ae3ddf084da845cf7ddf591e3d52106d", "output": "0x0000000000000000000000000000000000000000000000000000000022b34b56", "calls": [{"from": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "gas": "0x1f3498", "gasUsed": "0x229", "to": "0x2ce6311ddae708829bc0784c967b7d77d19fd779", "input": "0x70a08231000000000000000000000000f8593b97ae3ddf084da845cf7ddf591e3d52106d", "output": "0x0000000000000000000000000000000000000000000000000000000022b34b56", "value": "0x0", "type": "DELEGATECALL"}], "type": "STATICCALL"}], "value": "0x0", "type": "DELEGATECALL"}], "value": "0x0", "type": "CALL"}], "value": "0x0", "type": "DELEGATECALL"}], "value": "0x0", "type": "DELEGATECALL"}], "value": "0x0", "type": "CALL"}, {"from": "0x8f10b468b06c6fd214b65f87778827f7d113f996", "gas": "0x2251be", "gasUsed": "0x17501a", "to": "0x8f10b468b06c6fd214b65f87778827f7d113f996", "input": "0x049fd03500000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000223ff9e9e0e4b960000000000000000000000000000000000000000000000000222628218c862c8000000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000001e2a55000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda02913000000000000000000000000acfe6019ed1a7dc6f7b508c02d1b04ec88cc21bf0000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f99600000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000120d607af9f0000000000010000f329f5647432ec3195669550fa91c44168b374c60000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000055555522005bcae1c2424d474bfd5ed477749e3e00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000001ca000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "output": "0x000000000000000000000000000000000000000000000000022378aef095ef4a", "error": "execution reverted", "calls": [{"from": "0x8f10b468b06c6fd214b65f87778827f7d113f996", "gas": "0x21c2b8", "gasUsed": "0x1749e3", "to": "0x440c1a9786d3c6f09da46b6d42320e0cf2f336a7", "input": "0x049fd03500000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000223ff9e9e0e4b960000000000000000000000000000000000000000000000000222628218c862c8000000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000001e2a55000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda02913000000000000000000000000acfe6019ed1a7dc6f7b508c02d1b04ec88cc21bf0000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f99600000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000120d607af9f0000000000010000f329f5647432ec3195669550fa91c44168b374c60000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000055555522005bcae1c2424d474bfd5ed477749e3e00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000001ca000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "output": "0x000000000000000000000000000000000000000000000000022378aef095ef4a", "error": "execution reverted", "calls": [{"from": "0x8f10b468b06c6fd214b65f87778827f7d113f996", "gas": "0x212d7d", "gasUsed": "0x173b1f", "to": "0xf329f5647432ec3195669550fa91c44168b374c6", "input": "0xd607af9f00000000000000000000000000000000000000000000000000000000000000a080000000000000000000000000000000000000000000000000000000001e2a55000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda02913000000000000000000000000acfe6019ed1a7dc6f7b508c02d1b04ec88cc21bf0000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f996000000000000000000000000000000000000000000000000000000000000008000000000000000000000000055555522005bcae1c2424d474bfd5ed477749e3e00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000001ca00000000000000000000000000000000000000000000000000000000000000", "output": "0x000000000000000000000000000000000000000000000000022378aef095ef4a", "calls": [{"from": "0x8f10b468b06c6fd214b65f87778827f7d113f996", "gas": "0x20a419", "gasUsed": "0x2031", "to": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "input": "0x095ea7b300000000000000000000000055555522005bcae1c2424d474bfd5ed477749e3effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "output": "0x0000000000000000000000000000000000000000000000000000000000000001", "calls": [{"from": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "gas": "0x201f20", "gasUsed": "0x1d95", "to": "0x2ce6311ddae708829bc0784c967b7d77d19fd779", "input": "0x095ea7b300000000000000000000000055555522005bcae1c2424d474bfd5ed477749e3effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "output": "0x0000000000000000000000000000000000000000000000000000000000000001", "value": "0x0", "type": "DELEGATECALL"}], "value": "0x0", "type": "CALL"}, {"from": "0x8f10b468b06c6fd214b65f87778827f7d113f996", "gas": "0x2082f5", "gasUsed": "0x4e2", "to": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "input": "0x70a082310000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f996", "output": "0x00000000000000000000000000000000000000000000000000000000001e2a56", "calls": [{"from": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "gas": "0x1ffe64", "gasUsed": "0x229", "to": "0x2ce6311ddae708829bc0784c967b7d77d19fd779", "input": "0x70a082310000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f996", "output": "0x00000000000000000000000000000000000000000000000000000000001e2a56", "value": "0x0", "type": "DELEGATECALL"}], "type": "STATICCALL"}, {"from": "0x8f10b468b06c6fd214b65f87778827f7d113f996", "gas": "0x20729f", "gasUsed": "0x9fb", "to": "0xacfe6019ed1a7dc6f7b508c02d1b04ec88cc21bf", "input": "0x70a082310000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f996", "output": "0x0000000000000000000000000000000000000000000000000000000000000001", "type": "STATICCALL"}, {"from": "0x8f10b468b06c6fd214b65f87778827f7d113f996", "gas": "0x205c44", "gasUsed": "0x16d8c7", "to": "0x55555522005bcae1c2424d474bfd5ed477749e3e", "input": "0x3ae8b298000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda02913000000000000000000000000acfe6019ed1a7dc6f7b508c02d1b04ec88cc21bf00000000000000000000000000000000000000000000000000000000001e2a5500000000000000000000000000000000000000000000000000000000000000000000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f99600000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000001ca00000000000000000000000000000000000000000000000000000000000000", "calls": [{"from": "0x55555522005bcae1c2424d474bfd5ed477749e3e", "gas": "0x1fc560", "gasUsed": "0x1648f7", "to": "0x31e99e05fee3dce580af777c3fd63ee1b3b40c17", "input": "0xe70c3a4d000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda02913000000000000000000000000acfe6019ed1a7dc6f7b508c02d1b04ec88cc21bf00000000000000000000000000000000000000000000000000000000001e2a550000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f99600000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000001ca00000000000000000000000000000000000000000000000000000000000000", "output": "0x00000000000000000000000000000000000000000000000000000000001e2a55000000000000000000000000000000000000000000000000022378aef095ef4a", "calls": [{"from": "0x31e99e05fee3dce580af777c3fd63ee1b3b40c17", "gas": "0x1f1ab0", "gasUsed": "0x1fe1", "to": "0x402e0d314fd6f55348df7cc478bab811826e3e91", "input": "0xc55dae63", "output": "0x000000000000000000000000acfe6019ed1a7dc6f7b508c02d1b04ec88cc21bf", "calls": [{"from": "0x402e0d314fd6f55348df7cc478bab811826e3e91", "gas": "0x1e89bc", "gasUsed": "0xae3", "to": "0xffeeb84898a2cd25701e3cfe766f1709821742c0", "input": "0xc55dae63", "output": "0x000000000000000000000000acfe6019ed1a7dc6f7b508c02d1b04ec88cc21bf", "value": "0x0", "type": "DELEGATECALL"}], "type": "STATICCALL"}, {"from": "0x31e99e05fee3dce580af777c3fd63ee1b3b40c17", "gas": "0x1ef98b", "gasUsed": "0xcab", "to": "0x402e0d314fd6f55348df7cc478bab811826e3e91", "input": "0x217a4b70", "output": "0x000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda02913", "calls": [{"from": "0x402e0d314fd6f55348df7cc478bab811826e3e91", "gas": "0x1e7a69", "gasUsed": "0x941", "to": "0xffeeb84898a2cd25701e3cfe766f1709821742c0", "input": "0x217a4b70", "output": "0x000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda02913", "value": "0x0", "type": "DELEGATECALL"}], "type": "STATICCALL"}, {"from": "0x31e99e05fee3dce580af777c3fd63ee1b3b40c17", "gas": "0x1ee9b4", "gasUsed": "0x15e920", "to": "0x402e0d314fd6f55348df7cc478bab811826e3e91", "input": "0x4c7c47b7000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001e2a550000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f99600000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001ca00000000000000000000000000000000000000000000000000000000000000", "output": "0x000000000000000000000000000000000000000000000000022378aef095ef4a", "calls": [{"from": "0x402e0d314fd6f55348df7cc478bab811826e3e91", "gas": "0x1e6ab4", "gasUsed": "0x15e598", "to": "0xffeeb84898a2cd25701e3cfe766f1709821742c0", "input": "0x4c7c47b7000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001e2a550000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f99600000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001ca00000000000000000000000000000000000000000000000000000000000000", "output": "0x000000000000000000000000000000000000000000000000022378aef095ef4a", "calls": [{"from": "0x402e0d314fd6f55348df7cc478bab811826e3e91", "gas": "0x1ddd01", "gasUsed": "0x196", "to": "0x31e99e05fee3dce580af777c3fd63ee1b3b40c17", "input": "0x22f3e2d4", "output": "0x0000000000000000000000000000000000000000000000000000000000000001", "type": "STATICCALL"}, {"from": "0x402e0d314fd6f55348df7cc478bab811826e3e91", "gas": "0x1db9c1", "gasUsed": "0x196", "to": "0x31e99e05fee3dce580af777c3fd63ee1b3b40c17", "input": "0x22f3e2d4", "output": "0x0000000000000000000000000000000000000000000000000000000000000001", "type": "STATICCALL"}, {"from": "0x402e0d314fd6f55348df7cc478bab811826e3e91", "gas": "0x1cd126", "gasUsed": "0xa47", "to": "0x31e99e05fee3dce580af777c3fd63ee1b3b40c17", "input": "0x8c4cdb49", "output": "0x0000000000000000000000000000000000000000000000000000000002efbfb7", "type": "STATICCALL"}, {"from": "0x402e0d314fd6f55348df7cc478bab811826e3e91", "gas": "0x1cac43", "gasUsed": "0x1b29", "to": "0x7034c5c74f66d3337777772c2964db31765db23e", "input": "0x4a89bec000000000000000000000000000000000000000000000000000000000000000ca", "output": "0x00000000000000000000000000000000000000000000000000000000000000c8", "type": "STATICCALL"}, {"from": "0x402e0d314fd6f55348df7cc478bab811826e3e91", "gas": "0x1c8482", "gasUsed": "0xa9c", "to": "0x31e99e05fee3dce580af777c3fd63ee1b3b40c17", "input": "0x8547d9720000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f996", "output": "0x0000000000000000000000000000000000000000000000000000000000000001", "type": "STATICCALL"}, {"from": "0x402e0d314fd6f55348df7cc478bab811826e3e91", "gas": "0x1c7863", "gasUsed": "0xa9c", "to": "0x31e99e05fee3dce580af777c3fd63ee1b3b40c17", "input": "0x8547d972000000000000000000000000ad298577cc1040cce487922efc3f21a483226185", "output": "0x0000000000000000000000000000000000000000000000000000000000000000", "type": "STATICCALL"}, {"from": "0x402e0d314fd6f55348df7cc478bab811826e3e91", "gas": "0x1c6a9c", "gasUsed": "0x2cc", "to": "0x31e99e05fee3dce580af777c3fd63ee1b3b40c17", "input": "0x8547d9720000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f996", "output": "0x0000000000000000000000000000000000000000000000000000000000000001", "type": "STATICCALL"}, {"from": "0x402e0d314fd6f55348df7cc478bab811826e3e91", "gas": "0x1c6620", "gasUsed": "0x2cc", "to": "0x31e99e05fee3dce580af777c3fd63ee1b3b40c17", "input": "0x8547d972000000000000000000000000ad298577cc1040cce487922efc3f21a483226185", "output": "0x0000000000000000000000000000000000000000000000000000000000000000", "type": "STATICCALL"}, {"from": "0x402e0d314fd6f55348df7cc478bab811826e3e91", "gas": "0x1c4618", "gasUsed": "0x10fb69", "to": "0x9f924c0765815851d9f4982c7030e2189a7828a9", "input": "0x1ed58fc8000000000000000000000000ad298577cc1040cce487922efc3f21a4832261850000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f996", "output": "0x0000000000000000000000000000000000000000000000000000000000000000", "type": "STATICCALL"}, {"from": "0x402e0d314fd6f55348df7cc478bab811826e3e91", "gas": "0xb8c17", "gasUsed": "0xb7b", "to": "0x31e99e05fee3dce580af777c3fd63ee1b3b40c17", "input": "0xeb101276", "output": "0x0000000000000000000000000000000000000000000000000000000077359400", "type": "STATICCALL"}], "value": "0x0", "type": "DELEGATECALL"}], "value": "0x0", "type": "CALL"}], "value": "0x0", "type": "CALL"}, {"from": "0x55555522005bcae1c2424d474bfd5ed477749e3e", "gas": "0x9cb2a", "gasUsed": "0x3188", "to": "0xacfe6019ed1a7dc6f7b508c02d1b04ec88cc21bf", "input": "0x23b872dd0000000000000000000000003dbe077e7986657e95e1cc50089f17a5a4af0aae0000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f996000000000000000000000000000000000000000000000000022378aef095ef4a", "output": "0x0000000000000000000000000000000000000000000000000000000000000001", "value": "0x0", "type": "CALL"}, {"from": "0x55555522005bcae1c2424d474bfd5ed477749e3e", "gas": "0x998e4", "gasUsed": "0x33ac", "to": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "input": "0x23b872dd0000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f9960000000000000000000000003dbe077e7986657e95e1cc50089f17a5a4af0aae00000000000000000000000000000000000000000000000000000000001e2a55", "output": "0x0000000000000000000000000000000000000000000000000000000000000001", "calls": [{"from": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "gas": "0x97012", "gasUsed": "0x310a", "to": "0x2ce6311ddae708829bc0784c967b7d77d19fd779", "input": "0x23b872dd0000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f9960000000000000000000000003dbe077e7986657e95e1cc50089f17a5a4af0aae00000000000000000000000000000000000000000000000000000000001e2a55", "output": "0x0000000000000000000000000000000000000000000000000000000000000001", "value": "0x0", "type": "DELEGATECALL"}], "value": "0x0", "type": "CALL"}], "value": "0x0", "type": "CALL"}, {"from": "0x8f10b468b06c6fd214b65f87778827f7d113f996", "gas": "0x9dd1b", "gasUsed": "0x4e2", "to": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "input": "0x70a082310000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f996", "output": "0x0000000000000000000000000000000000000000000000000000000000000001", "calls": [{"from": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "gas": "0x9b321", "gasUsed": "0x229", "to": "0x2ce6311ddae708829bc0784c967b7d77d19fd779", "input": "0x70a082310000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f996", "output": "0x0000000000000000000000000000000000000000000000000000000000000001", "value": "0x0", "type": "DELEGATECALL"}], "type": "STATICCALL"}, {"from": "0x8f10b468b06c6fd214b65f87778827f7d113f996", "gas": "0x9d628", "gasUsed": "0x22b", "to": "0xacfe6019ed1a7dc6f7b508c02d1b04ec88cc21bf", "input": "0x70a082310000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f996", "output": "0x000000000000000000000000000000000000000000000000022378aef095ef4b", "type": "STATICCALL"}], "value": "0x0", "type": "DELEGATECALL"}], "value": "0x0", "type": "DELEGATECALL"}], "value": "0x0", "type": "CALL"}, {"from": "0x8f10b468b06c6fd214b65f87778827f7d113f996", "gas": "0xb59eb", "gasUsed": "0x107eb", "to": "0x5455c918e405a2831fbff8595c0aae35ee3db9d1", "input": "0x3b9d6e0900000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000001e2a55000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda02913000000000000000000000000acfe6019ed1a7dc6f7b508c02d1b04ec88cc21bf0000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f996000000000000000000000000000000000000000000000000000000000000004000000000000000000000000067a11022b7b6ed66f81233f6c8ed6e48f78265300000000000000000000000000000000000000000000000000000000100ad139d", "output": "0x0000000000000000000000000000000000000000000000000223ff9e9e0e4b96", "calls": [{"from": "0x8f10b468b06c6fd214b65f87778827f7d113f996", "gas": "0xb27bc", "gasUsed": "0xf92e", "to": "0x67a11022b7b6ed66f81233f6c8ed6e48f7826530", "input": "0x128acb080000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f996000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000001e2a550000000000000000000000000000000000000000000000000000000100ad139d00000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda02913", "output": "0x00000000000000000000000000000000000000000000000000000000001e2a55fffffffffffffffffffffffffffffffffffffffffffffffffddc006161f1b46a", "calls": [{"from": "0x67a11022b7b6ed66f81233f6c8ed6e48f7826530", "gas": "0xa8ca1", "gasUsed": "0x30c4", "to": "0xacfe6019ed1a7dc6f7b508c02d1b04ec88cc21bf", "input": "0xa9059cbb0000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f9960000000000000000000000000000000000000000000000000223ff9e9e0e4b96", "output": "0x0000000000000000000000000000000000000000000000000000000000000001", "value": "0x0", "type": "CALL"}, {"from": "0x67a11022b7b6ed66f81233f6c8ed6e48f7826530", "gas": "0xa5938", "gasUsed": "0xcb2", "to": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "input": "0x70a0823100000000000000000000000067a11022b7b6ed66f81233f6c8ed6e48f7826530", "output": "0x0000000000000000000000000000000000000000000000000000005fe032c023", "calls": [{"from": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "gas": "0xa2d4e", "gasUsed": "0x9f9", "to": "0x2ce6311ddae708829bc0784c967b7d77d19fd779", "input": "0x70a0823100000000000000000000000067a11022b7b6ed66f81233f6c8ed6e48f7826530", "output": "0x0000000000000000000000000000000000000000000000000000005fe032c023", "value": "0x0", "type": "DELEGATECALL"}], "type": "STATICCALL"}, {"from": "0x67a11022b7b6ed66f81233f6c8ed6e48f7826530", "gas": "0xa49c2", "gasUsed": "0x2f1b", "to": "0x8f10b468b06c6fd214b65f87778827f7d113f996", "input": "0xfa461e3300000000000000000000000000000000000000000000000000000000001e2a55fffffffffffffffffffffffffffffffffffffffffffffffffddc006161f1b46a00000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda02913", "calls": [{"from": "0x8f10b468b06c6fd214b65f87778827f7d113f996", "gas": "0xa1b2a", "gasUsed": "0x2889", "to": "0x440c1a9786d3c6f09da46b6d42320e0cf2f336a7", "input": "0xfa461e3300000000000000000000000000000000000000000000000000000000001e2a55fffffffffffffffffffffffffffffffffffffffffffffffffddc006161f1b46a00000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda02913", "calls": [{"from": "0x8f10b468b06c6fd214b65f87778827f7d113f996", "gas": "0x9ee7b", "gasUsed": "0x23ec", "to": "0xcd568949398283ba8608ed411f3706462e6e0e56", "input": "0x61f598cd00000000000000000000000000000000000000000000000000000000001e2a55fffffffffffffffffffffffffffffffffffffffffffffffffddc006161f1b46a00000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda02913", "calls": [{"from": "0x8f10b468b06c6fd214b65f87778827f7d113f996", "gas": "0x9c337", "gasUsed": "0x1fe3", "to": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "input": "0xa9059cbb00000000000000000000000067a11022b7b6ed66f81233f6c8ed6e48f782653000000000000000000000000000000000000000000000000000000000001e2a55", "output": "0x0000000000000000000000000000000000000000000000000000000000000001", "calls": [{"from": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "gas": "0x999a2", "gasUsed": "0x1d27", "to": "0x2ce6311ddae708829bc0784c967b7d77d19fd779", "input": "0xa9059cbb00000000000000000000000067a11022b7b6ed66f81233f6c8ed6e48f782653000000000000000000000000000000000000000000000000000000000001e2a55", "output": "0x0000000000000000000000000000000000000000000000000000000000000001", "value": "0x0", "type": "DELEGATECALL"}], "value": "0x0", "type": "CALL"}], "value": "0x0", "type": "DELEGATECALL"}], "value": "0x0", "type": "DELEGATECALL"}], "value": "0x0", "type": "CALL"}, {"from": "0x67a11022b7b6ed66f81233f6c8ed6e48f7826530", "gas": "0xa18eb", "gasUsed": "0x4e2", "to": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "input": "0x70a0823100000000000000000000000067a11022b7b6ed66f81233f6c8ed6e48f7826530", "output": "0x0000000000000000000000000000000000000000000000000000005fe050ea78", "calls": [{"from": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "gas": "0x9ee02", "gasUsed": "0x229", "to": "0x2ce6311ddae708829bc0784c967b7d77d19fd779", "input": "0x70a0823100000000000000000000000067a11022b7b6ed66f81233f6c8ed6e48f7826530", "output": "0x0000000000000000000000000000000000000000000000000000005fe050ea78", "value": "0x0", "type": "DELEGATECALL"}], "type": "STATICCALL"}], "value": "0x0", "type": "CALL"}], "value": "0x0", "type": "DELEGATECALL"}, {"from": "0x8f10b468b06c6fd214b65f87778827f7d113f996", "gas": "0xa487f", "gasUsed": "0x22b", "to": "0xacfe6019ed1a7dc6f7b508c02d1b04ec88cc21bf", "input": "0x70a082310000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f996", "output": "0x0000000000000000000000000000000000000000000000000223ff9e9e0e4b97", "type": "STATICCALL"}, {"from": "0x8f10b468b06c6fd214b65f87778827f7d113f996", "gas": "0xa3beb", "gasUsed": "0x1b4df", "to": "0x5455c918e405a2831fbff8595c0aae35ee3db9d1", "input": "0x3b9d6e0900000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000223ff9e9e0e4b96000000000000000000000000acfe6019ed1a7dc6f7b508c02d1b04ec88cc21bf00000000000000000000000042000000000000000000000000000000000000060000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f9960000000000000000000000000000000000000000000000000000000000000040000000000000000000000000ed39c843ca9bc596f5db20b8ebb8199345e7d643000000000000000000000000ffb57e36d5b8bf4597abf0bbcfe764cfe8b12112", "output": "0x0000000000000000000000000000000000000000000000000003ab35886b8369", "calls": [{"from": "0x8f10b468b06c6fd214b65f87778827f7d113f996", "gas": "0xa0497", "gasUsed": "0x19c4e", "to": "0xed39c843ca9bc596f5db20b8ebb8199345e7d643", "input": "0x128acb080000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f99600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000223ff9e9e0e4b96000000000000000000000000ffb57e36d5b8bf4597abf0bbcfe764cfe8b1211200000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000acfe6019ed1a7dc6f7b508c02d1b04ec88cc21bf", "output": "0xfffffffffffffffffffffffffffffffffffffffffffffffffffc54ca77947c970000000000000000000000000000000000000000000000000223ff9e9e0e4b96", "calls": [{"from": "0xed39c843ca9bc596f5db20b8ebb8199345e7d643", "gas": "0x99ffa", "gasUsed": "0x5b16", "to": "0xc03573c6a7c8dfa885ed79d4cadb91be56dc446e", "input": "0x214a6fe2000000000000000000000000000000000000000000000000000000006a69e6a5", "calls": [{"from": "0xc03573c6a7c8dfa885ed79d4cadb91be56dc446e", "gas": "0x96437", "gasUsed": "0x2cb0", "to": "0xc6a2db661d5a5690172d8eb0a7dea2d3008665a3", "input": "0xa15ea89f000000000000000000000000ed39c843ca9bc596f5db20b8ebb8199345e7d643", "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006a6b6b33", "type": "STATICCALL"}], "value": "0x0", "type": "CALL"}, {"from": "0xed39c843ca9bc596f5db20b8ebb8199345e7d643", "gas": "0x8c17d", "gasUsed": "0x3139", "to": "0x4200000000000000000000000000000000000006", "input": "0xa9059cbb0000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f9960000000000000000000000000000000000000000000000000003ab35886b8369", "output": "0x0000000000000000000000000000000000000000000000000000000000000001", "value": "0x0", "type": "CALL"}, {"from": "0xed39c843ca9bc596f5db20b8ebb8199345e7d643", "gas": "0x88da3", "gasUsed": "0x9fb", "to": "0xacfe6019ed1a7dc6f7b508c02d1b04ec88cc21bf", "input": "0x70a08231000000000000000000000000ed39c843ca9bc596f5db20b8ebb8199345e7d643", "output": "0x0000000000000000000000000000000000000000000000006042a4e01715234f", "type": "STATICCALL"}, {"from": "0xed39c843ca9bc596f5db20b8ebb8199345e7d643", "gas": "0x880d9", "gasUsed": "0x2586", "to": "0x8f10b468b06c6fd214b65f87778827f7d113f996", "input": "0x23a69e75fffffffffffffffffffffffffffffffffffffffffffffffffffc54ca77947c970000000000000000000000000000000000000000000000000223ff9e9e0e4b9600000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000acfe6019ed1a7dc6f7b508c02d1b04ec88cc21bf", "calls": [{"from": "0x8f10b468b06c6fd214b65f87778827f7d113f996", "gas": "0x85964", "gasUsed": "0x1ef4", "to": "0x440c1a9786d3c6f09da46b6d42320e0cf2f336a7", "input": "0x23a69e75fffffffffffffffffffffffffffffffffffffffffffffffffffc54ca77947c970000000000000000000000000000000000000000000000000223ff9e9e0e4b9600000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000acfe6019ed1a7dc6f7b508c02d1b04ec88cc21bf", "calls": [{"from": "0x8f10b468b06c6fd214b65f87778827f7d113f996", "gas": "0x833bc", "gasUsed": "0x1a57", "to": "0xcd568949398283ba8608ed411f3706462e6e0e56", "input": "0x61f598cdfffffffffffffffffffffffffffffffffffffffffffffffffffc54ca77947c970000000000000000000000000000000000000000000000000223ff9e9e0e4b9600000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000acfe6019ed1a7dc6f7b508c02d1b04ec88cc21bf", "calls": [{"from": "0x8f10b468b06c6fd214b65f87778827f7d113f996", "gas": "0x80f49", "gasUsed": "0x1634", "to": "0xacfe6019ed1a7dc6f7b508c02d1b04ec88cc21bf", "input": "0xa9059cbb000000000000000000000000ed39c843ca9bc596f5db20b8ebb8199345e7d6430000000000000000000000000000000000000000000000000223ff9e9e0e4b96", "output": "0x0000000000000000000000000000000000000000000000000000000000000001", "value": "0x0", "type": "CALL"}], "value": "0x0", "type": "DELEGATECALL"}], "value": "0x0", "type": "DELEGATECALL"}], "value": "0x0", "type": "CALL"}, {"from": "0xed39c843ca9bc596f5db20b8ebb8199345e7d643", "gas": "0x85971", "gasUsed": "0x22b", "to": "0xacfe6019ed1a7dc6f7b508c02d1b04ec88cc21bf", "input": "0x70a08231000000000000000000000000ed39c843ca9bc596f5db20b8ebb8199345e7d643", "output": "0x0000000000000000000000000000000000000000000000006266a47eb5236ee5", "type": "STATICCALL"}], "value": "0x0", "type": "CALL"}], "value": "0x0", "type": "DELEGATECALL"}, {"from": "0x8f10b468b06c6fd214b65f87778827f7d113f996", "gas": "0x88865", "gasUsed": "0x1c9", "to": "0x4200000000000000000000000000000000000006", "input": "0x70a082310000000000000000000000008f10b468b06c6fd214b65f87778827f7d113f996", "output": "0x0000000000000000000000000000000000000000000000000003ab35886b836a", "type": "STATICCALL"}, {"from": "0x8f10b468b06c6fd214b65f87778827f7d113f996", "gas": "0x87de7", "gasUsed": "0x23ab", "to": "0x4200000000000000000000000000000000000006", "input": "0x2e1a7d4d0000000000000000000000000000000000000000000000000003ab35886b8369", "calls": [{"from": "0x4200000000000000000000000000000000000006", "gas": "0x8fc", "gasUsed": "0x43", "to": "0x8f10b468b06c6fd214b65f87778827f7d113f996", "input": "0x", "value": "0x3ab35886b8369", "type": "CALL"}], "value": "0x0", "type": "CALL"}, {"from": "0x8f10b468b06c6fd214b65f87778827f7d113f996", "gas": "0x839a7", "gasUsed": "0x575", "to": "0xb92fe925dc43a0ecde6c8b1a2709c170ec4fff4f", "input": "0x", "value": "0x3ab35886b8369", "type": "CALL"}], "value": "0x0", "type": "DELEGATECALL"}], "value": "0x0", "type": "CALL"}], "value": "0x0", "type": "CALL"}], "value": "0x0", "type": "CALL"}], "value": "0x0", "type": "CALL"} \ No newline at end of file diff --git a/tools/hindsight/src/decoder/fixtures/trace_revert_out_of_gas_0x08fee57c.json b/tools/hindsight/src/decoder/fixtures/trace_revert_out_of_gas_0x08fee57c.json new file mode 100644 index 00000000..ab7f130c --- /dev/null +++ b/tools/hindsight/src/decoder/fixtures/trace_revert_out_of_gas_0x08fee57c.json @@ -0,0 +1 @@ +{"from": "0x7efeb39ecc2b02d7c651cd8c547eed22f83c1665", "gas": "0x82de4", "gasUsed": "0x813ac", "to": "0xccc88a9d1b4ed6b0eaba998850414b24f1c315be", "input": "0xf9e4bab400000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001400000000000000000000000007efeb39ecc2b02d7c651cd8c547eed22f83c16650000000000000000000000007efeb39ecc2b02d7c651cd8c547eed22f83c166500000000000000000000000000000000000000000000000000000000000009600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda029130000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000d0ef00000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000003e00000000000000000000000000000000000000000000000000000000000000680000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda029130000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000044095ea7b300000000000000000000000020f6ee51340adeed01a59b0e65cb3703f3dc860c000000000000000000000000000000000000000000000000000000000000d0ef00000000000000000000000000000000000000000000000000000000000000000000000000000000b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000001a49bb43718000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000001000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda029130000000000000000000000000000000000000000000000000000000000000001000000000000000000000000f70da97812cb96acdf810712aa562db8dfa3dbef000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000001cb0000000000000000000000000000000000000000000000000000000000000021582f3f59ff81e5e0c1b952abba1b7f70ee487db08547df4c6fe71b773e8d437901000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020f6ee51340adeed01a59b0e65cb3703f3dc860c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000001e446ec278a0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000019101590040b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f833589fcd6edb6e08f4c7c32d4f71b54bda029130000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cf24e00114d00119d001205247dd5e3941528e217ec203345b1780d16a25c8c20340603b95ef04708ad37bf0012700f0467b96f558daf66eecc4b42930c61495d1f12d71ccfbed82ddb2a78757905b3621c9eccef7aec22e0ba4af72bfc11d95a7072b0b91c04ac71c552df1d8d4871b00e06a69e7bad0193c7f05dd1ad0195cf60351fef02710833589fcd6edb6e08f4c7c32d4f71b54bda029138569213d10e502b0573f9cd6d33be324144b894ff0cf2402012902013d0e015101513df02124f801f80001015f0301630301650e015101510301654200000000000000000000000000000000000006020178020178050000020070020070070500200100000154015f000000000001670178013d060020018c018f0000050000018f019500000600200195019800000300000198019f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c4a6bd8c9600000000000000000000000000000000000000000000000000000000000000000000000000000000000000007efeb39ecc2b02d7c651cd8c547eed22f83c166500000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000021582f3f59ff81e5e0c1b952abba1b7f70ee487db08547df4c6fe71b773e8d43790000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000021582f3f59ff81e5e0c1b952abba1b7f70ee487db08547df4c6fe71b773e8d437900000000000000000000000000000000000000000000000000000000000000009734d8e377b17ef6c4fd74580bd784ee07f7b1abba259b1c0e5e18ff95f3f285", "error": "execution reverted", "calls": [{"from": "0xccc88a9d1b4ed6b0eaba998850414b24f1c315be", "gas": "0x761ef", "gasUsed": "0xbd2c", "to": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "input": "0x23b872dd0000000000000000000000007efeb39ecc2b02d7c651cd8c547eed22f83c1665000000000000000000000000b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f000000000000000000000000000000000000000000000000000000000000d0ef", "output": "0x0000000000000000000000000000000000000000000000000000000000000001", "calls": [{"from": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "gas": "0x728fa", "gasUsed": "0xa126", "to": "0x2ce6311ddae708829bc0784c967b7d77d19fd779", "input": "0x23b872dd0000000000000000000000007efeb39ecc2b02d7c651cd8c547eed22f83c1665000000000000000000000000b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f000000000000000000000000000000000000000000000000000000000000d0ef", "output": "0x0000000000000000000000000000000000000000000000000000000000000001", "value": "0x0", "type": "DELEGATECALL"}], "value": "0x0", "type": "CALL"}, {"from": "0xccc88a9d1b4ed6b0eaba998850414b24f1c315be", "gas": "0x681ac", "gasUsed": "0x681ac", "to": "0xb92fe925dc43a0ecde6c8b1a2709c170ec4fff4f", "input": "0xcd6e13f700000000000000000000000000000000000000000000000000000000000000800000000000000000000000007efeb39ecc2b02d7c651cd8c547eed22f83c16650000000000000000000000007efeb39ecc2b02d7c651cd8c547eed22f83c166500000000000000000000000000000000000000000000000000000000000008a000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000003e00000000000000000000000000000000000000000000000000000000000000680000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda029130000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000044095ea7b300000000000000000000000020f6ee51340adeed01a59b0e65cb3703f3dc860c000000000000000000000000000000000000000000000000000000000000d0ef00000000000000000000000000000000000000000000000000000000000000000000000000000000b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000001a49bb43718000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000001000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda029130000000000000000000000000000000000000000000000000000000000000001000000000000000000000000f70da97812cb96acdf810712aa562db8dfa3dbef000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000001cb0000000000000000000000000000000000000000000000000000000000000021582f3f59ff81e5e0c1b952abba1b7f70ee487db08547df4c6fe71b773e8d437901000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020f6ee51340adeed01a59b0e65cb3703f3dc860c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000001e446ec278a0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000019101590040b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f833589fcd6edb6e08f4c7c32d4f71b54bda029130000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cf24e00114d00119d001205247dd5e3941528e217ec203345b1780d16a25c8c20340603b95ef04708ad37bf0012700f0467b96f558daf66eecc4b42930c61495d1f12d71ccfbed82ddb2a78757905b3621c9eccef7aec22e0ba4af72bfc11d95a7072b0b91c04ac71c552df1d8d4871b00e06a69e7bad0193c7f05dd1ad0195cf60351fef02710833589fcd6edb6e08f4c7c32d4f71b54bda029138569213d10e502b0573f9cd6d33be324144b894ff0cf2402012902013d0e015101513df02124f801f80001015f0301630301650e015101510301654200000000000000000000000000000000000006020178020178050000020070020070070500200100000154015f000000000001670178013d060020018c018f0000050000018f019500000600200195019800000300000198019f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c4a6bd8c9600000000000000000000000000000000000000000000000000000000000000000000000000000000000000007efeb39ecc2b02d7c651cd8c547eed22f83c166500000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000021582f3f59ff81e5e0c1b952abba1b7f70ee487db08547df4c6fe71b773e8d43790000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000021582f3f59ff81e5e0c1b952abba1b7f70ee487db08547df4c6fe71b773e8d43790000000000000000000000000000000000000000000000000000000000000000", "error": "out of gas", "calls": [{"from": "0xb92fe925dc43a0ecde6c8b1a2709c170ec4fff4f", "gas": "0x606b2", "gasUsed": "0x2031", "to": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "input": "0x095ea7b300000000000000000000000020f6ee51340adeed01a59b0e65cb3703f3dc860c000000000000000000000000000000000000000000000000000000000000d0ef", "output": "0x0000000000000000000000000000000000000000000000000000000000000001", "calls": [{"from": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "gas": "0x5ec2f", "gasUsed": "0x1d95", "to": "0x2ce6311ddae708829bc0784c967b7d77d19fd779", "input": "0x095ea7b300000000000000000000000020f6ee51340adeed01a59b0e65cb3703f3dc860c000000000000000000000000000000000000000000000000000000000000d0ef", "output": "0x0000000000000000000000000000000000000000000000000000000000000001", "value": "0x0", "type": "DELEGATECALL"}], "value": "0x0", "type": "CALL"}, {"from": "0xb92fe925dc43a0ecde6c8b1a2709c170ec4fff4f", "gas": "0x5d609", "gasUsed": "0x3e34", "to": "0xb92fe925dc43a0ecde6c8b1a2709c170ec4fff4f", "input": "0x9bb43718000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000001000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda029130000000000000000000000000000000000000000000000000000000000000001000000000000000000000000f70da97812cb96acdf810712aa562db8dfa3dbef000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000001cb0000000000000000000000000000000000000000000000000000000000000021582f3f59ff81e5e0c1b952abba1b7f70ee487db08547df4c6fe71b773e8d43790100000000000000000000000000000000000000000000000000000000000000", "calls": [{"from": "0xb92fe925dc43a0ecde6c8b1a2709c170ec4fff4f", "gas": "0x5b5b8", "gasUsed": "0x27b3", "to": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "input": "0xa9059cbb000000000000000000000000f70da97812cb96acdf810712aa562db8dfa3dbef00000000000000000000000000000000000000000000000000000000000001cb", "output": "0x0000000000000000000000000000000000000000000000000000000000000001", "calls": [{"from": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "gas": "0x59c59", "gasUsed": "0x24f7", "to": "0x2ce6311ddae708829bc0784c967b7d77d19fd779", "input": "0xa9059cbb000000000000000000000000f70da97812cb96acdf810712aa562db8dfa3dbef00000000000000000000000000000000000000000000000000000000000001cb", "output": "0x0000000000000000000000000000000000000000000000000000000000000001", "value": "0x0", "type": "DELEGATECALL"}], "value": "0x0", "type": "CALL"}], "value": "0x0", "type": "CALL"}, {"from": "0xb92fe925dc43a0ecde6c8b1a2709c170ec4fff4f", "gas": "0x5741a", "gasUsed": "0x5471e", "to": "0x20f6ee51340adeed01a59b0e65cb3703f3dc860c", "input": "0x46ec278a0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000019101590040b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f833589fcd6edb6e08f4c7c32d4f71b54bda029130000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cf24e00114d00119d001205247dd5e3941528e217ec203345b1780d16a25c8c20340603b95ef04708ad37bf0012700f0467b96f558daf66eecc4b42930c61495d1f12d71ccfbed82ddb2a78757905b3621c9eccef7aec22e0ba4af72bfc11d95a7072b0b91c04ac71c552df1d8d4871b00e06a69e7bad0193c7f05dd1ad0195cf60351fef02710833589fcd6edb6e08f4c7c32d4f71b54bda029138569213d10e502b0573f9cd6d33be324144b894ff0cf2402012902013d0e015101513df02124f801f80001015f0301630301650e015101510301654200000000000000000000000000000000000006020178020178050000020070020070070500200100000154015f000000000001670178013d060020018c018f0000050000018f019500000600200195019800000300000198019f0000000000000000000000000000000000", "output": "0x0000000000000000000000000000000000000000000000000000193fd5c83779", "calls": [{"from": "0x20f6ee51340adeed01a59b0e65cb3703f3dc860c", "gas": "0x54958", "gasUsed": "0xbb8", "to": "0x0000000000000000000000000000000000000001", "input": "0x16ea7b1e2a0c132304bd27f39948dfca4a5b2db21cdb0cfaf558327a4cc9ecfb000000000000000000000000000000000000000000000000000000000000001bf0467b96f558daf66eecc4b42930c61495d1f12d71ccfbed82ddb2a78757905b3621c9eccef7aec22e0ba4af72bfc11d95a7072b0b91c04ac71c552df1d8d487", "output": "0x00000000000000000000000028de4113921bd79388b0ef80a259f871977442e9", "type": "STATICCALL"}, {"from": "0x20f6ee51340adeed01a59b0e65cb3703f3dc860c", "gas": "0x527ec", "gasUsed": "0x7678", "to": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "input": "0x23b872dd000000000000000000000000b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f00000000000000000000000009ad820aac5779683b481c4674208a4e1b024afa000000000000000000000000000000000000000000000000000000000000cf24", "output": "0x0000000000000000000000000000000000000000000000000000000000000001", "calls": [{"from": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "gas": "0x510de", "gasUsed": "0x73d6", "to": "0x2ce6311ddae708829bc0784c967b7d77d19fd779", "input": "0x23b872dd000000000000000000000000b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f00000000000000000000000009ad820aac5779683b481c4674208a4e1b024afa000000000000000000000000000000000000000000000000000000000000cf24", "output": "0x0000000000000000000000000000000000000000000000000000000000000001", "value": "0x0", "type": "DELEGATECALL"}], "value": "0x0", "type": "CALL"}, {"from": "0x20f6ee51340adeed01a59b0e65cb3703f3dc860c", "gas": "0x4a6bd", "gasUsed": "0x44eec", "to": "0x09ad820aac5779683b481c4674208a4e1b024afa", "input": "0x158f68940000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000019101590040b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f833589fcd6edb6e08f4c7c32d4f71b54bda029130000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cf24e00114d00119d001205247dd5e3941528e217ec203345b1780d16a25c8c20340603b95ef04708ad37bf0012700f0467b96f558daf66eecc4b42930c61495d1f12d71ccfbed82ddb2a78757905b3621c9eccef7aec22e0ba4af72bfc11d95a7072b0b91c04ac71c552df1d8d4871b00e06a69e7bad0193c7f05dd1ad0195cf60351fef02710833589fcd6edb6e08f4c7c32d4f71b54bda029138569213d10e502b0573f9cd6d33be324144b894ff0cf2402012902013d0e015101513df02124f801f80001015f0301630301650e015101510301654200000000000000000000000000000000000006020178020178050000020070020070070500200100000154015f000000000001670178013d060020018c018f0000050000018f019500000600200195019800000300000198019f0000", "output": "0x0000000000000000000000000000000000000000000000000000000000000000", "calls": [{"from": "0x09ad820aac5779683b481c4674208a4e1b024afa", "gas": "0x47e43", "gasUsed": "0x62fd", "to": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "input": "0x095ea7b30000000000000000000000008569213d10e502b0573f9cd6d33be324144b894f000000000000000000000000000000000000000000000000000000000000cf24", "output": "0x0000000000000000000000000000000000000000000000000000000000000001", "calls": [{"from": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "gas": "0x469e1", "gasUsed": "0x6061", "to": "0x2ce6311ddae708829bc0784c967b7d77d19fd779", "input": "0x095ea7b30000000000000000000000008569213d10e502b0573f9cd6d33be324144b894f000000000000000000000000000000000000000000000000000000000000cf24", "output": "0x0000000000000000000000000000000000000000000000000000000000000001", "value": "0x0", "type": "DELEGATECALL"}], "value": "0x0", "type": "CALL"}, {"from": "0x09ad820aac5779683b481c4674208a4e1b024afa", "gas": "0x40a2b", "gasUsed": "0x368c6", "to": "0x8569213d10e502b0573f9cd6d33be324144b894f", "input": "0x3df0212400000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cf240000000000000000000000000000000000000000000000000000000000000000", "output": "0x0000000000000000000000000000000000000000000000000000193fd5c83779", "calls": [{"from": "0x8569213d10e502b0573f9cd6d33be324144b894f", "gas": "0x3d65e", "gasUsed": "0x1648e", "to": "0xa30f6ef7211d4c8882036a2d2a0f01351a10e84b", "input": "0x3ba0b9a900000000000000000000000000000000000000000000000000000000", "output": "0x000000000000000000000000000000000000000000000067870891e50ed2b000", "calls": [{"from": "0xa30f6ef7211d4c8882036a2d2a0f01351a10e84b", "gas": "0x39857", "gasUsed": "0x11331", "to": "0xb2cc224c1c9fee385f8ad6a55b4d94e92359dc59", "input": "0x883bdbfd0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000", "output": "0x000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000002fffffffffffffffffffffffffffffffffffffffffffffffffffff350119bd4c7fffffffffffffffffffffffffffffffffffffffffffffffffffff3501198c4820000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000006a00000014856537ffa4f1bcbeab3c3c38a000000000000000000000000000006a00000014856537ffdc97ec4097df8ed85", "calls": [{"from": "0xb2cc224c1c9fee385f8ad6a55b4d94e92359dc59", "gas": "0x37fc2", "gasUsed": "0x10891", "to": "0xec8e5342b19977b4ef8892e02d8daecfa1315831", "input": "0x883bdbfd0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000", "output": "0x000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000002fffffffffffffffffffffffffffffffffffffffffffffffffffff350119bd4c7fffffffffffffffffffffffffffffffffffffffffffffffffffff3501198c4820000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000006a00000014856537ffa4f1bcbeab3c3c38a000000000000000000000000000006a00000014856537ffdc97ec4097df8ed85", "value": "0x0", "type": "DELEGATECALL"}], "type": "STATICCALL"}], "type": "STATICCALL"}, {"from": "0x8569213d10e502b0573f9cd6d33be324144b894f", "gas": "0x27474", "gasUsed": "0x18", "to": "0x0000000000000000000000000000000000000004", "input": "0x0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000067870891e50ed2b000000000000000000000000000000000000000000c9f2c9cd04674edea40000000", "output": "0x0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000067870891e50ed2b000000000000000000000000000000000000000000c9f2c9cd04674edea40000000", "type": "STATICCALL"}, {"from": "0x8569213d10e502b0573f9cd6d33be324144b894f", "gas": "0x273c1", "gasUsed": "0x18", "to": "0x0000000000000000000000000000000000000004", "input": "0x0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000067870891e50ed2b000000000000000000000000000000000000000000c9f2c9cd04674edea40000000", "output": "0x0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000067870891e50ed2b000000000000000000000000000000000000000000c9f2c9cd04674edea40000000", "type": "STATICCALL"}, {"from": "0x8569213d10e502b0573f9cd6d33be324144b894f", "gas": "0x23f5b", "gasUsed": "0x18", "to": "0x0000000000000000000000000000000000000004", "input": "0x00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000005d95d43223cec11b0000000000000000000000000000000000000000000000000000000094f677cc", "output": "0x00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000005d95d43223cec11b0000000000000000000000000000000000000000000000000000000094f677cc", "type": "STATICCALL"}, {"from": "0x8569213d10e502b0573f9cd6d33be324144b894f", "gas": "0x23ea7", "gasUsed": "0x18", "to": "0x0000000000000000000000000000000000000004", "input": "0x00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000005d95d43223cec11b0000000000000000000000000000000000000000000000000000000094f677cc", "output": "0x00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000005d95d43223cec11b0000000000000000000000000000000000000000000000000000000094f677cc", "type": "STATICCALL"}, {"from": "0x8569213d10e502b0573f9cd6d33be324144b894f", "gas": "0x23e00", "gasUsed": "0x18", "to": "0x0000000000000000000000000000000000000004", "input": "0x0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000067870891e50ed2b000000000000000000000000000000000000000000c9f2c9cd04674edea40000000", "output": "0x0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000067870891e50ed2b000000000000000000000000000000000000000000c9f2c9cd04674edea40000000", "type": "STATICCALL"}, {"from": "0x8569213d10e502b0573f9cd6d33be324144b894f", "gas": "0x23d5b", "gasUsed": "0x18", "to": "0x0000000000000000000000000000000000000004", "input": "0x00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000005d95d43223cec11b0000000000000000000000000000000000000000000000000000000094f677cc", "output": "0x00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000005d95d43223cec11b0000000000000000000000000000000000000000000000000000000094f677cc", "type": "STATICCALL"}, {"from": "0x8569213d10e502b0573f9cd6d33be324144b894f", "gas": "0x23a3f", "gasUsed": "0x18", "to": "0x0000000000000000000000000000000000000004", "input": "0x00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000002ba23fbc9f9e7c9fb6e0000000000000000000000000000000000000000000000877b18b83f8df8c000", "output": "0x00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000002ba23fbc9f9e7c9fb6e0000000000000000000000000000000000000000000000877b18b83f8df8c000", "type": "STATICCALL"}, {"from": "0x8569213d10e502b0573f9cd6d33be324144b894f", "gas": "0x2398c", "gasUsed": "0x18", "to": "0x0000000000000000000000000000000000000004", "input": "0x00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000002ba23fbc9f9e7c9fb6e0000000000000000000000000000000000000000000000877b18b83f8df8c000", "output": "0x00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000002ba23fbc9f9e7c9fb6e0000000000000000000000000000000000000000000000877b18b83f8df8c000", "type": "STATICCALL"}, {"from": "0x8569213d10e502b0573f9cd6d33be324144b894f", "gas": "0x23854", "gasUsed": "0xcb2", "to": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "input": "0x70a082310000000000000000000000008569213d10e502b0573f9cd6d33be324144b894f", "output": "0x00000000000000000000000000000000000000000000000000000000a970676d", "calls": [{"from": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "gas": "0x22ced", "gasUsed": "0x9f9", "to": "0x2ce6311ddae708829bc0784c967b7d77d19fd779", "input": "0x70a082310000000000000000000000008569213d10e502b0573f9cd6d33be324144b894f", "output": "0x00000000000000000000000000000000000000000000000000000000a970676d", "value": "0x0", "type": "DELEGATECALL"}], "type": "STATICCALL"}, {"from": "0x8569213d10e502b0573f9cd6d33be324144b894f", "gas": "0x22a87", "gasUsed": "0x240c", "to": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "input": "0x23b872dd00000000000000000000000009ad820aac5779683b481c4674208a4e1b024afa0000000000000000000000008569213d10e502b0573f9cd6d33be324144b894f000000000000000000000000000000000000000000000000000000000000cf24", "output": "0x0000000000000000000000000000000000000000000000000000000000000001", "calls": [{"from": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "gas": "0x21f6e", "gasUsed": "0x216a", "to": "0x2ce6311ddae708829bc0784c967b7d77d19fd779", "input": "0x23b872dd00000000000000000000000009ad820aac5779683b481c4674208a4e1b024afa0000000000000000000000008569213d10e502b0573f9cd6d33be324144b894f000000000000000000000000000000000000000000000000000000000000cf24", "output": "0x0000000000000000000000000000000000000000000000000000000000000001", "value": "0x0", "type": "DELEGATECALL"}], "value": "0x0", "type": "CALL"}, {"from": "0x8569213d10e502b0573f9cd6d33be324144b894f", "gas": "0x205c5", "gasUsed": "0x4e2", "to": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "input": "0x70a082310000000000000000000000008569213d10e502b0573f9cd6d33be324144b894f", "output": "0x00000000000000000000000000000000000000000000000000000000a9713691", "calls": [{"from": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "gas": "0x1fb29", "gasUsed": "0x229", "to": "0x2ce6311ddae708829bc0784c967b7d77d19fd779", "input": "0x70a082310000000000000000000000008569213d10e502b0573f9cd6d33be324144b894f", "output": "0x00000000000000000000000000000000000000000000000000000000a9713691", "value": "0x0", "type": "DELEGATECALL"}], "type": "STATICCALL"}, {"from": "0x8569213d10e502b0573f9cd6d33be324144b894f", "gas": "0x1f2b2", "gasUsed": "0x18", "to": "0x0000000000000000000000000000000000000004", "input": "0x00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000002ba23fbc9f9e7c9fb6e0000000000000000000000000000000000000000000000877b18b83f8df8c000", "output": "0x00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000002ba23fbc9f9e7c9fb6e0000000000000000000000000000000000000000000000877b18b83f8df8c000", "type": "STATICCALL"}, {"from": "0x8569213d10e502b0573f9cd6d33be324144b894f", "gas": "0x1f20d", "gasUsed": "0x18", "to": "0x0000000000000000000000000000000000000004", "input": "0x0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000067870891e50ed2b000000000000000000000000000000000000000000c9f2c9cd04674edea40000000", "output": "0x0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000067870891e50ed2b000000000000000000000000000000000000000000c9f2c9cd04674edea40000000", "type": "STATICCALL"}, {"from": "0x8569213d10e502b0573f9cd6d33be324144b894f", "gas": "0x1e0ae", "gasUsed": "0x18", "to": "0x0000000000000000000000000000000000000004", "input": "0x00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000002ba23fbc9f9e7c9fb6e0000000000000000000000000000000000000000000000877b18b83f8df8c000", "output": "0x00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000002ba23fbc9f9e7c9fb6e0000000000000000000000000000000000000000000000877b18b83f8df8c000", "type": "STATICCALL"}, {"from": "0x8569213d10e502b0573f9cd6d33be324144b894f", "gas": "0x1ced3", "gasUsed": "0x18", "to": "0x0000000000000000000000000000000000000004", "input": "0x00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000002ba23fbc9f9e7c9fb6e0000000000000000000000000000000000000000000000877b18b83f8df8c000", "output": "0x00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000002ba23fbc9f9e7c9fb6e0000000000000000000000000000000000000000000000877b18b83f8df8c000", "type": "STATICCALL"}, {"from": "0x8569213d10e502b0573f9cd6d33be324144b894f", "gas": "0x19e2d", "gasUsed": "0x18", "to": "0x0000000000000000000000000000000000000004", "input": "0x00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000002ba23fbc9f9e7c9fb6e0000000000000000000000000000000000000000000000877b18b83f8df8c000", "output": "0x00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000002ba23fbc9f9e7c9fb6e0000000000000000000000000000000000000000000000877b18b83f8df8c000", "type": "STATICCALL"}, {"from": "0x8569213d10e502b0573f9cd6d33be324144b894f", "gas": "0x19d1c", "gasUsed": "0x18", "to": "0x0000000000000000000000000000000000000004", "input": "0x00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000002ba233f0d84426c3c030000000000000000000000000000000000000000000000877bd51ceeed1f0000", "output": "0x00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000002ba233f0d84426c3c030000000000000000000000000000000000000000000000877bd51ceeed1f0000", "type": "STATICCALL"}, {"from": "0x8569213d10e502b0573f9cd6d33be324144b894f", "gas": "0x19400", "gasUsed": "0x15", "to": "0x0000000000000000000000000000000000000004", "input": "0x000000000000000000000000000000000000000000000000000000006a69e681000000000000000000000000000000000000000000000000000000006a69e681", "output": "0x000000000000000000000000000000000000000000000000000000006a69e681000000000000000000000000000000000000000000000000000000006a69e681", "type": "STATICCALL"}, {"from": "0x8569213d10e502b0573f9cd6d33be324144b894f", "gas": "0x18211", "gasUsed": "0x15", "to": "0x0000000000000000000000000000000000000004", "input": "0x000000000000000000000000000000000000000000000000000000000000000100000000000000000de104b22b6139ea00000000000000000de72dfca2472490", "output": "0x000000000000000000000000000000000000000000000000000000000000000100000000000000000de104b22b6139ea00000000000000000de72dfca2472490", "type": "STATICCALL"}, {"from": "0x8569213d10e502b0573f9cd6d33be324144b894f", "gas": "0x1816e", "gasUsed": "0x18", "to": "0x0000000000000000000000000000000000000004", "input": "0x00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000002ba233f0d84426c3c030000000000000000000000000000000000000000000000877bd51ceeed1f0000", "output": "0x00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000002ba233f0d84426c3c030000000000000000000000000000000000000000000000877bd51ceeed1f0000", "type": "STATICCALL"}, {"from": "0x8569213d10e502b0573f9cd6d33be324144b894f", "gas": "0x17b69", "gasUsed": "0x15", "to": "0x0000000000000000000000000000000000000004", "input": "0x00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000de72de9d1f0db24", "output": "0x00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000de72de9d1f0db24", "type": "STATICCALL"}, {"from": "0x8569213d10e502b0573f9cd6d33be324144b894f", "gas": "0x17ab9", "gasUsed": "0x15", "to": "0x0000000000000000000000000000000000000004", "input": "0x00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000de72de9d1f0db24", "output": "0x00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000de72de9d1f0db24", "type": "STATICCALL"}, {"from": "0x8569213d10e502b0573f9cd6d33be324144b894f", "gas": "0x16cec", "gasUsed": "0x15", "to": "0x0000000000000000000000000000000000000004", "input": "0x0000000000000000000000000000000000000000000000000de72de9d1f0db240000000000000000000000000000000000000000000000000de12c42d31343fb", "output": "0x0000000000000000000000000000000000000000000000000de72de9d1f0db240000000000000000000000000000000000000000000000000de12c42d31343fb", "type": "STATICCALL"}, {"from": "0x8569213d10e502b0573f9cd6d33be324144b894f", "gas": "0x14aac", "gasUsed": "0x15", "to": "0x0000000000000000000000000000000000000004", "input": "0x0000000000000000000000000000000000000000000003417b65ba2dd2ea5b7d000000000000000000000000000000000000000000000333388f3d402de1d758", "output": "0x0000000000000000000000000000000000000000000003417b65ba2dd2ea5b7d000000000000000000000000000000000000000000000333388f3d402de1d758", "type": "STATICCALL"}, {"from": "0x8569213d10e502b0573f9cd6d33be324144b894f", "gas": "0x11a9c", "gasUsed": "0x7405", "to": "0x4200000000000000000000000000000000000006", "input": "0xa9059cbb00000000000000000000000009ad820aac5779683b481c4674208a4e1b024afa0000000000000000000000000000000000000000000000000000193fd5c83779", "output": "0x0000000000000000000000000000000000000000000000000000000000000001", "value": "0x0", "type": "CALL"}], "value": "0x0", "type": "CALL"}, {"from": "0x09ad820aac5779683b481c4674208a4e1b024afa", "gas": "0xa9c9", "gasUsed": "0x1c9", "to": "0x4200000000000000000000000000000000000006", "input": "0x70a0823100000000000000000000000009ad820aac5779683b481c4674208a4e1b024afa", "output": "0x0000000000000000000000000000000000000000000000000000193fd5c83779", "type": "STATICCALL"}, {"from": "0x09ad820aac5779683b481c4674208a4e1b024afa", "gas": "0xa1af", "gasUsed": "0x239f", "to": "0x4200000000000000000000000000000000000006", "input": "0x2e1a7d4d0000000000000000000000000000000000000000000000000000193fd5c83779", "calls": [{"from": "0x4200000000000000000000000000000000000006", "gas": "0x8fc", "gasUsed": "0x37", "to": "0x09ad820aac5779683b481c4674208a4e1b024afa", "input": "0x", "value": "0x193fd5c83779", "type": "CALL"}], "value": "0x0", "type": "CALL"}, {"from": "0x09ad820aac5779683b481c4674208a4e1b024afa", "gas": "0x588e", "gasUsed": "0x37", "to": "0x20f6ee51340adeed01a59b0e65cb3703f3dc860c", "input": "0x", "value": "0x193fd5c83779", "type": "CALL"}], "value": "0x0", "type": "CALL"}, {"from": "0x20f6ee51340adeed01a59b0e65cb3703f3dc860c", "gas": "0x4ca2", "gasUsed": "0x575", "to": "0xb92fe925dc43a0ecde6c8b1a2709c170ec4fff4f", "input": "0x", "value": "0x193fd5c83779", "type": "CALL"}], "value": "0x0", "type": "CALL"}, {"from": "0xb92fe925dc43a0ecde6c8b1a2709c170ec4fff4f", "gas": "0x245e", "gasUsed": "0x245e", "to": "0xb92fe925dc43a0ecde6c8b1a2709c170ec4fff4f", "input": "0xa6bd8c9600000000000000000000000000000000000000000000000000000000000000000000000000000000000000007efeb39ecc2b02d7c651cd8c547eed22f83c166500000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000021582f3f59ff81e5e0c1b952abba1b7f70ee487db08547df4c6fe71b773e8d43790000000000000000000000000000000000000000000000000000000000000000", "error": "out of gas", "value": "0x0", "type": "CALL"}], "value": "0x0", "type": "CALL"}], "value": "0x0", "type": "CALL"} \ No newline at end of file diff --git a/tools/hindsight/src/decoder/fixtures/trace_revert_transfer_failure_0x12d802d5.json b/tools/hindsight/src/decoder/fixtures/trace_revert_transfer_failure_0x12d802d5.json new file mode 100644 index 00000000..70e737c3 --- /dev/null +++ b/tools/hindsight/src/decoder/fixtures/trace_revert_transfer_failure_0x12d802d5.json @@ -0,0 +1 @@ +{"from": "0x4d3dc340825d4814cad5f9161a55bcecd5053347", "gas": "0x46d77", "gasUsed": "0x3299e", "to": "0xccc88a9d1b4ed6b0eaba998850414b24f1c315be", "input": "0xf9e4bab400000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000f70da97812cb96acdf810712aa562db8dfa3dbef000000000000000000000000f70da97812cb96acdf810712aa562db8dfa3dbef00000000000000000000000000000000000000000000000000000000000009600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000fb42da273158b0f642f59f2ba7cc1d5457481677000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000707ed010ffa5ea4440000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001600000000000000000000000000000000000000000000000000000000000000520000000000000000000000000fb42da273158b0f642f59f2ba7cc1d54574816770000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000044095ea7b300000000000000000000000020f6ee51340adeed01a59b0e65cb3703f3dc860c00000000000000000000000000000000000000000000000707ed010ffa5ea4440000000000000000000000000000000000000000000000000000000000000000000000000000000020f6ee51340adeed01a59b0e65cb3703f3dc860c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000030446ec278a000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000002a6026e00e0b92fe925dc43a0ecde6c8b1a2709c170ec4fff4ffb42da273158b0f642f59f2ba7cc1d5457481677833589fcd6edb6e08f4c7c32d4f71b54bda0291300000000000000000000000000000000000000000000000707ed010ffa5ea444e00114e80119e8011d5247dd5e3941528e217ec203345b1780d16a25c8c20340603b95ef04708ad37bf0012100737558fbc499693830614d9970cd462c591fad84dc7f50ac6742b5556bbc0075201fc69582f0a9ce58fac80660faccb6dd8c4d5ffce4c41b6098a6ec1e5b444f1c00e06a69e79ee8106450e811bef0f02710128acb08f800b80707ed010ffa5ea444000000000000000000000000fffd8963efd1fc6a506488495d951d5263988d2500000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000014fb42da273158b0f642f59f2ba7cc1d5457481677000000000000000000000000010123060301270e0129012904013300809399da51c1a85e64cce4b30b554875d2b89b244500070a0000000000000000000000000000000000000000000000000000000000000301d8050000f80100000000000000000000000000000000000000000000000000000001000276a400000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000144200000000000000000000000000000000000006000000000000000000000000010123060301ff0500400402010080aad23a67f2ac693abbe543489aeb3f24f561d5170301d8050080020070020070070500c000004001b301c401c407002001f901ff000000004002810290029007002002a402aa000006002002aa02ad000003000002ad02b40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000022473b7bb2f000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000001000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda0291300000000000000000000000000000000000000000000000000000000000000010000000000000000000000004cd00e387622c35bddb9b4c962c136462338bc310000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000645a1ee3ac0000000000000000000000004d3dc340825d4814cad5f9161a55bcecd5053347000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda02913ce8d84b62a6b2adb46cc898e66842b9445275d1b74989c64fffd1f9fc1d6e50b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000214e487265f90e44f2364464f1957efde34a656cb1ff1139b3dd83cea608c5ee090000000000000000000000000000000000000000000000000000000000000000ce8d84b62a6b2adb46cc898e66842b9445275d1b74989c64fffd1f9fc1d6e50b", "output": "0xe450d38c000000000000000000000000b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f000000000000000000000000000000000000000000000006daed7aa2c74f79c4000000000000000000000000000000000000000000000006f16d3dd960d70f04", "error": "execution reverted", "calls": [{"from": "0xccc88a9d1b4ed6b0eaba998850414b24f1c315be", "gas": "0x3adcb", "gasUsed": "0xcf61", "to": "0xfb42da273158b0f642f59f2ba7cc1d5457481677", "input": "0x23b872dd0000000000000000000000004d3dc340825d4814cad5f9161a55bcecd5053347000000000000000000000000b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f00000000000000000000000000000000000000000000000707ed010ffa5ea444", "output": "0x0000000000000000000000000000000000000000000000000000000000000001", "value": "0x0", "type": "CALL"}, {"from": "0xccc88a9d1b4ed6b0eaba998850414b24f1c315be", "gas": "0x2bdec", "gasUsed": "0x184f3", "to": "0xb92fe925dc43a0ecde6c8b1a2709c170ec4fff4f", "input": "0xcd6e13f70000000000000000000000000000000000000000000000000000000000000080000000000000000000000000f70da97812cb96acdf810712aa562db8dfa3dbef000000000000000000000000f70da97812cb96acdf810712aa562db8dfa3dbef00000000000000000000000000000000000000000000000000000000000008a00000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001600000000000000000000000000000000000000000000000000000000000000520000000000000000000000000fb42da273158b0f642f59f2ba7cc1d54574816770000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000044095ea7b300000000000000000000000020f6ee51340adeed01a59b0e65cb3703f3dc860c00000000000000000000000000000000000000000000000707ed010ffa5ea4440000000000000000000000000000000000000000000000000000000000000000000000000000000020f6ee51340adeed01a59b0e65cb3703f3dc860c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000030446ec278a000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000002a6026e00e0b92fe925dc43a0ecde6c8b1a2709c170ec4fff4ffb42da273158b0f642f59f2ba7cc1d5457481677833589fcd6edb6e08f4c7c32d4f71b54bda0291300000000000000000000000000000000000000000000000707ed010ffa5ea444e00114e80119e8011d5247dd5e3941528e217ec203345b1780d16a25c8c20340603b95ef04708ad37bf0012100737558fbc499693830614d9970cd462c591fad84dc7f50ac6742b5556bbc0075201fc69582f0a9ce58fac80660faccb6dd8c4d5ffce4c41b6098a6ec1e5b444f1c00e06a69e79ee8106450e811bef0f02710128acb08f800b80707ed010ffa5ea444000000000000000000000000fffd8963efd1fc6a506488495d951d5263988d2500000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000014fb42da273158b0f642f59f2ba7cc1d5457481677000000000000000000000000010123060301270e0129012904013300809399da51c1a85e64cce4b30b554875d2b89b244500070a0000000000000000000000000000000000000000000000000000000000000301d8050000f80100000000000000000000000000000000000000000000000000000001000276a400000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000144200000000000000000000000000000000000006000000000000000000000000010123060301ff0500400402010080aad23a67f2ac693abbe543489aeb3f24f561d5170301d8050080020070020070070500c000004001b301c401c407002001f901ff000000004002810290029007002002a402aa000006002002aa02ad000003000002ad02b40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000022473b7bb2f000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000001000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda0291300000000000000000000000000000000000000000000000000000000000000010000000000000000000000004cd00e387622c35bddb9b4c962c136462338bc310000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000645a1ee3ac0000000000000000000000004d3dc340825d4814cad5f9161a55bcecd5053347000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda02913ce8d84b62a6b2adb46cc898e66842b9445275d1b74989c64fffd1f9fc1d6e50b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000214e487265f90e44f2364464f1957efde34a656cb1ff1139b3dd83cea608c5ee090000000000000000000000000000000000000000000000000000000000000000", "output": "0xe450d38c000000000000000000000000b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f000000000000000000000000000000000000000000000006daed7aa2c74f79c4000000000000000000000000000000000000000000000006f16d3dd960d70f04", "error": "execution reverted", "calls": [{"from": "0xb92fe925dc43a0ecde6c8b1a2709c170ec4fff4f", "gas": "0x252a5", "gasUsed": "0x632c", "to": "0xfb42da273158b0f642f59f2ba7cc1d5457481677", "input": "0x095ea7b300000000000000000000000020f6ee51340adeed01a59b0e65cb3703f3dc860c00000000000000000000000000000000000000000000000707ed010ffa5ea444", "output": "0x0000000000000000000000000000000000000000000000000000000000000001", "value": "0x0", "type": "CALL"}, {"from": "0xb92fe925dc43a0ecde6c8b1a2709c170ec4fff4f", "gas": "0x1d62e", "gasUsed": "0xa2a7", "to": "0x20f6ee51340adeed01a59b0e65cb3703f3dc860c", "input": "0x46ec278a000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000002a6026e00e0b92fe925dc43a0ecde6c8b1a2709c170ec4fff4ffb42da273158b0f642f59f2ba7cc1d5457481677833589fcd6edb6e08f4c7c32d4f71b54bda0291300000000000000000000000000000000000000000000000707ed010ffa5ea444e00114e80119e8011d5247dd5e3941528e217ec203345b1780d16a25c8c20340603b95ef04708ad37bf0012100737558fbc499693830614d9970cd462c591fad84dc7f50ac6742b5556bbc0075201fc69582f0a9ce58fac80660faccb6dd8c4d5ffce4c41b6098a6ec1e5b444f1c00e06a69e79ee8106450e811bef0f02710128acb08f800b80707ed010ffa5ea444000000000000000000000000fffd8963efd1fc6a506488495d951d5263988d2500000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000014fb42da273158b0f642f59f2ba7cc1d5457481677000000000000000000000000010123060301270e0129012904013300809399da51c1a85e64cce4b30b554875d2b89b244500070a0000000000000000000000000000000000000000000000000000000000000301d8050000f80100000000000000000000000000000000000000000000000000000001000276a400000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000144200000000000000000000000000000000000006000000000000000000000000010123060301ff0500400402010080aad23a67f2ac693abbe543489aeb3f24f561d5170301d8050080020070020070070500c000004001b301c401c407002001f901ff000000004002810290029007002002a402aa000006002002aa02ad000003000002ad02b400000000000000000000000000000000000000000000000000000000", "output": "0xe450d38c000000000000000000000000b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f000000000000000000000000000000000000000000000006daed7aa2c74f79c4000000000000000000000000000000000000000000000006f16d3dd960d70f04", "error": "execution reverted", "calls": [{"from": "0x20f6ee51340adeed01a59b0e65cb3703f3dc860c", "gas": "0x1b9d0", "gasUsed": "0xbb8", "to": "0x0000000000000000000000000000000000000001", "input": "0xffbd6c0ae1f6e4bcb5711fd14675933f9e2412e8bbbe6f27743cf04c0c27bba3000000000000000000000000000000000000000000000000000000000000001c737558fbc499693830614d9970cd462c591fad84dc7f50ac6742b5556bbc0075201fc69582f0a9ce58fac80660faccb6dd8c4d5ffce4c41b6098a6ec1e5b444f", "output": "0x00000000000000000000000028de4113921bd79388b0ef80a259f871977442e9", "type": "STATICCALL"}, {"from": "0x20f6ee51340adeed01a59b0e65cb3703f3dc860c", "gas": "0x1997b", "gasUsed": "0x2616", "to": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "input": "0x70a08231000000000000000000000000b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f", "output": "0x0000000000000000000000000000000000000000000000000000000000000000", "calls": [{"from": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "gas": "0x17791", "gasUsed": "0x9f9", "to": "0x2ce6311ddae708829bc0784c967b7d77d19fd779", "input": "0x70a08231000000000000000000000000b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f", "output": "0x0000000000000000000000000000000000000000000000000000000000000000", "value": "0x0", "type": "DELEGATECALL"}], "type": "STATICCALL"}, {"from": "0x20f6ee51340adeed01a59b0e65cb3703f3dc860c", "gas": "0x172b3", "gasUsed": "0xcb2", "to": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "input": "0x70a0823100000000000000000000000020f6ee51340adeed01a59b0e65cb3703f3dc860c", "output": "0x0000000000000000000000000000000000000000000000000000000000101b00", "calls": [{"from": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "gas": "0x16a63", "gasUsed": "0x9f9", "to": "0x2ce6311ddae708829bc0784c967b7d77d19fd779", "input": "0x70a0823100000000000000000000000020f6ee51340adeed01a59b0e65cb3703f3dc860c", "output": "0x0000000000000000000000000000000000000000000000000000000000101b00", "value": "0x0", "type": "DELEGATECALL"}], "type": "STATICCALL"}, {"from": "0x20f6ee51340adeed01a59b0e65cb3703f3dc860c", "gas": "0x15bdf", "gasUsed": "0x2db7", "to": "0xfb42da273158b0f642f59f2ba7cc1d5457481677", "input": "0x23b872dd000000000000000000000000b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f00000000000000000000000009ad820aac5779683b481c4674208a4e1b024afa00000000000000000000000000000000000000000000000707ed010ffa5ea444", "output": "0xe450d38c000000000000000000000000b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f000000000000000000000000000000000000000000000006daed7aa2c74f79c4000000000000000000000000000000000000000000000006f16d3dd960d70f04", "error": "execution reverted", "value": "0x0", "type": "CALL"}], "value": "0x0", "type": "CALL"}], "value": "0x0", "type": "CALL"}], "value": "0x0", "type": "CALL"} \ No newline at end of file diff --git a/tools/hindsight/src/decoder/fixtures/trace_revert_zeroex_slippage_0x157e025b.json b/tools/hindsight/src/decoder/fixtures/trace_revert_zeroex_slippage_0x157e025b.json new file mode 100644 index 00000000..772d4b90 --- /dev/null +++ b/tools/hindsight/src/decoder/fixtures/trace_revert_zeroex_slippage_0x157e025b.json @@ -0,0 +1 @@ +{"from": "0xf70da97812cb96acdf810712aa562db8dfa3dbef", "gas": "0x121681", "gasUsed": "0x4a627", "to": "0xb92fe925dc43a0ecde6c8b1a2709c170ec4fff4f", "input": "0xcd6e13f70000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b400000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000008400000000000000000000000000000000000001ff3684f28c67538d4d072c2273400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001c01d57175ff16a1000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000007442213bc0b0000000000000000000000007747f8d2a76bd6345cc29622a946a929647f235900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001c01d57175ff16a10000000000000000000000007747f8d2a76bd6345cc29622a946a929647f235900000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000006641fff991f000000000000000000000000b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda0291300000000000000000000000000000000000000000000000000000000d92d88c000000000000000000000000000000000000000000000000000000000000000a0138b51c04603f9ab1fe5513d0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000026000000000000000000000000000000000000000000000000000000000000003a000000000000000000000000000000000000000000000000000000000000004e00000000000000000000000000000000000000000000000000000000000000044bd01c226000000000000000000000000000000000000000000000000000000006a6e693a0000000000000000000000000000000000000000000000001c01d57175ff16a100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010438c9c147000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee00000000000000000000000000000000000000000000000000000000000027100000000000000000000000004200000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000024d0e30db000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010438c9c147000000000000000000000000420000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000027100000000000000000000000004200000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000242e1a7d4d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000104d47868c9000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee00000000000000000000000000000000000000000000000000000000000027100000000000000000000000006a6951db3bb99f76a8ccb9f8a8399b613666298b00000000000000000000000000000000000000000000000000005af3107a400000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000dd2ab92600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008434ee90ca000000000000000000000000f5c4f3dc02c3fb9279495a8fef7b0741da956157000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda0291300000000000000000000000000000000000000000000000000000000dd9c30950000000000000000000000000000000000000000000000000000000000002710000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000001a49bb43718000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000001000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda029130000000000000000000000000000000000000000000000000000000000000001000000000000000000000000f70da97812cb96acdf810712aa562db8dfa3dbef0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000210000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "output": "0x97a6f3b9000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda0291300000000000000000000000000000000000000000000000000000000dd2ab9260000000000000000000000000000000000000000000000000000000000000000", "error": "execution reverted", "calls": [{"from": "0xb92fe925dc43a0ecde6c8b1a2709c170ec4fff4f", "gas": "0x11011d", "gasUsed": "0x3d3b3", "to": "0x0000000000001ff3684f28c67538d4d072c22734", "input": "0x2213bc0b0000000000000000000000007747f8d2a76bd6345cc29622a946a929647f235900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001c01d57175ff16a10000000000000000000000007747f8d2a76bd6345cc29622a946a929647f235900000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000006641fff991f000000000000000000000000b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda0291300000000000000000000000000000000000000000000000000000000d92d88c000000000000000000000000000000000000000000000000000000000000000a0138b51c04603f9ab1fe5513d0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000026000000000000000000000000000000000000000000000000000000000000003a000000000000000000000000000000000000000000000000000000000000004e00000000000000000000000000000000000000000000000000000000000000044bd01c226000000000000000000000000000000000000000000000000000000006a6e693a0000000000000000000000000000000000000000000000001c01d57175ff16a100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010438c9c147000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee00000000000000000000000000000000000000000000000000000000000027100000000000000000000000004200000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000024d0e30db000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010438c9c147000000000000000000000000420000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000027100000000000000000000000004200000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000242e1a7d4d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000104d47868c9000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee00000000000000000000000000000000000000000000000000000000000027100000000000000000000000006a6951db3bb99f76a8ccb9f8a8399b613666298b00000000000000000000000000000000000000000000000000005af3107a400000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000dd2ab92600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008434ee90ca000000000000000000000000f5c4f3dc02c3fb9279495a8fef7b0741da956157000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda0291300000000000000000000000000000000000000000000000000000000dd9c309500000000000000000000000000000000000000000000000000000000000027100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "output": "0x97a6f3b9000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda0291300000000000000000000000000000000000000000000000000000000dd2ab9260000000000000000000000000000000000000000000000000000000000000000", "error": "execution reverted", "calls": [{"from": "0x0000000000001ff3684f28c67538d4d072c22734", "gas": "0x10b138", "gasUsed": "0xe6", "to": "0x7747f8d2a76bd6345cc29622a946a929647f2359", "input": "0x70a08231000000000000000000000000b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f", "output": "0x00", "type": "STATICCALL"}, {"from": "0x0000000000001ff3684f28c67538d4d072c22734", "gas": "0x109399", "gasUsed": "0x3a92e", "to": "0x7747f8d2a76bd6345cc29622a946a929647f2359", "input": "0x1fff991f000000000000000000000000b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda0291300000000000000000000000000000000000000000000000000000000d92d88c000000000000000000000000000000000000000000000000000000000000000a0138b51c04603f9ab1fe5513d0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000026000000000000000000000000000000000000000000000000000000000000003a000000000000000000000000000000000000000000000000000000000000004e00000000000000000000000000000000000000000000000000000000000000044bd01c226000000000000000000000000000000000000000000000000000000006a6e693a0000000000000000000000000000000000000000000000001c01d57175ff16a100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010438c9c147000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee00000000000000000000000000000000000000000000000000000000000027100000000000000000000000004200000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000024d0e30db000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010438c9c147000000000000000000000000420000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000027100000000000000000000000004200000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000242e1a7d4d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000104d47868c9000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee00000000000000000000000000000000000000000000000000000000000027100000000000000000000000006a6951db3bb99f76a8ccb9f8a8399b613666298b00000000000000000000000000000000000000000000000000005af3107a400000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000dd2ab92600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008434ee90ca000000000000000000000000f5c4f3dc02c3fb9279495a8fef7b0741da956157000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda0291300000000000000000000000000000000000000000000000000000000dd9c3095000000000000000000000000000000000000000000000000000000000000271000000000000000000000000000000000000000000000000000000000b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f", "output": "0x97a6f3b9000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda0291300000000000000000000000000000000000000000000000000000000dd2ab9260000000000000000000000000000000000000000000000000000000000000000", "error": "execution reverted", "calls": [{"from": "0x7747f8d2a76bd6345cc29622a946a929647f2359", "gas": "0x102155", "gasUsed": "0x5cfa", "to": "0x4200000000000000000000000000000000000006", "input": "0xd0e30db00000000000000000000000000000000000000000000000001c01d57175ff16a1", "value": "0x1c01d57175ff16a1", "type": "CALL"}, {"from": "0x7747f8d2a76bd6345cc29622a946a929647f2359", "gas": "0xfbf14", "gasUsed": "0x1c9", "to": "0x4200000000000000000000000000000000000006", "input": "0x70a082310000000000000000000000007747f8d2a76bd6345cc29622a946a929647f2359", "output": "0x0000000000000000000000000000000000000000000000001c01d57175ff16a1", "type": "STATICCALL"}, {"from": "0x7747f8d2a76bd6345cc29622a946a929647f2359", "gas": "0xfbba5", "gasUsed": "0x23b3", "to": "0x4200000000000000000000000000000000000006", "input": "0x2e1a7d4d0000000000000000000000000000000000000000000000001c01d57175ff16a1", "calls": [{"from": "0x4200000000000000000000000000000000000006", "gas": "0x8fc", "gasUsed": "0x4b", "to": "0x7747f8d2a76bd6345cc29622a946a929647f2359", "input": "0x", "value": "0x1c01d57175ff16a1", "type": "CALL"}], "value": "0x0", "type": "CALL"}, {"from": "0x7747f8d2a76bd6345cc29622a946a929647f2359", "gas": "0xf6e22", "gasUsed": "0x2aba7", "to": "0x6a6951db3bb99f76a8ccb9f8a8399b613666298b", "input": "0xf513ef4c00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000004ed5000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000ffffffffffffffffffffffffffffffff000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "calls": [{"from": "0x6a6951db3bb99f76a8ccb9f8a8399b613666298b", "gas": "0xf1de5", "gasUsed": "0x298ad", "to": "0x57dafd63aa581657b0a76db413f371e17a1b0cbe", "input": "0xf513ef4c00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000004ed5000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000ffffffffffffffffffffffffffffffff000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "calls": [{"from": "0x6a6951db3bb99f76a8ccb9f8a8399b613666298b", "gas": "0xeadb7", "gasUsed": "0x4cbf", "to": "0x4ad6120c42d5e1bf8ec56f37c437da7b8621c582", "input": "0xb8b5ce81", "value": "0x0", "type": "CALL"}, {"from": "0x6a6951db3bb99f76a8ccb9f8a8399b613666298b", "gas": "0xe604e", "gasUsed": "0x20e1", "to": "0x4ad6120c42d5e1bf8ec56f37c437da7b8621c582", "input": "0x24675a09000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004ed500000000000000000000000000000000ffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000000000000001", "output": "0x0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "type": "STATICCALL"}, {"from": "0x6a6951db3bb99f76a8ccb9f8a8399b613666298b", "gas": "0xe2fc0", "gasUsed": "0x59d4", "to": "0x7c976801c8589d924ce93bbcce6966d34edf91bf", "input": "0xc3f909d4", "output": "0x00000000000000000000000000000000000000000000000000005af3107a400000000000000000000000000000000000000000000000000000000000000000010000000000000000000000004200000000000000000000000000000000000006000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda0291300000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000030bf17711657bfb5d6645aad39984d5ccb086ec0000000000000000000000003484f248f7b0dfe1f826b145e9d582a6e034ccb400000000000000000000000000000000000000000000000006f05b59d3b2000000000000000000000000000000000000000000000000000000005af3107a4000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "calls": [{"from": "0x7c976801c8589d924ce93bbcce6966d34edf91bf", "gas": "0xde4a6", "gasUsed": "0x46cb", "to": "0xf1466addf635bf0ba0a0c4bb9fecaa03b0945510", "input": "0xc3f909d4", "output": "0x00000000000000000000000000000000000000000000000000005af3107a400000000000000000000000000000000000000000000000000000000000000000010000000000000000000000004200000000000000000000000000000000000006000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda0291300000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000030bf17711657bfb5d6645aad39984d5ccb086ec0000000000000000000000003484f248f7b0dfe1f826b145e9d582a6e034ccb400000000000000000000000000000000000000000000000006f05b59d3b2000000000000000000000000000000000000000000000000000000005af3107a4000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "value": "0x0", "type": "DELEGATECALL"}], "type": "STATICCALL"}, {"from": "0x6a6951db3bb99f76a8ccb9f8a8399b613666298b", "gas": "0xdc613", "gasUsed": "0xb92", "to": "0x3484f248f7b0dfe1f826b145e9d582a6e034ccb4", "input": "0xe146af4700000000000000000000000000000000000000000000000000000100000000010000000000000000000000000000000000000000000000000000000000004ed500000000000000000000000000000000ffffffffffffffffffffffffffffffff", "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "type": "STATICCALL"}, {"from": "0x6a6951db3bb99f76a8ccb9f8a8399b613666298b", "gas": "0xdb6af", "gasUsed": "0xf3e", "to": "0x4200000000000000000000000000000000000006", "input": "0xd0e30db0", "value": "0x0", "type": "CALL"}, {"from": "0x6a6951db3bb99f76a8ccb9f8a8399b613666298b", "gas": "0xda5dc", "gasUsed": "0x1193", "to": "0x4200000000000000000000000000000000000006", "input": "0x095ea7b30000000000000000000000007c976801c8589d924ce93bbcce6966d34edf91bf0000000000000000000000000000000000000000000000000000000000000000", "output": "0x0000000000000000000000000000000000000000000000000000000000000001", "value": "0x0", "type": "CALL"}, {"from": "0x6a6951db3bb99f76a8ccb9f8a8399b613666298b", "gas": "0xd92c2", "gasUsed": "0xc62", "to": "0x7c976801c8589d924ce93bbcce6966d34edf91bf", "input": "0x506dcce40000000000000000000000007747f8d2a76bd6345cc29622a946a929647f23590000000000000000000000006a6951db3bb99f76a8ccb9f8a8399b613666298b", "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "calls": [{"from": "0x7c976801c8589d924ce93bbcce6966d34edf91bf", "gas": "0xd5b64", "gasUsed": "0xb26", "to": "0xf1466addf635bf0ba0a0c4bb9fecaa03b0945510", "input": "0x506dcce40000000000000000000000007747f8d2a76bd6345cc29622a946a929647f23590000000000000000000000006a6951db3bb99f76a8ccb9f8a8399b613666298b", "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "value": "0x0", "type": "DELEGATECALL"}], "type": "STATICCALL"}, {"from": "0x6a6951db3bb99f76a8ccb9f8a8399b613666298b", "gas": "0xd837b", "gasUsed": "0x6fbd", "to": "0x7c976801c8589d924ce93bbcce6966d34edf91bf", "input": "0xf513ef4c00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000004ed5000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000ffffffffffffffffffffffffffffffff000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000006a6e6811", "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "calls": [{"from": "0x7c976801c8589d924ce93bbcce6966d34edf91bf", "gas": "0xd4c37", "gasUsed": "0x6e57", "to": "0xf1466addf635bf0ba0a0c4bb9fecaa03b0945510", "input": "0xf513ef4c00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000004ed5000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000ffffffffffffffffffffffffffffffff000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000006a6e6811", "output": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "calls": [{"from": "0x7c976801c8589d924ce93bbcce6966d34edf91bf", "gas": "0xcf87f", "gasUsed": "0x31c", "to": "0x3484f248f7b0dfe1f826b145e9d582a6e034ccb4", "input": "0xa558898f00000000000000000000000000000000000000000000000000000100000000010000000000000000000000000000000000000000000000000000000000004ed5", "output": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "value": "0x0", "type": "CALL"}, {"from": "0x7c976801c8589d924ce93bbcce6966d34edf91bf", "gas": "0xcd0c2", "gasUsed": "0x153", "to": "0x3484f248f7b0dfe1f826b145e9d582a6e034ccb4", "input": "0x2e286683", "output": "0x0000000000000000000000000000000000000000000000000000000000000000", "type": "STATICCALL"}, {"from": "0x7c976801c8589d924ce93bbcce6966d34edf91bf", "gas": "0xcc39b", "gasUsed": "0x923", "to": "0x030bf17711657bfb5d6645aad39984d5ccb086ec", "input": "0x2e286683", "output": "0x0000000000000000000000000000000000000000000000000000000000000000", "type": "STATICCALL"}, {"from": "0x7c976801c8589d924ce93bbcce6966d34edf91bf", "gas": "0xcb803", "gasUsed": "0xb7f", "to": "0x4ad6120c42d5e1bf8ec56f37c437da7b8621c582", "input": "0x558f514800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "value": "0x0", "type": "CALL"}], "value": "0x0", "type": "DELEGATECALL"}], "value": "0x0", "type": "CALL"}, {"from": "0x6a6951db3bb99f76a8ccb9f8a8399b613666298b", "gas": "0xcffb3", "gasUsed": "0x1f25", "to": "0x01dda2b73ede58c840d73379710a18cbc5fe290e", "input": "0x4d35daf300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "calls": [{"from": "0x01dda2b73ede58c840d73379710a18cbc5fe290e", "gas": "0xcaf2d", "gasUsed": "0x123", "to": "0x947c13afe52b8676144664e12487544718089267", "input": "0x8d64be7e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "output": "0x0000000000000000000000000000000000000000000000000000000000000000", "type": "STATICCALL"}], "value": "0x0", "type": "CALL"}, {"from": "0x6a6951db3bb99f76a8ccb9f8a8399b613666298b", "gas": "0xcdf98", "gasUsed": "0x1c9", "to": "0x4200000000000000000000000000000000000006", "input": "0x70a082310000000000000000000000006a6951db3bb99f76a8ccb9f8a8399b613666298b", "output": "0x0000000000000000000000000000000000000000000000000000000000000000", "type": "STATICCALL"}, {"from": "0x6a6951db3bb99f76a8ccb9f8a8399b613666298b", "gas": "0xcd264", "gasUsed": "0x2616", "to": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "input": "0x70a082310000000000000000000000006a6951db3bb99f76a8ccb9f8a8399b613666298b", "output": "0x0000000000000000000000000000000000000000000000000000000000000000", "calls": [{"from": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "gas": "0xc8397", "gasUsed": "0x9f9", "to": "0x2ce6311ddae708829bc0784c967b7d77d19fd779", "input": "0x70a082310000000000000000000000006a6951db3bb99f76a8ccb9f8a8399b613666298b", "output": "0x0000000000000000000000000000000000000000000000000000000000000000", "value": "0x0", "type": "DELEGATECALL"}], "type": "STATICCALL"}, {"from": "0x6a6951db3bb99f76a8ccb9f8a8399b613666298b", "gas": "0xc91b3", "gasUsed": "0x4b", "to": "0x7747f8d2a76bd6345cc29622a946a929647f2359", "input": "0x", "value": "0x1c01bb41f5374000", "type": "CALL"}, {"from": "0x6a6951db3bb99f76a8ccb9f8a8399b613666298b", "gas": "0xc8d9c", "gasUsed": "0x39ae", "to": "0x4ad6120c42d5e1bf8ec56f37c437da7b8621c582", "input": "0xec3a5314", "calls": [{"from": "0x4ad6120c42d5e1bf8ec56f37c437da7b8621c582", "gas": "0xc4e3a", "gasUsed": "0x901", "to": "0x49b1a6b424c19803fa64a5b40baa7f0428e258b3", "input": "0x18160ddd", "output": "0x000000000000000000000000000000000000000000005987575d2b7d9d3cecb0", "type": "STATICCALL"}], "value": "0x0", "type": "CALL"}], "value": "0x1c01bb41f5374000", "type": "DELEGATECALL"}], "value": "0x1c01bb41f5374000", "type": "CALL"}, {"from": "0x7747f8d2a76bd6345cc29622a946a929647f2359", "gas": "0xccc28", "gasUsed": "0x1561", "to": "0x6a6951db3bb99f76a8ccb9f8a8399b613666298b", "input": "0xc3f909d4", "output": "0x00000000000000000000000000000000000000000000000000005af3107a400000000000000000000000000000000000000000000000000000000000000000010000000000000000000000004200000000000000000000000000000000000006000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda0291300000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000030bf17711657bfb5d6645aad39984d5ccb086ec0000000000000000000000003484f248f7b0dfe1f826b145e9d582a6e034ccb400000000000000000000000000000000000000000000000006f05b59d3b2000000000000000000000000000000000000000000000000000000005af3107a4000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "calls": [{"from": "0x6a6951db3bb99f76a8ccb9f8a8399b613666298b", "gas": "0xc97ea", "gasUsed": "0x13ec", "to": "0x57dafd63aa581657b0a76db413f371e17a1b0cbe", "input": "0xc3f909d4", "output": "0x00000000000000000000000000000000000000000000000000005af3107a400000000000000000000000000000000000000000000000000000000000000000010000000000000000000000004200000000000000000000000000000000000006000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda0291300000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000030bf17711657bfb5d6645aad39984d5ccb086ec0000000000000000000000003484f248f7b0dfe1f826b145e9d582a6e034ccb400000000000000000000000000000000000000000000000006f05b59d3b2000000000000000000000000000000000000000000000000000000005af3107a4000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "calls": [{"from": "0x6a6951db3bb99f76a8ccb9f8a8399b613666298b", "gas": "0xc61aa", "gasUsed": "0x9c0", "to": "0x7c976801c8589d924ce93bbcce6966d34edf91bf", "input": "0xc3f909d4", "output": "0x00000000000000000000000000000000000000000000000000005af3107a400000000000000000000000000000000000000000000000000000000000000000010000000000000000000000004200000000000000000000000000000000000006000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda0291300000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000030bf17711657bfb5d6645aad39984d5ccb086ec0000000000000000000000003484f248f7b0dfe1f826b145e9d582a6e034ccb400000000000000000000000000000000000000000000000006f05b59d3b2000000000000000000000000000000000000000000000000000000005af3107a4000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "calls": [{"from": "0x7c976801c8589d924ce93bbcce6966d34edf91bf", "gas": "0xc2f16", "gasUsed": "0x84b", "to": "0xf1466addf635bf0ba0a0c4bb9fecaa03b0945510", "input": "0xc3f909d4", "output": "0x00000000000000000000000000000000000000000000000000005af3107a400000000000000000000000000000000000000000000000000000000000000000010000000000000000000000004200000000000000000000000000000000000006000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda0291300000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000030bf17711657bfb5d6645aad39984d5ccb086ec0000000000000000000000003484f248f7b0dfe1f826b145e9d582a6e034ccb400000000000000000000000000000000000000000000000006f05b59d3b2000000000000000000000000000000000000000000000000000000005af3107a4000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "value": "0x0", "type": "DELEGATECALL"}], "type": "STATICCALL"}], "value": "0x0", "type": "DELEGATECALL"}], "type": "STATICCALL"}], "value": "0x1c01d57175ff16a1", "type": "CALL"}], "value": "0x1c01d57175ff16a1", "type": "CALL"}], "value": "0x1c01d57175ff16a1", "type": "CALL"} \ No newline at end of file diff --git a/tools/hindsight/src/decoder/intents/cow.rs b/tools/hindsight/src/decoder/intents/cow.rs index afc0868c..3aa49410 100644 --- a/tools/hindsight/src/decoder/intents/cow.rs +++ b/tools/hindsight/src/decoder/intents/cow.rs @@ -147,7 +147,7 @@ mod tests { use super::*; use crate::decoder::{ registry::Registry, - test_utils::{addr, receipt, swap, tx_hash}, + test_utils::{addr, frame, receipt, swap, tx_hash}, transfer_ledger::TransferLedger, }; @@ -187,6 +187,7 @@ mod tests { let mut code_cache = HashMap::new(); let receipt = receipt(tx_hash(1), addr(2), Some(COW_SETTLEMENT), logs); let transfer_ledger = TransferLedger::from_transaction(&[], &[]); + let root = frame("CALL", addr(2), COW_SETTLEMENT, 0); let mut ctx = DecodeContext { provider: &provider, registry: ®istry, @@ -195,6 +196,7 @@ mod tests { entry_point: COW_SETTLEMENT, transfer_ledger: &transfer_ledger, input: &[], + root: &root, venue: None, }; CowSettlement.decode(&mut ctx).await diff --git a/tools/hindsight/src/decoder/matching.rs b/tools/hindsight/src/decoder/matching.rs index 5c980e3f..5ebd9079 100644 --- a/tools/hindsight/src/decoder/matching.rs +++ b/tools/hindsight/src/decoder/matching.rs @@ -12,53 +12,150 @@ use tracing::debug; use crate::decoder::{registry::Registry, solvers}; -/// A transaction identified as a solver trade, ready to be traced and decoded. +/// A transaction identified as a solver trade, ready to be traced and decoded — settled or +/// reverted, distinguished by `reverted`. A reverted match carries no logs to net a settled +/// amount from, so it is decoded differently (see `decoder::decode_transaction`), but it is the +/// same shape as a settled one otherwise: it was still a trade, just one that did not fill. Every +/// field is a reference or `Copy`, so the type itself is `Copy` — passed around freely rather than +/// threaded by move. +#[derive(Clone, Copy)] pub(crate) struct MatchedSolverTrade<'a> { pub receipt: &'a AnyTransactionReceipt, /// The contract the transaction entered through (`tx.to`). pub entry_point: Address, + pub reverted: bool, } -/// Match a receipt as a solver trade. +/// Venues whose reverted swaps are worth decoding for the trader's `min_amount_out`. A reverted +/// transaction emits no logs, so entry-point matching is the only signal available — gated by +/// venue name (not chain), so a venue not listed here is simply not attempted. +const REVERT_DECODED_VENUES: &[&str] = &["relay"]; + +/// Match a receipt as a solver trade, settled or reverted. +/// +/// A settled transaction qualifies two ways: its entry point (`tx.to`) is a known venue or +/// solver, or one of its logs was emitted by a known solver (filler-initiated intent fills, +/// where `tx.to` is a rotating filler). Matched transactions whose logs mark a non-swap order +/// shape are vetoed here (see `solvers::solver_veto`), before they cost a trace. /// -/// A transaction qualifies two ways: its entry point (`tx.to`) is a known -/// venue or solver, or one of its logs was emitted by a known solver -/// (filler-initiated intent fills, where `tx.to` is a rotating filler). -/// Matched transactions whose logs mark a non-swap order shape are vetoed -/// here (see `solvers::solver_veto`), before they cost a trace. +/// A reverted transaction emits no logs, so entry-point matching is the only signal available: it +/// matches only when `tx.to` is a venue in [`REVERT_DECODED_VENUES`]. There is no veto path for +/// reverts — a veto reads logs a revert never has. pub(crate) fn select<'a>( receipt: &'a AnyTransactionReceipt, registry: &Registry, ) -> Option> { - let matched = match_entry(receipt, registry)?; - if let Some(veto) = solvers::solver_veto(matched.receipt.logs(), matched.entry_point, registry) - { - debug!( - tx = %matched.receipt.transaction_hash, - venue = %registry.label(matched.entry_point), - ?veto, - "matched transaction is not a same-chain swap; skipping" - ); - return None; + if receipt.status() { + let matched = match_settled(receipt, registry)?; + if let Some(veto) = + solvers::solver_veto(matched.receipt.logs(), matched.entry_point, registry) + { + debug!( + tx = %matched.receipt.transaction_hash, + venue = %registry.label(matched.entry_point), + ?veto, + "matched transaction is not a same-chain swap; skipping" + ); + return None; + } + return Some(matched); } - Some(matched) + match_reverted(receipt, registry) } -/// Match a receipt by its entry point or its solver logs. -fn match_entry<'a>( +/// Match a settled receipt by its entry point or its solver logs. +fn match_settled<'a>( receipt: &'a AnyTransactionReceipt, registry: &Registry, ) -> Option> { - if !receipt.status() { - return None; - } let entry_point = receipt.to?; if registry.is_known(entry_point) { - return Some(MatchedSolverTrade { receipt, entry_point }); + return Some(MatchedSolverTrade { receipt, entry_point, reverted: false }); } let via_log = receipt .logs() .iter() .any(|log| registry.is_solver(log.address())); - via_log.then_some(MatchedSolverTrade { receipt, entry_point }) + via_log.then_some(MatchedSolverTrade { receipt, entry_point, reverted: false }) +} + +/// Match a reverted receipt as a candidate for revert decoding: `tx.to` is an entry point of a +/// venue in [`REVERT_DECODED_VENUES`]. +fn match_reverted<'a>( + receipt: &'a AnyTransactionReceipt, + registry: &Registry, +) -> Option> { + let entry_point = receipt.to?; + let venue_name = registry.venue_name(entry_point)?; + REVERT_DECODED_VENUES + .iter() + .find(|&&name| name == venue_name)?; + Some(MatchedSolverTrade { receipt, entry_point, reverted: true }) +} + +#[cfg(test)] +mod tests { + use alloy::primitives::address; + + use super::*; + use crate::decoder::test_utils::{addr, receipt, reverted_receipt, tx_hash}; + + fn relay_entry_point(registry: &Registry) -> Address { + *registry + .venue("relay") + .unwrap() + .entry_points + .iter() + .next() + .unwrap() + } + + #[test] + fn test_select_reverted_relay_tx_matches() { + let registry = Registry::ethereum(); + let relay = relay_entry_point(®istry); + let tx = reverted_receipt(tx_hash(1), addr(1), Some(relay)); + + let matched = select(&tx, ®istry).unwrap(); + assert_eq!(matched.entry_point, relay); + assert!(matched.reverted); + } + + #[test] + fn test_select_reverted_non_venue_tx_does_not_match() { + let registry = Registry::ethereum(); + // A registered solver (not a revert-decoded venue) reverting must not match: reverts are + // only decoded for venues in REVERT_DECODED_VENUES. + let oneinch = address!("0x111111125421ca6dc452d289314280a0f8842a65"); + let tx = reverted_receipt(tx_hash(1), addr(1), Some(oneinch)); + assert!(select(&tx, ®istry).is_none()); + + // An address the registry knows nothing about. + let unknown = reverted_receipt(tx_hash(2), addr(1), Some(addr(200))); + assert!(select(&unknown, ®istry).is_none()); + } + + #[test] + fn test_select_reverted_requires_reverted_status() { + let registry = Registry::ethereum(); + let relay = relay_entry_point(®istry); + let successful = receipt(tx_hash(1), addr(1), Some(relay), vec![]); + let matched = select(&successful, ®istry).unwrap(); + assert!(!matched.reverted); + } + + #[test] + fn test_select_successful_path_unchanged() { + // The existing successful-match path is untouched by the revert path. + let registry = Registry::ethereum(); + let relay = relay_entry_point(®istry); + let successful = receipt(tx_hash(1), addr(1), Some(relay), vec![]); + let matched = select(&successful, ®istry).unwrap(); + assert_eq!(matched.entry_point, relay); + assert!(!matched.reverted); + + let reverted = reverted_receipt(tx_hash(2), addr(1), Some(relay)); + let matched = select(&reverted, ®istry).unwrap(); + assert!(matched.reverted); + } } diff --git a/tools/hindsight/src/decoder/mod.rs b/tools/hindsight/src/decoder/mod.rs index 59d57842..3b2ec8f6 100644 --- a/tools/hindsight/src/decoder/mod.rs +++ b/tools/hindsight/src/decoder/mod.rs @@ -44,7 +44,7 @@ use futures::stream::StreamExt; use tracing::{debug, warn}; use crate::decoder::{ - decode::{recover, DecodeContext, GasScope}, + decode::{recover, DecodeContext, GasScope, TraderFlow}, matching::MatchedSolverTrade, trace::{collect_native_transfers, fetch_trace, route_gas}, transfer_ledger::TransferLedger, @@ -52,12 +52,37 @@ use crate::decoder::{ pub(crate) use crate::decoder::{ registry::Registry, sandwich::SandwichEvidence, - solvers::{attribution::AttributionSource, SolverQuote}, + solvers::{attribution::AttributionSource, SwapIntent}, + trace::RevertCause, }; -/// A decoded solver trade: what token went in, what came out. +/// Whether a matched transaction settled or reverted on-chain. Serialized flattened into +/// `DecodedTrade`'s own JSON object (`"status":"settled"`, or `"status":"reverted"` plus +/// `"cause"`), so a settled and a reverted trade read as the same record shape, told apart by +/// this one field rather than by which fields happen to be present. +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)] +#[serde(tag = "status", rename_all = "snake_case")] +pub(crate) enum TradeStatus { + Settled, + Reverted { cause: RevertCause }, +} + +/// A decoded solver trade: what token went in, what came out — settled or reverted, told apart by +/// `status`. Both are trades in the same sense: a trader routed through a solver; one just did +/// not fill. /// /// Native ETH is represented as `Address::ZERO`. +/// +/// # The settled/reverted invariant +/// +/// A settled trade always carries `token_in`/`token_out`/`amount_in`/`amount_out`: +/// `Decoder::decode_settled` only ever returns `Some(DecodedTrade)` once a `TraderFlow` decoder +/// has recovered a real swap, and never leaves them empty. A reverted trade carries +/// `token_in`/`token_out`/`amount_in` only when its solver frame's calldata parsed into a +/// `SwapIntent` — a revert emits no logs, so calldata is the only source; when it did not parse, +/// all three are `None`. The trade is still recorded (not filtered out) so parser coverage stays +/// measurable against every reverted candidate. `amount_out` is settled-only: nothing was +/// delivered on a revert, by definition. #[derive(Debug, Clone, serde::Serialize)] pub(crate) struct DecodedTrade { pub tx_hash: TxHash, @@ -65,53 +90,129 @@ pub(crate) struct DecodedTrade { /// The transaction's position in its block, from the receipt (falls back to its position in /// the fetched receipt slice when the RPC omitted it). pub tx_index: u64, + #[serde(flatten)] + pub status: TradeStatus, pub venue: String, pub solver: String, /// The evidence tier the solver label came from (see `solvers::attribution`). Downstream /// analysis weighs low-trust tiers (`largest_call`, fallback) differently — e.g. when judging /// an embedded quote. pub solver_source: AttributionSource, - /// Which decoder recovered this trade (see `decode`). Once several decoders can carry a - /// venue's trades this measures how often each one carries a trade the others could not. + /// Which decoder recovered this trade (see `decode`), or `"reverted"` when nothing settled to + /// hand to a `TradeDecoder` — a reverted trade's terms, when recoverable, come straight from + /// the solver frame's own calldata, not the netting/calldata decoder chain. Once several + /// decoders can carry a venue's trades this measures how often each one carries a trade the + /// others could not. pub decoder: &'static str, pub sender: Address, - pub token_in: Address, - pub token_out: Address, + /// `None` only for a reverted trade whose solver frame's calldata did not parse — see the + /// settled/reverted invariant above. + #[serde(skip_serializing_if = "Option::is_none")] + pub token_in: Option
, + #[serde(skip_serializing_if = "Option::is_none")] + pub token_out: Option
, /// Input amount that actually entered the swap — a venue fee taken from the input (see - /// `venue_fee_in`) is already subtracted, so a re-solve compares like-for-like. - pub amount_in: U256, + /// `venue_fee_in`) is already subtracted, so a re-solve compares like-for-like. For a + /// reverted trade this is the calldata-declared amount, not a netted one (see the + /// settled/reverted invariant above). + #[serde(skip_serializing_if = "Option::is_none")] + pub amount_in: Option, /// Gross swap output — a venue fee taken from the output (see `venue_fee_out`) is added /// back, so the settled amount is the full swap proceeds, comparable to Fynd's gross output. - pub amount_out: U256, + /// `None` for a reverted trade: nothing was delivered. + #[serde(skip_serializing_if = "Option::is_none")] + pub amount_out: Option, /// Venue fee taken from the input token before swapping (e.g. Relay's fee), in `token_in` - /// units. `None` when no known fee collector took a cut. Recorded for transparency; it is - /// already excluded from `amount_in`. + /// units. `None` when no known fee collector took a cut, or the trade reverted. Recorded for + /// transparency; it is already excluded from `amount_in`. #[serde(skip_serializing_if = "Option::is_none")] pub venue_fee_in: Option, /// Venue fee taken from the output token after swapping, in `token_out` units. `None` when - /// no known fee collector took a cut. Recorded for transparency; it is already added back into - /// `amount_out`. + /// no known fee collector took a cut, or the trade reverted. Recorded for transparency; it is + /// already added back into `amount_out`. #[serde(skip_serializing_if = "Option::is_none")] pub venue_fee_out: Option, /// Wei cost of the gas the trader paid for the settled route (`gas_used × /// effective_gas_price`). For venue-wrapped entries (Relay, `MetaMask`) the venue's own /// overhead is excluded — it is charged whichever router the venue picks, like the venue /// fee. `None` when the trader did not pay the transaction's gas (intent fills, solver - /// rebalances) or the route's gas could not be isolated from the trace. + /// rebalances), the route's gas could not be isolated from the trace, or the trade reverted + /// (nothing settled to charge gas against). #[serde(skip_serializing_if = "Option::is_none")] pub settled_gas: Option, - /// The solver's own off-chain quote for this swap, recovered from calldata (see - /// `solvers::embedded_quote` for the solvers that declare one). Informational — it is what - /// the venue compared against at decision time, as opposed to `amount_out`, which is what - /// execution delivered. + /// The on-chain enforced floor declared in the settling solver frame's own calldata (see + /// `solvers::swap_intent` for the solvers that declare one). A settled trade cleared this by + /// construction; it is recorded so avoidance analysis has the same field on both settled and + /// reverted trades. `None` when no solver frame was found or its calldata did not parse. + #[serde(skip_serializing_if = "Option::is_none")] + pub min_amount_out: Option, + /// The solver's own off-chain quote, when its calldata declares one (unit-checked against + /// the settled amount; a quote that fails the check is dropped, keeping `min_amount_out`). + /// This is calldata-declared and self-reported — distinct from `amount_out`, which is what + /// the settlement ledger actually delivered. #[serde(skip_serializing_if = "Option::is_none")] - pub quote: Option, + pub declared_quote: Option, + /// Unix timestamp of `declared_quote`, when the calldata carries one. + #[serde(skip_serializing_if = "Option::is_none")] + pub quote_timestamp: Option, /// Evidence that a front-run and a back-run bracketed this trade (see - /// `sandwich::detect`). `None` when no bracket pair was found. + /// `sandwich::detect`). `None` when no bracket pair was found, or the trade reverted — + /// nothing settled to be sandwiched around. #[serde(skip_serializing_if = "Option::is_none")] pub sandwich: Option, } +impl DecodedTrade { + /// This trade's swap terms, when known — always `Some` for a settled trade (see the + /// settled/reverted invariant above), and for a reverted trade only when its solver frame's + /// calldata parsed. `resolve::resolve_block_range` uses this to decide whether a trade can be + /// solved at all: a trade with unknown terms is recorded but not re-solved. + pub(crate) fn terms(&self) -> Option<(Address, Address, U256)> { + Some((self.token_in?, self.token_out?, self.amount_in?)) + } +} + +/// Log a disagreement between the calldata-recovered intent and the netted flow, on any of the +/// three terms they both claim. The ledger stays authoritative for what settled; two +/// independently-derived readings landing on different terms is diagnostic signal we would +/// otherwise lose, not a decode failure. Skipped for `relay-calldata`, whose flow already IS the +/// intent, so there is nothing independent to disagree with. +fn warn_on_intent_disagreement( + decoder: &str, + tx_hash: TxHash, + intent: Option<&SwapIntent>, + flow: &TraderFlow, +) { + let Some(intent) = intent.filter(|_| decoder != "relay-calldata") else { + return; + }; + if intent.token_in == flow.swap.token_in && + intent.token_out == flow.swap.token_out && + intent.amount_in == flow.swap.amount_in + { + return; + } + warn!( + tx = %tx_hash, + intent_token_in = %intent.token_in, + intent_token_out = %intent.token_out, + intent_amount_in = %intent.amount_in, + flow_token_in = %flow.swap.token_in, + flow_token_out = %flow.swap.token_out, + flow_amount_in = %flow.swap.amount_in, + "calldata-recovered intent disagrees with the netted flow" + ); +} + +/// Copy the calldata-declared terms off a parsed intent, or all-`None` when no intent was +/// recovered. +fn intent_fields(intent: Option<&SwapIntent>) -> (Option, Option, Option) { + let min_amount_out = intent.map(|intent| intent.min_amount_out); + let declared_quote = intent.and_then(SwapIntent::declared_quote); + let quote_timestamp = intent.and_then(|intent| intent.timestamp); + (min_amount_out, declared_quote, quote_timestamp) +} + /// Max concurrent trace requests per block. Bounds RPC load so a block /// with many solver trades still completes within the block time /// without tripping provider rate limits. @@ -145,15 +246,16 @@ impl Decoder

{ &self.registry } - /// Decode solver trades from a block. + /// Decode solver trades from a block — settled and reverted alike, as one list told apart by + /// `DecodedTrade::status`. /// - /// Fetches all receipts in one `eth_getBlockReceipts` call, then matches a - /// transaction two ways: its entry point (`tx.to`) is a known venue or - /// solver, or one of its logs was emitted by a known solver. The - /// second case catches filler-initiated intent fills (`UniswapX`, 1inch - /// limit orders) where `tx.to` is a rotating filler. Matched transactions are - /// traced concurrently; the trace recovers native ETH flows and attributes - /// the settling solver. + /// Fetches all receipts in one `eth_getBlockReceipts` call, then matches each transaction one + /// of three ways: a settled trade, matched by entry point or by a known solver's log (the log + /// path catches filler-initiated intent fills, `UniswapX`, 1inch limit orders, where `tx.to` + /// is a rotating filler); a reverted candidate, matched by entry point alone (a revert emits + /// no logs — see `matching::select`); or neither, and the transaction is dropped before it + /// costs a trace. Both matched shapes join one bounded trace wave; the trace recovers native + /// ETH flows and attributes the settling (or attempted) solver either way. pub(crate) async fn decode_block( &mut self, block_number: u64, @@ -202,20 +304,17 @@ impl Decoder

{ .collect::>() .await; - let mut trades = Vec::with_capacity(matched.len()); + let mut trades = Vec::new(); for ((index, matched), trace) in matched.into_iter().zip(traces) { let tx_index = matched .receipt .transaction_index .unwrap_or(index as u64); + let tx_hash = matched.receipt.transaction_hash; let root = match trace { Ok(root) => root, Err(e) => { - warn!( - block = block_number, - tx = %matched.receipt.transaction_hash, - "skipping untraceable transaction: {e}" - ); + warn!(block = block_number, tx = %tx_hash, "skipping untraceable transaction: {e}"); crate::telemetry::record_untraced_transaction(); continue; } @@ -224,25 +323,99 @@ impl Decoder

{ .decode_transaction(matched, &root, block_number, tx_index) .await { - let evidence = sandwich::detect(&receipts, index, &trade, &self.registry); - trade.sandwich = evidence; + // Sandwich detection only makes sense around a trade that actually moved the + // pools; a reverted trade has nothing to be sandwiched around. + if trade.status == TradeStatus::Settled { + trade.sandwich = sandwich::detect(&receipts, index, &trade, &self.registry); + } trades.push(trade); } } Ok(trades) } - /// Decode one matched transaction from its trace: build the transfer ledger, run the decoders - /// for its entity, veto non-trades, attribute the solver, and account gas and quote. + /// Decode one matched transaction: settled trades run the full `TraderFlow` decoder chain + /// (`decode_settled`); reverted candidates skip it — there is no netted flow to decode, only + /// the settling solver frame's own calldata to read (`decode_reverted`). async fn decode_transaction( &mut self, matched: MatchedSolverTrade<'_>, root: &CallFrame, block_number: u64, tx_index: u64, + ) -> Option { + if matched.reverted { + return Some(self.decode_reverted(matched, root, block_number, tx_index)); + } + self.decode_settled(matched, root, block_number, tx_index) + .await + } + + /// Decode a reverted candidate from its trace: attribute the solver the same way a settled + /// trade would be (the strict-then-tolerant `find_solver_frame` walk falls back to the frame + /// that tried, since nothing settled), recover its swap terms when that solver's calldata + /// supports it, and classify why the transaction failed. The terms are `None` — not an + /// error — when no solver frame was found, its solver has no `swap_intent` support, or the + /// calldata did not parse; the trade is still recorded so parser coverage is measurable + /// against every reverted candidate. Always produces a trade: unlike a settled decode, there + /// is no veto or missing-flow case to decline on. + fn decode_reverted( + &self, + matched: MatchedSolverTrade<'_>, + root: &CallFrame, + block_number: u64, + tx_index: u64, + ) -> DecodedTrade { + let registry = &self.registry; + let MatchedSolverTrade { receipt, entry_point, .. } = matched; + let sender = receipt.from; + let venue = registry.label(entry_point); + let attribution = + solvers::attribution::attribute(None, root, entry_point, sender, registry); + // A reverted trade has no netted flow to draw an input-amount hint from — only the + // ABI/offset-based extractors (Fly, KyberSwap) can recover an intent here. + let intent = trace::find_solver_frame(root, registry) + .and_then(|frame| solvers::swap_intent(&attribution.solver, &frame.input, None)); + let (min_amount_out, declared_quote, quote_timestamp) = intent_fields(intent.as_ref()); + let (token_in, token_out, amount_in) = match &intent { + Some(intent) => (Some(intent.token_in), Some(intent.token_out), Some(intent.amount_in)), + None => (None, None, None), + }; + DecodedTrade { + tx_hash: receipt.transaction_hash, + block_number, + tx_index, + status: TradeStatus::Reverted { cause: trace::classify_revert_cause(root) }, + venue, + solver: attribution.solver, + solver_source: attribution.source, + decoder: "reverted", + sender, + token_in, + token_out, + amount_in, + amount_out: None, + venue_fee_in: None, + venue_fee_out: None, + settled_gas: None, + min_amount_out, + declared_quote, + quote_timestamp, + sandwich: None, + } + } + + /// Decode one settled transaction from its trace: build the transfer ledger, run the + /// decoders for its entity, veto non-trades, attribute the solver, and account gas and quote. + async fn decode_settled( + &mut self, + matched: MatchedSolverTrade<'_>, + root: &CallFrame, + block_number: u64, + tx_index: u64, ) -> Option { let Self { provider, registry, code_cache } = self; - let MatchedSolverTrade { receipt, entry_point } = matched; + let MatchedSolverTrade { receipt, entry_point, .. } = matched; let logs = receipt.logs(); let sender = receipt.from; @@ -258,6 +431,7 @@ impl Decoder

{ entry_point, transfer_ledger: &transfer_ledger, input: &root.input, + root, venue: None, }; let Some((decoder, mut flow)) = recover(&mut ctx).await else { @@ -311,29 +485,49 @@ impl Decoder

{ } .map(|units| units * U256::from(receipt.effective_gas_price)); - // The solver's off-chain quote, when its calldata declares one. Dispatched on the - // attributed solver so a lookalike blob from another router cannot masquerade as a - // quote, and unit-checked against the settled amount (quotes are self-reported). - let quote = solvers::embedded_quote(&attribution.solver, &root.input, flow.swap.amount_in) - .filter(|quote| solvers::plausible_quote(quote, flow.swap.amount_out)); + // The trader's swap terms, when the settling solver frame's own calldata declares them. + // Dispatched with the solver frame's input, not the root transaction's — a packed + // calldata layout (Fly) uses offsets valid only in its own frame — and with the decoded + // flow's input amount as a hint for scan-based extractors (ParaSwap). Only the netting + // amounts above stay authoritative for what actually settled; this is informational. A + // declared quote that fails the unit-plausibility check against the settled amount is + // dropped (quotes are self-reported); the ABI-decoded terms stay either way. + let intent = trace::find_solver_frame(root, registry) + .and_then(|frame| { + solvers::swap_intent(&attribution.solver, &frame.input, Some(flow.swap.amount_in)) + }) + .map(|mut intent| { + if let Some(quoted) = intent.declared_quote() { + if !solvers::plausible_quote(quoted, flow.swap.amount_out) { + intent.clear_quote(); + } + } + intent + }); + + warn_on_intent_disagreement(decoder, receipt.transaction_hash, intent.as_ref(), &flow); + let (min_amount_out, declared_quote, quote_timestamp) = intent_fields(intent.as_ref()); Some(DecodedTrade { tx_hash: receipt.transaction_hash, block_number, tx_index, + status: TradeStatus::Settled, venue, solver: attribution.solver, solver_source: attribution.source, decoder, sender: flow.tracked, - token_in: flow.swap.token_in, - token_out: flow.swap.token_out, - amount_in: flow.swap.amount_in, - amount_out: flow.swap.amount_out, + token_in: Some(flow.swap.token_in), + token_out: Some(flow.swap.token_out), + amount_in: Some(flow.swap.amount_in), + amount_out: Some(flow.swap.amount_out), venue_fee_in: flow.venue_fee_in, venue_fee_out: flow.venue_fee_out, settled_gas, - quote, + min_amount_out, + declared_quote, + quote_timestamp, // The full receipts slice isn't available here (this fn only sees the one matched // transaction); the caller (`decode_block`) fills this in once decoding succeeds. sandwich: None, @@ -392,4 +586,47 @@ mod tests { assert_eq!(trades.len(), 1); assert_eq!(trades[0].tx_hash, tx_hash(2)); } + + /// A Relay transaction that reverted before settling, decoded end to end through + /// `decode_block`: matched by entry point alone, attributed via the trace's tolerant + /// fallback, and recorded with no swap terms (an unregistered "solver" frame, so its calldata + /// carries no `swap_intent`). + #[tokio::test] + async fn test_reverted_relay_transaction_decodes_with_a_reverted_status() { + let registry = Registry::ethereum(); + let relay = *registry + .venue("relay") + .unwrap() + .entry_points + .iter() + .next() + .unwrap(); + let sender = addr(1); + + let asserter = Asserter::new(); + asserter.push_success(&vec![crate::decoder::test_utils::reverted_receipt( + tx_hash(1), + sender, + Some(relay), + )]); + let mut root = frame("CALL", sender, relay, 0); + root.error = Some("execution reverted".to_string()); + + asserter.push_success(&root); + + let mut decoder = + Decoder::new(ProviderBuilder::default().connect_mocked_client(asserter), registry); + let trades = decoder + .decode_block(21_000_000) + .await + .expect("decode_block should succeed"); + + assert_eq!(trades.len(), 1); + let trade = &trades[0]; + assert!(matches!(trade.status, TradeStatus::Reverted { .. })); + assert_eq!(trade.venue, "relay"); + assert!(trade.token_in.is_none()); + assert!(trade.amount_out.is_none()); + assert!(trade.sandwich.is_none()); + } } diff --git a/tools/hindsight/src/decoder/registry/ethereum.toml b/tools/hindsight/src/decoder/registry/ethereum.toml index 85e3372c..555f53ac 100644 --- a/tools/hindsight/src/decoder/registry/ethereum.toml +++ b/tools/hindsight/src/decoder/registry/ethereum.toml @@ -57,6 +57,8 @@ batch_settlers = ["0x9008d19f58aabd9ed0d60971565aa8510560ab41"] "0x1f8db310f32d48b6180ff902ec60c586128cef47" = "tycho" "0xfd0b31d2e955fa55e3fa641fe90e08b677188d35" = "tycho" "0xda892c989d07a18b5dd3f392d949f00df15c5736" = "tycho" +# Fly (formerly Magpie) DexAggregator — same address on every chain (docs.fly.trade/developers/deployments) +"0x20f6ee51340adeed01a59b0e65cb3703f3dc860c" = "fly" # Venues — platforms that initiate a trade and route it through a solver found in the # trace. Each section name must have a decode strategy in code (venues::Venue); diff --git a/tools/hindsight/src/decoder/sandwich.rs b/tools/hindsight/src/decoder/sandwich.rs index 58702f6a..0411a580 100644 --- a/tools/hindsight/src/decoder/sandwich.rs +++ b/tools/hindsight/src/decoder/sandwich.rs @@ -63,7 +63,10 @@ pub(crate) fn detect( if victim_pools.is_empty() { return None; } - let token = direction_token(victim.token_out, registry); + // Sandwich detection only makes sense for a trade that actually moved the pools; a reverted + // trade (or one whose calldata did not parse) has no settled output token to key the + // direction on. + let token = direction_token(victim.token_out?, registry); let front_start = victim_index.saturating_sub(WINDOW); let back_end = (victim_index + WINDOW).min(receipts.len().saturating_sub(1)); @@ -272,19 +275,22 @@ mod tests { tx_hash: TxHash::default(), block_number: 1, tx_index: 0, + status: crate::decoder::TradeStatus::Settled, venue: "relay".into(), solver: "1inch".into(), solver_source: AttributionSource::TraceMatch, decoder: "sender-netting", sender, - token_in: addr(59), - token_out, - amount_in: U256::from(1_000u64), - amount_out: U256::from(2_000u64), + token_in: Some(addr(59)), + token_out: Some(token_out), + amount_in: Some(U256::from(1_000u64)), + amount_out: Some(U256::from(2_000u64)), venue_fee_in: None, venue_fee_out: None, settled_gas: None, - quote: None, + min_amount_out: None, + declared_quote: None, + quote_timestamp: None, sandwich: None, } } diff --git a/tools/hindsight/src/decoder/solvers/fixtures/fly_input.txt b/tools/hindsight/src/decoder/solvers/fixtures/fly_input.txt new file mode 100644 index 00000000..2563db59 --- /dev/null +++ b/tools/hindsight/src/decoder/solvers/fixtures/fly_input.txt @@ -0,0 +1 @@ +0x46ec278a000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000002f8029c0060b92fe925dc43a0ecde6c8b1a2709c170ec4fff4ffde4c96c8593536e31f229ea8f37b2ada2699bb2000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000012c8433e00114c80119c80121d4bab7c80cd411884b18508d554ca0309e595c30c0646af115b15994b8358ca6f00129004a3906e5558ccdfa982936002b29324330801e01bd02442a78c80e2b7007faff3ad0ed3c180893cf7f26b451bf555a62e1ff80c422cacb99709d3727fb4a05cb1b00e06a69e7f3c8244d1fd7c87dc5c824aafe8c631505f02710fde4c96c8593536e31f229ea8f37b2ada2699bb231c2f6fcff4f8759b3bd5bf0e1084a055615c768e0012c843302012b02013f0e0153015387517c45d9c500dff816a1da21a48a732d3498bf09dc9aeb01016302012b0201670e0153015303011324856bc3f840f880f801001000000000000000000000000000000000000000000000000000000000000000f820f00360f80300060c0f0000000000000000000000000000000000000000000000000000000000f860f00200f00260f00180833589fcd6edb6e08f4c7c32d4f71b54bda02913f80060a0ffb9c1ce1fe56963b0321b32e7a0302114058bf805e8020000f0014001018c0301900301920301940301960301940301b70301b90301900301920301bc0301be0301bc0301df0301e10301e40301e70301b70201ea02012b0301fe0302000302150302170301fe0e015301530301fe03021b0301fe03019002012b0e015301530301900201ea0301fe8569213d10e502b0573f9cd6d33be324144b894f0201ea02028b0500003df021240102a80301940301fe0500000301fe42000000000000000000000000000000000000060202bb0202bb05002002007002007007050040010000015801630000000000017b018c013f000000021e028b0167060020025402570000010000029f02a8000000000002ac02bb028b06002002cf02d2000005000002d202d8000006002002d802db000003000002db02e200000000000000000000 diff --git a/tools/hindsight/src/decoder/solvers/fixtures/zeroex_reverted_input.txt b/tools/hindsight/src/decoder/solvers/fixtures/zeroex_reverted_input.txt new file mode 100644 index 00000000..2565e99e --- /dev/null +++ b/tools/hindsight/src/decoder/solvers/fixtures/zeroex_reverted_input.txt @@ -0,0 +1 @@ +0x2213bc0b0000000000000000000000007747f8d2a76bd6345cc29622a946a929647f235900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001c01d57175ff16a10000000000000000000000007747f8d2a76bd6345cc29622a946a929647f235900000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000006641fff991f000000000000000000000000b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda0291300000000000000000000000000000000000000000000000000000000d92d88c000000000000000000000000000000000000000000000000000000000000000a0138b51c04603f9ab1fe5513d0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000026000000000000000000000000000000000000000000000000000000000000003a000000000000000000000000000000000000000000000000000000000000004e00000000000000000000000000000000000000000000000000000000000000044bd01c226000000000000000000000000000000000000000000000000000000006a6e693a0000000000000000000000000000000000000000000000001c01d57175ff16a100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010438c9c147000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee00000000000000000000000000000000000000000000000000000000000027100000000000000000000000004200000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000024d0e30db000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010438c9c147000000000000000000000000420000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000027100000000000000000000000004200000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000242e1a7d4d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000104d47868c9000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee00000000000000000000000000000000000000000000000000000000000027100000000000000000000000006a6951db3bb99f76a8ccb9f8a8399b613666298b00000000000000000000000000000000000000000000000000005af3107a400000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000dd2ab92600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008434ee90ca000000000000000000000000f5c4f3dc02c3fb9279495a8fef7b0741da956157000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda0291300000000000000000000000000000000000000000000000000000000dd9c309500000000000000000000000000000000000000000000000000000000000027100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 diff --git a/tools/hindsight/src/decoder/solvers/fixtures/zeroex_settled_input.txt b/tools/hindsight/src/decoder/solvers/fixtures/zeroex_settled_input.txt new file mode 100644 index 00000000..304b16c5 --- /dev/null +++ b/tools/hindsight/src/decoder/solvers/fixtures/zeroex_settled_input.txt @@ -0,0 +1 @@ +0x2213bc0b0000000000000000000000007747f8d2a76bd6345cc29622a946a929647f2359000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002fad28d3d05f6350000000000000000000000007747f8d2a76bd6345cc29622a946a929647f235900000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000006641fff991f000000000000000000000000b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda0291300000000000000000000000000000000000000000000000000000000172260e800000000000000000000000000000000000000000000000000000000000000a02295bdf4b4858633de1db1260000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000024000000000000000000000000000000000000000000000000000000000000004e00000000000000000000000000000000000000000000000000000000000000044bd01c226000000000000000000000000000000000000000000000000000000006a6e89f900000000000000000000000000000000000000000000000002fad28d3d05f63500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010438c9c147000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee00000000000000000000000000000000000000000000000000000000000027100000000000000000000000004200000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000024d0e30db000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000026438c9c14700000000000000000000000042000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000002710000000000000000000000000292deca668291262341f92b840830f78db35b6e500000000000000000000000000000000000000000000000000000000000000a400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000184865f50930000000000000000000000000000000000000000000000000000000000000020000000000000000000000000501568a7c6d9257ded9506008de044565c68fd600000000000000000000000004200000000000000000000000000000000000006000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda029130000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000153f08360000000000000000000000007747f8d2a76bd6345cc29622a946a929647f2359000000000000000000000000000000000000000000000000000000006a6e89f90000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008434ee90ca000000000000000000000000f5c4f3dc02c3fb9279495a8fef7b0741da956157000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda0291300000000000000000000000000000000000000000000000000000000179b5f0300000000000000000000000000000000000000000000000000000000000027100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 diff --git a/tools/hindsight/src/decoder/solvers/fly.rs b/tools/hindsight/src/decoder/solvers/fly.rs new file mode 100644 index 00000000..da9f4cca --- /dev/null +++ b/tools/hindsight/src/decoder/solvers/fly.rs @@ -0,0 +1,225 @@ +//! Fly (formerly Magpie) `DexAggregator` calldata extraction. +//! +//! Fly's entry functions all take one `bytes` argument that is not standard ABI-encoded but a +//! packed blob `LibRouter.getData()` reads via hardcoded absolute byte offsets (verified against +//! Fly's Sourcify-published source, chain 8453). Addresses and the input amount sit at fixed +//! offsets; `amountOutMin` and `expectedAmountOut` sit behind a 3-byte packed header (a right-shift +//! amount, then a 2-byte big-endian pointer to the 32-byte word holding the value) — Magpie's own +//! calldata-packing scheme, not a proxy indirection. `amountOutMin` is the floor the trade reverts +//! below (`InsufficientAmountOut()`, selector `0xe52970aa`); `expectedAmountOut` is Magpie's +//! off-chain quote, usable as this solver's declared quote. + +use alloy::primitives::{Address, U256}; + +use crate::decoder::solvers::{SolverKnowledge, SwapIntent}; + +/// Selectors sharing `LibRouter`'s packed layout (`swapWithBackendSignature`, +/// `swapWithMagpieSignature`, `swapWithUserSignature`, `swapWithoutSignature`, `swap`). +const SELECTORS: [[u8; 4]; 5] = [ + [0x46, 0xec, 0x27, 0x8a], + [0x73, 0xfc, 0x44, 0x57], + [0x25, 0xe6, 0x51, 0xed], + [0x15, 0x8f, 0x68, 0x94], + [0x62, 0x7d, 0xd5, 0x6a], +]; + +/// Fly's `InsufficientAmountOut()` custom error selector (`DexAggregator.sol`) — the marker for a +/// slippage-floor revert. +const INSUFFICIENT_AMOUNT_OUT: [u8; 4] = [0xe5, 0x29, 0x70, 0xaa]; + +const TO_ADDRESS_OFFSET: usize = 72; +const FROM_ASSET_OFFSET: usize = 92; +const TO_ASSET_OFFSET: usize = 112; +const AMOUNT_IN_OFFSET: usize = 132; +const AMOUNT_OUT_MIN_HEADER: usize = 167; +const EXPECTED_AMOUNT_OUT_HEADER: usize = 170; +const ADDRESS_LEN: usize = 20; +const WORD_LEN: usize = 32; + +/// The fields of `LibRouter.SwapData` this decoder needs, read from one frame's packed calldata. +struct SwapData { + from_asset: Address, + to_asset: Address, + amount_in: U256, + amount_out_min: U256, + /// Magpie's off-chain quote. Can legitimately be absent (zero) in some calldata variants. + expected_amount_out: U256, +} + +/// Read a 3-byte packed header at `header_offset`: a right-shift byte, then a 2-byte big-endian +/// pointer to the 32-byte word holding the value. `None` on any out-of-bounds read — a +/// malformed or truncated blob, not a real Fly call. +fn read_packed(input: &[u8], header_offset: usize) -> Option { + let shift = *input.get(header_offset)?; + let ptr_bytes = input.get(header_offset + 1..header_offset + 3)?; + let ptr = usize::from(u16::from_be_bytes([ptr_bytes[0], ptr_bytes[1]])); + let word = input.get(ptr..ptr + WORD_LEN)?; + Some(U256::from_be_slice(word) >> usize::from(shift)) +} + +/// Whether `input` opens with one of Fly's packed-layout selectors. +fn has_fly_selector(input: &[u8]) -> Option<()> { + let selector: [u8; 4] = input.get(0..4)?.try_into().ok()?; + SELECTORS + .contains(&selector) + .then_some(()) +} + +/// Parse a Fly/Magpie frame's packed calldata. `None` when the selector does not match, or any +/// field's bytes fall outside the input — bounds are checked, never assumed. +fn parse(input: &[u8]) -> Option { + has_fly_selector(input)?; + let from_asset = + Address::from_slice(input.get(FROM_ASSET_OFFSET..FROM_ASSET_OFFSET + ADDRESS_LEN)?); + let to_asset = Address::from_slice(input.get(TO_ASSET_OFFSET..TO_ASSET_OFFSET + ADDRESS_LEN)?); + let amount_in = U256::from_be_slice(input.get(AMOUNT_IN_OFFSET..AMOUNT_IN_OFFSET + WORD_LEN)?); + let amount_out_min = read_packed(input, AMOUNT_OUT_MIN_HEADER)?; + let expected_amount_out = read_packed(input, EXPECTED_AMOUNT_OUT_HEADER)?; + Some(SwapData { from_asset, to_asset, amount_in, amount_out_min, expected_amount_out }) +} + +/// The Fly (Magpie) `DexAggregator` solver. +pub(crate) struct Fly; + +impl SolverKnowledge for Fly { + /// The trader's enforced swap terms: `amountOutMin` is the on-chain floor + /// (`InsufficientAmountOut()` below it); `expectedAmountOut`, when present, is Magpie's + /// declared quote and must not be stricter than the floor it is quoted against. `input` must + /// carry Fly's packed layout (e.g. it is `None` when `input` is the outer Relay wrapper, not + /// Fly's own frame); the hint is unused — Fly's fields sit at fixed offsets, not located by + /// value. + fn swap_intent(&self, input: &[u8], _amount_in_hint: Option) -> Option { + let data = parse(input)?; + if data.amount_in.is_zero() || data.amount_out_min.is_zero() { + return None; + } + if !data.expected_amount_out.is_zero() && data.amount_out_min > data.expected_amount_out { + return None; + } + let intent = + SwapIntent::new(data.from_asset, data.to_asset, data.amount_in, data.amount_out_min); + Some(if data.expected_amount_out.is_zero() { + intent + } else { + intent.with_quote(data.expected_amount_out, None) + }) + } + + /// The packed blob's `toAddress` field. In practice this is Relay's own router, not the + /// trader — Relay receives the output and forwards it — so callers must read the settled + /// output as what this address *received*, not treat it as the trader. + fn output_recipient(&self, input: &[u8]) -> Option

{ + has_fly_selector(input)?; + Some(Address::from_slice(input.get(TO_ADDRESS_OFFSET..TO_ADDRESS_OFFSET + ADDRESS_LEN)?)) + } + + /// `InsufficientAmountOut()`'s selector as the frame's revert output. + fn is_slippage_floor(&self, output: Option<&[u8]>, _revert_reason: Option<&str>) -> bool { + output.is_some_and(|output| output.starts_with(&INSUFFICIENT_AMOUNT_OUT)) + } +} + +#[cfg(test)] +mod tests { + use alloy::primitives::address; + + use super::*; + + /// Live-validated Fly calldata (tx `0x1ae5b3e2…`, Base): `fromAsset` USDT, `toAsset` native, + /// `amountIn` matching the transfer log exactly, `amountOutMin` 55bps below the settled + /// amount, `expectedAmountOut` 45bps above it. + fn real_input() -> Vec { + let text = include_str!("fixtures/fly_input.txt").trim(); + alloy::hex::decode(text.strip_prefix("0x").unwrap_or(text)).unwrap() + } + + #[test] + fn test_real_fixture_swap_intent() { + let intent = Fly + .swap_intent(&real_input(), None) + .unwrap(); + assert_eq!(intent.token_in, address!("0xfde4c96c8593536e31f229ea8f37b2ada2699bb2")); + assert_eq!(intent.token_out, Address::ZERO); + assert_eq!(intent.amount_in, U256::from(19_694_643u64)); + assert_eq!(intent.min_amount_out, U256::from(10_217_898_321_149_381u64)); + assert_eq!(intent.declared_quote(), Some(U256::from(10_321_109_415_302_405u64))); + } + + #[test] + fn test_real_fixture_output_recipient() { + // Relay's own router — the delivery address, not the trader (see the method's doc). + let recipient = Fly + .output_recipient(&real_input()) + .unwrap(); + assert_eq!(recipient, address!("0xb92fe925dc43a0ecde6c8b1a2709c170ec4fff4f")); + } + + #[test] + fn test_output_recipient_wrong_selector() { + let mut input = real_input(); + input[0] = 0xff; + assert!(Fly.output_recipient(&input).is_none()); + } + + #[test] + fn test_output_recipient_truncated_input() { + let full = real_input(); + assert!(Fly + .output_recipient(&full[..80]) + .is_none()); + } + + #[test] + fn test_wrong_selector() { + let mut input = real_input(); + input[0] = 0xff; + assert!(Fly.swap_intent(&input, None).is_none()); + } + + #[test] + fn test_truncated_input() { + let full = real_input(); + // Cut before the fixed-offset fields are readable at all. + assert!(Fly + .swap_intent(&full[..100], None) + .is_none()); + // Cut inside the packed-header pointer's target word. + assert!(Fly + .swap_intent(&full[..300], None) + .is_none()); + } + + #[test] + fn test_empty_input() { + assert!(Fly.swap_intent(&[], None).is_none()); + } + + #[test] + fn test_zero_amount_out_min_rejected() { + let mut input = real_input(); + // Zero out the word the amountOutMin pointer resolves to (ptr 281 in this fixture). + input[281..313].fill(0); + assert!(Fly.swap_intent(&input, None).is_none()); + } + + #[test] + fn test_is_slippage_floor_matches_the_selector() { + assert!(Fly.is_slippage_floor(Some(&INSUFFICIENT_AMOUNT_OUT), None)); + assert!(Fly.is_slippage_floor(Some(&[0xe5, 0x29, 0x70, 0xaa, 0x00, 0x01]), None)); + assert!(!Fly.is_slippage_floor(Some(&[0xde, 0xad, 0xbe, 0xef]), None)); + assert!(!Fly.is_slippage_floor(None, None)); + } + + #[test] + fn test_amount_out_min_above_expected_rejected() { + let mut input = real_input(); + // Zero the shift in amountOutMin's packed header (offset 167): decoding then reads the + // target word unshifted, a value far above expectedAmountOut — an inconsistent blob (the + // floor cannot exceed the quote). Corrupting the shift rather than the word's bytes + // matters here: amountOutMin's and expectedAmountOut's target words overlap in this + // fixture (ptrs 281 and 289), so filling the word instead would corrupt both readings + // identically and leave them equal, not violate the check. + input[AMOUNT_OUT_MIN_HEADER] = 0; + assert!(Fly.swap_intent(&input, None).is_none()); + } +} diff --git a/tools/hindsight/src/decoder/solvers/kyberswap.rs b/tools/hindsight/src/decoder/solvers/kyberswap.rs index b21a70d8..e6c0f6d6 100644 --- a/tools/hindsight/src/decoder/solvers/kyberswap.rs +++ b/tools/hindsight/src/decoder/solvers/kyberswap.rs @@ -6,43 +6,129 @@ //! amount tells us what the user got; the quote tells us what the solver promised at decision //! time, which is the number a venue like Relay actually compared against ours. -use alloy::primitives::U256; +use alloy::{ + primitives::{Address, U256}, + sol, + sol_types::SolCall, +}; -use crate::decoder::solvers::{SolverKnowledge, SolverQuote}; +use crate::decoder::solvers::{SolverKnowledge, SwapIntent}; + +/// `KyberSwap` represents native ETH with this sentinel address rather than the zero address — +/// hindsight's convention — so it is normalized on the way out. +const KYBERSWAP_NATIVE: Address = + alloy::primitives::address!("0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"); + +/// `KyberSwap`'s slippage-floor revert reason. +const INSUFFICIENT_RETURN: &str = "Return amount is not enough"; + +sol! { + /// `MetaAggregationRouterV2.swap`'s parameter shape, verified against a live reverted trade + /// (tx 0xd3b7ffae…, Base): decoding recovered `srcToken`/`dstToken`/`amount`/`minReturnAmount` + /// matching the on-chain values exactly. Only the fields this decoder reads are commented; + /// every field must still be declared for the tuple to decode at the right offsets. + struct SwapDescriptionV2 { + address srcToken; + address dstToken; + address[] srcReceivers; + uint256[] srcAmounts; + address[] feeReceivers; + uint256[] feeAmounts; + address dstReceiver; + uint256 amount; + uint256 minReturnAmount; + uint256 flags; + bytes permit; + } + + struct SwapExecutionParams { + address callTarget; + address approveTarget; + bytes targetData; + SwapDescriptionV2 desc; + bytes clientData; + } + + function swap(SwapExecutionParams execution) external payable returns (uint256, uint256); +} + +/// Native ETH is `Address::ZERO` in hindsight's convention. +fn normalize_native(token: Address) -> Address { + if token == KYBERSWAP_NATIVE { + Address::ZERO + } else { + token + } +} + +/// `KyberSwap`'s declared quote, scanned out of a swap frame's calldata: `AmountOut` and, when +/// present, `Timestamp`. `Source` is not read — nothing downstream consumes it. +/// +/// The blob is plain ASCII JSON inside ABI-encoded bytes, so it is located by its `{"Source"` +/// marker rather than by decoding the router call — which also finds it when Kyber's calldata is +/// nested inside a wrapper's (Relay, `MetaMask`). The JSON is flat, so the object ends at the +/// first closing brace. Anything malformed or missing returns `None` — a quote is decoration, not +/// a hard fact, so its absence must never fail the swap intent it decorates. +fn declared_quote(input: &[u8]) -> Option<(U256, Option)> { + const MARKER: &[u8] = b"{\"Source\""; + let start = input + .windows(MARKER.len()) + .position(|window| window == MARKER)?; + let rest = &input[start..]; + let end = rest + .iter() + .position(|&byte| byte == b'}')?; + let json: serde_json::Value = serde_json::from_slice(&rest[..=end]).ok()?; + let amount_out = json + .get("AmountOut")? + .as_str()? + .parse::() + .ok()?; + let timestamp = json + .get("Timestamp") + .and_then(serde_json::Value::as_u64); + Some((amount_out, timestamp)) +} /// The `KyberSwap` solver. pub(crate) struct Kyberswap; impl SolverKnowledge for Kyberswap { - /// Extract `KyberSwap`'s `clientData` quote from transaction calldata. - /// - /// The blob is plain ASCII JSON inside ABI-encoded bytes, so it is located by its `{"Source"` - /// marker rather than by decoding the router call — which also finds it when Kyber's calldata - /// is nested inside a wrapper's (Relay, `MetaMask`). The JSON is flat, so the object ends at - /// the first closing brace. Anything malformed or missing returns `None`. - fn embedded_quote(&self, input: &[u8], _amount_in: U256) -> Option { - const MARKER: &[u8] = b"{\"Source\""; - let start = input - .windows(MARKER.len()) - .position(|window| window == MARKER)?; - let rest = &input[start..]; - let end = rest - .iter() - .position(|&byte| byte == b'}')?; - let json: serde_json::Value = serde_json::from_slice(&rest[..=end]).ok()?; - let amount_out = json - .get("AmountOut")? - .as_str()? - .parse::() - .ok()?; - let source = json - .get("Source")? - .as_str()? - .to_string(); - let timestamp = json - .get("Timestamp") - .and_then(serde_json::Value::as_u64); - Some(SolverQuote { amount_out, source: Some(source), timestamp }) + /// Extract the trader's swap terms from a `swap` call's `SwapDescriptionV2`: + /// `srcToken`/`dstToken` (native ETH normalized to `Address::ZERO`), `amount`, and the + /// enforced floor `minReturnAmount` (the revert reads "Return amount is not enough" below + /// it) — a proper ABI decode, not a byte scan, since the shape is a nested tuple, not + /// word-aligned data. The hint is unused: `KyberSwap`'s fields are decoded by ABI position, not + /// located by value. When the calldata also carries a `clientData` quote, it is attached; a + /// missing or malformed one does not fail the intent. + fn swap_intent(&self, input: &[u8], _amount_in_hint: Option) -> Option { + let call = swapCall::abi_decode(input).ok()?; + let desc = call.execution.desc; + if desc.amount.is_zero() || desc.minReturnAmount.is_zero() { + return None; + } + let intent = SwapIntent::new( + normalize_native(desc.srcToken), + normalize_native(desc.dstToken), + desc.amount, + desc.minReturnAmount, + ); + Some(match declared_quote(input) { + Some((amount_out, timestamp)) => intent.with_quote(amount_out, timestamp), + None => intent, + }) + } + + /// `SwapDescriptionV2.dstReceiver` — `KyberSwap` passes this straight down to the inner pool, + /// which pays it directly; the router itself never touches the output. + fn output_recipient(&self, input: &[u8]) -> Option
{ + let call = swapCall::abi_decode(input).ok()?; + Some(call.execution.desc.dstReceiver) + } + + /// "Return amount is not enough" as the frame's revert reason. + fn is_slippage_floor(&self, _output: Option<&[u8]>, revert_reason: Option<&str>) -> bool { + revert_reason.is_some_and(|reason| reason.contains(INSUFFICIENT_RETURN)) } } @@ -65,35 +151,166 @@ mod tests { } #[test] - fn test_real_relay_blob() { - let quote = Kyberswap - .embedded_quote(&calldata_with(BLOB), U256::ZERO) + fn test_declared_quote_real_relay_blob() { + let (amount_out, timestamp) = declared_quote(&calldata_with(BLOB)).unwrap(); + assert_eq!(amount_out, U256::from(70_400_409_935u64)); + assert_eq!(timestamp, Some(1_783_421_726)); + } + + #[test] + fn test_declared_quote_without_blob() { + assert!(declared_quote(&calldata_with("")).is_none()); + assert!(declared_quote(&[]).is_none()); + } + + #[test] + fn test_declared_quote_truncated_or_fieldless_blob() { + // Truncated before the closing brace: no valid JSON object to parse. + let truncated = &BLOB[..BLOB.len() - 20]; + assert!(declared_quote(&calldata_with(truncated)).is_none()); + // Well-formed but missing AmountOut. + assert!(declared_quote(&calldata_with("{\"Source\":\"relay\"}")).is_none()); + } + + /// Build a `swap` call's calldata via the `sol!` types, mirroring how a real trade encodes. + /// `client_data` is embedded verbatim as raw bytes, mirroring `KyberSwap`'s plain-ASCII-JSON + /// blob inside the ABI-encoded field. + fn swap_calldata( + src: Address, + dst: Address, + amount: u64, + min_return: u64, + client_data: &str, + ) -> Vec { + let desc = SwapDescriptionV2 { + srcToken: src, + dstToken: dst, + srcReceivers: vec![], + srcAmounts: vec![], + feeReceivers: vec![], + feeAmounts: vec![], + dstReceiver: Address::repeat_byte(0x77), + amount: U256::from(amount), + minReturnAmount: U256::from(min_return), + flags: U256::ZERO, + permit: vec![].into(), + }; + let execution = SwapExecutionParams { + callTarget: Address::repeat_byte(0x88), + approveTarget: Address::ZERO, + targetData: vec![].into(), + desc, + clientData: client_data.as_bytes().to_vec().into(), + }; + swapCall { execution }.abi_encode() + } + + #[test] + fn test_swap_intent_round_trip() { + let src = Address::repeat_byte(0x11); + let dst = Address::repeat_byte(0x22); + let intent = Kyberswap + .swap_intent(&swap_calldata(src, dst, 1_000_000, 990_000, ""), None) .unwrap(); - assert_eq!(quote.amount_out, U256::from(70_400_409_935u64)); - assert_eq!(quote.source.as_deref(), Some("relay")); - assert_eq!(quote.timestamp, Some(1_783_421_726)); + assert_eq!(intent.token_in, src); + assert_eq!(intent.token_out, dst); + assert_eq!(intent.amount_in, U256::from(1_000_000u64)); + assert_eq!(intent.min_amount_out, U256::from(990_000u64)); + // No clientData quote declared. + assert_eq!(intent.declared_quote(), None); + assert_eq!(intent.timestamp, None); } #[test] - fn test_calldata_without_blob() { + fn test_output_recipient_round_trip() { + let src = Address::repeat_byte(0x11); + let dst = Address::repeat_byte(0x22); + let recipient = Kyberswap + .output_recipient(&swap_calldata(src, dst, 1_000_000, 990_000, "")) + .unwrap(); + assert_eq!(recipient, Address::repeat_byte(0x77)); + } + + #[test] + fn test_output_recipient_garbage_input() { + assert!(Kyberswap + .output_recipient(&[]) + .is_none()); + } + + #[test] + fn test_swap_intent_with_declared_quote() { + let src = Address::repeat_byte(0x11); + let dst = Address::repeat_byte(0x22); + let intent = Kyberswap + .swap_intent(&swap_calldata(src, dst, 1_000_000, 990_000, BLOB), None) + .unwrap(); + assert_eq!(intent.min_amount_out, U256::from(990_000u64)); + assert_eq!(intent.declared_quote(), Some(U256::from(70_400_409_935u64))); + assert_eq!(intent.timestamp, Some(1_783_421_726)); + } + + #[test] + fn test_swap_intent_malformed_quote_does_not_fail_the_intent() { + // clientData present but missing AmountOut: the ABI-decoded terms are still recovered, + // the quote is just absent. + let src = Address::repeat_byte(0x11); + let dst = Address::repeat_byte(0x22); + let intent = Kyberswap + .swap_intent( + &swap_calldata(src, dst, 1_000_000, 990_000, "{\"Source\":\"relay\"}"), + None, + ) + .unwrap(); + assert_eq!(intent.declared_quote(), None); + assert_eq!(intent.timestamp, None); + } + + #[test] + fn test_swap_intent_normalizes_native_eth() { + let intent = Kyberswap + .swap_intent( + &swap_calldata(KYBERSWAP_NATIVE, Address::repeat_byte(0x22), 1_000, 900, ""), + None, + ) + .unwrap(); + assert_eq!(intent.token_in, Address::ZERO); + assert_eq!(intent.token_out, Address::repeat_byte(0x22)); + } + + #[test] + fn test_swap_intent_zero_amounts_rejected() { + let a = Address::repeat_byte(0x11); + let b = Address::repeat_byte(0x22); assert!(Kyberswap - .embedded_quote(&calldata_with(""), U256::ZERO) + .swap_intent(&swap_calldata(a, b, 0, 900, ""), None) .is_none()); assert!(Kyberswap - .embedded_quote(&[], U256::ZERO) + .swap_intent(&swap_calldata(a, b, 1_000, 0, ""), None) .is_none()); } #[test] - fn test_truncated_or_fieldless_blob() { - // Truncated before the closing brace: no valid JSON object to parse. - let truncated = &BLOB[..BLOB.len() - 20]; + fn test_is_slippage_floor_matches_the_revert_reason() { + assert!(Kyberswap.is_slippage_floor(None, Some(INSUFFICIENT_RETURN))); assert!(Kyberswap - .embedded_quote(&calldata_with(truncated), U256::ZERO) + .is_slippage_floor(None, Some("execution reverted: Return amount is not enough"))); + assert!(!Kyberswap.is_slippage_floor(None, Some("execution reverted"))); + assert!(!Kyberswap.is_slippage_floor(None, None)); + } + + #[test] + fn test_swap_intent_garbage_input() { + assert!(Kyberswap + .swap_intent(&[], None) .is_none()); - // Well-formed but missing AmountOut. assert!(Kyberswap - .embedded_quote(&calldata_with("{\"Source\":\"relay\"}"), U256::ZERO) + .swap_intent(&[0xde, 0xad, 0xbe, 0xef], None) + .is_none()); + // A well-formed but unrelated call (KyberSwap's own clientData blob calldata) must not + // decode as a `swap` execution. + assert!(Kyberswap + .swap_intent(&calldata_with(BLOB), None) .is_none()); } } diff --git a/tools/hindsight/src/decoder/solvers/mod.rs b/tools/hindsight/src/decoder/solvers/mod.rs index 57f16c62..b8d760c3 100644 --- a/tools/hindsight/src/decoder/solvers/mod.rs +++ b/tools/hindsight/src/decoder/solvers/mod.rs @@ -3,10 +3,11 @@ //! Solver addresses live in the address book's `[solvers]` section, and for most solvers that //! line is all that is needed: matching, attribution, and gas isolation work from the address //! alone. A solver whose transactions carry more information than that gets a module here with a -//! `SolverKnowledge` impl registered in `IMPLEMENTATIONS`: an off-chain quote embedded in -//! calldata, or a matching veto for order shapes that are not same-chain swaps. +//! `SolverKnowledge` impl registered in `IMPLEMENTATIONS`: a swap intent recovered from calldata, +//! or a matching veto for order shapes that are not same-chain swaps. pub(crate) mod attribution; +pub(crate) mod fly; pub(crate) mod kyberswap; pub(crate) mod lifi; pub(crate) mod paraswap; @@ -19,31 +20,98 @@ use alloy::{ use crate::decoder::{registry::Registry, veto::Veto}; -/// A solver's own off-chain quote for the swap, recovered from calldata. +/// A trader's swap terms recovered from a solver frame's own calldata: what the trade moved, the +/// floor the trader would accept, and — when the calldata declares one — the solver's own +/// off-chain quote. /// -/// This is the number the venue compared against at decision time — what the solver's API -/// promised — as opposed to the settled amount, which is what execution delivered. The fields -/// carry no solver name; the record's `solver` column already says who. -#[derive(Debug, Clone, serde::Serialize)] -pub(crate) struct SolverQuote { - /// Quoted output in `token_out` native units. - pub amount_out: U256, - /// The integrator that requested the route (e.g. "relay", "metamask", "Instadapp") — the - /// true frontend, even when the transaction enters through a wrapper contract. Only some - /// solvers declare it. - pub source: Option, - /// Unix timestamp of the quote, when present. Joined against block time downstream to - /// separate stale-quote slippage from routing quality. +/// `token_in`/`token_out`/`amount_in`/`min_amount_out` are the on-chain enforced terms of the +/// swap itself, recovered so a reverted swap can still be judged against its floor, since a +/// revert emits no logs to net a settled amount from. The declared quote is different: it is the +/// number the venue compared against at decision time — what the solver's API promised — as +/// opposed to the settled amount, which is what execution delivered. It is self-reported and not +/// every solver declares one, so it is read through [`SwapIntent::declared_quote`], `None` when +/// the calldata carried no quote. +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)] +pub(crate) struct SwapIntent { + /// `Address::ZERO` for native ETH. + pub token_in: Address, + /// `Address::ZERO` for native ETH. + pub token_out: Address, + pub amount_in: U256, + /// The trader's on-chain enforced floor for `token_out` — the swap reverts below it. + pub min_amount_out: U256, + /// The solver's declared off-chain quote, when its calldata carries one. Private: the + /// ABI-decoded fields above are hard facts, this one is self-reported, so it is read only + /// through the accessors, never assumed present. + quoted_amount_out: Option, + /// Unix timestamp of the declared quote, when present. Only `KyberSwap`'s `clientData` + /// exposes one. pub timestamp: Option, } +impl SwapIntent { + /// A swap intent with just the ABI-enforced terms: token in/out, amount in, and the on-chain + /// floor. No declared quote or timestamp — attach one with [`SwapIntent::with_quote`]. + pub(crate) fn new( + token_in: Address, + token_out: Address, + amount_in: U256, + min_amount_out: U256, + ) -> Self { + Self { + token_in, + token_out, + amount_in, + min_amount_out, + quoted_amount_out: None, + timestamp: None, + } + } + + /// Attach the solver's declared off-chain quote and, when known, its timestamp. + pub(crate) fn with_quote(mut self, quoted_amount_out: U256, timestamp: Option) -> Self { + self.quoted_amount_out = Some(quoted_amount_out); + self.timestamp = timestamp; + self + } + + /// The solver's declared off-chain quote, `None` when the calldata carried none. + pub(crate) fn declared_quote(&self) -> Option { + self.quoted_amount_out + } + + /// Drop the declared quote, keeping the ABI-enforced terms. Used when the settled amount + /// shows the quote was self-reported garbage (see [`plausible_quote`]) — the ABI fields stay + /// trustworthy either way. + pub(crate) fn clear_quote(&mut self) { + self.quoted_amount_out = None; + } +} + /// Solver-specific knowledge beyond the address-book entry. /// /// Every method has a default meaning "this solver has nothing to add", so a solver only /// implements the capabilities it has; most solvers need no code at all. pub(crate) trait SolverKnowledge: Send + Sync { - /// The solver's off-chain quote declared in the transaction's calldata, when it embeds one. - fn embedded_quote(&self, _input: &[u8], _amount_in: U256) -> Option { + /// The swap terms encoded in the solver frame's own calldata, when this solver's calldata + /// carries them plainly enough to recover without netting a settled amount. Dispatched with + /// the solver frame's input (found via `trace::find_solver_frame`/the reverted-tolerant + /// variant), not the root transaction's — a packed calldata layout (Fly) uses offsets valid + /// only in its own frame. + /// + /// `amount_in_hint` is the decoded flow's input amount, when one is known — absent for a + /// reverted trade, which has no netted flow to draw it from. Some extractors (`ParaSwap`) need + /// it to locate fields by value rather than by ABI offset. + fn swap_intent(&self, _input: &[u8], _amount_in_hint: Option) -> Option { + None + } + + /// The address this solver's calldata declares as the output recipient, when it carries one + /// plainly enough to recover — how a calldata-primary decode learns whose receipt to read the + /// settled amount from, since calldata alone never carries a settled amount. Dispatched with + /// the same solver-frame input as `swap_intent`. `None` when the calldata carries no such + /// field (most solvers deliver to the caller implicitly) or it did not parse. + fn output_recipient(&self, _input: &[u8]) -> Option
{ None } @@ -61,15 +129,25 @@ pub(crate) trait SolverKnowledge: Send + Sync { fn integrator(&self, _logs: &[Log]) -> Option { None } + + /// Whether a reverted call frame's output or revert reason matches this solver's + /// slippage-floor marker — the avoidable class of revert a fresher quote could have cleared + /// (Fly's `InsufficientAmountOut()` selector, `KyberSwap`'s "Return amount is not enough" + /// revert reason). Checked against every frame in a reverted trace's subtree (see + /// `trace::classify_revert_cause`), so a solver need not be attributed yet to be recognized. + fn is_slippage_floor(&self, _output: Option<&[u8]>, _revert_reason: Option<&str>) -> bool { + false + } } /// The solvers with a `SolverKnowledge` implementation, by address-book name. A solver absent /// here needs none — its address-book entry alone is complete. const IMPLEMENTATIONS: &[(&str, &'static dyn SolverKnowledge)] = &[ - ("0x", &zeroex::ZeroEx), + ("fly", &fly::Fly), ("kyberswap", &kyberswap::Kyberswap), ("lifi", &lifi::Lifi), ("paraswap", ¶swap::Paraswap), + ("0x", &zeroex::ZeroEx), ]; /// The veto a solver places on a matched transaction that must be skipped instead of decoded, @@ -102,27 +180,47 @@ pub(crate) fn integrator(logs: &[Log]) -> Option { .find_map(|(_, knowledge)| knowledge.integrator(logs)) } -/// The solver's off-chain quote declared in the transaction's calldata, when the attributed -/// solver is known to embed one. -pub(crate) fn embedded_quote(solver: &str, input: &[u8], amount_in: U256) -> Option { +/// The swap terms encoded in the solver frame's own calldata, dispatched on the attributed +/// solver so a lookalike blob from another router cannot masquerade as an intent. +pub(crate) fn swap_intent( + solver: &str, + input: &[u8], + amount_in_hint: Option, +) -> Option { + let (_, knowledge) = IMPLEMENTATIONS + .iter() + .find(|(name, _)| *name == solver)?; + knowledge.swap_intent(input, amount_in_hint) +} + +/// The address the solver frame's own calldata declares as the output recipient, dispatched on +/// the attributed solver so a lookalike blob from another router cannot masquerade as one. +pub(crate) fn output_recipient(solver: &str, input: &[u8]) -> Option
{ let (_, knowledge) = IMPLEMENTATIONS .iter() .find(|(name, _)| *name == solver)?; - knowledge.embedded_quote(input, amount_in) + knowledge.output_recipient(input) } -/// Whether a quoted output is in the same units as the settled one. +/// Whether a reverted call frame's output or revert reason matches any registered solver's +/// slippage-floor marker. Unscoped by attribution — the marker is a hard fact about the frame's +/// own bytes, and only one solver's check can ever match a given frame — so every implementation +/// is tried. +pub(crate) fn is_slippage_floor(output: Option<&[u8]>, revert_reason: Option<&str>) -> bool { + IMPLEMENTATIONS + .iter() + .any(|(_, knowledge)| knowledge.is_slippage_floor(output, revert_reason)) +} + +/// Whether a declared quote is in the same units as the settled output. /// /// Quotes are self-reported calldata: integrators sometimes fill them in a different token or /// decimal basis (seen live: quoted 1.2e23 vs settled 1.2e11), which would fabricate a -100% /// slippage. A real quote and its settlement differ by slippage, never by orders of magnitude, /// so anything outside a 2x band is dropped rather than recorded. -pub(crate) fn plausible_quote(quote: &SolverQuote, settled_amount_out: U256) -> bool { - quote.amount_out <= settled_amount_out.saturating_mul(U256::from(2)) && - settled_amount_out <= - quote - .amount_out - .saturating_mul(U256::from(2)) +pub(crate) fn plausible_quote(quoted_amount_out: U256, settled_amount_out: U256) -> bool { + quoted_amount_out <= settled_amount_out.saturating_mul(U256::from(2)) && + settled_amount_out <= quoted_amount_out.saturating_mul(U256::from(2)) } #[cfg(test)] @@ -185,32 +283,33 @@ mod tests { #[test] fn test_plausible_quote_slippage_and_unit_mismatch() { - let quote = |amount: u128| SolverQuote { - amount_out: U256::from(amount), - source: None, - timestamp: None, - }; // The audited Relay+KyberSwap trade: quoted 70,400.41, settled 69,996.28 — 57bps of // slippage, kept. - assert!(plausible_quote("e(70_400_409_935), U256::from(69_996_280_564u64))); + assert!(plausible_quote(U256::from(70_400_409_935u64), U256::from(69_996_280_564u64))); // Seen live via Instadapp: quoted in 18-decimal units, settled in 6 — dropped. assert!(!plausible_quote( - "e(120_001_117_253_254_637_416_284), + U256::from(120_001_117_253_254_637_416_284u128), U256::from(120_000_000_000u64) )); } #[test] - fn test_embedded_quote_dispatch() { - // A ParaSwap-shaped word triple only parses when the attributed solver is paraswap; - // an unlisted solver never yields a quote from the same bytes. + fn test_swap_intent_dispatch_scoped_to_the_attributed_solver() { + // A ParaSwap-shaped calldata (token pair, then the fromAmount/toAmount/quotedAmount + // triple) only parses into an intent when the attributed solver is paraswap; an unlisted + // solver never yields one from the same bytes. let amount_in = U256::from(171_521_496u64); let mut input = vec![0xe3u8, 0xea, 0xd5, 0x9e]; - for word in [amount_in, U256::from(171_430_663u64), U256::from(171_602_266u64), U256::ZERO] - { + for word in [ + U256::from(0x1111u64), // srcToken + U256::from(0x2222u64), // destToken + amount_in, + U256::from(171_430_663u64), + U256::from(171_602_266u64), + ] { input.extend_from_slice(&word.to_be_bytes::<32>()); } - assert!(embedded_quote("paraswap", &input, amount_in).is_some()); - assert!(embedded_quote("1inch", &input, amount_in).is_none()); + assert!(swap_intent("paraswap", &input, Some(amount_in)).is_some()); + assert!(swap_intent("1inch", &input, Some(amount_in)).is_none()); } } diff --git a/tools/hindsight/src/decoder/solvers/paraswap.rs b/tools/hindsight/src/decoder/solvers/paraswap.rs index 8a93c123..fc367d69 100644 --- a/tools/hindsight/src/decoder/solvers/paraswap.rs +++ b/tools/hindsight/src/decoder/solvers/paraswap.rs @@ -1,48 +1,89 @@ //! ParaSwap-specific calldata extraction. //! //! Augustus v6 swap methods carry the trade parameters as consecutive 32-byte words: -//! `…, fromAmount, toAmount, quotedAmount, …`. `toAmount` is the slippage floor and -//! `quotedAmount` the off-chain quoted output, kept on-chain for `ParaSwap`'s surplus accounting -//! (the user is capped at the quote, so settled often equals it exactly). The triple sits at a -//! different offset per method selector, so instead of per-selector ABI decoding it is located -//! by value: find the word equal to the trade's decoded input amount, then read the -//! floor-and-quote pair that follows it. +//! `…, srcToken, destToken, …, fromAmount, toAmount, quotedAmount, …`. `toAmount` is the +//! slippage floor and `quotedAmount` the off-chain quoted output, kept on-chain for `ParaSwap`'s +//! surplus accounting (the user is capped at the quote, so settled often equals it exactly). The +//! triple sits at a different offset per method selector, so instead of per-selector ABI decoding +//! it is located by value: find the word equal to the trade's known input amount — the caller's +//! hint, there is no other way to find it — then read the floor-and-quote pair that follows it and +//! the token pair that precedes it. -use alloy::primitives::U256; +use alloy::primitives::{Address, U256}; -use crate::decoder::solvers::{SolverKnowledge, SolverQuote}; +use crate::decoder::solvers::{SolverKnowledge, SwapIntent}; + +/// Byte length of an ABI-encoded word. +const WORD_LEN: usize = 32; +/// A `bytes32` word encoding an `address` has 12 zero prefix bytes, then the 20-byte address. +const ADDRESS_PREFIX_LEN: usize = 12; + +/// Whether a word is address-shaped: 12 zero prefix bytes and a non-zero address. Rejects a +/// coincidental match against unrelated calldata bytes. +fn is_address_word(word: U256) -> bool { + let bytes = word.to_be_bytes::(); + bytes[..ADDRESS_PREFIX_LEN] + .iter() + .all(|&byte| byte == 0) && + bytes[ADDRESS_PREFIX_LEN..] + .iter() + .any(|&byte| byte != 0) +} + +/// The address encoded in a word already known to be address-shaped. +fn address_from_word(word: U256) -> Address { + Address::from_slice(&word.to_be_bytes::()[ADDRESS_PREFIX_LEN..]) +} /// The `ParaSwap` solver. pub(crate) struct Paraswap; impl SolverKnowledge for Paraswap { - /// Extract `ParaSwap`'s `quotedAmount` from Augustus calldata. + /// Extract the trader's swap terms from Augustus calldata: the enforced floor and declared + /// quote by scanning for the word equal to `amount_in_hint`, the tokens from the two words + /// immediately preceding it. /// - /// Scans word-aligned calldata for `amount_in` and reads the two words after it as - /// `(toAmount, quotedAmount)`. A false positive would need a word that equals the exact - /// input amount *and* is followed by a plausible floor/quote pair (`0 < toAmount <= - /// quotedAmount <= 2 * toAmount`) — and the caller's settled-amount plausibility check - /// still applies after. Returns `None` when no such triple exists (e.g. a partner fee made - /// the decoded input differ from `fromAmount`), which just leaves the record without a - /// quote. - fn embedded_quote(&self, input: &[u8], amount_in: U256) -> Option { - if amount_in.is_zero() || input.len() < 4 { + /// A false positive would need a word that equals the exact input amount *and* is followed + /// by a plausible floor/quote pair (`0 < toAmount <= quotedAmount <= 2 * toAmount`) *and* + /// preceded by two address-shaped words — and the caller's settled-amount plausibility check + /// still applies to the quote afterward. Returns `None` when no such shape exists (e.g. a + /// partner fee made the decoded input differ from `fromAmount`, or the words before it are + /// not a token pair) — the intent is lost along with the quote, since there is nothing left + /// to recover the floor from. A reverted trade has no netted flow to draw a hint from, so + /// `amount_in_hint: None` always yields `None`. + fn swap_intent(&self, input: &[u8], amount_in_hint: Option) -> Option { + let amount_in = amount_in_hint.filter(|hint| !hint.is_zero())?; + if input.len() < 4 { return None; } let words: Vec = input[4..] - .as_chunks::<32>() + .as_chunks::() .0 .iter() .map(|word| U256::from_be_slice(word)) .collect(); - for window in words.windows(3) { + for (index, window) in words.windows(3).enumerate() { let (from_amount, to_amount, quoted) = (window[0], window[1], window[2]); if from_amount != amount_in || to_amount.is_zero() { continue; } - if quoted >= to_amount && quoted <= to_amount.saturating_mul(U256::from(2)) { - return Some(SolverQuote { amount_out: quoted, source: None, timestamp: None }); + if quoted < to_amount || quoted > to_amount.saturating_mul(U256::from(2)) { + continue; + } + if index < 2 { + continue; + } + let (src_token, dst_token) = (words[index - 2], words[index - 1]); + if !is_address_word(src_token) || !is_address_word(dst_token) { + continue; } + let intent = SwapIntent::new( + address_from_word(src_token), + address_from_word(dst_token), + amount_in, + to_amount, + ); + return Some(intent.with_quote(quoted, None)); } None } @@ -66,35 +107,61 @@ mod tests { // Live tx 0x1192b394… (block range of run5): srcToken, destToken, fromAmount, toAmount // (floor, -10bps), quotedAmount — the settled amount was 171,602,265, one unit under the // quote (ParaSwap caps the user at the quote and keeps the surplus). + let src_token = U256::from(0x1111u64); + let dst_token = U256::from(0x2222u64); + let amount_in = U256::from(171_521_496u64); let words = [ - U256::from(0x1111u64), // srcToken - U256::from(0x2222u64), // destToken - U256::from(171_521_496u64), + src_token, + dst_token, + amount_in, U256::from(171_430_663u64), U256::from(171_602_266u64), U256::ZERO, // metadata ]; - let quote = Paraswap - .embedded_quote(&calldata(&words), U256::from(171_521_496u64)) + let intent = Paraswap + .swap_intent(&calldata(&words), Some(amount_in)) .unwrap(); - assert_eq!(quote.amount_out, U256::from(171_602_266u64)); - assert_eq!(quote.source, None); - assert_eq!(quote.timestamp, None); + assert_eq!(intent.token_in, address_from_word(src_token)); + assert_eq!(intent.token_out, address_from_word(dst_token)); + assert_eq!(intent.amount_in, amount_in); + assert_eq!(intent.min_amount_out, U256::from(171_430_663u64)); + assert_eq!(intent.declared_quote(), Some(U256::from(171_602_266u64))); } #[test] fn test_missing_input_amount() { // A partner fee (or any decode difference) means no word equals the decoded input. - let words = - [U256::from(171_521_496u64), U256::from(171_430_663u64), U256::from(171_602_266u64)]; + let words = [ + U256::from(0x1111u64), + U256::from(0x2222u64), + U256::from(171_521_496u64), + U256::from(171_430_663u64), + U256::from(171_602_266u64), + ]; assert!(Paraswap - .embedded_quote(&calldata(&words), U256::from(999u64)) + .swap_intent(&calldata(&words), Some(U256::from(999u64))) .is_none()); assert!(Paraswap - .embedded_quote(&[], U256::from(1u64)) + .swap_intent(&[], Some(U256::from(1u64))) .is_none()); assert!(Paraswap - .embedded_quote(&calldata(&words), U256::ZERO) + .swap_intent(&calldata(&words), Some(U256::ZERO)) + .is_none()); + } + + #[test] + fn test_no_hint_never_yields_an_intent() { + // The revert path: no netted flow means no hint, so there is no way to locate the + // triple even when the calldata is otherwise well-formed. + let words = [ + U256::from(0x1111u64), + U256::from(0x2222u64), + U256::from(171_521_496u64), + U256::from(171_430_663u64), + U256::from(171_602_266u64), + ]; + assert!(Paraswap + .swap_intent(&calldata(&words), None) .is_none()); } @@ -103,13 +170,54 @@ mod tests { // The words after the input match are not a floor/quote pair: quote below the floor, or // wildly above it (different units). let amount_in = U256::from(1_000_000u64); - let below = [amount_in, U256::from(990_000u64), U256::from(400_000u64)]; + let below = [ + U256::from(0x1111u64), + U256::from(0x2222u64), + amount_in, + U256::from(990_000u64), + U256::from(400_000u64), + ]; assert!(Paraswap - .embedded_quote(&calldata(&below), amount_in) + .swap_intent(&calldata(&below), Some(amount_in)) .is_none()); - let far_above = [amount_in, U256::from(990_000u64), U256::from(10_000_000u64)]; + let far_above = [ + U256::from(0x1111u64), + U256::from(0x2222u64), + amount_in, + U256::from(990_000u64), + U256::from(10_000_000u64), + ]; + assert!(Paraswap + .swap_intent(&calldata(&far_above), Some(amount_in)) + .is_none()); + } + + #[test] + fn test_non_address_shaped_tokens_rejected() { + // The words before fromAmount are not address-shaped (top bytes set): the intent is + // dropped even though the floor/quote pair itself is plausible — there is nothing to + // build a trustworthy token pair from. + let amount_in = U256::from(1_000_000u64); + let words = [ + U256::MAX, // not address-shaped: every byte set + U256::from(0x2222u64), + amount_in, + U256::from(990_000u64), + U256::from(995_000u64), + ]; + assert!(Paraswap + .swap_intent(&calldata(&words), Some(amount_in)) + .is_none()); + } + + #[test] + fn test_fromamount_too_early_for_token_words() { + // fromAmount at the very start of the calldata: no room for the two preceding token + // words, even though the hint matches and the floor/quote pair is plausible. + let amount_in = U256::from(1_000_000u64); + let words = [amount_in, U256::from(990_000u64), U256::from(995_000u64)]; assert!(Paraswap - .embedded_quote(&calldata(&far_above), amount_in) + .swap_intent(&calldata(&words), Some(amount_in)) .is_none()); } } diff --git a/tools/hindsight/src/decoder/solvers/zeroex.rs b/tools/hindsight/src/decoder/solvers/zeroex.rs index b133bc8a..b0c2433b 100644 --- a/tools/hindsight/src/decoder/solvers/zeroex.rs +++ b/tools/hindsight/src/decoder/solvers/zeroex.rs @@ -1,108 +1,338 @@ //! 0x calldata extraction. //! -//! 0x's Settler calldata is a list of encoded actions whose numbers are slippage floors, not the -//! quoted output. Only the positive-slippage action carries the quote: when 0x skims surplus for an -//! integrator, it records the output the route was quoted at, and execution above it is the -//! surplus. That is the one quote 0x exposes on-chain. +//! Relay's 0x flow enters through `AllowanceHolder.exec(operator, token, amount, target, data)` +//! before reaching Settler's own `execute(AllowedSlippage, actions, zid)` — confirmed in a live +//! 21-transaction Base sample (settled and reverted): every one is `AllowanceHolder`-wrapped, and +//! for two settled trades the decoded `token`/`amount` matched the netted `token_in`/`amount_in` +//! hindsight already recorded exactly, and the decoded `buyToken` matched `token_out` exactly. +//! Both the wrapper (`exec`) and Settler's own entry (`execute`) are verified against 0x-settler's +//! published source (github.com/0xProject/0x-settler): `IAllowanceHolder.sol`, +//! `ISettlerTakerSubmitted.sol`, `ISettlerBase.sol`'s `AllowedSlippage` struct, and +//! `ISettlerActions.sol`'s `POSITIVE_SLIPPAGE` action. //! -//! So a quote is recovered only for swaps that let 0x skim surplus. Wallets that take their own fee -//! (Phantom, Robinhood) do not, and decode without a quote — partial coverage of the highest-volume -//! competitor. +//! A bare Settler entry (no `AllowanceHolder` wrapper) never occurred in the sample, and — unlike +//! `AllowedSlippage`, which is read the same way either way — has no calldata field that reliably +//! carries `token_in`/`amount_in`: Settler's `actions` array is heterogeneous per liquidity source, +//! so scanning it for an input-token address would be a guess, not a decode. `swap_intent` for a +//! bare entry is declined rather than guessed, per the "no dead code, no guessing" rule; if bare +//! entries turn out to matter, the `token_in` question needs its own investigation, not a shortcut +//! here. +//! +//! `is_slippage_floor` covers 0x's own `TooMuchSlippage(address,uint256,uint256)`, thrown by +//! `SettlerAbstract`/`SettlerErrors.sol` (`revertTooMuchSlippage`) — also verified against +//! 0x-settler's source and confirmed live: the selector appears in real reverted Base traces +//! bubbling through both registered 0x addresses (Settler and `AllowanceHolder`), full +//! ABI-encoded as `(token, expected, actual)` (see +//! `fixtures/trace_revert_zeroex_slippage_0x157e025b.json`, whose deepest frame decodes to +//! `token=USDC, expected=0xdd2ab926, actual=0`). `"return too low"` is not 0x's own error — it is +//! the revert string of at least one inner liquidity source 0x's Settler routes through (observed +//! live from a `Kipseli PropAMM` pool) — but 0x is the only address our registry knows about +//! anywhere in those traces, so it is recognized here rather than left unclassified. + +use alloy::{ + primitives::{Address, U256}, + sol, + sol_types::SolCall, +}; + +use crate::decoder::solvers::{SolverKnowledge, SwapIntent}; + +sol! { + /// `IAllowanceHolder.exec` — Relay's 0x flow always enters through this wrapper before + /// reaching Settler; `token`/`amount` are the taker's declared input, temporarily authorised + /// for `operator` (Settler) to consume. + function exec(address operator, address token, uint256 amount, address target, bytes data) + external + payable + returns (bytes memory result); + + /// `ISettlerBase.AllowedSlippage` — the taker's floor and payout address, read by + /// `Settler.execute` regardless of whether it was reached via `AllowanceHolder` or directly. + struct AllowedSlippage { + address recipient; + address buyToken; + uint256 minAmountOut; + } + + /// `ISettlerTakerSubmitted.execute` — Settler's main taker-submitted entry. + function execute(AllowedSlippage slippage, bytes[] actions, bytes32 zidAndAffiliate) + external + payable + returns (bool); -use alloy::primitives::U256; + /// `ISettlerActions.POSITIVE_SLIPPAGE` — one element of `execute`'s `actions` array, present + /// only when the route has slippage headroom to declare; `expectedAmount` is 0x's own + /// off-chain quote for the trade, usable as this solver's declared quote. + function POSITIVE_SLIPPAGE(address recipient, address token, uint256 expectedAmount, uint256 maxBps) + external; +} + +/// 0x's own native-ETH sentinel — distinct from hindsight's `Address::ZERO` convention, so it is +/// normalized on the way out (matching `KyberSwap`'s equivalent). +const ZEROEX_NATIVE: Address = + alloy::primitives::address!("0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"); -use crate::decoder::solvers::{SolverKnowledge, SolverQuote}; +fn normalize_native(token: Address) -> Address { + if token == ZEROEX_NATIVE { + Address::ZERO + } else { + token + } +} -/// Selector of 0x's positive-slippage action (`bytes4` of its signature; see the test). -const POSITIVE_SLIPPAGE: [u8; 4] = [0x34, 0xee, 0x90, 0xca]; +/// `keccak256("TooMuchSlippage(address,uint256,uint256)")[..4]`. +const TOO_MUCH_SLIPPAGE: [u8; 4] = [0x97, 0xa6, 0xf3, 0xb9]; + +/// Revert reason from a liquidity source 0x's Settler routed through, when its own output floor +/// was not met. +const RETURN_TOO_LOW: &str = "return too low"; + +/// Settler's own terms, decoded from an `execute` call regardless of how it was reached (wrapped +/// in `AllowanceHolder.exec` or, if it ever occurs, called directly). +struct SettlerTerms { + recipient: Address, + buy_token: Address, + min_amount_out: U256, + declared_quote: Option, +} + +/// Decode `execute`'s `AllowedSlippage` plus, when present, a `POSITIVE_SLIPPAGE` action's +/// declared quote. Each element of `actions` is itself a standalone ABI-encoded call (its own +/// 4-byte selector and args, exactly like top-level calldata) — verified against a real decode — +/// so trying `POSITIVE_SLIPPAGE`'s decode against every action and keeping the one that succeeds +/// is a correct way to find it, not a heuristic. +fn decode_execute(input: &[u8]) -> Option { + let call = executeCall::abi_decode(input).ok()?; + let declared_quote = call + .actions + .iter() + .find_map(|action| POSITIVE_SLIPPAGECall::abi_decode(action).ok()) + .map(|action| action.expectedAmount); + Some(SettlerTerms { + recipient: call.slippage.recipient, + buy_token: normalize_native(call.slippage.buyToken), + min_amount_out: call.slippage.minAmountOut, + declared_quote, + }) +} -/// The 0x solver. pub(crate) struct ZeroEx; impl SolverKnowledge for ZeroEx { - /// Extract 0x's quoted output from the positive-slippage action, when the calldata has one. - /// - /// The action is found by its selector, not by decoding the whole Settler call, so it is picked - /// up wherever 0x sits — the entry point, behind the shared allowance contract, or nested in - /// another venue's calldata. The two address words after the selector must have their top 12 - /// bytes zero, which rejects a stray selector match in unrelated bytes. The caller unit-checks - /// the quote against the settled amount. - fn embedded_quote(&self, input: &[u8], _amount_in: U256) -> Option { - let start = input - .windows(POSITIVE_SLIPPAGE.len()) - .position(|window| window == POSITIVE_SLIPPAGE)?; - // Three words after the selector: recipient, token, expectedAmount. - let action = input.get(start + 4..start + 4 + 96)?; - let is_address = |word: &[u8]| word[..12].iter().all(|&byte| byte == 0); - if !is_address(&action[0..32]) || !is_address(&action[32..64]) { + /// The trader's swap terms from `AllowanceHolder.exec`'s own parameters (`token`/`amount`, + /// the input side) and the wrapped `execute` call's `AllowedSlippage` (`buyToken`/ + /// `minAmountOut`, the output side). `minAmountOut` is passed through as-is, including a + /// legitimate zero (Settler's per-action slippage checks can leave the top-level floor at + /// zero) — the intent is still worth recording, and the fillable/margin judgment already + /// treats a zero floor sanely (trivially fillable, no margin to compute). `amount_in_hint` is + /// unused: `AllowanceHolder`'s own parameter is the real amount, not a value to locate a field + /// by. + fn swap_intent(&self, input: &[u8], _amount_in_hint: Option) -> Option { + let call = execCall::abi_decode(input).ok()?; + if call.amount.is_zero() { return None; } - let amount_out = U256::from_be_slice(&action[64..96]); - if amount_out.is_zero() { - return None; + let terms = decode_execute(&call.data)?; + let intent = SwapIntent::new( + normalize_native(call.token), + terms.buy_token, + call.amount, + terms.min_amount_out, + ); + Some(match terms.declared_quote { + Some(quote) => intent.with_quote(quote, None), + None => intent, + }) + } + + /// Settler's `AllowedSlippage.recipient` — the address whose receipt `RelayCalldata` anchors + /// the settled amount on, same as Fly/KyberSwap. Tried both wrapped (`AllowanceHolder.exec`) + /// and bare (`execute` called directly): unlike `swap_intent`, the recipient needs nothing + /// `AllowanceHolder` adds, so a bare entry still resolves even though its `token_in` cannot. + fn output_recipient(&self, input: &[u8]) -> Option
{ + if let Ok(call) = execCall::abi_decode(input) { + return decode_execute(&call.data).map(|terms| terms.recipient); } - Some(SolverQuote { amount_out, source: None, timestamp: None }) + decode_execute(input).map(|terms| terms.recipient) + } + + /// Whether a reverted frame's output is 0x's own `TooMuchSlippage` selector, or its revert + /// reason is the "return too low" string a liquidity source 0x's Settler routes through + /// returns. + fn is_slippage_floor(&self, output: Option<&[u8]>, revert_reason: Option<&str>) -> bool { + output.is_some_and(|output| output.starts_with(&TOO_MUCH_SLIPPAGE)) || + revert_reason.is_some_and(|reason| reason.contains(RETURN_TOO_LOW)) } } #[cfg(test)] mod tests { - use alloy::primitives::Address; + use alloy::primitives::address; use super::*; - use crate::decoder::test_utils::addr; - /// A `POSITIVE_SLIPPAGE` action, ABI-encoded as it appears in the Settler `actions` array, - /// surrounded by other calldata bytes. - fn calldata_with_positive_slippage(token: Address, expected: u128) -> Vec { - let mut action = POSITIVE_SLIPPAGE.to_vec(); - action.extend_from_slice(addr(7).into_word().as_slice()); // recipient - action.extend_from_slice(token.into_word().as_slice()); - action.extend_from_slice(&U256::from(expected).to_be_bytes::<32>()); - action.extend_from_slice(&U256::from(50u64).to_be_bytes::<32>()); // maxBps + /// The `AllowanceHolder.exec` calldata of a real settled Base trade (tx + /// `0x229f8cd137f7c9de635021de197d5472759d03993564853ff4690c327707ed79`): decoding recovers + /// `token_in`/`amount_in` matching the netted settled record exactly (native ETH, + /// 214715436309542453 wei) and `token_out` matching too (Base USDC). + fn settled_input() -> Vec { + let text = include_str!("fixtures/zeroex_settled_input.txt").trim(); + alloy::hex::decode(text.strip_prefix("0x").unwrap_or(text)).unwrap() + } + + /// The `AllowanceHolder.exec` calldata of a real reverted Base trade (tx + /// `0x157e025bd22ff0b222e4d2a04bb27caaef241f53f2bb7ecd22b7ab438c6b713f`) whose deepest frame + /// reverted with `TooMuchSlippage` (see `trace.rs`'s + /// `test_classify_revert_cause_real_zeroex_slippage`, which shares this transaction). + fn reverted_input() -> Vec { + let text = include_str!("fixtures/zeroex_reverted_input.txt").trim(); + alloy::hex::decode(text.strip_prefix("0x").unwrap_or(text)).unwrap() + } - let mut input = vec![0x11u8; 68]; // an outer selector + leading words - input.extend_from_slice(&action); - input.extend_from_slice(&[0u8; 32]); // trailing calldata - input + const USDC: Address = address!("0x833589fcd6edb6e08f4c7c32d4f71b54bda02913"); + const RELAY_ROUTER: Address = address!("0xb92fe925dc43a0ecde6c8b1a2709c170ec4fff4f"); + + #[test] + fn test_real_settled_swap_intent() { + let intent = ZeroEx + .swap_intent(&settled_input(), None) + .unwrap(); + assert_eq!(intent.token_in, Address::ZERO); // 0x's native-ETH sentinel, normalized + assert_eq!(intent.token_out, USDC); + assert_eq!(intent.amount_in, U256::from(214_715_436_309_542_453u64)); + assert_eq!(intent.min_amount_out, U256::from(388_129_000u64)); + assert_eq!(intent.declared_quote(), Some(U256::from(396_058_371u64))); } #[test] - fn test_selector_matches_settler_action() { - // bytes4(keccak256("POSITIVE_SLIPPAGE(address,address,uint256,uint256)")) — verified - // against the 0x-settler ISettlerActions source. - assert_eq!(POSITIVE_SLIPPAGE, [0x34, 0xee, 0x90, 0xca]); + fn test_real_settled_output_recipient() { + assert_eq!(ZeroEx.output_recipient(&settled_input()), Some(RELAY_ROUTER)); } #[test] - fn test_reads_expected_amount() { - let input = calldata_with_positive_slippage(addr(11), 2_000_000); - let quote = ZeroEx - .embedded_quote(&input, U256::ZERO) + fn test_real_reverted_swap_intent() { + // The reverted trade's terms decode the same way a settled one's do — a revert emits no + // logs, so calldata is the only source, and it is read no differently here. + let intent = ZeroEx + .swap_intent(&reverted_input(), None) .unwrap(); - assert_eq!(quote.amount_out, U256::from(2_000_000u64)); - assert_eq!(quote.source, None); - assert_eq!(quote.timestamp, None); + assert_eq!(intent.token_in, Address::ZERO); + assert_eq!(intent.token_out, USDC); + assert_eq!(intent.amount_in, U256::from(2_018_128_791_326_365_345u64)); + assert_eq!(intent.min_amount_out, U256::from(3_643_640_000u64)); + assert_eq!(intent.declared_quote(), Some(U256::from(3_718_000_789u64))); } #[test] - fn test_no_positive_slippage_action() { - // A 0x swap without the action (a wallet integrator taking its own fee) carries no quote. + fn test_real_reverted_output_recipient() { + assert_eq!(ZeroEx.output_recipient(&reverted_input()), Some(RELAY_ROUTER)); + } + + #[test] + fn test_selectors_match_computed_signatures() { + assert_eq!(execCall::SELECTOR, [0x22, 0x13, 0xbc, 0x0b]); + assert_eq!(executeCall::SELECTOR, [0x1f, 0xff, 0x99, 0x1f]); + assert_eq!(POSITIVE_SLIPPAGECall::SELECTOR, [0x34, 0xee, 0x90, 0xca]); + } + + #[test] + fn test_bare_settler_entry_has_no_swap_intent_but_resolves_recipient() { + // A direct `execute` call (no `AllowanceHolder` wrapper): swap_intent has nowhere to read + // token_in/amount_in from, so it declines; output_recipient does not need them. + let call = executeCall { + slippage: AllowedSlippage { + recipient: RELAY_ROUTER, + buyToken: USDC, + minAmountOut: U256::from(1_000u64), + }, + actions: vec![], + zidAndAffiliate: alloy::primitives::FixedBytes::default(), + }; + let input = executeCall::abi_encode(&call); + assert!(ZeroEx + .swap_intent(&input, None) + .is_none()); + assert_eq!(ZeroEx.output_recipient(&input), Some(RELAY_ROUTER)); + } + + #[test] + fn test_garbage_input_declines() { assert!(ZeroEx - .embedded_quote(&[0x00; 200], U256::ZERO) + .swap_intent(&[0xde, 0xad, 0xbe, 0xef], None) .is_none()); assert!(ZeroEx - .embedded_quote(&[], U256::ZERO) + .output_recipient(&[0xde, 0xad, 0xbe, 0xef]) .is_none()); } #[test] - fn test_stray_selector_without_address_words() { - // The selector bytes appearing amid non-address data (a coincidental match) are rejected. - let mut input = POSITIVE_SLIPPAGE.to_vec(); - input.extend_from_slice(&[0xff; 96]); + fn test_zero_amount_in_rejected() { + // A garbage/zero AllowanceHolder amount is not a real trade. + let call = execCall { + operator: RELAY_ROUTER, + token: Address::ZERO, + amount: U256::ZERO, + target: RELAY_ROUTER, + data: alloy::primitives::Bytes::new(), + }; + let input = execCall::abi_encode(&call); assert!(ZeroEx - .embedded_quote(&input, U256::ZERO) + .swap_intent(&input, None) .is_none()); } + + #[test] + fn test_zero_min_amount_out_still_produces_an_intent() { + // A legitimate shape: Settler can leave the top-level floor at zero when per-action + // slippage checks are used instead. The intent is still recorded rather than dropped. + let execute_call = executeCall { + slippage: AllowedSlippage { + recipient: RELAY_ROUTER, + buyToken: USDC, + minAmountOut: U256::ZERO, + }, + actions: vec![], + zidAndAffiliate: alloy::primitives::FixedBytes::default(), + }; + let call = execCall { + operator: RELAY_ROUTER, + token: Address::ZERO, + amount: U256::from(1_000u64), + target: RELAY_ROUTER, + data: executeCall::abi_encode(&execute_call).into(), + }; + let input = execCall::abi_encode(&call); + let intent = ZeroEx + .swap_intent(&input, None) + .unwrap(); + assert_eq!(intent.min_amount_out, U256::ZERO); + } + + #[test] + fn test_native_eth_sentinel_normalized() { + assert_eq!(normalize_native(ZEROEX_NATIVE), Address::ZERO); + assert_eq!(normalize_native(USDC), USDC); + } + + #[test] + fn test_is_slippage_floor_matches_the_selector() { + // The full ABI-encoded shape (token, expected, actual) — only the selector is checked. + let mut output = TOO_MUCH_SLIPPAGE.to_vec(); + output.extend_from_slice(&[0u8; 96]); + assert!(ZeroEx.is_slippage_floor(Some(&output), None)); + assert!(!ZeroEx.is_slippage_floor(Some(&[0xde, 0xad, 0xbe, 0xef]), None)); + } + + #[test] + fn test_is_slippage_floor_matches_the_revert_reason() { + assert!(ZeroEx.is_slippage_floor(None, Some("return too low"))); + // Substring, not exact match: geth's callTracer sometimes carries surrounding context. + assert!(ZeroEx.is_slippage_floor(None, Some("execution reverted: return too low"))); + assert!(!ZeroEx.is_slippage_floor(None, Some("out of gas"))); + } + + #[test] + fn test_is_slippage_floor_declines_when_neither_is_present() { + assert!(!ZeroEx.is_slippage_floor(None, None)); + } } diff --git a/tools/hindsight/src/decoder/test_utils.rs b/tools/hindsight/src/decoder/test_utils.rs index c57909fe..3b64b83c 100644 --- a/tools/hindsight/src/decoder/test_utils.rs +++ b/tools/hindsight/src/decoder/test_utils.rs @@ -120,3 +120,35 @@ pub(crate) fn receipt( contract_address: None, }) } + +/// A synthetic reverted (status-0) receipt: no logs, since a reverted transaction emits none. +pub(crate) fn reverted_receipt( + hash: TxHash, + from: Address, + to: Option
, +) -> AnyTransactionReceipt { + WithOtherFields::new(TransactionReceipt { + inner: AnyReceiptEnvelope { + inner: ReceiptWithBloom { + receipt: Receipt { + status: Eip658Value::Eip658(false), + cumulative_gas_used: 0, + logs: vec![], + }, + logs_bloom: Bloom::default(), + }, + r#type: 0, + }, + transaction_hash: hash, + transaction_index: None, + block_hash: None, + block_number: None, + gas_used: 0, + effective_gas_price: 0, + blob_gas_used: None, + blob_gas_price: None, + from, + to, + contract_address: None, + }) +} diff --git a/tools/hindsight/src/decoder/trace.rs b/tools/hindsight/src/decoder/trace.rs index 5eb2f765..abfe1c4d 100644 --- a/tools/hindsight/src/decoder/trace.rs +++ b/tools/hindsight/src/decoder/trace.rs @@ -5,7 +5,7 @@ use alloy::{ }; use anyhow::Context; -use crate::decoder::registry::Registry; +use crate::decoder::{registry::Registry, solvers}; /// Fetch the callTracer root frame for a transaction. /// @@ -79,18 +79,40 @@ pub(crate) fn route_gas(root: &CallFrame, registry: &Registry) -> Option { .filter(|gas| !gas.is_zero()) } -/// Depth-first search for the first call frame into a known solver, skipping reverted frames -/// (and their subtrees), which settle nothing. +/// Depth-first search for the call frame that settled or tried the swap with a known solver. /// -/// The one walk serves both questions asked of a trace: *who* settled the swap (the frame's -/// `to`, for attribution) and *what the route cost* (the frame's `gas_used`, for gas -/// accounting) — so the gas charged is always the gas of the exact frame the solver label came -/// from. +/// Prefers the frame that actually settled — skipping reverted frames and their subtrees, which +/// settle nothing — and, only when that search finds nothing, falls back to a tolerant search +/// that descends into reverted frames too. Plain "ignore every revert" (the simpler-looking +/// alternative) would get a settled trade wrong: a router that tried solver A (which reverted) +/// before succeeding via solver B has both frames in its trace, and a revert-blind walk can +/// attribute to whichever it reaches first in depth-first order, not whichever actually settled. +/// The two-phase preference handles both shapes with one function: a settled trade always finds +/// its real solver frame in the first (strict) pass, and a reverted trade — which has no settled +/// frame by definition — falls through to the second (tolerant) pass, since a revert emits no +/// logs and the frame that tried is the only frame there is to attribute to or recover calldata +/// from (see the trace fixtures for both shapes observed live). +/// +/// The one walk serves every question asked of a trace: *who* settled or tried the swap (the +/// frame's `to`, for attribution and calldata extraction) and *what the route cost* (the frame's +/// `gas_used`, for gas accounting) — so the gas charged is always the gas of the exact frame the +/// solver label came from. pub(crate) fn find_solver_frame<'a>( frame: &'a CallFrame, registry: &Registry, ) -> Option<&'a CallFrame> { - if frame.error.is_some() { + find_solver_frame_impl(frame, registry, false) + .or_else(|| find_solver_frame_impl(frame, registry, true)) +} + +/// Shared walk for both passes of [`find_solver_frame`]: the only difference is whether a frame's +/// own revert stops the search or not. +fn find_solver_frame_impl<'a>( + frame: &'a CallFrame, + registry: &Registry, + tolerate_reverts: bool, +) -> Option<&'a CallFrame> { + if frame.error.is_some() && !tolerate_reverts { return None; } if let Some(to) = frame.to { @@ -101,7 +123,103 @@ pub(crate) fn find_solver_frame<'a>( frame .calls .iter() - .find_map(|child| find_solver_frame(child, registry)) + .find_map(|child| find_solver_frame_impl(child, registry, tolerate_reverts)) +} + +/// The geth call tracer's exact error string for a frame that ran out of gas. +const OUT_OF_GAS: &str = "out of gas"; + +/// Why a reverted transaction failed, classified from its trace. +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)] +#[serde(tag = "kind", content = "detail", rename_all = "snake_case")] +pub(crate) enum RevertCause { + /// A slippage-floor revert: Fly's `InsufficientAmountOut()` or `KyberSwap`'s "Return amount is + /// not enough" — the class of revert a fresher quote could have avoided. + SlippageFloor, + /// A frame ran out of gas. + OutOfGas, + /// Any other cause, taken from the deepest reverted frame's error or revert reason. + Other(String), +} + +/// Classify why a reverted transaction's root frame failed, from its whole reverted subtree. +/// +/// Checked in order of specificity: a slippage-floor marker (Fly's custom-error selector in any +/// frame's output, or `KyberSwap`'s revert reason string) anywhere in the subtree wins even when a +/// deeper frame failed for an unrelated reason — real reverts often cascade through several +/// frames, and the slippage-floor check is the one classification callers act on (it is the +/// avoidable class). Otherwise, any frame with the exact "out of gas" error settles it. Anything +/// else falls back to the deepest reverted frame's own error or revert reason. +pub(crate) fn classify_revert_cause(root: &CallFrame) -> RevertCause { + if has_slippage_floor_marker(root) { + return RevertCause::SlippageFloor; + } + if has_out_of_gas(root) { + return RevertCause::OutOfGas; + } + RevertCause::Other(deepest_revert_reason(root).unwrap_or_else(|| "unknown revert".to_string())) +} + +fn has_slippage_floor_marker(frame: &CallFrame) -> bool { + let output = frame.output.as_ref().map(AsRef::as_ref); + solvers::is_slippage_floor(output, frame.revert_reason.as_deref()) || + frame + .calls + .iter() + .any(has_slippage_floor_marker) +} + +fn has_out_of_gas(frame: &CallFrame) -> bool { + frame.error.as_deref() == Some(OUT_OF_GAS) || frame.calls.iter().any(has_out_of_gas) +} + +/// The error or revert reason of the deepest reverted frame, by depth-first search — the last +/// (deepest) hit wins, since a subtree's own failure is usually more specific than an ancestor's +/// bubbled-up "execution reverted". +fn deepest_revert_reason(frame: &CallFrame) -> Option { + let mut deepest: Option<(usize, String)> = None; + collect_deepest_revert_reason(frame, 0, &mut deepest); + deepest.map(|(_, reason)| reason) +} + +fn collect_deepest_revert_reason( + frame: &CallFrame, + depth: usize, + deepest: &mut Option<(usize, String)>, +) { + if let Some(reason) = frame_revert_reason(frame) { + if deepest + .as_ref() + .is_none_or(|(best_depth, _)| depth >= *best_depth) + { + *deepest = Some((depth, reason)); + } + } + for child in &frame.calls { + collect_deepest_revert_reason(child, depth + 1, deepest); + } +} + +/// One frame's revert reason: the tracer's own ABI-decoded string when it provides one, otherwise +/// the generic error message with the frame's raw output selector appended when there is one to +/// show (`"execution reverted (0x12345678)"`). Several RPC providers never populate +/// `revert_reason` at all, even for a frame whose output does encode a selector (a custom error, +/// or an `Error(string)` the tracer did not bother decoding) — the selector is what is left to +/// classify offline, so it is surfaced rather than dropped. +fn frame_revert_reason(frame: &CallFrame) -> Option { + if let Some(reason) = frame.revert_reason.clone() { + return Some(reason); + } + let error = frame.error.clone()?; + let selector = frame + .output + .as_ref() + .filter(|output| output.len() >= 4) + .map(|output| format!("0x{}", alloy::hex::encode(&output[..4]))); + Some(match selector { + Some(selector) => format!("{error} ({selector})"), + None => error, + }) } /// Best guess at an unknown router: the entry point's direct child call that moved the most @@ -144,6 +262,7 @@ pub(crate) fn largest_external_call( #[cfg(test)] mod tests { use alloy::primitives::address; + use tycho_simulation::tycho_common::models::Chain; use super::*; use crate::decoder::test_utils::{addr, frame}; @@ -270,15 +389,183 @@ mod tests { } #[test] - fn test_find_solver_frame_reverted_frames() { + fn test_find_solver_frame_prefers_a_settled_frame_over_a_reverted_attempt() { + // The exact shape the doc comment calls out: a router tried solver A (which reverted) + // before succeeding via solver B. The strict pass must attribute to B, not whichever it + // reaches first in depth-first order. let registry = Registry::ethereum(); let oneinch = address!("0x111111125421ca6dc452d289314280a0f8842a65"); + let zerox = address!("0x0000000000001ff3684f28c67538d4d072c22734"); - let mut reverted = frame("CALL", addr(2), oneinch, 0); - reverted.error = Some("execution reverted".to_string()); + let mut attempt_a = frame("CALL", addr(2), oneinch, 0); + attempt_a.error = Some("execution reverted".to_string()); + let attempt_b = frame("CALL", addr(2), zerox, 0); let mut root = frame("CALL", addr(1), addr(2), 0); - root.calls = vec![reverted]; + root.calls = vec![attempt_a, attempt_b]; + + let found = find_solver_frame(&root, ®istry).unwrap(); + assert_eq!(found.to, Some(zerox)); + } + + #[test] + fn test_find_solver_frame_falls_back_when_nothing_settled() { + // A reverted trade has no settled frame by definition: the strict pass finds nothing, so + // the tolerant fallback is the only way to recover which router the trader was routed + // through — the frame that tried is the only frame there is to find. + let registry = Registry::ethereum(); + let oneinch = address!("0x111111125421ca6dc452d289314280a0f8842a65"); - assert!(find_solver_frame(&root, ®istry).is_none()); + let mut solver_call = frame("CALL", addr(2), oneinch, 0); + solver_call.error = Some("execution reverted".to_string()); + let mut root = frame("CALL", addr(1), addr(2), 0); + root.error = Some("execution reverted".to_string()); + root.calls = vec![solver_call]; + + let found = find_solver_frame(&root, ®istry).unwrap(); + assert_eq!(found.to, Some(oneinch)); + } + + #[test] + fn test_find_solver_frame_real_fly_slippage_trace() { + // Real reverted trace (Base, Relay -> Fly): the fly frame itself reverted + // (InsufficientAmountOut), nested inside the reverted router and approval-proxy frames. + // Nothing settled, so the strict pass finds nothing and the fallback recovers it. + let root: CallFrame = serde_json::from_str(include_str!( + "fixtures/trace_revert_fly_slippage_0xcba01d0c.json" + )) + .unwrap(); + let fly = address!("0x20f6ee51340adeed01a59b0e65cb3703f3dc860c"); + let found = find_solver_frame(&root, &Registry::builtin(Chain::Base).unwrap()).unwrap(); + assert_eq!(found.to, Some(fly)); + } + + #[test] + fn test_find_solver_frame_real_out_of_gas_trace() { + // Real reverted trace (Base): the fly frame itself succeeded (err = None) — the router + // ran out of gas on a later, unrelated call — so the strict pass already finds it, even + // though it sits under two reverted ancestors. + let root: CallFrame = + serde_json::from_str(include_str!("fixtures/trace_revert_out_of_gas_0x08fee57c.json")) + .unwrap(); + let fly = address!("0x20f6ee51340adeed01a59b0e65cb3703f3dc860c"); + let found = find_solver_frame(&root, &Registry::builtin(Chain::Base).unwrap()).unwrap(); + assert_eq!(found.to, Some(fly)); + assert!(found.error.is_none()); + } + + #[test] + fn test_classify_revert_cause_real_fly_slippage() { + let root: CallFrame = serde_json::from_str(include_str!( + "fixtures/trace_revert_fly_slippage_0xcba01d0c.json" + )) + .unwrap(); + assert_eq!(classify_revert_cause(&root), RevertCause::SlippageFloor); + } + + #[test] + fn test_classify_revert_cause_real_kyber_slippage() { + let root: CallFrame = serde_json::from_str(include_str!( + "fixtures/trace_revert_kyber_slippage_0xd3b7ffae.json" + )) + .unwrap(); + assert_eq!(classify_revert_cause(&root), RevertCause::SlippageFloor); + } + + #[test] + fn test_classify_revert_cause_real_out_of_gas() { + let root: CallFrame = + serde_json::from_str(include_str!("fixtures/trace_revert_out_of_gas_0x08fee57c.json")) + .unwrap(); + assert_eq!(classify_revert_cause(&root), RevertCause::OutOfGas); + } + + #[test] + fn test_classify_revert_cause_real_other() { + // A `transferFrom` failure deep inside Fly's frame (a custom ERC-20 error, not one of the + // known slippage markers): neither Fly's nor KyberSwap's marker matches, so it falls back + // to the deepest reverted frame's own error — this RPC never populates `revertReason`, so + // the frame's raw output selector (0xe450d38c, `ERC20InsufficientBalance(address,uint256, + // uint256)`) is appended to the generic message rather than left unclassifiable. + let root: CallFrame = serde_json::from_str(include_str!( + "fixtures/trace_revert_transfer_failure_0x12d802d5.json" + )) + .unwrap(); + assert_eq!( + classify_revert_cause(&root), + RevertCause::Other("execution reverted (0xe450d38c)".to_string()) + ); + } + + #[test] + fn test_classify_revert_cause_synthetic_slippage_and_gas() { + let mut fly_floor = frame("CALL", addr(1), addr(2), 0); + fly_floor.error = Some("execution reverted".to_string()); + fly_floor.output = Some(alloy::primitives::Bytes::from_static(&[0xe5, 0x29, 0x70, 0xaa])); + assert_eq!(classify_revert_cause(&fly_floor), RevertCause::SlippageFloor); + + let mut kyber_floor = frame("CALL", addr(1), addr(2), 0); + kyber_floor.error = Some("execution reverted".to_string()); + kyber_floor.revert_reason = Some("Return amount is not enough".to_string()); + assert_eq!(classify_revert_cause(&kyber_floor), RevertCause::SlippageFloor); + + let mut out_of_gas = frame("CALL", addr(1), addr(2), 0); + out_of_gas.error = Some("out of gas".to_string()); + assert_eq!(classify_revert_cause(&out_of_gas), RevertCause::OutOfGas); + } + + #[test] + fn test_classify_revert_cause_prefers_deepest_reason() { + let mut inner = frame("CALL", addr(2), addr(3), 0); + inner.error = Some("transferFrom failed".to_string()); + let mut outer = frame("CALL", addr(1), addr(2), 0); + outer.error = Some("execution reverted".to_string()); + outer.calls = vec![inner]; + + assert_eq!( + classify_revert_cause(&outer), + RevertCause::Other("transferFrom failed".to_string()) + ); + } + + #[test] + fn test_classify_revert_cause_real_zeroex_slippage() { + // Real reverted trace (Base): 0x's own TooMuchSlippage(address,uint256,uint256) bubbles + // through the Settler and AllowanceHolder frames, both registered as solver "0x". + let root: CallFrame = serde_json::from_str(include_str!( + "fixtures/trace_revert_zeroex_slippage_0x157e025b.json" + )) + .unwrap(); + assert_eq!(classify_revert_cause(&root), RevertCause::SlippageFloor); + } + + #[test] + fn test_frame_revert_reason_appends_the_selector_when_undecoded() { + // No `revert_reason` (this RPC never populates it) but the frame's raw output carries a + // selector: the generic message gets it appended, so an unrecognized custom error stays + // classifiable offline instead of collapsing into a bare "execution reverted". + let mut custom_error = frame("CALL", addr(1), addr(2), 0); + custom_error.error = Some("execution reverted".to_string()); + custom_error.output = + Some(alloy::primitives::Bytes::from_static(&[0x12, 0x34, 0x56, 0x78])); + assert_eq!( + frame_revert_reason(&custom_error), + Some("execution reverted (0x12345678)".to_string()) + ); + } + + #[test] + fn test_frame_revert_reason_prefers_the_decoded_reason() { + let mut decoded = frame("CALL", addr(1), addr(2), 0); + decoded.error = Some("execution reverted".to_string()); + decoded.revert_reason = Some("Return amount is not enough".to_string()); + decoded.output = Some(alloy::primitives::Bytes::from_static(&[0x12, 0x34, 0x56, 0x78])); + assert_eq!(frame_revert_reason(&decoded), Some("Return amount is not enough".to_string())); + } + + #[test] + fn test_frame_revert_reason_no_output_stays_bare() { + let mut bare = frame("CALL", addr(1), addr(2), 0); + bare.error = Some("execution reverted".to_string()); + assert_eq!(frame_revert_reason(&bare), Some("execution reverted".to_string())); } } diff --git a/tools/hindsight/src/decoder/transfer_ledger.rs b/tools/hindsight/src/decoder/transfer_ledger.rs index c60dd821..3fae7593 100644 --- a/tools/hindsight/src/decoder/transfer_ledger.rs +++ b/tools/hindsight/src/decoder/transfer_ledger.rs @@ -153,6 +153,15 @@ impl TransferLedger { participants } + /// Gross amount of `token` received by `recipient` alone, regardless of sender (native ETH is + /// `Address::ZERO`). Zero when the recipient received none of it. + pub(crate) fn received_by_address(&self, recipient: Address, token: Address) -> U256 { + self.transfers + .iter() + .filter(|&&(transfer_token, _, to, _)| transfer_token == token && to == recipient) + .fold(U256::ZERO, |total, &(_, _, _, value)| total.saturating_add(value)) + } + /// Gross total received per token by any of `recipients`, regardless of sender (native ETH /// keyed by `Address::ZERO`). pub(crate) fn received_by(&self, recipients: &HashSet
) -> HashMap { @@ -540,6 +549,28 @@ mod tests { .is_empty()); } + #[test] + fn test_received_by_address_sums_only_the_one_recipient_and_token() { + let recipient = addr(7); + let token = addr(10); + let logs = vec![ + make_transfer_log(token, addr(50), recipient, U256::from(1_000)), + make_transfer_log(token, addr(51), recipient, U256::from(500)), + // Different recipient, same token: excluded. + make_transfer_log(token, addr(50), addr(8), U256::from(999)), + // Same recipient, different token: excluded. + make_transfer_log(addr(11), addr(50), recipient, U256::from(999)), + ]; + let native = vec![(addr(50), recipient, U256::from(999))]; + let transfer_ledger = TransferLedger::from_transaction(&logs, &native); + assert_eq!(transfer_ledger.received_by_address(recipient, token), U256::from(1_500u64)); + assert_eq!( + transfer_ledger.received_by_address(recipient, Address::ZERO), + U256::from(999u64) + ); + assert_eq!(transfer_ledger.received_by_address(addr(200), token), U256::ZERO); + } + #[test] fn test_group_round_trips() { // The group sends 1000 of token A and gets 300 back: net sent 700, no net receipt. diff --git a/tools/hindsight/src/decoder/venues/coinbase.rs b/tools/hindsight/src/decoder/venues/coinbase.rs index 6a761f5f..396ba5b9 100644 --- a/tools/hindsight/src/decoder/venues/coinbase.rs +++ b/tools/hindsight/src/decoder/venues/coinbase.rs @@ -48,7 +48,7 @@ mod tests { use super::*; use crate::decoder::{ registry::Registry, - test_utils::{addr, make_transfer_log, receipt, swap, tx_hash}, + test_utils::{addr, frame, make_transfer_log, receipt, swap, tx_hash}, transfer_ledger::TransferLedger, }; @@ -71,6 +71,7 @@ mod tests { let provider = RootProvider::new(RpcClient::mocked(Asserter::new())); let mut code_cache = HashMap::new(); let receipt = receipt(tx_hash(1), sender, Some(entry_point), vec![]); + let root = frame("CALL", sender, entry_point, 0); let mut ctx = DecodeContext { provider: &provider, registry, @@ -79,6 +80,7 @@ mod tests { entry_point, transfer_ledger: ledger, input: &[], + root: &root, venue: registry.venue("coinbase"), }; CoinbaseNetting.decode(&mut ctx).await diff --git a/tools/hindsight/src/decoder/venues/metamask.rs b/tools/hindsight/src/decoder/venues/metamask.rs index c53d9ebb..eae4787f 100644 --- a/tools/hindsight/src/decoder/venues/metamask.rs +++ b/tools/hindsight/src/decoder/venues/metamask.rs @@ -73,7 +73,7 @@ mod tests { use crate::decoder::{ decode::GasScope, registry::Registry, - test_utils::{addr, make_transfer_log, receipt, swap, tx_hash}, + test_utils::{addr, frame, make_transfer_log, receipt, swap, tx_hash}, transfer_ledger::TransferLedger, }; @@ -108,6 +108,7 @@ mod tests { let provider = RootProvider::new(RpcClient::mocked(Asserter::new())); let mut code_cache = HashMap::new(); let receipt = receipt(tx_hash(1), sender, Some(entry_point), vec![]); + let root = frame("CALL", sender, entry_point, 0); let mut ctx = DecodeContext { provider: &provider, registry, @@ -116,6 +117,7 @@ mod tests { entry_point, transfer_ledger: ledger, input, + root: &root, venue: registry.venue("metamask"), }; MetaMaskNetting.decode(&mut ctx).await diff --git a/tools/hindsight/src/decoder/venues/mod.rs b/tools/hindsight/src/decoder/venues/mod.rs index 61c476e2..eda8c24e 100644 --- a/tools/hindsight/src/decoder/venues/mod.rs +++ b/tools/hindsight/src/decoder/venues/mod.rs @@ -39,7 +39,7 @@ use crate::decoder::decode::TradeDecoder; /// no decoders is rejected by the registry at load time (see `has_decoder`). pub(crate) fn decoders_for(name: &str) -> Vec>> { match name { - "relay" => vec![Box::new(relay::RelayNetting)], + "relay" => vec![Box::new(relay::RelayCalldata), Box::new(relay::RelayNetting)], "metamask" => vec![Box::new(metamask::MetaMaskNetting)], "rabby" => vec![Box::new(rabby::RabbyNetting)], "coinbase" => vec![Box::new(coinbase::CoinbaseNetting)], diff --git a/tools/hindsight/src/decoder/venues/rabby.rs b/tools/hindsight/src/decoder/venues/rabby.rs index b681207e..27a1bd39 100644 --- a/tools/hindsight/src/decoder/venues/rabby.rs +++ b/tools/hindsight/src/decoder/venues/rabby.rs @@ -69,7 +69,7 @@ mod tests { use crate::decoder::{ decode::GasScope, registry::Registry, - test_utils::{addr, make_transfer_log, receipt, swap, tx_hash}, + test_utils::{addr, frame, make_transfer_log, receipt, swap, tx_hash}, transfer_ledger::TransferLedger, }; @@ -93,6 +93,7 @@ mod tests { let provider = RootProvider::new(RpcClient::mocked(Asserter::new())); let mut code_cache = HashMap::new(); let receipt = receipt(tx_hash(1), sender, Some(entry_point), vec![]); + let root = frame("CALL", sender, entry_point, 0); let mut ctx = DecodeContext { provider: &provider, registry, @@ -101,6 +102,7 @@ mod tests { entry_point, transfer_ledger: ledger, input: &[], + root: &root, venue: registry.venue("rabby"), }; RabbyNetting.decode(&mut ctx).await diff --git a/tools/hindsight/src/decoder/venues/rainbow.rs b/tools/hindsight/src/decoder/venues/rainbow.rs index a25c76b0..0f9d55c2 100644 --- a/tools/hindsight/src/decoder/venues/rainbow.rs +++ b/tools/hindsight/src/decoder/venues/rainbow.rs @@ -70,7 +70,7 @@ mod tests { use super::*; use crate::decoder::{ registry::Registry, - test_utils::{addr, make_transfer_log, receipt, swap, tx_hash}, + test_utils::{addr, frame, make_transfer_log, receipt, swap, tx_hash}, transfer_ledger::TransferLedger, }; @@ -95,6 +95,7 @@ mod tests { let mut code_cache = HashMap::new(); let user = addr(1); let receipt = receipt(tx_hash(1), user, Some(entry_point), vec![]); + let root = frame("CALL", user, entry_point, 0); let mut ctx = DecodeContext { provider: &provider, registry: ®istry, @@ -103,6 +104,7 @@ mod tests { entry_point, transfer_ledger: ledger, input, + root: &root, venue: registry.venue("rainbow"), }; RainbowCalldata.decode(&mut ctx).await diff --git a/tools/hindsight/src/decoder/venues/relay.rs b/tools/hindsight/src/decoder/venues/relay.rs index ceeefa8c..6d966beb 100644 --- a/tools/hindsight/src/decoder/venues/relay.rs +++ b/tools/hindsight/src/decoder/venues/relay.rs @@ -3,6 +3,11 @@ //! Relay differs from direct solver swaps in two ways: its router sends a venue fee to a collector //! address on either side of the swap, and its solvers submit rebalancing fills whose transaction //! sender has no net flow. +//! +//! Two decoders, tried in order (`venues::decoders_for`): [`RelayCalldata`] reads the trader's +//! terms straight from the settling solver's own calldata, and [`RelayNetting`] nets the ledger +//! for the solvers `RelayCalldata` cannot parse (0x Settler) or transactions with no solver frame +//! at all. See `.claude/plans/calldata-first-decoding.md` for the empirics behind the ordering. use std::collections::HashSet; @@ -13,11 +18,99 @@ use alloy::{ use async_trait::async_trait; use crate::decoder::{ - decode::{DecodeContext, TradeDecoder, TraderFlow}, + decode::{DecodeContext, GasScope, TradeDecoder, TraderFlow}, netting_decoders::venue_flow, + solvers, trace, transfer_ledger::{NetSwap, TransferLedger}, }; +/// Relay's calldata-primary decoder. +/// +/// Reads `token_in`/`token_out`/`amount_in` straight from the settling solver frame's `SwapIntent` +/// (already the post-fee, on-chain-enforced terms — Relay pays its input-side fee to the +/// collector *before* forwarding into the solver call) and recovers the settled `amount_out` as +/// the gross amount of `intent.token_out` received by the output recipient the same calldata +/// declares — the one field calldata can never carry. Declines (falling through to +/// [`RelayNetting`]) when no solver frame or intent is found, the recipient never received the +/// token, or either guard below fails. +/// +/// Two guards protect against the recipient-receipt query mis-attributing a multi-order +/// transaction's output (see the design doc's risk section — not observed in the sampled traffic, +/// but cheap to check): the recovered output must clear the intent's on-chain floor (a successful +/// trade cleared it by construction, so a violation means the wrong legs were picked up), and, +/// when the calldata also declares a quote, it must sit within `plausible_quote`'s band of the +/// recovered output. +pub(crate) struct RelayCalldata; + +#[async_trait] +impl TradeDecoder

for RelayCalldata { + fn name(&self) -> &'static str { + "relay-calldata" + } + + async fn decode(&self, ctx: &mut DecodeContext<'_, P>) -> Option { + let addresses = ctx.venue?; + let solver_frame = trace::find_solver_frame(ctx.root, ctx.registry)?; + let solver = ctx.registry.label(solver_frame.to?); + let intent = solvers::swap_intent(&solver, &solver_frame.input, None)?; + let recipient = solvers::output_recipient(&solver, &solver_frame.input)?; + + let amount_out = ctx + .transfer_ledger + .received_by_address(recipient, intent.token_out); + if amount_out.is_zero() || amount_out < intent.min_amount_out { + return None; + } + if let Some(quoted) = intent.declared_quote() { + if !solvers::plausible_quote(quoted, amount_out) { + return None; + } + } + + // Both fees are already on the right basis (§1 of the design doc): the intent's + // `amount_in` is post-input-fee and the recipient's receipt is pre-output-fee, so neither + // amount above needs adjusting — the fee is recorded for transparency only. + let fees = ctx + .transfer_ledger + .received_by(&addresses.fee_collectors); + let venue_fee_in = fees + .get(&intent.token_in) + .copied() + .filter(|fee| !fee.is_zero()); + let venue_fee_out = fees + .get(&intent.token_out) + .copied() + .filter(|fee| !fee.is_zero()); + + // A trader-sent Relay transaction charges the solver frame's gas (see `GasScope`); a + // solver-initiated rebalance charges nothing. Ledger-derived rather than assumed: the + // sender net-sending the input token is what "trader-funded" means here. + let sender = ctx.receipt.from; + let net_sent = ctx + .transfer_ledger + .group_net_sent(&HashSet::from([sender])); + let gas_scope = if net_sent.contains_key(&intent.token_in) { + GasScope::SolverFrame + } else { + GasScope::NotCharged + }; + + Some(TraderFlow { + tracked: sender, + swap: NetSwap { + token_in: intent.token_in, + amount_in: intent.amount_in, + token_out: intent.token_out, + amount_out, + }, + venue_fee_in, + venue_fee_out, + solver_override: None, + gas_scope, + }) + } +} + /// Relay's netting decoder. pub(crate) struct RelayNetting; @@ -133,7 +226,7 @@ mod tests { use crate::decoder::{ decode::GasScope, registry::Registry, - test_utils::{addr, make_transfer_log, receipt, swap, tx_hash}, + test_utils::{addr, frame, make_transfer_log, receipt, swap, tx_hash}, }; fn transfer_ledger(logs: &[Log], native: &[(Address, Address, U256)]) -> TransferLedger { @@ -160,6 +253,7 @@ mod tests { let provider = RootProvider::new(RpcClient::mocked(Asserter::new())); let mut code_cache = HashMap::new(); let receipt = receipt(tx_hash(1), sender, Some(entry_point), vec![]); + let root = frame("CALL", sender, entry_point, 0); let mut ctx = DecodeContext { provider: &provider, registry, @@ -168,6 +262,7 @@ mod tests { entry_point, transfer_ledger: ledger, input: &[], + root: &root, venue: registry.venue("relay"), }; RelayNetting.decode(&mut ctx).await @@ -353,4 +448,199 @@ mod tests { assert_eq!(flow.venue_fee_out, None); assert_eq!(flow.gas_scope, GasScope::NotCharged); } + + mod relay_calldata { + use alloy::{primitives::address, rpc::types::trace::geth::CallFrame}; + + use super::*; + + /// Fly's own router — same address on every chain (`docs.fly.trade`). + const FLY: Address = address!("0x20f6ee51340adeed01a59b0e65cb3703f3dc860c"); + /// 0x's `AllowanceHolder` — a registered solver with no `swap_intent` support. + const ZEROX: Address = address!("0xdef1c0ded9bec7f1a1670819833240f027b25eff"); + /// Relay's own router — in the live fixture this is both the entry point and the + /// declared output recipient Fly's calldata carries (Relay receives and forwards). + const ROUTER: Address = address!("0xb92fe925dc43a0ecde6c8b1a2709c170ec4fff4f"); + + /// The real Fly calldata used by `solvers::fly`'s fixture tests: USDT in, native out, + /// `amount_in` 19,694,643, `min_amount_out` 10,217,898,321,149,381, declared quote + /// 10,321,109,415,302,405. + fn fly_input() -> Vec { + let text = include_str!("../solvers/fixtures/fly_input.txt").trim(); + alloy::hex::decode(text.strip_prefix("0x").unwrap_or(text)).unwrap() + } + + const TOKEN_IN: Address = address!("0xfde4c96c8593536e31f229ea8f37b2ada2699bb2"); + const AMOUNT_IN: u64 = 19_694_643; + const MIN_AMOUNT_OUT: u128 = 10_217_898_321_149_381; + const QUOTED_AMOUNT_OUT: u128 = 10_321_109_415_302_405; + + /// A root frame: `sender -> router -> solver`, the solver frame carrying `input`. + fn root_with_solver_frame(sender: Address, router: Address, solver: Address) -> CallFrame { + let mut solver_call = frame("CALL", router, solver, 0); + solver_call.input = fly_input().into(); + let mut root = frame("CALL", sender, router, 0); + root.calls = vec![solver_call]; + root + } + + async fn decode_calldata( + registry: &Registry, + root: &CallFrame, + ledger: &TransferLedger, + sender: Address, + router: Address, + ) -> Option { + let provider = RootProvider::new(RpcClient::mocked(Asserter::new())); + let mut code_cache = HashMap::new(); + let receipt = receipt(tx_hash(1), sender, Some(router), vec![]); + let mut ctx = DecodeContext { + provider: &provider, + registry, + code_cache: &mut code_cache, + receipt: &receipt, + entry_point: router, + transfer_ledger: ledger, + input: &[], + root, + venue: registry.venue("relay"), + }; + RelayCalldata.decode(&mut ctx).await + } + + #[tokio::test] + async fn test_decode_recovers_output_from_recipient_receipt() { + // The router — the declared recipient — receives native ETH above the floor; the + // sender pays the input token directly (sender-funded). + let registry = Registry::ethereum(); + let sender = addr(1); + let root = root_with_solver_frame(sender, ROUTER, FLY); + let logs = vec![make_transfer_log(TOKEN_IN, sender, ROUTER, U256::from(AMOUNT_IN))]; + let native = vec![(addr(50), ROUTER, U256::from(MIN_AMOUNT_OUT + 1_000))]; + let ledger = TransferLedger::from_transaction(&logs, &native); + + let flow = decode_calldata(®istry, &root, &ledger, sender, ROUTER) + .await + .unwrap(); + assert_eq!(flow.tracked, sender); + assert_eq!(flow.swap.token_in, TOKEN_IN); + assert_eq!(flow.swap.token_out, Address::ZERO); + assert_eq!(flow.swap.amount_in, U256::from(AMOUNT_IN)); + assert_eq!(flow.swap.amount_out, U256::from(MIN_AMOUNT_OUT + 1_000)); + assert_eq!(flow.gas_scope, GasScope::SolverFrame); + } + + #[tokio::test] + async fn test_decode_below_floor_declines() { + // The recipient's receipt sits under the intent's on-chain floor: a successful trade + // clears its floor by construction, so this means the query mis-attributed. + let registry = Registry::ethereum(); + let sender = addr(1); + let root = root_with_solver_frame(sender, ROUTER, FLY); + let native = vec![(addr(50), ROUTER, U256::from(MIN_AMOUNT_OUT - 1))]; + let ledger = TransferLedger::from_transaction(&[], &native); + + assert!(decode_calldata(®istry, &root, &ledger, sender, ROUTER) + .await + .is_none()); + } + + #[tokio::test] + async fn test_decode_no_recipient_receipt_declines() { + let registry = Registry::ethereum(); + let sender = addr(1); + let root = root_with_solver_frame(sender, ROUTER, FLY); + let ledger = TransferLedger::from_transaction(&[], &[]); + + assert!(decode_calldata(®istry, &root, &ledger, sender, ROUTER) + .await + .is_none()); + } + + #[tokio::test] + async fn test_decode_no_solver_frame_declines() { + let registry = Registry::ethereum(); + let sender = addr(1); + let root = frame("CALL", sender, ROUTER, 0); + let native = vec![(addr(50), ROUTER, U256::from(MIN_AMOUNT_OUT + 1_000))]; + let ledger = TransferLedger::from_transaction(&[], &native); + + assert!(decode_calldata(®istry, &root, &ledger, sender, ROUTER) + .await + .is_none()); + } + + #[tokio::test] + async fn test_decode_solver_without_intent_support_declines() { + // 0x is a registered solver (matches `find_solver_frame`) but has no `swap_intent` + // implementation: the calldata path has nothing to recover, so it falls through. + let registry = Registry::ethereum(); + let sender = addr(1); + let root = root_with_solver_frame(sender, ROUTER, ZEROX); + let native = vec![(addr(50), ROUTER, U256::from(MIN_AMOUNT_OUT + 1_000))]; + let ledger = TransferLedger::from_transaction(&[], &native); + + assert!(decode_calldata(®istry, &root, &ledger, sender, ROUTER) + .await + .is_none()); + } + + #[tokio::test] + async fn test_decode_implausible_quote_declines() { + // A recovered output more than 2x the declared quote: `plausible_quote`'s band would + // reject it as a unit mismatch or a mis-attributed receipt, even though it clears the + // floor comfortably. + let registry = Registry::ethereum(); + let sender = addr(1); + let root = root_with_solver_frame(sender, ROUTER, FLY); + let implausible = U256::from(QUOTED_AMOUNT_OUT) * U256::from(3u64); + let native = vec![(addr(50), ROUTER, implausible)]; + let ledger = TransferLedger::from_transaction(&[], &native); + + assert!(decode_calldata(®istry, &root, &ledger, sender, ROUTER) + .await + .is_none()); + } + + #[tokio::test] + async fn test_decode_collector_funded_is_not_charged_gas() { + // The fee collector, not the sender, net-sends the input token: a solver-initiated + // rebalance, which charges no gas to any trader. + let registry = Registry::ethereum(); + let sender = addr(1); + let collector = relay_collector(®istry); + let root = root_with_solver_frame(sender, ROUTER, FLY); + let logs = vec![make_transfer_log(TOKEN_IN, collector, ROUTER, U256::from(AMOUNT_IN))]; + let native = vec![(addr(50), ROUTER, U256::from(MIN_AMOUNT_OUT + 1_000))]; + let ledger = TransferLedger::from_transaction(&logs, &native); + + let flow = decode_calldata(®istry, &root, &ledger, sender, ROUTER) + .await + .unwrap(); + assert_eq!(flow.gas_scope, GasScope::NotCharged); + } + + #[tokio::test] + async fn test_decode_records_venue_fee_without_adjusting_amounts() { + // An input-side fee leg to the real Relay collector: recorded for transparency, but + // `amount_in` stays the intent's raw figure — it is already post-fee (§1 of the design + // doc), unlike netting's fee back-out. + let registry = Registry::ethereum(); + let sender = addr(1); + let collector = relay_collector(®istry); + let root = root_with_solver_frame(sender, ROUTER, FLY); + let logs = vec![ + make_transfer_log(TOKEN_IN, sender, ROUTER, U256::from(AMOUNT_IN)), + make_transfer_log(TOKEN_IN, ROUTER, collector, U256::from(40)), + ]; + let native = vec![(addr(50), ROUTER, U256::from(MIN_AMOUNT_OUT + 1_000))]; + let ledger = TransferLedger::from_transaction(&logs, &native); + + let flow = decode_calldata(®istry, &root, &ledger, sender, ROUTER) + .await + .unwrap(); + assert_eq!(flow.swap.amount_in, U256::from(AMOUNT_IN)); + assert_eq!(flow.venue_fee_in, Some(U256::from(40))); + } + } } diff --git a/tools/hindsight/src/main.rs b/tools/hindsight/src/main.rs index 45db9be8..e73d4ba1 100644 --- a/tools/hindsight/src/main.rs +++ b/tools/hindsight/src/main.rs @@ -16,7 +16,7 @@ use tracing_subscriber::EnvFilter; use tycho_simulation::tycho_common::models::Chain; use crate::{ - decoder::{DecodedTrade, Decoder, Registry}, + decoder::{DecodedTrade, Decoder, Registry, TradeStatus}, report::ReportArgs, resolve::monitor::MonitorArgs, verify::allium::AlliumClient, @@ -182,11 +182,11 @@ async fn run_decode(args: DecodeArgs) -> anyhow::Result<()> { for block_number in &blocks { info!(block = block_number, "decoding solver trades"); let start = Instant::now(); - let trades = match decoder + let decoded = match decoder .decode_block(*block_number) .await { - Ok(trades) => trades, + Ok(decoded) => decoded, Err(error) => { warn!(block = block_number, %error, "failed to decode block; skipping"); continue; @@ -194,12 +194,22 @@ async fn run_decode(args: DecodeArgs) -> anyhow::Result<()> { }; let elapsed_ms = start.elapsed().as_millis(); - if trades.is_empty() { + if decoded.is_empty() { info!(block = block_number, elapsed_ms, "no solver trades found"); } else { - info!(block = block_number, count = trades.len(), elapsed_ms, "decoded trades"); + let reverted = decoded + .iter() + .filter(|trade| trade.status != TradeStatus::Settled) + .count(); + info!( + block = block_number, + trades = decoded.len(), + reverted, + elapsed_ms, + "decoded trades" + ); } - all_trades.extend(trades); + all_trades.extend(decoded); } if args.json { @@ -256,6 +266,9 @@ pub(crate) async fn resolve_blocks( } } +/// Print every decoded trade — settled or reverted, told apart by `status`. A settled trade's +/// terms are always known (see `DecodedTrade`'s settled/reverted invariant); a reverted one may +/// have none, when its solver calldata did not parse. #[expect(clippy::print_stdout)] fn print_trades(trades: &[DecodedTrade]) { if trades.is_empty() { @@ -271,10 +284,14 @@ fn print_trades(trades: &[DecodedTrade]) { println!(" venue: {}", trade.venue); println!(" solver: {}", trade.solver); println!(" sender: {}", trade.sender); - println!(" token_in: {}", trade.token_in); - println!(" amount_in: {}", trade.amount_in); - println!(" token_out: {}", trade.token_out); - println!(" amount_out: {}", trade.amount_out); + match &trade.status { + TradeStatus::Settled => println!(" status: settled"), + TradeStatus::Reverted { cause } => println!(" status: reverted ({cause:?})"), + } + print_option("token_in:", trade.token_in); + print_option("amount_in:", trade.amount_in); + print_option("token_out:", trade.token_out); + print_option("amount_out:", trade.amount_out); if let Some(sandwich) = &trade.sandwich { println!( " sandwich: front={} back={} attacker={}", @@ -285,6 +302,16 @@ fn print_trades(trades: &[DecodedTrade]) { } } +/// Print a labeled field that may be unknown — `unknown` (not blank) so a reverted trade whose +/// terms did not parse still reads as "we checked and found nothing" rather than an empty value. +#[expect(clippy::print_stdout)] +fn print_option(label: &str, value: Option) { + match value { + Some(value) => println!(" {label:<12}{value}"), + None => println!(" {label:<12}unknown"), + } +} + const MAX_RANGE_BLOCKS: u64 = 1000; fn parse_range(range: &str) -> anyhow::Result> { diff --git a/tools/hindsight/src/report/aggregate.rs b/tools/hindsight/src/report/aggregate.rs index 793c0ba1..46aa0526 100644 --- a/tools/hindsight/src/report/aggregate.rs +++ b/tools/hindsight/src/report/aggregate.rs @@ -5,7 +5,7 @@ use std::collections::HashMap; -use crate::report::record::Comparison; +use crate::report::record::{Comparison, State}; /// Number of trades listed in the biggest-wins and biggest-losses tables. const TOP_TRADES: usize = 10; @@ -75,14 +75,27 @@ pub(crate) struct GroupStats { /// One row in the biggest-wins or biggest-losses table. pub(crate) struct TradeRow { - pub settled_tx: String, + pub tx_hash: String, pub venue: String, pub solver: String, pub net_bps: Option, pub improvement_usd: f64, } -/// Aggregate every view from the parsed records. +/// A settled record's `top` state. `report::run` filters records to `status == "settled"` before +/// this module ever sees them, and a settled trade always carries a `top` state by construction +/// (see `DecodedTrade`'s settled/reverted invariant) — a missing one here means that filter was +/// bypassed. +fn top(record: &Comparison) -> &State { + record + .top + .as_ref() + .expect("settled comparison record must carry a top state") +} + +/// Aggregate every view from the parsed records. Callers must pre-filter to `status == "settled"` +/// (see `report::run`) — every aggregation here is judged on `top`, which only a settled record is +/// guaranteed to carry. pub(crate) fn build(records: &[Comparison]) -> Report { Report { summary: summary(records), @@ -110,10 +123,9 @@ fn verdict_stats(records: &[Comparison]) -> Vec { let mut counts: HashMap<&str, usize> = HashMap::new(); let mut notional: HashMap<&str, f64> = HashMap::new(); for record in records { - let verdict = record.top.verdict.as_str(); + let verdict = top(record).verdict.as_str(); *counts.entry(verdict).or_default() += 1; - *notional.entry(verdict).or_default() += record - .top + *notional.entry(verdict).or_default() += top(record) .settled_value_usd .unwrap_or(0.0); } @@ -136,26 +148,26 @@ fn verdict_stats(records: &[Comparison]) -> Vec { fn savings(records: &[Comparison]) -> Savings { let scored: Vec<&Comparison> = records .iter() - .filter(|r| r.top.is_scored()) + .filter(|r| top(r).is_scored()) .collect(); // The savings-bps headline is over wins only — how much better Fynd was when it won, not // diluted by the losses. let mut win_bps: Vec = scored .iter() - .filter(|r| r.top.verdict == "win") - .filter_map(|r| r.top.net_bps) + .filter(|r| top(r).verdict == "win") + .filter_map(|r| top(r).net_bps) .collect(); Savings { scored: scored.len(), wins: scored .iter() - .filter(|r| r.top.verdict == "win") + .filter(|r| top(r).verdict == "win") .count(), median_win_bps: median(&mut win_bps), won_usd: scored .iter() - .filter(|r| r.top.verdict == "win") - .filter_map(|r| r.top.improvement_usd) + .filter(|r| top(r).verdict == "win") + .filter_map(|r| top(r).improvement_usd) .sum(), } } @@ -173,29 +185,29 @@ fn group_stats(records: &[Comparison], key: impl Fn(&Comparison) -> &String) -> .map(|(name, group)| { let mut net_bps: Vec = group .iter() - .filter(|r| r.top.is_scored()) - .filter_map(|r| r.top.net_bps) + .filter(|r| top(r).is_scored()) + .filter_map(|r| top(r).net_bps) .collect(); GroupStats { name: name.clone(), count: group.len(), wins: group .iter() - .filter(|r| r.top.verdict == "win") + .filter(|r| top(r).verdict == "win") .count(), losses: group .iter() - .filter(|r| r.top.verdict == "loss") + .filter(|r| top(r).verdict == "loss") .count(), unsolved: group .iter() - .filter(|r| !r.top.is_served()) + .filter(|r| !top(r).is_served()) .count(), median_net_bps: median(&mut net_bps), total_improvement_usd: group .iter() - .filter(|r| r.top.is_scored()) - .filter_map(|r| r.top.improvement_usd) + .filter(|r| top(r).is_scored()) + .filter_map(|r| top(r).improvement_usd) .sum(), } }) @@ -231,15 +243,15 @@ fn top_losses(records: &[Comparison]) -> Vec { fn trade_rows(records: &[Comparison], verdict: &str) -> Vec { records .iter() - .filter(|r| r.top.verdict == verdict) + .filter(|r| top(r).verdict == verdict) .filter_map(|r| { - r.top + top(r) .improvement_usd .map(|usd| TradeRow { - settled_tx: r.settled_tx.clone(), + tx_hash: r.tx_hash.clone(), venue: r.venue.clone(), solver: r.solver.clone(), - net_bps: r.top.net_bps, + net_bps: top(r).net_bps, improvement_usd: usd, }) }) @@ -250,14 +262,14 @@ fn unsolvable_tokens(records: &[Comparison]) -> Vec { let mut counts: HashMap<&str, usize> = HashMap::new(); for record in records .iter() - .filter(|r| !r.top.is_served()) + .filter(|r| !top(r).is_served()) { - *counts - .entry(record.token_in.as_str()) - .or_default() += 1; - *counts - .entry(record.token_out.as_str()) - .or_default() += 1; + if let Some(token_in) = record.token_in.as_deref() { + *counts.entry(token_in).or_default() += 1; + } + if let Some(token_out) = record.token_out.as_deref() { + *counts.entry(token_out).or_default() += 1; + } } let mut ranked: Vec = counts .into_iter() @@ -299,11 +311,12 @@ mod tests { ) -> Comparison { serde_json::from_value(serde_json::json!({ "block": block, - "settled_tx": format!("0x{block:064x}"), + "tx_hash": format!("0x{block:064x}"), "venue": venue, "solver": solver, "token_in": "0xaaa", "token_out": "0xbbb", + "status": "settled", "top": { "verdict": verdict, "net_bps": bps, diff --git a/tools/hindsight/src/report/html.rs b/tools/hindsight/src/report/html.rs index ec69abc8..1f5717bb 100644 --- a/tools/hindsight/src/report/html.rs +++ b/tools/hindsight/src/report/html.rs @@ -241,7 +241,7 @@ fn trades_section(title: &str, trades: &[TradeRow]) -> String { table, "{}{}{}\ {}{}", - escape(&short_hash(&trade.settled_tx)), + escape(&short_hash(&trade.tx_hash)), escape(&trade.venue), escape(&trade.solver), fmt_bps(trade.net_bps), @@ -421,13 +421,15 @@ mod tests { fn sample_report() -> Report { let records: Vec = vec![ serde_json::json!({ - "block": 1, "settled_tx": "0xabc0000000000000000000000000000000000000000000000000000000000001", + "block": 1, "tx_hash": "0xabc0000000000000000000000000000000000000000000000000000000000001", "venue": "relay", "solver": "1inch", "token_in": "0xaaa", "token_out": "0xbbb", + "status": "settled", "top": {"verdict": "win", "net_bps": 20.0, "improvement_usd": 12.0, "settled_value_usd": 1000.0} }), serde_json::json!({ - "block": 2, "settled_tx": "0xdef0000000000000000000000000000000000000000000000000000000000002", + "block": 2, "tx_hash": "0xdef0000000000000000000000000000000000000000000000000000000000002", "venue": "relay", "solver": "0x", "token_in": "0xccc", "token_out": "0xddd", + "status": "settled", "top": {"verdict": "unsolvable", "net_bps": null, "improvement_usd": null, "settled_value_usd": 50.0} }), ] diff --git a/tools/hindsight/src/report/mod.rs b/tools/hindsight/src/report/mod.rs index 174d8024..996dd5e5 100644 --- a/tools/hindsight/src/report/mod.rs +++ b/tools/hindsight/src/report/mod.rs @@ -41,7 +41,13 @@ pub(crate) fn run(args: ReportArgs) -> anyhow::Result<()> { if all.is_empty() { bail!("no comparison records found in {}", args.comparisons_dir.display()); } - let records = filter_by_venue(all, &args.venue)?; + // Savings and win-rate only mean something for a trade that actually delivered an output — + // filter reverted trades out explicitly rather than relying on their absent `top` state. + let settled: Vec = all + .into_iter() + .filter(|record| record.status == "settled") + .collect(); + let records = filter_by_venue(settled, &args.venue)?; let report = aggregate::build(&records); let filter = (!args.venue.is_empty()).then(|| args.venue.join(", ")); let html = html::render(&report, filter.as_deref()); @@ -85,7 +91,8 @@ fn filter_by_venue(records: Vec, venues: &[String]) -> anyhow::Resul Ok(filtered) } -/// Read and parse every `.jsonl` file in `dir` into comparison records. Malformed lines are +/// Read and parse every `comparisons-*.jsonl` file in `dir` into comparison records — settled and +/// reverted trades alike; `run` filters to settled before aggregating. Malformed lines are /// counted and skipped rather than failing the whole report — a truncated final line from an /// interrupted run should not lose the rest of the data. fn read_comparisons(dir: &Path) -> anyhow::Result> { @@ -95,12 +102,15 @@ fn read_comparisons(dir: &Path) -> anyhow::Result> { .map(|entry| entry.path()) .filter(|path| { path.extension() - .is_some_and(|ext| ext == "jsonl") + .is_some_and(|ext| ext == "jsonl") && + path.file_name() + .and_then(|name| name.to_str()) + .is_some_and(|name| name.starts_with("comparisons-")) }) .collect(); files.sort(); if files.is_empty() { - bail!("no .jsonl files in {}", dir.display()); + bail!("no comparisons-*.jsonl files in {}", dir.display()); } let mut records = Vec::new(); @@ -142,8 +152,9 @@ mod tests { fn line(block: u64, verdict: &str) -> String { serde_json::json!({ - "block": block, "settled_tx": format!("0x{block:064x}"), + "block": block, "tx_hash": format!("0x{block:064x}"), "venue": "relay", "solver": "1inch", "token_in": "0xaaa", "token_out": "0xbbb", + "status": "settled", "top": {"verdict": verdict, "net_bps": 1.0, "improvement_usd": 1.0, "settled_value_usd": 1.0}, }) .to_string() @@ -171,6 +182,22 @@ mod tests { fs::remove_dir_all(&dir).unwrap(); } + #[test] + fn test_ignores_non_comparisons_jsonl_files() { + // Only `comparisons-*.jsonl` files are read; anything else in the directory (an old + // stream from a prior version, a stray file) is left alone rather than fed to the parser. + let dir = write_dir( + "with-other-files", + &[ + ("comparisons-2026-07-20.jsonl", &format!("{}\n", line(1, "win"))), + ("other-2026-07-20.jsonl", "{\"block\":1}\n"), + ], + ); + let records = read_comparisons(&dir).unwrap(); + assert_eq!(records.len(), 1); + fs::remove_dir_all(&dir).unwrap(); + } + #[test] fn test_empty_dir_is_an_error() { let dir = write_dir("empty", &[]); @@ -180,8 +207,8 @@ mod tests { fn venue_record(venue: &str) -> Comparison { serde_json::from_value(serde_json::json!({ - "block": 1, "settled_tx": "0x1", "venue": venue, "solver": "1inch", - "token_in": "0xaaa", "token_out": "0xbbb", + "block": 1, "tx_hash": "0x1", "venue": venue, "solver": "1inch", + "token_in": "0xaaa", "token_out": "0xbbb", "status": "settled", "top": {"verdict": "win", "net_bps": 1.0, "improvement_usd": 1.0, "settled_value_usd": 1.0}, })) .unwrap() diff --git a/tools/hindsight/src/report/record.rs b/tools/hindsight/src/report/record.rs index 04ea84fe..4440b613 100644 --- a/tools/hindsight/src/report/record.rs +++ b/tools/hindsight/src/report/record.rs @@ -9,16 +9,25 @@ use serde::Deserialize; /// One re-solved trade: the settled trade's identity plus Fynd's result at each block state. +/// Carries both settled and reverted trades (told apart by `status`) — the report filters to +/// `status == "settled"` before aggregating (see `report::run`), since Allium-style savings and +/// win-rate views only make sense for a trade that actually delivered an output. #[derive(Debug, Clone, Deserialize)] pub(crate) struct Comparison { pub block: u64, - pub settled_tx: String, + pub tx_hash: String, pub venue: String, pub solver: String, - pub token_in: String, - pub token_out: String, + /// `None` only for a reverted trade with no known terms — always present once filtered to + /// `status == "settled"`. + pub token_in: Option, + pub token_out: Option, + pub status: String, /// Optimistic state (N-1); the report's headline, matching the monitor's headline verdict. - pub top: State, + /// `None` only for a reverted trade with no known terms — a settled trade always carries one + /// (see `DecodedTrade`'s settled/reverted invariant), so every record this module aggregates + /// (already filtered to `status == "settled"`) has one. + pub top: Option, } /// Fynd's result at one block state. @@ -81,19 +90,22 @@ mod tests { tx_hash: TxHash::repeat_byte(0x42), block_number: 25_000_000, tx_index: 0, + status: crate::decoder::TradeStatus::Settled, venue: "relay".into(), solver: "1inch".into(), solver_source: AttributionSource::TraceMatch, decoder: "sender-netting", sender: Address::ZERO, - token_in: weth, - token_out: usdc, - amount_in: U256::from(1_000u64), - amount_out: U256::from(1_000_000_000u64), // settled 1000 USDC + token_in: Some(weth), + token_out: Some(usdc), + amount_in: Some(U256::from(1_000u64)), + amount_out: Some(U256::from(1_000_000_000u64)), // settled 1000 USDC venue_fee_in: None, venue_fee_out: None, settled_gas: None, - quote: None, + min_amount_out: None, + declared_quote: None, + quote_timestamp: None, sandwich: None, }; // Top solved above settled → a win with a positive USD improvement. @@ -108,9 +120,7 @@ mod tests { let range = build_range( &trade, &prices, - top, - Outcome::Unsolvable("x".into()), - &Outcome::Unsolvable("x".into()), + Some((top, Outcome::Unsolvable("x".into()), Outcome::Unsolvable("x".into()))), ); let mut buf = Vec::new(); @@ -121,10 +131,12 @@ mod tests { assert_eq!(record.block, 25_000_000); assert_eq!(record.venue, "relay"); assert_eq!(record.solver, "1inch"); - assert_eq!(record.top.verdict, "win"); - assert!(record.top.is_scored()); - assert!(record.top.net_bps.unwrap() > 0.0); - assert!((record.top.improvement_usd.unwrap() - 10.0).abs() < 1e-3); - assert_eq!(record.token_out, format!("{usdc:#x}")); + assert_eq!(record.status, "settled"); + let top = record.top.unwrap(); + assert_eq!(top.verdict, "win"); + assert!(top.is_scored()); + assert!(top.net_bps.unwrap() > 0.0); + assert!((top.improvement_usd.unwrap() - 10.0).abs() < 1e-3); + assert_eq!(record.token_out, Some(format!("{usdc:#x}"))); } } diff --git a/tools/hindsight/src/resolve/compare.rs b/tools/hindsight/src/resolve/compare.rs index 98deccae..e098bb04 100644 --- a/tools/hindsight/src/resolve/compare.rs +++ b/tools/hindsight/src/resolve/compare.rs @@ -14,7 +14,9 @@ fn to_biguint(amount: U256) -> BigUint { } /// Basis-point deltas of a Fynd quote against the settled amount (positive = Fynd better). -#[derive(Debug, Clone, Copy, PartialEq, Serialize)] +/// `Deltas::default()` (both `None`) when there is nothing settled to compare against — an +/// unsolved outcome, or a reverted trade. +#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize)] pub(crate) struct Deltas { /// `fynd amount_out` vs the settled amount, both gross of gas — always like-for-like, and /// the basis of the headline `Verdict`. @@ -26,10 +28,6 @@ pub(crate) struct Deltas { pub net_bps: Option, } -impl Deltas { - const NONE: Self = Self { raw_bps: None, net_bps: None }; -} - /// Slippage of the top-of-block route re-executed at back-of-block: how the route's output moved /// between quote time (N-1) and execution time (N). Positive = the route produced more than /// quoted — the surplus we would keep if we charged positive slippage. @@ -115,7 +113,7 @@ pub(crate) fn compare( settled_net_gas: U256, ) -> Deltas { let Outcome::Solved(solved) = outcome else { - return Deltas::NONE; + return Deltas::default(); }; Deltas { raw_bps: raw_bps_diff(&to_biguint(solved.amount_out), &to_biguint(settled_amount_out)), @@ -141,6 +139,29 @@ pub(crate) fn verdict(outcome: &Outcome, deltas: &Deltas) -> Verdict { } } +/// Judge a solved outcome against a floor (`min_amount_out`), independent of whether anything +/// settled — the same judgment a reverted trade needs against its on-chain floor, and a settled +/// trade can use too when a floor happens to be known (it cleared the floor by construction, but +/// the margin is still informative). `(None, None)` when the state was not solved or no floor is +/// known. +/// +/// Returns `(fillable, margin_bps)`: `fillable` is whether the quoted output would have cleared +/// the floor; `margin_bps` is the signed bps of `(quote - floor) / floor`, positive meaning the +/// quote cleared it with room to spare. `margin_bps` is `None` when the floor is zero (no +/// denominator to divide by) even though `fillable` still answers in that case, since any output +/// clears a zero floor. +pub(crate) fn floor_judgment( + outcome: &Outcome, + min_amount_out: Option, +) -> (Option, Option) { + let (Outcome::Solved(solved), Some(floor)) = (outcome, min_amount_out) else { + return (None, None); + }; + let fillable = solved.amount_out >= floor; + let margin_bps = raw_bps_diff(&to_biguint(solved.amount_out), &to_biguint(floor)); + (Some(fillable), margin_bps) +} + #[cfg(test)] mod tests { use super::*; @@ -191,7 +212,7 @@ mod tests { fn test_compare_unsolvable() { let (settled, net) = gross(10_000); let d = compare(&Outcome::Unsolvable("no route".into()), settled, net); - assert_eq!(d, Deltas::NONE); + assert_eq!(d, Deltas::default()); } #[test] @@ -286,4 +307,51 @@ mod tests { fn slippage_none_for_zero_quoted_output() { assert_eq!(slippage(&solved(0, 0), &solved(10, 10)), None); } + + #[test] + fn test_floor_judgment_exactly_at_floor() { + let (fillable, margin_bps) = + floor_judgment(&solved(10_000, 9_900), Some(U256::from(10_000u64))); + assert_eq!(fillable, Some(true)); + assert_eq!(margin_bps, Some(0.0)); + } + + #[test] + fn test_floor_judgment_above_and_below_floor() { + let (fillable, margin_bps) = + floor_judgment(&solved(10_100, 10_000), Some(U256::from(10_000u64))); + assert_eq!(fillable, Some(true)); + assert!(margin_bps.unwrap() > 0.0); + + let (fillable, margin_bps) = + floor_judgment(&solved(9_900, 9_800), Some(U256::from(10_000u64))); + assert_eq!(fillable, Some(false)); + assert!(margin_bps.unwrap() < 0.0); + } + + #[test] + fn test_floor_judgment_unsolved() { + let (fillable, margin_bps) = + floor_judgment(&Outcome::Unsolvable("no route".into()), Some(U256::from(10_000u64))); + assert_eq!(fillable, None); + assert_eq!(margin_bps, None); + } + + #[test] + fn test_floor_judgment_no_floor_known() { + // A solved outcome with no floor to judge against (e.g. a settled trade whose calldata + // declared no min_amount_out) — neither field applies. + let (fillable, margin_bps) = floor_judgment(&solved(10_000, 9_900), None); + assert_eq!(fillable, None); + assert_eq!(margin_bps, None); + } + + #[test] + fn test_floor_judgment_zero_floor() { + // Any non-negative quote clears a zero floor: fillable is Some(true), but there is no + // denominator to divide by for the margin. + let (fillable, margin_bps) = floor_judgment(&solved(10_000, 9_900), Some(U256::ZERO)); + assert_eq!(fillable, Some(true)); + assert_eq!(margin_bps, None); + } } diff --git a/tools/hindsight/src/resolve/jsonl.rs b/tools/hindsight/src/resolve/jsonl.rs index b2001d74..95713473 100644 --- a/tools/hindsight/src/resolve/jsonl.rs +++ b/tools/hindsight/src/resolve/jsonl.rs @@ -16,13 +16,20 @@ use fynd_core::types::{OrderQuote, Swap, Transaction}; use tracing::{info, warn}; use crate::{ + decoder::{RevertCause, TradeStatus}, resolve::{render_route, Outcome, RangeComparison, StateResult}, + telemetry::revert_cause_label, usd::Prices, }; -/// Append-only comparisons writer that rotates to a new file at each UTC day boundary — -/// `comparisons-YYYY-MM-DD.jsonl` inside its directory — so an external sync job (e.g. an S3 -/// upload `CronJob`) ships closed daily files instead of re-shipping one ever-growing one. +/// The rotating file's name prefix — every trade, settled or reverted, writes here (see +/// `RangeComparison`'s `status` field); there is only one stream. +const PREFIX: &str = "comparisons"; + +/// Append-only writer that rotates to a new file at each UTC day boundary — `comparisons-YYYY-MM +/// -DD.jsonl` inside its directory — so an external sync job (e.g. an S3 upload `CronJob`) ships +/// closed daily files instead of re-shipping one ever-growing one. `monitor` opens one of these in +/// its `--comparisons-dir`. pub(crate) struct RotatingWriter { dir: PathBuf, date: String, @@ -30,7 +37,8 @@ pub(crate) struct RotatingWriter { } impl RotatingWriter { - /// Open today's file inside `dir` for appending, creating the directory if needed. + /// Open today's `comparisons-*.jsonl` file inside `dir` for appending, creating the directory + /// if needed. pub(crate) fn open(dir: impl Into) -> anyhow::Result { let dir = dir.into(); std::fs::create_dir_all(&dir) @@ -59,23 +67,23 @@ impl RotatingWriter { return; } if let Err(e) = self.writer.flush() { - warn!(error = %e, "failed to flush comparisons file before rotation"); + warn!(error = %e, "failed to flush jsonl file before rotation"); } match open_dated(&self.dir, &date) { Ok(writer) => { - info!(path = %dated_path(&self.dir, &date).display(), "rotated comparisons file"); + info!(path = %dated_path(&self.dir, &date).display(), "rotated jsonl file"); self.writer = writer; self.date = date; } Err(e) => { - warn!(error = %e, "failed to rotate comparisons file; keeping the previous day's"); + warn!(error = %e, "failed to rotate jsonl file; keeping the previous day's"); } } } } fn dated_path(dir: &Path, date: &str) -> PathBuf { - dir.join(format!("comparisons-{date}.jsonl")) + dir.join(format!("{PREFIX}-{date}.jsonl")) } fn open_dated(dir: &Path, date: &str) -> anyhow::Result> { @@ -139,61 +147,87 @@ pub(crate) fn write_comparisons( } } -/// Build the JSON record for one re-solved trade: block, settled tx, decoded amounts, a `top` -/// and `back` state (each with its verdict, bps, USD delta, and slim route/calldata or unsolvable -/// reason), and the top route's slippage between the two states. Top is valued at N-1 prices, -/// back (and the slippage) at N prices, matching the state each was produced at. +/// A record's flat status fields: `status` ("settled"/"reverted"), and — for a revert — a bounded +/// `cause` label (matching `telemetry::revert_cause_label`) plus its free-text `cause_detail`. +/// Kept flat (not the nested `{"kind":...}` shape `RevertCause` itself serializes to) so a jq +/// pass can filter on `status`/`cause` directly, matching the old `reverts-*.jsonl` ergonomics +/// now that both streams are one. +fn status_fields(status: &TradeStatus) -> (&'static str, Option<&'static str>, Option<&str>) { + match status { + TradeStatus::Settled => ("settled", None, None), + TradeStatus::Reverted { cause } => { + let detail = match cause { + RevertCause::Other(detail) => Some(detail.as_str()), + RevertCause::SlippageFloor | RevertCause::OutOfGas => None, + }; + ("reverted", Some(revert_cause_label(cause)), detail) + } + } +} + +/// Build the JSON record for one re-solved trade — settled or reverted, told apart by `status`: +/// block, tx, decoded amounts, a `top` and `back` state (each with its verdict, bps, fillable/ +/// margin judgment, and slim route/calldata or unsolvable reason), and the top route's slippage +/// between the two states. `top`/`back`/`slippage` are `null` when the trade's terms were +/// unknown — there was nothing to solve. Top is valued at N-1 prices, back (and the slippage) at +/// N prices, matching the state each was produced at. fn comparison_record( range: &RangeComparison, prices_top: &Prices, prices_back: &Prices, ) -> serde_json::Value { + let (status, cause, cause_detail) = status_fields(&range.status); // Signed in both directions; the positive records are the "revenue if we charged positive // slippage" view, filtered downstream. let slippage = range.slippage.map(|slippage| { - serde_json::json!({ - "bps": slippage.bps, - "usd": prices_back.savings_usd( - range.token_out, + let usd = range.token_out.and_then(|token_out| { + prices_back.savings_usd( + token_out, slippage.reexecuted_amount_out, slippage.quoted_amount_out, - ), - }) + ) + }); + serde_json::json!({ "bps": slippage.bps, "usd": usd }) }); serde_json::json!({ "block": range.block_number, "tx_index": range.tx_index, - "settled_tx": range.tx_hash, + "tx_hash": range.tx_hash, + "status": status, + "cause": cause, + "cause_detail": cause_detail, "venue": range.venue, "solver": range.solver, "solver_source": range.solver_source, "decoder": range.decoder, - "token_in": format!("{:#x}", range.token_in), - "token_out": format!("{:#x}", range.token_out), - "amount_in": range.amount_in.to_string(), - "settled_amount_out": range.settled_amount_out.to_string(), - "settled_amount_out_net_gas": range.settled_amount_out_net_gas.to_string(), + "sender": format!("{:#x}", range.sender), + "token_in": range.token_in.map(|token| format!("{token:#x}")), + "token_out": range.token_out.map(|token| format!("{token:#x}")), + "amount_in": range.amount_in.map(|amount| amount.to_string()), + "settled_amount_out": range.settled_amount_out.map(|amount| amount.to_string()), + "settled_amount_out_net_gas": range.settled_amount_out_net_gas.map(|amount| amount.to_string()), "settled_gas_cost": range.settled_gas.map(|gas| gas.to_string()), - "quoted_amount_out": range.quote.as_ref().map(|q| q.amount_out.to_string()), - "quote_source": range.quote.as_ref().and_then(|q| q.source.clone()), - "quote_timestamp": range.quote.as_ref().and_then(|q| q.timestamp), + "min_amount_out": range.min_amount_out.map(|amount| amount.to_string()), + "quoted_amount_out": range.declared_quote.map(|amount| amount.to_string()), + "quote_timestamp": range.quote_timestamp, "sandwich": range.sandwich, "slippage": slippage, - "top": state_record(&range.top, range, prices_top), - "back": state_record(&range.back, range, prices_back), + "top": range.top.as_ref().map(|top| state_record(top, range, prices_top)), + "back": range.back.as_ref().map(|back| state_record(back, range, prices_back)), }) } -/// JSON for one block-state of an improvement: verdict, bps, Fynd amounts, the USD improvement -/// (gross Fynd output minus the gross settled output, valued at `prices` — the same basis as the -/// headline verdict), the winning route's algorithm and rendered path, and the slim quote. -/// `settled_value_usd` stays gross — it is the trade's notional, not a comparison. +/// JSON for one block-state of a trade: verdict, bps, Fynd amounts, the USD improvement (gross +/// Fynd output minus the gross settled output, valued at `prices` — the same basis as the +/// headline verdict), the winning route's algorithm and rendered path, the slim quote, and the +/// fillable/margin judgment against `min_amount_out` (present whenever a floor is known, settled +/// or reverted). `settled_value_usd`/`improvement_usd` are `null` for a reverted trade — nothing +/// settled to value or improve on. fn state_record( state: &StateResult, range: &RangeComparison, prices: &Prices, ) -> serde_json::Value { - let token_out = range.token_out; let solved = match &state.outcome { Outcome::Solved(solved) => Some(solved), Outcome::Partial(_) | Outcome::Unsolvable(_) => None, @@ -204,13 +238,25 @@ fn state_record( Outcome::Unsolvable(reason) | Outcome::Partial(reason) => Some(reason.as_str()), Outcome::Solved(_) => None, }; - let improvement_usd = - solved.and_then(|s| prices.savings_usd(token_out, s.amount_out, range.settled_amount_out)); - let fynd_value_usd = solved.and_then(|s| prices.value_usd(token_out, s.amount_out)); + let token_and_settled = range + .token_out + .zip(range.settled_amount_out); + let improvement_usd = solved.and_then(|s| { + token_and_settled + .and_then(|(token_out, settled)| prices.savings_usd(token_out, s.amount_out, settled)) + }); + let fynd_value_usd = range + .token_out + .zip(solved) + .and_then(|(token_out, s)| prices.value_usd(token_out, s.amount_out)); + let settled_value_usd = + token_and_settled.and_then(|(token_out, settled)| prices.value_usd(token_out, settled)); serde_json::json!({ "verdict": state.verdict, "net_bps": state.deltas.net_bps, "raw_bps": state.deltas.raw_bps, + "fillable": state.fillable, + "margin_bps": state.margin_bps, "fynd_amount_out": solved.map(|s| s.amount_out.to_string()), "fynd_amount_out_net_gas": solved.map(|s| s.amount_out_net_gas.to_string()), "gas_estimate": solved.map(|s| s.gas_estimate.to_string()), @@ -220,7 +266,7 @@ fn state_record( "route": solved.map(|s| s.solved_route.as_deref().map(render_route).unwrap_or_default()), "improvement_usd": improvement_usd, "fynd_value_usd": fynd_value_usd, - "settled_value_usd": prices.value_usd(token_out, range.settled_amount_out), + "settled_value_usd": settled_value_usd, "unsolvable_reason": unsolvable_reason, "quote": solved .and_then(|s| s.quote_json.as_deref()) @@ -281,7 +327,7 @@ mod tests { use super::*; use crate::{ - decoder::{AttributionSource, DecodedTrade, Registry, SandwichEvidence, SolverQuote}, + decoder::{AttributionSource, DecodedTrade, Registry, SandwichEvidence}, resolve::{build_range, test_support, SolvedAmount}, }; @@ -326,45 +372,49 @@ mod tests { } #[test] - fn test_comparison_record_with_solver_quote() { + fn test_comparison_record_with_declared_quote() { let trade = DecodedTrade { tx_hash: TxHash::default(), block_number: 25_480_207, tx_index: 3, + status: TradeStatus::Settled, venue: "relay".into(), solver: "kyberswap".into(), solver_source: AttributionSource::TraceMatch, decoder: "sender-netting", - sender: Address::ZERO, - token_in: Address::ZERO, - token_out: Address::repeat_byte(0x22), - amount_in: U256::from(1_000u64), - amount_out: U256::from(69_996_280_564u64), + sender: Address::repeat_byte(0x77), + token_in: Some(Address::ZERO), + token_out: Some(Address::repeat_byte(0x22)), + amount_in: Some(U256::from(1_000u64)), + amount_out: Some(U256::from(69_996_280_564u64)), venue_fee_in: None, venue_fee_out: None, settled_gas: None, - quote: Some(SolverQuote { - amount_out: U256::from(70_400_409_935u64), - source: Some("relay".to_string()), - timestamp: Some(1_783_421_726), - }), + min_amount_out: Some(U256::from(69_996_280_564u64)), + declared_quote: Some(U256::from(70_400_409_935u64)), + quote_timestamp: Some(1_783_421_726), sandwich: None, }; let range = build_range( &trade, &empty_prices(), - Outcome::Unsolvable("x".into()), - Outcome::Unsolvable("x".into()), - &Outcome::Unsolvable("x".into()), + Some(( + Outcome::Unsolvable("x".into()), + Outcome::Unsolvable("x".into()), + Outcome::Unsolvable("x".into()), + )), ); let rec = comparison_record(&range, &empty_prices(), &empty_prices()); assert_eq!(rec.pointer("/tx_index").unwrap(), 3); + assert_eq!(rec.pointer("/sender").unwrap(), &format!("{:#x}", Address::repeat_byte(0x77))); + assert_eq!(rec.pointer("/status").unwrap(), "settled"); assert_eq!( rec.pointer("/quoted_amount_out") .unwrap(), "70400409935" ); - assert_eq!(rec.pointer("/quote_source").unwrap(), "relay"); + assert_eq!(rec.pointer("/min_amount_out").unwrap(), "69996280564"); + assert!(rec.pointer("/quote_source").is_none()); assert_eq!( rec.pointer("/quote_timestamp") .unwrap() @@ -410,19 +460,22 @@ mod tests { tx_hash: TxHash::default(), block_number: 25_000_000, tx_index: 0, + status: TradeStatus::Settled, venue: "relay".into(), solver: "1inch".into(), solver_source: AttributionSource::TraceMatch, decoder: "sender-netting", sender: Address::ZERO, - token_in: weth, - token_out: usdc, - amount_in: U256::from(1_000u64), - amount_out: U256::from(1_000_000_000u64), // settled 1000 USDC + token_in: Some(weth), + token_out: Some(usdc), + amount_in: Some(U256::from(1_000u64)), + amount_out: Some(U256::from(1_000_000_000u64)), // settled 1000 USDC venue_fee_in: None, venue_fee_out: None, settled_gas: None, - quote: None, + min_amount_out: None, + declared_quote: None, + quote_timestamp: None, sandwich: None, }; // quote_json is already the slim projection (what order_quote_to_outcome stores). @@ -455,7 +508,7 @@ mod tests { quote_json: quote, solved_route: Some(solved_route), }); - let range = build_range(&trade, &prices, top, back.clone(), &back); + let range = build_range(&trade, &prices, Some((top, back.clone(), back))); comparison_record(&range, &prices, &prices) } @@ -532,28 +585,33 @@ mod tests { tx_hash: TxHash::default(), block_number: 25_000_000, tx_index: 0, + status: TradeStatus::Settled, venue: "relay".into(), solver: "1inch".into(), solver_source: AttributionSource::TraceMatch, decoder: "sender-netting", sender: Address::ZERO, - token_in: Address::repeat_byte(0x11), - token_out: Address::repeat_byte(0x22), - amount_in: U256::from(1_000u64), - amount_out: U256::from(1_000u64), + token_in: Some(Address::repeat_byte(0x11)), + token_out: Some(Address::repeat_byte(0x22)), + amount_in: Some(U256::from(1_000u64)), + amount_out: Some(U256::from(1_000u64)), venue_fee_in: None, venue_fee_out: None, settled_gas: None, - quote: None, + min_amount_out: None, + declared_quote: None, + quote_timestamp: None, sandwich: None, }; // A coverage gap: Fynd could not solve at either state. let range = build_range( &trade, &empty_prices(), - Outcome::Unsolvable("missing token in Tycho".into()), - Outcome::Unsolvable("missing token in Tycho".into()), - &Outcome::Unsolvable("no top-of-block route to re-execute".into()), + Some(( + Outcome::Unsolvable("missing token in Tycho".into()), + Outcome::Unsolvable("missing token in Tycho".into()), + Outcome::Unsolvable("no top-of-block route to re-execute".into()), + )), ); let rec = comparison_record(&range, &empty_prices(), &empty_prices()); assert_eq!(rec.pointer("/top/verdict").unwrap(), "unsolvable"); @@ -589,19 +647,22 @@ mod tests { tx_hash: TxHash::default(), block_number: 25_000_000, tx_index: 42, + status: TradeStatus::Settled, venue: "relay".into(), solver: "1inch".into(), solver_source: AttributionSource::TraceMatch, decoder: "sender-netting", sender: Address::ZERO, - token_in: Address::repeat_byte(0x11), - token_out: Address::repeat_byte(0x22), - amount_in: U256::from(1_000u64), - amount_out: U256::from(1_000u64), + token_in: Some(Address::repeat_byte(0x11)), + token_out: Some(Address::repeat_byte(0x22)), + amount_in: Some(U256::from(1_000u64)), + amount_out: Some(U256::from(1_000u64)), venue_fee_in: None, venue_fee_out: None, settled_gas: None, - quote: None, + min_amount_out: None, + declared_quote: None, + quote_timestamp: None, sandwich: None, }; trade.sandwich = Some(SandwichEvidence { @@ -622,8 +683,11 @@ mod tests { solved_route: None, }) }; - let range = - build_range(&trade, &empty_prices(), solved(1_100), solved(1_050), &solved(1_050)); + let range = build_range( + &trade, + &empty_prices(), + Some((solved(1_100), solved(1_050), solved(1_050))), + ); let rec = comparison_record(&range, &empty_prices(), &empty_prices()); assert_eq!(rec.pointer("/tx_index").unwrap(), 42); @@ -640,4 +704,148 @@ mod tests { &format!("{:#x}", Address::repeat_byte(0x44)) ); } + + fn revert_solved(amount_out: u64) -> Outcome { + Outcome::Solved(SolvedAmount { + amount_out: U256::from(amount_out), + amount_out_net_gas: U256::from(amount_out), + gas_estimate: U256::from(21_000), + algorithm: String::new(), + quote_json: None, + solved_route: None, + }) + } + + fn reverted_trade(cause: RevertCause) -> DecodedTrade { + DecodedTrade { + tx_hash: TxHash::repeat_byte(0x55), + block_number: 25_000_000, + tx_index: 9, + status: TradeStatus::Reverted { cause }, + venue: "relay".into(), + solver: "fly".into(), + solver_source: AttributionSource::TraceMatch, + decoder: "reverted", + sender: Address::repeat_byte(0x99), + token_in: Some(Address::repeat_byte(0x11)), + token_out: Some(Address::repeat_byte(0x22)), + amount_in: Some(U256::from(1_000u64)), + amount_out: None, + venue_fee_in: None, + venue_fee_out: None, + settled_gas: None, + min_amount_out: Some(U256::from(10_000u64)), + declared_quote: None, + quote_timestamp: None, + sandwich: None, + } + } + + #[test] + fn test_reverted_record_slippage_floor_fillable_at_back() { + let trade = reverted_trade(RevertCause::SlippageFloor); + let range = build_range( + &trade, + &empty_prices(), + Some((revert_solved(9_800), revert_solved(10_200), revert_solved(10_200))), + ); + let rec = comparison_record(&range, &empty_prices(), &empty_prices()); + + assert_eq!(rec.pointer("/tx_index").unwrap(), 9); + assert_eq!(rec.pointer("/tx_hash").unwrap(), &TxHash::repeat_byte(0x55).to_string()); + // A reverted trade still records who sent it — the tx sender, since there is no netted + // flow to draw a different tracked party from. + assert_eq!(rec.pointer("/sender").unwrap(), &format!("{:#x}", Address::repeat_byte(0x99))); + assert_eq!(rec.pointer("/status").unwrap(), "reverted"); + assert_eq!(rec.pointer("/cause").unwrap(), "slippage_floor"); + assert!(rec + .pointer("/cause_detail") + .unwrap() + .is_null()); + assert_eq!(rec.pointer("/min_amount_out").unwrap(), "10000"); + // Nothing settled: settled-only fields are absent. + assert!(rec + .pointer("/settled_amount_out") + .unwrap() + .is_null()); + assert_eq!(rec.pointer("/top/fillable").unwrap(), false); + assert_eq!(rec.pointer("/back/fillable").unwrap(), true); + assert!( + rec.pointer("/back/margin_bps") + .unwrap() + .as_f64() + .unwrap() > + 0.0 + ); + } + + #[test] + fn test_reverted_record_other_cause_carries_detail() { + let trade = reverted_trade(RevertCause::Other("execution reverted".to_string())); + let range = build_range( + &trade, + &empty_prices(), + Some(( + Outcome::Unsolvable("no route".into()), + Outcome::Unsolvable("no route".into()), + Outcome::Unsolvable("no route".into()), + )), + ); + let rec = comparison_record(&range, &empty_prices(), &empty_prices()); + + assert_eq!(rec.pointer("/cause").unwrap(), "other"); + assert_eq!(rec.pointer("/cause_detail").unwrap(), "execution reverted"); + assert!(rec + .pointer("/top/fillable") + .unwrap() + .is_null()); + } + + #[test] + fn test_reverted_trade_with_unknown_terms_has_no_top_or_back() { + let mut trade = reverted_trade(RevertCause::Other("unknown revert".to_string())); + trade.token_in = None; + trade.token_out = None; + trade.amount_in = None; + trade.min_amount_out = None; + let range = build_range(&trade, &empty_prices(), None); + let rec = comparison_record(&range, &empty_prices(), &empty_prices()); + + assert_eq!(rec.pointer("/status").unwrap(), "reverted"); + assert!(rec.pointer("/top").unwrap().is_null()); + assert!(rec.pointer("/back").unwrap().is_null()); + assert!(rec + .pointer("/token_in") + .unwrap() + .is_null()); + } + + #[test] + fn test_write_comparisons_appends_lines_for_settled_and_reverted() { + let settled_range = build_range( + &reverted_trade(RevertCause::OutOfGas), + &empty_prices(), + Some((Outcome::Unsolvable("x".into()), revert_solved(10_000), revert_solved(10_000))), + ); + let mut buf: Vec = Vec::new(); + write_comparisons( + &mut buf, + std::slice::from_ref(&settled_range), + &empty_prices(), + &empty_prices(), + ); + write_comparisons( + &mut buf, + std::slice::from_ref(&settled_range), + &empty_prices(), + &empty_prices(), + ); + + let text = String::from_utf8(buf).unwrap(); + assert_eq!(text.lines().count(), 2); + for line in text.lines() { + let value: serde_json::Value = serde_json::from_str(line).unwrap(); + assert_eq!(value["cause"], "out_of_gas"); + } + } } diff --git a/tools/hindsight/src/resolve/mod.rs b/tools/hindsight/src/resolve/mod.rs index f9f83caf..413b7038 100644 --- a/tools/hindsight/src/resolve/mod.rs +++ b/tools/hindsight/src/resolve/mod.rs @@ -22,7 +22,7 @@ use serde::Serialize; use tycho_simulation::tycho_common::models::Address as CoreAddress; use crate::{ - decoder::{AttributionSource, DecodedTrade, SandwichEvidence, SolverQuote}, + decoder::{AttributionSource, DecodedTrade, SandwichEvidence, TradeStatus}, usd::Prices, }; @@ -207,55 +207,93 @@ pub(crate) enum Outcome { #[derive(Debug, Clone, Serialize)] pub(crate) struct StateResult { pub outcome: Outcome, + /// Fynd vs the settled output. `Deltas::default()` (all `None`) when nothing settled to + /// compare against — a reverted trade. pub deltas: Deltas, pub verdict: Verdict, + /// Whether the quote cleared the trade's on-chain floor (`min_amount_out`), when one is + /// known. Computed for both settled and reverted trades — a settled trade cleared its floor + /// by construction, but the margin is still informative. `None` when the state was not + /// solved or no floor is known. + pub fillable: Option, + pub margin_bps: Option, } impl StateResult { - fn new(outcome: Outcome, settled_amount_out: U256, settled_net_gas: U256) -> Self { - let outcome = compare::served(outcome, settled_amount_out); - let deltas = compare::compare(&outcome, settled_amount_out, settled_net_gas); + /// `settled` is `Some((gross, net_gas))` for a settled trade, `None` for a reverted one — + /// there is nothing settled to compare gross output against, so `deltas`/`verdict` fall back + /// to their unsolved-comparison values (`Deltas::default()`, `Verdict::Unsolvable`) even when + /// `outcome` itself solved; `fillable`/`margin_bps` are unaffected, since they judge the + /// outcome against `min_amount_out`, not against a settled amount. + fn new(outcome: Outcome, settled: Option<(U256, U256)>, min_amount_out: Option) -> Self { + let outcome = match settled { + Some((gross, _)) => compare::served(outcome, gross), + None => outcome, + }; + let deltas = match settled { + Some((gross, net)) => compare::compare(&outcome, gross, net), + None => Deltas::default(), + }; let verdict = compare::verdict(&outcome, &deltas); - Self { outcome, deltas, verdict } + let (fillable, margin_bps) = compare::floor_judgment(&outcome, min_amount_out); + Self { outcome, deltas, verdict, fillable, margin_bps } } } -/// A trade re-solved at both block states, presented as a range. +/// A trade re-solved at both block states, presented as a range — settled or reverted, told +/// apart by `status`. A reverted trade's settled-only fields (`settled_amount_out` and friends) +/// are simply absent; everything else about the record has the same shape. #[derive(Debug, Clone, Serialize)] pub(crate) struct RangeComparison { pub tx_hash: TxHash, pub block_number: u64, pub tx_index: u64, + #[serde(flatten)] + pub status: TradeStatus, pub venue: String, pub solver: String, /// The evidence tier the solver label came from (from the decoder). pub solver_source: AttributionSource, - /// Which decoder recovered the settled trade. + /// Which decoder recovered the trade. pub decoder: &'static str, - pub token_in: Address, - pub token_out: Address, - pub amount_in: U256, - pub settled_amount_out: U256, + /// The trader, from the decoder: the netted flow's tracked party for a settled trade, or the + /// transaction sender for a reverted one (there is no netted flow to draw a different party + /// from). Lets a venue-filler fill (e.g. Relay's rotating filler) be segmented by its actual + /// trader without an on-chain lookup. + pub sender: Address, + /// `None` only for a reverted trade whose solver frame's calldata did not parse. + pub token_in: Option

, + pub token_out: Option
, + pub amount_in: Option, + /// `None` for a reverted trade: nothing was delivered. + pub settled_amount_out: Option, /// Settled output after the gas the trader paid for the route, in `token_out` units. Equals /// `settled_amount_out` when that gas is unknown, was paid by someone else, or the output - /// token is unpriced. - pub settled_amount_out_net_gas: U256, + /// token is unpriced. `None` for a reverted trade. + pub settled_amount_out_net_gas: Option, /// Wei cost of the settled route's gas, when the trader paid it (from the decoder). pub settled_gas: Option, - /// The solver's own off-chain quote from its calldata, when declared (from the decoder). - pub quote: Option, + /// The on-chain enforced floor declared in the settling solver frame's own calldata (from + /// the decoder). + pub min_amount_out: Option, + /// The solver's own off-chain quote, when its calldata declares one (from the decoder; + /// unit-checked against the settled amount). + pub declared_quote: Option, + /// Unix timestamp of `declared_quote`, when the calldata carries one. + pub quote_timestamp: Option, /// Evidence that a front-run and a back-run bracketed this trade (from the decoder). `None` - /// when no bracket pair was found. + /// when no bracket pair was found, or the trade reverted. pub sandwich: Option, - /// Optimistic: solved at state N-1, before the block's swaps moved the pools. - pub top: StateResult, + /// Optimistic: solved at state N-1, before the block's swaps moved the pools. `None` when + /// the trade's terms were unknown — there was nothing to solve. + pub top: Option, /// Pessimistic: solved fresh at state N, after the block's swaps moved the pools — what - /// routing at the block's end state would deliver. - pub back: StateResult, - /// Headline verdict — top-of-block (the optimistic default). - pub verdict: Verdict, + /// routing at the block's end state would deliver. `None` alongside `top`. + pub back: Option, + /// Headline verdict — top-of-block (the optimistic default). `None` alongside `top`. + pub verdict: Option, /// Slippage of the top route between quote time (N-1) and re-execution (N). `None` when the - /// top was unsolved or the re-execution failed. + /// top was unsolved, the re-execution failed, or the trade's terms were unknown. pub slippage: Option, } @@ -274,14 +312,16 @@ pub(crate) trait SteppingSolver { async fn reexecute(&self, top: &SolvedAmount) -> Outcome; } -/// Build a `RangeComparison` from a trade's three outcomes: the top-of-block solve, the fresh -/// back-of-block solve, and the top route's re-execution at back-of-block (which feeds only the -/// `slippage` field). +/// Build a `RangeComparison` from a trade and, when its terms were known, the three outcomes +/// solving it produced: the top-of-block solve, the fresh back-of-block solve, and the top +/// route's re-execution at back-of-block (which feeds only the `slippage` field). `None` when the +/// trade's terms were unknown — there was nothing to solve, so `top`/`back`/`verdict`/`slippage` +/// on the returned comparison are all `None` too, but the trade is still recorded. /// /// When the decoder isolated the gas the trader paid for the settled route, its cost is converted /// into `token_out` units at the `prices` snapshot (top-of-block — a fine approximation for a gas /// deduction) and subtracted from the settled output, so both sides of the net comparison carry -/// their own gas. +/// their own gas. Reverted trades have no settled output to deduct from — `settled` is `None`. /// /// When the decoder flagged the trade as sandwiched, each *solved* state's verdict becomes /// `Verdict::Sandwiched`: its win or loss measures the MEV that moved the settled output, not @@ -292,42 +332,59 @@ pub(crate) trait SteppingSolver { pub(crate) fn build_range( trade: &DecodedTrade, prices: &Prices, - top: Outcome, - back: Outcome, - reexecuted: &Outcome, + solved: Option<(Outcome, Outcome, Outcome)>, ) -> RangeComparison { - let settled_net_gas = trade - .settled_gas - .and_then(|gas| prices.gas_in_token(gas, trade.token_out)) - .map_or(trade.amount_out, |gas_out| trade.amount_out.saturating_sub(gas_out)); - // Computed from the raw outcomes: the coverage-miss reclassification below discards the - // solved amounts the slippage is measured from. - let slippage = compare::slippage(&top, reexecuted); - let mut top = StateResult::new(top, trade.amount_out, settled_net_gas); - let mut back = StateResult::new(back, trade.amount_out, settled_net_gas); - if trade.sandwich.is_some() { - for state in [&mut top, &mut back] { - if let Outcome::Solved(_) = state.outcome { - state.verdict = Verdict::Sandwiched; + let settled = trade.amount_out.map(|gross| { + let net_gas = trade + .settled_gas + .and_then(|gas| { + trade + .token_out + .and_then(|token| prices.gas_in_token(gas, token)) + }) + .map_or(gross, |gas_out| gross.saturating_sub(gas_out)); + (gross, net_gas) + }); + + let (top, back, verdict, slippage) = match solved { + Some((top_outcome, back_outcome, reexecuted)) => { + // Computed from the raw outcomes: the coverage-miss reclassification below discards + // the solved amounts the slippage is measured from. + let slippage = compare::slippage(&top_outcome, &reexecuted); + let mut top = StateResult::new(top_outcome, settled, trade.min_amount_out); + let mut back = StateResult::new(back_outcome, settled, trade.min_amount_out); + if trade.sandwich.is_some() { + for state in [&mut top, &mut back] { + if let Outcome::Solved(_) = state.outcome { + state.verdict = Verdict::Sandwiched; + } + } } + let verdict = top.verdict; + (Some(top), Some(back), Some(verdict), slippage) } - } - let verdict = top.verdict; + None => (None, None, None, None), + }; + RangeComparison { tx_hash: trade.tx_hash, block_number: trade.block_number, tx_index: trade.tx_index, + status: trade.status.clone(), venue: trade.venue.clone(), solver: trade.solver.clone(), solver_source: trade.solver_source, decoder: trade.decoder, + sender: trade.sender, token_in: trade.token_in, token_out: trade.token_out, amount_in: trade.amount_in, - settled_amount_out: trade.amount_out, - settled_amount_out_net_gas: settled_net_gas, + settled_amount_out: settled.map(|(gross, _)| gross), + settled_amount_out_net_gas: settled.map(|(_, net)| net), settled_gas: trade.settled_gas, - quote: trade.quote.clone(), + min_amount_out: trade.min_amount_out, + declared_quote: trade.declared_quote, + quote_timestamp: trade.quote_timestamp, sandwich: trade.sandwich.clone(), top, back, @@ -337,10 +394,15 @@ pub(crate) fn build_range( } /// Re-solve every trade in a held block at top-of-block, advance to back-of-block, then measure -/// each trade twice at the new state: re-execute its top route against the pools as the block -/// left them (for the slippage), and solve it fresh (for the `back` comparison). Solving all -/// trades at one state before advancing keeps each state's reads consistent and steps the chain -/// only once per block. +/// each again at the new state: its top route is re-executed against the pools as the block left +/// them (for the slippage) and it is solved fresh (for the `back` comparison). Solving everything +/// at one state before advancing keeps each state's reads consistent and steps the chain only +/// once per block. Settled and reverted trades go through the same wave — a reverted trade whose +/// solver calldata parsed into a `SwapIntent` is solved exactly like a settled one, judged against +/// its `min_amount_out` floor instead of a settled amount. +/// +/// A trade with unknown terms (a reverted trade whose solver calldata did not parse) is recorded +/// but not solved — there is nothing to feed the solver. pub(crate) async fn resolve_block_range( solver: &S, trades: &[DecodedTrade], @@ -348,28 +410,41 @@ pub(crate) async fn resolve_block_range( ) -> anyhow::Result> { let mut tops = Vec::with_capacity(trades.len()); for trade in trades { - tops.push( - solver - .solve(trade.token_in, trade.token_out, trade.amount_in) - .await, - ); + let top = match trade.terms() { + Some((token_in, token_out, amount_in)) => Some( + solver + .solve(token_in, token_out, amount_in) + .await, + ), + None => None, + }; + tops.push(top); } solver.advance().await?; let mut ranges = Vec::with_capacity(trades.len()); for (trade, top) in trades.iter().zip(tops) { + let Some(top) = top else { + ranges.push(build_range(trade, prices, None)); + continue; + }; let reexecuted = match &top { Outcome::Solved(solved) => solver.reexecute(solved).await, Outcome::Partial(_) | Outcome::Unsolvable(_) => { Outcome::Unsolvable("no top-of-block route to re-execute".to_string()) } }; + // `top` was solved from `trade.terms()`, so the terms are known here too. + let (token_in, token_out, amount_in) = trade + .terms() + .expect("a solved top implies known terms"); let back = solver - .solve(trade.token_in, trade.token_out, trade.amount_in) + .solve(token_in, token_out, amount_in) .await; - ranges.push(build_range(trade, prices, top, back, &reexecuted)); + ranges.push(build_range(trade, prices, Some((top, back, reexecuted)))); } + Ok(ranges) } @@ -389,19 +464,77 @@ mod tests { tx_hash: TxHash::default(), block_number: 21_000_000, tx_index: 0, + status: TradeStatus::Settled, venue: "relay".into(), solver: "tycho".into(), solver_source: AttributionSource::TraceMatch, decoder: "sender-netting", sender: Address::ZERO, - token_in: Address::repeat_byte(0x11), - token_out: Address::repeat_byte(0x22), - amount_in: U256::from(1_000u64), - amount_out: U256::from(settled), + token_in: Some(Address::repeat_byte(0x11)), + token_out: Some(Address::repeat_byte(0x22)), + amount_in: Some(U256::from(1_000u64)), + amount_out: Some(U256::from(settled)), + venue_fee_in: None, + venue_fee_out: None, + settled_gas: None, + min_amount_out: None, + declared_quote: None, + quote_timestamp: None, + sandwich: None, + } + } + + /// A reverted trade whose solver calldata parsed into a `SwapIntent` — carries terms and a + /// floor (`min_amount_out`) but no settled output. + fn reverted_trade(cause: crate::decoder::RevertCause, min_amount_out: u64) -> DecodedTrade { + DecodedTrade { + tx_hash: TxHash::repeat_byte(0x02), + block_number: 21_000_000, + tx_index: 1, + status: TradeStatus::Reverted { cause }, + venue: "relay".into(), + solver: "fly".into(), + solver_source: AttributionSource::TraceMatch, + decoder: "reverted", + sender: Address::ZERO, + token_in: Some(Address::repeat_byte(0x11)), + token_out: Some(Address::repeat_byte(0x22)), + amount_in: Some(U256::from(1_000u64)), + amount_out: None, + venue_fee_in: None, + venue_fee_out: None, + settled_gas: None, + min_amount_out: Some(U256::from(min_amount_out)), + declared_quote: None, + quote_timestamp: None, + sandwich: None, + } + } + + /// A reverted trade whose solver calldata did not parse — no terms, nothing to solve. + fn reverted_trade_without_terms() -> DecodedTrade { + DecodedTrade { + tx_hash: TxHash::repeat_byte(0x03), + block_number: 21_000_000, + tx_index: 2, + status: TradeStatus::Reverted { + cause: crate::decoder::RevertCause::Other("unknown revert".to_string()), + }, + venue: "relay".into(), + solver: "relay".into(), + solver_source: AttributionSource::Fallback, + decoder: "reverted", + sender: Address::ZERO, + token_in: None, + token_out: None, + amount_in: None, + amount_out: None, venue_fee_in: None, venue_fee_out: None, settled_gas: None, - quote: None, + min_amount_out: None, + declared_quote: None, + quote_timestamp: None, sandwich: None, } } @@ -460,12 +593,12 @@ mod tests { let range = build_range( &trade(10_000), &empty_prices(), - solved(10_200, 10_100), - solved(10_010, 9_990), - &solved(10_010, 9_990), + Some((solved(10_200, 10_100), solved(10_010, 9_990), solved(10_010, 9_990))), ); - assert_eq!(range.verdict, Verdict::Win); // top is the headline - assert!(range.top.deltas.raw_bps.unwrap() > range.back.deltas.raw_bps.unwrap()); + assert_eq!(range.verdict, Some(Verdict::Win)); // top is the headline + let top = range.top.unwrap(); + let back = range.back.unwrap(); + assert!(top.deltas.raw_bps.unwrap() > back.deltas.raw_bps.unwrap()); } #[test] @@ -474,13 +607,12 @@ mod tests { let range = build_range( &trade(10_000), &empty_prices(), - solved(1_000, 990), - solved(1_000, 990), - &solved(1_000, 990), + Some((solved(1_000, 990), solved(1_000, 990), solved(1_000, 990))), ); - assert_eq!(range.verdict, Verdict::CoverageMiss); - assert_eq!(range.top.deltas, Deltas { raw_bps: None, net_bps: None }); - assert!(matches!(range.top.outcome, Outcome::Partial(_))); + assert_eq!(range.verdict, Some(Verdict::CoverageMiss)); + let top = range.top.unwrap(); + assert_eq!(top.deltas, Deltas { raw_bps: None, net_bps: None }); + assert!(matches!(top.outcome, Outcome::Partial(_))); } #[test] @@ -495,17 +627,17 @@ mod tests { let range = build_range( &sandwiched, &empty_prices(), - solved(10_200, 10_100), - solved(9_800, 9_700), - &solved(9_800, 9_700), + Some((solved(10_200, 10_100), solved(9_800, 9_700), solved(9_800, 9_700))), ); - assert_eq!(range.verdict, Verdict::Sandwiched); - assert_eq!(range.top.verdict, Verdict::Sandwiched); - assert_eq!(range.back.verdict, Verdict::Sandwiched); + assert_eq!(range.verdict, Some(Verdict::Sandwiched)); + let top = range.top.unwrap(); + let back = range.back.unwrap(); + assert_eq!(top.verdict, Verdict::Sandwiched); + assert_eq!(back.verdict, Verdict::Sandwiched); // Deltas are unaffected by the override: still computed for offline analysis. - assert!(range.top.deltas.raw_bps.unwrap() > 0.0); - assert!(range.back.deltas.raw_bps.unwrap() < 0.0); + assert!(top.deltas.raw_bps.unwrap() > 0.0); + assert!(back.deltas.raw_bps.unwrap() < 0.0); } #[test] @@ -522,14 +654,16 @@ mod tests { let range = build_range( &sandwiched, &empty_prices(), - solved(10_200, 10_100), - Outcome::Unsolvable("missing token in Tycho".into()), - &Outcome::Unsolvable("re-execution failed".into()), + Some(( + solved(10_200, 10_100), + Outcome::Unsolvable("missing token in Tycho".into()), + Outcome::Unsolvable("re-execution failed".into()), + )), ); - assert_eq!(range.top.verdict, Verdict::Sandwiched); - assert_eq!(range.back.verdict, Verdict::Unsolvable); - assert_eq!(range.verdict, Verdict::Sandwiched); // headline follows top + assert_eq!(range.top.unwrap().verdict, Verdict::Sandwiched); + assert_eq!(range.back.unwrap().verdict, Verdict::Unsolvable); + assert_eq!(range.verdict, Some(Verdict::Sandwiched)); // headline follows top } #[test] @@ -539,18 +673,16 @@ mod tests { let mut with_gas = trade(10_000); with_gas.settled_gas = Some(U256::from(100u64)); let mut prices = empty_prices(); - prices.insert(with_gas.token_out, 2.0); + prices.insert(with_gas.token_out.unwrap(), 2.0); let range = build_range( &with_gas, &prices, - solved(10_050, 9_990), - solved(10_050, 9_990), - &solved(10_050, 9_990), + Some((solved(10_050, 9_990), solved(10_050, 9_990), solved(10_050, 9_990))), ); - assert_eq!(range.settled_amount_out_net_gas, U256::from(9_800u64)); - assert_eq!(range.settled_amount_out, U256::from(10_000u64)); - assert_eq!(range.verdict, Verdict::Win); + assert_eq!(range.settled_amount_out_net_gas, Some(U256::from(9_800u64))); + assert_eq!(range.settled_amount_out, Some(U256::from(10_000u64))); + assert_eq!(range.verdict, Some(Verdict::Win)); } #[test] @@ -563,12 +695,42 @@ mod tests { let range = build_range( &with_gas, &empty_prices(), - solved(10_050, 9_990), - solved(10_050, 9_990), - &solved(10_050, 9_990), + Some((solved(10_050, 9_990), solved(10_050, 9_990), solved(10_050, 9_990))), ); - assert_eq!(range.settled_amount_out_net_gas, U256::from(10_000u64)); - assert_eq!(range.verdict, Verdict::Win); + assert_eq!(range.settled_amount_out_net_gas, Some(U256::from(10_000u64))); + assert_eq!(range.verdict, Some(Verdict::Win)); + } + + #[test] + fn test_build_range_unknown_terms_records_without_solving() { + // A reverted trade whose solver calldata did not parse: nothing to solve, but it is + // still recorded, tagged by its cause. + let range = build_range(&reverted_trade_without_terms(), &empty_prices(), None); + assert!(matches!(range.status, TradeStatus::Reverted { .. })); + assert!(range.token_in.is_none()); + assert!(range.top.is_none()); + assert!(range.back.is_none()); + assert_eq!(range.verdict, None); + assert_eq!(range.slippage, None); + } + + #[test] + fn test_build_range_reverted_trade_judged_against_floor() { + // A reverted trade with known terms and a floor: no settled amount to compare against + // (deltas/verdict stay at their unsolved defaults), but fillable/margin_bps still judge + // the quote against min_amount_out. + let range = build_range( + &reverted_trade(crate::decoder::RevertCause::SlippageFloor, 10_000), + &empty_prices(), + Some((solved(9_800, 9_700), solved(10_100, 10_000), solved(10_100, 10_000))), + ); + assert!(range.settled_amount_out.is_none()); + let top = range.top.unwrap(); + let back = range.back.unwrap(); + assert_eq!(top.deltas, Deltas { raw_bps: None, net_bps: None }); + assert_eq!(top.fillable, Some(false)); + assert_eq!(back.fillable, Some(true)); + assert!(back.margin_bps.unwrap() > 0.0); } #[tokio::test] @@ -588,9 +750,11 @@ mod tests { assert_eq!(ranges.len(), 2); for range in &ranges { - assert_eq!(range.top.verdict, Verdict::Win); - assert_eq!(range.back.verdict, Verdict::Loss); - assert!(range.top.deltas.raw_bps.unwrap() > range.back.deltas.raw_bps.unwrap()); + let top = range.top.as_ref().unwrap(); + let back = range.back.as_ref().unwrap(); + assert_eq!(top.verdict, Verdict::Win); + assert_eq!(back.verdict, Verdict::Loss); + assert!(top.deltas.raw_bps.unwrap() > back.deltas.raw_bps.unwrap()); let slippage = range.slippage.unwrap(); assert!(slippage.bps < 0.0, "re-execution below quote must be negative slippage"); assert_eq!(slippage.quoted_amount_out, U256::from(10_200u64)); @@ -613,11 +777,48 @@ mod tests { .await .unwrap(); - assert_eq!(ranges[0].top.verdict, Verdict::Unsolvable); - assert_eq!(ranges[0].back.verdict, Verdict::Win); + assert_eq!(ranges[0].top.as_ref().unwrap().verdict, Verdict::Unsolvable); + assert_eq!(ranges[0].back.as_ref().unwrap().verdict, Verdict::Win); assert_eq!(ranges[0].slippage, None); } + #[tokio::test] + async fn resolve_block_range_solves_reverts_and_skips_unknown_terms() { + // The floor is 10_000: unfillable at top, fillable at back — the shape a sequencer that + // fills against fresher state would avoid. The intent-less revert has no terms, so it is + // recorded but never reaches the solver. + let solver = MockStepping { + advanced: std::sync::atomic::AtomicBool::new(false), + top: solved(9_800, 9_700), + back: solved(10_100, 10_000), + reexecuted: solved(10_100, 10_000), + }; + let trades = [ + reverted_trade(crate::decoder::RevertCause::SlippageFloor, 10_000), + reverted_trade_without_terms(), + ]; + let ranges = resolve_block_range(&solver, &trades, &empty_prices()) + .await + .unwrap(); + + assert_eq!(ranges.len(), 2); + assert!(ranges[1].top.is_none(), "the terms-less revert must not be solved"); + + let comparison = &ranges[0]; + assert_eq!(comparison.min_amount_out, Some(U256::from(10_000u64))); + assert_eq!( + comparison + .top + .as_ref() + .unwrap() + .fillable, + Some(false) + ); + let back = comparison.back.as_ref().unwrap(); + assert_eq!(back.fillable, Some(true)); + assert!(back.margin_bps.unwrap() > 0.0); + } + #[test] fn build_range_positive_slippage_from_raw_outcomes() { // The route re-executed to more than quoted: the surplus we could charge. The fresh @@ -625,9 +826,7 @@ mod tests { let range = build_range( &trade(10_000), &empty_prices(), - solved(10_000, 9_900), - solved(10_500, 10_400), - &solved(10_050, 9_950), + Some((solved(10_000, 9_900), solved(10_500, 10_400), solved(10_050, 9_950))), ); let slippage = range.slippage.unwrap(); assert!((slippage.bps - 50.0).abs() < 0.01, "expected +50 bps, got {}", slippage.bps); @@ -640,11 +839,13 @@ mod tests { let range = build_range( &trade(10_000), &empty_prices(), - solved(10_000, 9_900), - Outcome::Unsolvable("no route at back-of-block".into()), - &solved(10_050, 9_950), + Some(( + solved(10_000, 9_900), + Outcome::Unsolvable("no route at back-of-block".into()), + solved(10_050, 9_950), + )), ); - assert_eq!(range.back.verdict, Verdict::Unsolvable); + assert_eq!(range.back.unwrap().verdict, Verdict::Unsolvable); let slippage = range.slippage.unwrap(); assert!((slippage.bps - 50.0).abs() < 0.01, "expected +50 bps, got {}", slippage.bps); } @@ -657,11 +858,9 @@ mod tests { let range = build_range( &trade(10_000), &empty_prices(), - solved(1_000, 990), - solved(1_010, 1_000), - &solved(1_010, 1_000), + Some((solved(1_000, 990), solved(1_010, 1_000), solved(1_010, 1_000))), ); - assert_eq!(range.verdict, Verdict::CoverageMiss); + assert_eq!(range.verdict, Some(Verdict::CoverageMiss)); let slippage = range.slippage.unwrap(); assert!((slippage.bps - 100.0).abs() < 0.01, "expected +100 bps, got {}", slippage.bps); } diff --git a/tools/hindsight/src/resolve/monitor.rs b/tools/hindsight/src/resolve/monitor.rs index b09463e3..c8258a71 100644 --- a/tools/hindsight/src/resolve/monitor.rs +++ b/tools/hindsight/src/resolve/monitor.rs @@ -33,7 +33,7 @@ use tycho_simulation::tycho_common::models::{Address as CoreAddress, Chain}; use crate::{ decoder::{DecodedTrade, Decoder, Registry}, provider_from, - resolve::{resolve_block_range, Outcome, SolvedAmount, SteppingSolver}, + resolve::{resolve_block_range, Outcome, RangeComparison, SolvedAmount, SteppingSolver}, telemetry, usd::Prices, }; @@ -500,9 +500,12 @@ pub(crate) async fn run(cfg: MonitorArgs) -> anyhow::Result<()> { let mut comparisons = match cfg.comparisons_dir.as_ref() { Some(dir) => { - let writer = super::jsonl::RotatingWriter::open(dir)?; - info!(path = %writer.current_path().display(), "appending comparisons to JSONL"); - Some(writer) + let comparisons = super::jsonl::RotatingWriter::open(dir)?; + info!( + comparisons_path = %comparisons.current_path().display(), + "appending comparisons to JSONL" + ); + Some(comparisons) } None => None, }; @@ -617,59 +620,50 @@ async fn run_session( } } - let trades = match decode_block_when_available(decoder, target, pacing.rpc_lag_budget).await - { - Ok(trades) => trades, - Err(e) => { - totals.skipped_blocks += 1; - telemetry::record_skipped_block(); - warn!( - block = target, - skipped_total = totals.skipped_blocks, - "decode failed, skipping block: {e}" - ); - if let Err(e) = adapter.advance().await { - return SessionEnd::Unhealthy(e.to_string()); + let decoded = + match decode_block_when_available(decoder, target, pacing.rpc_lag_budget).await { + Ok(decoded) => decoded, + Err(e) => { + totals.skipped_blocks += 1; + telemetry::record_skipped_block(); + warn!( + block = target, + skipped_total = totals.skipped_blocks, + "decode failed, skipping block: {e}" + ); + if let Err(e) = adapter.advance().await { + return SessionEnd::Unhealthy(e.to_string()); + } + continue; } - continue; - } - }; + }; let start = Instant::now(); // Snapshot token prices at top-of-block (N-1) for the headline metric and the top-of-block // USD valuation. let prices_top = snapshot_prices(adapter.solver, decoder.registry()).await; - let ranges = match resolve_block_range(adapter, &trades, &prices_top).await { - Ok(ranges) => ranges, + let (ranges, prices_back) = match resolve_and_snapshot_back( + adapter, + decoder, + &decoded, + &prices_top, + target, + ) + .await + { + Ok(resolved) => resolved, Err(e) => return SessionEnd::Unhealthy(e.to_string()), }; - // resolve_block_range advanced the solver to back-of-block (N); snapshot again so the - // back-of-block improvement is valued against the state it was solved at. - let prices_back = snapshot_prices(adapter.solver, decoder.registry()).await; - // The back-of-block solve should land on `target`. On a reorg/gap/resync the stream can - // apply a different block, silently pairing the back state with another block's trades. - // The top-of-block (N-1) headline is unaffected; warn so the mispaired back state is - // visible. - let applied = adapter.current_block().await; - if applied != Some(target) { - warn!( - target, - applied = ?applied, - "back-of-block state is not the target block; back comparison may be off" - ); - } - for range in &ranges { - telemetry::record_range( - range, - &cfg.chain.name, - &prices_top, - &prices_back, - decoder.registry(), - ); - } - if let Some(rotating) = comparisons.as_mut() { - super::jsonl::write_comparisons(rotating.writer(), &ranges, &prices_top, &prices_back); - } + record_block_resolution( + &BlockRecording { + chain: &cfg.chain.name, + registry: decoder.registry(), + ranges: &ranges, + prices_top: &prices_top, + prices_back: &prices_back, + }, + comparisons, + ); let elapsed_s = start.elapsed().as_secs_f64(); telemetry::record_block_seconds(elapsed_s); @@ -686,6 +680,71 @@ async fn run_session( } } +/// Resolve one block's trades (settled and reverted) at both states, then snapshot back-of-block +/// prices. +/// +/// `resolve_block_range` advances the solver to back-of-block (N) as a side effect, so the price +/// snapshot must be taken after it returns to value the back-of-block improvement against the +/// state it was solved at. The back-of-block solve should land on `target`; on a reorg, gap, or +/// resync the stream can apply a different block, silently pairing the back state with another +/// block's trades — the top-of-block (N-1) headline is unaffected, but this is worth a warning so +/// the mispairing is visible. +async fn resolve_and_snapshot_back( + adapter: &StepAdapter<'_>, + decoder: &Decoder

, + decoded: &[DecodedTrade], + prices_top: &Prices, + target: u64, +) -> anyhow::Result<(Vec, Prices)> { + let ranges = resolve_block_range(adapter, decoded, prices_top).await?; + let prices_back = snapshot_prices(adapter.solver, decoder.registry()).await; + let applied = adapter.current_block().await; + if applied != Some(target) { + warn!( + target, + applied = ?applied, + "back-of-block state is not the target block; back comparison may be off" + ); + } + Ok((ranges, prices_back)) +} + +/// One block's re-solved trades, bundled so the recording helper below takes a handful of +/// parameters instead of every constituent piece separately. +struct BlockRecording<'a> { + chain: &'a str, + registry: &'a Registry, + ranges: &'a [RangeComparison], + prices_top: &'a Prices, + prices_back: &'a Prices, +} + +/// Record and persist one block's re-solved trades — settled and reverted alike, told apart by +/// each record's `status`: Prometheus metrics, plus the single JSONL stream when the run was +/// configured with `--comparisons-dir`. +fn record_block_resolution( + block: &BlockRecording<'_>, + comparisons: &mut Option, +) { + for range in block.ranges { + telemetry::record_range( + range, + block.chain, + block.prices_top, + block.prices_back, + block.registry, + ); + } + if let Some(rotating) = comparisons.as_mut() { + super::jsonl::write_comparisons( + rotating.writer(), + block.ranges, + block.prices_top, + block.prices_back, + ); + } +} + /// Snapshot the solver's current token prices as `Prices` (token native-units per wei of /// the gas token), anchored by `registry`'s USD anchor tokens. Empty until the first /// derived-data computation completes; tokens with an unconvertible price are skipped. diff --git a/tools/hindsight/src/telemetry.rs b/tools/hindsight/src/telemetry.rs index 398f538e..cb41d29c 100644 --- a/tools/hindsight/src/telemetry.rs +++ b/tools/hindsight/src/telemetry.rs @@ -6,12 +6,13 @@ use std::time::Duration; use actix_web::{web, App, HttpResponse, HttpServer, Responder}; +use alloy::primitives::{Address, U256}; use metrics::{counter, describe_counter, describe_histogram, histogram, Unit}; use metrics_exporter_prometheus::{PrometheusBuilder, PrometheusHandle}; use tracing::{error, info, warn}; use crate::{ - decoder::Registry, + decoder::{Registry, RevertCause, TradeStatus}, resolve::{render_route, Outcome, RangeComparison, StateResult, Verdict}, usd::Prices, }; @@ -30,6 +31,9 @@ const RPC_INDEX_WAIT: &str = "hindsight_rpc_index_wait_seconds"; const SKIPPED_BLOCKS: &str = "hindsight_skipped_blocks_total"; const FEED_REBUILDS: &str = "hindsight_feed_rebuilds_total"; const UNTRACED_TRANSACTIONS: &str = "hindsight_untraced_transactions_total"; +const REVERTED_SWAPS_TOTAL: &str = "hindsight_reverted_swaps_total"; +const REVERT_FILLABLE_TOTAL: &str = "hindsight_revert_fillable_total"; +const REVERT_MARGIN_BPS: &str = "hindsight_revert_margin_bps"; /// Absolute USD savings beyond which a comparison is logged with full per-trade context, so large /// outliers can be traced and classified (a genuinely large trade vs a token-mispricing artifact @@ -107,8 +111,25 @@ pub(crate) fn outcome_label(verdict: Verdict) -> &'static str { } } +/// Metric label for a reverted swap's cause — a closed, bounded set. `Other`'s free-text detail +/// stays in the JSONL records, never in a label. +pub(crate) fn revert_cause_label(cause: &RevertCause) -> &'static str { + match cause { + RevertCause::SlippageFloor => "slippage_floor", + RevertCause::OutOfGas => "out_of_gas", + RevertCause::Other(_) => "other", + } +} + /// Register metric descriptions with the active recorder. pub(crate) fn describe() { + describe_trade_metrics(); + describe_revert_metrics(); +} + +/// Register descriptions for the per-trade re-solve metrics: savings, slippage, volume, and the +/// monitor's own pacing. +fn describe_trade_metrics() { describe_counter!( TRADES_TOTAL, "Re-solved trades above the dust floor, labeled by venue / solver / chain / outcome / \ @@ -202,9 +223,29 @@ pub(crate) fn describe() { ); } -/// Record a two-state range: the top-of-block (N-1) and back-of-block (N) outcomes, each tagged -/// with a `state` label ("top"/"back"). `prices_top`/`prices_back` are the solver's token-price -/// snapshots at each state, used to value savings in USD; an empty map disables USD for that state. +/// Register descriptions for the reverted-swap metrics. +fn describe_revert_metrics() { + describe_counter!( + REVERTED_SWAPS_TOTAL, + "Reverted Relay swaps seen, labeled by venue / solver / chain / cause \ + (slippage_floor|out_of_gas|other) / decoded (true|false). The decoded=false slice is \ + parser-coverage: reverts whose solver frame or calldata could not be recovered" + ); + describe_counter!( + REVERT_FILLABLE_TOTAL, + "Decoded, solved reverts whose quote is judged against the trader's on-chain floor \ + (min_amount_out), labeled by venue / solver / chain / state (top|back) / fillable \ + (true|false). The avoidance rate is fillable=true over the total at either state" + ); + describe_histogram!( + REVERT_MARGIN_BPS, + Unit::Count, + "Signed bps of (quote - min_amount_out) / min_amount_out for decoded, solved reverts \ + (positive = cleared the floor with room to spare), labeled by venue / solver / chain / \ + state (top|back)" + ); +} +/// Record one re-solved trade — settled or reverted, told apart by `range.status`. pub(crate) fn record_range( range: &RangeComparison, chain: &str, @@ -212,6 +253,49 @@ pub(crate) fn record_range( prices_back: &Prices, registry: &Registry, ) { + match &range.status { + TradeStatus::Settled => { + record_settled_range(range, chain, prices_top, prices_back, registry); + } + TradeStatus::Reverted { cause } => record_reverted_range(range, cause, chain, registry), + } +} + +/// Record a settled trade's two-state range: the top-of-block (N-1) and back-of-block (N) +/// outcomes, each tagged with a `state` label ("top"/"back"). `prices_top`/`prices_back` are the +/// solver's token-price snapshots at each state, used to value savings in USD; an empty map +/// disables USD for that state. +fn record_settled_range( + range: &RangeComparison, + chain: &str, + prices_top: &Prices, + prices_back: &Prices, + registry: &Registry, +) { + // A settled trade always carries these by construction (see `DecodedTrade`'s settled/ + // reverted invariant); the early return is a defensive guard, not the expected path. + let ( + Some(top), + Some(back), + Some(verdict), + Some(token_in), + Some(token_out), + Some(amount_in), + Some(settled_amount_out), + ) = ( + &range.top, + &range.back, + range.verdict, + range.token_in, + range.token_out, + range.amount_in, + range.settled_amount_out, + ) + else { + warn!(tx = %range.tx_hash, "settled range is missing terms or solved states; skipping metrics"); + return; + }; + let labels = MetricLabels { venue: venue_label(&range.venue, registry), solver: solver_label(&range.solver, registry), @@ -225,15 +309,15 @@ pub(crate) fn record_range( // outside the solver's graph) — without the fallback, unsolvable volume would be // systematically undercounted. let volume = prices_top - .value_usd(range.token_out, range.settled_amount_out) - .or_else(|| prices_top.value_usd(range.token_in, range.amount_in)); + .value_usd(token_out, settled_amount_out) + .or_else(|| prices_top.value_usd(token_in, amount_in)); if let Some(volume) = volume { histogram!( VOLUME_USD, "venue" => labels.venue.to_string(), "solver" => labels.solver.to_string(), "chain" => labels.chain.to_string(), - "outcome" => outcome_label(range.verdict).to_string(), + "outcome" => outcome_label(verdict).to_string(), ) .record(volume); } @@ -243,9 +327,10 @@ pub(crate) fn record_range( // notional and is kept. The notional is a per-trade quantity, so both states share this gate. let above_floor = volume.is_none_or(|usd| usd >= MIN_NOTIONAL_USD); - let savings_top = record_state(range, &range.top, "top", &labels, prices_top, above_floor); - record_state(range, &range.back, "back", &labels, prices_back, above_floor); - record_slippage(range, &labels, prices_back); + let settled = (token_out, settled_amount_out); + let savings_top = record_state(range, top, "top", &labels, prices_top, above_floor, settled); + record_state(range, back, "back", &labels, prices_back, above_floor, settled); + record_slippage(range, &labels, prices_back, token_out, verdict); // One structured line per priced comparison, on the headline basis (top-of-block, gross). // Loki ingests pod stdout, so this line feeds the dashboard's top-trades table; keep the @@ -256,10 +341,10 @@ pub(crate) fn record_range( // `route` is last on purpose: it is the only field whose value contains spaces, so a LogQL // regexp can only bound it by end-of-line. Keep it last, or the dashboard's route column // silently swallows every field after it. - if let (Some(savings_usd), Outcome::Solved(solved)) = (savings_top, &range.top.outcome) { + if let (Some(savings_usd), Outcome::Solved(solved)) = (savings_top, &top.outcome) { let priced = |amount| { prices_top - .value_usd(range.token_out, amount) + .value_usd(token_out, amount) .unwrap_or(0.0) }; info!( @@ -267,14 +352,14 @@ pub(crate) fn record_range( block = range.block_number, venue = %range.venue, solver = %range.solver, - token_in = %range.token_in, - token_out = %range.token_out, - verdict = %outcome_label(range.verdict), - algorithm = %algorithm_label(&range.top.outcome), + token_in = %token_in, + token_out = %token_out, + verdict = %outcome_label(verdict), + algorithm = %algorithm_label(&top.outcome), volume_usd = volume.unwrap_or(0.0), - settled_usd = priced(range.settled_amount_out), + settled_usd = priced(settled_amount_out), fynd_usd = priced(solved.amount_out), - quoted_usd = range.quote.as_ref().map_or(0.0, |quote| priced(quote.amount_out)), + quoted_usd = range.declared_quote.map_or(0.0, priced), savings_usd, route = %solved.solved_route.as_deref().map(render_route).unwrap_or_default(), "trade comparison" @@ -282,16 +367,16 @@ pub(crate) fn record_range( } } -/// Record one block-state of a range under a `state` label. Emits the trade counter, and — for a -/// solved state — the gross bps delta, the signed USD savings, and the USD uplift (only when -/// Fynd beats the settled trade; a venue routes elsewhere when Fynd is worse). All highlighted -/// metrics compare gross vs gross, matching the headline verdict. +/// Record one block-state of a settled range under a `state` label. Emits the trade counter, and +/// — for a solved state — the gross bps delta, the signed USD savings, and the USD uplift (only +/// when Fynd beats the settled trade; a venue routes elsewhere when Fynd is worse). All +/// highlighted metrics compare gross vs gross, matching the headline verdict. /// /// A sandwiched state's output was moved by MEV, not by Fynd's own routing, so it skips the /// `SAVINGS_BPS`/`SAVINGS_USD`/`IMPROVEMENT_USD` histograms — the USD histograms carry no /// outcome label, so skipping is the only way to keep the "value of adding Fynd" aggregates /// clean. The USD value is still computed and returned so the per-trade Loki line (in -/// `record_range`) keeps logging. +/// `record_settled_range`) keeps logging. /// /// Returns the signed USD savings it computed, `None` when the state is unsolved or unpriced. fn record_state( @@ -301,7 +386,9 @@ fn record_state( labels: &MetricLabels<'_>, prices: &Prices, above_floor: bool, + settled: (Address, U256), ) -> Option { + let (token_out, settled_amount_out) = settled; let algorithm = algorithm_label(&state.outcome); if above_floor { @@ -337,7 +424,7 @@ fn record_state( let Outcome::Solved(solved) = &state.outcome else { return None; }; - let usd = prices.savings_usd(range.token_out, solved.amount_out, range.settled_amount_out)?; + let usd = prices.savings_usd(token_out, solved.amount_out, settled_amount_out)?; if sandwiched { return Some(usd); } @@ -349,12 +436,9 @@ fn record_state( state = state_label, venue = %range.venue, solver = %range.solver, - token_in = %range.token_in, - token_out = %range.token_out, - amount_in = %range.amount_in, - settled_out = %range.settled_amount_out, + settled_out = %settled_amount_out, fynd_out = %solved.amount_out, - token_out_price = ?prices.get(range.token_out), + token_out_price = ?prices.get(token_out), usd, "USD outlier — inspect for token mispricing vs genuinely large trade" ); @@ -390,11 +474,17 @@ fn record_state( /// Valued at `prices_back`, the state the surplus is realized at. Sandwiched trades are not /// skipped: the comparison is Fynd-quote vs Fynd-re-execution, so the settled trade's MEV does /// not enter it, and block N's pool moves are real either way. -fn record_slippage(range: &RangeComparison, labels: &MetricLabels<'_>, prices: &Prices) { +fn record_slippage( + range: &RangeComparison, + labels: &MetricLabels<'_>, + prices: &Prices, + token_out: Address, + verdict: Verdict, +) { let Some(slippage) = range.slippage else { return; }; - let outcome = outcome_label(range.verdict).to_string(); + let outcome = outcome_label(verdict).to_string(); histogram!( SLIPPAGE_BPS, "venue" => labels.venue.to_string(), @@ -404,11 +494,9 @@ fn record_slippage(range: &RangeComparison, labels: &MetricLabels<'_>, prices: & ) .record(slippage.bps); - let Some(usd) = prices.savings_usd( - range.token_out, - slippage.reexecuted_amount_out, - slippage.quoted_amount_out, - ) else { + let Some(usd) = + prices.savings_usd(token_out, slippage.reexecuted_amount_out, slippage.quoted_amount_out) + else { return; }; histogram!( @@ -429,10 +517,10 @@ fn record_slippage(range: &RangeComparison, labels: &MetricLabels<'_>, prices: & block = range.block_number, venue = %range.venue, solver = %range.solver, - token_out = %range.token_out, + token_out = %token_out, quoted_out = %slippage.quoted_amount_out, reexecuted_out = %slippage.reexecuted_amount_out, - token_out_price = ?prices.get(range.token_out), + token_out_price = ?prices.get(token_out), usd, "positive slippage USD outlier — inspect for token mispricing vs a genuine pool move" ); @@ -446,6 +534,64 @@ fn record_slippage(range: &RangeComparison, labels: &MetricLabels<'_>, prices: & .record(usd); } +/// Record a reverted trade: always the "seen" counter (parser coverage, via the `decoded` label — +/// whether its solver calldata parsed into terms), and — when its terms were known enough to +/// solve — each state's fillable/margin judgment against `min_amount_out`. +fn record_reverted_range( + range: &RangeComparison, + cause: &RevertCause, + chain: &str, + registry: &Registry, +) { + counter!( + REVERTED_SWAPS_TOTAL, + "venue" => venue_label(&range.venue, registry).to_string(), + "solver" => solver_label(&range.solver, registry).to_string(), + "chain" => chain.to_string(), + "cause" => revert_cause_label(cause).to_string(), + "decoded" => if range.token_in.is_some() { "true" } else { "false" }, + ) + .increment(1); + + let labels = MetricLabels { + venue: venue_label(&range.venue, registry), + solver: solver_label(&range.solver, registry), + chain, + }; + if let Some(top) = &range.top { + record_revert_state(top, "top", &labels); + } + if let Some(back) = &range.back { + record_revert_state(back, "back", &labels); + } +} + +/// Record one state of a revert judgment: the fillable counter and the signed margin histogram, +/// both only when the state was solved (there is nothing to judge otherwise). +fn record_revert_state(state: &StateResult, state_label: &'static str, labels: &MetricLabels<'_>) { + if let Some(fillable) = state.fillable { + counter!( + REVERT_FILLABLE_TOTAL, + "venue" => labels.venue.to_string(), + "solver" => labels.solver.to_string(), + "chain" => labels.chain.to_string(), + "state" => state_label, + "fillable" => if fillable { "true" } else { "false" }, + ) + .increment(1); + } + if let Some(margin_bps) = state.margin_bps { + histogram!( + REVERT_MARGIN_BPS, + "venue" => labels.venue.to_string(), + "solver" => labels.solver.to_string(), + "chain" => labels.chain.to_string(), + "state" => state_label, + ) + .record(margin_bps); + } +} + pub(crate) fn record_block_seconds(seconds: f64) { histogram!(BLOCK_SECONDS).record(seconds); } @@ -551,6 +697,7 @@ fn configure_buckets( .set_buckets_for_metric(Matcher::Full(IMPROVEMENT_USD.into()), SAVINGS_USD_BUCKETS)? .set_buckets_for_metric(Matcher::Full(SLIPPAGE_BPS.into()), SAVINGS_BPS_BUCKETS)? .set_buckets_for_metric(Matcher::Full(SLIPPAGE_USD.into()), SAVINGS_USD_BUCKETS)? + .set_buckets_for_metric(Matcher::Full(REVERT_MARGIN_BPS.into()), SAVINGS_BPS_BUCKETS)? .set_buckets_for_metric( Matcher::Full(POSITIVE_SLIPPAGE_USD.into()), POSITIVE_SLIPPAGE_USD_BUCKETS, @@ -565,6 +712,7 @@ fn configure_buckets( mod tests { use alloy::primitives::{address, Address, TxHash, U256}; use metrics_exporter_prometheus::PrometheusBuilder; + use tycho_simulation::tycho_common::models::Chain; use super::*; use crate::{ @@ -581,19 +729,74 @@ mod tests { tx_hash: TxHash::default(), block_number: 21_000_000, tx_index: 0, + status: TradeStatus::Settled, venue: "relay".into(), solver: "tycho".into(), solver_source: AttributionSource::TraceMatch, decoder: "sender-netting", sender: Address::ZERO, - token_in: Address::repeat_byte(0x11), - token_out, - amount_in: U256::from(1_000u64), - amount_out: U256::from(settled), + token_in: Some(Address::repeat_byte(0x11)), + token_out: Some(token_out), + amount_in: Some(U256::from(1_000u64)), + amount_out: Some(U256::from(settled)), venue_fee_in: None, venue_fee_out: None, settled_gas: None, - quote: None, + min_amount_out: None, + declared_quote: None, + quote_timestamp: None, + sandwich: None, + } + } + + /// A reverted trade with known terms and a floor, for the unified revert-metrics tests. + fn reverted_trade(cause: RevertCause, min_amount_out: u64) -> DecodedTrade { + DecodedTrade { + tx_hash: TxHash::default(), + block_number: 21_000_000, + tx_index: 0, + status: TradeStatus::Reverted { cause }, + venue: "relay".into(), + solver: "fly".into(), + solver_source: AttributionSource::TraceMatch, + decoder: "reverted", + sender: Address::ZERO, + token_in: Some(Address::repeat_byte(0x11)), + token_out: Some(Address::repeat_byte(0x22)), + amount_in: Some(U256::from(1_000u64)), + amount_out: None, + venue_fee_in: None, + venue_fee_out: None, + settled_gas: None, + min_amount_out: Some(U256::from(min_amount_out)), + declared_quote: None, + quote_timestamp: None, + sandwich: None, + } + } + + /// A reverted trade whose solver calldata did not parse — no terms, nothing to solve. + fn undecoded_reverted_trade(cause: RevertCause) -> DecodedTrade { + DecodedTrade { + tx_hash: TxHash::default(), + block_number: 21_000_000, + tx_index: 1, + status: TradeStatus::Reverted { cause }, + venue: "relay".into(), + solver: "relay".into(), + solver_source: AttributionSource::Fallback, + decoder: "reverted", + sender: Address::ZERO, + token_in: None, + token_out: None, + amount_in: None, + amount_out: None, + venue_fee_in: None, + venue_fee_out: None, + settled_gas: None, + min_amount_out: None, + declared_quote: None, + quote_timestamp: None, sandwich: None, } } @@ -641,9 +844,11 @@ mod tests { let range = build_range( &trade(usdc, 1_000_000_000), &empty_prices(), - solved_by("path_frank_wolfe", 1_010_000_000, 1_005_000_000), - solved_by("path_frank_wolfe", 1_010_000_000, 1_005_000_000), - &Outcome::Unsolvable("x".into()), + Some(( + solved_by("path_frank_wolfe", 1_010_000_000, 1_005_000_000), + solved_by("path_frank_wolfe", 1_010_000_000, 1_005_000_000), + Outcome::Unsolvable("x".into()), + )), ); let mut prices = empty_prices(); prices.insert(usdc, 2e-9); @@ -682,9 +887,11 @@ mod tests { let range = build_range( &trade(Address::repeat_byte(0x22), 1_000), &empty_prices(), - Outcome::Unsolvable("missing token in Tycho".into()), - Outcome::Unsolvable("missing token in Tycho".into()), - &Outcome::Unsolvable("no top-of-block route to re-execute".into()), + Some(( + Outcome::Unsolvable("missing token in Tycho".into()), + Outcome::Unsolvable("missing token in Tycho".into()), + Outcome::Unsolvable("no top-of-block route to re-execute".into()), + )), ); let recorder = PrometheusBuilder::new().build_recorder(); let handle = recorder.handle(); @@ -739,9 +946,11 @@ mod tests { let range = build_range( &trade(usdc, 1_000_000_000), &empty_prices(), - solved(1_010_000_000, 1_005_000_000), - solved(998_000_000, 995_000_000), - &solved(998_000_000, 995_000_000), + Some(( + solved(1_010_000_000, 1_005_000_000), + solved(998_000_000, 995_000_000), + solved(998_000_000, 995_000_000), + )), ); // USDC priced at 2e-9 native-units per ETH-wei (ETH = $2000) anchors ETH→USD. let mut prices = empty_prices(); @@ -794,9 +1003,11 @@ mod tests { let range = build_range( &trade(usdc, 1_000_000_000), &empty_prices(), - solved(1_000_000_000, 995_000_000), - solved(1_005_000_000, 1_000_000_000), - &solved(1_005_000_000, 1_000_000_000), + Some(( + solved(1_000_000_000, 995_000_000), + solved(1_005_000_000, 1_000_000_000), + solved(1_005_000_000, 1_000_000_000), + )), ); let mut prices = empty_prices(); prices.insert(usdc, 2e-9); @@ -837,9 +1048,11 @@ mod tests { let range = build_range( &trade(usdc, 1_000_000_000), &empty_prices(), - solved(1_000_000_000, 995_000_000), - solved(995_000_000, 990_000_000), - &solved(995_000_000, 990_000_000), + Some(( + solved(1_000_000_000, 995_000_000), + solved(995_000_000, 990_000_000), + solved(995_000_000, 990_000_000), + )), ); let mut prices = empty_prices(); prices.insert(usdc, 2e-9); @@ -878,9 +1091,11 @@ mod tests { let range = build_range( &trade(usdc, 1_000_000_000), &empty_prices(), - solved(1_000_000_000, 995_000_000), - solved(1_002_000_000, 997_000_000), - &Outcome::Unsolvable("re-execution failed: no simulation state".into()), + Some(( + solved(1_000_000_000, 995_000_000), + solved(1_002_000_000, 997_000_000), + Outcome::Unsolvable("re-execution failed: no simulation state".into()), + )), ); let mut prices = empty_prices(); prices.insert(usdc, 2e-9); @@ -929,9 +1144,7 @@ mod tests { let range = build_range( &t, &empty_prices(), - solved(1_100, 1_050), - solved(1_100, 1_050), - &solved(1_100, 1_050), + Some((solved(1_100, 1_050), solved(1_100, 1_050), solved(1_100, 1_050))), ); let recorder = PrometheusBuilder::new().build_recorder(); @@ -962,9 +1175,7 @@ mod tests { let range = build_range( &t, &empty_prices(), - solved(1_100, 1_050), - solved(1_100, 1_050), - &solved(1_100, 1_050), + Some((solved(1_100, 1_050), solved(1_100, 1_050), solved(1_100, 1_050))), ); let recorder = PrometheusBuilder::new().build_recorder(); @@ -990,14 +1201,16 @@ mod tests { // unsolvable volume would be systematically undercounted. let usdc = address!("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"); let mut t = trade(Address::repeat_byte(0x42), 1_000); - t.token_in = usdc; - t.amount_in = U256::from(1_000_000_000u64); // 1000 USDC + t.token_in = Some(usdc); + t.amount_in = Some(U256::from(1_000_000_000u64)); // 1000 USDC let range = build_range( &t, &empty_prices(), - Outcome::Unsolvable("no route".into()), - Outcome::Unsolvable("no route".into()), - &Outcome::Unsolvable("no top-of-block route to re-execute".into()), + Some(( + Outcome::Unsolvable("no route".into()), + Outcome::Unsolvable("no route".into()), + Outcome::Unsolvable("no top-of-block route to re-execute".into()), + )), ); let mut prices = empty_prices(); prices.insert(usdc, 2e-9); @@ -1022,9 +1235,11 @@ mod tests { let range = build_range( &trade(Address::repeat_byte(0x22), 1_000), &empty_prices(), - Outcome::Unsolvable("x".into()), - Outcome::Unsolvable("x".into()), - &Outcome::Unsolvable("x".into()), + Some(( + Outcome::Unsolvable("x".into()), + Outcome::Unsolvable("x".into()), + Outcome::Unsolvable("x".into()), + )), ); let recorder = PrometheusBuilder::new().build_recorder(); let handle = recorder.handle(); @@ -1059,11 +1274,13 @@ mod tests { let range = build_range( &sandwiched, &prices, - solved(1_100_000_000, 1_090_000_000), - solved(1_100_000_000, 1_090_000_000), - &solved(1_100_000_000, 1_090_000_000), + Some(( + solved(1_100_000_000, 1_090_000_000), + solved(1_100_000_000, 1_090_000_000), + solved(1_100_000_000, 1_090_000_000), + )), ); - assert_eq!(range.verdict, Verdict::Sandwiched); + assert_eq!(range.verdict, Some(Verdict::Sandwiched)); let recorder = configure_buckets(PrometheusBuilder::new()) .unwrap() @@ -1091,9 +1308,11 @@ mod tests { let range = build_range( &trade(usdc, 1_000_000), &empty_prices(), - solved(1_005_000, 1_005_000), - solved(1_005_000, 1_005_000), - &solved(1_005_000, 1_005_000), + Some(( + solved(1_005_000, 1_005_000), + solved(1_005_000, 1_005_000), + solved(1_005_000, 1_005_000), + )), ); let mut prices = empty_prices(); prices.insert(usdc, 2e-9); @@ -1122,9 +1341,7 @@ mod tests { let range = build_range( &trade(Address::repeat_byte(0x42), 1_000), &empty_prices(), - solved(1_100, 1_050), - solved(1_100, 1_050), - &solved(1_100, 1_050), + Some((solved(1_100, 1_050), solved(1_100, 1_050), solved(1_100, 1_050))), ); let recorder = configure_buckets(PrometheusBuilder::new()) .unwrap() @@ -1143,4 +1360,133 @@ mod tests { assert!(rendered.contains("hindsight_trades_total"), "rendered: {rendered}"); assert!(rendered.contains("hindsight_savings_bps"), "rendered: {rendered}"); } + + #[test] + fn test_revert_cause_labels() { + assert_eq!(revert_cause_label(&RevertCause::SlippageFloor), "slippage_floor"); + assert_eq!(revert_cause_label(&RevertCause::OutOfGas), "out_of_gas"); + assert_eq!( + revert_cause_label(&RevertCause::Other("execution reverted".to_string())), + "other" + ); + } + + #[test] + fn test_record_range_reverted_labels_decoded_and_cause() { + let decoded = + build_range(&reverted_trade(RevertCause::SlippageFloor, 10_000), &empty_prices(), None); + let undecoded = + build_range(&undecoded_reverted_trade(RevertCause::OutOfGas), &empty_prices(), None); + + let recorder = PrometheusBuilder::new().build_recorder(); + let handle = recorder.handle(); + metrics::with_local_recorder(&recorder, || { + record_range( + &decoded, + "base", + &empty_prices(), + &empty_prices(), + &Registry::builtin(Chain::Base).unwrap(), + ); + record_range( + &undecoded, + "base", + &empty_prices(), + &empty_prices(), + &Registry::builtin(Chain::Base).unwrap(), + ); + }); + let rendered = handle.render(); + + assert!(rendered.contains("cause=\"slippage_floor\""), "rendered: {rendered}"); + assert!(rendered.contains("cause=\"out_of_gas\""), "rendered: {rendered}"); + assert!(rendered.contains("decoded=\"true\""), "rendered: {rendered}"); + assert!(rendered.contains("decoded=\"false\""), "rendered: {rendered}"); + } + + #[test] + fn test_record_range_reverted_fillable_and_margin() { + let trade = reverted_trade(RevertCause::SlippageFloor, 10_000); + let range = build_range( + &trade, + &empty_prices(), + Some((solved(9_800, 9_700), solved(10_200, 10_100), solved(10_200, 10_100))), + ); + + let recorder = configure_buckets(PrometheusBuilder::new()) + .unwrap() + .build_recorder(); + let handle = recorder.handle(); + metrics::with_local_recorder(&recorder, || { + record_range( + &range, + "base", + &empty_prices(), + &empty_prices(), + &Registry::builtin(Chain::Base).unwrap(), + ); + }); + let rendered = handle.render(); + + assert!(rendered.contains("hindsight_revert_fillable_total"), "rendered: {rendered}"); + assert!(rendered.contains("state=\"top\""), "rendered: {rendered}"); + assert!(rendered.contains("state=\"back\""), "rendered: {rendered}"); + assert!(rendered.contains("fillable=\"false\""), "rendered: {rendered}"); + assert!(rendered.contains("fillable=\"true\""), "rendered: {rendered}"); + assert!(rendered.contains("hindsight_revert_margin_bps_bucket"), "rendered: {rendered}"); + } + + #[test] + fn test_record_range_reverted_unsolved_state_skips_fillable_metrics() { + let trade = reverted_trade(RevertCause::OutOfGas, 10_000); + let range = build_range( + &trade, + &empty_prices(), + Some(( + Outcome::Unsolvable("no route".into()), + Outcome::Unsolvable("no route".into()), + Outcome::Unsolvable("no route".into()), + )), + ); + + let recorder = PrometheusBuilder::new().build_recorder(); + let handle = recorder.handle(); + metrics::with_local_recorder(&recorder, || { + record_range( + &range, + "base", + &empty_prices(), + &empty_prices(), + &Registry::builtin(Chain::Base).unwrap(), + ); + }); + let rendered = handle.render(); + + assert!(!rendered.contains("hindsight_revert_fillable_total"), "rendered: {rendered}"); + assert!(!rendered.contains("hindsight_revert_margin_bps"), "rendered: {rendered}"); + } + + #[test] + fn test_record_range_reverted_with_unknown_terms_still_counted_as_seen() { + // No terms to solve, but the "seen" counter (parser coverage) must still fire. + let trade = undecoded_reverted_trade(RevertCause::Other("execution reverted".to_string())); + let range = build_range(&trade, &empty_prices(), None); + + let recorder = PrometheusBuilder::new().build_recorder(); + let handle = recorder.handle(); + metrics::with_local_recorder(&recorder, || { + record_range( + &range, + "base", + &empty_prices(), + &empty_prices(), + &Registry::builtin(Chain::Base).unwrap(), + ); + }); + let rendered = handle.render(); + + assert!(rendered.contains("hindsight_reverted_swaps_total"), "rendered: {rendered}"); + assert!(rendered.contains("decoded=\"false\""), "rendered: {rendered}"); + assert!(!rendered.contains("hindsight_revert_fillable_total"), "rendered: {rendered}"); + } } diff --git a/tools/hindsight/src/verify/mod.rs b/tools/hindsight/src/verify/mod.rs index c529dedb..91b93328 100644 --- a/tools/hindsight/src/verify/mod.rs +++ b/tools/hindsight/src/verify/mod.rs @@ -17,7 +17,7 @@ use anyhow::Context; use tracing::warn; use crate::{ - decoder::{DecodedTrade, Decoder}, + decoder::{DecodedTrade, Decoder, TradeStatus}, verify::allium::{AlliumClient, AlliumRow}, }; @@ -109,7 +109,12 @@ pub(crate) async fn run( let mut decimals = HashMap::new(); for &block in blocks { let ours = match decoder.decode_block(block).await { - Ok(ours) => ours, + // Allium's `aggregator_trades` only records swaps that settled; a reverted trade has + // no counterpart there to diff against. + Ok(decoded) => decoded + .into_iter() + .filter(|trade| trade.status == TradeStatus::Settled) + .collect::>(), Err(error) => { warn!(block, %error, "failed to decode block; skipping"); continue; @@ -188,6 +193,27 @@ async fn compare_block( } } +/// The settled swap terms a comparison needs, extracted from a `DecodedTrade` up front so the +/// rest of this module can work with plain values rather than re-checking `Option`s everywhere. +/// `run` only ever passes settled trades into `compare_block` (Allium has nothing to diff a +/// revert against), so by the settled/reverted invariant these are always present — but the type +/// system does not know that from a filter, so a missing term is still handled, not unwrapped. +struct SettledTerms { + token_in: Address, + token_out: Address, + amount_in: U256, + amount_out: U256, +} + +fn settled_terms(trade: &DecodedTrade) -> Option { + Some(SettledTerms { + token_in: trade.token_in?, + token_out: trade.token_out?, + amount_in: trade.amount_in?, + amount_out: trade.amount_out?, + }) +} + async fn compare_trade( provider: &P, ours: &DecodedTrade, @@ -195,10 +221,20 @@ async fn compare_trade( tolerance_bps: f64, decimals: &mut HashMap, ) -> TxComparison { + let Some(terms) = settled_terms(ours) else { + return TxComparison { + tx_hash: ours.tx_hash, + status: Status::OursOnly, + detail: "settled trade has no known terms (should have been filtered out earlier)" + .to_string(), + }; + }; + let mut detail = Vec::new(); - let token_ok = tokens_agree(ours, rows, &mut detail); + let token_ok = tokens_agree(&terms, rows, &mut detail); let solver_ok = solver_agrees(ours, rows, &mut detail); - let amount_ok = amounts_agree(provider, ours, rows, tolerance_bps, decimals, &mut detail).await; + let amount_ok = + amounts_agree(provider, &terms, rows, tolerance_bps, decimals, &mut detail).await; let status = if !token_ok { Status::TokenMismatch @@ -215,8 +251,8 @@ async fn compare_trade( /// Allium splits a multi-leg swap into per-leg rows, so check membership in sets rather than /// exact one-to-one matching: our netted `token_in` must appear among the sold tokens and our -/// `token_out` among the bought tokens across all rows for this tx. -fn tokens_agree(ours: &DecodedTrade, rows: &[&AlliumRow], detail: &mut Vec) -> bool { +/// `token_out` among the bought tokens across all rows for a tx. +fn tokens_agree(terms: &SettledTerms, rows: &[&AlliumRow], detail: &mut Vec) -> bool { let sold: HashSet

= rows .iter() .filter_map(|r| { @@ -234,13 +270,13 @@ fn tokens_agree(ours: &DecodedTrade, rows: &[&AlliumRow], detail: &mut Vec( provider: &P, - ours: &DecodedTrade, + terms: &SettledTerms, rows: &[&AlliumRow], tolerance_bps: f64, decimals: &mut HashMap, @@ -281,11 +317,15 @@ async fn amounts_agree( return true; }; - let in_leg = - AmountLeg { token: ours.token_in, ours: ours.amount_in, theirs: sold, field: "amount_in" }; + let in_leg = AmountLeg { + token: terms.token_in, + ours: terms.amount_in, + theirs: sold, + field: "amount_in", + }; let out_leg = AmountLeg { - token: ours.token_out, - ours: ours.amount_out, + token: terms.token_out, + ours: terms.amount_out, theirs: bought, field: "amount_out", }; @@ -418,19 +458,22 @@ mod tests { tx_hash: TxHash::ZERO, block_number: 1, tx_index: 0, + status: crate::decoder::TradeStatus::Settled, venue: "relay".to_string(), solver: solver.to_string(), solver_source: AttributionSource::TraceMatch, decoder: "sender-netting", sender: addr(1), - token_in, - token_out, - amount_in: U256::from(1000), - amount_out: U256::from(2000), + token_in: Some(token_in), + token_out: Some(token_out), + amount_in: Some(U256::from(1000)), + amount_out: Some(U256::from(2000)), venue_fee_in: None, venue_fee_out: None, settled_gas: None, - quote: None, + min_amount_out: None, + declared_quote: None, + quote_timestamp: None, sandwich: None, } } @@ -449,21 +492,33 @@ mod tests { #[test] fn test_token_match_both_present() { let ours = trade(addr(10), addr(11), "1inch"); + let terms = settled_terms(&ours).unwrap(); let allium = row(addr(10), addr(11), "1inch"); let mut detail = Vec::new(); - assert!(tokens_agree(&ours, &[&allium], &mut detail)); + assert!(tokens_agree(&terms, &[&allium], &mut detail)); assert!(detail.is_empty()); } #[test] fn test_token_match_mismatched_tokens() { let ours = trade(addr(10), addr(99), "1inch"); + let terms = settled_terms(&ours).unwrap(); let allium = row(addr(10), addr(11), "1inch"); let mut detail = Vec::new(); - assert!(!tokens_agree(&ours, &[&allium], &mut detail)); + assert!(!tokens_agree(&terms, &[&allium], &mut detail)); assert_eq!(detail.len(), 1); } + #[test] + fn test_settled_terms_is_none_for_a_reverted_trade() { + let mut ours = trade(addr(10), addr(11), "1inch"); + ours.status = crate::decoder::TradeStatus::Reverted { + cause: crate::decoder::RevertCause::Other("execution reverted".to_string()), + }; + ours.token_in = None; + assert!(settled_terms(&ours).is_none()); + } + #[test] fn test_solver_match_prefix() { let ours = trade(addr(10), addr(11), "uniswap");