Circle CCTP expliqué : transférer de l'USDC natif entre blockchains, et vers ou depuis Hyperliquid

Photo de Thomas CosiallsThomas Cosialls

Faire 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.

Bridge lock-and-mint vs burn-and-mint de CCTP : un coffre de bridge verrouille l'USDC et émet une reconnaissance de dette wrappée, tandis que CCTP brûle l'USDC sur la chaîne A et l'attestation de Circle permet de frapper de l'USDC natif sur la chaîne B
Un bridge lock-and-mint laisse un pot de miel derrière lui. CCTP ne laisse rien : l'offre se déplace, elle ne s'accumule pas.

Voici comment CCTP se compare aux deux familles de bridges qu'il remplace pour l'USDC :

Bridge lock-and-mintBridge à pools de liquiditéCCTP
Ce que vous recevezun jeton wrappé (USDC.e, axlUSDC...)de l'USDC natif, puisé dans un poolde l'USDC natif, fraîchement frappé
Capacitéce que le coffre garantitla profondeur du pool, qui peut s'épuiserillimitée en mode Standard
Prix1:1, mais la reconnaissance de dette peut décrocherslippage quand les pools sont déséquilibrésexactement 1:1, moins des frais connus
Tiers de confiancevalidateurs ou multisig du bridgeopérateurs du bridge et LPle service d'attestation de Circle
Fonds au repos à attaquerle coffreles poolsaucun

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.

Cycle de vie d'un message CCTP : l'application appelle depositForBurn sur la chaîne source, Circle Iris observe la destruction et signe une attestation, l'application la récupère via l'API et appelle receiveMessage sur la chaîne de destination, qui frappe l'USDC
Le cycle de vie CCTP. Avec le Forwarding Service, les étapes 5 à 7 se font sans vous.

Quatre composants le font fonctionner :

  • TokenMessengerV2, le point d'entrée sur chaque chaîne. Vous y appelez depositForBurn (ou depositForBurnWithHook). 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énement MessageSent sur la chaîne source et, sur la destination, vérifie l'attestation dans receiveMessage avant 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, Arbitrum 3, Base 6, HyperEVM 19. 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 :

ChampRôle
destinationDomaindomaine CCTP de la chaîne de destination
mintRecipientqui reçoit l'USDC frappé sur la destination
destinationCallerqui peut appeler receiveMessage ; zéro signifie n'importe qui
maxFeeles frais maximums acceptés, en sous-unités d'USDC ; les frais réels sont enregistrés dans feeExecuted
minFinalityThreshold1000 pour un Fast Transfer, 2000 pour un Standard
hookDatades 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.

Frise comparant les transferts Fast et Standard : le Fast est attesté après un ou deux blocs, en 8 à 20 secondes environ, en débitant l'allocation Fast Transfer jusqu'à la finalité ; le Standard attend 15 à 19 minutes la finalité d'Ethereum et il est gratuit
Le Fast Transfer échange de petits frais contre l'attente de la finalité. Sur les chaînes qui finalisent en quelques secondes, le Standard est déjà rapide.
  • 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 sur GET /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 sourceFast TransferFrais FastStandard Transfer
Ethereum~20 s1 bps~15-19 min
Arbitrum~8 s1,4 bps~15-19 min
Base~8 s1,3 bps~15-19 min
Solana~8 s1 bps~25 s
Linea~8 s13 bps~6-32 heures
HyperEVMn/an/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.

forward-hook.ts
// "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 :

ContratChaîneRôle
CctpForwarderHyperEVMreçoit les frappes CCTP destinées à HyperCore et les transfère
CoreDepositWalletHyperEVMdétient l'USDC natif, crédite les soldes HyperCore, brûle lors des retraits
CctpExtensionArbitrumdépôts en une transaction autorisés par une signature EIP-3009
CctpExtensionV2Arbitrumdépôts sponsorisés : un relayeur soumet la destruction et paie le gas
Contrats CCTP HyperCore (mainnet)
CctpForwarder      HyperEVM   0xb21D281DEdb17AE5B501F6AA8256fe38C4e45757
CoreDepositWallet  HyperEVM   0x6B9E773128f453f5c2C60935Ee2DE2CBc5390A24
CctpExtension      Arbitrum   0xA95d9c1F655341597C94393fDdc30cf3c08E4fcE
CctpExtensionV2    Arbitrum   0x3289e443a95B28Bcedacc4B33C689b0C9b84ffAB

Une 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

Flux de dépôt HyperCore : l'USDC est brûlé sur n'importe quelle chaîne CCTP avec un hook cctp-forward, Circle atteste et le Forwarding Service frappe sur HyperEVM vers le CctpForwarder, qui appelle CoreDepositWallet pour créditer le solde perps ou spot du destinataire sur HyperCore
Un dépôt est un transfert CCTP ordinaire vers HyperEVM, dont le hook indique au CctpForwarder où créditer les fonds sur HyperCore.

Un dépôt est un transfert CCTP standard vers HyperEVM, avec trois réglages spécifiques :

  1. destinationDomain vaut 19 (HyperEVM).
  2. mintRecipient et destinationCaller valent tous deux l'adresse du CctpForwarder. L'USDC est frappé vers le forwarder, et seul le forwarder peut finaliser le message.
  3. hookData indique au forwarder quoi faire : l'en-tête cctp-forward, suivi du destinataire sur HyperCore et du solde de destination, 0 pour les perps ou 4294967295 (le uint32 maximal) 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'
response
[
  {
    "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 :

hypercore.ts
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.

deposit-arbitrum.ts
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 :

deposit-base.ts
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

Flux de retrait HyperCore : l'utilisateur signe une action sendToEvmWithData envoyée à l'API exchange d'Hyperliquid, HyperCore débite le solde, CoreDepositWallet sur HyperEVM brûle l'USDC via CCTP, Circle atteste et le Forwarding Service frappe de l'USDC natif pour le destinataire sur la chaîne de destination
Un retrait commence par une action HyperCore signée et se termine par une frappe CCTP sur la chaîne de destination.

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 :

ChampValeur
sourceDex'' pour retirer des perps, 'spot' pour le spot
destinationRecipientadresse du destinataire sur la chaîne de destination
addressEncoding'hex' pour les chaînes EVM, 'base58' pour Solana
destinationChainIdle domaine CCTP, pas le chain ID EVM : 3 Arbitrum, 0 Ethereum, 6 Base, 5 Solana
gasLimitbudget de gas pour l'exécution sur la destination
data'0x' pour le forwarding automatique ; des octets personnalisés deviennent le hookData CCTP
signatureChainIdle chain ID utilisé dans le domaine EIP-712, en hexadécimal
noncel'horodatage courant en millisecondes

Voici le retrait complet, toujours avec viem :

withdraw.ts
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 :

RouteFrais de protocole CCTPFrais de forwarding
Arbitrum vers HyperCore (Fast)00,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)0comme en Fast, mais en minutes plutôt qu'en secondes
HyperCore vers Arbitrum, Base et la plupart des chaînes00,20 USDC, plus les frais HyperCore
HyperCore vers Ethereum01,20 USDC, plus les frais HyperCore
HyperCore vers Solana00,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 mintRecipient et destinationCaller. 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. Un transfer direct 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. 0 crédite les perps, 4294967295 le 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 maxFee trop juste dégrade en silence. Si maxFee ne 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 userRole d'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.

Plus d’articles

Image de couverture de Circle Nanopayments expliqué : des paiements USDC sans gas jusqu'à 0,000001 $

Circle Nanopayments expliqué : des paiements USDC sans gas jusqu'à 0,000001 $

Comment Circle Nanopayments utilise le règlement par lots de Gateway pour rendre économiques des paiements USDC inférieurs au centime, avec un tutoriel d'intégration complet en Node.js : verrouiller une API Express derrière un prix de 0,0001 $ et la payer depuis un agent IA, sans gas des deux côtés.

Lire la suite
Image de couverture de Verrouiller un endpoint d'API avec x402 : accepter les paiements USDC sur votre serveur Node.js

Verrouiller un endpoint d'API avec x402 : accepter les paiements USDC sur votre serveur Node.js

Un tutoriel complet et pratique pour monétiser une API Express avec le protocole de paiement x402 : verrouiller un endpoint derrière un prix en USDC sur Base, puis le payer par programmation depuis des clients JavaScript et Python.

Lire la suite

Prêt à faire passer votre projet au niveau supérieur ?

Contactez-nous dès aujourd’hui pour voir comment nous pouvons vous aider à atteindre vos objectifs dans la blockchain.