Skip to content

Robinhood Chain (4663): token-input swaps through the DexAggregator fail simulation #1102

Description

@Buidlor

Robinhood Chain (4663): token-input swaps through the DexAggregator fail simulation — same trade succeeds through the 0x Settler path in the same minute

Hey team — we run a trading frontend on relay-kit and have been chasing the chain-4663 "Transaction simulation failed" / "Final transaction simulation failed" class for a while. This week we instrumented our failure path to persist the full /quote/v2 request, the full response, and the decoded on-chain evidence for every failed swap, and reproduced the problem in controlled sessions with our own test wallet. Everything below is reproducible from public chain data.

One thing up front, in fairness: while decoding these we also found a bug on our side (a hardcoded deposit gas limit) that explains the broadcast reverts we would otherwise have blamed on you. That's disclosed in §5 and we're fixing it. The simulation failures — your /quote/v2 400s and client-side estimateGas reverts on uncapped gas — are not explained by our bug, and that's what this issue is about.

TL;DR

On chain 4663, swap legs routed through Robinhood's DexAggregator (0x20F6ee51340aDEed01A59B0e65cB3703f3dc860c, swapWithBackendSignature(bytes) = 0x46ec278a) fail simulation when the input is an ERC-20, with empty revert data:

  • same-chain sells: /quote/v2 itself returns 400 Final transaction simulation failed
  • cross-chain sells: the quote succeeds, then the deposit (a transferAndMulticall embedding the same DexAggregator call) reverts in estimateGas

Meanwhile, in the same minute, same wallet, same token, same amount:

  • buys (native-ETH input) through the same swapWithBackendSignature selector succeed, and
  • a same-chain sell routed through the 0x AllowanceHolder → Settler path succeeds.

So a working route for token-input swaps exists on 4663 and your router sometimes picks it — the failures track the DexAggregator selection 1:1.

Environment

  • relay-sdk ^5.2.6, relay-kit-ui ^7.1.6, relay-kit-hooks ^3.0.16
  • Chain 4663 (Robinhood), test wallet 0x2AE4cd5346D327DF52EC964c25400DC78400a2A5
  • Tokens: BAG 0x616bcd920e1e1F354750BBaf2FB3b3fa3B4aAE16, FROGCCACIA 0xc74af3880cb9801e608ab20991ef87631cbbec2c — both stock, verified Robinhood-factory UERC20 contracts (plain ERC-20 + permit, no transfer hooks, no blacklist; we checked the source)

1. Controlled session, 2026-08-09 (token BAG)

One wallet, ~5 minutes, all UTC:

Time Action Route Result
10:59:00 buy, ETH → BAG (same-chain) RelayRouterV3 → DexAggregator 0x46ec278a ✅ success — 0x6b2572dec1b4b13b98c6ade0f56b531c078e631de3aa5917e0b445b29a845afc
10:59:04 buy, ETH → BAG same ✅ success — 0xf103f1f3079f0d826e04748c2467ab07d97b102165e324d842b0d4b246017775 (used 292,209 gas)
11:00:19 → 11:03:43 six sell attempts via SwapWidget, BAG → ETH (Base) and BAG → SOL deposit transferAndMulticall embedding DexAggregator 0x46ec278a ❌ all fail in estimateGas: "Execution reverted for an unknown reason" — no revert data
11:02:43 sell, BAG → ETH same-chain via SwapWidget ApprovalProxy → 0x AllowanceHolder 0x0000000000001fF3684f28c67538d4D072C227340x39b38686a19836ac10162c490e4558e120cbbe5f ✅ success — 0xea8098bc2719cb51c64e5c26c49abb3ca1614e74cd3a896133d193921eb0cc1e (used 296,401 gas)

The sell quotes were fresh (1.7–2.8 s old at failure). Slippage tolerance 20%. The wallet held ~33k BAG bought two minutes earlier.

The decoded diff — this is the whole issue in one table

Both transactions call transferAndMulticall on RelayApprovalProxyV3 0xCcC88a9d1B4ED6b0EABA998850414b24f1c315bE. Amounts differ by one base unit. The only difference is the inner route:

Failing (every cross-chain sell — this exact shape also reverts in estimateGas):

tokens:  [0x616bcd…aAE16]   amounts: [32806196769011064976350]
calls:
  [0] BAG.approve(0x20F6ee51…c860c, 32806…33de)          ← approve DexAggregator
  [1] DexAggregator.swapWithBackendSignature(…)           ← 0x46ec278a  ❌ reverts here
  [2] RelayRouterV3.cleanupErc20sViaCall(…)
refundTo / nftRecipient: 0xf70da97812CB96acDF810712Aa562db8dfA3dbEF (solver)

Succeeding (same-chain sell, 43 seconds later):

tokens:  [0x616bcd…aAE16]   amounts: [32806196769011064976349]
calls:
  [0] BAG.approve(0x0000000000001fF3…C22734, …)           ← approve 0x AllowanceHolder
  [1] AllowanceHolder.exec(0x39b38686…cbbe5f, BAG, …)     ← 0x Settler ✅ works
  [2] RelayRouterV3.cleanupNative(…)
  [3] RelayRouterV3.cleanupNative(…)
refundTo / nftRecipient: 0x2AE4cd53…a2A5 (the user)

And the control in the other direction: the buys at 10:59 invoke the same DexAggregator.swapWithBackendSignature and succeed — with native ETH as input. The failures are specific to ERC-20-input DexAggregator legs.

The estimateGas failure, verbatim

From a stored failure (sell BAG → ETH-Base, 11:03:43, requestId 0x969d5891d6d748e4e35f60fd8901f7ab8532f64a05d8992276e7401ec6a8a466):

Execution reverted for an unknown reason.

Estimate Gas Arguments:
  from:   0x2AE4cd5346D327DF52EC964c25400DC78400a2A5
  to:     0xccc88a9d1b4ed6b0eaba998850414b24f1c315be
  value:  0 ETH
  data:   0xf9e4bab4…  (transferAndMulticall wrapping approve(DexAggregator) +
                        swapWithBackendSignature; full calldata available on request)

No gas cap was supplied on these estimates — this is not a gas-limit artifact (see §5 for the ones that were).


2. Same-chain presentation, 2026-08-08 (token FROGCCACIA)

Same wallet, same shape, other direction of the coin:

  • Selling FROGCCACIA same-chain (4663 → 4663): /quote/v2 itself returned 400 Final transaction simulation failed — repeatedly, ~15:51 UTC.
  • Selling it cross-chain at the same moment quoted fine (the deposits then reverted — that part turned out to be our §5 bug).
  • A cross-chain widget sell of the same token went through at 16:05 UTC.
  • At 16:37 UTC we replayed the identical same-chain and cross-chain /quote/v2 bodies 12× each: 24/24 returned 200.

So your own quote-time simulator intermittently hits the same revert we see in estimateGas — which is consistent: both are simulating the same DexAggregator leg. Request body we replayed (verbatim except the app-fee recipient):

{
  "user": "0x2AE4cd5346D327DF52EC964c25400DC78400a2A5",
  "recipient": "0x2AE4cd5346D327DF52EC964c25400DC78400a2A5",
  "originChainId": 4663, "destinationChainId": 4663,
  "originCurrency": "0xc74af3880cb9801e608ab20991ef87631cbbec2c",
  "destinationCurrency": "0x0000000000000000000000000000000000000000",
  "tradeType": "EXACT_INPUT", "amount": "31015455605969606400517",
  "slippageTolerance": "2000", "overridePriceImpact": true
}

3. What we ruled out

  • The tokens. Both are verified stock UERC20 factory contracts: plain ERC-20 + permit. No hooks, no blacklist, no max-wallet. Source on Blockscout.
  • Stale quotes. Quote age at failure: 1.2–2.8 s across the session.
  • Slippage/price movement. 20% tolerance, and the reverts carry no dataTooMuchSlippage/InsufficientAmountOut on this chain revert with data (we've decoded plenty).
  • Our priority-fee / gas injection — not applicable on EVM estimates with no cap supplied.

4. Where we'd look (from the DexAggregator's verified source)

swapWithBackendSignature gates before any AMM math:

  1. verifyBackendSignature — the signed Swap(...) struct includes address sender bound to caller() and a uint256 deadline; LibRouter reverts ExpiredTransaction() (selector 0x931997cf) when deadline < block.timestamp, and InvalidSignature() on recovery mismatch.
    • If the backend-signed deadline is tight, quote→wallet-confirmation latency (5–15 s for a human) would expire it deterministically — matching "simulation failed, but hammering sometimes works".
    • If the signature binds a specific calling contract, any difference between the contract your quote-sim calls from and the one the real execution calls from (RouterV3 vs ApprovalProxyV3 paths) would fail recovery.
  2. Route selection — your router demonstrably has a working token-input path on 4663 (AllowanceHolder → 0x39b38686…). The failures track DexAggregator selection exactly. Preferring the Settler path for ERC-20 inputs (or falling back to it when the DexAggregator leg fails simulation) would fix this class outright.

5. Full disclosure: the on-chain deposit reverts were ours

Five reverted deposits across these two sessions (0xde3f5f82…, 0x38518d0d…, 0x75d44569… on BAG; 0xae8aa134…, 0xb0b636b9… on FROGCCACIA) all show: gas limit 150,000, used 149,110–149,130, empty revert data — and the internal trace says out of gas inside the DexAggregator call. That 150,000 was a depositGasLimit our integration passed to execute() — sized for plain deposits, fatal for 4663 deposits that embed an origin-side swap (which need ~300k; your quotes leave data.gas unset on these deposit steps). We've removed it and now let estimateGas size deposits, matching your SwapWidget, which never sets depositGasLimit. Two asks that fall out of this anyway:

  • Populate gas on deposit step items in /quote/v2 when the deposit embeds a swap — integrators can't guess that a "deposit" costs 300k.
  • The simulation failures in §1–§2 happened with no gas cap and at your quote-time sim — they are a separate, real issue.

6. Smaller asks

  • Return the revert selector in the 400 body instead of the bare string Final transaction simulation failed — if it's 0x931997cf (ExpiredTransaction) this whole issue resolves in one config change.
  • None of the failing request ids resolve in GET /requests/v3 ({"requests":[]}), e.g. 0x969d5891…, 0xbe7892de… — so we suspect this class is invisible in your own telemetry.

We can reproduce this within minutes on demand and are happy to run any controlled experiment that helps — full quote responses, more request ids, or byte-exact calldata for any of the transactions above.

Thanks!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions