bitpro가 원문에서 글자만 모은 내용이에요. 표·그림·수식은 빠지거나 깨질 수 있어요. 전체 75쪽 중 35쪽
n(
signedTransaction as Parameters<typeof sendAndConfirmTransaction>[0],
{
commitment: "confirmed",
},
);
const signature = getSignatureFromTransaction(signedTransaction);
console.log(`Mint successful! Signature: ${signature}`);
return signature;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (message.includes("already been processed")) {
console.log("Nonce already used - mint may have already completed.");
}
throw error;
}
}
// Use attestation data from the API
const attestationData: AttestationData = {
message: "0x000000000000000500000000...", // Full message hex from API
attestation: "0xdc485fb2f9a8f68c871f4ca7386dee9086ff9d43...", // Full attestation hex from API
};
await retryMintOnSolana(attestationData, 0); // 0 = Ethereum Sepolia domain
```
<Note>
**Note:** The recipient's USDC token account must exist before calling
`receiveMessage`. If the account doesn't exist, create it using the Associated
Token Program before retrying the mint.
</Note>
</Tab>
</Tabs>
## Handle destination caller restrictions
If the burn specified a `destinationCaller` address, only that address can call
`receiveMessage`. If you're seeing authorization errors:
1. Check the `destinationCaller` field in the attestation's `decodedMessage`
2. If it's not `0x0000...0000`, ensure you're calling from the specified address
# Transfer USDC from Arbitrum to HyperCore
Source: https://developers.circle.com/cctp/howtos/transfer-usdc-from-arbitrum-to-hypercore
This guide shows how to transfer USDC from Arbitrum to HyperCore using the
`CctpExtension` contract.
<Note>
Fast Transfers from Arbitrum to HyperEVM have no fees, however there is a flat
forwarding fee for Arbitrum transfers to HyperCore. Fast Transfer is the default
for transfers from Arbitrum to HyperEVM.
</Note>
## Prerequisites
Before you begin, ensure that you've:
* Installed [Node.js v22.18+](https://nodejs.org/)
* Prepared an EVM testnet wallet with the private key available
* Added Arbitrum Sepolia network to your wallet
([network details](https://docs.arbitrum.io/build-decentralized-apps/reference/node-providers))
* Funded your wallet with the following testnet tokens:
* Arbitrum Sepolia ETH (native token) from a
[public faucet](https://faucet.quicknode.com/arbitrum/sepolia)
* Arbitrum Sepolia USDC from the [Circle Faucet](https://faucet.circle.com)
* Created a new Node project and installed dependencies:
```bash theme={null}
npm install viem
npm install -D typescript @types/node
```
* Created a `.env` file with required environment variables:
```text theme={null}
PRIVATE_KEY=0x...
FORWARD_RECIPIENT=0x... # Your HyperCore address to receive the USDC
```
## Steps
Use the following steps to transfer USDC from Arbitrum to HyperCore.
### Step 1. Get CCTP fees from the API
Query the CCTP API for the fees for transferring USDC from Arbitrum to
HyperCore. This value is passed to the `maxFee` parameter in the
`batchDepositForBurnWithAuth` transaction. The following is an example request
to the CCTP using source domain 3 (Arbitrum) and destination domain 19
(HyperEVM):
```shell theme={null}
curl --request GET \
--url 'https://iris-api-sandbox.circle.com/v2/burn/USDC/fees/3/19?forward=true&hyperCoreDeposit=true' \
--header 'Content-Type: application/json'
```
**Response:**
```json theme={null}
[
{
"finalityThreshold": 1000, // fast transfer
"minimumFee": 0, // no protocol fee
"forwardFee": {
"low": 200000, // 0.20 USDC
"med": 200000, // low, med, high will be the same static fee
"high": 200000
}
},
{
"finalityThreshold": 2000, // standard transfer
"minimumFee": 0,
"forwardFee": {
"low": 200000,
"med": 200000,
"high": 200000
}
}
]
```
### Step 2. Calculate the USDC amounts minus fees
There is no fee to deposit USDC from Arbitrum to HyperEVM, but there is a flat
forwarding fee for the transfer to HyperCore. The forwarding fee is 0.20 USDC
(`0_200_000` subunits). For a 10 USDC transfer from Arbitrum to HyperCore, the
total fee is 0.20 USDC.
<Note>
The forwarding fee is deducted from your transfer amount. For a 10 USDC
transfer, you will receive 9.80 USDC on HyperCore.
</Note>
### Step 3. Sign a `ReceiveWithAuthorization` transaction on the USDC contract
Create a `ReceiveWithAuthorization` transaction for the USDC contract with the
following parameters:
* `from`: Your wallet address
* `to`: The `CctpExtension` contract address
* `value`: The amount of USDC to transfer
* `validAfter`: The timestamp after which the transaction is valid
* `validBefore`: The timestamp before which the transaction is valid
* `nonce`: A random nonce
Sign the hash of the transaction with your private key, and derive the `v`, `r`,
`s` values. Broadcast the transaction to the blockchain.
### Step 4. Sign and broadcast a `batchDepositForBurnWithAuth` transaction on the `CctpExtension` contract
Create a `batchDepositForBurnWithAuth` transaction for the `CctpExtension`
contract with the following parameters:
* `destinationDomain`: 19 (HyperEVM)
* `mintRecipient`: The `CctpForwarder` contract address on HyperEVM
* `destinationCaller`: The `CctpForwarder` contract address on HyperEVM
* `maxFee`: `0_200_000` (0.20 USDC, from step 2)
* `minFinalityThreshold`: `1000` (Fast Transfer)
* `hookData`: The hook data to call the `CctpForwarder` contract on HyperEVM
<Warning>
Always set both `mintRecipient` and `destinationCaller` to the `CctpForwarder`
[contract address](/cctp/references/hypercore-contract-addresses) on HyperEVM
when you transfer USDC to HyperCore.
* If `destinationCaller` is wrong, the forwarder cannot complete the transfer.
* If `mintRecipient` is wrong, the minted USDC is not sent to the forwarder.
In either case, funds become permanently stuck and **cannot be recovered**.
</Warning>
The `hookData` is the data to execute the forwarder to HyperCore. The following
is an example of the hook data:
```ts TypeScript theme={null}
/**
* Generate CCTP forwarder hook data for HyperCore
*
* Hook Data Format:
* Field Bytes Type Index
* magicBytes 24 bytes24 0 ASCII prefix "cctp-forward", followed by padding
* version 4 uint32 24
* dataLength 4 uint32 28
* hyperCoreMintRecipient 20 address 32 EVM address - optional, included if requesting a deposit to HyperCore
* hyperCoreDestinationDex 4 uint32 52 The destinationDexId on HyperCore (0 for perp and uint32.max for spot)
*/
function encodeForwardHookData(
hyperCoreMintRecipient?: `0x${string}`,
hyperCoreDestinationDex: number = 0,
): `0x${string}` {
// Validate hex prefix if recipient provided
if (hyperCoreMintRecipient && !hyperCoreMintRecipient.startsWith("0x")) {
throw new Error("Address must start with 0x");
}
// Magic bytes: "cctp-forward" (12 chars) padded to 24 bytes with zeros
const magic = "cctp-forward";
const magicHex = Buffer.from(magic, "utf-8").toString("hex").padEnd(48, "0");
// Version: uint32 = 0 (4 bytes, big-endian)
const version = "00000000";
if (!hyperCoreMintRecipient) {
// No recipient: dataLength = 0, return header only (32 bytes)
const dataLength = "00000000";
return `0x${magicHex}${version}${dataLength}`;
}
// With recipient: dataLength = 24 (20 bytes address + 4 bytes dex)
const dataLength = "00000018"; // 24 in hex
// Address: 20 bytes (remove 0x prefix)
const address = hyperCoreMintRecipient.slice(2).toLowerCase();
// Destination DEX: uint32 big-endian
// 0 = perps, 4294967295 (0xFFFFFFFF) = spot
const dex = (hyperCoreDestinationDex >>> 0).toString(16).padStart(8, "0");
return `0x${magicHex}${version}${dataLength}${address}${dex}`;
}
```
Once the deposit transaction is confirmed, the USDC is minted on HyperEVM and
automatically forwarded to your address on HyperCore.
By default (when `hyperCoreDestinationDex` is `0`), deposits credit the perps
balance on HyperCore. To deposit to the spot balance, set
`hyperCoreDestinationDex` to `4294967295` (uint32 max value).
## Full example code
The following is a complete example of how to transfer USDC from Arbitrum to
HyperCore.
```ts script.ts expandable theme={null}
/**
* Script: Call CctpExtension.batchDepositForBurnWithAuth
* - Generates EIP-3009 receiveWithAuthorization signature
* - Executes a CCTP burn via the extension
* - Supports Forwarder hook data to auto-forward to HyperCore
*/
import {
createWalletClient,
createPublicClient,
http,
parseUnits,
formatUnits,
type Address,
type Hex,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { arbitrumSepolia } from "viem/chains";
// -------- Contract ABIs --------
const CCTP_EXTENSION_ABI = [
{
name: "batchDepositForBurnWithAuth",
type: "function",
stateMutability: "nonpayable",
inputs: [
{
name: "_receiveWithAuthorizationData",
type: "tuple",
components: [
{ name: "amount", type: "uint256" },
{ name: "authValidAfter", type: "uint256" },
{ name: "authValidBefore", type: "uint256" },
{ name: "authNonce", type: "bytes32" },
{ name: "v", type: "uint8" },
{ name: "r", type: "bytes32" },
{ name: "s", type: "bytes32" },
],
},
{
name: "_depositForBurnData",
type: "tuple",
components: [
{ name: "amount", type: "uint256" },
{ name: "destinationDomain", type: "uint32" },
{ name: "mintRecipient", type: "bytes32" },
{ name: "destinationCaller", type: "bytes32" },
{ name: "maxFee", type: "uint256" },
{ name: "minFinalityThreshold", type: "uint32" },
{ name: "hookData", type: "bytes" },
],
},
],
outputs: [],
},
] as const;
// -------- Configuration --------
const config = {
privateKey: (process.env.PRIVATE_KEY || "0x") as Hex,
// Contract addresses (Arbitrum Sepolia Testnet)
cctpExtension: "0x8E4e3d0E95C1bEC4F3eC7F69aa48473E0Ab6eB8D" as Address,
usdcToken: "0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d" as Address,
// Transfer parameters
amount: "2", // USDC amount to transfer
maxFee: "0.2", // Max fee in USDC
// CCTP parameters
destinationDomain: 19, // HyperEVM domain
cctpForwarder: "0x02e39ECb8368b41bF68FF99ff351aC9864e5E2a2" as Address, // HyperEVM testnet
// HyperCore recipient
forwardRecipient: process.env.FORWARD_RECIPIENT as Address,
destinationDex: 0, // 0 = perps, 4294967295 = spot
// EIP-3009 validity window (seconds)
validAfter: Math.floor(Date.now() / 1000) - 3600, // 1 hour ago
validBefore: Math.floor(Date.now() / 1000) + 3600, // 1 hour from now
};
// -------- Generate Hook Data --------
function encodeForwardHookData(
hyperCoreMintRecipient?: `0x${string}`,
hyperCoreDestinationDex: number = 0,
): `0x${string}` {
if (hyperCoreMintRecipient && !hyperCoreMintRecipient.startsWith("0x")) {
throw new Error("Address must start with 0x");
}
const magic = "cctp-forward";
const magicHex = Buffer.from(magic, "utf-8").toString("hex").padEnd(48, "0");
const version = "00000000";
if (!hyperCoreMintRecipient) {
const dataLength = "00000000";
return `0x${magicHex}${version}${dataLength}`;
}
const dataLength = "00000018";
const address = hyperCoreMintRecipient.slice(2).toLowerCase();
const dex = (hyperCoreDestinationDex >>> 0).toString(16).padStart(8, "0");
return `0x${magicHex}${version}${dataLength}${address}${dex}`;
}
// -------- Generate Random Nonce --------
function generateNonce(): Hex {
const randomBytes = crypto.getRandomValues(new Uint8Array(32));
return `0x${Array.from(randomBytes)
.map((b) => b.toString(16).padStart(2, "0"))
.join("")}`;
}
// -------- Main Function --------
async function main() {
// Validate private key and recipient
if (!config.privateKey || config.privateKey === "0x") {
throw new Error("Set PRIVATE_KEY");
}
if (!config.forwardRecipient) {
throw new Error("Set FORWARD_RECIPIENT");
}
// Setup account and clients
const account = privateKeyToAccount(config.privateKey);
const publicClient = createPublicClient({
chain: arbitrumSepolia,
transport: http(),
});
const walletClient = createWalletClient({
chain: arbitrumSepolia,
transport: http(),
account,
});
const amount = parseUnits(config.amount, 6);
const maxFee = parseUnits(config.maxFee, 6);
console.log("User:", account.address);
console.log("Extension:", config.cctpExtension);
console.log("USDC:", config.usdcToken);
console.log("Total (USDC):", config.amount);
console.log(
"Dest Domain:",
config.destinationDomain,
"\nMint Recipient:",
config.cctpForwarder,
);
console.log("Max Fee (USDC):", config.maxFee, "\nMin Finality:", 1000);
// Check USDC balance
const balance = await publicClient.readContract({
address: config.usdcToken,
abi: [
{
name: "balanceOf",
type: "function",
stateMutability: "view",
inputs: [{ name: "account", type: "address" }],
outputs: [{ name: "", type: "uint256" }],
},
],
functionName: "balanceOf",
args: [account.address],
});
if (balance < amount) {
throw new Error(
`Insufficient USDC: have ${formatUnits(balance, 6)}, need ${config.amount}`,
);
}
// Generate hook data
const hookData = encodeForwardHookData(
config.forwardRecipient,
config.destinationDex,
);
console.log(
"Forwarder hook enabled -> Final recipient:",
config.forwardRecipient,
);
console.log("Hook Data:", hookData);
// Convert addresses to bytes32
const mintRecipientBytes32 =
`0x${config.cctpForwarder.slice(2).padStart(64, "0")}` as Hex;
const destinationCallerBytes32 =
`0x${config.cctpForwarder.slice(2).padStart(64, "0")}` as Hex;
console.log("Destination Caller (bytes32):", destinationCallerBytes32);
// Generate nonce for EIP-3009
const nonce = generateNonce();
// Sign EIP-3009 ReceiveWithAuthorization
const signature = await walletClient.signTypedData({
domain: {
name: "USD Coin",
version: "2",
chainId: arbitrumSepolia.id,
verifyingContract: config.usdcToken,
},
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: config.cctpExtension,
value: amount,
validAfter: BigInt(config.validAfter),
validBefore: BigInt(config.validBefore),
nonce,
},
});
// Parse signature into v, r, s
const r = signature.slice(0, 66) as Hex;
const s = `0x${signature.slice(66, 130)}` as Hex;
const v = parseInt(signature.slice(130, 132), 16);
// Estimate gas
const gasEstimate = await publicClient.estimateContractGas({
address: config.cctpExtension,
abi: CCTP_EXTENSION_ABI,
functionName: "batchDepositForBurnWithAuth",
args: [
{
amount,
authValidAfter: BigInt(config.validAfter),
authValidBefore: BigInt(config.validBefore),
authNonce: nonce,
v,
r,
s,
},
{
amount,
destinationDomain: config.destinationDomain,
mintRecipient: mintRecipientBytes32,
destinationCaller: destinationCallerBytes32,
maxFee,
minFinalityThreshold: 1000,
hookData,
},
],
account,
});
console.log("Estimated gas:", gasEstimate.toString());
// Execute batchDepositForBurnWithAuth
const hash = await walletClient.writeContract({
address: config.cctpExtension,
abi: CCTP_EXTENSION_ABI,
functionName: "batchDepositForBurnWithAuth",
args: [
{
amount,
authValidAfter: BigInt(config.validAfter),
authValidBefore: BigInt(config.validBefore),
authNonce: nonce,
v,
r,
s,
},
{
amount,
destinationDomain: config.destinationDomain,
mintRecipient: mintRecipientBytes32,
destinationCaller: destinationCallerBytes32,
maxFee,
minFinalityThreshold: 1000,
hookData,
},
],
gas: (gasEstimate * 120n) / 100n, // +20%
});
console.log("Tx hash:", hash);
// Wait for transaction receipt
const receipt = await publicClient.waitForTransactionReceipt({ hash });
console.log("Status:", receipt.status === "success" ? "SUCCESS" : "FAILED");
console.log(
"Block:",
receipt.blockNumber,
"\nGas Used:",
receipt.gasUsed.toString(),
);
}
// Run
main().catch((error) => {
console.error("Error:", error.message);
process.exit(1);
});
```
Run the script:
```bash theme={null}
node --env-file=.env script.ts
```
# How-to: Transfer USDC from Arbitrum to HyperCore with CctpExtensionV2
Source: https://developers.circle.com/cctp/howtos/transfer-usdc-from-arbitrum-to-hypercore-with-cctp-extension-v2
Submit a gas-sponsored CCTP deposit to HyperCore using EIP-3009 and a relayer.
Submit a gas-sponsored CCTP deposit from Arbitrum to HyperCore using the
`CctpExtensionV2` contract. This workflow suits users who hold USDC on Arbitrum
but lack native gas tokens. A relayer submits the Arbitrum transaction on their
behalf.
For self-custody integrators who pay their own gas, use
[Transfer USDC from Arbitrum to HyperCore](/cctp/howtos/transfer-usdc-from-arbitrum-to-hypercore)
with `CctpExtension` instead.
For background on the contract, see
[CctpExtensionV2 Contract Interface](/cctp/references/cctp-extension-v2-contract-interface).
## Prerequisites
Before you begin, ensure that you've:
* Deployed or operate a relayer that can submit transactions on Arbitrum and pay
ETH gas
* Collected an EIP-3009 `ReceiveWithAuthorization` signature from each depositor
* Identified the `CctpExtensionV2` proxy address for your environment (see
[HyperCore contract addresses](/cctp/references/hypercore-contract-addresses))
* Configured CCTP deposit parameters for HyperCore (HyperEVM domain `19`,
`CctpForwarder` as `mintRecipient` / `destinationCaller`, and forwarder hook
data for the HyperCore recipient)
## Steps
### Step 1. Collect the user's EIP-3009 signature
The user signs `ReceiveWithAuthorization` with:
* `from`: depositor address
* `to`: `CctpExtensionV2` proxy address
* `value`: total USDC amount covered by this authorization
* `validAfter` / `validBefore`: authorization time window
* `nonce`: deterministic value equal to
`keccak256(abi.encode(from, amount, validAfter, validBefore, depositData))`,
where `depositData` is the ABI-encoded `SponsoredDepositForBurnData` struct
for this deposit
Binding the nonce to `depositData` prevents a relayer from redirecting funds to
a different destination or fee configuration than the user signed.
### Step 2. Submit the deposit via your relayer
Call `batchSponsorDepositForBurn` on the `CctpExtensionV2` proxy with:
* `_authData`: `ReceiveWithAuthorizationDataWithFrom[]` (includes explicit
`from`)
* `_depositData`: `SponsoredDepositForBurnData[]` with CCTP fields aligned to
your HyperCore destination
Use the same forwarder hook encoding as
[Transfer USDC from Arbitrum to HyperCore](/cctp/howtos/transfer-usdc-from-arbitrum-to-hypercore).
You may batch multiple users in one transaction. Entries with an
already-consumed nonce or insufficient balance are skipped
(`SponsoredDepositForBurnSkipped` event) without reverting the whole batch.
### Step 3. Monitor the crosschain transfer
After the Arbitrum transaction confirms, Circle's attestation service picks up
the burn event. The `CctpForwarder` on HyperEVM receives the minted USDC and
routes it to the HyperCore recipient via the forwarder hook.
No special handling is required beyond standard CCTP and forwarder monitoring.
For details, see [CCTP on HyperCore](/cctp/concepts/cctp-on-hypercore).
# Transfer USDC from Ethereum to HyperCore
Source: https://developers.circle.com/cctp/howtos/transfer-usdc-from-ethereum-to-hypercore
This guide shows the steps to transfer USDC from Ethereum to HyperCore using the
`TokenMessengerV2` contract with hook data to call the `CctpForwarder` contract
on HyperEVM. This CCTP flow follows the same pattern as USDC transfers from
Ethereum to any other domain, except for the inclusion of hook data to call the
`CctpForwarder` contract on HyperEVM.
While this guide uses Ethereum as an example, the same steps apply to any EVM
chain that supports CCTP via `TokenMessengerV2`. Adjust the source domain ID and
contract addresses for your chain.
This guide does not provide full example code, you can find an example of
transfers from Ethereum in the
[CCTP 프로젝트로 돌아가기