CCTP: cross-chain inflows
Most of Corridor’s work never leaves Stellar: anchors settle on the hub, so the common payout path has no bridge in it. Other chains appear in two places only:
- Inflows. A customer already holding USDC on Base or Solana brings it to the hub, so its balance is one balance.
- The one corridor whose licensed partner settles elsewhere. Naira, today.
Why CCTP and not a bridge
Section titled “Why CCTP and not a bridge”CCTP burns USDC on the source chain and mints canonical USDC on the destination. There is no wrapped asset, no bridge liquidity to trust and no second representation of the dollar to reconcile — which matters for a ledger whose whole claim is that it matches the chain.
That is also why these edges exist at all. Corridor settles in one asset on one hub; an inflow that arrived as a wrapped token would be a different asset with a different risk, and the ledger would have to say so.
The edges
Section titled “The edges”{ kind: "cctp.transfer", from: { venue: "base", asset: "USDC" }, to: { venue: "stellar", asset: "USDC" }, env: ["mainnet"] },{ kind: "cctp.transfer", from: { venue: "solana", asset: "USDC" }, to: { venue: "stellar", asset: "USDC" }, env: ["mainnet"] },{ kind: "cctp.transfer", from: { venue: "stellar", asset: "USDC" }, to: { venue: "base", asset: "USDC" }, env: ["mainnet"] },{ kind: "cctp.transfer", from: { venue: "stellar", asset: "USDC" }, to: { venue: "solana", asset: "USDC" }, env: ["mainnet"] },Both directions, mainnet only. The routes they produce:
| Flow | Mainnet route |
|---|---|
| EUR → NGN payout | reserve → cctp.transfer (Stellar → Solana) → partner.payout |
| NGN collection | partner.collect (→ Solana) → cctp.transfer (Solana → Stellar) → ledger.credit |
| USDC inflow from Base | cctp.transfer (Base → Stellar) → ledger.credit |
The testnet stand-in
Section titled “The testnet stand-in”bridge.simulated has the same interface and the same bookings:
- The Stellar side is real. USDC (or XLM on a funded-asset-free testnet) moves between the treasury vault and an escrow vault, carrying the flow’s Corridor Reference as
MEMO_HASH. - The far side is simulated, booked against
sim:${idempotencyKey}and markedsimulated: truein the entry’s metadata, so no run can be mistaken for a real cross-chain movement. - It compensates. On failure it sends the value back on Stellar with the same reference and books
bridge.unfillandbridge.refund.
Both sides go through the usd-peg clearing pair, which nets to zero across the flow — the same shape the real leg will book.
Precision at the boundary
Section titled “Precision at the boundary”USDC is 6 decimal places on Solana and Base, and 7 on Stellar. Corridor converts through rescale:
export const toHubPrecision = (amount: bigint) => rescale(amount, 6, 7);export const toSpokePrecision = (amount: bigint) => rescale(amount, 7, 6);Going up is exact. Going down, an amount carrying a seventh decimal throws rather than rounding it away, so a stroop of dust surfaces as an error instead of disappearing (ADR 0012).
When this goes away
Section titled “When this goes away”No corridor needs this today: naira has a partner that settles in USDC on Stellar, so OFF_STELLAR_PAYOUT_CURRENCIES is empty and every payout stays on the hub. The list is what decides it, and routing a currency over a bridge again is one line:
export const OFF_STELLAR_PAYOUT_CURRENCIES: readonly Fiat[] = ["NGN"];Inflows from Base and Solana stay useful regardless, for customers whose balances start somewhere else.
How receiving works
Section titled “How receiving works”Three recorded steps with a wait in the middle, which is why it is a saga leg rather than a call.
- The instruction.
depositInstruction({ recipient, network })returns the fields a sender’s wallet needs: the destination domain (27),mintRecipient,destinationCaller, andhookData. Corridor does not burn — on the way in the funds are the sender’s, on the sender’s own chain. - The attestation. Circle observes the burn and signs the message.
Iris.waitForAttestationpolls every five seconds, treats a 404 as “not indexed yet” rather than an error, and doubles its wait on a 429 because going over Circle’s rate limit blocks for five minutes. - The mint.
mint_and_forward(message, attestation)on the forwarder: it verifies Circle’s signature, mints the USDC to itself and pays it on to the account in the hook data, in one transaction. The hook is decoded and the recipient checked before anything is signed.
The two addresses are the forwarder, never the beneficiary
Section titled “The two addresses are the forwarder, never the beneficiary”CCTP treats mintRecipient as a contract address. A burn aimed at a G… account mints nowhere and the funds cannot be recovered. So mintRecipient and destinationCaller are both the CctpForwarder, and the real beneficiary travels in hook data as a strkey:
bytes 0–23 zerobytes 24–27 hook version (uint32 big-endian)bytes 28–31 strkey length in bytes (uint32 big-endian)bytes 32+ the strkey, UTF-8A G… or M… recipient must already hold a USDC trustline or the mint fails — which is exactly what Ramp’s sponsored onboarding guarantees.
Precision
Section titled “Precision”USDC is 6 decimals on Base and Solana and 7 on Stellar, so a burn of 2900000 is 29000000 on the hub. That conversion goes through rescale, which scales up exactly and refuses the direction that could lose value (ADR 0012).
Exactly once
Section titled “Exactly once”The mint record is keyed by the burn transaction, not by the saga step: one burn can only be minted once, and two sagas started over the same burn would otherwise each mint and each book. The hash is written down the moment it is submitted, before the wait for settlement, so a crash in between is recoverable — and a recorded mint that never settled is re-sent, because the message’s nonce is still unspent.
What is verified
Section titled “What is verified”Domain ids, the Stellar contract ids, the hook layout, the Iris endpoint shape and minFinalityThreshold: 2000 for a standard transfer all come from Circle’s own references, checked on 22 October 2026. Verify a contract id against those pages again before a mainnet deployment uses it.