Circle CCTP explicado: mover USDC nativo entre cadenas, y dentro y fuera de Hyperliquid
Mover un dólar de una blockchain a otra solía implicar confiar en el vault de alguien. Los bridges clásicos bloquean tu USDC en la cadena A y acuñan un pagaré (un IOU) en la cadena B, y esos vaults han sido el objetivo más rentable del mundo cripto: solo entre Ronin, Wormhole y Nomad se perdieron más de mil millones de dólares. Los bridges basados en pools de liquidez evitan el pagaré, pero solo pueden mover lo que contienen sus pools, y a un precio que varía con el pool.
El Cross-Chain Transfer Protocol (CCTP) de Circle saca el vault de la ecuación. Como Circle es quien emite el USDC, puede hacer lo que ningún bridge de terceros puede: quemar USDC en la cadena de origen y acuñar el mismo USDC nativo en la de destino. Sin tokens envueltos (wrapped), sin pools, sin slippage.
Este artículo aborda CCTP en dos partes. Primero, el protocolo en sí: qué es, cómo funciona el ciclo quemar-atestar-acuñar, las transferencias Fast y Standard, los hooks y las cadenas en las que funciona. Después, el caso que más nos ocupa últimamente: depositar USDC en Hyperliquid y volver a retirarlo, con código TypeScript, el cálculo de las comisiones y las trampas que pueden dejar fondos bloqueados para siempre.
¿Qué es CCTP?
CCTP es una utilidad on-chain sin permisos (permissionless) operada por Circle. Cualquiera puede llamar a sus contratos: no hay clave de API, ni lista blanca, ni cuenta que abrir. Mueve USDC entre blockchains destruyéndolo en un lado y recreándolo en el otro, de modo que el suministro total de USDC nunca cambia y el token que recibes es el USDC canónico de la cadena de destino.
La primera versión se lanzó en 2023. CCTP V2, publicada en marzo de 2025, añadió las dos funciones que lo hacen útil para aplicaciones en tiempo real: Fast Transfer (liquidación en segundos en lugar de esperar a la finalidad) y Hooks (metadatos que activan lógica en el destino). V2 es ahora la versión canónica; V1 es una versión heredada (legacy) que solo sigue siendo necesaria para un par de cadenas (Noble y Sui). CCTP también se ha extendido a EURC y a tokens de terceros registrados, pero este artículo se centra en USDC.

Así se compara CCTP con las dos familias de bridges a las que sustituye para USDC:
| Bridge lock-and-mint | Bridge con pools de liquidez | CCTP | |
|---|---|---|---|
| Lo que recibes | un token envuelto (USDC.e, axlUSDC...) | USDC nativo, de un pool | USDC nativo, recién acuñado |
| Capacidad | lo que respalde el vault | la profundidad del pool, que puede agotarse | ilimitada en modo Standard |
| Precio | 1:1, pero el pagaré puede perder la paridad | slippage cuando los pools están desequilibrados | exactamente 1:1, menos una comisión conocida |
| Parte de confianza | validadores o multisig del bridge | operadores del bridge y LP | el servicio de atestación de Circle |
| Fondos en reposo que atacar | el vault | los pools | ninguno |
CCTP no es trustless: el servicio de atestación lo opera Circle. Pero si tienes USDC, ya confías en que Circle responda por él, así que CCTP no añade una nueva parte de confianza a tu stack. Los contratos fueron auditados por ChainSecurity y OtterSec.
El concepto clave: quemar, atestar, acuñar
Toda transferencia CCTP, sean cuales sean las cadenas implicadas, sigue el mismo ritmo en tres tiempos.

Cuatro componentes lo hacen funcionar:
TokenMessengerV2, el punto de entrada en cada cadena. En él llamas adepositForBurn(o adepositForBurnWithHook). En la mayoría de las cadenas EVM está desplegado en la misma dirección,0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d.TokenMinterV2, que es el que realmente quema el USDC en la cadena de origen y lo acuña en la de destino.MessageTransmitterV2, una capa genérica de paso de mensajes. Emite el eventoMessageSenten la cadena de origen y, en el destino, verifica la atestación enreceiveMessageantes de permitir la acuñación.- Iris, el servicio de atestación off-chain de Circle. Vigila cada quema, espera el nivel de finalidad que pide el mensaje y firma ese mensaje. Su API está en
iris-api.circle.com.
Así que el ciclo de vida queda así: quemas en la cadena de origen, lo que emite un mensaje (pasos 1-2); Iris espera la finalidad requerida y lo firma (pasos 3-4); obtienes el mensaje firmado con GET /v2/messages/{sourceDomain}?transactionHash=... (pasos 5-6); y lo envías a receiveMessage en el destino, que comprueba la firma, marca el nonce como usado y acuña el USDC para el destinatario (pasos 7-8).
Algunos detalles de diseño importan en cuanto empiezas a escribir código contra el protocolo:
- Dominios, no chain IDs. CCTP identifica las cadenas con sus propios números de dominio: Ethereum es
0, Arbitrum3, Base6, HyperEVM19. No tienen nada que ver con los chain IDs de EVM. - Las direcciones son
bytes32. CCTP abarca cadenas EVM y no EVM, así que los destinatarios son valores de 32 bytes. Una dirección EVM simplemente se rellena con ceros por la izquierda. - Cada nonce acuña una sola vez. El destino registra cada nonce de mensaje que ha procesado, lo que hace imposible reutilizar (replay) una atestación.
- Las atestaciones caducan. Un mensaje de quema incluye un bloque de expiración situado unas 24 horas más adelante. Una quema caducada no se pierde:
POST /v2/reattest/{nonce}te da una atestación nueva.
Los campos que realmente vas a configurar están en la cabecera del mensaje y en el cuerpo del mensaje de quema:
| Campo | Para qué sirve |
|---|---|
destinationDomain | dominio CCTP de la cadena de destino |
mintRecipient | quién recibe el USDC acuñado en el destino |
destinationCaller | quién puede llamar a receiveMessage; cero significa cualquiera |
maxFee | la comisión máxima que aceptas, en subunidades de USDC; la comisión real queda registrada como feeExecuted |
minFinalityThreshold | 1000 para Fast Transfer, 2000 para Standard |
hookData | bytes arbitrarios que viajan hasta el destino |
Fast Transfer frente a Standard Transfer
El umbral de finalidad es el principal parámetro que puedes ajustar en CCTP. En Ethereum y sus rollups, una transacción se incluye en un bloque en cuestión de segundos, pero solo se vuelve irreversible cuando se finaliza el bloque de Ethereum que la contiene, unos 65 bloques o entre 15 y 19 minutos después. Iris puede firmar en cualquiera de los dos momentos.

- Standard Transfer (
minFinalityThreshold: 2000) espera a la finalidad definitiva. Hoy es gratuita en todas las cadenas, y lenta allí donde la finalidad es lenta: de 15 a 19 minutos desde Ethereum, Arbitrum, Base u OP Mainnet, de 2 a 4 horas desde Starknet y de 6 a 32 horas desde Linea. - Fast Transfer (
minFinalityThreshold: 1000) se atesta en cuanto se confirma la quema, normalmente en 8 a 20 segundos. Mientras tanto, Circle asume el riesgo de reorganización (reorg), respaldado por un cupo global de Fast Transfer (Fast Transfer allowance): cada quema rápida descuenta del cupo, y el importe se repone cuando la quema alcanza la finalidad definitiva. Si el cupo llegara a agotarse, toca esperar o recurrir a Standard. Puedes consultar el cupo restante enGET /v2/fastBurn/USDC/allowance; cuando escribimos esto, era de decenas de millones de USDC.
Las comisiones de Fast Transfer se cobran en puntos básicos sobre el importe y se descuentan cuando se acuña el USDC:
| Cadena de origen | Fast Transfer | Comisión 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 horas |
| HyperEVM | n/d | n/d | ~5 s |
Las cadenas marcadas como n/d no ofrecen Fast Transfer como origen porque su transferencia Standard ya es así de rápida. Las comisiones cambian con el tiempo, así que nunca las fijes en el código: léelas de GET /v2/burn/USDC/fees/{source}/{destination} y fija maxFee a partir de la respuesta con un pequeño margen. Un maxFee por debajo de la comisión vigente puede convertir sin avisar una transferencia Fast en una Standard.
Hooks y el Forwarding Service
Los hooks son los bytes de hookData que se adjuntan a una quema. CCTP no los ejecuta: los transporta intactos hasta el destino, y el contrato que recibe el mensaje decide qué hacer con ellos. Es intencionado. Así el núcleo del protocolo se mantiene pequeño, y los integradores pueden construir a su manera flujos del tipo "cruzar el bridge y luego hacer X" (depositar en un vault, abrir una posición, pagar una factura).
El hook más útil es uno que lee la propia Circle. Haz que los datos del hook empiecen por los bytes mágicos cctp-forward y el Forwarding Service de Circle se encarga de los pasos 5 a 7 del ciclo de vida: obtiene la atestación y envía la acuñación en la cadena de destino, pagando el gas en destino. Una transferencia CCTP se convierte en una sola transacción en la cadena de origen, sin relayer que operar ni token de gas que tener en el destino.
// "cctp-forward" magic + version 0 + empty payload: just forward, nothing else
const forwardHookData = '0x636374702d666f72776172640000000000000000000000000000000000000000'El servicio cobra una comisión que cubre el gas en destino más una tarifa de servicio, y se descuenta de la transferencia (o, en las rutas compatibles, se paga por adelantado en la cadena de origen si el destinatario tiene que recibir el importe exacto). La tarifa de servicio es de 0,05 $ en la mayoría de las rutas, con precios específicos para Hyperliquid que detallamos más abajo.
¿Qué cadenas admite CCTP?
CCTP V2 está operativo en unas 30 blockchains, tanto EVM como no EVM. Las que probablemente más uses son Ethereum, Arbitrum, Base, OP Mainnet, Polygon PoS, Avalanche, Solana, Linea, Unichain y HyperEVM, la cadena que conecta CCTP con Hyperliquid. Todas las cadenas compatibles pueden recibir transferencias, testnets incluidas, y Circle mantiene al día la lista completa con los IDs de dominio.
CCTP en Hyperliquid: HyperEVM y HyperCore
Hyperliquid es una sola L1 con dos entornos de ejecución. HyperCore es el motor de exchange nativo: los libros de órdenes de perps y spot, el margen y los saldos de las cuentas; es donde ocurre el trading, y no es una EVM. HyperEVM es una EVM de propósito general que funciona sobre la misma cadena y el mismo consenso, con HYPE como gas. Ambas comparten estado, y eso es lo que permite que un contrato en HyperEVM acredite un saldo de trading en HyperCore.
Circle lanzó USDC nativo y CCTP V2 en HyperEVM (dominio 19) el 16 de septiembre de 2025, y en las semanas siguientes habilitó los depósitos y retiros CCTP para HyperCore. Antes de eso, la vía de entrada canónica era el propio contrato de bridge de Hyperliquid en Arbitrum, protegido por su conjunto de validadores. Con CCTP, cada cadena CCTP es una rampa de acceso (on-ramp) directa.
Como CCTP no puede acuñar en un motor que no es EVM, Circle añadió algunos contratos alrededor del protocolo estándar:
| Contrato | Cadena | Función |
|---|---|---|
CctpForwarder | HyperEVM | recibe las acuñaciones CCTP destinadas a HyperCore y las reenvía |
CoreDepositWallet | HyperEVM | guarda el USDC nativo, acredita los saldos de HyperCore y quema en los retiros |
CctpExtension | Arbitrum | depósitos en una sola transacción autorizados con una firma EIP-3009 |
CctpExtensionV2 | Arbitrum | depósitos patrocinados: un relayer envía la quema y paga el gas |
CctpForwarder HyperEVM 0xb21D281DEdb17AE5B501F6AA8256fe38C4e45757
CoreDepositWallet HyperEVM 0x6B9E773128f453f5c2C60935Ee2DE2CBc5390A24
CctpExtension Arbitrum 0xA95d9c1F655341597C94393fDdc30cf3c08E4fcE
CctpExtensionV2 Arbitrum 0x3289e443a95B28Bcedacc4B33C689b0C9b84ffABUn matiz que conviene tener presente: un saldo de USDC en HyperCore es un crédito a nivel de protocolo, no un token. El USDC nativo real está en el contrato CoreDepositWallet de HyperEVM, respaldando esos créditos uno a uno, y es un retiro lo que vuelve a convertir un crédito en USDC real.
Depositar USDC en Hyperliquid

Un depósito es una transferencia CCTP estándar a HyperEVM con tres ajustes específicos:
destinationDomaines19(HyperEVM).mintRecipientydestinationCallerson ambos la dirección delCctpForwarder. El USDC se acuña para el forwarder, y solo el forwarder puede completar el mensaje.hookDatale dice al forwarder qué hacer: la cabeceracctp-forwardseguida del destinatario en HyperCore y del saldo de destino,0para perps o4294967295(eluint32máximo) para spot.
A continuación, el Forwarding Service de Circle acuña en HyperEVM, el forwarder deposita el USDC en CoreDepositWallet en nombre del destinatario y el saldo aparece en HyperCore.
Empieza por preguntar a la API de comisiones cuánto cuesta la ruta. El parámetro hyperCoreDeposit=true incluye en el precio el reenvío (forwarding) a 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 }
}
]Desde Arbitrum, la ruta tiene condiciones especiales: Fast Transfer es gratis (minimumFee: 0) y el reenvío cuesta una tarifa fija de 0,20 USDC (200000 subunidades, ya que USDC tiene 6 decimales). Deposita 10 USDC y 9,80 llegan a HyperCore en cuestión de segundos. Desde otras cadenas pagas la comisión de Fast Transfer de la cadena de origen más una comisión de reenvío que incluye el gas de HyperEVM, de unos 0,25 USDC desde Base o Ethereum en el momento de escribir esto.
Utilidades compartidas: datos del hook y cotización de comisiones
Los scripts siguientes usan viem y se ejecutan directamente con Node.js 22.6+ (npm install viem y luego node --env-file=.env <script>.ts). Primero, las piezas que necesita cualquier depósito:
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)
}El codificador produce exactamente los 56 bytes que espera el forwarder; comprobamos su salida byte a byte contra la implementación de referencia de Circle. La utilidad de comisiones convierte los puntos básicos en subunidades de USDC (el * 100 mantiene exactas en aritmética entera tasas fraccionarias como 1,3 bps), añade un margen del 20 % sobre la comisión del protocolo, como recomienda Circle, y después suma la comisión de reenvío. low, med y high equilibran costo y velocidad de inclusión en el destino; en la ruta de Arbitrum, las tres son la misma tarifa fija de 0,20 USDC.
Depositar desde Arbitrum en una sola transacción
En Arbitrum, el contrato CctpExtension acepta una firma EIP-3009 ReceiveWithAuthorization en lugar de una aprobación ERC-20, así que todo el depósito se reduce a una firma y una transacción:
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}`)La wallet solo necesita USDC y un poco de ETH para el gas en Arbitrum; nada en Hyperliquid. El destinatario que se pasa a encodeHyperCoreHook puede ser cualquier dirección: así es como una app acredita la cuenta de HyperCore de un usuario desde una wallet de tesorería. Para los usuarios que tienen USDC pero no ETH, CctpExtensionV2 va un paso más allá: el usuario solo firma, y un relayer envía la quema y paga el gas.
Unos segundos después del recibo de la transacción, los fondos ya están en HyperCore. La API info pública de Hyperliquid muestra el saldo de perps:
curl -s https://api.hyperliquid.xyz/info \ -H 'Content-Type: application/json' \ -d '{"type":"clearinghouseState","user":"0xYourAddress"}'
Usa "type":"spotClearinghouseState" para el saldo spot. Para seguir la propia transferencia, GET /v2/messages/3?transactionHash=0x... en la API de Iris devuelve el mensaje y el estado de su atestación.
Depositar desde cualquier otra cadena
Todas las demás cadenas CCTP pasan por el TokenMessengerV2 estándar: apruébalo para que gaste el USDC y luego llama a depositForBurnWithHook con los mismos ajustes del forwarder. Esta vez acreditamos el saldo spot desde 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}`)De una cadena EVM a otra solo cambian la cadena, la dirección de USDC y el dominio de origen. Conceptualmente, Solana funciona igual, con sus propios programas CCTP.
¿Ya tienes USDC en HyperEVM?
Entonces no necesitas CCTP en absoluto. Aprueba el CoreDepositWallet y llama a una de sus funciones de depósito: deposit(amount, dex) acredita a quien hace la llamada, depositFor(recipient, amount, dex) acredita a un tercero, y depositWithAuth acepta una firma EIP-3009 en lugar de una aprobación.
# 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_KEYNunca envíes USDC a CoreDepositWallet con un simple transfer: no activa ningún depósito y los tokens se quedan bloqueados para siempre.
Retirar USDC de Hyperliquid

Un retiro recorre el mismo camino a la inversa, pero no empieza con una transacción EVM. Firmas una acción sendToEvmWithData con EIP-712 y la envías al endpoint /exchange de Hyperliquid. HyperCore debita tu saldo, CoreDepositWallet quema el USDC correspondiente a través de CCTP en HyperEVM, Iris atesta (HyperEVM finaliza en unos cinco segundos, así que los retiros son rápidos por defecto) y el Forwarding Service acuña USDC nativo para tu destinatario en la cadena de destino. Firmas un solo mensaje, sin necesidad de tener HYPE ni gas en el destino.
La acción recibe estos campos:
| Campo | Valor |
|---|---|
sourceDex | '' para retirar de perps, 'spot' para retirar de spot |
destinationRecipient | dirección del destinatario en la cadena de destino |
addressEncoding | 'hex' para cadenas EVM, 'base58' para Solana |
destinationChainId | el dominio CCTP, no el chain ID de EVM: 3 Arbitrum, 0 Ethereum, 6 Base, 5 Solana |
gasLimit | presupuesto de gas para la ejecución en el destino |
data | '0x' para el reenvío automático; los bytes personalizados se convierten en el hookData de CCTP |
signatureChainId | el chain ID que se usa en el dominio EIP-712, en hexadecimal |
nonce | la marca de tiempo actual en milisegundos |
Este es el retiro completo, de nuevo con 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)El mensaje EIP-712 y la acción JSON llevan los mismos valores; las únicas diferencias son la codificación (bigint para los campos uint64 del mensaje firmado, números simples en el JSON) y los dos campos que solo existen en la acción, type y signatureChainId. Para apuntar a otra cadena, cambia destinationChainId por su dominio CCTP; para usar testnet, cambia hyperliquidChain a 'Testnet' y envía la petición a api.hyperliquid-testnet.xyz.
Los retiros tienen dos comisiones: una del lado de HyperCore y, con el reenvío, la comisión del Forwarding Service, que depende del destino: 0,20 USDC hacia la mayoría de las cadenas, 1,20 USDC hacia Ethereum y 0,50 USDC hacia Solana. La comisión de reenvío exacta se puede leer en el contrato CoreDepositWallet. Si el importe no cubre las comisiones, la quema en HyperEVM se revierte.
Dos detalles más. En los retiros, el destinationCaller de CCTP es siempre la dirección cero, así que cualquiera puede completar la acuñación. Y si pones tus propios bytes en data en lugar de 0x, se convierten en los datos del hook de CCTP, el reenvío se desactiva y completar la acuñación en el destino pasa a ser tarea tuya (o de cualquiera).
Las comisiones de un vistazo
A septiembre de 2026, para una transferencia de 10 USDC:
| Ruta | Comisión del protocolo CCTP | Comisión de reenvío |
|---|---|---|
| Arbitrum a HyperCore (Fast) | 0 | 0,20 USDC fijos |
| Base a HyperCore (Fast) | 1,3 bps | ~0,25 USDC, cotizada en tiempo real |
| Ethereum a HyperCore (Fast) | 1 bps | ~0,25 USDC, cotizada en tiempo real |
| Cualquier cadena a HyperCore (Standard) | 0 | igual que en Fast, pero en minutos en lugar de segundos |
| HyperCore a Arbitrum, Base y la mayoría de las cadenas | 0 | 0,20 USDC, más la comisión de HyperCore |
| HyperCore a Ethereum | 0 | 1,20 USDC, más la comisión de HyperCore |
| HyperCore a Solana | 0 | 0,50 USDC, más la comisión de HyperCore |
Las cuentas nuevas de HyperCore pagan además una comisión de activación única de 1 USDC, que cobra Hyperliquid (no Circle) en la primera acción saliente de la cuenta.
Trampas que pueden costarte fondos
La mayoría de los errores con CCTP son ruidosos: una autorización caducada o una aprobación que falta se revierten en la cadena de origen, y no se mueve nada. La integración con Hyperliquid añade algunos silenciosos.
- El forwarder debe ser a la vez
mintRecipientydestinationCaller. Si en un depósito a HyperCore pones cualquier otra cosa en uno de los dos, el USDC se acuña en un lugar que no puede depositarlo. La documentación de Circle es explícita: esos fondos no se pueden recuperar. Mantén estos valores como constantes, nunca como entrada del usuario. - Nada de transferencias simples a
CoreDepositWallet. Solo sus funciones de depósito acreditan en HyperCore. Untransferdirecto deja los tokens bloqueados. - La comisión de activación se cobra a la salida, no a la entrada. Los depósitos de cualquier tamaño se completan, incluso por debajo de 1 USDC. Pero el primer retiro o la primera transferencia de una cuenta nueva necesita al menos 1 USDC para la comisión de activación, además de las comisiones de retiro, y si no, falla. Si tu sistema crea cuentas de forma programática, puedes activarlas de antemano.
- Perps o spot lo decide el hook.
0acredita en perps,4294967295acredita en spot y cualquier otro valor acaba en spot por defecto. Un bot de trading que espera tener su margen en perps no lo encontrará en spot. - Un
maxFeedemasiado ajustado degrada la transferencia sin avisar. SimaxFeeno cubre a la vez la comisión de Fast Transfer y el reenvío, CCTP mantiene el reenvío y pasa a una transferencia Standard: desde Arbitrum, eso son de 15 a 19 minutos en lugar de 8 segundos. Lo mismo ocurre si se agota el cupo de Fast Transfer. - La testnet tiene sus propias reglas. Un destinatario en la testnet de HyperCore ya debe existir en mainnet, puede recibir como máximo 1000 USDC de testnet, y las transferencias a direcciones desconocidas fallan en silencio. Comprueba antes la dirección con la consulta info
userRolede Hyperliquid. - Respeta el límite de peticiones de Iris. La API permite 40 peticiones por segundo; si lo superas, te bloquea durante cinco minutos. Consulta las atestaciones cada pocos segundos, no en un bucle cerrado.
Preguntas frecuentes
¿CCTP es un bridge? Funcionalmente, sí: mueve valor entre cadenas. Estructuralmente, no: no se bloquea nada ni se envuelve nada. Su propio emisor quema el USDC en un lado y lo acuña en el otro, así que no hay pool ni vault que vaciar.
¿Cuánto tarda un depósito en Hyperliquid? Con Fast Transfer desde Arbitrum o Base, unos 8 segundos para la atestación, más la transacción de reenvío en HyperEVM. Con Standard Transfer desde un rollup de Ethereum, de 15 a 19 minutos. Los retiros son rápidos por defecto porque HyperEVM finaliza en segundos.
¿Puedo depositar directamente en mi saldo spot?
Sí. Pon los últimos cuatro bytes de los datos del hook a 4294967295 (el uint32 máximo) en lugar de 0.
¿El USDC de HyperCore es USDC "real"?
Los saldos de HyperCore son créditos a nivel de protocolo. Cada uno está respaldado por USDC nativo guardado en el contrato CoreDepositWallet de HyperEVM, y un retiro lo canjea por USDC nativo en cualquier cadena CCTP.
¿Necesito HYPE o gas en Hyperliquid?
No. Un depósito solo cuesta gas en la cadena de origen (e incluso ese gas puede patrocinarse mediante CctpExtensionV2 en Arbitrum). Un retiro es una acción firmada en HyperCore cuyas comisiones se pagan en USDC.
Dónde encaja esto
CCTP convirtió USDC, de un token presente en muchas cadenas, en un único saldo que se mueve entre ellas en segundos, a un precio conocido y sin un bridge en el que confiar. Para Hyperliquid, significa que el capital puede fluir entre cualquier cadena importante y el libro de órdenes sin desvíos, y sin que nadie tenga que pasar por la interfaz de un bridge.
Este artículo surgió de un proyecto para un cliente en el que hemos estado usando CCTP para mover USDC entre cadenas EVM y Hyperliquid.
En Etherwave Labs construimos sobre cadenas EVM y sobre Hyperliquid, y estamos abiertos a ayudarte con cualquier cosa en ese ámbito: robots de trading y estrategias automatizadas, estrategias delta neutral (por ejemplo, cubrir posiciones spot o de liquidez con perps de Hyperliquid) y automatización on-chain en general, desde el rebalanceo de tesorería entre cadenas con CCTP hasta keepers y gestión de posiciones. Puedes ver cómo enfocamos la automatización en nuestra página del servicio de automatización de la gestión de liquidez, leer nuestra introducción a los libros de órdenes frente a los AMM y, si tienes una estrategia o un flujo que automatizar, habla con nosotros.


