Building cctp-rs: A Production-Grade Rust SDK for Cross-Chain USDC Transfers

Building cctp-rs: A Production-Grade Rust SDK for Cross-Chain USDC Transfers

How we built an open-source library that's moved millions in USDC across blockchains by Joseph Livesey, Semiotic Engineering


At Semiotic, we often find ourselves building infrastructure that doesn't exist yet. When we needed reliable cross-chain USDC transfers for our treasury management system, we discovered there was no production-ready Rust SDK for Circle's Cross-Chain Transfer Protocol (CCTP). So we built one.

Today, we're excited to share cctp-rs, a type-safe, production-hardened Rust implementation of CCTP that powers millions of dollars in USDC transfers for our internal systems. With our v1.0.0 release, we're making this battle-tested infrastructure available to the broader Rust and crypto ecosystem.

The Problem: Bridging at Scale

Our internal Likwid service manages router assets across 15+ blockchain networks. It autonomously monitors routers, liquidates accumulated tokens to USDC via DEX aggregators, and consolidates funds to a treasury on Base. This happens 24/7 without human intervention.

The bridging piece was the missing link. We needed to:

  • Transfer USDC from Ethereum, Arbitrum, Optimism, Avalanche, and Polygon to Base
  • Handle the full CCTP lifecycle: burn → attestation → mint
  • Survive network failures, rate limits, and third-party relayer races
  • Maintain complete observability for debugging production issues
  • Support both CCTP v1 (legacy) and v2 (with sub-30-second settlements)

Existing TypeScript SDKs weren't suitable for our Rust-native infrastructure. So we built cctp-rs from the ground up.

Design Philosophy: Type Safety as a Feature

Rust's type system isn't just about memory safety—it's a tool for encoding domain knowledge into your code. We leveraged this extensively:

Compile-Time Chain Validation

// Chain support is checked at compile time
let domain_id = NamedChain::Arbitrum.cctp_domain_id()?;
let fee_bps = NamedChain::Arbitrum.fast_transfer_fee_bps()?;

// Unsupported chains fail gracefully with typed errors
let err = NamedChain::Fantom.cctp_domain_id();
assert!(matches!(err, Err(CctpError::UnsupportedChain(_))));

Version-Specific APIs That Can't Be Misused

CCTP v1 and v2 have fundamentally different attestation lookups. v1 queries by message hash; v2 queries by transaction hash. This is easy to confuse, and the wrong choice means failed mints.

We solved this with separate types:

// V1: Query by message hash
let attestation = Cctp::get_attestation(message_hash, ...).await?;

// V2: Query by transaction hash, returns canonical message
let (message, attestation) = CctpV2Bridge::get_attestation(tx_hash, ...).await?;

You literally cannot call the wrong method—the compiler won't let you.

The V2 Nonce Foot-Gun

Here's a subtle bug we caught: CCTP v2's MessageSent event emits a "template" message with zeros in the nonce field. Circle's attestation service fills in the actual nonce before signing. If you use the message from the logs for minting, it will fail.

Our v2 API handles this automatically:

// get_attestation() always returns the canonical message from Circle's API
let (canonical_message, attestation) = bridge.get_attestation(tx_hash, ...).await?;
// Use canonical_message for minting—it has the correct nonce

This design choice eliminated an entire class of production bugs.

Relayer-Aware Design for the Real World

CCTP v2 is permissionless. Once Circle attests a message, anyone can relay it. On mainnet, third-party relayers actively monitor burns and may complete your transfer before your application does. (Shout-out to Proof Fairy—they've relayed every single one of our v2 transfers so far!) Note: testnet doesn't have active relayers, so you'll need to self-relay there—useful for testing your full flow.

This isn't a bug; it's a feature of decentralized systems. But it means your code needs to handle races gracefully:

use cctp_rs::{CctpV2Bridge, MintResult};

match bridge.mint_if_needed(message, attestation, from).await? {
    MintResult::Minted(tx_hash) => {
        // We completed the transfer
        log::info!("Minted via our relayer: {tx_hash}");
    }
    MintResult::AlreadyRelayed => {
        // A third-party relayer completed it—that's fine!
        log::info!("Transfer completed by third-party relayer");
    }
}

The mint_if_needed() method checks the on-chain nonce status before attempting a mint, preventing wasted gas and failed transactions. In Likwid's production environment, this pattern handles relayer races cleanly: the bridge job runs every 3 hours, and sometimes third-party relayers beat us to it. The system just moves on.

Production Hardening: The Details That Matter

HTTP Timeouts and Retry Logic

// 30-second timeout prevents indefinite hangs
let client = Client::builder()
    .timeout(Duration::from_secs(30))
    .build()?;

We poll Circle's Iris API with configurable retry logic:

  • Rate limit handling (429 → sleep 5 minutes)
  • 404 responses treated as "pending, retry"
  • Configurable max attempts and poll intervals

OpenTelemetry Instrumentation

Every operation is traced for production debugging:

#[tracing::instrument(
    name = "cctp_rs.get_attestation",
    fields(
        tx_hash = %tx_hash,
        source_chain = %source_chain,
        otel.status_code = "OK"
    )
)]
async fn get_attestation(...) { ... }

When a bridge takes 19 minutes to complete (standard v1 attestation time), you want visibility into exactly where it is in the process.

Comprehensive Error Types

#[derive(Error, Debug)]
pub enum CctpError {
    UnsupportedChain(NamedChain),      // Type-safe, zero allocation
    AlreadyRelayed { original: String }, // Relayer-aware
    AttestationTimeout,                 // Clear timeout semantics
    AttestationFailed { reason: String },
    // ... comprehensive coverage
}

No string-based errors. No panics. Every failure mode has a typed representation.

The Numbers

  • 26+ supported chains across mainnet and testnet
  • v2.0.0 stable with semantic versioning
  • Millions of USDC bridged in production via Likwid

The test suite validates everything from domain ID mappings to URL construction to error handling edge cases. Integration tests requiring live networks aren't practical in CI (10-15 minute attestation times, gas costs, rate limits), so we validate via comprehensive unit tests and runnable examples.

How Likwid Uses cctp-rs

Our treasury consolidation flow:

  1. Extraction: Discover tokens accumulated in routers across 15+ chains
  2. Liquidation: Swap tokens to USDC via Odos aggregator when thresholds are met
  3. Bridging: Move USDC to Base via CCTP v2
  4. State tracking: PostgreSQL tracks the full lifecycle (burn → attestation → mint)

The bridge service processes chains concurrently:

let tasks: Vec<_> = supported_chains
    .into_iter()
    .map(|chain| tokio::spawn(process_chain_bridges(chain)))
    .collect();

Each chain has configurable thresholds—bridge when any router reaches its limit. The whole system runs autonomously on Cloud Run Jobs, powered by cctp-rs.

Get Started

[dependencies]
cctp-rs = "2.0.0"

use cctp_rs::{CctpV2Bridge, CctpError};
use alloy_chains::NamedChain;

let bridge = CctpV2Bridge::builder()
    .source_chain(NamedChain::Mainnet)
    .destination_chain(NamedChain::Base)
    .source_provider(eth_provider)
    .destination_provider(base_provider)
    .recipient(recipient)
    .build();

// Burn, get attestation, mint
let burn_tx = burn_usdc(&bridge, amount).await?;
let (message, attestation) = bridge.get_attestation(burn_tx, None, None).await?;
bridge.mint_if_needed(message, attestation, from).await?;

Check out the examples for complete working code, including fast transfers with sub-30-second settlement.

What's Next

We're continuing to evolve cctp-rs as Circle expands CCTP support:

  • New chain additions as Circle announces them
  • Performance optimizations for high-frequency use cases
  • Enhanced hook support for programmable transfers

The crate is Apache 2.0 licensed and we welcome contributions. If you're building cross-chain infrastructure in Rust, we'd love to hear how you're using it.


cctp-rs is open source at github.com/semiotic-ai/cctp-rs. Documentation is available at docs.rs/cctp-rs.

Semiotic AI builds financial infrastructure. Learn more at https://www.semiotic.ai.