Circle Gateway relayer filling — design (not yet implemented)
Status: design only. This document specifies how a relayer would fund a CCTP fill from a Circle Gateway balance. No contract code or config for this exists in the repo yet — it is written up here so it is ready to build. The destination-execution feature (shipped) is independent of this.
Motivation
Today a relayer must already hold USDC on the destination chain to fill an order. Circle Gateway
lets a relayer instead keep a single unified USDC balance (deposited once into Circle's
GatewayWallet) and mint USDC on demand on any supported chain in <1s, with Circle fronting the
liquidity. The goal: a relayer detects an OrderCreated, requests a Gateway mint, and fills the
order with the freshly-minted USDC in one transaction — no pre-positioned inventory per chain.
This is separate from CctpExecutor. The executor solves destination CCTP mint liveness for an
already-burned transfer by paying mintFee to whoever relays execute(message, attestation).
Gateway solves optimistic filler inventory by letting a relayer acquire destination USDC just in
time before calling fill(order). They can coexist: a relayer may fill via Gateway, while a later
executor relayer mints and settles the CCTP message if the user opted into mintFee > 0.
How Circle Gateway works (relevant facts)
- Two contracts (same address across chains):
GatewayWallet(deposit / withdraw / unified balance, on the source) andGatewayMinter(mint on the destination). - The relayer signs an off-chain EIP-712 burn intent (a
TransferSpec: source/destination domains, tokens,sourceDepositor,destinationRecipient,value,salt, optionaldestinationCaller, optionalhookData) and POSTs it to Circle's Gateway API (POST /v1/transfer). The API returns an operator-signed attestation. - Anyone holding the attestation calls
GatewayMinter.gatewayMint(bytes attestationPayload, bytes signature)on the destination; it verifies Circle's operator signature, mintsvalue − feeUSDC todestinationRecipient, and marks the transfer spec used (replay guard). The mint is instant (Circle settles the source burn asynchronously). gatewayMintis permissionless unlessdestinationCalleris set in the burn intent (then only that address may submit it). Gateway reuses CCTP domain IDs (ETH 0, OP 2, ARB 3, BASE 6, …).- Fees come out of the source balance: a small per-transfer fee (~0.005%) + a chain base fee, so
minted = value − fee. The relayer choosesvalueso thatminted ≥ payout.
Recommended design — fillViaGateway (CCTP/USDC only)
Add to CctpAdapter:
function fillViaGateway(Order calldata order, bytes calldata attestation, bytes calldata signature)
external nonReentrant whenNotPaused returns (bytes32 orderId)
{
address token; address recipient; uint256 payout; uint256 fee;
(orderId, token, recipient, payout, fee) = _prepareFill(order, msg.sender); // records filler = msg.sender
IGatewayMinter(config.chainConfig(block.chainid).gatewayMinter).gatewayMint(attestation, signature);
SafeTransferLib.safeTransferFrom(token, msg.sender, address(this), payout); // pull the just-minted USDC
_deliverWithHook(orderId, token, recipient, payout, order.hookData, order.callbackGasLimit);
emit OrderFilled(orderId, msg.sender, payout, fee, uint40(block.timestamp));
}
The relayer's burn intent sets destinationRecipient = themselves and destinationCaller = the adapter. They pre-approve the adapter for USDC once (or batch a selfPermit via multicall). The
flow reuses the existing _prepareFill + delivery (including destination executions) verbatim — the
only new step is the gatewayMint that tops the relayer up just-in-time.
Why this is front-run-safe (the key property)
The attestation appears in the mempool when the relayer submits. A front-runner who copies it and calls
fillViaGateway themselves achieves nothing:
gatewayMintmints to the relayer (thedestinationRecipientis baked into the signed attestation, not chosen by the caller), not the front-runner.- The adapter then pulls
payoutfrommsg.sender= the front-runner, who has no minted USDC and no allowance ⇒transferFromreverts ⇒ the whole transaction reverts, rolling back the mint.
So the relayer's Gateway balance is never consumed by anyone else, and no inventory is stranded. The
relayer should set destinationCaller = the adapter so the attestation can only be used via
fillViaGateway (this also stops a griefer from calling gatewayMint directly to involuntarily
mint to the relayer's wallet). No attestation parsing is required.
Pieces to add
- A hand-written
^0.8IGatewayMintermirror (faithful to Circle's deployed ABI), e.g.:interface IGatewayMinter { function gatewayMint(bytes calldata attestationPayload, bytes calldata signature) external; } - A
gatewayMinteraddress field onChainConfig/FastFillConfig(per chain;address(0)⇒ the chain has no Gateway ⇒fillViaGatewayrevertsUnsupportedChain). Optionally cross-checkIGatewayMinter(...).domain() == config.cctpDomainon use, mirroring the existing live cross-checks. - Relayer tooling (off-chain): deposit → build & sign the burn intent →
POST /v1/transfer→ submitfillViaGateway(order, attestation, signature).
Alternative considered — mint to the adapter + parse the attestation
Mint to the adapter (destinationRecipient = adapter), then have the adapter parse Circle's
attestation to recover the burn's signer and record it as the filler (so a front-runner crediting
themselves is impossible), refunding any excess. This needs no relayer approval, but requires a
hand-written GatewayMessageLib mirroring Circle's exact attestation byte layout (confirmed against
the deployed contract). Heavier and more fragile than the recommended design — kept as a fallback.
Testing note
Unlike the CCTP burn (which we drive for real on a fork) and Permit2 (real signatures), gatewayMint
requires Circle's operator-signed attestation, which cannot be forged in a fork test. Coverage will
therefore be: a mock GatewayMinter (mints a configured amount to the recipient encoded in a test
attestation) for the unit/integration lifecycle, plus an interface-shape fork check (call a view
like domain() on the real deployed GatewayMinter to confirm the address + ABI). A full
end-to-end run needs a testnet with the live Circle Gateway API.
