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프로젝트로 돌아가기