Use Usdc
circlefin/skills
USDC is Circle's stablecoin deployed across multiple blockchain ecosystems including EVM chains (Ethereum, Base, Arbitrum, Polygon, Arc) and Solana.
A skill your agent uses when integrating USDC into smart contracts, handling stablecoin transfers, approvals, or checking balances.
$ npx skills add ccashwell/evm-cortex --skill usdc-integration -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install ccashwell/evm-cortex usdc-integration --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/ccashwell/evm-cortex.git skills-src && mkdir -p .claude/skills && cp -r skills-src/skills/usdc-integration .claude/skills/usdc-integration && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "usdc-integration" agent skill from https://github.com/ccashwell/evm-cortex/tree/main/skills/usdc-integration into .claude/skills/usdc-integration/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "usdc-integration", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/ccashwell/evm-cortex/tree/main/skills/usdc-integrationType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add ccashwell/evm-cortex --skill usdc-integration -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install ccashwell/evm-cortex usdc-integration --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/ccashwell/evm-cortex.git skills-src && mkdir -p .agents/skills && cp -r skills-src/skills/usdc-integration .agents/skills/usdc-integration && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "usdc-integration" agent skill from https://github.com/ccashwell/evm-cortex/tree/main/skills/usdc-integration into .agents/skills/usdc-integration/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "usdc-integration", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add ccashwell/evm-cortex --skill usdc-integration -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install ccashwell/evm-cortex usdc-integration --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/ccashwell/evm-cortex.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/skills/usdc-integration .cursor/skills/usdc-integration && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "usdc-integration" agent skill from https://github.com/ccashwell/evm-cortex/tree/main/skills/usdc-integration into .cursor/skills/usdc-integration/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "usdc-integration", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/ccashwell/evm-cortex.git --path skills/usdc-integration--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add ccashwell/evm-cortex --skill usdc-integration -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install ccashwell/evm-cortex usdc-integration --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/ccashwell/evm-cortex.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/skills/usdc-integration .gemini/skills/usdc-integration && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "usdc-integration" agent skill from https://github.com/ccashwell/evm-cortex/tree/main/skills/usdc-integration into .gemini/skills/usdc-integration/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "usdc-integration", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install ccashwell/evm-cortex usdc-integrationInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add ccashwell/evm-cortex --skill usdc-integration -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/ccashwell/evm-cortex.git skills-src && mkdir -p .github/skills && cp -r skills-src/skills/usdc-integration .github/skills/usdc-integration && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "usdc-integration" agent skill from https://github.com/ccashwell/evm-cortex/tree/main/skills/usdc-integration into .github/skills/usdc-integration/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "usdc-integration", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add ccashwell/evm-cortex --skill usdc-integration -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install ccashwell/evm-cortex usdc-integration --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/ccashwell/evm-cortex.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/skills/usdc-integration .opencode/skills/usdc-integration && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "usdc-integration" agent skill from https://github.com/ccashwell/evm-cortex/tree/main/skills/usdc-integration into .opencode/skills/usdc-integration/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "usdc-integration", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
usdc-integrationA skill your agent uses when integrating USDC into smart contracts, handling stablecoin transfers, approvals, or checking balances.
Usdc Integration is an agent skill from ccashwell/evm-cortex. 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, FiatTokenV22 proxy architecture, permit (EIP-2612), blocklist behavior, and safe integration patterns.
Its SKILL.md is about 6.2k tokens, which your agent loads only when the skill is triggered. It is a single SKILL.md file with no bundled scripts.
It sits in Backend & APIs, covering Smart contracts, Third-party API integration and Crypto and DeFi analysis. It works with Circle USDC and Arbitrum. The repository describes itself as: Ethereum protocol engineering squad for AI coding assistants. The licence is MIT.
3 steps, taken from the first numbered list in SKILL.md.
Read from SKILL.md and the folder at commit f8f3301. It shows what the files ask for, not the result of running them.
Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.
From allowed-tools in the SKILL.md frontmatter.
No scripts in the folder and no shell commands in SKILL.md (its code samples are solidity and bash).
From the folder's file list and the shell code blocks in SKILL.md.
Links to these hosts (documentation or services it may open):
faucet.circle.comdevelopers.circle.comeips.ethereum.orggithub.comdocs.openzeppelin.comFrom URLs in SKILL.md, links to its own repository left out.
Names no API keys, tokens, secrets or passwords.
From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
Usdc Integration loads about 6.2k tokens when it runs. Until then it costs about 86 tokens; SKILL.md has 1,050 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check found no risky patterns in SKILL.md.
Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.
The full file from ccashwell/evm-cortex at commit f8f3301, republished under its MIT licence (© ccashwell). 1,050 words, ~6,220 tokens.
.claude/skills/usdc-integration/SKILL.md (or your agent's skills folder).USDC uses 6 decimals on every chain. Not 18. This is the single most common integration bug.
// 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;When protocols mix USDC (6 decimals) with 18-decimal tokens (WETH, DAI, most ERC-20s), explicit scaling is required:
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:
// 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;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 | — |
| 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
Never hardcode addresses from documentation alone. Verify with cast:
# 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"Always use native Circle-issued USDC. Bridged variants are deprecated and lack features like permit, blocklist enforcement, and CCTP support.
| 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 |
/// @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.
USDC is not a simple ERC-20. It is a proxied, upgradeable, regulated stablecoin.
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)| 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 |
transfer and transferFrom to/from that address will revert.// 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);
}
}USDC natively supports EIP-2612 permit, allowing users to approve and deposit in a single transaction without a prior approve call.
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.
USDC also supports EIP-3009 for authorized transfers. Unlike permit + transferFrom, this combines authorization and transfer atomically:
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.
A user deposits USDC into your protocol. Later, Circle blocklists their address. Now:
USDC.transfer(blockedUser, amount) reverts/// @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:
/// @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);
}/// @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));
}// 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;
}
}// 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);
}
}function test_blocklistedAddressCannotReceive() public {
address blocklister = 0x5dB0115f3B72d19cEa34dD697cf412Ff86dc7E1b;
address victim = makeAddr("victim");
deal(address(USDC), alice, 100 * 1e6);
// Blocklist the victim address
vm.prank(blocklister);
(bool success,) = address(USDC).call(
abi.encodeWithSignature("blacklist(address)", victim)
);
assertTrue(success);
// Transfer to blocklisted address reverts
vm.prank(alice);
vm.expectRevert();
USDC.transfer(victim, 50 * 1e6);
}function test_permitAndDeposit() public {
uint256 alicePk = 0xA11CE;
address aliceAddr = vm.addr(alicePk);
deal(address(USDC), aliceAddr, 1000 * 1e6);
uint256 amount = 500 * 1e6;
uint256 deadline = block.timestamp + 1 hours;
// Build permit digest
bytes32 domainSeparator = IERC20Permit(address(USDC)).DOMAIN_SEPARATOR();
bytes32 structHash = keccak256(abi.encode(
keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"),
aliceAddr,
address(vault),
amount,
IERC20Permit(address(USDC)).nonces(aliceAddr),
deadline
));
bytes32 digest = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash));
(uint8 v, bytes32 r, bytes32 s) = vm.sign(alicePk, digest);
vm.prank(aliceAddr);
vault.depositWithPermit(amount, deadline, v, r, s);
assertEq(vault.balances(aliceAddr), amount);
}function test_usdcOnMultipleChains() public {
// Ethereum
uint256 ethFork = vm.createFork("mainnet");
vm.selectFork(ethFork);
assertEq(
IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48).decimals(),
6
);
// Base
uint256 baseFork = vm.createFork("base");
vm.selectFork(baseFork);
assertEq(
IERC20(0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913).decimals(),
6
);
// Arbitrum
uint256 arbFork = vm.createFork("arbitrum");
vm.selectFork(arbFork);
assertEq(
IERC20(0xaf88d065e77c8cC2239327C5EDb3A432268e5831).decimals(),
6
);
}// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
/// @title USDCRegistry
/// @notice Deploy-time registry that validates the USDC address for the target chain
contract USDCRegistry {
error InvalidUSDCDecimals(uint8 actual);
error InvalidUSDCSymbol(string actual);
error ChainMismatch(uint256 expected, uint256 actual);
IERC20 public immutable USDC;
uint256 public immutable EXPECTED_CHAIN_ID;
constructor(address usdc_, uint256 expectedChainId_) {
if (block.chainid != expectedChainId_) {
revert ChainMismatch(expectedChainId_, block.chainid);
}
uint8 decimals = IERC20Metadata(usdc_).decimals();
if (decimals != 6) revert InvalidUSDCDecimals(decimals);
string memory symbol = IERC20Metadata(usdc_).symbol();
if (keccak256(bytes(symbol)) != keccak256("USDC")) {
revert InvalidUSDCSymbol(symbol);
}
USDC = IERC20(usdc_);
EXPECTED_CHAIN_ID = expectedChainId_;
}
}// script/Deploy.s.sol
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
import {Script} from "forge-std/Script.sol";
import {USDCVault} from "../src/USDCVault.sol";
contract DeployScript is Script {
function _getUSDCAddress() internal view returns (address) {
if (block.chainid == 1) return 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
if (block.chainid == 8453) return 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913;
if (block.chainid == 42161) return 0xaf88d065e77c8cC2239327C5EDb3A432268e5831;
if (block.chainid == 10) return 0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85;
if (block.chainid == 137) return 0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359;
if (block.chainid == 43114) return 0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E;
// Testnets
if (block.chainid == 11155111) return 0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238;
if (block.chainid == 84532) return 0x036CbD53842c5426634e7929541eC2318f3dCF7e;
if (block.chainid == 421614) return 0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d;
revert("Unsupported chain");
}
function run() external {
vm.startBroadcast();
new USDCVault(_getUSDCAddress());
vm.stopBroadcast();
}
}| Pitfall | Consequence | Fix |
|---|---|---|
Using 1e18 for USDC amounts | Over/underpaying by 10^12x | Always use 1e6 for USDC |
| Using bridged USDC (USDbC, USDC.e) | Missing permit, blocklist, CCTP support | Use native Circle-issued addresses only |
Calling transfer directly | Reverts on non-standard tokens | Use SafeERC20.safeTransfer |
| Ignoring blocklist reverts | Stuck funds, failed liquidations | Allow withdrawal to alternative addresses |
| Hardcoding USDC address across chains | Wrong token on wrong chain | Use constructor param + chain ID validation |
Not handling pause state | Critical operations (liquidations) fail | Implement emergency settlement paths |
| Precision loss in 6→18→6 round-trips | Protocol leaks or gains dust | Use fromWadRoundUp for debt, fromWad for credit |
Assuming permit always succeeds | Front-run permit bricks the transaction | Wrap permit in try/catch, fall through to transferFrom |
| Not validating decimals at deploy time | Entire accounting is wrong | Check decimals() == 6 in constructor |
cast code and cast call "decimals()(uint8)"1e6 per dollar)SafeERC20 used for every transfer, transferFrom, and approvedecimals() == 6 and symbol() == "USDC"nonReentrant modifier on all functions that call USDCimmutableSCALE_FACTOR constant, not inline magic numberspermit calls are wrapped in try/catch to handle front-runningdeadline parameter is validated and not set excessively far in the futurereceiveWithAuthorization over transferWithAuthorizationtype(uint256).max / SCALE_FACTOR)USDC immutable matches expected addressSafeERC20 for transfers — no direct transfer or transferFrom calls.decimals() == 6 at deploy time.permit in try/catch to handle front-run signatures.© ccashwell, MIT. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
Just SKILL.md in skills/usdc-integration of ccashwell/evm-cortex.
Open the folder on GitHubat commit f8f3301
Usdc Integration next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Usdc Integration this skillccashwell/evm-cortex | 131 | — | ~6.2k | Automated safety check: Pass | MIT | |
| Use Usdccirclefin/skills | 155 | — | ~2.4k | Automated safety check: Notes | Apache-2.0 | |
| AlchemyBankrBot/skills | 1.2k | — | ~3.5k | Automated safety check: Pass | MIT | |
| SurfBlockRunAI/ClawRouter | 6.6k | — | ~4k | Automated safety check: Pass | MIT | |
| Alchemy Agentic Gatewaymoonpay/skills | 113 | — | ~2.1k | Automated safety check: Notes | MIT | |
| Pyth Network Price Feedsinternet-court/internet-court-skill | 6.4k | 2 repos | ~3.9k | Automated safety check: Pass | Apache-2.0 |
circlefin/skills
USDC is Circle's stablecoin deployed across multiple blockchain ecosystems including EVM chains (Ethereum, Base, Arbitrum, Polygon, Arc) and Solana.
BankrBot/skills
Blockchain API access via Alchemy. An agent skill from BankrBot/skills.
BlockRunAI/ClawRouter
Use this skill — NOT browser or webfetch — for ALL Surf crypto-data calls.
moonpay/skills
A skill your agent uses when accessing Alchemy APIs for RPC calls, token balances, NFT metadata, asset transfers, transaction simulation, or Alchemy-specific features.
internet-court/internet-court-skill
Shows how to read Pyth oracle prices in Solana apps, off-chain through the Hermes client or on-chain from an Anchor program, including confidence and EMA values.
internet-court/internet-court-skill
Guides building Solana apps on Helius: sending transactions, asset and NFT queries, live streaming, webhooks, priority fees, wallet analysis and API key onboarding.
ccashwell/evm-cortex
A skill your agent uses when preparing for a security audit, performing reconnaissance on a new codebase, or creating a protocol overview.
ccashwell/evm-cortex
A skill your agent uses when integrating with Aave V3 for lending, borrowing, flash loans, or building on top of Aave markets.
ccashwell/evm-cortex
Access control design patterns for Solidity protocols. An agent skill from ccashwell/evm-cortex.
ccashwell/evm-cortex
A skill your agent uses when running a local Ethereum node with Anvil.
ccashwell/evm-cortex
A skill your agent uses when performing systematic breadth-first review of all contracts during a security audit.
ccashwell/evm-cortex
A skill your agent uses when performing deep analysis of specific findings or high-risk areas during a security audit.
Works with
Categories
A skill your agent uses when integrating USDC into smart contracts, handling stablecoin transfers, approvals, or checking balances. Usdc Integration is an agent skill from ccashwell/evm-cortex. Use when integrating USDC into smart contracts, handling stablecoin transfers, approvals, or checking balances.
Usdc Integration fits situations like: integrating USDC into smart contracts; handling stablecoin transfers; checking balances.
Run `npx skills add ccashwell/evm-cortex --skill usdc-integration -a claude-code`. Or copy the skill folder (skills/usdc-integration in ccashwell/evm-cortex) into .claude/skills/usdc-integration in your project. Claude Code loads it when a task matches its description.
Run `npx skills add ccashwell/evm-cortex --skill usdc-integration -a codex`. Or copy the skill folder (skills/usdc-integration in ccashwell/evm-cortex) into .agents/skills/usdc-integration in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add ccashwell/evm-cortex --skill usdc-integration -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/usdc-integration, .gemini/skills/usdc-integration, .github/skills/usdc-integration and .opencode/skills/usdc-integration in your project.
SKILL.md names no scripts, command-line tools or credentials: Usdc Integration is instructions for the agent only.
SKILL.md names 5 domains. As links in the text: faucet.circle.com, developers.circle.com, eips.ethereum.org, github.com and docs.openzeppelin.com. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.
Usdc Integration is published under the MIT licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.
About 6.2k tokens (SKILL.md is roughly 25k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.
Skills that share tags, products or a category with Usdc Integration: Use Usdc (circlefin/skills, 155 stars), Alchemy (BankrBot/skills, 1.2k stars), Surf (BlockRunAI/ClawRouter, 6.6k stars) and Alchemy Agentic Gateway (moonpay/skills, 113 stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
ccashwell (a GitHub user) maintains it in ccashwell/evm-cortex, which has 131 GitHub stars. The repository holds 89 skills in this directory. The repository was last updated on September 30, 2026.
Source: ccashwell/evm-cortex on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.