Crypto Training
Upgradeable Contracts: Initializers, UUPS Footguns, and Storage Discipline
Upgrades multiply risk. Most upgrade exploits are not fancy; they’re misconfigured initializers, storage collisions, or privileged upgrade paths.
Upgradeable systems are a trade:
- you can patch bugs
- you can also introduce new bugs after users trust you
When upgrades go wrong, the chain doesn’t forgive you. The root causes are usually boring:
- initializer called twice
- initializer never called
- implementation left in a dangerous state
- upgrade auth weaker than intended
- storage layout drift
This post is a practical guide: what to lock down, what to test, and what tends to get missed.
1) The proxy reality: constructors don’t run (for users)#
Users interact with the proxy. The proxy delegates to the implementation.
That means the implementation constructor does not initialize proxy storage.
So you must use an initializer.
This trips up even experienced devs because local tests that deploy the implementation directly can accidentally “work” while production proxy deployments are broken.
2) Initializers: the failure modes#
A good initializer has three properties:
- callable exactly once
- sets every privileged role and critical parameter
- cannot be bypassed by calling an uninitialized code path
A minimal shape:
contract Vault is Initializable, OwnableUpgradeable {
function initialize(address owner_) external initializer {
__Ownable_init(owner_);
// set critical params
}
}
Common initializer bugs (and why they matter)#
| Bug | What happens | Typical impact |
|---|---|---|
missing initializer | can re-init | role takeover |
| forgot to set owner | owner is zero | “anyone is admin” or “nobody can admin” |
| partial init | some params default | unexpected economics |
| init happens via wrong path | bypass guard | permanent misconfig |
3) The “uninitialized implementation” trap#
Even if users call the proxy, attackers can sometimes interact with the implementation contract itself.
Depending on the pattern, initializing the implementation can:
- grant roles
- enable upgrade paths
- create confusion in monitoring
Best practice: explicitly disable initializers in the implementation.
4) UUPS: the upgrade function lives in the implementation#
UUPS moves upgrade logic into the implementation.
That is elegant, but it means the upgradeTo authorization is existential.
Audit questions:
- is upgrade auth enforced on every upgrade path?
- can it be bypassed via delegatecall or a different selector?
- can someone swap the implementation to a contract with a different auth model?
Even a small access control slip turns into “full protocol takeover”.
5) Storage discipline (append-only or die)#
Never reorder variables.
Never change their meaning.
Here’s a fast table that catches most layout disasters:
| Change | Safe? | Why |
|---|---|---|
| append new vars at end | usually | preserves existing slots |
| reorder vars | no | changes slots |
change type size (uint256 -> uint128) | no | packing shifts |
| remove a var | no | shifts slots |
| add a var in the middle | no | shifts slots |
If you need refactors, use explicit “namespaced storage” patterns or a storage struct pinned to a fixed slot.
6) Governance and operational security#
Upgrades are also an operational system.
Your “security boundary” includes:
- who holds the upgrade key (multisig?)
- whether there is a timelock
- whether upgrades are reviewed and reproducible
- whether emergency powers can be abused
If a single compromised laptop can push an upgrade, your Solidity code is the least of your problems.
7) Testing upgrades like you mean it#
I want to see tests that simulate the real life-cycle:
- deploy proxy
- initialize
- perform some user actions
- upgrade
- verify old state is intact
A lot of projects test the new implementation in isolation and never test an actual upgrade.
That’s how “works in tests” becomes “breaks on mainnet”.
8) A deploy checklist#
Before shipping an upgrade:
- confirm initializer called once on the proxy
- confirm implementation initializers disabled
- run a storage layout diff (append-only)
- verify upgrade auth with negative tests (attacker cannot upgrade)
- verify timelock / multisig signing process
- verify emergency pause is safe and scoped
Further reading#
- UUPS proxy concepts: https://www.rareskills.io/post/uups-proxy
- Better initialization patterns: https://medium.com/@gweiworld/a-new-better-way-to-initialize-upgradeable-contracts-39080d72f066
- Proxy deep dives: https://proxies.yacademy.dev/