tfpdev
3.4Part 3 · Tokens

Fake USDC

Anyone can make their own digital dollar and call it USDC. I made one. Pay a bill with it, and a check that reads the name says paid. A check that looks at who issued it isn't fooled.

01Try it

One bill, two ways to pay: Circle's digital dollar, or a copy I made and gave the same name. Both payments really happen.

NW-2026-122 · 5,800 USDCIts own address: —
Both buttons send a real test payment. One is Circle's digital dollar. The other is one I made and gave the same name.15 of 15 payments left this hour
  1. No payments yet
BreaksReads the name
Says USDC on the label? Then it's paid.
  1. —
  2. —
NW-2026-122Not paid
HoldsChecks who issued it
Only counts digital dollars issued by Circle.
  1. —
  2. —
NW-2026-122Not paid

One bill for 5,800 USDC. Pay it with the fake money first and see what each side says.

15 payments an hour each. Every payment is real and uses up the lab's test money.

02Step 2

Why the name proves nothing

Anyone can use the name

A digital dollar's name is just text its maker typed in. Nobody checks it. Making ours took one step and cost almost nothing.

Who issued it is what counts

Real USDC comes from one place that Circle runs. Every payment shows where it came from, and that's the only part you should check.

Fakes look real in a wallet

The fake payment really arrives and many apps show it as 5,800 USDC. Don't just skip it: someone should look at who sent it.

03Step 3

The code

What runs behind the buttons, in one sentence. The code itself is one click away.

The bad version believes the label on the coin. The good version checks that the coin came from Circle, and sets anything else aside for a person.

Show the code anyway
BreaksBreaks
The version that loses money. Read the grey notes under it, not the code.
symbol-check.tsts
for (const log of receipt.logs) {  const t = decodeTransfer(log);  if (!t || t.to !== invoice.address) continue;  const symbol = await readContract({ address: log.address, functionName: "symbol" });  if (symbol === "USDC") credit(invoice, t.value);}
name(), symbol() and decimals() are whatever the deployer wrote. My fake returns exactly what Circle's does.
HoldsHolds
The version running behind the buttons above.
address-check.tsts
const USDC = { 11155111: "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238" } as const;for (const log of receipt.logs) {  const t = decodeTransfer(log);  if (!t || t.to !== invoice.address) continue;  if (isAddressEqual(log.address, USDC[chainId])) credit(invoice, t.value);  else flagForReview(invoice, log.address, t.value);}
A token is its contract address on one chain. Pin the address per chain in config; everything else is a label.
04Step 4

Still curious?

A few more questions people ask. Open what you need.

Is the fake money real?

It really exists, on a test network only. I made it, can make more for free, and it says inside that it isn't Circle's.

Does this happen with real money?

Yes. Copies with the same name are cheap to make on any network and get sent to businesses that take crypto. Even some genuine-looking versions are different coins from the one you asked for.

Why check the network too?

The same address can mean different things on different networks. The check has to know both.

Not sure this could happen to you?

That's what the call is for. Tell me how your product works, and I'll tell you what applies to you and what I'd check first.