Smart Contract Upgrade Patterns and How They Fail
Upgradeability is not inherently bad. Undocumented upgradeability, and the storage bugs that come with it, are what turn a routine deployment into a total loss.
The short version
- A proxy separates storage from logic. Storage layout is the fragile part.
- Appending variables is safe; reordering or removing them corrupts live state.
- Uninitialised implementations are a recurring and severe failure mode.
- "Who holds the admin key" outranks every other question.
Immutability is a strong property and an uncomfortable one. Bugs are permanent, parameters cannot be tuned, and integrations cannot be repaired. So most substantial deployments are upgradeable, which is a reasonable decision that introduces an entirely new class of failure.
How a proxy works
The pattern separates two things that are normally together. A proxy contract holds all the storage and the address users interact with. An implementation contract holds the code and no meaningful state. The proxy forwards calls to the implementation using a delegate call, which executes the implementation’s code in the proxy’s storage context.
Upgrading means pointing the proxy at a new implementation. The address users know stays the same, the balances stay the same, the behaviour changes.
The entire fragility of the pattern follows from that one sentence: code from one contract runs against storage laid out by another.
Storage collisions
Solidity assigns storage slots by declaration order. The proxy therefore depends on the new implementation declaring its variables in exactly the same order as the old one.
Append a variable at the end: safe. Insert one in the middle, reorder two, change a type’s size, or remove one: every subsequent variable shifts, and each now reads whatever the previous occupant left behind. A balance becomes an address. A boolean becomes a fragment of a mapping key.
Nothing reverts. The contract continues operating on corrupted state, which is considerably worse than failing.
The mitigations are well established — storage gaps reserving unused slots for future variables, and the unstructured-storage approach that puts the admin and implementation pointers at hashed pseudo-random slots so they cannot collide with the implementation’s own layout. Both work. Both depend on the deploying team actually following them on every subsequent upgrade, which is where discipline decays.
The uninitialised implementation
Proxied contracts cannot use constructors for their initial state, because a constructor runs in the implementation’s context and the proxy’s storage would stay empty. So they use an initialiser function guarded to run only once.
Two recurring failures. First, the guard is missing or incorrectly applied and the initialiser can be called again, letting an attacker reassign ownership. Second — and more subtly — the implementation contract itself is left uninitialised. Someone else initialises it directly, takes ownership of the implementation, and in patterns where the implementation can be made to self-destruct or to delegate onward, that ownership becomes leverage over every proxy pointing at it.
The fix is to disable initialisers on the implementation at deployment. It is one line and it is regularly omitted.
Function selector clashes
A proxy that exposes its own administrative functions can find one of its selectors matching a function in the implementation. Calls then hit the wrong one. Transparent proxy patterns address this by routing based on caller — admin calls go to the proxy, everyone else falls through — and diamond patterns address it with explicit selector routing. Both are solved problems that resurface in bespoke implementations.
The question that outranks all of this
Who can perform the upgrade?
Every technical mitigation above is subordinate to that answer. If a single externally-owned account can repoint the proxy, then the contract’s security is that account’s key management, regardless of audits. A multisig improves it in proportion to threshold and signer independence. A timelock improves it more than either, because it does not prevent a malicious upgrade but gives users a window to exit before it lands.
When evaluating a protocol, this is the first thing to establish and it is usually discoverable on chain in a few minutes. The gap between what the documentation implies and what the admin slot actually contains is a recurring and under-reported finding.
Keep reading
Related from the newsroom
What a Smart Contract Audit Does and Does Not Tell You
How to read a smart contract audit report: scope, commit hash, severity handling and the questions that reveal whether an audit…
Leave a comment