fix: cashout — switch to Bungee deposit route (fills in seconds, vs unfillable autoRoute)#378
Open
hudsonhrh wants to merge 1 commit into
Open
fix: cashout — switch to Bungee deposit route (fills in seconds, vs unfillable autoRoute)#378hudsonhrh wants to merge 1 commit into
hudsonhrh wants to merge 1 commit into
Conversation
…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>
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.
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
depositroute — a vanilla cross-chain USDC bridge with no destination call. Every Bungee solver fulfills these.Live API probe before/after:
estimatedTime: 2)The actual research findings
1.
depositRoutewas renamed, not deprecatedBungee renamed
result.depositRoute→result.depositand gated it behind two query flags:enableDepositAddress=true&refundAddress=<addr>. Without those flags, the API only returnsautoRoute. 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.
destinationPayloadis a coverage death zone at small sizesThe 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
CashOutService.jsdepositroute. NewgetDepositRouteQuotepassesenableDepositAddress=true&refundAddress=<user>. Returns a pre-builtUSDC.transfer(depositAddr, amount)tx — user just signs it. Removed BungeeInbox plumbing, the slippage override, theBUNGEE_INBOX_ADDRESSconstant, the Inbox ABI. NewnotifyBackendhelper that POSTs the cashout intent for backend fulfillment.CashOutModal.jsxsigner.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
destinationPayloadwas 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 callCashOutRelay.createDepositFromBalance(params)to convert it into a P2P deposit owned by the user.createDepositFromBalanceis currently owner-only on the relay. Three options for triggering it:NEXT_PUBLIC_CASHOUT_INTENT_ENDPOINT(no-op when unset).script/cashout-e2e.mjsin the contracts repo does this. Frontend logs the intent JSON; admin runs the script with those params.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 buildcleancashout-e2e.mjswith logged intent → ZKP2P deposit created → Venmo arrives within minutesSources
🤖 Generated with Claude Code