Skip to content

fix: cashout — switch to Bungee deposit route (fills in seconds, vs unfillable autoRoute)#378

Open
hudsonhrh wants to merge 1 commit into
mainfrom
hudsonhrh/cashout-deposit-route
Open

fix: cashout — switch to Bungee deposit route (fills in seconds, vs unfillable autoRoute)#378
hudsonhrh wants to merge 1 commit into
mainfrom
hudsonhrh/cashout-deposit-route

Conversation

@hudsonhrh
Copy link
Copy Markdown
Member

Why

Two consecutive bridges from production stayed unfilled for 10 minutes and auto-refunded. Investigation found the root cause is structural, not parameter tuning: Bungee Auto's transmitter pool has poor coverage for small-amount requests with destinationPayload, regardless of slippage. Even with a 5% override giving transmitters $0.35 margin on a $7 bridge, no solver took it.

This PR switches to Bungee's deposit route — a vanilla cross-chain USDC bridge with no destination call. Every Bungee solver fulfills these.

Live API probe before/after:

Route Settlement Fill Slippage
autoRoute + destinationPayload + 5% slippage 10 min, then auto-refund 5%
deposit route, no payload ~2 seconds (estimatedTime: 2) ~0.4%

The actual research findings

1. depositRoute was renamed, not deprecated

Bungee renamed result.depositRouteresult.deposit and gated it behind two query flags: enableDepositAddress=true&refundAddress=<addr>. Without those flags, the API only returns autoRoute. We weren't passing them, so we thought the route was gone.

Confirmed live:
```
GET /v1/bungee/quote?...&enableDepositAddress=true&refundAddress=0xa6f4...
→ result.deposit.estimatedTime: 2
→ result.deposit.txData.to:
→ result.deposit.txData.data: 0xa9059cbb... (transfer selector)
→ result.deposit.depositData.address: 0x9140dC9... (per-request unique)
```

2. destinationPayload is a coverage death zone at small sizes

The Bungee transmitter network is open — solvers elect into requests. A request that asks them to (a) front USDC on Base and (b) submit a tx with a 600–800k gas destination call against an unknown receiver is filtered out by most solvers, especially when the spread is <$1. Increasing slippage to 5% didn't fix this because the pool of solvers willing to handle destination payloads at $7 is essentially empty, regardless of margin.

3. The script that "worked" was using the deposit route

The original script/cashout-e2e.mjs (in the contracts repo) used `result.depositRoute` (now `result.deposit`) and triggered the ZKP2P deposit manually as relay owner. That worked because it's a vanilla bridge. The frontend was the one that drifted onto the autoRoute+destinationPayload path.

What changed

File Change
CashOutService.js Rewritten around the deposit route. New getDepositRouteQuote passes enableDepositAddress=true&refundAddress=<user>. Returns a pre-built USDC.transfer(depositAddr, amount) tx — user just signs it. Removed BungeeInbox plumbing, the slippage override, the BUNGEE_INBOX_ADDRESS constant, the Inbox ABI. New notifyBackend helper that POSTs the cashout intent for backend fulfillment.
CashOutModal.jsx Single tx for both EOA (signer.sendTransaction) and passkey (PasskeyAccount.execute(), no batch needed). Progress bar collapsed to 4 steps (Register / Quote / Sign / Bridge). Fee estimate updated: ~0.5% bridge + 2% P2P = user receives ~97% of input as fiat. Final state copy explains the bridge fills in seconds and fiat shows up within ~5 min.

Tradeoff: ZKP2P deposit creation moves off-chain

The whole point of destinationPayload was to deliver USDC and create the ZKP2P deposit atomically in one cross-chain hop. Without it, USDC arrives at the relay and someone needs to call CashOutRelay.createDepositFromBalance(params) to convert it into a P2P deposit owned by the user.

createDepositFromBalance is currently owner-only on the relay. Three options for triggering it:

  1. Backend bot (recommended — separate PR): Cloudflare worker holding the relay owner key, polls relay USDC balance, fulfills pending intents. The frontend already POSTs intents to NEXT_PUBLIC_CASHOUT_INTENT_ENDPOINT (no-op when unset).
  2. Manual admin script (works today): the existing script/cashout-e2e.mjs in the contracts repo does this. Frontend logs the intent JSON; admin runs the script with those params.
  3. Permissionless via signed intent (further out): contract upgrade for EIP-1271 sig verification on createDepositFromBalance. Trustless but more work.

For this PR: option 2 is the immediate path (one user, one admin trigger per bridge). Option 1 is the next step.

Test plan

  • yarn build clean
  • EOA on Arb, $5 cashout → wallet popup with one tx (USDC.transfer to a Bungee deposit address) → bridge fills within seconds → admin runs cashout-e2e.mjs with logged intent → ZKP2P deposit created → Venmo arrives within minutes
  • Passkey on Arb, same flow, single biometric prompt
  • Cancel in wallet → friendly error message
  • No console spam (no autoRoute polling = no CORS / 429)
  • Confirm intent JSON is logged to console when no backend env var set

Sources

🤖 Generated with Claude Code

…osit route

Two consecutive bridges from production stayed unfilled for 10 minutes and
auto-refunded. Investigation found the root cause is structural, not parameter
tuning: Bungee Auto's transmitter pool has poor coverage for small-amount
requests with `destinationPayload`, regardless of slippage. Even with a 5%
override giving transmitters $0.35 margin on a $7 bridge, no solver took it.

Switching to Bungee's `deposit` route, which is a vanilla cross-chain USDC
bridge with no destination call. Every Bungee solver fulfills these — typical
settlement is ~2 seconds.

What changed:
- `CashOutService.js`: rewritten around `deposit` route. Quote with
  `enableDepositAddress=true&refundAddress=<user>` (the flags were the missing
  piece — without them the API only returns autoRoute). The route returns a
  pre-built `USDC.transfer(depositAddress, amount)` tx; the user just signs it.
  Removed all the Bungee Inbox createRequest plumbing (`buildRequestFromQuote`,
  `BUNGEE_INBOX_ADDRESS`, the Inbox ABI, the slippage override, etc.) — the
  deposit route doesn't go through the inbox.
- `CashOutModal.jsx`: single tx submission for both EOA and passkey
  (`PasskeyAccount.execute()` for passkey, `signer.sendTransaction()` for EOA),
  no batch needed. Progress bar collapsed to 4 steps. Fee estimate updated to
  reflect ~0.5% bridge slippage + 2% P2P spread (~97% of input as fiat).
- New `notifyBackend` helper that POSTs the cashout intent (depositor, payee
  hash, rate, etc.) to `NEXT_PUBLIC_CASHOUT_INTENT_ENDPOINT` so a backend
  service can call `CashOutRelay.createDepositFromBalance` once USDC arrives at
  the relay. If the env var is unset, the intent is logged for an admin to
  fulfill manually with the existing `script/cashout-e2e.mjs`.

Tradeoff: ZKP2P deposit creation moves off-chain, requiring a backend trigger
(or admin script) to call the relay's `createDepositFromBalance`. The
`destinationPayload` approach was supposed to make it atomic, but in practice
the solver coverage isn't there. Atomicity isn't worth ten minutes of dead
time and an auto-refund.

Build clean (yarn build).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant