bitpro가 원문에서 글자만 모은 내용이에요. 표·그림·수식은 빠지거나 깨질 수 있어요. 전체 75쪽 중 28쪽
ce<br/>(singleton)"]
TM1["TokenManager<br/>tokenId 0x1234..."]
TM2["TokenManager<br/>tokenId 0x5678..."]
TM3["TokenManager<br/>tokenId 0xabcd..."]
Token1[ERC-20 token A]
Token2[ERC-20 token B]
CCT["CrossChainToken<br/>(deployed by CCTP for non-USDC)"]
CCTS --> TM1
CCTS --> TM2
CCTS --> TM3
TM1 -.manages.-> Token1
TM2 -.manages.-> Token2
TM3 -.manages.-> CCT
```
The `CrossChainTokenService` is one contract per domain. It fronts every
`TokenManager` on that domain. A `TokenManager` is created for each configured
token and is identified across all domains by its `tokenId`.
## Token manager types
Every `TokenManager` has a type that determines how it moves tokens on the
source and destination blockchains during a transfer. The type is set when the
token is configured.
```solidity Solidity theme={null}
enum TokenManagerType {
NATIVE_CROSSCHAIN_TOKEN, // 0
BURN_MINT, // 1
LOCK_UNLOCK // 2
}
```
| Type | Source domain behavior | Destination domain behavior | When to use |
| - | - | - | - |
| `NATIVE_CROSSCHAIN_TOKEN` | Token self-burns from the sender | Token self-mints to the recipient | The token is a `CrossChainToken` deployed by the protocol |
| `BURN_MINT` | `TokenManager` pulls tokens from sender and burns them | `TokenManager` mints tokens to the recipient | An existing issuer-controlled token where the issuer can grant minter rights |
| `LOCK_UNLOCK` | `TokenManager` pulls tokens from sender and locks them | `TokenManager` unlocks tokens to the recipient | An existing token the protocol cannot mint or burn |
Most tokens use the same type on all blockchains. Exception: the ownerless model
pairs `LOCK_UNLOCK` on the home blockchain with `NATIVE_CROSSCHAIN_TOKEN` on
remote blockchains. See
[Ownerless versus custom tokens](/cctp/expanded-assets/concepts/ownerless-vs-custom-tokens).
## How tokens are identified
Each token is identified by its `tokenId`, a `bytes32` value derived from the
address that configured the token and a caller-chosen salt. The same `tokenId`
identifies the same token across every blockchain where it is configured.
You do not construct a `tokenId` manually. The `CrossChainTokenService` returns
it when you configure or deploy a token, and exposes read helpers to rederive it
later:
* `getOwnerlessTokenId(tokenAddress)` for ownerless tokens
* `crossChainTokenId(deployer, salt)` for native `CrossChainToken` deployments
(`deployCrossChainToken`)
* `crossChainTokenId(deployer, customTokenDeploySalt(deployer, salt))` for
custom tokens. Calling `crossChainTokenId(deployer, salt)` alone yields a
different id. `customTokenDeploySalt` is also the storage salt used to derive
the custom token's `TokenManager`.
Because the derivation uses `(msg.sender, salt)`, using the same deployer
address and the same `salt` on every blockchain produces the same `tokenId`
everywhere. This is what lets a single value identify a token on every supported
blockchain without an offchain registry.
Once you have a `tokenId`, look up the local contracts:
* `resolveTokenManager(tokenId)` returns the `TokenManager` address.
* `resolveTokenAddress(tokenId)` returns the underlying ERC-20 address.
Both revert with `TokenNotRegistered(tokenId)` if no token manager has been
deployed for the supplied `tokenId` on the local domain. The predicted address
is deterministic through CREATE3, so callers that need a non-throwing existence
check can wrap the call in `try`/`catch` or perform their own `extcodesize`
check.
### Decimal consistency
The protocol does not scale token amounts when delivering transfers. A token
must use the same number of decimals on every blockchain where it is deployed.
## Transfer finality and fast burn allowance
Two transfer speeds are available, set by the `minFinalityThreshold` parameter
on `crossChainTransfer`:
* **Standard transfer** (`minFinalityThreshold = 2000`) waits for source
blockchain finality before attestation.
* **Fast transfer** (`minFinalityThreshold = 1000`) attests on soft finality and
consumes a per-token fast burn allowance.
Each token has a fast burn allowance maintained by Circle. The allowance is
denominated in USD and limits the total value that can be moved through fast
transfers of that token before the allowance refills. Standard transfers do not
consume the allowance. When a fast transfer is initiated, the protocol consumes
more than the face value of the transfer from the allowance to account for
short-term price movement of the token.
Before initiating a fast transfer, query `GET /v2/cctpx/allowances` to confirm
remaining capacity and that the token has FX pricing configured. If allowance is
insufficient or pricing data is unavailable (for example
`FX_SYMBOL_NOT_CONFIGURED`), Iris rejects the fast quote; there is no automatic
fallback. Request a standard quote (omit `PRE_FINALITY`) and use
`minFinalityThreshold = 2000` instead.
USDC and non-USDC fast transfers share the same `MessageTransmitterV2` and
finality thresholds, but each tracks its own allowance pool. USDC transfers
consume the USDC allowance; non-USDC transfers consume a per-token allowance.
## Forwarding service
An optional forwarding service delivers completed transfers to the recipient on
the destination blockchain. When forwarding is used, Circle relays the Iris
attestation and calls `receiveMessage` on the destination `MessageTransmitterV2`
on behalf of the sender, so neither the sender nor the recipient needs to submit
the attestation manually.
Forwarding is opt-in and must be requested when you obtain the fee quote from
the Iris API. The fee quote response includes a signed claim that you pass as
the `claim` parameter on `crossChainTransfer`. Transfers that omit the claim
proceed without automatic relay, and you or the recipient are responsible for
submitting the attestation to `MessageTransmitterV2`.
You might choose not to use forwarding if you manage delivery yourself, if you
are performing complex hook-based transactions that require manual relay
sequencing, or if you have your own relayer infrastructure.
## Reuse of CCTP infrastructure
No separate attestation service runs for non-USDC transfers. Every message flows
through the same `MessageTransmitterV2` and Iris (Circle's attestation API) that
CCTP uses for USDC transfers, including the same set of attesters, the same
finality thresholds, and the same message verification logic. USDC and non-USDC
transfers share the attestation pipeline but are otherwise independent. USDC
transfer behavior is unchanged.
# CCTP for non-USDC compared to CCTP
Source: https://developers.circle.com/cctp/expanded-assets/concepts/cctp-differences
Key integration differences for developers already familiar with CCTP who want to transfer existing configured tokens.
Integrating CCTP for non-USDC when you already know CCTP for USDC is mostly a
matter of switching entry contracts and adjusting for a different fee model. The
following sections document the key differences for developers integrating
pre-configured or third-party-configured tokens.
## What stays the same
This layer shares CCTP's core infrastructure:
* **Message transmitter**: Both use `MessageTransmitterV2`. Attestations are
delivered to the same contract on the destination.
* **Attestation service**: Both use Iris (Circle's attestation API). Polling
`/v2/messages/{sourceDomain}` works the same way.
* **Finality thresholds**: `minFinalityThreshold = 2000` for standard, `1000`
for fast. Same semantics as CCTP fast transfers.
* **Hooks**: Available on both protocols. The `CrossChainTokenExecutable`
interface is analogous to CCTP's `IMessageHandlerV2`.
## Key integration differences
### Entry contract and approval target
| | CCTP | CCTP for non-USDC |
| - | - | - |
| Source entry | `TokenMessengerV2` | `CrossChainTokenService` |
| Approval target | `TokenMessengerV2` | Per-token `TokenManager` (not the service) |
For CCTP, you approve `TokenMessengerV2` and call `depositForBurn`. For non-USDC
transfers, resolve the per-token `TokenManager` address from
`CrossChainTokenService` using `resolveTokenManager(tokenId)`, approve that
address, then call `crossChainTransfer` on the service.
Approving the service contract directly does not work; the approval must go to
the `TokenManager`.
### Fee model
CCTP deducts its fee from the transferred USDC amount; the recipient receives
slightly less than the sender transferred. Non-USDC fees are paid in native gas
on the source blockchain at transfer time, so the recipient receives the full
transferred amount.
### Forwarding
CCTP forwarding (auto-delivery of the destination mint) is compulsory whenever
the forwarding service is used. For non-USDC transfers, forwarding is opt-in:
include a `FORWARD` entry in the `requests` array when you fetch the Iris fee
quote. Transfers that omit `FORWARD` require you or the recipient to submit
`receiveMessage` on the destination `MessageTransmitterV2` manually.
### Allowance pools for fast transfers
Both protocols support fast transfers, but they track allowance separately. USDC
fast transfers consume the USDC allowance; non-USDC fast transfers consume a
per-token allowance maintained by Circle. Check the per-token allowance using
`GET /v2/cctpx/allowances` before initiating a fast transfer; if the allowance
is insufficient or FX pricing is not configured for the token, the quote returns
`FX_SYMBOL_NOT_CONFIGURED` and the fast transfer cannot proceed.
### Token prerequisite
USDC works on CCTP without setup. Non-USDC transfers require the token to be
configured before it can be transferred. If you are using a pre-configured token
(such as EURC), the token is already available and no configuration step is
needed on your end. To check which tokens are pre-configured, see
[Supported blockchains](/cctp/expanded-assets/concepts/supported-chains-and-domains).
# Denylist layers
Source: https://developers.circle.com/cctp/expanded-assets/concepts/denylist-layers
The two independent address-screening layers enforced on every non-USDC transfer.
CCTP for non-USDC enforces two independent address-screening layers on every
non-USDC crosschain transfer. Both must clear before a transfer succeeds. They
differ in scope, in who manages them, and in where they execute.
## Layer 1: service-level denylist
Every transfer on this blockchain—outbound and inbound, regardless of
`TokenManagerType`—is screened against an `IDenylistProvider` configured on the
local `CrossChainTokenService`. The provider exposes a single view:
```solidity Solidity theme={null}
interface IDenylistProvider {
function isDenylisted(address account) external view returns (bool);
}
```
* **Outbound:** every payable transfer entrypoint screens both `msg.sender` and
`tx.origin`. Denied addresses cause the call to revert before the burn or lock
happens.
* **Inbound:** the service screens the transfer recipient before delivering
tokens. Denied recipients cause the inbound `receiveMessage` to revert with
`DenylistedAddress(recipient)`.
The provider is set by the service operator using
[`setDenylistProvider`](/cctp/expanded-assets/references/contract-reference).
The check is fail-closed: if the provider's `staticcall` reverts, the transfer
reverts.
This layer applies to every token on this blockchain. There is no opt-out at the
token level.
## Layer 2: per-token denylist on the `CrossChainToken`
Each `CrossChainToken` (CCT) deployed as `NATIVE_CROSSCHAIN_TOKEN` can carry its
own `IDenylistProvider`. The CCT owner sets it by calling
[`updateDenylistProvider`](/cctp/expanded-assets/references/contract-reference).
Setting `address(0)` removes per-token screening; the service-level layer still
applies.
The check runs inside the ERC-20 `_update` and screens both `from` and `to` on
every transfer, mint, and burn. `transferFrom` additionally screens the spender
at the entrypoint. Denied accounts cause `AccountDenylisted(account)` to revert.
Layer 2 exists only for protocol-deployed tokens. `BURN_MINT` and `LOCK_UNLOCK`
configurations of pre-existing ERC-20 tokens do not have this layer; whatever
screening the underlying token implements applies independently.
## Initial provider on a new `CrossChainToken`
When the service deploys a `CrossChainToken`, the new contract's initial
`denylistProvider` value depends on the deploy path:
| Deploy path | Initial `denylistProvider` |
| - | - |
| Remote ownerless token (`deployRemoteOwnerlessToken`) | Service's `denylistProvider` (inherited) |
| Remote custom token (`deployRemoteCrossChainToken`) | `address(0)` (owner opts in) |
| Local custom token (`deployCrossChainToken`) | `address(0)` (owner opts in) |
In other words: ownerless crosschain tokens get Circle's screening list out of
the box on every remote domain. Custom tokens always start with no per-token
screening; the issuer opts in by calling `updateDenylistProvider` themselves.
## Comparison
| Aspect | Layer 1 (service-level) | Layer 2 (per-token) |
| - | - | - |
| Where it lives | Service storage; queried by `CrossChainTokenService` | The `CrossChainToken` (only `NATIVE_CROSSCHAIN_TOKEN`) |
| Configured by | Service operator | The CCT owner |
| Applies to | Every flow on this blockchain (outbound and inbound) | ERC-20 movements of the specific CCT, when its provider is non-zero |
| Initial value | Set during service initialization | Inherited from the service for remote ownerless; `address(0)` otherwise |
| Provider interface | `isDenylisted(address)` | `isDenylisted(address)` |
| Revert | `DenylistedAddress(account)` (service) / fail-closed on provider error | `AccountDenylisted(account)` (CCT) |
## Composition
The two layers are independent. Layer 1 fires at the service boundary; layer 2
fires inside the `CrossChainToken`'s `_update`. A transfer must clear both.
Layer 2 has a single provider slot. Setting a new provider replaces the previous
one—it does not chain. An owner who wants both Circle's screening and a custom
list must implement a composite provider that calls both upstream lists. The
service-level layer at the boundary is unaffected by what the owner does on the
CCT; layer 1 always runs.
# Hooks
Source: https://developers.circle.com/cctp/expanded-assets/concepts/hooks
Atomically execute logic on the destination blockchain when a CCTP for non-USDC transfer arrives.
CCTP for non-USDC transfers can carry arbitrary `hookData` that is delivered
together with the tokens on the destination blockchain. When
`autoExecuteHookData` is `true` and the recipient implements
`executeWithCrossChainToken`, CCTP for non-USDC invokes that entrypoint in the
same transaction that delivers the tokens.
## Enabling hook execution
A sender provides three parameters on
[`crossChainTransfer`](/cctp/expanded-assets/references/contract-reference) to
control hook behavior:
| Parameter | Purpose |
| - | - |
| `destinationAddress` | The contract that receives the tokens and the hook callback |
| `hookData` | Arbitrary `bytes` payload passed to the recipient's hook |
| `autoExecuteHookData` | When `true` and `hookData` is non-empty, the destination `CrossChainTokenService` executes the hook |
If `autoExecuteHookData` is `false`, the tokens are delivered but the hook is
not called. If `hookData` is empty, the hook is not called even when
`autoExecuteHookData` is `true`.
<Warning>
Hook receivers execute arbitrary logic in the same transaction that delivers
tokens. A vulnerable hook can lock or redirect funds permanently. Thoroughly
audit any hook contract before deploying to mainnet—incorrect access control,
unchecked external calls, and missing input validation are common failure modes.
</Warning>
## Receiver contract requirement
When `autoExecuteHookData` is `true` and `hookData` is non-empty, the
`destinationAddress` **must** be a contract that implements
`executeWithCrossChainToken` and returns the
[`EXECUTE_SUCCESS`](/cctp/expanded-assets/references/contract-reference#execute_success)
sentinel. Restrict the caller to the local `CrossChainTokenService`.
```solidity Solidity theme={null}
function executeWithCrossChainToken(
uint32 sourceDomain,
bytes calldata sourceAddress,
bytes32 tokenId,
address token,
uint256 amount,
uint32 finalityThresholdExecuted,
bytes calldata hookData
) external returns (bytes32);
```
Return `keccak256("circle-cctpx-execute-success")`. See
[Build a hook receiver](/cctp/expanded-assets/howtos/build-hook-receiver) for a
full example.
If the destination is an externally owned account, doesn't implement the
interface, or returns a value other than `EXECUTE_SUCCESS`, the destination
service reverts with `HookDataExecutionFailed`. Sending hook data to an address
that can't satisfy the interface will fail the transfer at delivery.
## Atomicity
The hook executes in the same transaction as token delivery. If the hook
reverts, the entire transfer reverts. Tokens are not delivered and the
source-domain transfer remains intact. Designing hooks that consider their own
failure modes is the receiver's responsibility.
## Hook parameters
The `sourceAddress` is encoded as `bytes` rather than `address` to support
source blockchains where addresses do not fit in 20 bytes.
`finalityThresholdExecuted` is the finality threshold at which the inbound
message was attested: `2000` for finalized transfers, `1000` for fast
(pre-finalized) transfers. Receivers can branch on this value to decide whether
to perform side-effects immediately or defer them.
## Finality threshold and rollback
Hooks may execute on pre-finalized messages when the sender opts into
`minFinalityThreshold < 2000`. At that finality, the source blockchain
transaction can still be reorged, which would invalidate the delivery.
Integrators using `autoExecuteHookData` with `minFinalityThreshold < 2000` must
treat hook execution on unfinalized messages as provisional and design their
hook contracts to tolerate potential rollback—for example, by deferring
irreversible side-effects until `finalityThresholdExecuted` equals `2000` or
until finality is otherwise confirmed.
Prefer standard finality (`2000`) for hook transfers unless you have explicitly
designed for provisional execution.
## Self-relay required for hook transfers
Iris does not relay transfers that carry hook data with
`autoExecuteHookData = true`. Do not include `FORWARD` in the fee quote for
these transfers. You or your own relayer infrastructure must call
`receiveMessage` on the destination `MessageTransmitterV2` to complete the
transfer. Plan for this when designing a hook-based integration: every hook
transfer requires the sender (or a relayer the sender operates) to drive the
destination call.
# Ownerless and custom tokens
Source: https://developers.circle.com/cctp/expanded-assets/concepts/ownerless-vs-custom-tokens
The two integration models CCTP for non-USDC supports for making a token crosschain.
Two integration models exist for taking an existing token crosschain: ownerless
tokens and custom tokens. Anyone can configure either type. The two models
differ in who controls the resulting bridge connection and whether Circle can
intervene.
## Ownerless tokens
An ownerless token is an existing token configured for crosschain use under an
ownerless connection. Ownerless tokens are intended for community assets without
an active issuer.
Connections start ownerless. At configuration, no owner or operator can be set
on the resulting `TokenManager`. Circle can assume operatorship or ownership
later in special circumstances.
Ownerless connections have no rate limits or maximum transfer amount by default.
Circle can add controls only if the bridged token reaches a meaningful total
value locked (TVL) threshold and has a reliable price oracle.
On the home blockchain the `TokenManager` uses `LOCK_UNLOCK`. On remote
blockchains, CCTP for non-USDC deploys a wrapped `CrossChainToken` and the
`TokenManager` uses `NATIVE_CROSSCHAIN_TOKEN`, which uses mint and burn.
<Warning>
Ownerless tokens are 프로젝트로 돌아가기