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.

Crypto Training2026-02-044 min read

Upgradeability illustration

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:

SOLIDITY
contract Vault is Initializable, OwnableUpgradeable {
    function initialize(address owner_) external initializer {
        __Ownable_init(owner_);
        // set critical params
    }
}

Common initializer bugs (and why they matter)#

BugWhat happensTypical impact
missing initializercan re-initrole takeover
forgot to set ownerowner is zero“anyone is admin” or “nobody can admin”
partial initsome params defaultunexpected economics
init happens via wrong pathbypass guardpermanent 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:

ChangeSafe?Why
append new vars at endusuallypreserves existing slots
reorder varsnochanges slots
change type size (uint256 -> uint128)nopacking shifts
remove a varnoshifts slots
add a var in the middlenoshifts 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:

  1. deploy proxy
  2. initialize
  3. perform some user actions
  4. upgrade
  5. 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#