> ## Documentation Index
> Fetch the complete documentation index at: https://docs.defindex.io/llms.txt
> Use this file to discover all available pages before exploring further.

# USDC from Base with CCTP and Crossmint

> Move native USDC from Base or another EVM chain to a Crossmint Stellar smart wallet with Circle CCTP, then deposit it into a DeFindex vault.

## When to use this path

Your users hold USDC in Crossmint smart wallets on Base (or another EVM chain that Circle CCTP supports) and you want that USDC earning yield in a DeFindex vault. DeFindex vaults run on Stellar, so the USDC has to cross first.

Circle's Cross-Chain Transfer Protocol (CCTP) burns USDC on the source chain and mints the same USDC on Stellar. There is no wrapped token and no liquidity pool in the middle: what lands in the wallet is the same USDC that DeFindex USDC vaults accept.

| | CCTP (this page) | Sodax ([Crossmint Smart Wallets](https://docs.defindex.io/integration-guide/guides-and-tutorials/crossmint-smart-wallets.md)) |
| - | - | - |
| Mechanism | Burn on the source chain, Circle attestation, mint on Stellar | Intent filled by a solver through the Sodax hub |
| Asset received | Native USDC issued by Circle | USDC delivered by the solver |
| Fee | Standard Transfer is free; Fast Transfer charges a fee per route, read from Circle's API | Solver and bridge fees in the quote |
| Steps you run | Burn on the EVM chain, poll the attestation, call `mint_and_forward` on Stellar | Quote, submit, wait for the intent to be filled |
| Use it when | You want native USDC and no third-party liquidity | You need an asset or route CCTP does not cover |

The Crossmint setup is the same in both: one server key registered as `adminSigner` on an EVM smart wallet and on a Stellar smart wallet, and every transaction sent through Crossmint's REST API. If you have not set that up yet, start with the [reference repository](https://github.com/defindex-io/crossmint-defindex-guide) and the [Crossmint Smart Wallets guide](https://docs.defindex.io/integration-guide/guides-and-tutorials/crossmint-smart-wallets.md).

## The flow

```mermaid theme={null}
sequenceDiagram
    participant S as Your server
    participant CM as Crossmint API
    participant B as Base (TokenMessengerV2)
    participant I as Circle Iris API
    participant F as Stellar CctpForwarder
    participant W as Stellar smart wallet (C...)
    participant V as DeFindex vault

    S->>CM: approve USDC to TokenMessengerV2 (EVM wallet)
    S->>CM: depositForBurnWithHook, destination domain 27
    CM->>B: burn USDC
    S->>I: GET /v2/messages/6?transactionHash=...
    I-->>S: message + attestation (status complete)
    S->>F: mint_and_forward(message, attestation)
    F->>W: mint USDC and forward to the wallet
    S->>CM: contract-call deposit on the vault (Stellar wallet)
    CM->>V: deposit(amounts_desired, amounts_min, from, invest)
```

1. **Burn on Base.** The Crossmint EVM smart wallet approves USDC to Circle's `TokenMessengerV2` and calls `depositForBurnWithHook` with destination domain `27` (Stellar). `mintRecipient` and `destinationCaller` are the Stellar `CctpForwarder` contract, and the hook data carries the address of the user's Stellar smart wallet.
2. **Attestation.** Circle's attestation service (Iris) signs the burn. You poll it until the message is `complete`.
3. **Mint and forward on Stellar.** Anyone can call `mint_and_forward(message, attestation)` on `CctpForwarder`. In one Soroban invocation it mints the USDC to itself and transfers it to the address in the hook data.
4. **Deposit.** The Stellar smart wallet deposits the USDC into a DeFindex vault, the same way as any other deposit from a Crossmint wallet.

> [!CAUTION]
>
> On the burn, `mintRecipient` and `destinationCaller` must both be the `CctpForwarder` contract. Circle states that if either is wrong the funds are **permanently stuck and cannot be recovered**. Never put the user's wallet address in `mintRecipient`: it goes only in the hook data. See [CCTP on Stellar](https://developers.circle.com/cctp/references/stellar#use-cctpforwarder-for-stellar-recipients).

## Addresses and parameters

Every value below comes from Circle's documentation, linked next to it. Read them from Circle's pages again before going to mainnet: Circle's own instructions are to look addresses up rather than hardcode them.

### Mainnet

| Item | Value | Source |
| - | - | - |
| CCTP domain, Base | `6` | [Supported chains and domains](https://developers.circle.com/cctp/concepts/supported-chains-and-domains) |
| CCTP domain, Stellar | `27` | [Supported chains and domains](https://developers.circle.com/cctp/concepts/supported-chains-and-domains) |
| `TokenMessengerV2`, Base | `0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d` | [Contract addresses](https://developers.circle.com/cctp/references/contract-addresses) |
| USDC, Base | `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` | [USDC contract addresses](https://developers.circle.com/stablecoins/usdc-contract-addresses) |
| `CctpForwarder`, Stellar | `CBZL2IH7F6BIDAA3WBNXYKIXSATJGMSW7K5P5MJ6STX5RXN47TZJDF5T` | [CCTP Stellar contracts](https://developers.circle.com/cctp/references/stellar-contracts) |
| `TokenMessengerMinter`, Stellar | `CAE2G5Z77UP7GYPYGFOWFGW7C7J6I4YP2AFGSADRKQY62SYUFLPNFTXL` | [CCTP Stellar contracts](https://developers.circle.com/cctp/references/stellar-contracts) |
| `MessageTransmitter`, Stellar | `CACMENFFJPJMSDAJQLX4R7K3SFZIW2LJSE3R2UMLGSWHFHS353FVXAZV` | [CCTP Stellar contracts](https://developers.circle.com/cctp/references/stellar-contracts) |
| USDC, Stellar (asset) | `USDC-GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN` | [USDC contract addresses](https://developers.circle.com/stablecoins/usdc-contract-addresses) |
| USDC, Stellar (contract) | `CCW67TSZV3SSS2HXMBQ5JFGCKJNXKZM7UQUWUZPUTHXSTZLEO7SJMI75` | Stellar Asset Contract of the asset above, see [Contract Deployments](https://docs.defindex.io/contract-deployments.md) |
| Attestation API | `https://iris-api.circle.com` | [Get messages](https://developers.circle.com/api-reference/cctp/all/get-messages-v2) |

### Testnet

| Item | Value | Source |
| - | - | - |
| `TokenMessengerV2`, Base Sepolia | `0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA` | [Contract addresses](https://developers.circle.com/cctp/references/contract-addresses) |
| USDC, Base Sepolia | `0x036CbD53842c5426634e7929541eC2318f3dCF7e` | [USDC contract addresses](https://developers.circle.com/stablecoins/usdc-contract-addresses) |
| `CctpForwarder`, Stellar testnet | `CA66Q2WFBND6V4UEB7RD4SAXSVIWMD6RA4X3U32ELVFGXV5PJK4T4VSZ` | [CCTP Stellar contracts](https://developers.circle.com/cctp/references/stellar-contracts) |
| Attestation API | `https://iris-api-sandbox.circle.com` | [Get messages](https://developers.circle.com/api-reference/cctp/all/get-messages-v2) |

> [!NOTE]
>
> Crossmint `staging` runs on `base-sepolia` and Stellar testnet. The reference repository's Sodax example uses a Crossmint test token on Base Sepolia; CCTP burns only Circle's testnet USDC listed above.

## Step by step

The snippets reuse the `CrossmintRestClient` and helpers from [`defindex-io/crossmint-defindex-guide`](https://github.com/defindex-io/crossmint-defindex-guide): `getOrCreateEvmWallet`, `getStellarWalletAddress`, `sendTransactionAndGetHash`, `getStellarDepositBalance` and `depositToDefindexVault`. They use `ethers` v6 for EVM encoding and `@stellar/stellar-sdk` for Stellar.

### 1. Configuration

```typescript theme={null}
// Circle CCTP, mainnet. Sources in the table above.
export const cctp = {
  baseDomain: 6,
  stellarDomain: 27,
  baseTokenMessengerV2: "0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d",
  baseUsdc: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
  stellarCctpForwarder: "CBZL2IH7F6BIDAA3WBNXYKIXSATJGMSW7K5P5MJ6STX5RXN47TZJDF5T",
  irisApi: "https://iris-api.circle.com",
};

export const stellar = {
  rpcUrl: process.env.STELLAR_SOROBAN_RPC_URL ?? "https://mainnet.sorobanrpc.com",
  networkPassphrase: "Public Global Stellar Network ; September 2015",
  // Funded classic account (G...) that pays the fee of mint_and_forward. See step 5.
  relayerSecret: process.env.STELLAR_RELAYER_SECRET!,
};
```

### 2. Get both wallets

The EVM wallet holds the USDC to move. The Stellar wallet is a Soroban smart contract wallet, so its address is a `C...` contract address. That address is the final recipient of the USDC.

```typescript theme={null}
import { CrossmintRestClient } from "./shared/crossmint-rest.js";
import { config } from "./shared/config.js";

const rest = new CrossmintRestClient(config.apiKey, config.baseUrl);

const { address: evmWallet } = await rest.getOrCreateEvmWallet();   // 0x...
const stellarWallet = await rest.getStellarWalletAddress();          // C...
```

### 3. Build the hook data

These two helpers are Circle's, from [CCTP on Stellar](https://developers.circle.com/cctp/references/stellar#building-forwarder-hook-data-example). The forwarder is encoded as the raw 32 bytes of its contract ID. The recipient goes in the hook data as its full strkey text.

```typescript theme={null}
import { StrKey } from "@stellar/stellar-sdk";

function contractStrkeyToBytes32(strkey: string): `0x${string}` {
  if (!StrKey.isValidContract(strkey)) {
    throw new Error(`Invalid contract strkey: ${strkey}`);
  }
  return `0x${Buffer.from(StrKey.decodeContract(strkey)).toString("hex")}`;
}

// bytes 0-23 zero, 24-27 version (0), 28-31 recipient length, 32+ recipient strkey (UTF-8)
function buildCctpForwarderHookData(forwardRecipientStrkey: string): `0x${string}` {
  const isValid =
    StrKey.isValidEd25519PublicKey(forwardRecipientStrkey) ||
    StrKey.isValidContract(forwardRecipientStrkey) ||
    StrKey.isValidMed25519PublicKey(forwardRecipientStrkey);
  if (!isValid) throw new Error(`Invalid forward recipient: ${forwardRecipientStrkey}`);

  const recipientBytes = Buffer.from(forwardRecipientStrkey, "utf8");
  const hookData = Buffer.alloc(32 + recipientBytes.length);
  hookData.writeUInt32BE(0, 24);
  hookData.writeUInt32BE(recipientBytes.length, 28);
  recipientBytes.copy(hookData, 32);
  return `0x${hookData.toString("hex")}`;
}

const forwarderBytes32 = contractStrkeyToBytes32(cctp.stellarCctpForwarder);
const hookData = buildCctpForwarderHookData(stellarWallet); // the Crossmint C... address
```

### 4. Approve and burn on Base through Crossmint

Both calls are ordinary EVM transactions from the Crossmint smart wallet: `POST /transactions`, sign the approval message with the `adminSigner` key, `POST /approvals`, poll for `onChain.txId`. `sendTransactionAndGetHash` in the reference repository does all of that.

The parameters of `depositForBurnWithHook` are from Circle's [contract interfaces](https://developers.circle.com/cctp/references/contract-interfaces#depositforburnwithhook). The amount is in Base USDC units, 6 decimals.

```typescript theme={null}
import { ethers } from "ethers";

const erc20 = new ethers.Interface([
  "function approve(address spender, uint256 amount) returns (bool)",
]);
const tokenMessenger = new ethers.Interface([
  "function depositForBurnWithHook(uint256 amount, uint32 destinationDomain, bytes32 mintRecipient, address burnToken, bytes32 destinationCaller, uint256 maxFee, uint32 minFinalityThreshold, bytes hookData)",
]);

const amount = ethers.parseUnits("100", 6); // 100 USDC on Base

// 4a. Allow TokenMessengerV2 to pull the USDC
await rest.sendTransactionAndGetHash(
  evmWallet,
  {
    to: cctp.baseUsdc,
    data: erc20.encodeFunctionData("approve", [cctp.baseTokenMessengerV2, amount]),
  },
  "base"
);

// 4b. Burn, Standard Transfer: maxFee 0, minFinalityThreshold 2000
const burnTxHash = await rest.sendTransactionAndGetHash(
  evmWallet,
  {
    to: cctp.baseTokenMessengerV2,
    data: tokenMessenger.encodeFunctionData("depositForBurnWithHook", [
      amount,
      cctp.stellarDomain,  // 27
      forwarderBytes32,    // mintRecipient = CctpForwarder
      cctp.baseUsdc,       // burnToken
      forwarderBytes32,    // destinationCaller = CctpForwarder
      0n,                  // maxFee
      2000,                // Standard Transfer
      hookData,
    ]),
  },
  "base"
);
```

> [!TIP]
>
> **Fast Transfer.** Base supports Fast Transfer as a source chain (`minFinalityThreshold` `1000`), which shortens the wait from the Base finality window to seconds for a fee. Read the current fee for the route from `GET /v2/burn/USDC/fees/6/27` on the Iris API (`minimumFee` in basis points, entry with `finalityThreshold` `1000`) and set `maxFee` from it with a small buffer. Circle asks integrators not to hardcode the fee. See [CCTP fees](https://developers.circle.com/cctp/concepts/fees).

Gas on Base: Crossmint sponsors gas for wallet transactions by default and bills it to your project in production. See [Crossmint gas sponsorship](https://docs.crossmint.com/wallets/guides/gas-sponsorship).

### 5. Wait for the attestation and mint on Stellar

Poll Circle's [`GET /v2/messages/{sourceDomainId}`](https://developers.circle.com/api-reference/cctp/all/get-messages-v2) with the burn transaction hash until `status` is `complete`. The source domain is Base (`6`), not Stellar.

```typescript theme={null}
interface AttestationMessage {
  message: string;     // 0x-prefixed hex
  attestation: string; // 0x-prefixed hex
  status: string;
}

async function waitForAttestation(burnTxHash: string): Promise<AttestationMessage> {
  const url = `${cctp.irisApi}/v2/messages/${cctp.baseDomain}?transactionHash=${burnTxHash}`;
  while (true) {
    const res = await fetch(url);
    if (res.ok) {
      const data = (await res.json()) as { messages?: AttestationMessage[] };
      const msg = data.messages?.[0];
      if (msg?.status === "complete") return msg;
    } else if (res.status !== 404) {
      console.warn(`Iris ${res.status}: ${await res.text()}`);
    }
    await new Promise((r) => setTimeout(r, 5000)); // Iris rate limit is 40 req/s
  }
}

const attestation = await waitForAttestation(burnTxHash);
```

Circle's Forwarding Service, which submits the destination transaction for you, does not support Stellar as a destination. Your backend submits `mint_and_forward` itself. `CctpForwarder` is publicly callable, so any funded Stellar account can pay the fee: the minted USDC goes to the address in the hook data, not to the caller. This is Circle's `submitSorobanTx` pattern from the [Stellar quickstart](https://developers.circle.com/cctp/quickstarts/transfer-usdc-stellar-arc), signed with a relayer account your server controls.

```typescript theme={null}
import { Contract, Keypair, TransactionBuilder, rpc, xdr } from "@stellar/stellar-sdk";

async function mintAndForward(att: AttestationMessage): Promise<string> {
  const server = new rpc.Server(stellar.rpcUrl);
  const relayer = Keypair.fromSecret(stellar.relayerSecret);
  const account = await server.getAccount(relayer.publicKey());

  const messageBytes = Buffer.from(att.message.replace("0x", ""), "hex");
  const attestationBytes = Buffer.from(att.attestation.replace("0x", ""), "hex");

  const tx = new TransactionBuilder(account, {
    fee: "10000000",
    networkPassphrase: stellar.networkPassphrase,
  })
    .addOperation(
      new Contract(cctp.stellarCctpForwarder).call(
        "mint_and_forward",
        xdr.ScVal.scvBytes(messageBytes),
        xdr.ScVal.scvBytes(attestationBytes)
      )
    )
    .setTimeout(120)
    .build();

  const sim = await server.simulateTransaction(tx);
  if (rpc.Api.isSimulationError(sim)) throw new Error(`Simulation failed: ${sim.error}`);

  const prepared = rpc.assembleTransaction(tx, sim).build();
  prepared.sign(relayer);

  const sent = await server.sendTransaction(prepared);
  if (sent.status === "ERROR") throw new Error(`Send failed: ${JSON.stringify(sent)}`);

  let result = await server.getTransaction(sent.hash);
  while (result.status === "NOT_FOUND") {
    await new Promise((r) => setTimeout(r, 2000));
    result = await server.getTransaction(sent.hash);
  }
  if (result.status !== "SUCCESS") throw new Error(`mint_and_forward failed: ${sent.hash}`);
  return sent.hash;
}

const mintTxHash = await mintAndForward(attestation);
```

> [!NOTE]
>
> To keep a single signing path, you could also send `mint_and_forward` as a Crossmint `contract-call` from the Stellar smart wallet. Crossmint's [custom Stellar transactions](https://docs.crossmint.com/wallets/guides/send-transaction-stellar) guide passes `args` as a JSON object and does not document how to pass Soroban `Bytes`, so this page uses a plain relayer account. Confirm the encoding with Crossmint before taking that route.

### 6. Deposit into the vault

The wallet now holds USDC with 7 decimals. Read what actually arrived and deposit it.

```typescript theme={null}
const VAULT = process.env.DEFINDEX_VAULT_ADDRESS!; // a USDC vault

const balance = await getStellarDepositBalance(stellarWallet); // USDC, 7 decimals
if (balance === 0n) throw new Error("USDC not in the wallet yet");

const depositTxHash = await depositToDefindexVault(rest, stellarWallet, VAULT, balance);
```

`depositToDefindexVault` sends a Crossmint `contract-call` to the vault's `deposit` method with `amounts_desired`, `amounts_min` (0.5% slippage), `from` set to the smart wallet, and `invest: true`. The parameters mean the same as in [Deposit](https://docs.defindex.io/integration-guide/vault-operations/deposit.md) and in [Vault Methods](https://docs.defindex.io/advanced-documentation/direct-contract-calls/vault-methods.md#deposit). The DeFindex API's deposit endpoint returns an unsigned transaction for a classic account to sign, while a Crossmint smart wallet signs through Crossmint, which is why the reference repository calls the vault method directly. Use the API for everything that only reads, such as the position: `GET /vault/{address}/balance` ([Get balance](https://docs.defindex.io/integration-guide/vault-operations/get-balance.md)).

> [!WARNING]
>
> `invest: true` on a vault that has never invested leaves the deposit idle until the manager runs one rebalance. See [Deposit with `invest: true` stays idle](https://docs.defindex.io/integration-guide/troubleshooting#deposit-with-invest-true-stays-idle).

## Stellar specifics

**Precision.** USDC has 6 decimals on Base and every other CCTP chain, and 7 on Stellar. The CCTP message `amount` is always in 6-decimal units, and the mint on Stellar multiplies it by 10: `100000000` (100 USDC) burned on Base becomes `1000000000` on Stellar. Use 7 decimals for every amount you pass to the vault. In the other direction, a burn from Stellar ignores the seventh decimal and leaves it in the wallet. Source: [USDC precision for CCTP and Stellar](https://developers.circle.com/cctp/references/stellar#usdc-precision-for-cctp-and-stellar).

**Address encoding.** CCTP messages carry 32 bytes per address with no type prefix, so the protocol cannot tell an account (`G...`) from a contract (`C...`) and assumes `mintRecipient` is a contract. That is why the mint goes to `CctpForwarder` and the real recipient travels as a strkey in the hook data. Source: [Stellar address types](https://developers.circle.com/cctp/references/stellar#stellar-address-types).

**Contract wallets need no trustline.** A Crossmint Stellar wallet is a contract (`C...`) and can receive USDC directly. A classic `G...` or `M...` recipient needs a USDC trustline first, or the forward fails. Source: [Transfer USDC to and from Stellar](https://developers.circle.com/cctp/quickstarts/transfer-usdc-stellar-arc).

**Atomic and non-custodial.** `mint_and_forward` validates the message, mints to the forwarder and pays the recipient in one invocation. Any failure reverts the whole call, and Circle does not hold the funds in between. Source: [How it works](https://developers.circle.com/cctp/references/stellar#how-it-works).

**Fast Transfer and forwarding.** Fast Transfer is not applicable when **Stellar is the source**, because Stellar's standard attestation already takes seconds. With Base as the source, Fast Transfer is available for a fee. Circle's Forwarding Service does not cover Stellar as a destination, so you always submit `mint_and_forward` yourself. Source: [Supported chains and domains](https://developers.circle.com/cctp/concepts/supported-chains-and-domains).

**Timing.** Most of the wait is on the source side. A Standard Transfer from Base waits for Ethereum finality of the Base batch, which Circle lists at roughly 15 to 19 minutes; a Fast Transfer from Base attests in seconds. The Stellar side is one Soroban transaction. Source: [Finality and block confirmations](https://developers.circle.com/cctp/concepts/finality-and-block-confirmations).

**Null fields in Iris.** For messages that involve Stellar, Iris returns the address fields of `decodedMessage` as `null`, because it cannot tell an account from a contract. Read the raw `message` hex if you need them. `amount` and `feeExecuted` are still decoded. Source: [Null address fields](https://developers.circle.com/cctp/references/stellar#null-address-fields-in-api-responses).

**Balances are not on Horizon.** A contract wallet's USDC balance lives in the token contract. Read it by simulating `balance(C...)` on the USDC contract over Soroban RPC, as `getStellarDepositBalance` does.

## Troubleshooting

| Symptom | Likely cause | What to do |
| - | - | - |
| Iris keeps returning 404 or `pending_confirmations` | Standard Transfer still waiting for Base finality, or wrong source domain in the URL | Wait up to the finality window; the path is `/v2/messages/6` for Base, not `/27` |
| Iris answers 429 | More than 40 requests per second | Poll every few seconds; Circle blocks the client for a few minutes after a 429 |
| `mint_and_forward` simulation fails | Message already received, wrong network (mainnet message on testnet forwarder or the reverse), or malformed hook data | Check the burn's network matches the forwarder's, and rebuild the hook data with the helper above |
| Burn succeeded, funds never arrive | `mintRecipient` or `destinationCaller` was not `CctpForwarder` | Unrecoverable per Circle. Validate both before every burn, and test the full path on testnet first |
| Wallet balance reads `0` after the mint | Reading Horizon, or reading the wrong token contract | Simulate `balance` on the USDC contract `CCW67...MI75` with the wallet's `C...` address |
| Vault deposit fails with an amount error | Amount passed with 6 decimals | Multiply by 10, or read the balance as in step 6 |
| Received less than burned | Fast Transfer fee deducted at mint | Expected; check `feeExecuted` in the Iris response, or use a Standard Transfer |
| `approve` or burn stuck in `awaiting-approval` | Approval message signed incorrectly | EVM messages are hex bytes: sign `ethers.getBytes(message)`. See the gotchas in the [reference repository](https://github.com/defindex-io/crossmint-defindex-guide/blob/main/docs/08-gotchas.md) |

## Resources

* [CCTP on Stellar](https://developers.circle.com/cctp/references/stellar), Circle
* [CCTP Stellar contracts and interfaces](https://developers.circle.com/cctp/references/stellar-contracts), Circle
* [Transfer USDC to and from Stellar](https://developers.circle.com/cctp/quickstarts/transfer-usdc-stellar-arc), Circle quickstart
* [Custom EVM transactions](https://docs.crossmint.com/wallets/guides/send-transaction-evm) and [custom Stellar transactions](https://docs.crossmint.com/wallets/guides/send-transaction-stellar), Crossmint
* [`defindex-io/crossmint-defindex-guide`](https://github.com/defindex-io/crossmint-defindex-guide), the reference code for the Crossmint wallets and the vault calls


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.