Documentation Index
Fetch the complete documentation index at: https://docs.arc.io/llms.txt
Use this file to discover all available pages before exploring further.
Circle’s Cross-Chain Transfer Protocol (CCTP)
uses a burn-and-mint model to transfer native USDC between blockchains without
wrapped or bridged token variants. Arc’s CCTP domain is 26. Transfers into Arc
mint USDC directly to your recipient address; transfers out burn USDC and mint
on the destination blockchain. Because Arc has instant finality, outbound
transfers reach attestation faster than transfers from blockchains that require
multiple confirmations.
Prerequisites
Before you begin, ensure you have:
- An RPC endpoint for Arc Testnet (
https://rpc.testnet.arc.network)
- An RPC endpoint for the source or destination blockchain (for example,
Ethereum Sepolia)
- USDC balance on the sending blockchain
- Familiarity with the
CCTP developer docs
- A TypeScript environment with
viem installed
Contract addresses
Bridge USDC between Arc and other blockchains
Use the inbound workflow to fund your exchange hot wallet on Arc from another
blockchain, and the outbound workflow to rebalance liquidity from Arc to another
blockchain.
Inbound (into Arc)
Outbound (out of Arc)
CCTP requires the recipient as a bytes32 value. Left-pad the 20-byte Ethereum
address with zeros:import { pad, type Address } from "viem";
const recipientAddress: Address = "0xYourArcHotWalletAddress";
const mintRecipient = pad(recipientAddress, { size: 32 });
Step 2. Approve USDC on the source blockchain
Approve the source blockchain’s TokenMessengerV2 contract to spend your USDC:import { parseUnits } from "viem";
const amount = parseUnits("10000", 6); // 10,000 USDC
const approvalTx = await sourceWalletClient.writeContract({
address: SOURCE_USDC_ADDRESS,
abi: [
{
name: "approve",
type: "function",
inputs: [
{ name: "spender", type: "address" },
{ name: "amount", type: "uint256" },
],
outputs: [{ type: "bool" }],
stateMutability: "nonpayable",
},
],
functionName: "approve",
args: [SOURCE_TOKEN_MESSENGER_ADDRESS, amount],
});
Step 3. Call depositForBurn on the source blockchain
Burn USDC on the source blockchain, targeting Arc (domain 26):const ARC_DOMAIN = 26;
const burnTx = await sourceWalletClient.writeContract({
address: SOURCE_TOKEN_MESSENGER_ADDRESS,
abi: [
{
name: "depositForBurn",
type: "function",
inputs: [
{ name: "amount", type: "uint256" },
{ name: "destinationDomain", type: "uint32" },
{ name: "mintRecipient", type: "bytes32" },
{ name: "burnToken", type: "address" },
],
outputs: [{ type: "uint64" }],
stateMutability: "nonpayable",
},
],
functionName: "depositForBurn",
args: [amount, ARC_DOMAIN, mintRecipient, SOURCE_USDC_ADDRESS],
});
Step 4. Retrieve the message hash
After the burn transaction confirms, extract the MessageSent event to get the
message hash:const burnReceipt = await sourcePublicClient.waitForTransactionReceipt({
hash: burnTx,
});
const messageSentEvent = burnReceipt.logs.find(
(log) =>
log.topics[0] ===
"0x2fa9ca894982930190727e75500a97d8dc500233a5065e0f3126c48fbe0343c0",
);
const messageBytes = messageSentEvent?.data;
const messageHash = keccak256(messageBytes!);
Step 5. Wait for attestation
Poll Circle’s attestation service until the attestation is available:async function getAttestation(messageHash: string): Promise<string> {
const url = `https://iris-api.circle.com/v2/attestations/${messageHash}`;
while (true) {
const response = await fetch(url);
const data = await response.json();
if (data.status === "complete") {
return data.attestation;
}
// Poll every 10 seconds
await new Promise((resolve) => setTimeout(resolve, 10_000));
}
}
const attestation = await getAttestation(messageHash);
Attestation typically takes approximately 60 seconds but varies based on the
source blockchain’s finality time. Transfers from blockchains with longer
finality (such as Ethereum) take longer than those from blockchains with fast
finality.
Step 6. Call receiveMessage on Arc
Submit the message and attestation to Arc’s MessageTransmitterV2 to mint USDC:const ARC_MESSAGE_TRANSMITTER = "0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275";
const receiveTx = await arcWalletClient.writeContract({
address: ARC_MESSAGE_TRANSMITTER,
abi: [
{
name: "receiveMessage",
type: "function",
inputs: [
{ name: "message", type: "bytes" },
{ name: "attestation", type: "bytes" },
],
outputs: [{ type: "bool" }],
stateMutability: "nonpayable",
},
],
functionName: "receiveMessage",
args: [messageBytes, attestation],
});
Once the transaction confirms, USDC is minted to the recipient address on Arc. Pad the destination address to bytes32:import { pad, type Address } from "viem";
const destinationAddress: Address = "0xYourDestinationAddress";
const mintRecipient = pad(destinationAddress, { size: 32 });
Step 2. Approve USDC on Arc
Approve Arc’s TokenMessengerV2 to spend your USDC:import { parseUnits } from "viem";
const ARC_USDC = "0x3600000000000000000000000000000000000000";
const ARC_TOKEN_MESSENGER = "0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA";
const amount = parseUnits("10000", 6); // 10,000 USDC
const approvalTx = await arcWalletClient.writeContract({
address: ARC_USDC,
abi: [
{
name: "approve",
type: "function",
inputs: [
{ name: "spender", type: "address" },
{ name: "amount", type: "uint256" },
],
outputs: [{ type: "bool" }],
stateMutability: "nonpayable",
},
],
functionName: "approve",
args: [ARC_TOKEN_MESSENGER, amount],
});
Step 3. Call depositForBurn on Arc
Burn USDC on Arc, targeting the destination domain:// Example: Ethereum = 0, Avalanche = 1, Arbitrum = 3
const DESTINATION_DOMAIN = 0; // Replace with your target domain
const burnTx = await arcWalletClient.writeContract({
address: ARC_TOKEN_MESSENGER,
abi: [
{
name: "depositForBurn",
type: "function",
inputs: [
{ name: "amount", type: "uint256" },
{ name: "destinationDomain", type: "uint32" },
{ name: "mintRecipient", type: "bytes32" },
{ name: "burnToken", type: "address" },
],
outputs: [{ type: "uint64" }],
stateMutability: "nonpayable",
},
],
functionName: "depositForBurn",
args: [amount, DESTINATION_DOMAIN, mintRecipient, ARC_USDC],
});
Step 4. Retrieve the message hash
Extract the message from the burn transaction receipt:const burnReceipt = await arcPublicClient.waitForTransactionReceipt({
hash: burnTx,
});
const messageSentEvent = burnReceipt.logs.find(
(log) =>
log.topics[0] ===
"0x2fa9ca894982930190727e75500a97d8dc500233a5065e0f3126c48fbe0343c0",
);
const messageBytes = messageSentEvent?.data;
const messageHash = keccak256(messageBytes!);
Step 5. Wait for attestation
Poll for the attestation. Because Arc has instant finality, attestation for
outbound transfers is typically faster:const attestation = await getAttestation(messageHash);
Outbound transfers from Arc benefit from instant finality. The attestation
service can process the message once the block is produced, without waiting
for additional confirmations.
Step 6. Call receiveMessage on the destination blockchain
Submit the message and attestation to the destination blockchain’s
MessageTransmitterV2:const receiveTx = await destinationWalletClient.writeContract({
address: DESTINATION_MESSAGE_TRANSMITTER,
abi: [
{
name: "receiveMessage",
type: "function",
inputs: [
{ name: "message", type: "bytes" },
{ name: "attestation", type: "bytes" },
],
outputs: [{ type: "bool" }],
stateMutability: "nonpayable",
},
],
functionName: "receiveMessage",
args: [messageBytes, attestation],
});
Once confirmed, USDC is minted to the recipient on the destination blockchain.
Monitor transfer status
Track a CCTP transfer by polling the attestation API:
import { keccak256 } from "viem";
async function checkTransferStatus(messageHash: string) {
const response = await fetch(
`https://iris-api.circle.com/v2/attestations/${messageHash}`,
);
const data = await response.json();
// Possible statuses: "pending", "complete"
return data.status;
}
Each CCTP message can only be received once. If receiveMessage reverts,
verify that the message has not already been processed by checking the
usedNonces mapping on the destination MessageTransmitterV2 contract.
CCTP V2 depositForBurnWithHook
CCTP V2 introduces depositForBurnWithHook, which allows you to specify a
destination caller and attach a hook for post-mint actions. This is useful for
triggering automated workflows (such as depositing into a vault) immediately
after USDC is minted:
const burnWithHookTx = await arcWalletClient.writeContract({
address: ARC_TOKEN_MESSENGER,
abi: [
{
name: "depositForBurnWithHook",
type: "function",
inputs: [
{ name: "amount", type: "uint256" },
{ name: "destinationDomain", type: "uint32" },
{ name: "mintRecipient", type: "bytes32" },
{ name: "burnToken", type: "address" },
{ name: "destinationCaller", type: "bytes32" },
{ name: "hookData", type: "bytes" },
],
outputs: [{ type: "uint64" }],
stateMutability: "nonpayable",
},
],
functionName: "depositForBurnWithHook",
args: [
amount,
DESTINATION_DOMAIN,
mintRecipient,
ARC_USDC,
destinationCaller, // bytes32-padded address authorized to call receiveMessage
hookData, // Encoded calldata for post-mint execution
],
});
When destinationCaller is set to a non-zero value, only that address can
call receiveMessage for this transfer on the destination blockchain. Set it
to 0x000000 to allow any address to complete the transfer.
See also