Overview

Sending USDC, or other ERC-20 tokens, is one of the most common actions taken by wallet users on Ethereum-based chains. This guide will walk you through how to send USDC using Dynamic’s embedded wallets.

Step 1: Get the USDC Contract Address

To send USDC, you’ll need the contract address for USDC. The address is different for each network, so make sure you get the correct address for the network you’re targeting. You can go to Circle’s website to look up the USDC address on both main networks and test networks.

Step 2: Format the Transaction Data

USDC, and other ERC-20 tokens, are smart contracts. In order to send these tokens, your transaction needs to call the transfer function on the contract, which takes in two parameters:
  • to: The address of the recipient
  • value: The amount of tokens to send
When formatting the transaction data, you must define the expected interface of the function you’re calling by providing an ABI. You can use helper packages like viem to provide the ABI and encode the function parameters. Additionally, each ERC-20 token defines a decimals value, which is the number of decimal places for the token. For USDC, the decimals value is 6. First, ensure you have the required dependencies installed for your platform:
npm install viem / yarn add viem / pnpm add viem
Then, build the transaction data.
import { encodeFunctionData, erc20Abi, parseUnits } from 'viem';

const recipientAddress = '0x...';
const amountToSend = "1"; // Sender wants to send 1 USDC

const encodedData = encodeFunctionData({
  abi: erc20Abi,
  functionName: 'transfer',
  args: [recipientAddress, parseUnits(amountToSend, 6)]
});

Step 3: Send the Transaction

You can send the transaction using Dynamic’s wallet APIs. Below are examples for React, React Native, and Swift.
import { useState } from "react";
import { useDynamicContext } from "@dynamic-labs/sdk-react-core";
import { isEthereumWallet } from "@dynamic-labs/ethereum";
import { parseUnits, erc20Abi } from 'viem';

const SendUSDCComponent = () => {
  const { primaryWallet } = useDynamicContext();
  const [txHash, setTxHash] = useState("");
  const [isLoading, setIsLoading] = useState(false);

  const sendUSDC = async (recipientAddress: string, amount: string) => {
    if (!primaryWallet || !isEthereumWallet(primaryWallet)) {
      throw new Error("Wallet not connected or not EVM compatible");
    }

    setIsLoading(true);

    try {
      const walletClient = await primaryWallet.getWalletClient();
      const publicClient = await primaryWallet.getPublicClient();

      // USDC contract address (replace with your network's USDC address)
      const usdcAddress = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; // Ethereum mainnet

      // Convert amount to USDC units (6 decimals)
      const amountInUnits = parseUnits(amount, 6);

      // Use writeContract for ERC-20 transfers
      const hash = await walletClient.writeContract({
        address: usdcAddress as `0x${string}`,
        abi: erc20Abi,
        functionName: 'transfer',
        args: [recipientAddress as `0x${string}`, amountInUnits],
      });

      setTxHash(hash);

      // Wait for transaction receipt
      const receipt = await publicClient.waitForTransactionReceipt({ hash });
      console.log("USDC transfer successful:", receipt);

    } catch (error) {
      console.error("Failed to send USDC:", error);
      throw error;
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <div>
      <button 
        onClick={() => sendUSDC("0x1234567890123456789012345678901234567890", "1")} // 1 USDC
        disabled={isLoading}
      >
        {isLoading ? "Sending..." : "Send 1 USDC"}
      </button>
      {txHash && (
        <div>
          <p>Transaction Hash: {txHash}</p>
          <a href={`https://etherscan.io/tx/${txHash}`} target="_blank" rel="noopener">
            View on Etherscan
          </a>
        </div>
      )}
    </div>
  );
};

Important Considerations

Gas Fees

  • EVM Networks: Gas fees are paid in the native token (ETH, MATIC, etc.)
  • Gasless Transactions: Some networks support gasless USDC transfers using account abstraction in React and React Native.

USDC Decimals

  • EVM USDC: 6 decimal places
  • Always use parseUnits(amount, 6) when converting amounts

Error Handling

Always implement proper error handling for failed transactions:
try {
  await sendUSDC(recipient, amount);
  console.log("USDC sent successfully");
} catch (error) {
  if (error.message.includes("insufficient funds")) {
    console.error("Insufficient USDC balance");
  } else if (error.message.includes("user rejected")) {
    console.error("User rejected the transaction");
  } else {
    console.error("Transaction failed:", error);
  }
}

Best Practices

  1. Always validate addresses before sending transactions
  2. Check USDC balance before attempting transfers
  3. Implement proper error handling and user feedback
  4. Test on testnets before deploying to mainnet
  5. Consider gas fees when calculating transfer amounts
  6. Use transaction simulation to show users exactly what will happen