# Ekx Circle Usdc

> Working with USDC and Circle infrastructure across Ekinoxis products — canonical token addresses per chain, the 6-decimal rule, testnet funding via faucet.circle.com, approve/transferFrom payment flows, CCTP cross-chain transfers, and accepting USDC in a contract. Use when building any payment, settlement, escrow, auction or payout that moves USDC, when picking a token address for a network, or when a transfer reverts or arrives with the wrong magnitude.

- Skill: `ekinoxis-evm/ekx-circle-usdc` (Agent Skill)
- Install (CLI): `npx skillmds@latest add ekinoxis-evm/ekx-circle-usdc`
- Raw SKILL.md: https://api.skillmd.com/api/skills/ekinoxis-evm/ekx-circle-usdc/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: Ekinoxis-evm (https://skillmd.com/u/ekinoxis-evm)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/ekinoxis-evm/ekx-circle-usdc

---


# Circle / USDC

**USDC is the Ekinoxis unit of account onchain.** Auctions settle in it, staking pools
hold it, rentals are paid in it, lending pools lend it.

Note: there is **no Circle npm package anywhere in the portfolio**. We integrate USDC
as a plain ERC-20 at known addresses, and use Circle only for the testnet faucet and
(where relevant) CCTP. That is the right default — the Circle Web3 Services SDK is for
custodial wallets, which is Privy/CDP's job here.

Docs: https://developers.circle.com/stablecoins

---

## Rule one: 6 decimals

```
1 USDC = 1_000_000 units  (1e6)
```

Not 1e18. This is the most expensive mistake available in our codebase, because
`parseEther("100")` on USDC does not error — it produces a number 12 orders of
magnitude too large, which either reverts on balance (best case) or, in a contract
that trusts its input, moves the wrong amount.

```ts
parseUnits("100", 6)      // ✅ 100000000n
parseEther("100")         // ❌ 100000000000000000000n
formatUnits(bal, 6)       // ✅ display
```

In Solidity: `uint256 constant ONE_USDC = 1e6;` — and never assume `1 ether`.

In tests: `deal(USDC, alice, 1_000e6)`.

---

## Addresses

| Network | USDC address |
|---|---|
| Base mainnet | `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` |
| Base Sepolia | `0x036CbD53842c5426634e7929541eC2318f3dcF7e` |
| Ethereum Sepolia | `0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238` |
| Tempo — pathUSDC | `0x20c0000000000000000000000000000000000000` |

Canonical list: https://developers.circle.com/stablecoins/usdc-contract-addresses

Wire the address per network, never hardcode one address for all chains:

```ts
const USDC: Record<number, `0x${string}`> = {
  8453:  "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
  84532: "0x036CbD53842c5426634e7929541eC2318f3dcF7e",
};
export const usdc = USDC[CHAIN.id];
```

⚠️ Our codebase contains the Base Sepolia address written with two different
capitalisations (`...f3dcF7e` and `...f3dCF7e`). Both work as addresses, but
`viem` checksum-validates `0x${string}` literals in some paths — use the checksummed
form above.

---

## Testnet funds

**https://faucet.circle.com** — real Circle testnet USDC on Base Sepolia, Ethereum
Sepolia, Arbitrum Sepolia and others.

Do not deploy a `MockUSDC` to a public testnet. A local-only `MockUSDC.sol`
exists and is fine for a local Hardhat node, but testing against a mock on a shared
testnet hides real behaviours: USDC is upgradeable, pausable, blacklists addresses,
and (on some deployments) has a non-standard `approve` that requires zeroing before
re-approving.

---

## The payment flow

USDC is a plain ERC-20, so any pull payment is **approve → transferFrom**, two
transactions. Full frontend implementation in
[`../ekx-viem-wagmi/SKILL.md`](../ekx-viem-wagmi/SKILL.md#writing--the-approve-then-spend-dance).

Contract side:

```solidity
using SafeERC20 for IERC20;

IERC20 public immutable usdc;

function bid(uint256 amount) external {
    if (amount <= highestBid) revert BidTooLow();
    usdc.safeTransferFrom(msg.sender, address(this), amount);   // pull
    // refund the previous bidder
    if (highestBidder != address(0)) usdc.safeTransfer(highestBidder, highestBid);
    highestBid = amount;
    highestBidder = msg.sender;
}
```

**Use `SafeERC20`.** USDC returns a bool, but other tokens in the same code path may
not, and `safeTransfer` handles both. Use it everywhere.

**Never `transfer` to an address that might be a contract without checking** — and
prefer pull-payments (let winners withdraw) over push-payments in loops, so one
blacklisted or reverting recipient cannot brick a settlement.

### USDC blacklist

Circle can freeze an address. A `safeTransfer` to a blacklisted recipient reverts.
In a loop, that bricks the whole batch. Pull-payment pattern avoids it:

```solidity
mapping(address => uint256) public withdrawable;
function withdraw() external {
    uint256 amt = withdrawable[msg.sender];
    withdrawable[msg.sender] = 0;
    usdc.safeTransfer(msg.sender, amt);
}
```

---

## CCTP (cross-chain)

Circle's Cross-Chain Transfer Protocol burns USDC on the source chain and mints it on
the destination — real USDC on both sides, no wrapped asset, no bridge liquidity risk.

Relevant if a product ever needs to accept funds on Ethereum and settle on Base.
Currently **not implemented anywhere in the portfolio** — one repo references it in
docs only. If you build it: burn via `TokenMessenger.depositForBurn`, wait for
Circle's attestation service, then `MessageTransmitter.receiveMessage` on the
destination. Attestation takes ~15 minutes on mainnet; design the UX around that
wait rather than blocking on it.

---

## Gotchas

1. **6 decimals.** Rule one.
2. **Mock USDC on a public testnet** hides pausability, blacklists and upgrade behaviour. Use the faucet.
3. **Approve race.** Some ERC-20s require setting allowance to 0 before a new non-zero value. USDC does not, but if you generalise the code to other tokens, handle it.
4. **Infinite approvals.** Convenient, and a standing risk if the spender is ever compromised. Approve exact amounts. Keep it that way for anything holding real money.
5. **Blacklisted recipients revert.** Prefer pull payments.
6. **Base Sepolia address capitalisation** — use the checksummed form.

