Circle CCTP V2 Protocol
Technical documentation for Circle's Cross-Chain Transfer Protocol integration in Quantum DEX.
Overview
Circle's Cross-Chain Transfer Protocol (CCTP) enables native USDC transfers between blockchains without wrapped tokens or liquidity pools.
Core Benefits
- Native USDC on all supported chains
- Capital efficient (no liquidity pools)
- Permissionless (open protocol)
- Audited and maintained by Circle
- Composable with any application
Architecture
Components
TokenMessenger
- Handles USDC burn on source chain
- Emits deposit message events
- Manages sender and recipient mapping
MessageTransmitter
- Receives and verifies attestations
- Executes mint on destination chain
- Implements nonce tracking and replay protection
Attestation Service
- Monitors burn events across chains
- Generates cryptographic attestations
- Signs messages with Circle's private key
Protocol Flow
Source Chain → TokenMessenger (Burn) → Attestation Service
↓
Destination Chain ← MessageTransmitter (Mint) ← Signed AttestationTiming:
- Burn confirmation: 10-30 seconds
- Attestation: 60-90 seconds
- Mint confirmation: 10-30 seconds
- Total: ~2 minutes
Smart Contracts
Burn Function
function depositForBurn(
uint256 amount,
uint32 destinationDomain,
bytes32 mintRecipient,
address burnToken
) external returns (uint64 nonce)Parameters:
amount: USDC amount (6 decimals)destinationDomain: Target chain domain IDmintRecipient: Recipient address (bytes32 format)burnToken: USDC contract address
Returns:
nonce: Unique message identifier
Mint Function
function receiveMessage(
bytes message,
bytes attestation
) external returns (bool success)Parameters:
message: Original burn messageattestation: Circle's signed attestation
Returns:
success: True if mint succeeded
Domain Identifiers
| Chain | Domain ID |
|---|---|
| Ethereum | 0 |
| Avalanche | 1 |
| Optimism | 2 |
| Arbitrum | 3 |
| Base | 6 |
| Polygon | 7 |
Testnet chains use the same domain IDs as mainnet. Use correct contract addresses for each environment.
Message Format
Burn Message
type BurnMessage = {
version: number;
sourceDomain: number;
destinationDomain: number;
nonce: bigint;
sender: string;
recipient: string;
destinationCaller: string;
messageBody: bytes;
}Message Body
type MessageBody = {
version: number;
burnToken: string;
mintRecipient: string;
amount: bigint;
messageSender: string;
}Implementation
Frontend Integration
// Approve USDC
await usdcContract.approve(
tokenMessengerAddress,
amount
);
// Burn tokens
const tx = await tokenMessenger.depositForBurn(
amount,
destinationDomain,
recipientBytes32,
usdcAddress
);
// Wait for attestation
const attestation = await fetchAttestation(
tx.hash,
sourceChainId
);
// Mint on destination
await messageTransmitter.receiveMessage(
message,
attestation
);Attestation Polling
async function fetchAttestation(
txHash: string,
sourceChain: number
): Promise<string> {
const apiUrl = getAttestationAPI(sourceChain);
while (true) {
const response = await fetch(`${apiUrl}/${txHash}`);
const data = await response.json();
if (data.attestation) {
return data.attestation;
}
await sleep(5000);
}
}Poll Circle's attestation API every 5 seconds until attestation is available.
Recovery System
Quantum DEX implements persistent storage for failed mints:
type PendingMint = {
message: string;
attestation: string;
fromChainId: number;
toChainId: number;
amount: string;
burnTxHash: string;
};
const useBridgeStore = create(
persist(
(set) => ({ /*...*/ }),
{
name: 'quantum-bridge-storage',
partialize: (state) => ({
pendingMint: state.pendingMint
})
}
)
);Data persists in localStorage across sessions. Users can retry mint anytime.
Security
Message Verification
Signature Validation:
- Attestation signed by Circle's private key
- ECDSA signature verification on-chain
- Public key rotation supported
Replay Protection:
- Nonce tracking per source domain
- Used nonces marked permanently
- Same attestation cannot be reused
Domain Validation:
- Source domain must match chain
- Destination domain must be valid
- Cross-domain attacks prevented
Transaction Limits
Per-Transaction:
- Testnet minimum: 1 USDC
- Testnet maximum: 10,000 USDC
- Mainnet: Dynamic limits per chain
Daily Limits:
- Aggregate caps per chain
- Based on available liquidity
- Monitored by Circle
Attack Mitigation
Protected:
- Replay attacks (nonce system)
- Front-running (atomic operations)
- MEV extraction (deterministic)
- Rug pulls (no custody)
User Responsibility:
- Verify contract addresses
- Manage token approvals
- Validate recipient address
- Check transaction details
Performance
Gas Costs
Ethereum Mainnet:
- Approval: ~46,000 gas (~$2-5)
- Burn: ~120,000 gas (~$5-15)
- Mint: ~150,000 gas (~$6-20)
L2 Networks:
- Approval: ~46,000 gas (~$0.01-0.10)
- Burn: ~120,000 gas (~$0.02-0.20)
- Mint: ~150,000 gas (~$0.03-0.30)
Arc Testnet:
- Approval: ~46,000 gas (~$0.001)
- Burn: ~120,000 gas (~$0.002)
- Mint: ~150,000 gas (~$0.003)
Attestation Time
Expected: 60-90 seconds
Maximum: 3-5 minutes during network congestion
Factors Affecting Time:
- Source chain finality requirements
- Attestation service load
- Network conditions
Testing
Testnet Endpoints
Attestation API:
https://iris-api-sandbox.circle.com/attestations/{txHash}Contract Addresses: Verify addresses in Circle's documentation for each testnet.
Test Flow
- Get testnet USDC from faucet
- Approve TokenMessenger
- Execute burn transaction
- Poll attestation API
- Verify attestation signature
- Execute mint on destination
- Confirm USDC balance
Common Issues
Burn Fails:
- Check USDC balance
- Verify approval amount
- Ensure sufficient gas
- Confirm contract address
Attestation Delayed:
- Wait 60-90 seconds minimum
- Check burn transaction finalized
- Verify source chain status
- Poll attestation API manually
Mint Fails:
- Confirm gas on destination
- Verify attestation validity
- Check nonce not used
- Review error message
Integration Examples
Basic Bridge
import { ethers } from 'ethers';
async function bridgeUSDC(
amount: string,
fromChain: number,
toChain: number,
recipient: string
) {
const provider = new ethers.providers.Web3Provider(window.ethereum);
const signer = provider.getSigner();
// Get contracts
const usdc = new ethers.Contract(usdcAddress, usdcAbi, signer);
const messenger = new ethers.Contract(messengerAddress, messengerAbi, signer);
// Approve
const approveTx = await usdc.approve(messengerAddress, amount);
await approveTx.wait();
// Burn
const burnTx = await messenger.depositForBurn(
amount,
toChain,
ethers.utils.formatBytes32String(recipient),
usdcAddress
);
const receipt = await burnTx.wait();
return receipt.transactionHash;
}Mint with Attestation
async function mintUSDC(
message: string,
attestation: string
) {
const provider = new ethers.providers.Web3Provider(window.ethereum);
const signer = provider.getSigner();
const transmitter = new ethers.Contract(
transmitterAddress,
transmitterAbi,
signer
);
const mintTx = await transmitter.receiveMessage(
message,
attestation
);
const receipt = await mintTx.wait();
return receipt.transactionHash;
}Error Handling
try {
const txHash = await bridgeUSDC(amount, fromChain, toChain, recipient);
console.log('Burn successful:', txHash);
} catch (error) {
if (error.code === 'INSUFFICIENT_FUNDS') {
console.error('Not enough gas');
} else if (error.code === 'UNPREDICTABLE_GAS_LIMIT') {
console.error('Transaction will fail, check parameters');
} else {
console.error('Burn failed:', error.message);
}
}Monitoring
Track Bridge Status
type BridgeStatus = 'burning' | 'attesting' | 'minting' | 'complete' | 'failed';
async function monitorBridge(burnTxHash: string): Promise<BridgeStatus> {
// Wait for burn confirmation
const burnReceipt = await provider.waitForTransaction(burnTxHash);
if (!burnReceipt.status) return 'failed';
// Poll for attestation
let attestation;
while (!attestation) {
try {
const response = await fetch(`${attestationAPI}/${burnTxHash}`);
const data = await response.json();
attestation = data.attestation;
} catch {
await sleep(5000);
}
}
return 'attesting';
}Event Listening
// Listen for burn events
messenger.on('DepositForBurn', (
nonce,
burnToken,
amount,
depositor,
mintRecipient,
destinationDomain,
destinationTokenMessenger,
destinationCaller
) => {
console.log('Burn detected:', {
nonce: nonce.toString(),
amount: ethers.utils.formatUnits(amount, 6),
destination: destinationDomain
});
});
// Listen for mint events
transmitter.on('MessageReceived', (
caller,
sourceDomain,
nonce,
sender,
messageBody
) => {
console.log('Mint detected:', {
nonce: nonce.toString(),
source: sourceDomain
});
});Best Practices
Development
- Test on testnet first
- Verify all contract addresses
- Implement proper error handling
- Add transaction retry logic
- Store attestation data safely
Production
- Monitor gas prices
- Set reasonable transaction limits
- Implement rate limiting
- Log all transactions
- Provide clear user feedback
User Experience
- Show real-time progress
- Estimate completion time accurately
- Allow transaction cancellation before burn
- Persist incomplete bridges
- Provide recovery mechanism
Resources
Circle Documentation:
Quantum DEX:
FAQ
How does CCTP differ from traditional bridges?
CCTP burns and mints native USDC. Traditional bridges lock tokens and mint wrapped versions.
What happens if attestation service is down?
Attestations are retrievable via API anytime after burn. No time limit for minting.
Can I bridge other tokens?
CCTP only supports USDC. Other tokens require different bridge protocols.
Is there a maximum bridge amount?
Yes, per-transaction and daily limits exist. Check TokenMinter contract for current limits.
How long are attestations valid?
Attestations have no expiration. Mint anytime after receiving attestation.
Can I cancel after burning?
No. Once USDC is burned, complete the mint on destination or lose funds.
What if mint transaction fails?
Use Quantum DEX's recovery system to retry. Attestation remains valid.
