SPB Git

spb/os-vault Public

Self-custody, multi-chain crypto wallet for macOS. One recovery phrase, six chain families, zero API keys — nothing leaves your Mac.

Swift 96% Shell 3.4% Makefile 0.6%
5.1 KB

# How USDC and other stablecoins work (on Base)

Written before implementation, per project rule: understand the asset before building the wallet. Every address and decimal count below was verified live on-chain on 2026-08-05 via eth_call against https://mainnet.base.org / https://sepolia.base.org and the Etherscan contract-source API — not taken from memory.

# 1. What a stablecoin is, mechanically

A stablecoin like USDC is not a native blockchain asset. It is an entry in the storage of an ERC-20 smart contract. "Holding 10 USDC" means the USDC contract's internal balances[yourAddress] equals 10_000_000 (10 × 10⁶ base units).

Consequences for a wallet:

  • Balances are read with balanceOf(address) (an eth_call, selector 0x70a08231), not eth_getBalance (which returns native ETH).
  • Transfers are transactions to the token contract, with calldata transfer(to, amount) (selector 0xa9059cbb), value = 0. The recipient address lives inside the calldata, not in the tx to field.
  • Gas is always paid in ETH, never in the stablecoin. A wallet full of USDC with 0 ETH cannot move — the UI must surface this before the user hits a failed send.
  • Incoming/outgoing movements are observed via the contract's Transfer(from, to, value) event logs, which is what explorers index for token history.

# 2. Issuance models (why "stable")

Model Example Peg mechanism Trust assumption
Fiat-backed, centralized USDC, EURC (Circle), USDT (Tether) 1:1 reserves (cash + T-bills); mint/burn on deposit/redemption Issuer solvency + banking rails
Crypto-collateralized DAI (MakerDAO/Sky) Overcollateralized vaults + rates Protocol governance + collateral quality
Bridged representation USDbC (Base) Lock on L1, mint wrapped on L2 The bridge, plus the original issuer

Key operational facts for fiat-backed coins (USDC family):

  • The contract is an upgradeable proxy. Verified: Base USDC is Circle's FiatTokenProxy (AdminUpgradeabilityProxy pattern, implementation at 0x2ce6311ddae708829bc0784c967b7d77d19fd779, upgradeable by Circle's admin key). Behavior can change under the same address — one more reason a wallet should treat token metadata (decimals) as per-token config, verified on-chain, not assumptions.
  • The issuer can blocklist addresses (blacklist(address) in FiatToken). Funds at a blocklisted address are frozen at the contract level. Self-custody protects keys, not against issuer-level freezing — worth knowing, nothing for the wallet to implement.
  • Some support gasless-approval extensions (EIP-2612 permit, EIP-3009 transferWithAuthorization). v1 uses plain transfer only.

# 3. Decimals — the #1 bug source (verified live)

Decimals differ per token. A wallet must never hardcode 6.

Token Base mainnet address (chain 8453) Decimals Notes
USDC 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 6 Native Circle issue — the default token
EURC 0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42 6 Circle euro coin
USDT 0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2 6 Tether on Base
DAI 0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb 18 ⚠ 18, like ETH — not 6
USDbC 0xd9aAEc86B65D86f6A7B5B1b0c42FFA531710b6CA 6 Legacy bridged USDC; receive/display only, discourage new use

Testnet (Base Sepolia, chain 84532): USDC 0x036CbD53842c5426634e7929541eC2318f3dCF7e, decimals 6 (verified). Circle faucet: https://faucet.circle.com (20 USDC / 2 h); gas ETH from a Base Sepolia faucet.

Rules the code enforces:

  • All amounts are BigUInt base units internally; Double is banned for money.
  • Conversion display ⇄ base units goes through per-token decimals from the registry (TokenAmount), unit-tested for 6- and 18-decimal tokens.
  • ETH (gas) is 18 decimals; wei ⇄ ETH uses the same integer math.

# 4. Chain facts (verified live)

  • Base mainnet eth_chainId0x2105 = 8453; Base Sepolia → 0x14a34 = 84532.
  • Base is an OP-stack L2: EIP-1559 fees (maxFeePerGas / maxPriorityFeePerGas), tips are tiny (fractions of a gwei), an ERC-20 transfer costs well under a cent.
  • The signed transaction embeds the chain ID (EIP-155/1559) — replay protection across Sepolia/mainnet is automatic as long as the wallet signs with the selected network's ID.

# 5. What this means for OS Vault's design

  • A token registry (Token model) instead of a hardcoded USDC constant: symbol, name, per-network contract address, decimals. v1 ships USDC as default with EURC, DAI, USDT registered; adding a stablecoin is a registry entry, not a code change.
  • Per-token balance = one eth_call each; ETH balance fetched alongside for the gas warning ("not enough ETH to send").
  • Send flow is token-generic: encode transfer(to, amount) with the token's address and decimals; everything else (nonce, fees, signing, receipt polling) is identical across stablecoins.