Skip to main content

CCTP Lane (Cross-Chain Transfer Protocol)

What is CCTP?

Circle’s Cross-Chain Transfer Protocol. A protocol for moving USDC from one blockchain to another. The core mechanism is simple: burn → attest → mint.

The bank check analogy

Think of a bank check:
  1. Burn: Go to Bank A and say “issue me a 100check"BankAremoves100 check" → Bank A **removes** 100 from your account and issues the check
  2. Attestation: The check-issuing authority (Circle) stamps it with “this check is legitimate” (notarization)
  3. Mint: Take the notarized check to Bank B → Bank B creates $100 and deposits it into your account
  4. Receipt: Read your Bank B statement and confirm the deposit is really there, for the amount you expected
  5. Finality: Confirmation that “the deposit is finalized and cannot be reversed”
Why the “burn and mint” approach: tokens cannot be transferred directly between chains — each chain is an independent ledger. So the protocol destroys tokens on one side (burn) and creates an equal amount on the other side (mint). Total supply stays unchanged. Circle acts as the trust anchor for this process.

The CCTP path in focus right now

You can read this doc in general terms, but the path currently being validated is the EVM staging CCTP lane. The primary combination is:
  • Source chain: Ethereum Sepolia (11155111)
  • Destination chain: Base Sepolia (84532)
  • Source/Destination domain: 0 -> 6
  • Signer: a single local AWS KMS EVM signer
  • Key principle: both burn and mint use the same signer address as the destination_caller
This combination is useful because it lets you learn “on-chain signing,” “Circle attestation,” “minting on another chain,” “reading the destination-side effect back,” and “finality confirmation” all in one go — breaking down far more v3-style problems in a smaller scope than a CEX-only path.

CCTP Lane in Detail

Where the CEX Lane had 3 nodes, the seeded CCTP Lane has 5: burn → attestation → mint → mint_receive_observe → mint_finality. Attestation is an extra step CEX transfers do not have, and mint_receive_observe confirms the destination address actually received the USDC before finality is waited on. The receipt observation is what makes destination_finalized mean what it says. Finality alone confirms that the mint transaction is durable; it never reads the token or the amount. Only mint_receive_observe asserts that the expected amount of the expected token arrived at the expected address. For the full node config tables, see CCTP lane reference.

Data propagation between nodes


Operations perspective: preflight before live E2E

The current CCTP flow treats “check first” as more important than “run immediately.” The typical sequence looks like this:
  1. The preflight CLI checks signer health, chain ID, RPC capability, contract code, USDC balance, allowance, native gas, pending nonce, and Circle fast allowance.
  2. Only when ready does the operator proceed to a live staging E2E that creates an actual movement.
  3. The live E2E runs the lane — burn -> attestation -> mint -> mint_receive_observe -> mint_finality — end to end, and verifies that artifacts and provider refs are recorded as expected.
Why preflight matters: because burn happens first in CCTP, catching a misconfigured signer, allowance, or gas setting before execution is far cheaper. If a burn succeeds but the mint side is misconfigured, the recovery cost jumps dramatically.

Practical checklist

Node 1: Burn

The node that burns USDC on the source chain. Registered under the key exec.cctp.burn.

depositForBurn() contract call

The smart contract function actually called:

Preflight — 6 steps

Why gas estimation is in preflight: eth_estimateGas is not just about estimating the gas cost. It simulates the transaction. If the transaction would revert, it returns an error. In other words, it is a way to ask “will this transaction succeed?” before spending any gas.

destination_caller and security

The destination_caller is read from node config, must be present, and is validated as a canonical EVM address before the burn can proceed. A missing or invalid value fails the node immediately.
Why destination_caller matters: destination_caller is a security mechanism that restricts who can execute the mint.When you specify a destination_caller at burn time, only that address can call receiveMessage() on the destination chain. Without it? Anyone can mint. That is not necessarily a security problem (the mint recipient is fixed), but our system enforces explicit restriction as a principle.The dangerous scenario: if the destination_caller specified at burn time differs from the actual signer address used for minting → the mint fails permanently → USDC was burned but cannot be minted = funds lost.

bytes32 conversion

EVM addresses are 20 bytes (40 hex characters), but the CCTP contract requires 32 bytes (64 hex characters). So 12 bytes (24 zeros) are prepended as padding:
The address is validated as a canonical EVM address first, then left-padded to 32 bytes.

Prepare & Submit

prepare builds the EVM transaction payload and returns it with signing_required=true. As described in the Executor Protocol, an external Signer signs it, and then submit broadcasts the result via eth_sendRawTransaction. If submit times out, it returns UNKNOWN. In recover, the presence of a burn_tx_hash leads to COMPLETED; its absence leads to FAILED.

Node 2: Attestation

The step in which Circle’s IRIS API verifies the burn transaction and issues a certificate saying “this burn is legitimate.”

What the IRIS API does

Returned data:
  • attestation: the certificate containing Circle’s signature. Must be submitted to the destination chain contract to trigger the mint
  • message (= message_bytes): the original burn event message. Submitted together with the attestation when minting
  • eventNonce: unique message identifier (prevents duplicate mints)

Fast vs Standard

Determined by the min_finality_threshold value: For fast transfers (threshold = 1000), preflight runs one extra check: it queries Circle’s remaining fast-burn allowance and fails the node with FAST_ALLOWANCE_EXHAUSTED if the requested amount exceeds what’s available. Standard transfers (threshold = 2000) skip this check.

Attestation probe

Attestation checking is implemented as a dedicated CCTP probe behind a generic observe step:
Why the probe pattern: attestation checking is not CCTP-specific. Other bridge protocols also need a similar “proof verification” step. That is why the generic observe step calls through a protocol-probe interface, with the CCTP attestation probe looked up by key.

Artifact generation

When attestation completes, an audit artifact is recorded with:
  • artifact_type: attestation
  • content_format: hex
  • the attestation hex itself as the inline content
  • metadata: attestation_hash, message_nonce, source_domain_id
  • the attestation hash as the artifact digest
This is for the audit trail. It allows you to later look up “what attestation was used for this mint?”

Node 3: Mint

The node that mints new USDC on the destination chain. Registered under the key exec.cctp.mint.

receiveMessage() contract call

Both of these values are propagated from Node 2 (Attestation)‘s provider_context.

Preflight: destination_caller consistency check

Mint’s preflight performs the most critical security validation:
Why all three addresses must match:
  1. burn’s destination_caller: hardcoded into the burn TX. Cannot be changed
  2. mint’s node_config.destination_caller: system configuration value
  3. signer’s actual address: the address corresponding to the private key used for signing
If any one of these three differs:
  • The address trying to call mint differs from the caller specified at burn time → the contract rejects it
  • USDC has already been burned but minting fails → funds lost
That is why preflight verifies the consistency of all three values in advance. If this check fails, execution never proceeds to submit.

Recover: idempotency detection

Mint’s recover step is particularly interesting. It handles the scenario of “what if mint is attempted again after already succeeding?”:
  • If a mint_tx_hash is present, recover fetches the receipt. No receipt yet → UNKNOWN; a reverted receipt → FAILED (TX_REVERTED); a successful receipt → COMPLETED.
  • If no mint_tx_hash is present, recover simulates receiveMessage to probe whether the mint already happened:
    • If the simulation succeeds, the mint has not happened yet → FAILED (MINT_NOT_SUBMITTED).
    • If the simulation reverts with an “already processed” error, the mint already succeeded → COMPLETED.
    • Any other revert → FAILED.
Detecting “already processed” is the key to idempotency: the CCTP MessageTransmitter contract reverts if the same message is processed twice. The error message includes phrases like “already processed” or “nonce already used.” Recover detects this and interprets it as “ah, it already succeeded.”This is a classic distributed-systems pattern: inferring success from an error message.

Node 4: Destination Receipt

The step that turns “the mint transaction succeeded” into “the recipient actually holds the USDC.” It re-reads the mint receipt from the destination chain and matches the ERC-20 Transfer it contains against the token, the address, and the amount the movement asked for.

Why the mint result is not enough on its own

Node 3 reports COMPLETED when eth_sendRawTransaction returns a hash and the transaction is mined. That is a statement about the transaction, not about the money. The receipt node is what reads the destination-side effect back: Every one of those failures is fatal, not a retry. A mint that lands the wrong amount at the wrong address does not sit in OBSERVING waiting for a better answer — the node fails and the movement stops. The node’s timeout_policy.observe_seconds is not a receipt deadline by default: the observer only enforces it when MG_OBSERVE_TIMEOUT_ENFORCEMENT_ENABLED is on (it ships off), and enforcement moves the node to UNKNOWN with OBSERVATION_TIMEOUT for recovery to classify — it does not decide that the money did not arrive.

The amount is a floor, not an equality

Fast CCTP (min_finality_threshold=1000) mints amount - fee_executed, and the only bound on fee_executed is the maxFee the burn itself carried — Node 1 encodes max_fee_raw into the depositForBurn calldata, so the ceiling is set on-chain at burn time. An exact-equality check would therefore reject every fast transfer that paid any fee at all. The seeded lane sets amount_match: "at_least" with amount_floor_deduction_raw = max_fee_raw, so the assertion is:
This bounds the loss at the fee ceiling you already approved. It is deliberately not an assertion about what the fee actually was. On success the node records observed_amount as proof with proof_source: destination_chain — evidence read from the destination chain rather than from the transaction QTG submitted.

Node 5: Finality Observe

The final step that confirms the mint transaction has received enough confirmations.

Why you still need to wait after mint succeeds

Reorg (chain reorganization) risk: recent blocks on a blockchain are not yet “final.” Depending on network conditions, the most recent few blocks can be reorganized (reorged). This means the block containing the mint TX could disappear.That is why you need to verify that “a sufficient number of subsequent blocks have accumulated” — this is finality.

Confirmation check logic

The finality probe reads the mint transaction receipt and the current chain head, then computes:
If the receipt isn’t found yet, confirmations is 0 (not finalized). As long as confirmations stay below the required threshold the node remains OBSERVING; once they reach it, the node resolves COMPLETED. The required number of confirmations varies by chain: confirmations_required is set in node_config, so it can be adjusted to match chain characteristics when configuring a route.

Security summary

The scenarios below describe how funds can be lost in CCTP.

Signed-recipient invariant (validated == signed)

On top of the destination_caller checks, CCTP burn/mint are subject to QTG’s cross-lane signed-recipient invariant: the recipient and source addresses re-derived from the actual signed transaction must equal the allowlist-validated intent — checked both at prepare time (intent-only resolvers) and at submit/broadcast time (the raw signed legacy tx is RLP-decoded and re-anchored). CCTP is a strict source + destination action (cctp_burn / cctp_mint), so both ends are validated. A mismatch is fail-closed — a fatal movement error drives the node to FAILED instead of broadcasting:
  • SIGNED_RECIPIENT_MISMATCH — signed recipient ≠ validated destination authority.
  • SIGNED_SOURCE_MISMATCH — signed source ≠ validated source authority.
See Bridge Lane → Signed-recipient invariant for the shared model.

Scenario 1: destination_caller mismatch

Prevention:
  • Preflight triple-validates: signer_address == destination_caller == caller specified at burn
  • Signer retirement refuses while a committed nonterminal node still references the signer — matched by signer key, and by signer-address snapshot where that is recoverable — so an in-flight CCTP movement normally blocks retirement of the signer it is using. The scan reads committed rows, so treat it as a gate against ordinary operator sequencing, not as a serialization guarantee against a movement being created concurrently. Rotation is not this gate at all: it removes the old signer from binding resolution for newly created movements, while nodes already created keep the binding stamped on them. Both operations are part of the QTG Pro signer lifecycle surface

Scenario 2: Mint attempted without attestation

Minting without an attestation is impossible. The MessageTransmitter contract verifies Circle’s signature, so this is guaranteed at the protocol level.

Scenario 3: Burn succeeds + submit timeout

If the burn TX was sent but submit times out, it returns UNKNOWN rather than failing. Recover then sees that burn_tx_hash is present and resolves to COMPLETED. Since the burn TX is already on-chain, the attestation → mint process can continue.

Scenario 4: Duplicate mint attempt

Attempting to mint with the same attestation twice? → The contract reverts with “already processed.” Recover detects this error and resolves to COMPLETED (applying the Executor Protocol’s idempotency principle).

CCTP Lane architecture summary


  • Bridge Lane Overview — shared bridge family pattern and trust-anchor comparison
  • CCIP Lane — Chainlink CCIP cross-chain transfer (DON + Risk Management Network)
  • LayerZero Lane — Stargate / LayerZero v2 OFT transfer
  • Executor Protocol — Executor common interface and the prepare/submit separation design
  • CEX Lane — CEX exchange-to-exchange transfer (3 nodes, simpler)
  • State Machine — UNKNOWN state handling and the recover flow