feat(hindsight): decode Relay swaps from solver calldata - #388
Open
kayibal wants to merge 5 commits into
Open
Conversation
Add SwapIntent (token_in/token_out/amount_in/min_amount_out, plus an optional declared quote) recovered from the settling solver frame's own calldata: Fly's packed LibRouter layout, KyberSwap's ABI-decoded swap params plus its clientData quote, and ParaSwap's hint-located word triple. DecodedTrade and RangeComparison carry the recovered intent's three calldata-declared terms (min_amount_out, declared_quote, quote_timestamp) as flattened fields, next to the netted amounts, which stay authoritative for what settled. When a netting-based decoder's flow disagrees with the intent on token_in/token_out/amount_in, that is logged (tracing::warn!) as diagnostic signal rather than surfaced as a new metric. Replaces the narrower embedded_quote/SolverQuote mechanism, which only read a solver's off-chain quote and required netting to succeed first. zeroex.rs is removed (0x still matches via the address book; it has no calldata to recover terms from). Pre-commit hook not run: cargo nextest run --workspace --bin fynd SIGABRTs on this machine on an unrelated fynd-core test (a conda/cc PATH issue breaking Rust panic unwinding, tracked separately). Verified manually instead: cargo check --workspace --all-features, cargo clippy --locked --workspace --all-targets --all-features -- -D warnings, cargo nextest run -p hindsight (260 passed), cargo +nightly fmt --check, and RUSTDOCFLAGS="-D warnings" cargo doc --no-deps -p hindsight -p fynd-core -p fynd-rpc-types -p fynd-rpc -p fynd-client — all clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
kayibal
force-pushed
the
feat/hindsight-calldata-decoding
branch
from
July 31, 2026 13:44
495a7c5 to
cc743ea
Compare
Add RelayCalldata, a calldata-primary TradeDecoder registered ahead of RelayNetting: it reads token_in/token_out/amount_in from the settling solver frame's SwapIntent and recovers the settled amount_out as the gross amount of token_out received by the output recipient the same calldata declares (SolverKnowledge::output_recipient, implemented for Fly and KyberSwap) -- the one field calldata can never carry. Guards against a mis-attributed recipient receipt: the recovered output must clear the intent's on-chain floor, and any declared quote must sit within plausible_quote's band of it; either failure falls through to RelayNetting. Neither amount needs a fee adjustment -- Relay pays its input-side fee to the collector before forwarding into the solver call, so the intent's amount_in is already post-fee, and its output-side fee is taken after delivery, so the recipient's receipt is already gross. DecodeContext gains the trace root (CallFrame) so a decoder can walk it directly, and TransferLedger gains received_by_address for the recipient-anchored query. RelayNetting stays registered as the fallback for solvers RelayCalldata cannot parse (0x Settler) and transactions with no solver frame at all -- first-wins chain unchanged. See .claude/plans/calldata-first-decoding.md for the empirics behind the ordering. Pre-commit hook not run: cargo nextest run --workspace --bin fynd SIGABRTs on this machine on an unrelated fynd-core test (a conda/cc PATH issue breaking Rust panic unwinding, tracked separately). Verified manually instead: cargo check --workspace --all-features, cargo clippy --locked --workspace --all-targets --all-features -- -D warnings, cargo nextest run -p hindsight (274 passed), cargo +nightly fmt --check, and RUSTDOCFLAGS="-D warnings" cargo doc --no-deps -p hindsight -p fynd-core -p fynd-rpc-types -p fynd-rpc -p fynd-client -- all clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Update the decoder table, the venues/solvers module docs, and the architecture diagram for RelayCalldata + RelayNetting (the latter staying registered as the fallback for solvers RelayCalldata cannot parse) and the output_recipient capability. Describes DecodedTrade's calldata-declared terms as the flattened min_amount_out / declared_quote / quote_timestamp fields they are, not a nested SwapIntent. Pre-commit hook not run (see prior commits: cargo nextest run --workspace --bin fynd SIGABRTs on an unrelated fynd-core test on this machine). Docs-only change; no code checks apply. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
kayibal
force-pushed
the
feat/hindsight-calldata-decoding
branch
from
July 31, 2026 14:31
cc743ea to
0fb420d
Compare
0x is the dominant Relay solver on Base (50-90% of relay flow depending on window, 0x-solved), and every one of its trades previously fell through to RelayNetting (settled) or was counted-but-not-judged (a reverted trade with no terms parser). solvers/zeroex.rs recovers token_in/token_out/amount_in/min_amount_out (and, when declared, 0x's own off-chain quote) from the settling frame's own calldata, the same way fly.rs and kyberswap.rs already do -- flipping 0x-solved relay trades from the netting fallback to the calldata-primary path (RelayCalldata), and giving reverted 0x trades a real SwapIntent to judge against min_amount_out for the first time. Verified against a live 61-transaction Base sample (settled and reverted, drawn from a running monitor instance) and cross-checked against 0x-settler's published source (github.com/0xProject/0x-settler): every transaction sampled enters through AllowanceHolder.exec(operator, token, amount, target, data) before reaching Settler's own execute(AllowedSlippage, actions, zid) -- 100% AllowanceHolder-wrapped, 0% bare Settler entry. exec's own token/amount parameters are the input side; execute's AllowedSlippage struct (recipient, buyToken, minAmountOut) is the output side and the payout address RelayCalldata already anchors on for Fly/KyberSwap. For two settled trades in the sample, the decoded token/amount matched the netted token_in/amount_in hindsight already recorded exactly, and buyToken matched token_out exactly -- as good a validation as an independent ground truth gets. The actions array's POSITIVE_SLIPPAGE(recipient, token, expectedAmount, maxBps), when present, restores the declared-quote capability the now-deleted zeroex.rs (dead after the SwapIntent merge) used to scavenge a different way; every selector used (exec 0x2213bc0b, execute 0x1fff991f, POSITIVE_SLIPPAGE 0x34ee90ca) is computed from its signature and asserted against in a test. A bare Settler entry never occurred in the sample and 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 would be a guess); swap_intent declines for that shape rather than guessing, while output_recipient -- which only needs AllowedSlippage, present either way -- still resolves. minAmountOut is passed through as-is including a legitimate zero (Settler's per-action slippage checks can leave the top-level floor unset); the existing fillable/margin judgment already treats a zero floor sanely (trivially fillable, no margin to compute), so no special-casing was added. Manual verification (pre-commit hook not run -- cargo nextest run --workspace SIGABRTs on an unrelated fynd-core sim_guard test on this machine, a known conda/cc PATH issue breaking panic unwinding): - cargo check --workspace --all-features: pass - cargo clippy --locked --workspace --all-targets --all-features -- -D warnings: pass - cargo nextest run -p hindsight --all-features: 284 passed, 1 skipped - cargo +nightly fmt --all -- --check: pass - RUSTDOCFLAGS="-D warnings" cargo doc for hindsight, fynd-core, fynd-rpc-types, fynd-rpc, fynd-client: pass Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add zeroex.rs to the solvers/ row alongside fly.rs and kyberswap.rs. Docs-only change; no code checks apply. Pre-commit hook not run (see prior commit: cargo nextest run --workspace SIGABRTs on an unrelated fynd-core test on this machine). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a calldata-primary decoder for Relay swaps, ahead of the existing netting decoder:
token_in/token_out/amount_in/min_amount_out, plus an optional declared quote) recovered from the settling solver frame's own calldata — Fly's packedLibRouterlayout, KyberSwap's ABI-decodedswapparams plus itsclientDataquote, and ParaSwap's hint-located word triple. Replaces the narrowerembedded_quote/SolverQuotemechanism (only read a quote, needed netting to succeed first). Also restoreszeroex.rs: 0x's Settler wraps itsexecute(AllowedSlippage, actions, zid)entry in anAllowanceHolder.exec(operator, token, amount, target, data)call — a live 61-transaction Base sample (settled and reverted) is 100%AllowanceHolder-wrapped, soswap_intentreadstoken_in/amount_infrom the wrapper andtoken_out/min_amount_outfromAllowedSlippage, plus the declared quote from thePOSITIVE_SLIPPAGEaction when present. A hypothetical bare-Settler entry (never observed in the sample) has no reliabletoken_insource, soswap_intentdeclines rather than guesses for that shape.TradeDecoderregistered ahead ofRelayNetting. It readstoken_in/token_out/amount_infrom the intent and recovers the settledamount_outas the gross amount oftoken_outreceived by the output recipient the calldata declares (SolverKnowledge::output_recipient, implemented for Fly, KyberSwap, and 0x) — the one field calldata can never carry. Guards: the recovered output must clear the intent's on-chain floor, and any declared quote must sit withinplausible_quote's band of it; either failure falls through toRelayNetting. Neither amount needs a fee adjustment: Relay pays its input-side fee to the collector before forwarding into the solver call (soamount_inis already post-fee), and its output-side fee is taken after delivery (so the recipient's receipt is already gross).RelayCalldata's floor/plausibility guards reject a bad decode — first-wins chain unchanged. 0x, the dominant Relay solver on Base (50-90% of relay flow depending on window), now flows throughRelayCalldataviazeroex.rs'soutput_recipientrather than falling back to netting.Empirics (see
.claude/plans/calldata-first-decoding.md): 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.Test plan
cargo check --workspace --all-featurescargo clippy --locked --workspace --all-targets --all-features -- -D warningscargo nextest run -p hindsight(274 passed, 1 skipped)cargo +nightly fmt --checkRUSTDOCFLAGS="-D warnings" cargo doc --no-deps -p hindsight -p fynd-core -p fynd-rpc-types -p fynd-rpc -p fynd-clientPre-commit hook (
check.sh) was not used for the commits on this branch: itscargo nextest run --workspace --bin fyndstep SIGABRTs on this machine on an unrelatedfynd-coretest (sim_guard::tests::test_get_amount_out_guarded_converts_panic_to_error, aconda/ccPATH issue breaking Rust panic unwinding — environmental, not caused by this change). Verified manually instead; see the commit bodies.🤖 Generated with Claude Code