Circle CCTP expliqué : transférer de l'USDC natif entre blockchains, et vers ou depuis Hyperliquid
Thomas CosiallsFaire passer un dollar d'une blockchain à une autre revenait jusqu'ici à faire confiance au coffre de quelqu'un. Les bridges classiques verrouillent votre USDC sur la chaîne A et émettent une reconnaissance de dette sur la chaîne B, et ces coffres ont été la cible la plus rentable de la crypto : Ronin, Wormhole et Nomad ont perdu à eux trois plus d'un milliard de dollars. Les bridges à pools de liquidité évitent la reconnaissance de dette, mais ne transfèrent que ce que leurs pools contiennent, à un prix qui varie avec l'équilibre du pool.
Le Cross-Chain Transfer Protocol (CCTP) de Circle supprime le coffre. Comme Circle émet l'USDC, il peut faire ce qu'aucun bridge tiers ne peut faire : brûler l'USDC sur la chaîne source et frapper le même USDC natif sur la chaîne de destination. Pas de jeton wrappé, pas de pool, pas de slippage.
Cet article traite CCTP en deux temps. D'abord le protocole lui-même : ce qu'il est, le fonctionnement brûler-attester-frapper, les transferts Fast et Standard, les hooks et les chaînes supportées. Ensuite le cas qui nous occupe le plus ces derniers temps : déposer de l'USDC sur Hyperliquid et le retirer, avec du code TypeScript, le calcul des frais et les pièges qui peuvent bloquer des fonds définitivement.
Qu'est-ce que CCTP ?
CCTP est un utilitaire onchain sans permission opéré par Circle. N'importe qui peut appeler ses contrats : pas de clé d'API, pas de liste blanche, pas de compte à ouvrir. Il transfère l'USDC entre blockchains en le détruisant d'un côté et en le recréant de l'autre : l'offre totale d'USDC ne change jamais, et le jeton reçu est l'USDC canonique de la chaîne de destination.
La première version a été lancée en 2023. CCTP V2, sortie en mars 2025, a ajouté les deux fonctionnalités qui le rendent utile pour des applications temps réel : le Fast Transfer (un règlement en quelques secondes au lieu d'attendre la finalité) et les Hooks (des métadonnées qui déclenchent une logique sur la chaîne de destination). La V2 est désormais la version canonique ; la V1 est legacy et ne reste nécessaire que pour deux chaînes (Noble et Sui). CCTP a aussi été étendu à l'EURC et à des jetons tiers enregistrés, mais cet article se concentre sur l'USDC.

Voici comment CCTP se compare aux deux familles de bridges qu'il remplace pour l'USDC :
| Bridge lock-and-mint | Bridge à pools de liquidité | CCTP | |
|---|---|---|---|
| Ce que vous recevez | un jeton wrappé (USDC.e, axlUSDC...) | de l'USDC natif, puisé dans un pool | de l'USDC natif, fraîchement frappé |
| Capacité | ce que le coffre garantit | la profondeur du pool, qui peut s'épuiser | illimitée en mode Standard |
| Prix | 1:1, mais la reconnaissance de dette peut décrocher | slippage quand les pools sont déséquilibrés | exactement 1:1, moins des frais connus |
| Tiers de confiance | validateurs ou multisig du bridge | opérateurs du bridge et LP | le service d'attestation de Circle |
| Fonds au repos à attaquer | le coffre | les pools | aucun |
CCTP n'est pas trustless : le service d'attestation est opéré par Circle. Mais si vous détenez de l'USDC, vous faites déjà confiance à Circle pour l'honorer : CCTP n'ajoute donc aucun nouveau tiers de confiance. Les contrats ont été audités par ChainSecurity et OtterSec.
Le concept clé : brûler, attester, frapper
Chaque transfert CCTP, quelles que soient les chaînes concernées, suit le même rythme en trois temps.

Quatre composants le font fonctionner :
TokenMessengerV2, le point d'entrée sur chaque chaîne. Vous y appelezdepositForBurn(oudepositForBurnWithHook). Sur la plupart des chaînes EVM, il est déployé à la même adresse,0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d.TokenMinterV2, qui brûle réellement l'USDC sur la chaîne source et le frappe sur la chaîne de destination.MessageTransmitterV2, une couche générique de transmission de messages. Il émet l'événementMessageSentsur la chaîne source et, sur la destination, vérifie l'attestation dansreceiveMessageavant d'autoriser la frappe.- Iris, le service d'attestation offchain de Circle. Il surveille chaque destruction, attend le niveau de finalité demandé par le message, puis signe ce message. Son API se trouve sur
iris-api.circle.com.
Le cycle se lit donc ainsi : vous brûlez sur la chaîne source, ce qui émet un message (étapes 1-2) ; Iris attend la finalité requise et le signe (étapes 3-4) ; vous récupérez le message signé via GET /v2/messages/{sourceDomain}?transactionHash=... (étapes 5-6) ; puis vous le soumettez à receiveMessage sur la chaîne de destination, qui vérifie la signature, marque le nonce comme utilisé et frappe l'USDC pour le destinataire (étapes 7-8).
Quelques détails de conception comptent dès que l'on écrit du code :
- Des domaines, pas des chain IDs. CCTP identifie les chaînes par ses propres numéros de domaine : Ethereum est
0, Arbitrum3, Base6, HyperEVM19. Ils n'ont rien à voir avec les chain IDs EVM. - Les adresses sont en
bytes32. CCTP couvre des chaînes EVM et non EVM, donc les destinataires sont des valeurs sur 32 octets. Une adresse EVM est simplement complétée par des zéros à gauche. - Chaque nonce ne frappe qu'une fois. La destination enregistre chaque nonce traité, ce qui rend impossible le rejeu d'une attestation.
- Les attestations expirent. Un message de destruction porte un bloc d'expiration environ 24 heures plus tard. Une destruction expirée n'est pas perdue :
POST /v2/reattest/{nonce}fournit une nouvelle attestation.
Les champs que vous renseignerez vraiment se trouvent dans l'en-tête du message et dans le corps du message de destruction :
| Champ | Rôle |
|---|---|
destinationDomain | domaine CCTP de la chaîne de destination |
mintRecipient | qui reçoit l'USDC frappé sur la destination |
destinationCaller | qui peut appeler receiveMessage ; zéro signifie n'importe qui |
maxFee | les frais maximums acceptés, en sous-unités d'USDC ; les frais réels sont enregistrés dans feeExecuted |
minFinalityThreshold | 1000 pour un Fast Transfer, 2000 pour un Standard |
hookData | des octets arbitraires transmis à la destination |
Fast Transfer vs Standard Transfer
Le seuil de finalité est le principal réglage de CCTP. Sur Ethereum et ses rollups, une transaction est incluse dans un bloc en quelques secondes, mais elle ne devient irréversible qu'une fois finalisé le bloc Ethereum qui la contient, environ 65 blocs ou 15 à 19 minutes plus tard. Iris peut signer à l'un ou l'autre de ces moments.

- Le Standard Transfer (
minFinalityThreshold: 2000) attend la finalité définitive. Il est gratuit sur toutes les chaînes aujourd'hui, et lent partout où la finalité est lente : 15 à 19 minutes depuis Ethereum, Arbitrum, Base ou OP Mainnet, 2 à 4 heures depuis Starknet, et 6 à 32 heures depuis Linea. - Le Fast Transfer (
minFinalityThreshold: 1000) est attesté dès que la destruction est confirmée, généralement en 8 à 20 secondes. Circle porte entre-temps le risque de réorganisation, couvert par une allocation Fast Transfer globale : chaque destruction rapide débite l'allocation, et le montant est recrédité quand la destruction atteint la finalité définitive. Si l'allocation venait à s'épuiser, il faut attendre ou repasser en Standard. L'allocation restante se consulte surGET /v2/fastBurn/USDC/allowance; elle se comptait en dizaines de millions d'USDC au moment de l'écriture.
Les frais du Fast Transfer sont exprimés en points de base du montant et prélevés au moment de la frappe :
| Chaîne source | Fast Transfer | Frais Fast | Standard Transfer |
|---|---|---|---|
| Ethereum | ~20 s | 1 bps | ~15-19 min |
| Arbitrum | ~8 s | 1,4 bps | ~15-19 min |
| Base | ~8 s | 1,3 bps | ~15-19 min |
| Solana | ~8 s | 1 bps | ~25 s |
| Linea | ~8 s | 13 bps | ~6-32 heures |
| HyperEVM | n/a | n/a | ~5 s |
Les chaînes marquées n/a ne proposent pas le Fast Transfer en source, car leur Standard Transfer est déjà aussi rapide. Les frais évoluent : ne les codez jamais en dur. Lisez-les sur GET /v2/burn/USDC/fees/{source}/{destination} et fixez maxFee à partir de la réponse, avec une petite marge. Un maxFee inférieur aux frais en vigueur peut transformer discrètement un Fast Transfer en Standard.
Hooks et Forwarding Service
Les hooks sont les octets hookData attachés à une destruction. CCTP ne les exécute pas : il les transmet intacts à la destination, et le contrat qui reçoit le message décide de ce qu'il en fait. C'est voulu : le cœur du protocole reste réduit, et les intégrateurs construisent à leurs conditions des flux « bridger puis faire X » (déposer dans un vault, ouvrir une position, régler une facture).
Le hook le plus utile est celui que Circle lit lui-même. Faites commencer les données du hook par les octets magiques cctp-forward et le Forwarding Service de Circle prend en charge les étapes 5 à 7 du cycle : il récupère l'attestation et soumet la frappe sur la chaîne de destination, en payant le gas de destination. Un transfert CCTP devient une seule transaction sur la chaîne source, sans relayeur à opérer ni jeton de gas à détenir sur la destination.
// "cctp-forward" magic + version 0 + empty payload: just forward, nothing else
const forwardHookData = '0x636374702d666f72776172640000000000000000000000000000000000000000'Le service facture des frais qui couvrent le gas de destination plus des frais de service, déduits du transfert (ou payés d'avance sur la chaîne source sur les routes compatibles, si le destinataire doit recevoir le montant exact). Les frais de service sont de 0,05 $ sur la plupart des routes, avec des tarifs dédiés pour Hyperliquid que nous détaillons plus bas.
Quelles chaînes CCTP supporte-t-il ?
CCTP V2 fonctionne sur une trentaine de blockchains, EVM comme non EVM. Celles que vous utiliserez le plus probablement sont Ethereum, Arbitrum, Base, OP Mainnet, Polygon PoS, Avalanche, Solana, Linea, Unichain et HyperEVM, la chaîne qui relie CCTP à Hyperliquid. Toutes les chaînes supportées peuvent recevoir des transferts, testnets compris, et Circle tient à jour la liste complète avec les identifiants de domaine.
CCTP sur Hyperliquid : HyperEVM et HyperCore
Hyperliquid est un L1 unique doté de deux environnements d'exécution. HyperCore est le moteur d'échange natif : les carnets d'ordres perps et spot, la marge et les soldes des comptes ; c'est là que se passe le trading, et ce n'est pas une EVM. HyperEVM est une EVM généraliste qui tourne sur la même chaîne et le même consensus, avec le HYPE comme gas. Les deux partagent leur état, ce qui permet à un contrat sur HyperEVM de créditer un solde de trading sur HyperCore.
Circle a lancé l'USDC natif et CCTP V2 sur HyperEVM (domaine 19) le 16 septembre 2025, puis a activé dans les semaines suivantes les dépôts et retraits CCTP pour HyperCore. Auparavant, la voie d'entrée canonique était le contrat de bridge d'Hyperliquid sur Arbitrum, sécurisé par son ensemble de validateurs. Avec CCTP, chaque chaîne CCTP devient une rampe d'accès directe.
Comme CCTP ne peut pas frapper sur un moteur non EVM, Circle a ajouté quelques contrats autour du protocole standard :
| Contrat | Chaîne | Rôle |
|---|---|---|
CctpForwarder | HyperEVM | reçoit les frappes CCTP destinées à HyperCore et les transfère |
CoreDepositWallet | HyperEVM | détient l'USDC natif, crédite les soldes HyperCore, brûle lors des retraits |
CctpExtension | Arbitrum | dépôts en une transaction autorisés par une signature EIP-3009 |
CctpExtensionV2 | Arbitrum | dépôts sponsorisés : un relayeur soumet la destruction et paie le gas |
CctpForwarder HyperEVM 0xb21D281DEdb17AE5B501F6AA8256fe38C4e45757
CoreDepositWallet HyperEVM 0x6B9E773128f453f5c2C60935Ee2DE2CBc5390A24
CctpExtension Arbitrum 0xA95d9c1F655341597C94393fDdc30cf3c08E4fcE
CctpExtensionV2 Arbitrum 0x3289e443a95B28Bcedacc4B33C689b0C9b84ffABUne subtilité à garder en tête : un solde USDC sur HyperCore est un crédit au niveau du protocole, pas un jeton. L'USDC natif réside dans le contrat CoreDepositWallet sur HyperEVM, où il adosse ces crédits un pour un, et c'est un retrait qui reconvertit un crédit en véritable USDC.
Déposer de l'USDC sur Hyperliquid

Un dépôt est un transfert CCTP standard vers HyperEVM, avec trois réglages spécifiques :
destinationDomainvaut19(HyperEVM).mintRecipientetdestinationCallervalent tous deux l'adresse duCctpForwarder. L'USDC est frappé vers le forwarder, et seul le forwarder peut finaliser le message.hookDataindique au forwarder quoi faire : l'en-têtecctp-forward, suivi du destinataire sur HyperCore et du solde de destination,0pour les perps ou4294967295(leuint32maximal) pour le spot.
Le Forwarding Service de Circle frappe alors sur HyperEVM, le forwarder dépose l'USDC dans CoreDepositWallet pour le compte du destinataire, et le solde apparaît sur HyperCore.
Commencez par demander à l'API de frais ce que coûte la route. Le paramètre hyperCoreDeposit=true tarife le transfert vers HyperCore :
curl 'https://iris-api.circle.com/v2/burn/USDC/fees/3/19?forward=true&hyperCoreDeposit=true'
[
{
"finalityThreshold": 1000,
"minimumFee": 0,
"forwardFee": { "low": 200000, "med": 200000, "high": 200000 }
},
{
"finalityThreshold": 2000,
"minimumFee": 0,
"forwardFee": { "low": 200000, "med": 200000, "high": 200000 }
}
]Depuis Arbitrum, la route bénéficie d'un traitement particulier : le Fast Transfer est gratuit (minimumFee: 0) et le forwarding coûte 0,20 USDC fixe (200000 sous-unités, l'USDC ayant 6 décimales). Déposez 10 USDC et 9,80 arrivent sur HyperCore en quelques secondes. Depuis les autres chaînes, vous payez les frais Fast Transfer de la chaîne source plus des frais de forwarding qui incluent le gas HyperEVM, environ 0,25 USDC depuis Base ou Ethereum au moment de l'écriture.
Utilitaires partagés : données du hook et devis de frais
Les scripts ci-dessous utilisent viem et s'exécutent directement avec Node.js 22.6+ (npm install viem, puis node --env-file=.env <script>.ts). D'abord, les briques dont chaque dépôt a besoin :
import { concat, pad, stringToHex, toHex, type Address, type Hex } from 'viem'
export const IRIS_API = 'https://iris-api.circle.com'
export const HYPEREVM_DOMAIN = 19
export const FAST_TRANSFER = 1000
// CctpForwarder on HyperEVM: mints from CCTP land here, then get credited on HyperCore
export const CCTP_FORWARDER: Address = '0xb21D281DEdb17AE5B501F6AA8256fe38C4e45757'
export const PERPS_DEX = 0
export const SPOT_DEX = 0xffffffff // type(uint32).max
/**
* Forwarding hook read by CctpForwarder (version 0):
* bytes 0-23 "cctp-forward" magic, right-padded with zeros
* bytes 24-27 version = 0
* bytes 28-31 payload length = 24
* bytes 32-51 HyperCore recipient
* bytes 52-55 destination dex (0 = perps, uint32 max = spot)
*/
export function encodeHyperCoreHook(recipient: Address, dex: number = PERPS_DEX): Hex {
return concat([
stringToHex('cctp-forward', { size: 24 }),
toHex(0, { size: 4 }),
toHex(24, { size: 4 }),
recipient,
toHex(dex, { size: 4 }),
])
}
export const toBytes32 = (address: Address): Hex => pad(address, { size: 32 })
type FeeQuote = {
finalityThreshold: number
minimumFee: number // basis points of the amount
forwardFee: { low: number; med: number; high: number } // USDC subunits
}
/** maxFee for a Fast Transfer to HyperCore: protocol fee (+20% buffer) plus the forwarding fee. */
export async function quoteMaxFee(sourceDomain: number, amount: bigint): Promise<bigint> {
const url = `${IRIS_API}/v2/burn/USDC/fees/${sourceDomain}/${HYPEREVM_DOMAIN}?forward=true&hyperCoreDeposit=true`
const res = await fetch(url)
if (!res.ok) throw new Error(`Fee quote failed with HTTP ${res.status}`)
const quotes = (await res.json()) as FeeQuote[]
const fast = quotes.find((q) => q.finalityThreshold === FAST_TRANSFER)
if (!fast) throw new Error('No Fast Transfer quote for this route')
const protocolFee = (amount * BigInt(Math.round(fast.minimumFee * 100))) / 1_000_000n
return (protocolFee * 120n) / 100n + BigInt(fast.forwardFee.med)
}L'encodeur produit exactement les 56 octets attendus par le forwarder ; nous avons vérifié sa sortie octet par octet avec l'implémentation de référence de Circle. L'utilitaire de frais convertit les points de base en sous-unités d'USDC (le * 100 garde exacts, en arithmétique entière, des taux fractionnaires comme 1,3 bps), ajoute une marge de 20 % sur les frais de protocole comme le recommande Circle, puis ajoute les frais de forwarding. low, med et high arbitrent entre coût et rapidité d'inclusion sur la destination ; sur la route Arbitrum, les trois valent les mêmes 0,20 USDC fixes.
Déposer depuis Arbitrum en une seule transaction
Sur Arbitrum, le contrat CctpExtension accepte une signature EIP-3009 ReceiveWithAuthorization à la place d'une approbation ERC-20 : le dépôt complet tient en une signature et une transaction.
import {
createPublicClient,
createWalletClient,
http,
parseAbi,
parseSignature,
parseUnits,
toHex,
type Address,
} from 'viem'
import { arbitrum } from 'viem/chains'
import { privateKeyToAccount } from 'viem/accounts'
import {
CCTP_FORWARDER,
FAST_TRANSFER,
HYPEREVM_DOMAIN,
PERPS_DEX,
encodeHyperCoreHook,
quoteMaxFee,
toBytes32,
} from './hypercore.ts'
const CCTP_EXTENSION: Address = '0xA95d9c1F655341597C94393fDdc30cf3c08E4fcE'
const USDC: Address = '0xaf88d065e77c8cC2239327C5EDb3A432268e5831'
const ARBITRUM_DOMAIN = 3
const extensionAbi = parseAbi([
'struct ReceiveWithAuthorizationData { uint256 amount; uint256 authValidAfter; uint256 authValidBefore; bytes32 authNonce; uint8 v; bytes32 r; bytes32 s; }',
'struct DepositForBurnData { uint256 amount; uint32 destinationDomain; bytes32 mintRecipient; bytes32 destinationCaller; uint256 maxFee; uint32 minFinalityThreshold; bytes hookData; }',
'function batchDepositForBurnWithAuth(ReceiveWithAuthorizationData _receiveWithAuthorizationData, DepositForBurnData _depositForBurnData)',
])
const privateKey = process.env.PRIVATE_KEY as `0x${string}` | undefined
if (!privateKey) throw new Error('PRIVATE_KEY not configured')
const account = privateKeyToAccount(privateKey)
const publicClient = createPublicClient({ chain: arbitrum, transport: http() })
const walletClient = createWalletClient({ account, chain: arbitrum, transport: http() })
const amount = parseUnits('10', 6) // 10 USDC
const maxFee = await quoteMaxFee(ARBITRUM_DOMAIN, amount)
// 1. Authorize the extension to pull the USDC: an offchain EIP-3009 signature, no approve tx
const validAfter = 0n
const validBefore = BigInt(Math.floor(Date.now() / 1000) + 3600)
const nonce = toHex(crypto.getRandomValues(new Uint8Array(32)))
const signature = await walletClient.signTypedData({
domain: { name: 'USD Coin', version: '2', chainId: arbitrum.id, verifyingContract: USDC },
types: {
ReceiveWithAuthorization: [
{ name: 'from', type: 'address' },
{ name: 'to', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'validAfter', type: 'uint256' },
{ name: 'validBefore', type: 'uint256' },
{ name: 'nonce', type: 'bytes32' },
],
},
primaryType: 'ReceiveWithAuthorization',
message: {
from: account.address,
to: CCTP_EXTENSION,
value: amount,
validAfter,
validBefore,
nonce,
},
})
const { r, s, yParity } = parseSignature(signature)
// 2. Burn on Arbitrum, mint to the CctpForwarder on HyperEVM, credit our HyperCore perps balance
const hash = await walletClient.writeContract({
address: CCTP_EXTENSION,
abi: extensionAbi,
functionName: 'batchDepositForBurnWithAuth',
args: [
{
amount,
authValidAfter: validAfter,
authValidBefore: validBefore,
authNonce: nonce,
v: yParity + 27,
r,
s,
},
{
amount,
destinationDomain: HYPEREVM_DOMAIN,
mintRecipient: toBytes32(CCTP_FORWARDER), // must be the forwarder
destinationCaller: toBytes32(CCTP_FORWARDER), // must be the forwarder
maxFee,
minFinalityThreshold: FAST_TRANSFER,
hookData: encodeHyperCoreHook(account.address, PERPS_DEX),
},
],
})
const receipt = await publicClient.waitForTransactionReceipt({ hash })
console.log(`Burn ${receipt.status} on Arbitrum: ${hash}`)Le wallet n'a besoin que d'USDC et d'un peu d'ETH pour le gas sur Arbitrum ; rien sur Hyperliquid. Le destinataire passé à encodeHyperCoreHook peut être n'importe quelle adresse : c'est ainsi qu'une application crédite le compte HyperCore d'un utilisateur depuis un wallet de trésorerie. Pour les utilisateurs qui détiennent de l'USDC mais pas d'ETH, CctpExtensionV2 va plus loin : l'utilisateur signe seulement, et un relayeur soumet la destruction et paie le gas.
Quelques secondes après le reçu, les fonds sont sur HyperCore. L'API info publique d'Hyperliquid affiche le solde perps :
curl -s https://api.hyperliquid.xyz/info \ -H 'Content-Type: application/json' \ -d '{"type":"clearinghouseState","user":"0xYourAddress"}'
Utilisez "type":"spotClearinghouseState" pour le solde spot. Pour suivre le transfert lui-même, GET /v2/messages/3?transactionHash=0x... sur l'API Iris renvoie le message et le statut de son attestation.
Déposer depuis n'importe quelle autre chaîne
Toutes les autres chaînes CCTP passent par le TokenMessengerV2 standard : approuvez-le pour dépenser l'USDC, puis appelez depositForBurnWithHook avec les mêmes réglages de forwarder. Cette fois, nous créditons le solde spot depuis Base :
import { createPublicClient, createWalletClient, http, parseAbi, parseUnits, type Address } from 'viem'
import { base } from 'viem/chains'
import { privateKeyToAccount } from 'viem/accounts'
import {
CCTP_FORWARDER,
FAST_TRANSFER,
HYPEREVM_DOMAIN,
SPOT_DEX,
encodeHyperCoreHook,
quoteMaxFee,
toBytes32,
} from './hypercore.ts'
const TOKEN_MESSENGER_V2: Address = '0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d' // same on most EVM chains
const USDC: Address = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' // USDC on Base
const BASE_DOMAIN = 6
const abi = parseAbi([
'function approve(address spender, uint256 amount) returns (bool)',
'function depositForBurnWithHook(uint256 amount, uint32 destinationDomain, bytes32 mintRecipient, address burnToken, bytes32 destinationCaller, uint256 maxFee, uint32 minFinalityThreshold, bytes hookData)',
])
const privateKey = process.env.PRIVATE_KEY as `0x${string}` | undefined
if (!privateKey) throw new Error('PRIVATE_KEY not configured')
const account = privateKeyToAccount(privateKey)
const publicClient = createPublicClient({ chain: base, transport: http() })
const walletClient = createWalletClient({ account, chain: base, transport: http() })
const amount = parseUnits('10', 6)
const maxFee = await quoteMaxFee(BASE_DOMAIN, amount)
const approval = await walletClient.writeContract({
address: USDC,
abi,
functionName: 'approve',
args: [TOKEN_MESSENGER_V2, amount],
})
await publicClient.waitForTransactionReceipt({ hash: approval })
const hash = await walletClient.writeContract({
address: TOKEN_MESSENGER_V2,
abi,
functionName: 'depositForBurnWithHook',
args: [
amount,
HYPEREVM_DOMAIN,
toBytes32(CCTP_FORWARDER),
USDC,
toBytes32(CCTP_FORWARDER),
maxFee,
FAST_TRANSFER,
encodeHyperCoreHook(account.address, SPOT_DEX), // this time, credit the spot balance
],
})
console.log(`Burn submitted on Base: ${hash}`)D'une chaîne EVM à l'autre, seuls la chaîne, l'adresse de l'USDC et le domaine source changent. Solana fonctionne de la même manière sur le principe, avec ses propres programmes CCTP.
Vous détenez déjà de l'USDC sur HyperEVM ?
Alors vous n'avez pas besoin de CCTP. Approuvez le CoreDepositWallet et appelez l'une de ses fonctions de dépôt : deposit(amount, dex) crédite l'appelant, depositFor(recipient, amount, dex) crédite quelqu'un d'autre, et depositWithAuth accepte une signature EIP-3009 à la place d'une approbation.
# Approve, then deposit 100 USDC (6 decimals) to the perps balance (dex 0) cast send $USDC_HYPEREVM "approve(address,uint256)" 0x6B9E773128f453f5c2C60935Ee2DE2CBc5390A24 100000000 \ --rpc-url $HYPEREVM_RPC --private-key $PRIVATE_KEY cast send 0x6B9E773128f453f5c2C60935Ee2DE2CBc5390A24 "deposit(uint256,uint32)" 100000000 0 \ --rpc-url $HYPEREVM_RPC --private-key $PRIVATE_KEY
N'envoyez jamais d'USDC à CoreDepositWallet avec un simple transfer : cela ne déclenche aucun dépôt, et les jetons sont bloqués définitivement.
Retirer de l'USDC d'Hyperliquid

Un retrait emprunte le même chemin en sens inverse, mais il ne commence pas par une transaction EVM. Vous signez une action sendToEvmWithData en EIP-712 et l'envoyez à l'endpoint /exchange d'Hyperliquid. HyperCore débite votre solde, CoreDepositWallet brûle l'USDC correspondant via CCTP sur HyperEVM, Iris atteste (HyperEVM finalise en cinq secondes environ, les retraits sont donc rapides par défaut), et le Forwarding Service frappe de l'USDC natif pour votre destinataire sur la chaîne de destination. Vous signez un seul message, sans détenir de HYPE ni de gas sur la destination.
L'action prend les champs suivants :
| Champ | Valeur |
|---|---|
sourceDex | '' pour retirer des perps, 'spot' pour le spot |
destinationRecipient | adresse du destinataire sur la chaîne de destination |
addressEncoding | 'hex' pour les chaînes EVM, 'base58' pour Solana |
destinationChainId | le domaine CCTP, pas le chain ID EVM : 3 Arbitrum, 0 Ethereum, 6 Base, 5 Solana |
gasLimit | budget de gas pour l'exécution sur la destination |
data | '0x' pour le forwarding automatique ; des octets personnalisés deviennent le hookData CCTP |
signatureChainId | le chain ID utilisé dans le domaine EIP-712, en hexadécimal |
nonce | l'horodatage courant en millisecondes |
Voici le retrait complet, toujours avec viem :
import { parseSignature } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
const privateKey = process.env.PRIVATE_KEY as `0x${string}` | undefined
if (!privateKey) throw new Error('PRIVATE_KEY not configured')
const account = privateKeyToAccount(privateKey)
const nonce = Date.now()
const message = {
hyperliquidChain: 'Mainnet',
token: 'USDC',
amount: '10', // human-readable USDC, as a string
sourceDex: '', // '' = perps balance, 'spot' = spot balance
destinationRecipient: account.address, // receive on our own address
addressEncoding: 'hex', // 'base58' for a Solana recipient
destinationChainId: 3, // a CCTP domain, not an EVM chain id: 3 = Arbitrum
gasLimit: 200_000n,
data: '0x', // empty = let the Forwarding Service mint on the destination for us
nonce: BigInt(nonce),
} as const
const signature = await account.signTypedData({
domain: {
name: 'HyperliquidSignTransaction',
version: '1',
chainId: 42161, // must match signatureChainId below
verifyingContract: '0x0000000000000000000000000000000000000000',
},
types: {
'HyperliquidTransaction:SendToEvmWithData': [
{ name: 'hyperliquidChain', type: 'string' },
{ name: 'token', type: 'string' },
{ name: 'amount', type: 'string' },
{ name: 'sourceDex', type: 'string' },
{ name: 'destinationRecipient', type: 'string' },
{ name: 'addressEncoding', type: 'string' },
{ name: 'destinationChainId', type: 'uint32' },
{ name: 'gasLimit', type: 'uint64' },
{ name: 'data', type: 'bytes' },
{ name: 'nonce', type: 'uint64' },
],
},
primaryType: 'HyperliquidTransaction:SendToEvmWithData',
message,
})
const { r, s, yParity } = parseSignature(signature)
const res = await fetch('https://api.hyperliquid.xyz/exchange', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: {
type: 'sendToEvmWithData',
signatureChainId: '0xa4b1', // 42161 in hex
...message,
gasLimit: Number(message.gasLimit),
nonce,
},
nonce,
signature: { r, s, v: yParity + 27 },
}),
})
const result = await res.json()
if (!res.ok || result.status !== 'ok') {
throw new Error(`Withdrawal rejected: ${JSON.stringify(result)}`)
}
console.log('Withdrawal accepted by HyperCore:', result)Le message EIP-712 et l'action JSON portent les mêmes valeurs ; seules diffèrent la représentation (bigint pour les champs uint64 signés, nombres simples en JSON) et les deux champs propres à l'action, type et signatureChainId. Pour viser une autre chaîne, remplacez destinationChainId par son domaine CCTP ; pour le testnet, passez hyperliquidChain à 'Testnet' et envoyez la requête à api.hyperliquid-testnet.xyz.
Un retrait supporte deux frais : des frais côté HyperCore et, avec le forwarding, les frais du Forwarding Service, qui dépendent de la destination : 0,20 USDC vers la plupart des chaînes, 1,20 USDC vers Ethereum et 0,50 USDC vers Solana. Le montant exact des frais de forwarding se lit sur le contrat CoreDepositWallet. Si le montant ne couvre pas les frais, la destruction sur HyperEVM échoue.
Deux derniers détails. Le destinationCaller CCTP est toujours l'adresse zéro pour les retraits : n'importe qui peut finaliser la frappe. Et si vous placez vos propres octets dans data au lieu de 0x, ils deviennent le hook data CCTP, le forwarding est désactivé, et finaliser la frappe sur la destination devient votre affaire (ou celle de n'importe qui).
Les frais en un coup d'œil
En septembre 2026, pour un transfert de 10 USDC :
| Route | Frais de protocole CCTP | Frais de forwarding |
|---|---|---|
| Arbitrum vers HyperCore (Fast) | 0 | 0,20 USDC fixe |
| Base vers HyperCore (Fast) | 1,3 bps | ~0,25 USDC, cotés en direct |
| Ethereum vers HyperCore (Fast) | 1 bps | ~0,25 USDC, cotés en direct |
| N'importe quelle chaîne vers HyperCore (Standard) | 0 | comme en Fast, mais en minutes plutôt qu'en secondes |
| HyperCore vers Arbitrum, Base et la plupart des chaînes | 0 | 0,20 USDC, plus les frais HyperCore |
| HyperCore vers Ethereum | 0 | 1,20 USDC, plus les frais HyperCore |
| HyperCore vers Solana | 0 | 0,50 USDC, plus les frais HyperCore |
Les nouveaux comptes HyperCore paient aussi des frais d'activation uniques de 1 USDC, prélevés par Hyperliquid (et non par Circle) lors de la première action sortante du compte.
Les pièges qui peuvent coûter des fonds
La plupart des erreurs CCTP sont bruyantes : une autorisation expirée ou une approbation manquante échoue sur la chaîne source, et rien ne bouge. L'intégration Hyperliquid en ajoute quelques-unes silencieuses.
- Le forwarder doit être à la fois
mintRecipientetdestinationCaller. Mettez autre chose dans l'un des deux lors d'un dépôt HyperCore et l'USDC est frappé à un endroit incapable de le déposer. La documentation de Circle est explicite : ces fonds ne peuvent pas être récupérés. Gardez ces valeurs en constantes, jamais en saisie utilisateur. - Aucun transfert simple vers
CoreDepositWallet. Seules ses fonctions de dépôt créditent HyperCore. Untransferdirect bloque les jetons. - Les frais d'activation frappent à la sortie, pas à l'entrée. Les dépôts de toute taille réussissent, même sous 1 USDC. Mais le premier retrait ou transfert d'un nouveau compte exige au moins 1 USDC pour les frais d'activation, en plus des frais de retrait, et échoue sinon. Si votre système crée des comptes de façon programmatique, vous pouvez les pré-activer.
- Perps ou spot, c'est le hook qui décide.
0crédite les perps,4294967295le spot, et toute autre valeur retombe sur le spot. Un bot de trading qui attend sa marge en perps ne la trouvera pas en spot. - Un
maxFeetrop juste dégrade en silence. SimaxFeene couvre pas à la fois les frais Fast Transfer et le forwarding, CCTP conserve le forwarding et bascule en Standard Transfer : depuis Arbitrum, cela fait 15 à 19 minutes au lieu de 8 secondes. Même chose si l'allocation Fast Transfer est épuisée. - Le testnet a ses propres règles. Un destinataire sur le testnet HyperCore doit déjà exister sur mainnet, ne peut recevoir que 1 000 USDC de testnet au maximum, et les transferts vers des adresses inconnues échouent silencieusement. Vérifiez une adresse au préalable avec la requête info
userRoled'Hyperliquid. - Respectez la limite de débit d'Iris. L'API autorise 40 requêtes par seconde ; au-delà, vous êtes bloqué cinq minutes. Interrogez les attestations toutes les quelques secondes, pas dans une boucle serrée.
Questions fréquentes
CCTP est-il un bridge ? Fonctionnellement, oui : il déplace de la valeur entre chaînes. Structurellement, non : rien n'est verrouillé ni wrappé. L'USDC est brûlé d'un côté et frappé de l'autre par son propre émetteur, il n'y a donc ni pool ni coffre à vider.
Combien de temps prend un dépôt sur Hyperliquid ? En Fast Transfer depuis Arbitrum ou Base, environ 8 secondes pour l'attestation, plus la transaction de forwarding sur HyperEVM. En Standard Transfer depuis un rollup Ethereum, 15 à 19 minutes. Les retraits sont rapides par défaut, car HyperEVM finalise en quelques secondes.
Puis-je déposer directement sur mon solde spot ?
Oui. Mettez les quatre derniers octets des données du hook à 4294967295 (le uint32 maximal) au lieu de 0.
L'USDC sur HyperCore est-il du « vrai » USDC ?
Les soldes HyperCore sont des crédits au niveau du protocole. Chacun est adossé à de l'USDC natif détenu dans le contrat CoreDepositWallet sur HyperEVM, et un retrait l'échange contre de l'USDC natif sur n'importe quelle chaîne CCTP.
Ai-je besoin de HYPE ou de gas sur Hyperliquid ?
Non. Un dépôt ne coûte du gas que sur la chaîne source (et même ce gas peut être sponsorisé via CctpExtensionV2 sur Arbitrum). Un retrait est une action HyperCore signée, dont les frais sont payés en USDC.
Où cela s'inscrit
CCTP a transformé l'USDC, d'un jeton présent sur de nombreuses chaînes, en un solde unique qui circule entre elles en quelques secondes, à un prix connu, sans bridge auquel se fier. Pour Hyperliquid, cela signifie que le capital peut circuler entre n'importe quelle grande chaîne et le carnet d'ordres sans détour, et sans que personne n'ait à cliquer dans l'interface d'un bridge.
Cet article est né d'un projet client dans lequel nous utilisons CCTP pour bridger de l'USDC entre des chaînes EVM et Hyperliquid.
Chez Etherwave Labs, nous construisons sur les chaînes EVM et sur Hyperliquid, et nous sommes disponibles pour vous accompagner sur tout ce qui touche à ces écosystèmes : robots de trading et stratégies automatisées, stratégies delta-neutres (par exemple la couverture de positions spot ou de liquidité avec des perps Hyperliquid) et automatisation onchain en général, du rééquilibrage de trésorerie cross-chain avec CCTP aux keepers et à la gestion de positions. Découvrez notre approche de l'automatisation sur notre page de service de gestion automatisée de liquidité, lisez notre introduction aux carnets d'ordres face aux AMM, et si vous avez une stratégie ou un flux à automatiser, parlons-en.

