` 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");