Circle CCTP V2 Flow
CCTP is a burn-and-mint protocol for moving native Circle USDC between chains. It does not lock liquidity in a bridge pool and it does not mint wrapped USDC.
Deposit Into Injective
For source chain to Injective EVM:
- Source chain: approve
TokenMessengerV2to spend native USDC. - Source chain: call
depositForBurn. - Off-chain: poll Circle's iris API for message and attestation.
- Destination chain: switch wallet to Injective EVM.
- Destination chain: call
MessageTransmitterV2.receiveMessage.
The V2 depositForBurn signature is:
function depositForBurn(
uint256 amount,
uint32 destinationDomain,
bytes32 mintRecipient,
address burnToken,
bytes32 destinationCaller,
uint256 maxFee,
uint32 minFinalityThreshold
) returns (uint64 nonce)For standard transfer into Injective:
const destinationDomain = 29
const mintRecipient = zeroPadValue(recipientEvmAddress, 32)
const burnToken = sourceChain.usdc
const destinationCaller = '0x' + '0'.repeat(64)
const maxFee = 0n
const minFinalityThreshold = 2000Withdraw From Injective
For Injective EVM to another CCTP chain, the same flow runs in reverse:
- Injective EVM: approve Injective USDC to
TokenMessengerV2. - Injective EVM: call
depositForBurnwith the destination domain. - Poll Circle iris with source domain
29and the Injective burn tx hash. - Switch wallet to the destination chain.
- Call destination
MessageTransmitterV2.receiveMessage.
Attestation Polling
Use:
GET https://iris-api.circle.com/v2/messages/{sourceDomain}?transactionHash={burnTxHash}Treat the transfer as mintable only when the response contains a message whose:
statusiscompletemessageis presentattestationis present and is notPENDING
Poll every few seconds. Standard finality waits are source-chain dependent. Ethereum can take around 13 minutes; L2s and Avalanche are usually shorter.
Recovery
If burn confirmed and mint did not happen, funds are not lost. Re-fetch the
message and attestation with the burn hash, switch to the destination chain, and
call receiveMessage again.
This is why apps should persist or display the burn hash immediately. A user support flow can resume from that hash without asking the user to burn again.
Frontend State Model
Recommended statuses:
type CctpStatus =
| 'idle'
| 'approving'
| 'burning'
| 'waiting-for-attestation'
| 'ready-to-mint'
| 'minting'
| 'complete'
| 'failed'Persist enough context after burn to recover:
interface PendingCctpTransfer {
sourceDomain: number
destinationDomain: number
sourceChainId: number
destinationChainId: number
amountBaseUnits: string
burnTxHash: string
recipient: `0x${string}`
}Common Failures
- Wrong domain: chain ID and CCTP domain are different numbers.
- Wrong USDC:
burnTokenmust be native Circle USDC on the source chain. - Missing gas: user needs source native gas for burn and destination native gas for mint.
- Lost tab: recover from the burn hash.
- Regulatory hook gas on Injective: a
restricted actionstyle error can mean the EVM hook ran out of gas. Retry with a higher gas limit before assuming the transfer is blocked.