Skip to main content

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. 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 and the Crossmint Smart Wallets guide.

The flow

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

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

Testnet

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: getOrCreateEvmWallet, getStellarWalletAddress, sendTransactionAndGetHash, getStellarDepositBalance and depositToDefindexVault. They use ethers v6 for EVM encoding and @stellar/stellar-sdk for Stellar.

1. Configuration

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.

3. Build the hook data

These two helpers are Circle’s, from CCTP on Stellar. 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.

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. The amount is in Base USDC units, 6 decimals.
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.
Gas on Base: Crossmint sponsors gas for wallet transactions by default and bills it to your project in production. See Crossmint gas sponsorship.

5. Wait for the attestation and mint on Stellar

Poll Circle’s GET /v2/messages/{sourceDomainId} with the burn transaction hash until status is complete. The source domain is Base (6), not Stellar.
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, signed with a relayer account your server controls.
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 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.
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 and in Vault Methods. 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).
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.

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

Resources