Skip to content
Lattice

CCTPBridgeAdapterLib

Git Source

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

NameTypeDescription
$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

NameTypeDescription
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

NameTypeDescription
<none>address

messageTransmitter

function messageTransmitter() internal view returns (address);

Returns

NameTypeDescription
<none>address

usdc

function usdc() internal view returns (address);

Returns

NameTypeDescription
<none>address

getDomain

function getDomain(uint256 chainId) internal view returns (uint32);

Parameters

NameTypeDescription
chainIduint256

Returns

NameTypeDescription
<none>uint32

domainOwner

function domainOwner(uint32 domain) internal view returns (uint256);

Parameters

NameTypeDescription
domainuint32

Returns

NameTypeDescription
<none>uint256

isChainRegistered

function isChainRegistered(uint256 chainId) internal view returns (bool);

Parameters

NameTypeDescription
chainIduint256

Returns

NameTypeDescription
<none>bool

getDomainConfig

function getDomainConfig(uint32 domain)
        internal
        view
        returns (uint256 maxFee, uint32 minFinalityThreshold, bytes32 destinationCaller);

Parameters

NameTypeDescription
domainuint32

Returns

NameTypeDescription
maxFeeuint256
minFinalityThresholduint32
destinationCallerbytes32

hookExecutor

function hookExecutor() internal view returns (address);

Returns

NameTypeDescription
<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

NameTypeDescription
chainIduint256
domainuint32

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

NameTypeDescription
domainuint32
maxFeeuint256
minFinalityThresholduint32
destinationCallerbytes32

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

NameTypeDescription
amountuint256The USDC amount to burn (source of funds is the caller, NOT the Diamond balance).
recipientbytesThe 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

NameTypeDescription
amountuint256The USDC amount to burn (source of funds is the caller, NOT the Diamond balance).
recipientbytesThe full ERC-7930 interoperable recipient (chain reference + destination address).
hookDatabytesThe 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

NameTypeDescription
amountuint256
recipientbytes
hookDatabytes

_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

NameTypeDescription
$CCTPBridgeAdapterStorage
recipientbytes

Returns

NameTypeDescription
destChainIduint256
domainuint32
mintRecipientbytes32

_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

NameTypeDescription
$CCTPBridgeAdapterStorage
amountuint256
domainuint32
mintRecipientbytes32
hookDatabytes

_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

NameTypeDescription
cCctpBurnCall

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

NameTypeDescription
messagebytesThe CCTP message bytes emitted by the source-chain burn.
attestationbytesThe 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:

  1. pure validation: length, header/body version, header recipient == this TokenMessenger, and a well-formed Lattice envelope (length >= 24 && leading magic — never an abi.decode);
  2. mint via MessageTransmitterV2.receiveMessage (revert CCTPRelayFailed on false);
  3. 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 reports false → 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);
  4. emit HookExecuted + RelayedMessage.
function relayMessageWithHook(bytes calldata message, bytes calldata attestation) internal;

Parameters

NameTypeDescription
messagebytesThe CCTP v2 BurnMessageV2 bytes (must carry a Lattice hook envelope).
attestationbytesThe 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

NameTypeDescription
messagebytes
tokenMessenger_address

Returns

NameTypeDescription
<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

NameTypeDescription
hookDatabytes

Returns

NameTypeDescription
<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

NameTypeDescription
messagebytes
hookDatabytes

_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

NameTypeDescription
messagebytes
hookDatabytes

Returns

NameTypeDescription
hInboundHook

_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

NameTypeDescription
hInboundHook
payloadbytes

Returns

NameTypeDescription
okbool

_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

NameTypeDescription
refbytes

Returns

NameTypeDescription
chainIduint256