Skip to main content
This guide helps you complete a CCTP transfer when you have a valid attestation but the mint transaction on the destination blockchain fails or was never submitted.

Minting is safe to retry

CCTP minting is idempotent. Each attestation contains a unique nonce that can only be used once. If you submit the same attestation multiple times, only the first successful transaction mints USDC. Subsequent attempts revert with a “nonce already used” error but don’t result in duplicate minting. This means you can safely retry a failed mint without risking double-spending.

Common mint failure reasons

Failure reasonSymptomsSolution
Insufficient gasTransaction reverts or times outIncrease gas limit and retry
Nonce already usedTransaction reverts with nonce errorThe mint already succeeded; check recipient balance
Wrong contract addressTransaction may succeed with no USDC mintedVerify you’re using the correct MessageTransmitterV2 address for the destination blockchain
Destination caller restrictionTransaction revertsCheck if the burn specified a destinationCaller; only that address can mint
Token account doesn’t exist (Solana)Transaction failsCreate the recipient’s USDC token account first
Attestation expiredTransaction revertsUse re-attestation API to get a fresh attestation

Verify the current state

Before retrying, check whether the mint already succeeded:
1

Check recipient balance

Query the recipient’s USDC balance on the destination blockchain. If the expected amount is present, the mint already completed.
2

Check for existing mint transaction

Search the destination blockchain’s block explorer for receiveMessage transactions from your wallet to the MessageTransmitterV2 contract.
3

Check attestation status

Query the attestation API to confirm you have a complete status:

Retry the mint transaction

If the mint hasn’t completed, submit a new receiveMessage transaction using your attestation.
Call receiveMessage on the MessageTransmitterV2 contract:
JavaScript
import {
  createWalletClient,
  createPublicClient,
  http,
  encodeFunctionData,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { arbitrumSepolia } from "viem/chains";
import dotenv from "dotenv";

dotenv.config();

const account = privateKeyToAccount(`0x${process.env.PRIVATE_KEY}`);
const walletClient = createWalletClient({
  chain: arbitrumSepolia,
  transport: http(),
  account,
});
const publicClient = createPublicClient({
  chain: arbitrumSepolia,
  transport: http(),
});

// MessageTransmitterV2 contract address - verify for your destination chain
// See: https://developers.circle.com/stablecoins/cctp/references/contract-addresses
const MESSAGE_TRANSMITTER = "0xaCF1ceeF35caAc005e15888dDb8A3515C41B4872";

async function retryMint(message, attestation) {
  console.log("Retrying mint transaction...");

  try {
    const txHash = await walletClient.sendTransaction({
      to: MESSAGE_TRANSMITTER,
      data: encodeFunctionData({
        abi: [
          {
            type: "function",
            name: "receiveMessage",
            stateMutability: "nonpayable",
            inputs: [
              { name: "message", type: "bytes" },
              { name: "attestation", type: "bytes" },
            ],
            outputs: [],
          },
        ],
        functionName: "receiveMessage",
        args: [message, attestation],
      }),
    });

    console.log(`Mint transaction submitted: ${txHash}`);

    // Wait for confirmation
    const receipt = await publicClient.waitForTransactionReceipt({
      hash: txHash,
    });

    if (receipt.status === "success") {
      console.log("Mint successful!");
      return { success: true, txHash };
    } else {
      console.log("Mint transaction reverted");
      return { success: false, txHash };
    }
  } catch (error) {
    // Check if the error indicates nonce already used
    if (error.message.includes("nonce") || error.message.includes("already")) {
      console.log(
        "Nonce already used - mint may have already completed. Check recipient balance.",
      );
    }
    throw error;
  }
}

// Use the attestation data from the API response
const attestationData = {
  message: "0x000000000000000500000003...",
  attestation: "0xdc485fb2f9a8f68c871f4ca7386dee9086ff9d43...",
};

await retryMint(attestationData.message, attestationData.attestation);

Handle destination caller restrictions

If the burn specified a destinationCaller address, only that address can call receiveMessage. If you’re seeing authorization errors:
  1. Check the destinationCaller field in the attestation’s decodedMessage
  2. If it’s not 0x0000...0000, ensure you’re calling from the specified address