Skip to main content

usdc-integration

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.

Ir a la instalación

Datos de origen

Repositorio
ccashwell/evm-cortex
Última actividad en el origen
10 de abril de 2026 a las 16:31
Idioma detectado de SKILL.md
inglés
Estrellas
131
Forks
18

Opciones de instalación

De forma predeterminada está seleccionado el prompt que primero revisa el origen. Puedes cambiar a un comando directo o descargar una copia local.

Revisa los archivos de origen

Lee SKILL.md y los archivos complementarios que muestra SkillsMP antes de decidir si quieres instalarlo.

Mostrando SKILL.md

SKILL.md
Instrucciones de origen · Vista previa de solo lectura
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
Este SKILL.md es muy grande, por eso SkillsMP muestra aqui solo la primera seccion. Ver en GitHub