CCTPBridgeAdapterLib
Title: CCTPBridgeAdapterLib
Authors: David Dada <daveproxy80@gmail.com> (https://github.com/dadadave80), Modified from Circle CCTP v2 (https://github.com/circlefin/evm-cctp-contracts)
Logic + ERC-7201 storage for the Circle CCTP v2 USDC token-bridge adapter. Outbound
depositForBurn pulls exactly amount USDC from the caller, force-approves the TokenMessenger for
exactly that amount, burns via CCTP, then resets the allowance to 0 (approval hygiene). Inbound
relayMessage is a PERMISSIONLESS passthrough to MessageTransmitterV2.receiveMessage; the mint
goes DIRECTLY to the recipient through Circle's transmitter and the adapter adds no authorization —
except that a Lattice HOOKED burn is refused there (only relayMessageWithHook, or the attested
mintRecipient itself, may relay it), so the hook cannot be skipped by a third party.
HOOKED BURNS: for a Lattice hook envelope the destination domain's destinationCaller MUST be the
destination diamond (enforced outbound). Otherwise anyone can call Circle's transmitter directly and mint
hook-less.
TRUST MODEL: the inbound mint's correctness is rooted entirely in Circle's off-chain Iris attester set
and denylist (the transmitter verifies the attestation signatures) — this adapter neither attests nor
gates it. CCTP is a token bridge; it is intentionally NOT an IERC7786GatewaySource and is never
routed through ERC7786OpenBridge / CrosschainLink. Reuses the shared safe-transfer / force-approve
helpers (BridgeFungibleLib.pullExact, AdapterBaseLib.forceApprove); no bespoke ERC-20 plumbing.
Functions
cctpBridgeAdapterStorage
function cctpBridgeAdapterStorage() internal pure returns (CCTPBridgeAdapterStorage storage $);Returns
| Name | Type | Description |
|---|---|---|
| $ | CCTPBridgeAdapterStorage |
__CCTPBridgeAdapter_init
Configures the deployed CCTP contracts + USDC, deploys this diamond's CCTPHookExecutor, and registers the ICCTPBridgeAdapter ERC-165 id.
Reverts CCTPZeroAddress if any address is zero (BEFORE deploying the executor, so the zero-address
init-revert tests still hold). Called via delegatecall inside the diamond initializing window, so
address(this) IS the diamond — the executor is bound to it immutably as its relay.
function __CCTPBridgeAdapter_init(address tokenMessenger_, address messageTransmitter_, address usdc_) internal;Parameters
| Name | Type | Description |
|---|---|---|
| tokenMessenger_ | address | |
| messageTransmitter_ | address | |
| usdc_ | address |
registerInterface
Writes true to the ERC-165 map slot for ICCTPBridgeAdapter.
function registerInterface() internal;tokenMessenger
function tokenMessenger() internal view returns (address);Returns
| Name | Type | Description |
|---|---|---|
| <none> | address |
messageTransmitter
function messageTransmitter() internal view returns (address);Returns
| Name | Type | Description |
|---|---|---|
| <none> | address |
usdc
function usdc() internal view returns (address);Returns
| Name | Type | Description |
|---|---|---|
| <none> | address |
getDomain
function getDomain(uint256 chainId) internal view returns (uint32);Parameters
| Name | Type | Description |
|---|---|---|
| chainId | uint256 |
Returns
| Name | Type | Description |
|---|---|---|
| <none> | uint32 |
domainOwner
function domainOwner(uint32 domain) internal view returns (uint256);Parameters
| Name | Type | Description |
|---|---|---|
| domain | uint32 |
Returns
| Name | Type | Description |
|---|---|---|
| <none> | uint256 |
isChainRegistered
function isChainRegistered(uint256 chainId) internal view returns (bool);Parameters
| Name | Type | Description |
|---|---|---|
| chainId | uint256 |
Returns
| Name | Type | Description |
|---|---|---|
| <none> | bool |
getDomainConfig
function getDomainConfig(uint32 domain)
internal
view
returns (uint256 maxFee, uint32 minFinalityThreshold, bytes32 destinationCaller);Parameters
| Name | Type | Description |
|---|---|---|
| domain | uint32 |
Returns
| Name | Type | Description |
|---|---|---|
| maxFee | uint256 | |
| minFinalityThreshold | uint32 | |
| destinationCaller | bytes32 |
hookExecutor
function hookExecutor() internal view returns (address);Returns
| Name | Type | Description |
|---|---|---|
| <none> | address |
registerChainDomain
Registers chainId ⇒ CCTP domain (the domain table is caller-supplied, never inferred). Admin.
FAIL-LOUD identity registration (mirrors every other adapter's AlreadyRegistered guards): a chainId
registers exactly once (CCTPChainAlreadyRegistered) and a domain belongs to exactly one chainId
(CCTPDomainAlreadyRegistered) — a fat-fingered duplicate can no longer silently remap USDC burns
to the wrong destination or clobber another chain's domain config. chainId 0 is rejected so the
_domainOwner zero-sentinel stays unambiguous.
function registerChainDomain(uint256 chainId, uint32 domain) internal;Parameters
| Name | Type | Description |
|---|---|---|
| chainId | uint256 | |
| domain | uint32 |
configureDomain
Sets the per-domain outbound config (maxFee, minFinalityThreshold, destinationCaller). Admin.
Tunables stay UPDATABLE (unlike identity), but only for a registered domain
(CCTPDomainNotRegistered) — configuring an unowned domain is always a misconfiguration.
function configureDomain(uint32 domain, uint256 maxFee, uint32 minFinalityThreshold, bytes32 destinationCaller)
internal;Parameters
| Name | Type | Description |
|---|---|---|
| domain | uint32 | |
| maxFee | uint256 | |
| minFinalityThreshold | uint32 | |
| destinationCaller | bytes32 |
depositForBurn
Burns amount USDC (pulled from msg.sender) for minting to the ERC-7930 recipient on its
destination chain via CCTP v2. Strict CEI under the reentrancy guard: pull exactly amount,
approve the messenger for exactly amount, burn, then reset the allowance to 0.
function depositForBurn(uint256 amount, bytes calldata recipient) internal;Parameters
| Name | Type | Description |
|---|---|---|
| amount | uint256 | The USDC amount to burn (source of funds is the caller, NOT the Diamond balance). |
| recipient | bytes | The full ERC-7930 interoperable recipient (chain reference + destination address). |
depositForBurnWithHook
Like depositForBurn but attaches CCTP v2 hookData to the burn message (via
depositForBurnWithHook) for the destination recipient to execute.
Reverts CCTPEmptyHookData BEFORE any work if hookData is empty — a hook-less burn must use
depositForBurn so its DepositForBurn event and 7-arg CCTP call are unambiguous. For a Lattice
hook envelope (_isHookEnvelope) it reverts CCTPHookWithoutDestinationCaller BEFORE any token
movement if the destination domain's destinationCaller is bytes32(0): the domain MUST be locked to
the destination diamond so only that diamond can receive the message (where a third party's hook-less
relay is refused). Other hookData is relayed plainly at the destination, so it is not guarded.
function depositForBurnWithHook(uint256 amount, bytes calldata recipient, bytes calldata hookData) internal;Parameters
| Name | Type | Description |
|---|---|---|
| amount | uint256 | The USDC amount to burn (source of funds is the caller, NOT the Diamond balance). |
| recipient | bytes | The full ERC-7930 interoperable recipient (chain reference + destination address). |
| hookData | bytes | The CCTP v2 hook payload to carry in the burn message (destination-executed, non-empty). |
_burn
Shared burn body for depositForBurn (empty hookData) and depositForBurnWithHook. Strict CEI
with exact-amount approval hygiene: parse recipient → registered-chain check → maxFee guard →
pull exactly amount → approve exactly amount → burn (7-arg plain, or 8-arg WITH hook when
hookData is non-empty) → reset the allowance to 0 → emit.
The caller (each public entrypoint) owns the nonReentrant frame — _burn runs INSIDE it.
function _burn(uint256 amount, bytes calldata recipient, bytes calldata hookData) private;Parameters
| Name | Type | Description |
|---|---|---|
| amount | uint256 | |
| recipient | bytes | |
| hookData | bytes |
_resolveRecipient
Resolves an ERC-7930 recipient to its (destChainId, domain, mintRecipient), reverting
CCTPUnknownDestinationChain if the decoded chain has no registered CCTP domain.
Split out of _burn to keep its stack shallow (no via-IR).
function _resolveRecipient(CCTPBridgeAdapterStorage storage $, bytes calldata recipient)
private
view
returns (uint256 destChainId, uint32 domain, bytes32 mintRecipient);Parameters
| Name | Type | Description |
|---|---|---|
| $ | CCTPBridgeAdapterStorage | |
| recipient | bytes |
Returns
| Name | Type | Description |
|---|---|---|
| destChainId | uint256 | |
| domain | uint32 | |
| mintRecipient | bytes32 |
_settleBurn
Applies the maxFee guard, pulls EXACTLY amount from the caller, approves the messenger for
EXACTLY amount, burns (plain 7-arg depositForBurn, or 8-arg depositForBurnWithHook when
hookData is non-empty), then resets the allowance to 0 (approval hygiene).
destinationCaller guard (Lattice hook envelopes only, per _isHookEnvelope; before ANY token
movement): reverts CCTPHookWithoutDestinationCaller when the domain is permissionless. Other
hookData is relayed plainly at the destination anyway, so the lock would protect no Lattice hook.
maxFee guard (before ANY token movement): CCTP's TokenMessengerV2 requires amount > maxFee; the
>= form also rejects a zero-amount burn. An unconfigured domain has maxFee == 0, mapping to
CCTP's free permissionless standard transfer (passes for any amount > 0). Split out of _burn so
the deep 8-arg external call has a shallow stack (no via-IR).
function _settleBurn(
CCTPBridgeAdapterStorage storage $,
uint256 amount,
uint32 domain,
bytes32 mintRecipient,
bytes calldata hookData
) private;Parameters
| Name | Type | Description |
|---|---|---|
| $ | CCTPBridgeAdapterStorage | |
| amount | uint256 | |
| domain | uint32 | |
| mintRecipient | bytes32 | |
| hookData | bytes |
_cctpBurn
Forwards a resolved CctpBurnCall to CCTP: the plain 7-arg depositForBurn when hookData is
empty, else the 8-arg depositForBurnWithHook. The bundle is a single memory arg so each field is
an MLOAD at the call site — the deep call fits the legacy-codegen stack limit (no via-IR).
function _cctpBurn(CctpBurnCall memory c) private;Parameters
| Name | Type | Description |
|---|---|---|
| c | CctpBurnCall |
relayMessage
PERMISSIONLESS passthrough: forwards an Iris-attested CCTP message to the transmitter, which
mints USDC DIRECTLY to the recipient. Adds no authorization — trust is Circle's attester set +
denylist. Reverts CCTPRelayFailed if the transmitter reports failure.
A message that passes every relayMessageWithHook check (a Lattice hooked burn for this adapter's
TokenMessenger) is refused with CCTPHookRelayRequired BEFORE the mint — relaying it here would burn
the CCTP nonce without ever running the hook. The one exception is its attested mintRecipient
(compared as the full bytes32), which may take its funds hook-less, so a hook that can never
succeed cannot lock them. Every other message relays as before, so no message is refused by both
entry points.
function relayMessage(bytes calldata message, bytes calldata attestation) internal;Parameters
| Name | Type | Description |
|---|---|---|
| message | bytes | The CCTP message bytes emitted by the source-chain burn. |
| attestation | bytes | The Iris attestation over message. |
relayMessageWithHook
PERMISSIONLESS relay that ALSO executes a Lattice hook envelope carried in the burn message.
SECURITY MODEL: hookData is 100% attacker-controlled — anyone on any CCTP chain can burn 1 uUSDC
toward this diamond with arbitrary hookData, and a valid Iris attestation authenticates only
"someone burned >= 1 uUSDC with these bytes", NOT intent. So the hook is executed through the
role-less, fund-less CCTPHookExecutor (the ONLY caller of the target) with a FIXED selector and
CONTEXT ARGS READ FROM THE ATTESTED MESSAGE (never from hookData). Order — ALL validation BEFORE
the external mint so a revert consumes nothing:
- pure validation: length, header/body version, header recipient == this TokenMessenger, and a well-formed Lattice envelope (length >= 24 && leading magic — never an abi.decode);
- mint via
MessageTransmitterV2.receiveMessage(revertCCTPRelayFailedon false); - LENIENT
executeHook— a reverting/return-bombing target does NOT revert the relay (the mint stands, the nonce is consumed). A hostile hook re-entering relayMessage/depositForBurn hits the shared reentrancy guard → the inner call reverts → the executor reportsfalse→ this outer relay still completes. The exception is a target whose OWN frame runs out of gas: the executor reverts ICCTPHookExecutor.CCTPHookOutOfGas, unwinding the mint so the nonce stays live. A sub-call's out-of-gas is an ordinary revert unless the target re-raises it (ICCTPHookReceiver); - emit
HookExecuted+RelayedMessage.
function relayMessageWithHook(bytes calldata message, bytes calldata attestation) internal;Parameters
| Name | Type | Description |
|---|---|---|
| message | bytes | The CCTP v2 BurnMessageV2 bytes (must carry a Lattice hook envelope). |
| attestation | bytes | The Iris attestation over message. |
_checkHookEnvelope
Classifies message against the hook-relay checks WITHOUT reverting, so relayMessageWithHook
(reverts on anything but Valid) and relayMessage (refuses Valid) share one definition.
Checks, in order: length >= 376, header AND body version == 1, header recipient == tokenMessenger_
(else NotBurnMessage); then a Lattice envelope at byte 376 per _isHookEnvelope (else
InvalidHookData). The length check runs first so no fixed-offset read touches a short message.
function _checkHookEnvelope(bytes calldata message, address tokenMessenger_)
private
pure
returns (HookEnvelopeCheck);Parameters
| Name | Type | Description |
|---|---|---|
| message | bytes | |
| tokenMessenger_ | address |
Returns
| Name | Type | Description |
|---|---|---|
| <none> | HookEnvelopeCheck |
_isHookEnvelope
Whether hookData is a Lattice envelope HOOK_MAGIC (4) ‖ target (20) ‖ payload — the ONE
definition shared by the outbound destinationCaller guard and the inbound relay checks.
TWO cheap comparisons — never an abi.decode that could revert on attacker garbage (the ||
short-circuits before slicing on < 24 B).
function _isHookEnvelope(bytes calldata hookData) private pure returns (bool);Parameters
| Name | Type | Description |
|---|---|---|
| hookData | bytes |
Returns
| Name | Type | Description |
|---|---|---|
| <none> | bool |
_executeInboundHook
Reads the ATTESTED context from message, decodes the target/payload from the validated
Lattice hookData envelope, and calls the hook target through the CCTPHookExecutor LENIENTLY —
a reverting/return-bombing target yields success == false, never a revert.
Split out of relayMessageWithHook purely to keep that function's stack shallow (no via-IR). All
context is read from the message, not from hookData. Emits HookExecuted.
function _executeInboundHook(bytes calldata message, bytes calldata hookData) private;Parameters
| Name | Type | Description |
|---|---|---|
| message | bytes | |
| hookData | bytes |
_readInboundContext
Reads the ATTESTED context (never from hookData) plus the envelope's target into an
InboundHook. Split out purely to keep relayMessageWithHook's stack shallow (no via-IR).
function _readInboundContext(bytes calldata message, bytes calldata hookData)
private
pure
returns (InboundHook memory h);Parameters
| Name | Type | Description |
|---|---|---|
| message | bytes | |
| hookData | bytes |
Returns
| Name | Type | Description |
|---|---|---|
| h | InboundHook |
_runExecutor
Forwards the attested InboundHook + attacker payload to the CCTPHookExecutor LENIENTLY. The
bundle is one memory arg so the deep 7-arg executeHook call fits the legacy-codegen stack limit.
function _runExecutor(InboundHook memory h, bytes calldata payload) private returns (bool ok);Parameters
| Name | Type | Description |
|---|---|---|
| h | InboundHook | |
| payload | bytes |
Returns
| Name | Type | Description |
|---|---|---|
| ok | bool |
_chainIdFromReference
Right-aligns an ERC-7930 chain-reference (<= 32 bytes, big-endian) into a uint256 chainId.
For eip-155 chains the reference IS the chainId. Reverts CCTPChainReferenceTooLong if > 32 bytes.
function _chainIdFromReference(bytes memory ref) private pure returns (uint256 chainId);Parameters
| Name | Type | Description |
|---|---|---|
| ref | bytes |
Returns
| Name | Type | Description |
|---|---|---|
| chainId | uint256 |