The address you integrated against is not necessarily the code that runs. On many of the contracts you depend on, the bytecode at the address is a thin forwarder, and the real logic lives at an implementation address the owner can replace with a single admin transaction.
We wanted to know how widespread that actually is among the contracts people build on every day, so we resolved 44 of them and counted.
TL;DR
10 of 44 are delegatecall proxies, 33 are not, and 1 would not resolve. Upgradeability tracks almost perfectly with whether a contract holds other people’s money: both USDC deployments, stETH, Aave V3, Compound V3 and 5 of 7 bridge contracts can be upgraded. USDT, DAI, WETH and every Uniswap router through V4 cannot.
What we ran
One describe call per address against the evmquery API with resolution annotations turned on. This is the same call the proxy detector makes, and it returns the dispatch route: which pattern matched and which address the call ends up executing at. If you want the mechanics of that resolution step by step, we wrote them up separately in how evmquery resolves a contract read.
curl -X POST https://api.evmquery.com/api/v1/query/describe \ -H "content-type: application/json" \ -H "x-api-key: $EVMQUERY_API_KEY" \ -d '{ "chain": "evm_ethereum", "include": ["resolution"], "schema": { "contracts": { "c": { "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" } } } }'For USDC that comes back as:
{ "status": "verified", "route": [ { "kind": "zeppelinos", "to": "0x43506849D7C04F9138D1A2050bbF3A0c054402dd" } ]}An empty route means the address runs its own code. A non-empty one means it forwards, and each hop names the pattern and the destination.
Method
- 44 contracts plus 3 externally owned accounts, on Ethereum and Base. Sample
is hand-picked for usage, not random: major tokens, DeFi protocols, bridges,
NFT infrastructure and core infra. - First run 2026-08-03, re-resolved
2026-08-09 with every route unchanged. Both runs are automated and diffed
weekly, so a future change shows up as a diff rather than a re-count. - Labels
are machine-verified: contracts with a known ticker were confirmed with a live
symbol()read, so a mistyped address cannot produce a wrongly labelled row.
The 10 that can change
| Contract | Chain | Pattern | Currently executes at |
|---|---|---|---|
| USDC (Circle) | Ethereum | legacy OpenZeppelin | 0x43506849D7C04F9138D1A2050bbF3A0c054402dd |
| USDC native | Base | legacy OpenZeppelin | 0x2Ce6311ddAE708829bc0784C967b7d77D19FD779 |
| stETH (Lido) | Ethereum | implementation getter | 0x028271E30a695c0527A0C50cA30603feD004cDb0 |
| Aave V3 Pool | Ethereum | EIP-1967 | 0x728a138A4823392C2EFA55e028d434F526fE03CF |
| Compound V3 USDC Comet | Ethereum | EIP-1967 | 0x83D491269720CE925f92C6bF9F66B7a0779A293a |
| Arbitrum Delayed Inbox | Ethereum | EIP-1967 | 0x7C058ad1D0Ee415f7e7f30e62DB1BCf568470a10 |
| Optimism L1StandardBridge | Ethereum | EIP-1967 | 0xB37a11AadF167B2F0b8dD85372De4bC66CD4A891 |
| Base L1StandardBridge | Ethereum | EIP-1967 | 0x61525EaaCDdB97D9184aFc205827E6A4fd0Bf62A |
| Polygon PoS RootChainManager | Ethereum | implementation getter | 0xF0235dCa8fb0D3999685724dCBB9DD00c5d62DFa |
| Wormhole Core Bridge | Ethereum | EIP-1967 | 0x3c3d457f1522D3540AB3325Aa5f1864E34cBA9D0 |
Pattern distribution: EIP-1967 six times, legacy OpenZeppelin twice, implementation getter twice. No diamonds (EIP-2535) and no minimal proxies (EIP-1167) in this set, though both exist widely elsewhere. Every route was a single hop; nothing in this sample chained two proxies.
The word “currently” in that last column is the entire point. Those addresses were true at the moment we read them and carry no guarantee beyond it.
Upgradeability clusters where custody clusters
Sort the 10 by what they do and the pattern is hard to miss. Both USDC deployments, stETH, the Aave lending pool, the Compound market, and five of the seven bridge contracts we checked. Every one of them either issues an asset or holds a pile of somebody else’s.
Now the other side. These 33 cannot be upgraded at all:
- Tokens (13): USDT, DAI, WETH, WBTC, LINK, UNI, CRV, ENS, SHIB, PEPE, MATIC/POL, wstETH, rETH
- DeFi (10): Uniswap V2 Router, V3 Factory, SwapRouter02, V4 PoolManager, Permit2, Curve 3pool, Morpho Blue, 1inch AggregationRouterV5, 0x Exchange Proxy, Convex Booster
- Infra (5): Chainlink ETH/USD feed, ENS Registry, Multicall3, Beacon Deposit Contract, Safe Singleton 1.3.0
- NFT (3): BAYC, CryptoPunks, Seaport 1.5
- Bridge (2): LayerZero EndpointV2, Circle CCTP TokenMessenger
The two biggest stablecoins made opposite calls on the same question. USDC sits behind an upgradeable proxy on both chains we checked. USDT, deployed in 2017, cannot be upgraded at all. One issuer kept an exit hatch, the other welded the door shut.
The same split runs through DeFi. Uniswap ossifies deliberately, every router and factory through V4 immutable. Aave and Compound keep the ability to patch a live lending market. Neither is wrong, they are different answers to the same tradeoff between fixing bugs and being predictable.
Our read: the more a contract looks like a bank, the more likely someone kept the keys. The more it looks like plumbing, the more likely it was welded shut on purpose.
The name on the contract tells you nothing
Chainlink’s ETH/USD feed is literally named EACAggregatorProxy. It resolves with an empty route. It is not a delegatecall proxy: it forwards reads to the aggregator behind it by ordinary external call, and its own ABI is complete. The aggregator it points to can be repointed, which is a real thing to know about oracles, but no code swap happens underneath your feet at that address.
The 0x Exchange Proxy is the same story from the other direction: “proxy” in the name, empty route in the resolution.
Retire the name heuristic
If you have ever grepped a contract name for “proxy” to decide whether to worry, that check is wrong in both directions. Resolve the address instead.
EOAs now carry code too
Since Pectra shipped EIP-7702, “is this address a contract?” stopped being a yes-or-no question. An externally owned account can point at code that runs in its context. We checked three well-known EOAs:
| EOA | Delegation |
|---|---|
| vitalik.eth | 0x5A7FC11397E9a8AD41BF10bf13F22B0a63f96f6d |
| Titan Builder | 0x63c0c19a282a1B52b07dD5a65b58948A07DAE32B |
| beaverbuild | none, plain EOA |
Two of three delegate. Resolution reports these as eip7702-delegation hops, the same mechanism as a contract proxy route, so code that assumes “EOA means no code” is now wrong in a way that will not announce itself.
What did not resolve
EigenLayer’s DelegationManager returned Could not resolve ABI, and we are counting it honestly rather than guessing. The ABI source we reach for was not available for that address at read time; the contract itself is fine. One out of 44.
What this means if you integrate
An address is a stable identifier for a location, not for behaviour. For the 10 above, the code behind the address you pinned can be replaced without the address changing, without a new deployment for you to notice, and without anyone holding the asset signing anything.
Three things worth doing:
- Resolve before you trust. Check what an address actually dispatches to before you write it into a config. Naming, block explorer labels and token lists are all claims; the dispatch route is a fact.
- Record the implementation, not just the address. If you pin dependencies anywhere, pin what it resolved to and when.
- Watch the ones that can move. An implementation change on a contract you depend on is a silent event by design. Nothing emits “your dependency changed” to you.
We now re-resolve this list every week and diff it, because the interesting moment is not the census, it is the change. Anything that moves gets written up.
You can run the same check on any address, no API key needed, with the proxy detector.



