The Circle Forwarding Service is a service for CCTP that simplifies integration
by removing the need for you to run multichain infrastructure. This can improve
user experience for crosschain transfers by ensuring reliability and eliminating
the need to handle destination chain gas fees.
How it works
A CCTP transfer without the Forwarding Service is a three-step process:
- Create a transaction to burn USDC on the source chain and wait for Circle to
sign an attestation.
- Request an attestation from the Circle API.
- Create a transaction to mint USDC on the destination chain.
This process requires you to have a wallet that can sign transactions on the
source and destination chains, and native tokens for paying the transaction gas
fee on both chains.
You use the Forwarding Service by including a forward request in the hook data
of the burn transaction on the source chain. Circle validates the hook data,
signs the attestation, and broadcasts the mint transaction on the destination
chain for you, removing the need for you to handle the transaction on the
destination chain.
For a full example of how to use the Forwarding Service, see
Transfer USDC with the Forwarding Service.
The hook data for Forwarding Service begins with the reserved magic bytes
cctp-forward followed by versioning and payload fields. You can append your
own custom hook data after Circle’s reserved space. Forwarding Service doesn’t
support forwarding to wrapper contracts (for example, when destinationCaller
is set).
| Bytes | Type | Data |
|---|
| 0-23 | bytes24 | cctp-forward |
| 24-27 | uint32 | Version, set to 0 |
| 28-31 | uint32 | Length of additional Circle hook data, set to 0 |
| 32-51 | any | Developer-defined hook data |
If no additional integrator hook data is required, a static hex string can be
used for the forwarding hook data:
// Includes magic bytes ("cctp-forward") + hook version (0) + empty data length (0)
const forwardHookData =
"0x636374702d666f72776172640000000000000000000000000000000000000000";
Solana mintRecipient
When the destination blockchain is Solana, the mintRecipient parameter in
depositForBurnWithHook must be the recipient’s USDC
Associated Token Account (ATA)
address, not the recipient’s wallet address. Unlike EVM destinations where
mintRecipient is the wallet address, Solana requires the address of the SPL
token account that will hold the minted USDC.
You can derive the ATA address from the recipient’s wallet address and the USDC
mint address using the
getAssociatedTokenAddressSync
function from the @solana/spl-token library:
import { getAssociatedTokenAddressSync } from "@solana/spl-token";
import { PublicKey } from "@solana/web3.js";
const recipientWallet = new PublicKey("RecipientWalletAddress");
const USDC_MINT = new PublicKey("4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU"); // Solana devnet
const recipientAta = getAssociatedTokenAddressSync(USDC_MINT, recipientWallet);
const mintRecipient = `0x${Buffer.from(recipientAta.toBytes()).toString("hex")}`;
Solana hook data for ATA creation
If the recipient does not have an existing USDC ATA, you can request the
Forwarding Service to create it by encoding additional fields in the hook data.
The extended hook data format is:
| Bytes | Type | Data |
|---|
| 0-23 | bytes24 | cctp-forward |
| 24-27 | uint32 | Version, set to 0 |
| 28-31 | uint32 | Length of additional Circle hook data, set to 33 |
| 32 | uint8 | 1 (request ATA creation) |
| 33-64 | bytes32 | Recipient wallet address (ATA owner) |
| 65+ | any | Developer-defined hook data |
When using this format, the mintRecipient must be the ATA derived from the
wallet address in bytes 33-64 and the USDC mint. The Forwarding Service
validates that these values are consistent before creating the account.
The following example shows how to construct the extended hook data for Solana
with ATA creation:
import { PublicKey } from "@solana/web3.js";
// Magic bytes "cctp-forward" padded to 24 bytes
const magicBytes = Buffer.alloc(24);
magicBytes.write("cctp-forward", "utf-8");
// Version (uint32, big-endian) = 0
const version = Buffer.alloc(4);
// Length of additional Circle hook data (uint32, big-endian) = 33
const length = Buffer.alloc(4);
length.writeUInt32BE(33);
// ATA creation flag = 1
const ataFlag = Buffer.from([1]);
// Recipient wallet address (32 bytes)
const recipientWallet = new PublicKey("RecipientWalletAddress");
const walletBytes = Buffer.from(recipientWallet.toBytes());
const forwardHookData =
"0x" +
Buffer.concat([magicBytes, version, length, ataFlag, walletBytes]).toString(
"hex",
);
If the recipient already has a USDC ATA and no ATA creation is needed, use the
same static hook data as EVM:
// Includes magic bytes ("cctp-forward") + hook version (0) + empty data length (0)
const forwardHookData =
"0x636374702d666f72776172640000000000000000000000000000000000000000";
Fees and execution
The Forwarding Service charges a fee for each transfer, in addition to the CCTP
protocol fee. The Forwarding Service fee charged is to cover gas costs on the
destination chain and a small service fee. The Forwarding Service prioritizes
fast execution and quotes gas dynamically. If gas used is less than gas needed
for execution, the remainder is spent as an additional priority fee where they
are supported. On all chains, a higher fee provides a safety buffer for
successful transaction delivery on the destination chain. Circle does not refund
for excess gas and does not keep the excess gas, except in cases where excess
priority fees are rejected.
The
depositForBurnWithHook
transaction includes a maxFee parameter. When using the Forwarding Service,
this parameter should be set to a value that is large enough to cover the CCTP
protocol fee and the Forwarding Service fee. Because the gas budget for the
destination chain comes from a USDC fee on the source chain, choosing a lower
maxFee results in a lower priority fee on the destination chain. A higher
maxFee results in a higher priority fee on the destination chain and can
result in faster confirmation.
The Forwarding Service charges a service fee for each transfer:
| Destination chain | Service fee (USDC) |
|---|
| All chains | $0.20 |
If the maxFee parameter is insufficient to cover the both Fast Transfer
protocol fee and the Forwarding Service fee, CCTP will prioritize forwarding
execution over Fast Transfer. This means that the transfer will execute as a
Standard Transfer with the Forwarding Service.
Solana fees
When forwarding to Solana, the Forwarding Service fee includes both a gas
component and a rent component. Rent covers the cost of creating onchain
accounts required by each transfer. The fee estimate API returns forwardFee
values that already include rent, so no additional calculation is needed on your
part.
If the recipient does not already have a USDC
Associated Token Account (ATA)
on Solana, the transfer will fail. To have the Forwarding Service create the
ATA, you must:
- Pass
includeRecipientSetup=true when calling the fee estimate API so the
returned forwardFee covers the ATA creation cost.
- Encode the ATA creation fields in the
hook data of the burn transaction.
GET /v2/burn/USDC/fees/{sourceDomain}/{destDomain}?forward=true&includeRecipientSetup=true
For full details on this endpoint, see the
GET /v2/burn/USDC/fees API reference.
includeRecipientSetup only applies when the destination blockchain is Solana.
It has no effect for EVM destination blockchains.
Supported blockchains
For a full list of supported blockchains, see
CCTP Supported Blockchains.