LapTrinhBlockchain

Chia sẻ kiến thức về Lập Trình Blockchain

Kiến thức Blockchain, Kiến thức lập trình, Kiến thức phần mềm, Lập trình Blockchain, Lập trình NodeJs, Nâng cao Kiến thức

CCTP: Giao thức chuyển giao USDC chéo chuỗi do Circle phát triển

CCTP: Giao thức chuyển giao USDC chéo chuỗi do Circle phát triển

CCTP: Giao thức chuyển giao USDC chéo chuỗi do Circle phát triển

Chia sẻ bài viết
5
(68)

Cross-Chain Transfer Protocol (CCTP) là giao thức chuyển giao chuỗi chéo do Circle phát triển nhằm di chuyển USDC gốc (Native USDC) giữa các mạng lưới blockchain khác nhau một cách an toàn, chi phí thấp và tối ưu hóa tính thanh khoản. Bài này chúng ta tìm hiểu kỹ hơn về góc độ kỹ thuật, cách Dev thực hiện chuyển chéo chuỗi trực tiếp mà không cần qua bên trung gian thứ ba.

Mô hình dịch chuyển của CCTP: Burn & Mint

Vấn đề của các Bridge truyền thống

Trước khi CCTP xuất hiện, người dùng muốn chuyển USDC từ blockchain A sang blockchain B thường phải dùng các cầu nối (bridge) bên thứ ba theo mô hình Lock & Mint:

  • Cơ chế: Khóa USDC gốc trên chain A → Phát hành một phiên bản USDC bọc (Wrapped USDC như wUSDC, axlUSDC,…) trên chain B.
  • Hạn chế:
    • Phân mảnh thanh khoản: Xuất hiện nhiều bản sao USDC không đồng nhất giữa các chain.
    • Rủi ro an ninh cao: Các hợp đồng thông minh lưu giữ tài sản bị khóa (Lock Box) thường là mục tiêu hàng đầu của các vụ hack cross-chain.

Mô hình dịch chuyển của CCTP: Burn & Mint

Khác với phương pháp truyền thống, CCTP sử dụng cơ chế Burn & Mint trực tiếp từ cấp độ giao thức của Circle:

Cơ chế Mint & Burn của Circle cho USDC
USDC (Source Chain) → Circle Attestation} \xrightarrow{\text{Mint}} \text{USDC (Destination Chain)}$$

Quy trình 3 bước chính:

  • Đốt token (Burn): Người dùng yêu cầu chuyển USDC qua ứng dụng (DApp/Ví). CCTP gọi hợp đồng thông minh trên chuỗi nguồn (Source Chain) để tiêu hủy (burn) đúng số lượng USDC đó.
  • Xác thực (Attestation): Circle giám sát sự kiện đốt tiền trên chuỗi nguồn và phát hành một chứng thực ký số (attestation) xác nhận giao dịch hợp lệ.
  • Phát hành (Mint): Chứng thực được gửi tới chuỗi đích (Destination Chain). Hợp đồng CCTP trên chuỗi đích kiểm tra chứng thực và in mới (mint) đúng số lượng USDC gốc vào ví người nhận.

CCTP vận hành dựa trên hai Smart Contract chính do Circle triển khai trên mỗi blockchain hỗ trợ:

  • TokenMessenger: Tiếp nhận yêu cầu chuyển tiền từ ứng dụng/ví, kích hoạt lệnh tiêu hủy token và tương tác với chuỗi đích để mint lại USDC.
  • MessageTransmitter: Đóng vai trò là lớp truyền tin nhắn chung (Generic Messaging Layer), gửi thông điệp cross-chain từ chuỗi nguồn và tiếp nhận thông điệp ở chuỗi đích.

Ưu điểm và ứng dụng thực tế của CCTP

Ưu điểm nổi bật của CCTP

  • Thanh khoản thống nhất: Không cần tạo ra các phiên bản Wrapped USDC. Người dùng luôn nhận được USDC gốc 1:1 trên mọi chuỗi.
  • An toàn & An ninh cao: Loại bỏ hoàn toàn rủi ro bị rút ruột khỏi bể khóa tài sản của các cầu nối.
  • Tối ưu chi phí: Không yêu cầu phí trượt giá hay phí thanh khoản cho các nhà cung cấp LP.
  • Tính mở rộng cho nhà phát triển: CCTP v2 hỗ trợ tính năng Hooks, cho phép tự động hóa các thao tác tiếp theo sau khi mint (ví dụ: chuyển đổi tài sản, nạp tiền vào vị thế cho vay/sàn DEX ngay lập tức chỉ trong một giao dịch đơn lẻ).

Ứng dụng thực tế: CCTP được tích hợp vào các ví (MetaMask, OKX Wallet), sàn DEX và các giao thức hạ tầng cross-chain (LayerZero, Wormhole, Chainlink CCIP) để phục vụ:

  • Cross-chain Swaps: Đổi ETH (trên Ethereum) lấy ARB (trên Arbitrum) thông qua USDC làm đồng tiền trung gian một cách mượt mà.
  • Giao dịch DeFi xuyên chuỗi: Dùng USDC từ Solana để thế chấp nạp tiền vào sàn giao dịch hợp đồng tương lai trên Arbitrum mà không cần thao tác cầu nối thủ công.

So sánh CCTP V1 và CCTP V2

Hai phiên bản chính của Circle CCTP bao gồm CCTP V1 (Legacy)CCTP V2 (Canonical CCTP). Tháng 3/2025, Circle chính thức ra mắt CCTP V2, tích hợp nhiều cải tiến lớn về tốc độ, tính năng và trải nghiệm lập trình (DevEx). Circle đã nâng V2 thành phiên bản chuẩn (Canonical CCTP) và lên kế hoạch ngưng hỗ trợ V1. Chi tiết xem bài viết: CCTP V1 deprecation: CCTP V2 is now the canonical CCTP

Đặc tính / Tính năngCCTP V1 (Legacy)CCTP V2 (Canonical)
Trạng tháiPhiên bản cũ (đang rút dần hỗ trợ)Phiên bản chính thức chuẩn hiện tại
Tốc độ chuyển (Transfer Speed)Standard Transfer: Bắt buộc chờ chuỗi nguồn đạt Block Finality đầy đủ (thường mất từ vài phút đến hơn chục phút tùy chain).Hỗ trợ 2 chế độ:
Standard Transfer như V1
Fast Transfer: Xác thực siêu tốc ngay khi giao dịch được đưa vào block (vượt mức finality), hỗ trợ nhu cầu tức thì.
Lập trình hậu xử lý (Hooks)Không hỗ trợ. USDC chỉ đơn thuần cập bến ví/mạng lưới đích.Hỗ trợ Hooks: Cho phép kích hoạt tự động các logic Smart Contract trên chuỗi đích ngay sau khi mint.
Quy trình API (Off-chain API)Phức tạp hơn: Dev phải trích xuất message byte từ logs giao dịch on-chain, sau đó gọi /v1/attestations/{messageHash}.Tối ưu hóa: Gọi API /v2/messages/{sourceDomainId} bằng ngay txHash là nhận về cả message lẫn chứng thực.
Hạn mức giao dịchTối đa 1 triệu USD / giao dịch.Nâng lên tối đa 10 triệu USD / giao dịch.
Nâng cấp Smart ContractCố định, khó nâng cấp tính năng mới.Khả năng tự nâng cấp, xử lý lỗi và retry tự động tốt hơn.

Khuyến nghị cho Lập trình viên:

  • Nếu bạn đang xây dựng ứng dụng mới: Bắt buộc sử dụng các hợp đồng và API của CCTP V2.
  • Nếu bạn đang chạy ứng dụng dùng CCTP V1: Circle đã chính thức thông báo lộ trình ngừng hỗ trợ V1 (Deprecation). Bạn nên cập nhật lại địa chỉ hợp đồng thông minh, nâng cấp ABI (đặc biệt là các tham số mới của hàm depositForBurn) và chuyển hướng endpoint API từ /v1/ sang /v2/.

Hướng dẫn chuyển USDC giữa nhiều chain dựa trên CCTP V1

Cách Dev làm việc với CCTP V1 (Legacy)

Do V2 đã ra mắt và V1 dần dần sẽ không được hỗ trợ, tôi không khuyến khích sử dụng CCTP V1. Ở đây tôi chỉ nói về mô hình phát triển chung với CCTP V1 chứ không đi sâu:

  1. Gọi Smart Contract chuỗi nguồn: Dev tương tác với hàm depositForBurn() trên TokenMessengerV1 để hủy token.
  2. Lắng nghe Event: Bắt sự kiện MessageSent trên blockchain nguồn để trích xuất mảng byte message.
  3. Lấy Attestation: Gửi mảng byte này lên Circle API v1 (/v1/attestations/…) và đợi Circle trả về chữ ký xác thực.
  4. Gọi Smart Contract chuỗi đích: Đưa messageattestation vào hàm receiveMessage() trên MessageTransmitterV1 để mint USDC vào ví người nhận.

Một số thông tin cho cần cho Dev khi làm việc với giao thức CCTP V1:

Hướng dẫn viết code tương tác với CCTP V1

Vì Circle đã triển khai phiên bản V2 một cách đầy đủ và hoàn thiện, còn phiên bản V1 dần không còn sử dụng nữa. Nên việc tìm hiểu viết code tương tác với CCTP V1 là điều không cần thiết và vô nghĩa.

Hướng dẫn chuyển USDC giữa nhiều chain dựa trên CCTP V2

Cách Dev làm việc với CCTP V2 (Canonical CCTP)

Với CCTP V2, trải nghiệm lập trình đã được nâng cấp rõ rệt:

  1. Cấu hình chi tiết giao dịch (Smart Contract):
    • Hàm depositForBurn() trên V2 nhận thêm tham số finalityThreshold để chọn tốc độ chuyển đổi (Fast hoặc Standard).
    • Truyền thêm dữ liệu byte vào tham số hookData (Hooks). Ví dụ:
      Gửi lệnh “Sau khi mint USDC xong, tự động swap sang ETH trên Uniswap rồi nạp vào Aave”.
  2. Lấy Attestation qua API tinh gọn:
    • Không cần đọc log thủ công, Dev chỉ cần truyền Hash của giao dịch đốt tiền (txHash) lên API endpoint mới:
      https://iris-api.circle.com/v2/messages/{sourceDomainId}?transactionHash={txHash}
    • Hệ thống trả về cả thông điệp đã mã hóa và chứng thực cùng một lúc.
  3. Tích hợp nhanh bằng SDK/Bridge Kit:
    • Circle phát triển SDK mới (Bridge Kit) cho phép các ứng dụng DApp/ví tích hợp luồng chuyển tiền cross-chain của V2 chỉ với dưới 10 dòng code.

Thông tin cần thiết cho Dev khi làm việc với CCTP V2:

Hướng dẫn viết code tương tác với CCTP V2

Bạn có thể sử dụng Circle Bridge Kit do Circle cung cấp, đây là SDK chính thức mới giúp gói gọn toàn bộ quy trình chỉ trong vài dòng code JavaScript/TypeScript. Nhưng tôi sẽ sử dụng giải pháp tương tác trực tiếp để có thể kiểm soát sâu hơn vào logic trong tương lai. Luồng xử lý của chúng ta sẽ như sau:

Chúng ta sẽ sử dụng testnet trong quá trình phát triển để tránh trường hợp bị mất tiền một cách oan uổng không cần thiết.

Mã nguồn code demo như sau (Để trong tệp index.js):

require('dotenv').config();
const ethers = require('ethers');
const axios = require('axios').default;

// https://developers.circle.com/cctp/references/contract-addresses
const chainInfos = {
    ALL: {
        TESTNET: {
            tokenMessengerV2: "0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA",
            messageTransmitterV2: "0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275"
        },
        MAINNET: {
            tokenMessengerV2: "0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d",
            messageTransmitterV2: "0x81D40F21F12A8F0E3252Bccb954D722d4c464B64"
        }
    },
    SEPOLIA: {
        domain: 0,
        USDC: "0x1c7d4b196cb0c7b01d743fbc6116a902379c7238",
        rpc: "https://ethereum-sepolia-rpc.publicnode.com",
    },
    BASE_SEPOLIA: {
        domain: 6,
        USDC: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
        rpc: "https://base-sepolia-rpc.publicnode.com"
    }
};
const USDC_DECIMALS = 6;

// ABI V2
const ERC20_ABI = [
    "function allowance(address owner, address spender) external view returns (uint256)",
    "function approve(address spender, uint256 amount) returns (bool)"
];
const TOKEN_MESSENGER_V2_ABI = [
    `function depositForBurn(uint256 amount, uint32 destinationDomain, bytes32 mintRecipient, address burnToken, bytes32 destinationCaller, uint256 maxFee, uint32 finalityThreshold) returns (uint64 nonce)`
];
const MESSAGE_TRANSMITTER_V2_ABI = [
    "function receiveMessage(bytes message, bytes attestation) returns (bool success)"
];

const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

function getTokenMessengerV2(type, network) {
    if (chainInfos[network] && chainInfos[network].tokenMessengerV2) return chainInfos[network].tokenMessengerV2;
    if (type == "MAINNET") {
        return chainInfos.ALL.MAINNET.tokenMessengerV2;
    }
    return chainInfos.ALL.TESTNET.tokenMessengerV2;
}

function getMessageTransmitterV2(type, network) {
    if (chainInfos[network] && chainInfos[network].messageTransmitterV2) return chainInfos[network].messageTransmitterV2;
    if (type == "MAINNET") {
        return chainInfos.ALL.MAINNET.messageTransmitterV2;
    }
    return chainInfos.ALL.TESTNET.messageTransmitterV2;
}

async function burnUSDC(type, srcNetwork, dstNetwork, amount, recipientAddress) {
    // Init variables
    const botAddr = process.env.BOT_ADDR;
    const provider = new ethers.JsonRpcProvider(chainInfos[srcNetwork].rpc);
    const signer = new ethers.Wallet(process.env.PRIVATE_KEY, provider);
    const usdcAddr = chainInfos[srcNetwork].USDC;
    const messengerAddr = getTokenMessengerV2(type, srcNetwork);

    // Init contracts
    const usdc = new ethers.Contract(usdcAddr, ERC20_ABI, signer);
    const messenger = new ethers.Contract(messengerAddr, TOKEN_MESSENGER_V2_ABI, signer);

    // Call approve() function to allow tokenMessengerV2 using USDC
    const amountBn = ethers.parseUnits(amount.toFixed(6), USDC_DECIMALS);
    const allowance = await usdc.allowance(botAddr, messengerAddr);
    console.log(new Date(), "Allowance:", allowance);
    if (allowance < amountBn) {
        console.log(new Date(), "Approving USDC: Processing...");
        const approveTx = await usdc.approve(messengerAddr, amountBn - allowance);
        let receipt = await approveTx.wait();
        console.log(new Date(), `Approving USDC: DONE with tx ${receipt.hash}`);
    }

    // Call depositForBurn() to burn USDC
    const finalityThreshold = 0;                                    // 0 = Fast Transfer, 1 = Standard Finality
    const maxFee = "500";
    let recipient = ethers.zeroPadValue(botAddr, 32);
    const burnTx = await messenger.depositForBurn(
        amountBn,
        chainInfos[dstNetwork].domain,
        recipient,
        usdcAddr,
        ethers.ZeroHash,
        maxFee,
        finalityThreshold
    );
    let receipt = await burnTx.wait();
    console.log(new Date(), `Burn USDC: SUCCESS with tx ${receipt.hash}`);

    return receipt.hash;
}

// https://developers.circle.com/api-reference/cctp/all/reattest-message
async function fetchAttestation(type, srcNetwork, transactionHash) {
    // Endpoint CCTP V2
    let url = null;
    let srcDomainId = chainInfos[srcNetwork].domain;
    if (type=="MAINNET") {
        url = `https://iris-api.circle.com/v2/messages/${srcDomainId}?transactionHash=${transactionHash}`;
    } else {
        url = `https://iris-api-sandbox.circle.com/v2/messages/${srcDomainId}?transactionHash=${transactionHash}`;
    }

    // Get attestation
    console.log(new Date(), "Waiting for verification from Circle V2 API...");
    while (true) {
        try {
            const resp = await axios.get(url);
            if (resp.status==200) {
                const data = resp.data;
                // console.log("DATA", data);
                if (data.messages && data.messages.length > 0) {
                    const messageObj = data.messages[0];
                    console.log(new Date(), "Verification status:", messageObj.status);
                    if (messageObj.status === "complete") {
                        console.log(new Date(), "Verification from Circle V2 API: DONE");
                        return {
                            messageBytes: messageObj.message,
                            attestation: messageObj.attestation
                        };
                    }
                }
            }
        } catch (error) {
            console.error(new Date(), "Error to get verification from Circle V2 API", error);
        }

        // Wait 60 seconds and try again
        await delay(60000);
    }
}

async function mintUSDC(type, dstNetwork, attestation, messageBytes) {
    const destProvider = new ethers.JsonRpcProvider(chainInfos[dstNetwork].rpc);
    const destSigner = new ethers.Wallet(process.env.PRIVATE_KEY, destProvider);
    const msgTransmitterV2 = getMessageTransmitterV2(type, dstNetwork);
    const transmitter = new ethers.Contract(msgTransmitterV2, MESSAGE_TRANSMITTER_V2_ABI, destSigner);

    console.log(new Date(), "Minting USDC on the destination chain...");
    const mintTx = await transmitter.receiveMessage(messageBytes, attestation);
    const receipt = await mintTx.wait();
    console.log(new Date(), `Minting USDC on the destination chain: SUCCESS! Tx Hash: ${receipt.hash}`);
}

async function main() {
    let type = "TESTNET";
    let srcNetwork = "SEPOLIA";
    let dstNetwork = "BASE_SEPOLIA";
    let amount = 10;
    let startTime = Date.now();
    let txHash = await burnUSDC(type, srcNetwork, dstNetwork, amount);
    let msg = await fetchAttestation(type, srcNetwork, txHash);
    await mintUSDC(type, dstNetwork, msg.attestation, msg.messageBytes);
    let endTime = Date.now();
    let diffTime = (endTime - startTime)/(60*1000);
    console.log(new Date(), `Total time: ${diffTime.toFixed(2)} minutes!!!`);
}
main();

Để chạy bạn cần đánh lệnh sau để cài đặt thư viện cần thiết:
npm i dotenv axios ethers

Tạo tệp .env:

BOT_ADDR = <Địa chỉ account test>
PRIVATE_KEY = <Private Key của account test>

Bơm ít ETH cho hai account này trên 2 mạng lưới testnet: Ethereum SepoliaBase Sepolia. Sau đó vào trang https://faucet.circle.com, để faucet một ít USDC trên mạng lưới Ethereum Sepolia.

Sau đó đánh lệnh sau để chạy thử:
node index.js

Đây là kết quả hiển thị trên console của mã nguồn demo trên:

Bạn có thể kiểm tra 2 giao dịch trên Blockchain Explorer:

Trong code demo trên, tôi sử dụng hàm depositForBurn(), bạn có thể sử dụng hàm depositForBurnWithHook() để có thể thực hiện thêm các tác vụ khác ở chain đích.

TokenMinterV2 và MessageV2 để làm gì?

Khi lập trình viên viết code tương tác với CCTP, chúng ta hầu như chỉ làm việc trực tiếp với 2 hợp đồng TokenMessengerMessageTransmitter. Tuy nhiên, ở kiến trúc bên dưới, kiến trúc CCTP của Circle được thiết kế theo dạng phân tách trách nhiệm. Hai hợp đồng TokenMinterMessage đóng vai trò là lớp quản trị tài sản và định dạng dữ liệu.

Chi tiết vai trò của 2 hợp đồng này:

TokenMinterV2 (Hoạt động ở Tầng Quản Lý Tài Sản & Phân Quyền)

Nếu TokenMessenger là “nhân viên thu ngân” giao tiếp với người dùng, thì TokenMinter chính là “kho tiền” có chìa khóa để in/hủy tiền thực sự.

  • Lý do tồn tại: Để đảm bảo an toàn tuyệt đối cho hợp đồng USDC gốc. Hợp đồng USDC gốc (MasterMinter) chỉ cấp quyền in tiền (Minter Role) cho duy nhất hợp đồng TokenMinter. TokenMessenger không có quyền trực tiếp ra lệnh in USDC.
  • Chức năng chính của TokenMinterV2:
    1. Thực thi Mint/Burn thực tế: Khi TokenMessengerV2.depositForBurn() được gọi, nó sẽ ủy quyền sang TokenMinterV2 để thực hiện thao tác tiêu hủy USDC. Tương tự, ở chuỗi đích, MessageTransmitter sau khi xác minh xong sẽ gọi TokenMinterV2 để mint USDC mới ra.
    2. Quản lý Hạn mức: Quản lý giới hạn tối đa số lượng USDC có thể mint/burn trong một khoảng thời gian để tránh trường hợp nếu CCTP bị tấn công thì hệ thống cũng không bị rút cạn tiền.
    3. Quản lý danh sách Local Tokens: TokenMinter lưu giữ mapping giữa các token USDC gốc trên các chuỗi khác nhau.

MessageV2 (Lớp Định Dạng & Mã Hóa Thông Điệp)

MessageV2 không phải là một hợp đồng thông minh độc lập chạy giao dịch, mà nó đóng vai trò là một Thư viện mã hóa/giải mã.

  • Lý do tồn tại: Để gửi dữ liệu an toàn giữa các blockchain khác nhau (vốn có cấu trúc dữ liệu khác nhau), mọi thông tin như: ai gửi, ai nhận, số lượng bao nhiêu, domain nào, hookData là gì… phải được đóng gói thành một mảng byte chuẩn hóa (Raw Byte Array).
  • Chức năng chính của MessageV2:
    • Đóng gói: Chuyển đổi các tham số input từ TokenMessengerV2 thành dạng chuỗi Byte theo đúng cấu trúc chuẩn của Circle V2.
    • Trích xuất: Khi chuỗi đích nhận được một chuỗi byte từ Circle API (messageBytes), thư viện MessageV2 sẽ “bẻ” chuỗi byte đó ra lại thành các thông tin ban đầu:
      • Format V2 bổ sung thêm các trường mới như: destinationCaller, finalityThreshold, hookData… mà V1 không có.
    • Tạo Hash: Tính toán mã Hash của thông điệp để Circle Attestation Service dùng làm chỉ mục ký xác thực.

Tóm tắt Luồng Tương Tác Giữa 4 Smart Contracts

Sơ đồ dưới đây minh họa cách 4 hợp đồng này phối hợp với nhau bên dưới hậu trường:

* Luồng thực hiện burn ở chuỗi nguồn:

Luồng thực hiện burn ở chuỗi nguồn

* Luồng thực hiện mint ở chuỗi đích:

Luồng thực hiện mint ở chuỗi đích

Bài viết này có hữu ích với bạn?

Kích vào một biểu tượng ngôi sao để đánh giá bài viết!

Xếp hạng trung bình 5 / 5. Số phiếu: 68

Bài viết chưa có đánh giá! Hãy là người đầu tiên đánh giá bài viết này.

Trả lời

Giao diện bởi Anders Norén