Circle USYC 백서

Circle USYC

원문

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

designed for assets with no issuer or active controller. If you issue or have authority over the token, use the [custom token model](#custom-tokens) instead. Configuring an issuer-controlled asset as an ownerless token removes your ability to set rate limits, pause transfers, or manage the bridge connection. </Warning> ## Custom tokens A custom token is configured by a developer who keeps full control of the crosschain connection. The deployer sets the owner and operator at configuration, configures rate limits and max transfer amount, and holds the pauser role. **Circle cannot assume control of a custom-configured connection.** Custom tokens are intended for issuer-controlled assets, for example, fiat-backed stablecoins and RWAs. The issuer chooses `BURN_MINT` or `LOCK_UNLOCK` for the source blockchain's `TokenManager`. Remote blockchains always use `BURN_MINT`. If you use `LOCK_UNLOCK`, apply it on only one blockchain—using it on multiple source blockchains causes transfer errors when assets route between more than two blockchains. The issuer grants the local `TokenManager` the minter role on tokens using `BURN_MINT`. The per-token denylist (layer 2) applies only to protocol-deployed `CrossChainToken`s. A pre-existing ERC-20 configured as custom `BURN_MINT` relies on its own denylist plus the service-level layer. ## Comparison | Aspect | Ownerless | Custom | | - | - | - | | Who can configure | Anyone | Anyone | | Ownership at creation | No owner or operator | Deployer sets owner and operator | | Circle override | Circle can assume operatorship or ownership in special circumstances | Circle has no control over the connection | | Configurable rate limits | No (no rate limits by default; Circle may add controls if TVL and price oracle thresholds are met) | Yes | | Configurable denylist | Remote CCT inherits service denylist; no self-serve override | Service-level denylist applies; CCT layer 2 only on protocol-deployed `CrossChainToken` | | Home `TokenManagerType` | `LOCK_UNLOCK` | `BURN_MINT` or `LOCK_UNLOCK` (issuer's choice; use `LOCK_UNLOCK` on one blockchain only) | | Remote `TokenManagerType` | `NATIVE_CROSSCHAIN_TOKEN` | `BURN_MINT` | | Token contract on remote | Deployed by CCTP for non-USDC as a `CrossChainToken` | Deployed by the issuer directly or through the protocol on each blockchain | | Typical token | Ownerless community asset | Issuer-controlled (stablecoins, RWAs) | ## Choosing a model The two models exist to support two distinct integration shapes: * **Choose ownerless** when you don't own or operate the underlying token and want to bridge an unmanaged asset without taking on bridge governance. Ownerless connections run on default parameters and stay ownerless unless Circle assumes control to respond to a security incident or protocol-level change. * **Choose custom** when you issue the token (or otherwise have authority to manage it) and want to make your token crosschain with full, unilateral control over rate limits, pause, and upgrades. Custom connections give you authority that Circle cannot override. The per-token denylist is available when you deploy a `CrossChainToken`; existing ERC-20 custom configurations rely on your token's own controls plus the service denylist. The protocol guarantees that any pre-existing token has at least one ownerless bridge available, while still letting issuers run their own bridge when they need to. # Supported blockchains and domains Source: https://developers.circle.com/cctp/expanded-assets/concepts/supported-chains-and-domains Blockchains, domains, and pre-configured tokens available on CCTP for non-USDC. Non-USDC transfers are supported on EVM-compatible blockchains only. The domain IDs are shared with CCTP. Trust is configured bilaterally: a domain must be added as trusted on both ends before transfers can route between them. ## Supported blockchains The following EVM-compatible blockchains have CCTP for non-USDC deployments. The table includes both mainnet and testnet environments; domain IDs are consistent across both. Non-EVM blockchains, including Solana, are not supported. | Blockchain | Domain ID | | - | - | | Ethereum | 0 | | Avalanche | 1 | | OP Mainnet | 2 | | Arbitrum | 3 | | Base | 6 | | Polygon PoS | 7 | | Arc Testnet | 26 | Domain IDs match the [CCTP domain identifiers](/cctp/concepts/supported-chains-and-domains#domain-identifiers). For deployed contract addresses, see [Contract Addresses](/cctp/expanded-assets/references/contract-addresses). ## Pre-configured tokens The following tokens are pre-configured on the supported blockchains. Token IDs are stable identifiers used in API calls, contract interactions, and code samples. | Token | Symbol | Type | Token ID | | - | - | - | - | | Euro Coin | EURC | Custom | `0x6ca9e29fa53becc29becaf4a90b9ca7a995ad4d2234880da13ca38c657fb241c` | | Circle Wrapped Bitcoin | cirBTC | Custom | `0x3d26699fb5d40190fc3fa0dcbc1cd24e558355043c1997572ff9fd6efbb3fdca` | | Wrapped Ether | WETH | Ownerless | `0x5c2cf12eccf48ab63313169ce192dc00af7c37a58bbc546ba4becf54df4962ea` | USDC transfers use standard CCTP. To transfer USDC across blockchains, use [CCTP](/cctp). To retrieve the full list of supported tokens and routes, including sandbox token IDs and per-domain deployment addresses, query the Iris API. The endpoint is paginated. Use `pageSize` to set the number of results per page, and `pageAfter` with the `nextPageAfter` value from the previous response to advance to the next page. Pass `tokenid` to filter by a specific token: ```bash theme={null} # All tokens (mainnet) curl https://iris-api.circle.com/v2/cctpx/tokens # Filter by token ID (mainnet EURC) curl "https://iris-api.circle.com/v2/cctpx/tokens?tokenid=0x6ca9e29fa53becc29becaf4a90b9ca7a995ad4d2234880da13ca38c657fb241c" # All tokens (sandbox) with a custom page size curl "https://iris-api-sandbox.circle.com/v2/cctpx/tokens?pageSize=150" # Next page (sandbox) curl "https://iris-api-sandbox.circle.com/v2/cctpx/tokens?pageSize=150&pageAfter=<nextPageAfter>" ``` ## Domain trust The `CrossChainTokenService` on each domain maintains its own list of trusted remote domains. Both the source and destination services must list each other before transfers route successfully. To check whether a remote domain is configured on the local service, call [`isTrustedDomain`](/cctp/expanded-assets/references/contract-reference). # How-to: Build a hook receiver Source: https://developers.circle.com/cctp/expanded-assets/howtos/build-hook-receiver Implement executeWithCrossChainToken so logic runs atomically when a CCTP for non-USDC transfer arrives. A hook receiver is a destination-side contract that receives CCTP for non-USDC tokens and runs custom logic in the same transaction that delivers them. The receiver must implement `executeWithCrossChainToken` and return [`EXECUTE_SUCCESS`](/cctp/expanded-assets/references/contract-reference#execute_success). For background, see [Hooks](/cctp/expanded-assets/concepts/hooks). ## Prerequisites Before you begin, ensure that you've: * Set up a Solidity development environment (Foundry, Hardhat, or similar) * Located the local [`CrossChainTokenService` address](/cctp/expanded-assets/references/contract-addresses) ## Steps <Steps> <Step title="Implement executeWithCrossChainToken"> Create a contract that exposes the hook entrypoint the destination service calls. Restrict the caller to the local `CrossChainTokenService`, run your logic, and return `EXECUTE_SUCCESS`. ```solidity Solidity theme={null} import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; contract VaultDepositReceiver { bytes32 public constant EXECUTE_SUCCESS = keccak256("circle-cctpx-execute-success"); address public immutable service; address public immutable vault; error OnlyService(); constructor(address _service, address _vault) { service = _service; vault = _vault; } function executeWithCrossChainToken( uint32 /* sourceDomain */, bytes calldata /* sourceAddress */, bytes32 /* tokenId */, address token, uint256 amount, uint32 /* finalityThresholdExecuted */, bytes calldata hookData ) external returns (bytes32) { if (msg.sender != service) revert OnlyService(); address recipient = abi.decode(hookData, (address)); // Example app logic only — not a CCTP requirement. Replace with your own. IERC20(token).approve(vault, amount); IVault(vault).depositFor(recipient, token, amount); return EXECUTE_SUCCESS; } } ``` <Warning> Reverting inside `executeWithCrossChainToken` rolls back destination delivery (including the mint or unlock) in that `receiveMessage` transaction. The source burn or lock has already happened and is not undone. Delivery can be retried later. Use `autoExecuteHookData: false` if you want tokens delivered without running hook logic. </Warning> </Step> <Step title="Deploy the receiver on every destination blockchain"> Deploy the receiver on every blockchain where transfers will arrive with hook data. Each deployment must bind to the local `CrossChainTokenService`. </Step> <Step title="Send a transfer with hook data"> On the source blockchain, encode the hook payload and call [`crossChainTransfer`](/cctp/expanded-assets/references/contract-reference) with `destinationAddress` set to the receiver contract and `autoExecuteHookData` set to `true`. ```typescript TypeScript theme={null} import { encodeAbiParameters, encodePacked } from "viem"; // hookData is an application payload — ABI encoding is fine here. const hookData = encodeAbiParameters([{ type: "address" }], [recipient]); // destinationAddress must be raw packed address bytes (20 bytes on EVM). // Do not use encodeAbiParameters — that pads to 32 bytes and breaks delivery. const destinationAddress = encodePacked(["address"], [receiverAddress]); await walletClient.writeContract({ address: serviceAddress, abi: serviceAbi, functionName: "crossChainTransfer", args: [ tokenId, 100n * 10n ** 18n, destinationDomain, destinationAddress, "0x0000000000000000000000000000000000000000000000000000000000000000", 2000, // standard finality; prefer this for hooks claim, true, // autoExecuteHookData hookData, ], value: feeTotalAmount, }); ``` The `claim` and `feeTotalAmount` values are returned by the Iris fee-quote endpoint. Use a standard quote body (`amount` + `feeToken` only) unless your token is enabled for fast transfer. Do not include `FORWARD` when `autoExecuteHookData` is `true`—hook transfers require self-relay. See [`QuoteClaim`](/cctp/expanded-assets/references/contract-reference). Before calling `crossChainTransfer` on the service, approve the source `TokenManager` (not the service) for the transfer amount. This applies to `NATIVE_CROSSCHAIN_TOKEN`, `BURN_MINT`, and `LOCK_UNLOCK`. </Step> <Step title="Relay the message to the destination"> Iris does not relay transfers that carry hook data with `autoExecuteHookData` set to `true`. After Iris attests the source blockchain message, you or your own relayer must call `receiveMessage` on the destination `MessageTransmitterV2` to deliver the transfer and trigger the hook. See [Self-relay required for hook transfers](/cctp/expanded-assets/concepts/hooks#self-relay-required-for-hook-transfers). When the destination service receives the message, it delivers the tokens and calls your receiver's hook in the same transaction. </Step> </Steps> # How-to: Configure a denylist provider Source: https://developers.circle.com/cctp/expanded-assets/howtos/configure-denylist-provider Configure a per-token denylist provider on a CrossChainToken you own. Configure the per-token denylist on a `CrossChainToken` you own. The per-token layer is independent of the service-level denylist that Circle configures on every blockchain—see [Denylist layers](/cctp/expanded-assets/concepts/denylist-layers) for the two-layer model. The service-level layer always runs; this guide only covers the layer the CCT owner controls. <Note> Only `CrossChainToken` contracts deployed as `NATIVE_CROSSCHAIN_TOKEN` have a per-token denylist. `BURN_MINT` and `LOCK_UNLOCK` configurations of pre-existing ERC-20 tokens rely on whatever screening the underlying token already implements. </Note> ## Prerequisites Before you begin, ensure that you've: * Obtained the `tokenId` of the `CrossChainToken` you want to configure. * Confirmed that the wallet you'll use owns the `CrossChainToken` (the value returned by `owner()` on the CCT). The denylist provider can only be changed by the owner. ## Steps <Steps> The snippets assume that `publicClient`, `walletClient`, `serviceAddress`, `tokenId`, and `newProviderAddress` are already initialized. Each code example includes only the ABI entries needed for that operation. Receipt-handling code is omitted for brevity, but you should wait for each state-changing transaction to confirm before reading the updated state or submitting the next transaction. <Step title="Choose a denylist provider"> A denylist provider is any contract that implements: ```solidity Solidity theme={null} interface IDenylistProvider { function isDenylisted(address account) external view returns (bool); } ``` Common choices: * **Reuse Circle's screening list.** Read the service-level provider from the local `CrossChainTokenService` and configure your CCT to use the same one. Per-token screening then matches Circle's central list. * **Bridge a FiatToken denylist.** Wrap an existing FiatToken's `isBlacklisted` view in an `IDenylistProvider` adapter (Circle ships `FiatTokenDenylistAdapter`). * **Implement your own.** Any contract that exposes `isDenylisted(address)` can serve as a provider. * **Compose multiple lists.** Write a small provider that combines results from several upstream lists using a logical OR. The CCT has a single provider slot. Setting a new provider **replaces** the previous one. If you want both Circle's list and your own, deploy a composite provider that calls both internally. </Step> <Step title="Discover the CrossChainToken address"> ```typescript TypeScript theme={null} const serviceAbi = [ { name: "resolveTokenAddress", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "bytes32" }], outputs: [{ name: "token", type: "address" }], }, ] as const; const tokenAddress = await publicClient.readContract({ address: serviceAddress, abi: serviceAbi, functionName: "resolveTokenAddress", args: [tokenId], }); ``` </Step> <Step title="Set the denylist provider"> Call [`updateDenylistProvider`](/cctp/expanded-assets/references/contract-reference) on the `CrossChainToken` with the address of your chosen provider. ```typescript TypeScript theme={null} const updateDenylistProviderAbi = [ { name: "updateDenylistProvider", type: "function", stateMutability: "nonpayable", inputs: [{ name: "newProvider", type: "address" }], outputs: [], }, ] as const; await walletClient.writeContract({ address: tokenAddress, abi: updateDenylistProviderAbi, functionName: "updateDenylistProvider", args: [newProviderAddress], }); ``` The contract emits `DenylistProviderUpdated(oldProvider, newProvider)`. </Step> <Step title="Verify the new provider"> ```typescript TypeScript theme={null} const denylistProviderAbi = [ { name: "denylistProvider", type: "function", stateMutability: "view", inputs: [], outputs: [{ name: "", type: "address" }], }, ] as const; const current = await publicClient.readContract({ address: tokenAddress, abi: denylistProviderAbi, functionName: "denylistProvider", }); if (current.toLowerCase() !== newProviderAddress.toLowerCase()) { throw new Error("Denylist provider was not updated"); } ``` </Step> <Step title="Disable per-token screening (optional)"> To remove per-token screening, set the provider to `address(0)`: ```typescript TypeScript theme={null} const updateDenylistProviderAbi = [ { name: "updateDenylistProvider", type: "function", stateMutability: "nonpayable", inputs: [{ name: "newProvider", type: "address" }], outputs: [], }, ] as const; await walletClient.writeContract({ address: tokenAddress, abi: updateDenylistProviderAbi, functionName: "updateDenylistProvider", args: ["0x0000000000000000000000000000000000000000"], }); ``` After the transaction confirms, read `denylistProvider()` and verify that it returns `address(0)`. The service-level layer continues to apply. There is no way for a CCT owner to opt out of the service-level layer. </Step> </Steps> # How-to: Configure rate limits and transfer caps Source: https://developers.circle.com/cctp/expanded-assets/howtos/configure-rate-limits-and-caps Set the rate limit, max transfer amount, and rate-limit window on a CCTP for non-USDC TokenManager. The `TokenManager` applies two transfer controls per token: a rate limit over a fixed epoch window, and a per-transaction maximum transfer amount. The rate limit tracks inbound and outbound flow; the max transfer amount applies to outbound transfers. Both controls live on the `TokenManager` for the token. Use this guide to configure them. Attestation does not bypass destination `TokenManager` checks. An inbound transfer can still be rejected by the rate limit or blocked while the token is paused. <Warning> Custom token issuers must configure rate limits and a max transfer amount before allowing public transfers. A rate limit configured as `0` blocks inbound and outbound flows. Every `crossChainTransfer` call reverts until you set a non-zero limit. </Warning> ## How the throttles behave * **Rate limit**: Caps the absolute net flow (`|flowIn - flowOut|`) within each fixed epoch window. It applies to inbound and outbound flow. The default window length is six hours. Setting the rate limit to `0` blocks both directions. After configuration, always verify the limit is non-zero before your first `crossChainTransfer`; a common mistake is configuring with `rateLimit: 0` and skipping this guide. * **Max transfer amount**: Caps the size of a single outbound transfer transaction. Applies on top of the rate limit to prevent oversize single transfers regardless of the headroom remaining in the rate-limit window. Setting `maxTransferAmount` to `0` blocks all outbound transfers. Use `type(uint256).max` (or `2n ** 256n - 1n` in TypeScript) to disable the per-transfer cap. For example, with a rate limit of `1,000,000` over six hours: * A user transfers `300,000` out and `100,000` in during the window. Net outbound consumed is `200,000`. Remaining headroom: `800,000`. * A second user attempts to transfer `900,000` out in the same window. The transfer reverts because net outbound would exceed `1,000,000`. ## Prerequisites Before you begin, ensure that you've: * Obtained the `tokenId` for the token you want to configure. * Confirmed the wallet you'll use holds the operator role for that token's `TokenManager`. ## Steps <Steps> The snippets assume that `publicClient`, `walletClient`, `serviceAddress`, and `tokenId` are already initialized. Each code example includes only the ABI entries needed for that operation. Receipt-handling code is omitted for brevity, but you should wait for each state-changing transaction to confirm before reading the updated state or submitting the next transaction. <Step title="Discover the TokenManager address"> Read the local `TokenManager` address from the `CrossChainTokenService`: ```typescript TypeScript theme={null} const serviceAbi = [ { name: "resolveTokenManager", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "bytes32" }], outputs: [{ name: "tokenManager", type: "address" }], }, ] as const; const tokenManager: `0x${string}` = await publicClient.readContract({ address: serviceAddress, abi: serviceAbi, functionName: "resolveTokenManager", args: [tokenId], }); ``` </
프로젝트로 돌아가기