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.

Zur Installation springen

Quellinformationen

Repository
ccashwell/evm-cortex
Letzte Quellaktivität
10. April 2026 um 16:31
Erkannte Sprache von SKILL.md
Englisch
Sterne
131
Forks
18

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
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);
Auf GitHub ansehen
Diese SKILL.md ist sehr gross, daher zeigt SkillsMP hier nur den ersten Abschnitt. Auf GitHub ansehen