Skip to main content

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

ContractAddressDomain
TokenMessengerV2 (Arc)0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA26
MessageTransmitterV2 (Arc)0xE737e5cEBEEBa77EFE34D4aa090756590b1CE27526
USDC ERC-20 (Arc)0x3600000000000000000000000000000000000000
For CCTP contract addresses on other blockchains, see the CCTP contract addresses reference.

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.

Step 1. Format the recipient address

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.

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