- name
- usdc-integration
- description
- Use when integrating USDC into smart contracts, handling stablecoin transfers, approvals, or checking balances. Covers the 6-decimal rule, production contract addresses on all major chains, native vs bridged USDC variants, FiatTokenV2_2 proxy architecture, permit (EIP-2612), blocklist behavior, and safe integration patterns.
# USDC Integration for Smart Contracts
## The 6-Decimal Rule
USDC uses 6 decimals on every chain. Not 18. This is the single most common integration bug.
```solidity
// 1 USDC = 1_000_000 (1e6)
uint256 constant USDC_UNIT = 1e6;
// $100 USDC
uint256 amount = 100 * 1e6; // 100_000_000
// $0.01 USDC (one cent)
uint256 oneCent = 1e4; // 10_000
// WRONG — this is 1 trillion USDC ($1,000,000,000,000)
uint256 catastrophic = 1e18;
```
### Decimal Conversion
When protocols mix USDC (6 decimals) with 18-decimal tokens (WETH, DAI, most ERC-20s), explicit scaling is required:
```solidity
uint256 constant SCALE_FACTOR = 1e12; // 18 - 6 = 12
// Scale 6 → 18 (lossless)
uint256 wad = usdcAmount * SCALE_FACTOR;
// Scale 18 → 6 (LOSES up to 1e12 - 1 wei of precision)
uint256 usdc = wadAmount / SCALE_FACTOR;
```
Never scale in a single arithmetic expression without isolating the conversion. Multiply before dividing to preserve precision:
```solidity
// BAD — precision loss compounds
uint256 result = (usdcAmount * price) / 1e18;
// BETTER — scale USDC to 18 decimals first, then divide
uint256 result = (usdcAmount * SCALE_FACTOR * price) / 1e18;
// BEST — use a helper that makes intent explicit
uint256 result = _toWad(usdcAmount) * price / 1e18;
```
## Production Contract Addresses
### Mainnet — Native (Circle-issued) USDC
These are the canonical addresses issued directly by Circle via CCTP. All are FiatTokenV2_2 proxies.
| Chain | Address | Chain ID |
|-------|---------|----------|
| Ethereum | `0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48` | 1 |
| Base | `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` | 8453 |
| Arbitrum One | `0xaf88d065e77c8cC2239327C5EDb3A432268e5831` | 42161 |
| Optimism | `0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85` | 10 |
| Polygon PoS | `0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359` | 137 |
| Avalanche C-Chain | `0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E` | 43114 |
| Solana | `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v` | — |
### Testnet Addresses
| Chain | Address |
|-------|---------|
| Ethereum Sepolia | `0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238` |
| Base Sepolia | `0x036CbD53842c5426634e7929541eC2318f3dCF7e` |
| Arbitrum Sepolia | `0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d` |
| Optimism Sepolia | `0x5fd84259d66Cd46123540766Be93DFE6D43130D7` |
| Polygon Amoy | `0x41E94Eb019C0762f9Bfcf9Fb1E58725BfB0e7582` |
| Avalanche Fuji | `0x5425890298aed601595a70AB815c96711a31Bc65` |
Testnet faucet: https://faucet.circle.com
### Verifying Addresses Onchain
Never hardcode addresses from documentation alone. Verify with `cast`:
```bash
# Confirm USDC contract exists and is a proxy
cast code 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 --rpc-url mainnet | head -c 40
# Check decimals
cast call 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 "decimals()(uint8)" --rpc-url mainnet
# → 6
# Check symbol
cast call 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 "symbol()(string)" --rpc-url mainnet
# → "USDC"
```
## Native USDC vs Bridged Variants
Always use native Circle-issued USDC. Bridged variants are deprecated and lack features like permit, blocklist enforcement, and CCTP support.
### Deprecated Bridged Tokens — DO NOT USE
| Token | Chain | Address | Status |
|-------|-------|---------|--------|
| USDbC | Base | `0xd9aAEc86B65D86f6A7B5B1b0c42FFA531710b6Ca` | Deprecated |
| USDC.e | Arbitrum | `0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8` | Deprecated |
| USDC.e | Avalanche | `0xA7D7079b0FEaD91F3e65f86E8915Cb59c1a4C664` | Deprecated |
| USDC.e | Polygon | `0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174` | Deprecated |
| USDC.e | Optimism | `0x7F5c764cBc14f9669B88837ca1490cCa17c31607` | Deprecated |
### Detecting Native vs Bridged at Runtime
```solidity
/// @notice Validates that a USDC address is the native Circle-issued token
/// @dev Checks for EIP-2612 permit support as a heuristic — bridged variants lack it
function _validateNativeUSDC(address token) internal view {
// Native USDC supports EIP-2612 permit via DOMAIN_SEPARATOR
(bool success,) = token.staticcall(abi.encodeWithSignature("DOMAIN_SEPARATOR()"));
if (!success) revert NotNativeUSDC(token);
}
```
For deploy-time validation, pass the expected chain-specific address as a constructor argument and verify `decimals() == 6` plus `symbol` matches.
## FiatTokenV2_2 Architecture
USDC is not a simple ERC-20. It is a proxied, upgradeable, regulated stablecoin.
### Proxy Structure
```
FiatTokenProxy (storage + delegatecall)
└── FiatTokenV2_2 (implementation logic)
├── ERC-20 (transfer, approve, transferFrom, balanceOf, allowance)
├── EIP-2612 (permit — gasless approvals via signature)
├── EIP-3009 (transferWithAuthorization, receiveWithAuthorization)
├── Blocklist (Circle can block specific addresses)
├── Pause (Circle can halt all transfers globally)
└── Upgrade (Circle can swap the implementation)
```
### Key Administrative Roles
| Role | Capability |
|------|-----------|
| Admin | Upgrade implementation, change admin |
| Master Minter | Configure minters, set minting allowances |
| Blocklister | Add/remove addresses from blocklist |
| Pauser | Pause and unpause all transfers |
| Rescuer | Recover tokens accidentally sent to the USDC contract |
### Implications for Protocol Design
1. **Blocklist**: Any address can be blocklisted at any time. If a blocklisted address holds a position in your protocol, `transfer` and `transferFrom` to/from that address will revert.
2. **Pause**: All USDC transfers can be halted globally. Liquidation mechanisms that depend on USDC transfers will fail during a pause.
3. **Upgrade**: The implementation can change. Interface compatibility is maintained, but new behaviors (additional checks, storage changes) can be introduced.
## Safe Integration Patterns
### Basic Deposit/Withdraw
```solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
/// @title USDCVault
/// @notice Minimal vault demonstrating safe USDC integration
contract USDCVault is ReentrancyGuard {
using SafeERC20 for IERC20;
IERC20 public immutable USDC;
mapping(address user => uint256 balance) public balances;
error ZeroAmount();
error InsufficientBalance(uint256 available, uint256 requested);
event Deposited(address indexed user, uint256 amount);
event Withdrawn(address indexed user, address indexed recipient, uint256 amount);
constructor(address usdc_) {
USDC = IERC20(usdc_);
}
/// @notice Deposit USDC into the vault
/// @param amount Amount of USDC in 6-decimal units
function deposit(uint256 amount) external nonReentrant {
if (amount == 0) revert ZeroAmount();
balances[msg.sender] += amount;
emit Deposited(msg.sender, amount);
USDC.safeTransferFrom(msg.sender, address(this), amount);
}
/// @notice Withdraw USDC to a specified recipient
/// @dev Allows withdrawal to a different address in case msg.sender is blocklisted
/// @param recipient Address to receive USDC
/// @param amount Amount of USDC in 6-decimal units
function withdraw(address recipient, uint256 amount) external nonReentrant {
if (amount == 0) revert ZeroAmount();
uint256 bal = balances[msg.sender];
if (bal < amount) revert InsufficientBalance(bal, amount);
balances[msg.sender] = bal - amount;
emit Withdrawn(msg.sender, recipient, amount);
USDC.safeTransfer(recipient, amount);
}
}
```
### Permit Integration (Gasless Approvals)
USDC natively supports EIP-2612 `permit`, allowing users to approve and deposit in a single transaction without a prior `approve` call.
```solidity
import {IERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol";
/// @notice Deposit with a signed permit, enabling single-tx approve + deposit
/// @param amount USDC amount (6 decimals)
/// @param deadline Timestamp after which the permit signature expires
/// @param v Recovery byte of the permit signature
/// @param r First 32 bytes of the permit signature
/// @param s Second 32 bytes of the permit signature
function depositWithPermit(
uint256 amount,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) external nonReentrant {
if (amount == 0) revert ZeroAmount();
// permit may revert if signature is invalid or already used
try IERC20Permit(address(USDC)).permit(
msg.sender, address(this), amount, deadline, v, r, s
) {} catch {
// Permit may fail if:
// 1. Approval already exists (front-run or user pre-approved)
// 2. Signature was already used
// Proceed with transferFrom — it will fail if allowance is insufficient
}
balances[msg.sender] += amount;
emit Deposited(msg.sender, amount);
USDC.safeTransferFrom(msg.sender, address(this), amount);
}
```
The `try/catch` around `permit` is intentional. If a permit signature is front-run (someone else submits it first), the approval still exists and `transferFrom` succeeds. Reverting on a failed `permit` would brick the transaction unnecessarily.
### EIP-3009: transferWithAuthorization
USDC also supports EIP-3009 for authorized transfers. Unlike `permit` + `transferFrom`, this combines authorization and transfer atomically:
```solidity
interface IFiatTokenV2 {
function transferWithAuthorization(
address from,
address to,
uint256 value,
uint256 validAfter,
uint256 validBefore,
bytes32 nonce,
uint8 v,
bytes32 r,
bytes32 s
) external;
function receiveWithAuthorization(
address from,
address to,
uint256 value,
uint256 validAfter,
uint256 validBefore,
bytes32 nonce,
uint8 v,
bytes32 r,
bytes32 s
) external;
}
```
`receiveWithAuthorization` enforces `msg.sender == to`, preventing front-running of the authorization. Prefer it over `transferWithAuthorization` when the recipient is your contract.
## Handling Blocklist Scenarios
### The Problem
A user deposits USDC into your protocol. Later, Circle blocklists their address. Now:
- `USDC.transfer(blockedUser, amount)` reverts
- The user cannot withdraw
- If the user has a liquidatable position, liquidation may also revert
### Defense Patterns
```solidity
/// @notice Withdraw to an alternative recipient if the depositor is blocklisted
/// @dev Access-controlled so only the original depositor can redirect
function withdraw(address recipient, uint256 amount) external nonReentrant {
if (amount == 0) revert ZeroAmount();
uint256 bal = balances[msg.sender];
if (bal < amount) revert InsufficientBalance(bal, amount);
balances[msg.sender] = bal - amount;
emit Withdrawn(msg.sender, recipient, amount);
USDC.safeTransfer(recipient, amount);
}
```
For lending protocols where liquidation is critical:
```solidity
/// @notice Liquidate a position, sending seized USDC to the liquidator
/// @dev If the direct transfer fails (blocklist), escrow the funds
function liquidate(address borrower, uint256 repayAmount) external nonReentrant {
// ... checks and effects ...
uint256 seizedCollateral = _calculateSeizedCollateral(repayAmount);
balances[borrower] -= seizedCollateral;
// Attempt direct transfer; fall back to escrow on failure
try IERC20(address(USDC)).transfer(msg.sender, seizedCollateral) {
emit Liquidated(borrower, msg.sender, seizedCollateral);
} catch {
pendingWithdrawals[msg.sender] += seizedCollateral;
emit LiquidatedToEscrow(borrower, msg.sender, seizedCollateral);
}
}
/// @notice Claim escrowed funds from a failed liquidation transfer
function claimEscrow(address recipient) external nonReentrant {
uint256 amount = pendingWithdrawals[msg.sender];
if (amount == 0) revert NothingToClaim();
pendingWithdrawals[msg.sender] = 0;
USDC.safeTransfer(recipient, amount);
}
```
### Handling Global Pause
```solidity
/// @notice Check if USDC is currently paused
/// @dev Useful for UIs or circuits that need to know transfer availability
function isUSDCPaused() public view returns (bool) {
(bool success, bytes memory data) = address(USDC).staticcall(
abi.encodeWithSignature("paused()")
);
return success && abi.decode(data, (bool));
}
```
## Decimal Conversion Library
```solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
/// @title USDCLib
/// @notice Helpers for USDC decimal conversions
library USDCLib {
uint256 internal constant USDC_DECIMALS = 6;
uint256 internal constant USDC_UNIT = 1e6;
uint256 internal constant WAD = 1e18;
uint256 internal constant SCALE_FACTOR = 1e12;
/// @notice Convert USDC amount (6 decimals) to WAD (18 decimals)
/// @dev Lossless — always safe
function toWad(uint256 usdcAmount) internal pure returns (uint256) {
return usdcAmount * SCALE_FACTOR;
}
/// @notice Convert WAD (18 decimals) to USDC amount (6 decimals)
/// @dev Truncates — loses up to 999_999_999_999 wei (< $0.000001)
function fromWad(uint256 wadAmount) internal pure returns (uint256) {
return wadAmount / SCALE_FACTOR;
}
/// @notice Convert WAD to USDC, rounding up
/// @dev Use when the protocol should not lose value (e.g., debt calculations)
function fromWadRoundUp(uint256 wadAmount) internal pure returns (uint256) {
return (wadAmount + SCALE_FACTOR - 1) / SCALE_FACTOR;
}
/// @notice Construct a USDC amount from whole dollars
function dollars(uint256 amount) internal pure returns (uint256) {
return amount * USDC_UNIT;
}
/// @notice Construct a USDC amount from dollars and cents
function dollarsAndCents(uint256 wholeDollars, uint256 cents) internal pure returns (uint256) {
return wholeDollars * USDC_UNIT + cents * 1e4;
}
}
```
## Testing with USDC on Forks
### Fork Mainnet Setup
```solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
import {Test} from "forge-std/Test.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract USDCForkTest is Test {
IERC20 constant USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);
address alice = makeAddr("alice");
address bob = makeAddr("bob");
function setUp() public {
vm.createSelectFork("mainnet");
// deal() works for setting USDC balances on forks
deal(address(USDC), alice, 1_000_000 * 1e6); // $1M
deal(address(USDC), bob, 500_000 * 1e6); // $500K
}
function test_usdcDecimals() public view {
assertEq(USDC.decimals(), 6);
}
function test_transfer() public {
vm.prank(alice);
USDC.transfer(bob, 100 * 1e6); // $100
assertEq(USDC.balanceOf(bob), 600_000 * 1e6);
}
}
```
### Testing Blocklist Behavior
```solidity
function test_blocklistedAddressCannotReceive() public {
address blocklister = 0x5dB0115f3B72d19cEa34dD697cf412Ff86dc7E1b;
address victim = makeAddr("victim");
deal(address(USDC), alice, 100 * 1e6);
// Blocklist the victim address
vm.prank(blocklister);
Ver en GitHub