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],
});
```
</프로젝트로 돌아가기