Circle USYC 백서

Circle USYC

원문

bitpro가 원문에서 글자만 모은 내용이에요. 표·그림·수식은 빠지거나 깨질 수 있어요. 전체 75쪽 중 54쪽

copied, or stored) and must be consumed in the same transaction. The Move compiler enforces this at compile time, not at runtime, so a transaction that fails to consume a receipt will not compile. For working end-to-end examples, see [Transfer USDC between Aptos and Arc](/cctp/quickstarts/transfer-usdc-aptos-arc). ## Package addresses <Note> **Package IDs** identify deployed modules. Use them in `use` declarations and transaction payloads. **Object IDs** identify onchain state objects (such as configuration or shared resources) and are passed as function arguments when a transaction interacts with protocol state. </Note> ### Testnet #### Package IDs | Package | [Domain](/cctp/concepts/supported-chains-and-domains#domain-identifiers) | Address | | :- | :- | :- | | `MessageTransmitterV2` | 9 | `0xa582dc019ae17b0485f7d3fcc4ca85bea7941e3b70a4b682cea22759f36e711f` | | `TokenMessengerMinterV2` | 9 | `0xc601d3f7b89b77b552726d5d84cbf2a6d3e8a73adbbee3f09fad169a3286bb59` | | `CctpExtensions` | 9 | `0x84010f184db7b60e0067f360b6f809c16cf72ae6ea061cf15ffff2ce9b9b6963` | | `StablecoinHandler` | 9 | `0xe5d19c4eb6e1e42c5435901fc6153fd19f6615651be0e6cefe0f07226d4a6846` | #### Object IDs | Object | Object ID | | :- | :- | | `MessageTransmitterV2` | `0x20c2e0b179a05d08b968003d40563a8c467aa84e2fcabfa6fd5084be27988c47` | | `TokenMessengerMinterV2` | `0x6f57203ca59e901130d8bde217b33cde19faa622dd96ecf6681a2d5307412a14` | | `StablecoinHandler` | `0x22d1d88ffb2e81ac025077f60079dcdfbf45fab0fd1abf7aa5e57c43db25599b` | | `Stablecoin` | `0x69091fbab5f7d635ee7ac5098cf0c1efbe31d68fec0f2cd565e8d168daf52832` | ### Mainnet #### Package IDs | Package | [Domain](/cctp/concepts/supported-chains-and-domains#domain-identifiers) | Address | | :- | :- | :- | | `MessageTransmitterV2` | 9 | `0x1b3f6d749cb835451202f9a1932e49014266f7e3d51611a9a763d156cb4bdaf6` | | `TokenMessengerMinterV2` | 9 | `0x551e32781793c30bf1580d11e4f5171e8d068c3d45c3a5b20ae240bfdad99af8` | | `CctpExtensions` | 9 | `0x93c742af9a876b10c8e13584fb443597611f0efdb342530aa0ebc5ad208815ff` | | `StablecoinHandler` | 9 | `0x2b963f8aa28b815d2e875bd85a1a01d5f2d346f08f5b9fcf8953c77680735af2` | #### Object IDs | Object | Object ID | | :- | :- | | `MessageTransmitterV2` | `0x57ba011deccc5749aff12f4900fd30cac744098f7be91f1eba4e07847510ee72` | | `TokenMessengerMinterV2` | `0x52f4732b1cb52ce28f595e29978450132772f88277182e12bc431faa677be603` | | `StablecoinHandler` | `0x44a1eb6f97a962623ad9d4e8db3dc8408d1b62f90baf91ae6c0d6764b9f1b984` | | `Stablecoin` | `0xbae207659db88bea0cbead6da0ed00aac12edcdda169e591cd41c94180b46f3b` | ## CCTP interface * `TokenMessengerMinterV2`: initiates crosschain USDC burns and prepares mints upon attested message receipt. * `MessageTransmitterV2`: provides the core messaging layer, verifying attestations and routing messages to the appropriate handler. * `StablecoinHandler`: executes the actual burn and mint of USDC, consuming the receipt structs returned by `TokenMessengerMinterV2`. * `CctpExtensions`: provides protocol extension hooks for advanced integrations. ### TokenMessengerMinterV2 interface `TokenMessengerMinterV2` manages the USDC burn and mint lifecycle. Its functions return receipt structs (`BurnReceipt` and `MintReceipt`) that must be passed to the `StablecoinHandler` in the same transaction. | Function | Parameters | Returns | Notes | | :- | :- | :- | :- | | `deposit_for_burn` | `caller (Signer)`, `asset (FungibleAsset)`, `destination_domain (u32)`, `mint_recipient (address)`, `destination_caller (address)`, `max_fee (u64)`, `min_finality_threshold (u32)`, `hook_data (vector<u8>)` | `(BurnReceipt, FungibleAsset)` | Pass `@0x0` as `destination_caller` to allow any caller on the destination. The returned `BurnReceipt` must be passed to `stablecoin_handler::burn()` in the same transaction. | | `prepare_mint` | `receipt (Receipt)` | `MintReceipt` | Takes the `Receipt` returned by `MessageTransmitterV2::receive_message`. The returned `MintReceipt` must be passed to `stablecoin_handler::mint()` in the same transaction. | | `complete_mint` | `handler_signer (Signer)`, `mint_receipt (MintReceipt)`, `asset (FungibleAsset)`, `fee_asset (FungibleAsset)` | None | Not called directly by integrators; called internally by the `StablecoinHandler`. Deposits minted assets and fee assets, then consumes `MintReceipt`. | | `complete_burn` | `handler_signer (Signer)`, `burn_receipt (BurnReceipt)` | None | Not called directly by integrators; called internally by the `StablecoinHandler`. Sends the crosschain message and consumes `BurnReceipt`. | ### MessageTransmitterV2 interface `MessageTransmitterV2` provides the messaging layer for CCTP on Aptos. It verifies attestations, marks nonces as used, and returns a `Receipt` hot potato that the caller must consume in the same transaction. | Function | Parameters | Returns | Notes | | :- | :- | :- | :- | | `receive_message` | `caller (Signer)`, `message (&vector<u8>)`, `attestation (&vector<u8>)` | `Receipt` | `message` and `attestation` are passed by reference (`&vector<u8>`). Nonce is assigned by the attestation service; it is always `0` in the emitted message. The `Receipt` must be consumed in the same transaction by `TokenMessengerMinterV2::prepare_mint()` (for USDC) or `complete_receive_message()` (for generic messages). | | `send_message` | `caller (Signer)`, `destination_domain (u32)`, `recipient (address)`, `destination_caller (address)`, `min_finality_threshold (u32)`, `message_body (&vector<u8>)` | None | Called internally during the burn flow; do not call directly for USDC transfers. | | `complete_receive_message` | `caller (Signer)`, `receipt (Receipt)` | None | For generic messages only. USDC flows use `TokenMessengerMinterV2::prepare_mint()` instead. Destroys `Receipt` and emits a `MessageReceived` event. | ### StablecoinHandler interface `StablecoinHandler` is the entry point for executing USDC burns and mints on Aptos. Its functions consume the receipt structs produced by `TokenMessengerMinterV2` and call the appropriate completion functions internally. | Function | Parameters | Returns | Notes | | :- | :- | :- | :- | | `handler::burn` | `burn_receipt (BurnReceipt)`, `asset (FungibleAsset)` | None | Takes the `BurnReceipt` and `FungibleAsset` returned by `TokenMessengerMinterV2::deposit_for_burn()`. Calls `complete_burn()` internally. | | `handler::mint` | `mint_receipt (MintReceipt)` | None | Takes the `MintReceipt` returned by `TokenMessengerMinterV2::prepare_mint()`. Calls `complete_mint()` internally. | ### CctpExtensions interface `CctpExtensions` provides protocol extension hooks for advanced integrations. Function-level documentation is not yet available here. Refer to the [aptos-cctp GitHub repository](https://github.com/circlefin/aptos-cctp) for the current interface definitions. ## Move scripts <Note> Precompiled scripts for `deposit_for_burn`, `deposit_for_burn_with_hook`, and `receive_message` are available in [`typescript/example/precompiled-move-scripts/`](https://github.com/circlefin/aptos-cctp/tree/master/typescript/example/precompiled-move-scripts) in the aptos-cctp repository. The `v2/` subdirectory contains scripts for both testnet and mainnet. You can also compile them from source; see the [Aptos documentation on compiling scripts](https://aptos.dev/en/build/smart-contracts/scripts/compiling-scripts). </Note> | Script | Description | | :- | :- | | `deposit_for_burn.mv` | Withdraws USDC, calls `deposit_for_burn`, then `stablecoin_handler::burn` in one transaction. | | `deposit_for_burn_with_hook.mv` | Same as `deposit_for_burn.mv` but includes `hook_data`. | | `receive_message.mv` | Calls `receive_message`, then `prepare_mint`, then `stablecoin_handler::mint` in one transaction. | ## Examples ### Send USDC from Aptos (outbound) The outbound flow uses a Move script that withdraws the fungible asset, calls `deposit_for_burn` to create a `BurnReceipt`, then passes both to `stablecoin_handler::burn` to complete the transfer. ```move theme={null} script { use aptos_framework::fungible_asset::Metadata; use aptos_framework::object::{Self, Object}; use aptos_framework::primary_fungible_store; use token_messenger_minter_v2::token_messenger_minter; use stablecoin_handler::handler; use std::vector; fun deposit_for_burn( caller: &signer, amount: u64, destination_domain: u32, mint_recipient: address, destination_caller: address, burn_token: address, max_fee: u64, min_finality_threshold: u32, ) { let token_obj: Object<Metadata> = object::address_to_object(burn_token); let asset = primary_fungible_store::withdraw(caller, token_obj, amount); let (burn_receipt, asset) = token_messenger_minter::deposit_for_burn( caller, asset, destination_domain, mint_recipient, destination_caller, max_fee, min_finality_threshold, vector::empty<u8>(), ); handler::burn(burn_receipt, asset); } } ``` ### Send USDC from Aptos with hook data (outbound) For transfers that include hook data (for example, to trigger a post-mint action on the destination blockchain): ```move theme={null} script { use aptos_framework::fungible_asset::Metadata; use aptos_framework::object::{Self, Object}; use aptos_framework::primary_fungible_store; use token_messenger_minter_v2::token_messenger_minter; use stablecoin_handler::handler; fun deposit_for_burn_with_hook( caller: &signer, amount: u64, destination_domain: u32, mint_recipient: address, destination_caller: address, burn_token: address, max_fee: u64, min_finality_threshold: u32, hook_data: vector<u8>, ) { let token_obj: Object<Metadata> = object::address_to_object(burn_token); let asset = primary_fungible_store::withdraw(caller, token_obj, amount); let (burn_receipt, asset) = token_messenger_minter::deposit_for_burn( caller, asset, destination_domain, mint_recipient, destination_caller, max_fee, min_finality_threshold, hook_data, ); handler::burn(burn_receipt, asset); } } ``` ### Receive USDC on Aptos (inbound) The inbound flow verifies the attestation, creates a `Receipt`, exchanges it for a `MintReceipt` using `prepare_mint`, then passes the `MintReceipt` to `stablecoin_handler::mint` to deposit USDC to the recipient. ```move theme={null} script { use message_transmitter_v2::message_transmitter; use token_messenger_minter_v2::token_messenger_minter; use stablecoin_handler::handler; fun receive_message( caller: &signer, message: vector<u8>, attestation: vector<u8> ) { let receipt = message_transmitter::receive_message( caller, &message, &attestation ); let mint_receipt = token_messenger_minter::prepare_mint(receipt); handler::mint(mint_receipt); } } ``` ### Send a generic message (non-token transfer) ```move theme={null} script { use message_transmitter_v2::message_transmitter; fun send_message( caller: &signer, destination_domain: u32, recipient: address, destination_caller: address, min_finality_threshold: u32, message_body: vector<u8> ) { message_transmitter::send_message( caller, destination_domain, recipient, destination_caller, min_finality_threshold, &message_body ); } } ``` # Attestation verification Source: https://developers.circle.com/cctp/references/attestation-verification Technical reference for verifying CCTP attestation signatures When you retrieve an attestation from Circle's Attestation Service, you can optionally verify the attestation signature before using it to mint USDC on the destination blockchain. This page explains how the verification process works and when you might want to use it. ## How verification works The verification process uses cryptographic signature recovery to confirm that Circle's Attestation Service signed the message. It involves the following steps: <Steps> <Step title="Retrieve the public key"> Fetch Circle's current public key from the [`GET /v2/publicKeys`](/api-reference/cctp/all/get-public-keys-v2) endpoint. </Step> <Step title="Hash the message"> Create a `keccak256` hash of the message bytes. </Step> <Step title="Parse the attestation"> Split the 65-byte attestation into its `r`, `s`, and `v` components (ECDSA signature format). </Step> <Step title="Recover the signer"> Use the signature and message hash to recover the public key that signed the message. </Step> <Step title="Compare addresses"> Convert both the recovered public key and Circle's public key to Ethereum addresses and compare them. </Step> </Steps> If the addresses match, the attestation was signed by Circle's Attestation Service and is valid. ## When to verify attestations Attestation verification is optional because the CCTP contracts on the destination blockchain perform their own verification when you call `receiveMessage`. However, you might want to verify attestations before submitting the mint transaction if: * **Your application requires an additional layer of security**: Verifying before minting provides defense-in-depth by catching invalid attestations at the application layer. * **You want to detect invalid attestations before paying gas fees**: If an attestation is invalid, the mint transaction fails and you lose the gas fees. Pre-verification lets you catch this before submitting the transaction. * **You're building a relayer service that batches multiple attestations**: Relayers can verify each attestation in a batch before submitting, preventing a single invalid attestation from affecting the entire batch. ## Verification code example The following examples show how to verify an attestation signature using Viem or Ethers: <CodeGroup> ```ts Viem theme={null} import { keccak256, hexToBytes, recoverAddress, bytesToHex } from "viem"; interface PublicKey { publicKey: `0x${string}`; cctpVersion: number; } interface AttestationData { message: string; attestation: string; } function publicKeyToAddress(publicKey: `0x${string}`): `0x${string}` { // Remove '0x04' prefix (uncompressed public key marker) const publicKeyWithoutPrefix = `0x${publicKey.slice(4)}` as `0x${string}`; const hash = keccak256(hexToBytes(publicKeyWithoutPrefix)); // Take last 20 bytes (40 hex chars) as address return `0x${hash.slice(-40)}`; } async function getPublicKeys() { const response = await fetch( "https://iris-api-sandbox.circle.com/v2/publicKeys", ); const data = await response.json(); return data.publicKeys .filter((key: PublicKey) => key.cctpVersion === 2) .map((key: PublicKey) => key.publicKey); } async function verifyAttestation( attestationData: AttestationData, publicKeys: `0x${string}`[], ) { try { const messageHash = keccak256(attestationData.message as `0x${string}`); const attestationBytes = hexToBytes( attestationData.attestation as `0x${string}`, ); const signatureLength = 65; const numSignatures = attestationBytes.length / signatureLength; if (attestationBytes.length % signatureLength !== 0) { throw new Error(`Invalid attestation length: ${attestationBytes.length}`); } let validSignatures = 0; for (let i = 0; i < numSignatures; i++) { const start = i * signatureLength; const signature = attestationBytes.slice(start, start + signatureLength); const recoveredAddress = await recoverAddress({ hash: messageHash, signature: bytesToHex(signature), }); const isValid = publicKeys.some( (publicKey) => publicKeyToAddress(publicKey).toLowerCase() === recoveredAddress.toLowerCase(), ); if (isValid) validSignatures++; } const threshold = Math.ceil(publicKeys.length / 2); console.log( `Valid signatures: ${validSignatures}/${numSignatures}, threshold: ${threshold}`, ); return validSignatures >= threshold; } catch (error) { console.error( "Error verifying attestation:", error instanceof Error ? error.message : String(error), ); return false; } } const attestationData: AttestationData = { message: "0x000000010000001a00000015...", // Full message hex from API attestation: "0x3c5951abd82a83369d603ebaf9...", // Full attestation hex from API }; // Example usage const publicKeys = await getPublicKeys(); const isValid = await verifyAttestation(attestationData, publicKeys); ``` ```ts Ethers.js theme={null} import { ethers } from "ethers"; interface PublicKey { publicKey: string; cctpVersion: number; } interface AttestationData { message: string; attestation: string; } async function getPublicKeys() { const response = await fetch( "https://iris-api-sandbox.circle.com/v2/publicKeys", ); const data = await response.json(); // Get all public keys for CCTP V2 const v2Keys = data.publicKeys .filter((key: PublicKey) => key.cctpVersion === 2) .map((key: PublicKey) => key.publicKey); if (v2Keys.length === 0) { throw new Error("CCTP V2 public key not found"); } return v2Keys; } function verifyAttestation( attestationData: AttestationData, publicKeys: string[], ) { try { const messageHash = ethers.keccak256(attestationData.message); const attestationBytes = ethers.getBytes(attestationData.attestation); // V2 attestation has multiple 65-byte signatures const signatureLength = 65; const numSignatures = attestationBytes.length / signatureLength; if (attestationBytes.length % signatureLength !== 0) { throw new Error(`Invalid attestation length: ${attestationBytes.length}`); } let validSignatures = 0; // Verify each signature for (let i = 0; i < numSignatures; i++) { const start = i * signatureLength; const sigBytes = attestationBytes.slice(start, start + signatureLength); const r = ethers.hexlify(sigBytes.slice(0, 32)); const s = ethers.hexlify(sigBytes.slice(32, 64)); const v = sigBytes[64]; const signature = { r, s, v }; const recoveredAddress = ethers.recoverAddress(messageHash, signature); // Check if recovered address matches any V2 public key const isValid = publicKeys.some( (publicKey) => ethers.computeAddress(publicKey).toLowerCase() === recoveredAddress.toLowerCase(), ); if (isValid) validSignatures++; } const threshold = Math.ceil(publicKeys.length / 2); console.log( `Valid signatures: ${validSignatures}/${numSignatures}, threshold: ${threshold}`, ); return validSignatures >= threshold; } catch (error) { console.error("Error verifying attestation:", (error as Error).message); return false; } } // Use attestation data from the API const attestationData: AttestationData = { message: "0x000000010000001a00000015...", // Full message hex from API attestation: "0x3c5951abd82a83369d603ebaf9...", // Full attestation hex from API }; // Example usage const publicKeys = await getPublicKeys(); const isValid = verifyAttestation(attestationData, publicKeys); ``` </CodeGroup> # CctpExtensionV2 Contract Interface Source: https://developers.circle.com/cctp/references/cctp-extension-v2-contract-interface Sponsored CCTP deposits from Arbitrum using EIP-3009 and a relayer. `CctpExtensionV2` is an optional Arbitrum contract for sponsored CCTP deposits to HyperCore. It is separate from [`CctpExtension`](/cctp/howtos/transfer-usdc-from-arbitrum-to-hypercore), which expects the depositor to submit the burn transaction and pay gas. Circle deploys and maintains the contract. Circle does not operate the source-chain relayer: the integrator (or their partner) runs relayer infrastructure that submits Arbitrum transactions on users' behalf. ## What it does 1. The user signs an offchain EIP-3009 `ReceiveWithAuthorization` that authorizes the contract to pull USDC from their wallet. 2. A relayer calls `batchSponsorDepositForBurn` with parallel arrays of authorization data and CCTP deposit parameters, paying Arbitrum gas. 3. The contract validates that each authorization nonce matches the intended deposit parameters, pulls USDC, and burns via CCTP `TokenMessengerV2`. 4. CCTP attestation and destination forwarding proceed like any other Arbitrum → HyperEVM → HyperCore transfer (including optional `CctpForwarder` hook data). Primary entry point: `batchSponsorDepositForBurn
프로젝트로 돌아가기