Quantstamp reviewed Zircuit (Zircuit Finance - Circle CCTP Adapter) on 29 Apr 2026: 4 findings, 0 critical; 3 marked fixed, 1 acknowledged.
Report hosted on certificate.quantstamp.com.
Trustblock does not review code.
Statuses are the security firm’s own, from its report.
| Severity | Open | Acknowledged | Fixed |
|---|---|---|---|
| Critical | 0 | 0 | 0 |
| High | 0 | 0 | 0 |
| Medium | 0 | 0 | 0 |
| Low | 0 | 1 | 3 |
`CircleCCTPAdapter` maintains a bijection between chain identifiers and Circle CCTP domains through the `chainIdToConfig` and `domainToChainId` mappings, and signals every lifecycle change on a single event, `CCTPPeerSet(uint256 indexed chainId, uint32 indexed domain, address indexed adapter)`. Off-chain indexers, monitoring tooling, and any downstream subgraph rely on this event to mirror peer state. On additions the event carries the real domain and adapter values; on removals the current implementation emits `CCTPPeerSet(chainId, 0, address(0))` to encode "cleared". However, `0` is the canonical Circle CCTP v2 domain identifier for Ethereum mainnet — a legitimate value that may be registered as a peer at any time. Emitting `0` in the `indexed domain` topic as a removal sentinel is indistinguishable on-chain from an event describing a peer whose domain is Ethereum. Indexers that filter on `domain = 0` to track Ethereum-related peer changes will falsely match every `removePeer` emission, and indexers that reconstruct history by pairing set with remove will lose the original domain of the peer being removed because the topic overwrites it with a value that looks like an Ethereum registration. This could cause off-chain systems to misclassify peer removals as Ethereum peer registrations, corrupt historical peer state reconstructions, and complicate observability once Ethereum is legitimately registered. There is no on-chain fund impact, but the ambiguity is avoidable and undermines the otherwise clean event-based audit trail the contract provides.
`CircleCCTPAdapter` has no function to recover ERC20 tokens that land in the contract outside of the normal `receiveFunds` flow. In practice, USDC cannot get stuck via the protocol path — `messageTransmitter.receiveMessage()` and the subsequent transfer to the router are atomic, so any failure rolls back the mint entirely. However, if USDC or any other ERC20 is sent directly to this contract address by mistake (e.g. a user error, a failed airdrop, or dust from an unrelated interaction), it is permanently unrecoverable. Every other adapter in this package ( `OFTBridgeAdapter`, `ProtoCCTPGatewayAdapter`, `ArbitrumAdapterL1`, and `ArbitrumAdapterL2`) has a `RESCUE_ROLE`-gated recovery mechanism.
`CircleCCTPAdapter.receiveFunds()` authenticates an inbound CCTP v2 message by forwarding it to `messageTransmitter.receiveMessage()` and then hands it to `_parseReceivedMessage()`. The parser reads fields from the 228-byte `BurnMessageV2` body, `mintRecipient`, `amount`, `feeExecuted`, `messageSender`, and the trailing `hookData`, and compares each against state maintained by the adapter (the `domainToChainId` mapping, the stored peer adapter for the resolved source chain, and `address(this)`). These three body-level comparisons are the adapter's entire authorization model for inbound messages. The parsed `destAddress` and `extraData` are then passed to `router.handleReceivedFunds`, which gates the final destination through `Factory.isAuthorizedBridgeAddress()`. However, Circle's `MessageTransmitterV2.sendMessage()` is a permissionless entry point that produces valid, attestable messages whose body is entirely attacker-controlled bytes. The adapter never inspects the 148-byte message header, in particular the `sender` field (which for a genuine burn message equals the source-chain `TokenMessengerV2`) and the `recipient` field (which equals the destination-chain `TokenMessengerV2`). An attacker on a registered peer chain can therefore craft a message whose body is byte-compatible with `_parseReceivedMessage()` (correct `mintRecipient`, `messageSender` equal to the registered peer adapter, any chosen `amount`, any `feeExecuted`, any `destAddress` encoded in the hook data) have Circle attest it, and submit it via `receiveFunds` on the adapter. Circle dispatches the hook payload to an attacker-chosen no-op handler, so no USDC is minted to the adapter, yet the body-level checks all pass because the attacker wrote the bytes the parser expects. This could allow any USDC residing in the adapter — operational dust, accidental transfers, or balances introduced by a future refactor — to be routed to a vault of the attacker's choosing through `router.handleReceivedFunds()`. In the current integration, the destination is constrained to addresses that `Factory.isAuthorizedBridgeAddress()` has whitelisted, so the funds end up in a Zircuit-authorized vault rather than an attacker-controlled wallet; the attacker gains nothing and the worst case is a bounded shift of residual balance between authorized cluster vaults. The residual business impact is therefore informational, but the adapter has no defense-in-depth for future integrations that may permit non-cluster destinations or for scenarios in which the adapter accrues a non-trivial balance.
The report names a repository or files. Open the report to see its scope.
236 projects reviewed · 332 reports · latest Oct 2026
See Quantstamp’s auditsSomething wrong on this page? A missing report, a wrong date, a finding that was fixed since. Tell us and we check it against its source.
Tell us