·
Reading the USDC Contract: The 31 Write Functions and 17 Events You Overlooked
A teardown of USDC's main contract, FiatTokenV2_2: 24 view / 31 write / 17 events. How Circle splits power across 5 roles, how V2.2 packs balance and blacklist into a single storage slot, why rescuer is currently 0x0, and why both EIP-2612 and EIP-3009 coexist.
Contract: 0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 (proxy) → impl 0x435068…02dd
Version: FiatTokenV2_2 (Solidity 0.6.12, Apache-2.0, Circle)
Size: 24 view / 31 write / 17 events — 3× larger than a typical ERC-20. We’ll unpack why.
Read Functions (24)
1️⃣ ERC-20 Standard (6)
name() · symbol() · decimals() · totalSupply()
balanceOf(address) · allowance(owner, spender)
2️⃣ EIP-712 Domain Signature Constants (5 pure-view hashes)
DOMAIN_SEPARATOR()
PERMIT_TYPEHASH() ← EIP-2612
TRANSFER_WITH_AUTHORIZATION_TYPEHASH ← EIP-3009
RECEIVE_WITH_AUTHORIZATION_TYPEHASH ← EIP-3009
CANCEL_AUTHORIZATION_TYPEHASH ← EIP-3009
👉 Insight: USDC implements both EIP-2612 (permit) and EIP-3009 (transferWithAuthorization). The former is a signed allowance; the latter is a signed direct transfer — overlapping functionality, different semantics. EIP-3009 predates 2612, and Circle kept it to stay forward-compatible with their own V2 contract shipped in 2020.
3️⃣ Nonce State (2)
nonces(address owner) ← EIP-2612, monotonic counter
authorizationState(authorizer, bytes32 nonce) ← EIP-3009, bool flag
👉 Insight: Two nonce strategies coexist. permit uses monotonic increment (storage-efficient); EIP-3009 uses a nonce set (allows out-of-order / concurrent authorizations — better suited to bots and payment flows).
4️⃣ Permission / Role Queries (7)
owner() · masterMinter() · pauser() · blacklister() · rescuer()
isMinter(address) · minterAllowance(address)
👉 Insight: 5 separated roles + delegated minting is the core architectural difference between USDC and USDT. Tether has a single owner; USDC carves authority into 5 slots.
5️⃣ Risk Query (1)
isBlacklisted(address)
6️⃣ Other Metadata (3)
version() · currency() · paused()
Write Functions (31 — including 5 overloads)
1️⃣ ERC-20 Standard (5, all whenNotPaused + notBlacklisted)
transfer · transferFrom · approve
increaseAllowance · decreaseAllowance
👉 Insight: approve also carries whenNotPaused — once Circle pauses the contract, you can’t even change your allowance. That’s stricter than “just freeze transfers.”
2️⃣ Gasless Signed Transfers (8 = 4 functions × 2 overloads)
permit(...) × 2 ← (v,r,s) and (bytes signature)
transferWithAuthorization(...) × 2
receiveWithAuthorization(...) × 2 ← extra msg.sender check vs transfer, anti front-run
cancelAuthorization(...) × 2
👉 Insight: Each function has two signature overloads — classic (v, r, s) and packed bytes signature. The packed variant exists to support EIP-1271 smart-contract wallets (Safe, AA wallets). This is one of the core motivations behind the V2.2 upgrade.
3️⃣ Mint / Burn System (4)
mint(address to, uint256 amount)
burn(uint256 amount)
configureMinter(minter, allowance)
removeMinter(minter)
👉 Insight: Minting is distributed and capped. masterMinter assigns each minter a cap that’s decremented per-mint — so even if a minter key leaks, the blast radius is bounded by that minter’s allowance. Tether has no minter allowance concept; a single key controls all supply.
4️⃣ Admin (5, owner-only)
transferOwnership · updateMasterMinter · updatePauser
updateBlacklister · updateRescuer
5️⃣ Risk Controls (4)
pause() · unpause() ← pauser only
blacklist · unBlacklist ← blacklister only
6️⃣ Asset Rescue (1)
rescueERC20(tokenContract, to, amount) ← rescuer only
👉 Insight: This function is currently dead. Live state shows rescuer() = 0x0000…0000, meaning Circle has intentionally set the rescuer to the zero address — any other ERC-20 accidentally sent to the USDC contract itself is now permanently stuck.
7️⃣ Upgrade Initializers (4, one-shot each)
initialize · initializeV2 · initializeV2_1 · initializeV2_2
👉 Insight: The 4 initializers expose the full upgrade history: V1 → V2 (adds EIP-2612 / EIP-3009) → V2.1 (adds lostAndFound capture) → V2.2 (blacklist storage refactor).
Events (17)
| Group | Events |
|---|---|
| ERC-20 |
Transfer, Approval
|
| EIP-3009 |
AuthorizationUsed, AuthorizationCanceled
|
| Mint/Burn |
Mint, Burn, MinterConfigured, MinterRemoved
|
| Risk |
Pause, Unpause, Blacklisted, UnBlacklisted
|
| Role changes |
OwnershipTransferred, MasterMinterChanged, PauserChanged, BlacklisterChanged, RescuerChanged
|
👉 Insight — 17 events = 17 monitorable on-chain signals. USDT exposes only ~10 events; it lacks destroy / minter allowance / Authorization-class events.
5 High-Value Takeaways
1. USDC is an “institutional compliance template.”
5 separated roles (owner / masterMinter / pauser / blacklister / rescuer), each capability minimized — no single key compromise can sink the system. Tether runs a single-key, single-owner architecture.
2. V2.2’s storage trick is an underrated gas-saver.
balanceAndBlacklistStates packs balanceOf(address) and isBlacklisted(address) into the same storage slot (high bit flags blacklist, low 255 bits store the balance). Result: a blacklist check goes from an extra cold SLOAD (2100 gas) to zero.
3. EIP-3009 fits bots and payments better than EIP-2612.
The nonce is a bytes32 set, not a monotonic counter — multiple concurrent authorizations can settle out-of-order. That’s why Circle’s own CCTP / Circle Pay / Stripe USDC payouts all use transferWithAuthorization.
4. EIP-1271 overloads exist for AA wallets.
All 4 signature functions are overloaded — (v, r, s) and bytes signature. The latter enables EIP-1271 contract signatures, which means a Safe multisig can gaslessly approve USDC via permit.
5. rescuer = 0x0 means Circle deliberately gave up the rescue power.
Other ERC-20s mistakenly sent to the USDC contract itself are permanently stuck. One fewer privileged account = one fewer attack surface.
All structural data and live values in this post were pulled and verified via the smarts.md MCP server.
轉發此貼文?
與您的關注者分享。
回覆