CCTP와 USDC 크로스체인 결제
도입
CCTP를 bridge 목록의 하나로만 보면 중요한 차이를 놓친다. Circle의 CCTP는 source chain에서 USDC를 burn하고 destination chain에서 native USDC를 mint하는 방식이다. 일반 lock-and-mint bridge처럼 wrapped token을 발행하거나 liquidity pool에서 선지급하는 모델과 다르다.
제품 관점에서 핵심은 "한 번의 전송"처럼 보이는 사용자 경험이 실제로는 burn, finality, attestation, destination mint, fee/allowance 확인, 재시도 상태로 나뉜다는 점이다. 결제 시스템은 이 중간 상태를 숨기지 말고 사용자가 이해할 수 있는 언어로 표현해야 한다.
이 강의는 CCTP의 burn/mint 모델을 상태머신으로 만들고, Fast Transfer와 Standard Transfer를 제품 정책으로 나누는 법을 다룬다. 2026-05-14 기준 Circle 문서에서는 CCTP V2가 Fast Transfer, Hooks, 개선된 API를 포함하며, CCTP V1은 2026년 7월부터 10개월에 걸쳐 phase-out될 예정이라고 안내한다. 새 설계는 V2 문서를 기준으로 둔다.
여기서는 USDC 결제 제품 안에서 CCTP 상태를 어떻게 보여줄지에 집중한다. 브릿지와의 신뢰 모델 비교는 CCTP와 브릿지 신뢰 모델, 구현 연습은 CCTP 시뮬레이터 랩에서 이어진다.
학습 목표
- CCTP의 burn/mint와 브릿지 락업 모델을 구분한다.
- 메시지 attestation 지연을 결제 UX에 반영한다.
- Fast Transfer와 Standard Transfer의 finality, fee, allowance 차이를 제품 정책으로 분리한다.
개념 설명
핵심 개념
- CCTP의 burn/mint와 브릿지 락업 모델을 구분한다.
- 메시지 attestation 지연을 결제 UX에 반영한다.
검증 지점
- 발행/상환과 결제 상태가 분리되는가
- 이벤트와 내부 원장이 대조되는가
- burn/mint 모델을 설명한다.
실습 산출물
- CCTP 상태와 증거 정리하기
- Fast/Standard Transfer 정책 작성하기
- 페그·유동성 위험이 표시되는가
1. CCTP는 native USDC burn-and-mint 흐름이다
Circle 문서는 CCTP를 native USDC를 여러 블록체인 사이에서 이동시키는 permissionless onchain utility로 설명한다. 사용자는 source chain에서 USDC를 burn하고, Circle Attestation Service가 burn message를 확인해 서명한 뒤, destination chain에서 그 attestation을 제출해 USDC를 mint한다.
이 흐름에서 "USDC가 이동했다"는 말은 정확히는 source에서 burn되고 destination에서 새로 mint됐다는 뜻이다. 이 차이 때문에 destination asset은 wrapped token이 아니라 native USDC가 된다. 대신 Circle attestation, supported domain, destination contract, finality 정책을 제품 설계에 넣어야 한다.
2. CCTP와 bridge의 신뢰 경계가 다르다
| 항목 | CCTP burn/mint | Lock-and-mint bridge | Liquidity bridge |
|---|---|---|---|
| destination asset | native USDC | wrapped/bridged token | native 또는 wrapped |
| liquidity pool 필요 | 없음 | bridge custody 필요 | 필요 |
| 핵심 신뢰 | Circle attestation, supported domain | bridge contract/custodian | LP/solver/liquidity |
| 실패 상태 | burn 후 attestation/mint 지연 | bridge exploit, wrapped depeg | liquidity 부족, fee 변동 |
| 사용자 설명 | "USDC를 소각 후 다른 체인에서 새로 발행" | "예치 후 IOU 수령" | "유동성 제공자가 선지급" |
이 표를 제품 문구로 바꾸면 bridge와 CCTP를 같은 문장으로 설명하면 안 된다. CCTP는 liquidity pool 부족을 걱정하는 모델이 아니라 attestation, finality, destination mint 상태를 관리하는 모델이다. 반대로 lock-and-mint bridge는 wrapped asset과 custody risk를 설명해야 한다.
3. Fast Transfer와 Standard Transfer를 정책으로 나눈다
CCTP V2에서는 속도와 finality 기준을 설계자가 선택해야 한다. Circle 문서 기준으로 Fast Transfer는 source chain burn이 hard finality에 도달하기 전에 더 빠르게 attestation을 받을 수 있지만 Fast Transfer allowance와 fee가 걸린다. Standard Transfer는 더 느리지만 hard finality 기반으로 간다.
| 항목 | Fast Transfer | Standard Transfer |
|---|---|---|
| 제품 목표 | 빠른 사용자 경험 | 비용과 finality 안정성 우선 |
| attestation 기준 | soft/confirmed finality 후 빠른 attestation | hard/finalized finality 후 attestation |
| allowance | Fast Transfer allowance를 소비 | allowance를 소비하지 않음 |
| fee | Fast Transfer fee가 route별로 달라질 수 있음 | Circle 문서의 Standard Transfer fee 정책 확인 필요 |
| 실패/대기 UX | allowance 부족, fee 변동, soft finality 리스크 | 긴 대기 시간, finality 지연 |
| 사용자 문구 | "빠른 전송, 수수료와 용량 제한 가능" | "느리지만 finality 확인 후 진행" |
Fast Transfer allowance가 부족하면 기다리거나 Standard Transfer로 전환해야 한다. 따라서 checkout 제품은 "cross-chain 전송 중" 하나가 아니라 AllowanceInsufficient, AttestationPending, MintSubmitted 같은 상태를 가져야 한다.
4. 제품 상태는 source와 destination을 모두 저장한다
| 상태 | 사용자 설명 | 시스템 처리 |
|---|---|---|
BurnSubmitted | source chain에서 전송 시작 | source tx 모니터링 |
BurnObserved | burn message가 관찰됨 | message hash 저장 |
FinalityPending | finality 기준 도달 대기 | Fast/Standard 정책 적용 |
AttestationPending | Circle attestation 대기 | API polling, timeout policy |
AttestationReady | destination mint 증명 준비 | signed attestation 저장 |
MintSubmitted | destination mint 제출 | confirmation 대기 |
Minted | destination USDC 수령 | 완료 |
AllowanceInsufficient | Fast Transfer 용량 부족 | 대기 또는 Standard 전환 |
Delayed | 중간 단계 지연 | retry/manual support |
상태마다 저장할 데이터도 다르다. Source tx hash, source domain ID, destination domain ID, burn message hash, attestation response, destination tx hash, fee, allowance snapshot, finality threshold를 분리해서 저장해야 한다.
5. 개발 체크리스트
- source/destination domain ID를 정확히 매핑한다.
- source와 destination의 token messenger/message transmitter 주소를 확인한다.
- 같은 burn message가 double mint되지 않도록 message hash를 저장한다.
- attestation API 장애를 제품 상태로 표현한다.
- fast transfer를 쓰는 경우 fee, finality, liquidity tradeoff를 문서화한다.
- CCTP와 일반 bridge를 혼동하지 않도록 UI copy를 분리한다.
- CCTP V1 legacy와 V2 contract/API 차이를 설계 메모에 남긴다.
- 지원 체인과 domain 목록은 Circle 공식 문서 기준으로 확인하고 마지막 확인일을 저장한다.
- hooks를 사용할 때 destination execution 실패와 USDC mint 성공을 분리한다.
코드로 확인하기
위 설계 결정을 코드에서 확인한다. 상태, 서명, 원장 이동, 실패 처리가 어디에 놓이는지 따라 읽으면 앞의 모델이 실제 구현 경계로 내려온다.
컨트랙트CCTP TokenMessenger 인터페이스 — depositForBurn 호출
사용자(또는 결제 서버)가 source chain의
TokenMessenger.depositForBurn을 호출해 USDC를 소각하고, destination domain·recipient를 메시지에 박는다. message hash는 attestation의 키가 된다.
interface ITokenMessenger { function depositForBurn( uint256 amount, uint32 destinationDomain, bytes32 mintRecipient, address burnToken ) external returns (uint64 nonce); function depositForBurnWithCaller( uint256 amount, uint32 destinationDomain, bytes32 mintRecipient, address burnToken, bytes32 destinationCaller ) external returns (uint64 nonce);}interface IMessageTransmitter { function receiveMessage(bytes calldata message, bytes calldata attestation) external returns (bool);}contract CrossChainCheckout { ITokenMessenger public tokenMessenger; function payToRemoteMerchant( uint256 amount, uint32 destinationDomain, bytes32 mintRecipient, // 32바이트로 패딩된 destination merchant 주소 address usdc ) external returns (uint64 nonce) { IERC20(usdc).transferFrom(msg.sender, address(this), amount); IERC20(usdc).approve(address(tokenMessenger), amount); nonce = tokenMessenger.depositForBurn(amount, destinationDomain, mintRecipient, usdc); // nonce 는 attestation 폴링과 destination mint 키로 저장한다 }}백엔드Circle Attestation API 폴링 — exponential backoff
burn 직후 사용자 화면은 "처리 중"으로 두고 백엔드가 Circle API에서 attestation 을 받아온다. timeout은 길게(분 단위), backoff는 점진적으로.
const ATTESTATION_URL = "https://iris-api.circle.com/v1/attestations";export async function pollAttestation(messageHash: `0x${string}`, opts: { maxAttempts?: number; initialDelayMs?: number;} = {}): Promise<`0x${string}` | "timeout"> { const max = opts.maxAttempts ?? 60; let delay = opts.initialDelayMs ?? 2000; for (let i = 0; i < max; i++) { const res = await fetch(`${ATTESTATION_URL}/${messageHash}`); if (res.ok) { const data = await res.json() as { status: "pending_confirmations" | "complete"; attestation: `0x${string}` }; if (data.status === "complete") return data.attestation; } await new Promise((r) => setTimeout(r, delay)); delay = Math.min(delay * 1.5, 30_000); // 최대 30초 간격 } return "timeout";}백엔드Destination chain에서 receiveMessage 실행
attestation을 받은 후 destination chain의 MessageTransmitter에 message + attestation 을 제출. 결과는 USDC mint event로 확인한다.
import { writeContract, waitForTransactionReceipt } from "@wagmi/core";export async function finalizeOnDestination(args: { destinationChainId: number; message: `0x${string}`; attestation: `0x${string}`;}) { const txHash = await writeContract(config, { chainId: args.destinationChainId, address: MESSAGE_TRANSMITTER_ADDRESSES[args.destinationChainId], abi: messageTransmitterAbi, functionName: "receiveMessage", args: [args.message, args.attestation] }); const receipt = await waitForTransactionReceipt(config, { hash: txHash, chainId: args.destinationChainId }); // MintAndWithdraw event 가 있는지 확인 — 없으면 hooks 실패 가능성 const mintLog = receipt.logs.find((log) => log.topics[0] === MINT_AND_WITHDRAW_TOPIC); if (!mintLog) { throw new Error("destination mint event not found — check hooks execution"); } return { txHash, mintLog };}인덱서Cross-chain transfer 상태 추적 테이블
source burn / attestation / destination mint 가 다른 트랜잭션에서 일어나므로 하나의 행으로 통합 추적한다.
// model CrossChainTransfer {// id String @id @default(cuid())// userId String// amount BigInt// sourceChainId Int// destinationChainId Int// nonce BigInt // depositForBurn 반환값// messageHash String @unique// sourceTxHash String// sourceBurnedAt DateTime// attestation String? // Circle attestation hex// attestedAt DateTime?// destinationTxHash String?// destinationMintedAt DateTime?// status String // "burned" | "attested" | "minted" | "failed" | "timeout"//// @@index([status, sourceBurnedAt])// }강의 포인트
| 관점 | 강의 중 확인할 질문 | 학습 후 남길 증거 |
|---|---|---|
| 모델 구분 | CCTP가 wrapped token bridge와 다른 점은 무엇인가? | burn/mint와 lock/mint 비교표 |
| 상태 저장 | source burn, attestation, destination mint를 모두 저장하는가? | source tx, message hash, attestation, destination tx 필드 |
| finality 정책 | Fast Transfer와 Standard Transfer 중 무엇을 쓰는가? | SLA, fee, allowance, finality 정책표 |
| 사용자 문구 | attestation 지연을 사용자가 이해할 수 있는가? | 상태별 안내 문구 |
| 최신성 | CCTP V2 기준 설계와 V1 legacy 대응이 분리됐는가? | 확인일이 있는 migration 메모 |
실무 예시
백엔드[CLIENT] [OPS] 사용자가 Base에서 USDC를 결제했지만 merchant settlement는 Ethereum에서 받아야 한다고 가정한다. 제품은 Base에서 burn을 시작하고, attestation을 받은 뒤 Ethereum에서 mint한다. 사용자는 "전송 중"만 보면 답답하지만, 아래처럼 상태를 나누면 support가 가능하다.
| 상태 | 사용자 문구 | 운영자가 보는 데이터 |
|---|---|---|
BurnSubmitted | source chain에서 USDC 전송을 시작했습니다 | source tx hash, source domain |
FinalityPending | source chain 확인을 기다리고 있습니다 | finality threshold, block confirmations |
AttestationPending | destination mint 증명을 기다리고 있습니다 | Circle API status, message hash |
MintSubmitted | destination chain에서 USDC 수령을 처리 중입니다 | destination tx hash |
Minted | destination chain에서 USDC가 도착했습니다 | destination Transfer event |
Delayed | 전송이 지연되어 확인 중입니다 | retry count, timeout, support note |
이 예시는 checkout 상태머신과 연결된다. CCTP의 Minted가 되어야 merchant settlement로 넘어갈 수 있고, AttestationPending 상태에서는 사용자가 재결제를 누르지 않도록 해야 한다.
흔한 오해와 실패 시나리오
| 오해 | 실패 시나리오 | 바로잡는 방법 |
|---|---|---|
| CCTP도 일반 bridge와 같다고 설명한다 | 사용자가 wrapped token risk와 native burn/mint 모델을 혼동한다 | UI와 문서에서 CCTP burn/mint를 분리해 설명한다 |
| burn이 성공하면 destination 결제도 완료라고 본다 | attestation 또는 mint 지연 중 merchant settlement를 진행한다 | burn, attestation, mint 상태를 분리한다 |
| Fast Transfer는 항상 빠르다고 가정한다 | allowance 부족이나 fee 정책 변경으로 UX가 깨진다 | allowance와 fee 조회 실패 상태를 둔다 |
| supported chain 목록을 코드에 고정한다 | Circle 지원 범위나 domain 정보 변경을 놓친다 | 공식 문서 URL과 마지막 확인일을 allowlist에 저장한다 |
| hooks 실행 성공과 USDC mint 성공을 같은 것으로 본다 | destination logic 실패가 결제 실패로 잘못 처리된다 | mint 상태와 hook execution 상태를 분리한다 |
실습 과제
- 백엔드[INDEXER] CCTP 상태와 증거 정리하기: source burn, finality, attestation, destination mint, fee, allowance 상태마다 저장할 tx hash, message hash, domain ID, API response를 정리한다.
- 백엔드Fast/Standard Transfer 정책 작성하기: Fast Transfer와 Standard Transfer 중 어떤 상황에서 어느 방식을 쓸지 SLA, fee, allowance, finality, support message 기준으로 정책표를 만든다.
- 운영CCTP V2 전환 메모 작성하기: 새 설계를 CCTP V2 기준으로 둔다는 전제, V1 legacy phase-out 확인일, 지원 체인/domain 재확인 절차를 설계 메모로 작성한다.
완료 기준
- burn/mint 모델을 설명한다.
- attestation 대기 UX를 설계했다.
- 체인별 리스크 메모를 작성했다.
- CCTP V1 legacy phase-out과 V2 기준 설계 메모를 남겼다.
근거 자료
- CCTP USDC 크로스체인: 01-스테이블코인/05-CCTP-USDC-크로스체인.md
- Circle CCTP Documentation: https://developers.circle.com/cctp
- Circle: Migrate from CCTP V1 to V2: https://developers.circle.com/cctp/migration-from-v1-to-v2
- Circle: Fast Transfer Allowance: https://developers.circle.com/cctp/concepts/fast-transfer-allowance
- Circle: CCTP Fees: https://developers.circle.com/cctp/concepts/fees