In 2026, leaked or stolen admin keys overtook smart-contract bugs as the biggest cause of DeFi losses: at least $1.3B in January to August, by one count. Wasabi Protocol lost about $4.5M in April because a single deployer EOA could upgrade its contracts (CoinDesk).
An audit tells you whether the code is safe. It does not tell you who can replace the code tomorrow. You can check that yourself in about five RPC calls. This post shows the calls with Foundry's cast, the traps that produce wrong answers, and what the same check found across 146 Morpho vaults.
1. Is it a proxy?
Most upgradeable contracts keep the implementation address in a fixed storage slot (EIP-1967):
RPC=https://base-rpc.publicnode.com
C=0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 # USDC on Base
# EIP-1967 implementation slot
cast storage $C 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc --rpc-url $RPC
# EIP-1967 admin slot (transparent proxies)
cast storage $C 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103 --rpc-url $RPC
# EIP-1967 beacon slot
cast storage $C 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50 --rpc-url $RPC
Trap 1: older proxies use different slots. USDC returns zero for all three above, which looks like "not upgradeable". It is upgradeable: it uses the older ZeppelinOS slots, keccak256("org.zeppelinos.proxy.implementation") = 0x7050c9e0…f8c3 and keccak256("org.zeppelinos.proxy.admin") = 0x10d6a54a…390b. Check both families.
Trap 2: some admins aren't in storage at all. Aave's pool proxies keep the admin as an immutable in the bytecode. You find the authority through the pool's ADDRESSES_PROVIDER() and that contract's owner(). Aragon apps (Lido's stETH, for example) have no admin slot either: upgrades go through the DAO kernel's ACL, which you reach with kernel().
2. Who holds the admin?
If there's an admin slot, read it. If it's a UUPS proxy (implementation set, no admin), the upgrade right usually sits behind owner() or an AccessControl role:
cast call $C "owner()(address)" --rpc-url $RPC
# AccessControlEnumerable: who holds DEFAULT_ADMIN_ROLE (0x00)?
cast call $C "getRoleMemberCount(bytes32)(uint256)" 0x0000000000000000000000000000000000000000000000000000000000000000 --rpc-url $RPC
If the admin is an OpenZeppelin ProxyAdmin contract, it's only a hop: call owner() on it.
3. What kind of key is it?
This is the part that matters, and the part block explorers don't spell out:
A=0x... # the address you found
cast code $A --rpc-url $RPC # "0x" means an EOA: one private key
cast call $A "getThreshold()(uint256)" --rpc-url $RPC # answers if it's a Safe
cast call $A "getOwners()(address[])" --rpc-url $RPC
cast call $A "getMinDelay()(uint256)" --rpc-url $RPC # OpenZeppelin TimelockController
cast call $A "delay()(uint256)" --rpc-url $RPC # Compound-style Timelock
Trap 3: a Safe isn't automatically a multisig. A 1-of-1 or 1-of-3 Safe means one signature moves everything. Recurse into its owners: a 1-of-2 Safe whose owners are a timelock and a 9-of-13 Safe is fine, while a 1-of-1 whose owner is an EOA is one key with extra steps.
Trap 4: "EOA" doesn't mean "hot wallet". Circle almost certainly keeps the USDC admin key in an HSM, and institutional curators often sign with MPC. That is invisible on-chain. What you can say is that there is no on-chain multisig and no delay, so holders get no warning before an upgrade. Report it that way and don't claim more.
Trap 5: flaky public RPCs. A rate-limited node returning an error is easy to read as "empty slot" or "function doesn't exist". Retry against a second endpoint before you conclude anything.
What this found in Morpho vaults
Morpho vaults are a good test because the risky action is specific: a curator who can enable a new market (V1) or add an adapter and raise its cap (V2) can route deposits to a new destination. Both versions put that behind a vault-level timelock.
I read the configuration of the 146 Morpho vaults holding at least $2M (Morpho API) and resolved every owner and curator key on-chain (scanned 2026-10-13):
- All 122 vaults listed in the Morpho app had a timelock on these actions, typically 3 to 7 days. That's a real exit window for depositors.
- In 22 of those listed vaults (about $1.3B), the owner or curator is a single EOA or a Safe that resolves to one EOA signer. With the timelock in place, that's a governance-transparency point rather than an emergency, and some of these signers are probably MPC-backed.
- 5 unlisted vaults (about $305M combined, $283M of it in one vault) have a zero timelock and a single-EOA owner or curator, so one key can open a new market or adapter immediately. Unlisted vaults are typically private or still being set up, and the Morpho app doesn't surface them, but anything that integrates vault addresses directly should check this.
The full table, with every address, is at https://onekeyaway.appin60.com/morpho/ (raw JSON: vaults.json).
Tools
I built OneKeyAway to do the steps above in one click. Disclosure: it's my project.
- A free checker on the page that runs in your browser over public RPC.
- Free email alerts for up to 3 contracts or Safes: you get an email when the implementation, admin, owner, Safe signers, threshold, modules or guard change. That's the job OpenZeppelin Defender's hosted Monitor did before it shut down on 2026-07-01.
- An API for agents and scripts:
GET https://onekeyaway.appin60.com/api/preview?chain=8453&address=0x…returns a free verdict, and/api/controlreturns the full path (every admin, Safe threshold and signers, timelock delay) for $0.01 per call over x402 in USDC on Base or Arbitrum. There's no API key, and a failed call isn't charged.
If you'd rather do it by hand, the five cast calls above are all you need. Check the key type, not just the address.
Update: Safe modules (answering a reader's question)
@bloqarl pointed out a sixth trap in the comments: getThreshold() can say 3-of-5, but an enabled module can call execTransactionFromModule with no signatures at all, so check getModulesPaginated(0x1, n) next to getOwners().
I checked all 62 Safes that act as owner or curator of the 146 Morpho vaults. 6 have a module enabled, and none has a transaction guard. All 6 are 2-of-5 Safes curating kpk vaults (about $55M across six listed vaults on Ethereum and Base, plus a $4M Fluidkey vault on Base). Each module is a Zodiac Roles modifier that the Safe itself owns. That's a scoped-permissions setup: allow-listed operators can make specific calls without collecting signatures, and the Safe's signers configure what's allowed. It's a deliberate and fairly common pattern, but it does mean the 2-of-5 is not the whole story. What those roles permit is the next thing to read.
OneKeyAway's Safe check now reports modules, guard, fallback handler and singleton, and the free alerts fire when any of them change.
Top comments (1)
"An audit tells you whether the code is safe, not who can replace it tomorrow" is a line more teams should put in their launch docs. The code is in scope, but who holds the upgrade key often isn't.
One more trap for step 3: Safe modules. getThreshold() can say 3-of-5, but an enabled module can call execTransactionFromModule with no signatures at all. So I'd add getModulesPaginated (start at 0x1) next to getOwners(). Did any of the 146 vaults have a Safe with a module enabled?