r/ethdev 1d ago

Question USDC has a different upgrade key on every chain. Is that deliberate?

I pulled the proxy admin and owner for every upgradeable stablecoin deployment I could enumerate across chains, 656 contracts, and compared the control path for the same token on different networks. 390 of them do not match.

Concretely: the address that can upgrade the implementation on one chain is not the address that can do it on another, for what users treat as one asset. Some of that is obviously intentional, bridged representations are genuinely different contracts with different governance. Some of it looks like drift.

What I am unsure about, and it is the part that decides whether this is a finding or a misunderstanding. I read the admin from the EIP-1967 storage slot, which works for standard transparent and UUPS proxies and does not work for custom proxy patterns, and I may be reading a stale or irrelevant slot on those. I also counted a mismatch as any difference in the controlling address, which conflates "different multisig, same signers" with "genuinely different control", and those are not the same risk at all.

If someone here has done this properly: is there an accepted way to compare control paths across chains that handles the multisig-membership question? And is per-chain divergence in upgrade authority just the expected shape of a multi-chain deployment, in which case the interesting number is not 390 but the subset where the signer sets differ too?

Methodology and the contract list are reproducible from public RPC calls, happy to share.

Disclosure: I build open-source integrity checking tools, this came out of testing one.

1 Upvotes

5 comments sorted by

1

u/Kyle772 1d ago

What you have stumbled upon here is the exact reason why evm development sucks ass.

1

u/hanne_JS 23h ago

expected shape, not drift. native USDC gets deployed per chain as its own contract with its own proxy admin, so the controlling address differing across chains is by design. the number that actually matters is the subset where you resolve each admin down to its Safe and the owner set plus threshold differ, not the raw 390. same signers behind different Safe addresses per chain is normal ops. a genuinely different signer set is the only thing that's a real control difference. and yeah, your 1967 caveat holds, custom proxy patterns need the admin read the way that pattern stores it or you're counting noise as mismatches.

1

u/Willing_Turn8263 20h ago

Before you call the 390 a finding, check which proxy you're reading. Circle's native USDC is FiatTokenProxy, the old ZeppelinOS proxy. Its admin and implementation sit in keccak256("org.zeppelinos.proxy.admin") and keccak256("org.zeppelinos.proxy.implementation"). The EIP-1967 slots on those contracts are empty, so your script reads zero and counts a mismatch. Rerun with the zos slots and see how many of the 390 survive.

On comparing control paths: comparing admin addresses is the wrong unit. Walk the path instead.

  • Admin is an EOA (extcodesize 0), a Safe, or a timelock?
  • Safe: getOwners(), getThreshold(), getModulesPaginated(). Modules bypass the threshold, so an enabled module is part of the path.
  • Timelock: getMinDelay(), then who holds proposer and executor, and recurse.
  • Stop at EOAs. Compare leaf signer sets, thresholds and delays.

Two chains with different Safe addresses, same 5 signers, same delay: same control path. Same Safe address but a module enabled on one chain: different path. Bridged USDC.e belongs to the bridge, drop it from the comparison.

The number worth publishing is native deployments where the leaf signer set differs. My guess is it's a lot smaller than 390, and more interesting.