Circle USYC 백서

Circle USYC

원문

bitpro가 원문에서 글자만 모은 내용이에요. 표·그림·수식은 빠지거나 깨질 수 있어요. 전체 75쪽 중 31쪽

RNING: may be absent in the sandbox ABI — verify before submitting { name: "initialSupply", type: "uint256" }, { name: "tokenManagerSettings", type: "tuple", components: [ { name: "rateLimit", type: "uint256" }, { name: "maxTransferAmount", type: "uint256" }, ], }, ], }, ], outputs: [{ name: "tokenId", type: "bytes32" }], }, ] as const; // Encode addresses as 20-byte packed bytes. // Do NOT use encodeAbiParameters, which pads to 32 bytes and is incompatible // with the contract's address decoding. const ownerBytes = encodePacked(["address"], [account.address]); const operatorBytes = encodePacked(["address"], [account.address]); const sepoliaRegisterHash = await sepoliaWallet.writeContract({ address: CCTS_ADDRESS, abi: registerAbi, functionName: "registerCustomToken", args: [ salt, SEPOLIA_TOKEN, { tokenManagerType: 1, // BURN_MINT tokenManagerOwner: ownerBytes, tokenManagerOperator: operatorBytes, tokenOwner: "0x", // omit for an existing ERC-20 tokenMinter: "0x", // omit for an existing ERC-20 initialSupply: 0n, // see ABI warning above tokenManagerSettings: { rateLimit: 0n, // intentionally 0; you set a non-zero value in step 7 maxTransferAmount: 0n, // 0 = no per-transfer cap; set a non-zero value if you need a ceiling }, }, ], }); await sepoliaPublic.waitForTransactionReceipt({ hash: sepoliaRegisterHash }); console.log("Registered on Ethereum Sepolia:", sepoliaRegisterHash); ``` ## Step 4: Configure on Base Sepolia Call `registerCustomToken` on Base Sepolia using the same `deployer` wallet and the same `salt`. Pass the Base Sepolia token address. The `tokenId` is derived from `(deployer address, salt)`—not from blockchain state—so it is identical to the one computed in step 3. ```typescript theme={null} const baseRegisterHash = await baseWallet.writeContract({ address: CCTS_ADDRESS, abi: registerAbi, functionName: "registerCustomToken", args: [ salt, // same salt as Ethereum Sepolia — required for matching tokenId BASE_TOKEN, // Base Sepolia token address { tokenManagerType: 1, tokenManagerOwner: ownerBytes, tokenManagerOperator: operatorBytes, tokenOwner: "0x", tokenMinter: "0x", initialSupply: 0n, tokenManagerSettings: { rateLimit: 0n, maxTransferAmount: 0n, // 0 = no per-transfer cap; set a non-zero value if you need a ceiling }, }, ], }); await basePublic.waitForTransactionReceipt({ hash: baseRegisterHash }); console.log("Registered on Base Sepolia:", baseRegisterHash); console.log("tokenId (same on both chains):", tokenId); ``` ## Step 5: Retrieve the TokenManager addresses Call `resolveTokenManager` on each blockchain to get the deployed `TokenManager` address. You need these addresses to grant the minter role in the next step and to call `setRateLimit` in step 7. ```typescript theme={null} const sepoliaTokenManager = await sepoliaPublic.readContract({ address: CCTS_ADDRESS, abi: serviceAbi, functionName: "resolveTokenManager", args: [tokenId], }); const baseTokenManager = await basePublic.readContract({ address: CCTS_ADDRESS, abi: serviceAbi, functionName: "resolveTokenManager", args: [tokenId], }); console.log("Sepolia TokenManager:", sepoliaTokenManager); console.log("Base TokenManager:", baseTokenManager); ``` ## Step 6: Grant the minter role The `TokenManager` burns your token on the source blockchain and mints it on the destination blockchain. You must grant it the minter role on your ERC-20 on each blockchain before transfers can succeed. The exact call depends on your token's access-control interface. For FiatToken-compatible contracts (such as USDC-style tokens), call `configureMinter` from the `masterMinter` address. Replace the function name and ABI with whatever your contract exposes. ```typescript theme={null} // Example for FiatToken-compatible contracts. // Replace with the minting authorization method your token exposes. const fiatTokenMinterAbi = [ { name: "configureMinter", type: "function", stateMutability: "nonpayable", inputs: [ { name: "minter", type: "address" }, { name: "minterAllowedAmount", type: "uint256" }, ], outputs: [{ name: "ok", type: "bool" }], }, ] as const; const UNLIMITED = 2n ** 256n - 1n; // Grant minter role on Ethereum Sepolia — caller must be the masterMinter const sepoliaMinterHash = await sepoliaWallet.writeContract({ address: SEPOLIA_TOKEN, abi: fiatTokenMinterAbi, functionName: "configureMinter", args: [sepoliaTokenManager, UNLIMITED], }); await sepoliaPublic.waitForTransactionReceipt({ hash: sepoliaMinterHash }); // Grant minter role on Base Sepolia — caller must be the masterMinter const baseMinterHash = await baseWallet.writeContract({ address: BASE_TOKEN, abi: fiatTokenMinterAbi, functionName: "configureMinter", args: [baseTokenManager, UNLIMITED], }); await basePublic.waitForTransactionReceipt({ hash: baseMinterHash }); console.log("Minter role granted on both chains"); ``` If your token uses a different access-control pattern (such as OpenZeppelin `AccessControl` or a custom owner-managed role), call the equivalent grant function on each deployment. ## Step 7: Set a non-zero rate limit Every `TokenManager` deploys with `rateLimit = 0`. A zero rate limit blocks all outbound and inbound transfers for that token on that blockchain. Call `setRateLimit` on each `TokenManager` with a non-zero value. Only the operator can call `setRateLimit`. If you encoded your address as the operator in `DeploymentParams`, you can call it directly. If the operator field was left empty (`"0x"`), only the service operator can set the rate limit—contact Circle to adjust it. ```typescript theme={null} const tokenManagerAbi = [ { name: "setRateLimit", type: "function", stateMutability: "nonpayable", inputs: [{ name: "rateLimit", type: "uint256" }], outputs: [], }, ] as const; // Replace with an appropriate limit for your token and expected transfer volume. // This example sets a limit of 1 million tokens assuming 6 decimal places. const rateLimit = 1_000_000n * 10n ** 6n; const sepoliaRateLimitHash = await sepoliaWallet.writeContract({ address: sepoliaTokenManager, abi: tokenManagerAbi, functionName: "setRateLimit", args: [rateLimit], }); await sepoliaPublic.waitForTransactionReceipt({ hash: sepoliaRateLimitHash }); const baseRateLimitHash = await baseWallet.writeContract({ address: baseTokenManager, abi: tokenManagerAbi, functionName: "setRateLimit", args: [rateLimit], }); await basePublic.waitForTransactionReceipt({ hash: baseRateLimitHash }); console.log("Rate limits set on both chains"); ``` ## Step 8: Verify the configuration Call `resolveTokenAddress` on each blockchain to confirm that the `tokenId` resolves to your token addresses. ```typescript theme={null} const sepoliaResolved = await sepoliaPublic.readContract({ address: CCTS_ADDRESS, abi: serviceAbi, functionName: "resolveTokenAddress", args: [tokenId], }); const baseResolved = await basePublic.readContract({ address: CCTS_ADDRESS, abi: serviceAbi, functionName: "resolveTokenAddress", args: [tokenId], }); console.log("Sepolia resolved token:", sepoliaResolved); // should match SEPOLIA_TOKEN console.log("Base resolved token:", baseResolved); // should match BASE_TOKEN ``` Both addresses should match the token addresses you configured. Your token is now configured for crosschain transfers on Ethereum Sepolia and Base Sepolia. To initiate your first transfer, see [Transfer EURC from Ethereum to Arc](/cctp/expanded-assets/quickstarts/transfer-eurc-ethereum-to-arc)—the same pattern applies to your configured token: swap in your `tokenId` and adjust the source and destination domains as needed. # Configure an ownerless token Source: https://developers.circle.com/cctp/expanded-assets/quickstarts/configure-ownerless-token Set up a crosschain bridge for an existing ERC-20 using CCTP for non-USDC's ownerless token model. After configuration, anyone can bridge the token between the home blockchain and any destination you deploy to, without bridge governance or ownership. ## Prerequisites Before you begin, ensure that you've: * Deployed the ERC-20 on the home blockchain with the same number of decimals you intend to use on every destination * Noted the [`CrossChainTokenService` address](/cctp/expanded-assets/references/contract-addresses) for the home blockchain and each destination blockchain * Obtained access to the Iris fee-quote endpoint (`POST /v2/quote/cctpx/{tokenId}/{sourceDomain}/{destinationDomain}`) <Warning> Configure the token as ownerless on exactly one blockchain—the home blockchain where the token's supply originates. On every other blockchain, CCTP for non-USDC deploys a wrapped `CrossChainToken` automatically. Configuring the same token as ownerless on more than one blockchain will lock supply on each with no mechanism to release it. </Warning> ## Step 1. Configure the token on the home blockchain Call [`registerOwnerlessToken`](/cctp/expanded-assets/references/contract-reference) on the local service with the token's address. Submit the transaction and wait for confirmation, then read the deterministic `tokenId` from the service. ```typescript TypeScript theme={null} const txHash = await walletClient.writeContract({ address: serviceAddress, abi: serviceAbi, functionName: "registerOwnerlessToken", args: [tokenAddress], }); await publicClient.waitForTransactionReceipt({ hash: txHash }); const tokenId = await publicClient.readContract({ address: serviceAddress, abi: serviceAbi, functionName: "getOwnerlessTokenId", args: [tokenAddress], }); ``` Save the returned `tokenId`. You will use it in every subsequent step and in every crosschain transfer. Quote remote deploys with this `tokenId`; using a placeholder or sentinel in the Iris path causes `QuoteArgsMismatch`. ## Step 2. Quote and deploy the wrapped token on each destination For each destination, fetch a fee quote from Iris, then call [`deployRemoteOwnerlessToken`](/cctp/expanded-assets/references/contract-reference) on the home service. Pass the original token address, the destination domain ID, and the fee `claim`. Include a `FORWARD` request with `msgType: "DeployTokenMessage"` so the quote covers destination deploy gas and Circle can deliver the message. Iris still expects a positive `amount` string even for deploys. ```typescript TypeScript theme={null} const quoteRes = await fetch( `${IRIS_BASE}/v2/quote/cctpx/${tokenId}/${sourceDomain}/${destinationDomain}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ amount: "1", feeToken: zeroAddress, requests: [ { type: "FORWARD", params: { msgType: "DeployTokenMessage", autoExecuteHookData: false, }, }, ], }), }, ); if (!quoteRes.ok) throw new Error(await quoteRes.text()); const { signedQuote, feeTotalAmount } = await quoteRes.json(); const claim = { signedQuote, refundAddress: zeroAddress, // or your refund address }; await walletClient.writeContract({ address: serviceAddress, abi: serviceAbi, functionName: "deployRemoteOwnerlessToken", args: [tokenAddress, destinationDomain, claim], value: BigInt(feeTotalAmount), }); ``` This call sends a CCTP message that, when delivered, creates a remote `CrossChainToken` wrapper and `TokenManager`. That wrapper is a new protocol deployment; it is not the same contract as a native copy of your token that may already exist on the destination. Omit `FORWARD` only if you will self-relay in the next step. ## Step 3. Complete delivery (forwarded or self-relay) How the deploy lands depends on the quote: * **With `FORWARD`:** Circle typically submits `receiveMessage` on the destination. Poll until [`resolveTokenAddress`](/cctp/expanded-assets/references/contract-reference) returns a non-zero address on the destination service. * **Without `FORWARD`:** Poll Iris `/v2/messages/{sourceDomain}?transactionHash=…` until the attestation is complete, then call `receiveMessage` on the destination `MessageTransmitterV2` yourself. See [Attestation Verification](/cctp/references/attestation-verification). ## Step 4. Verify the remote contracts Once the destination has processed the message, look up the wrapped token and its `TokenManager` on the destination service: ```typescript TypeScript theme={null} const remoteTokenManager: `0x${string}` = await destinationPublicClient.readContract({ address: destinationServiceAddress, abi: serviceAbi, functionName: "resolveTokenManager", args: [tokenId], }); const remoteToken: `0x${string}` = await destinationPublicClient.readContract({ address: destinationServiceAddress, abi: serviceAbi, functionName: "resolveTokenAddress", args: [tokenId], }); ``` ## Step 5. Rederive the `tokenId` from the token address You can rederive the ownerless `tokenId` from a token address on the home blockchain at any time: ```typescript TypeScript theme={null} const tokenId: `0x${string}` = await publicClient.readContract({ address: serviceAddress, abi: serviceAbi, functionName: "getOwnerlessTokenId", args: [tokenAddress], }); ``` Before the first outbound transfer, confirm the remote `TokenManager` rate limit is non-zero; `rateLimit: 0` blocks all outbound transfers. See [Configure rate limits and transfer caps](/cctp/expanded-assets/howtos/configure-rate-limits-and-caps). # Deploy a native crosschain token Source: https://developers.circle.com/cctp/expanded-assets/quickstarts/deploy-native-crosschain-token Deploy a new CrossChainToken ERC-20 on Ethereum Sepolia and extend it to Base Sepolia using CCTP for non-USDC. Deploying a native crosschain token creates a brand-new ERC-20 that the protocol manages across multiple blockchains, with no pre-existing contract required. The service uses CREATE3 to deploy a `CrossChainToken` ERC-20 and a `NATIVE_CROSSCHAIN_TOKEN` `TokenManager` at deterministic addresses derived from your deployment wallet and a salt you choose. This quickstart walks you through deploying on Ethereum Sepolia and extending the token to Base Sepolia. <Warning> Three conditions silently block or break deployment if not met: * **Same wallet and salt on every blockchain**: The `tokenId` is derived from `(deployer address, salt)`. Using a different wallet or a different salt on any blockchain produces a different `tokenId`—no error is thrown, and the bridge connection is silently broken. * **Rate limit starts at zero**: Every `TokenManager` deploys with `rateLimit = 0`, which blocks all transfers. You must call `setRateLimit` with a non-zero value on each blockchain before any transfer can proceed. * **`name`, `symbol`, and `decimals` must match on every blockchain**: The protocol does not enforce consistency. A mismatch breaks user experience and indexers silently—there is no onchain error. </Warning> ## Prerequisites Before you begin, ensure that you've: * Installed [Node.js v22+](https://nodejs.org/) * Prepared an Ethereum-compatible testnet wallet whose private key you have available—this is the deployment wallet, and it must be the same wallet used on every blockchain * Funded the wallet with Sepolia ETH and Base Sepolia ETH for gas * Obtained access to the Iris fee-quote endpoint (`POST /v2/quote/cctpx/{tokenId}/{sourceDomain}/{destinationDomain}`) ## Step 1: Set up the project ### 1.1. Create the project and install dependencies ```bash theme={null} mkdir cctpx-deploy cd cctpx-deploy npm init -y npm pkg set type=module npm pkg set scripts.start="tsx --env-file=.env index.ts" npm install viem npm install --save-dev @types/node tsx typescript ``` ### 1.2. Configure TypeScript (optional) <Tip> This step is optional. It helps prevent missing types in your IDE or editor. </Tip> Create a `tsconfig.json` file: ```shell theme={null} npx tsc --init ``` Then, update the `tsconfig.json` file: ```shell theme={null} cat <<'EOF' > tsconfig.json { "compilerOptions": { "target": "ESNext", "module": "ESNext", "moduleResolution": "bundler", "strict": true, "types": ["node"] } } EOF ``` ### 1.3. Set environment variables Open `.env` in your editor and add: ```text theme={null} PRIVATE_KEY=YOUR_DEPLOYER_PRIVATE_KEY ``` `PRIVATE_KEY` is the private key for the wallet that deploys the token. You must use this same wallet on every blockchain where you deploy. <Tip> Open `.env` in your editor rather than writing values with shell commands, and add `.env` to your `.gitignore`. This prevents credentials from leaking into your shell history or version control. </Tip> The `npm run start` command loads variables from `.env` using Node.js native env-file support. <Warning> This example uses one or more private keys for local testing. In production, use a secure key management solution and never expose or share private keys. </Warning> ## Step 2: Configure clients and define token metadata Create `index.ts`. Set up public and wallet clients for both blockchains, then define your token metadata and a `bytes32` salt. The salt, together with your `deployer` address, determines your token's `tokenId`—it must be identical on every blockchain. ```typescript theme={null} import { createPublicClient, createWalletClient, encodePacked, http, keccak256, toBytes, zeroAddress, } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { baseSepolia, sepolia } from "viem/chains"; const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`); const sepoliaPublic = createPublicClient({ chain: sepolia, transport: http(), }); const sepoliaWallet = createWalletClient({ account, chain: sepolia, transport: http(), }); const basePublic = createPublicClient({ chain: baseSepolia, transport: http(), }); const baseWallet = createWalletClient({ account, chain: baseSepolia, transport: http(), }); // CrossChainTokenService — same address on all testnets const CCTS_ADDRESS = "0x63753E722bd2C2A5DF6EE19C5106662208B81077" as const; const IRIS_BASE = "https://iris-api-sandbox.circle.com"; // Token metadata — must be identical on every blockchain you deploy to const TOKEN_NAME = "My Token"; const TOKEN_SYMBOL = "MTKN"; const TOKEN_DECIMALS = 6; // bytes32 salt — must be the same on every blockchain you deploy to. // Use any unique bytes32 value; hashing a descriptive string is one approach. const salt: `0x${string}` = keccak256(toBytes("my-token-v1")); ``` Compute your `tokenId` using the `crossChainTokenId` view call. Pass the raw salt directly—not `customTokenDeploySalt`, which is only used by `registerCustomToken`. ```typescript theme={null} const serviceAbi = [ { name: "crossChainTokenId", type: "function", stateMutability: "pure", inputs: [ { name: "deployer", type: "address" }, { name: "salt", type: "bytes32" }, ], outputs: [{ name: "tokenId", type: "bytes32" }], }, { name: "resolveTokenAddress", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "bytes32" }], outputs: [{ name: "token", type: "address" }], }, { name: "resolveTokenManager", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "bytes32" }], outputs: [{ name: "tokenManager", type: "address" }], }, ] as const; const tokenId = await sepoliaPublic.readContract({ address: CCTS_ADDRESS, abi: serviceAbi, functionName: "crossChainTokenId", args: [account.address, salt], }); console.log("Your tokenId:", tokenId); // Note this value — it will be identical on every blockchain you deploy to. ``` ## Step 3: Deploy on Ethereum Sepolia Call `deployCrossChainToken` on Ethereum Sepolia. No fee quote is needed—this is a local CREATE3 deploy. The protocol creates the `CrossChainToken` ERC-20 and a `NATIVE_CROSSCHAIN_TOKEN` `TokenManager` in the same transaction. <Warning> **Verify the live ABI before copying this struct.** The sandbox contract's `DeploymentParams` may omit `initialSupply` while the production contract includes it, or vice versa. A struct with the wrong number of fields causes the transac
프로젝트로 돌아가기