Skip to content

feat(bridge): Circle CCTP as a second route for USDC - #130

Closed
Vasanthdev2004 wants to merge 2 commits into
feat/bridge-buyfrom
feat/bridge-cctp
Closed

Vasanthdev2004 wants to merge 2 commits into
feat/bridge-buyfrom
feat/bridge-cctp

Conversation

@Vasanthdev2004

Copy link
Copy Markdown
Collaborator

Phase 5 of the bridge update. Stacked on #126 (bridge, then buy); the base branch retargets as the lower PRs merge.

What it does

USDC to USDC into Base or Arc is now quoted through Circle CCTP v2 as well as Relay. Both quotes are requested together and the panel offers whichever delivers more. Every other route is Relay alone, unchanged. Relay stays the primary provider.

  • One wallet transaction: depositForBurnWithHook on Circle's TokenMessengerV2 0x28b5…cf5d. The contract has the same address on Ethereum, Optimism, Arbitrum, Base and Arc.
    • Burns the exact amount for the same wallet, with a zero destination caller.
    • Uses Fast finality, except from Arc, which has only Standard as a source and is final in about half a second.
    • Adds Circle's Forwarding Service hook, so Circle mints on the destination: no second step and no destination gas.
  • Fee: Circle's published rate for the route plus the high forwarding tier, set as maxFee.
    • With forwarding, Circle takes the whole maxFee. Every live forwarded burn checked had feeExecuted equal to maxFee.
    • So the quote shows one fixed fee and the exact amount received, with no slippage.
    • The server refuses a rate above 1% or a forward above $5. The browser applies the same 5% ceiling as for Relay.
  • What Circle decides: only the fee, within those bounds, and progress.
    • The server builds the calldata from pinned values.
    • The browser rebuilds it byte for byte from the wallet, amount and route before the wallet is asked.
  • When Circle is not offered, Relay's quote or error stands exactly as before:
    • any other route (Robinhood has a Circle domain but no live USDC route);
    • more than 10M USDC;
    • Circle's global Fast allowance below the amount;
    • a fee out of bounds;
    • Circle or the source RPC failing.
  • Tracking: by the burn's source transaction.
    • /api/bridge/status?provider=cctp&… reads Circle's message record. It accepts the record only when it names this wallet as sender and recipient, plus this amount, token, route, finality and hook.
    • Forward complete means arrived, with the mint hash. A delay or stalled forward means delayed.
    • A burn the wallet never returned (or a sped-up replacement) is found from the messenger's own DepositForBurn event after the saved source block. It is adopted only when exactly one burn matches.
  • UI: the provider is named in the quote's Route row, fee label, approval notice, footer, transfer steps, disclaimer and the /bridge page.
    • Circle transfers have no Relay link and no Relay transfer ID.
    • History rows show "Circle" where Relay rows link to Relay.

Live cost, 2026-11-10 (USDC, Relay loss / Circle fee)

Route $10 $100 $1,000 $10,000
Base to Arc 0.0226 / 0.0161 0.0406 / 0.0190 0.2206 / 0.0483 2.0207 / 0.3408
Ethereum to Arc 0.0226 / 0.0165 0.0406 / 0.0187 0.2206 / 0.0412 2.0206 / 0.2662
Arc to Base 0.0259 / 0.0542 0.0709 / 0.0542 0.5209 / 0.0542 5.0209 / 0.0542
Arbitrum to Base 0.0219 / 0.0556 0.0309 / 0.0682 0.1209 / 0.1942 1.0209 / 1.4542
  • Circle delivers more on every route into Arc, and from Arc to Base above about $30.
  • Relay stays cheaper from Ethereum, Arbitrum and Optimism into Base.
  • Circle's estimate is about 25 s, against Relay's about 1 s. The rule chooses on the amount delivered only, and the quote shows the arrival estimate.
  • All eight routes are in docs/bridge.md.

Changes

  • lib/bridge/cctp.ts (new, pure, client-safe): pinned contracts and domains, route rules, burn calldata, fee read-back and event match.
  • lib/bridge/cctp-server.ts (new, server-only):
    • fee and Fast-allowance reads from Iris, bounded;
    • gas estimate;
    • quote builder;
    • status parser;
    • a shared 600-per-minute upstream budget and short caches (Circle blocks an IP for 5 minutes past 40 req/s).
  • relay.ts:
    • getBridgeQuote asks both providers on Circle routes and keeps the larger output.
    • readBridgeJson moves to read-json.ts, still re-exported.
  • client.ts:
    • validateCctpQuote;
    • Circle branches for deposit matching;
    • version 4 recovery records with provider and sourceBlock;
    • bridgeStatusUrl, findCctpBurn.
  • approval.ts: Circle's messenger is the only spender allowed besides Relay's depository. Relay records and keys are unchanged.
  • useBridge.ts:
    • allowance reads use the approval's own spender;
    • status and discard use bridgeStatusUrl;
    • Circle recovery scans for the burn;
    • the deposit records the source block.
  • BridgeDialog, BridgeHistory, /bridge page: provider-aware copy. No em dashes.
  • history.ts: rows carry provider.
  • /ui-review-bridge: a "quote from: Circle" toggle for the quote, pending and arrived scenes, and a Circle history row.
  • docs/bridge.md: "Circle CCTP (2026-11-10)".

Not built

  • Manual mint (receiveMessage) when Circle's forward stalls. The transfer shows Delayed and tracking continues.
  • Elysium, which has no Circle domain.
  • Near the crossover amount, a quote can move between providers, and the approval's spender with it. An exact approval for one can then be followed by a second approval step for the other.

Verification

  • New tests:
    • route rules;
    • calldata and every tampered field;
    • event match;
    • fee parser and bounds;
    • quote building, null cases, caching and budget;
    • provider selection (Relay better, Circle better, either failing, both failing);
    • status mapping against Circle's real record of a forwarded burn;
    • status request parsing and the HTTP boundary;
    • v4 storage;
    • approval spender;
    • log search;
    • history provider;
    • UI guards.
  • Writing them caught a real bug: the parser for whole-unit fees built an invalid regex. Fixed.
  • Opt-in live test (RUN_BRIDGE_LIVE_TESTS=1, cctp.live.test.ts):
    • quotes all eight routes from Circle, each passing the browser's own validator;
    • matches a real forwarded Ethereum to Arc burn (0x0095…ce01) against this bridge's own calldata;
    • reads that burn from Circle as delivered, with its mint hash.
  • Full suite: 1494 tests, 28 failures. All 28 match the existing baseline on main; none are new.
  • tsc and eslint are clean.
  • Checked /ui-review-bridge Circle scenes on desktop and phone widths.
  • No wallet signed anything. A small real Circle transfer is still needed before this is described as exercised.

USDC to USDC into Base or Arc is now quoted through Circle's CCTP v2 as
well as Relay, and the panel offers whichever delivers more. Every other
route is Relay alone, unchanged.

- One wallet transaction: depositForBurnWithHook on the pinned
  TokenMessengerV2, Fast finality (Standard from Arc), zero destination
  caller and Circle's forwarding hook, so Circle mints on the destination
  with no second step and no destination gas.
- The fee is Circle's published rate plus the high forwarding tier,
  bounded on the server and re-checked under the 5% ceiling in the
  browser, which rebuilds the burn calldata byte for byte before the
  wallet is asked.
- Status reads Circle's message record by source transaction and only
  accepts one naming this wallet, amount, token, route, finality and
  hook. A burn the wallet never returned is found from the messenger's
  own event after the saved source block.
- Approvals accept Circle's messenger as the only other spender; Relay
  records and keys are unchanged. Recovery records for Circle are
  version 4.
- The dialog, history board and /bridge page name the provider in use.
- Tests cover the calldata, event, fee bounds, quote selection, status
  mapping against a real Circle record, storage and approvals. An opt-in
  live test quotes all eight routes and matches a real forwarded burn.
@coderabbitai

coderabbitai Bot commented Nov 10, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Essentials
  • Run ID: c817e13c-d1ed-4e5a-a862-2b0783f6a9fc

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

@Vasanthdev2004

Copy link
Copy Markdown
Collaborator Author

Closed in favour of #135, which holds the whole bridge update in one PR. The commits from this PR are in it unchanged.

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