Upgradeable contracts break several habits from ordinary Solidity. Replace constructors with initializers, lock the implementation contract, never reorder or retype existing storage, and avoid selfdestruct and delegatecall. These rules come from OpenZeppelin's upgrade documentation and exist because proxies keep state apart from code.
Why the rules exist
In a proxy design, one contract holds the state and forwards calls to a logic contract. A constructor runs on the logic contract, not on the proxy, so any setup in a constructor never reaches the state that matters. That one fact explains most of the guidance below.
The rules
No constructors. The documentation says no constructors can be used in upgradeable contracts. Use a regular initializer function with the initializer modifier so it runs once. Parent contracts use onlyInitializing, and you must call parent initializers yourself.
No inline initial values. Do not set starting values in field declarations. Setting them there will not work for upgradeable contracts. Constants are fine because they do not occupy storage.
Lock the implementation. Call _disableInitializers() in the implementation's constructor so nobody can initialize it directly. The Parity freeze of 2017 is the cautionary story.
Use the upgradeable library variants. The standard OpenZeppelin contracts rely on constructors, so use the contracts-upgradeable package.
Avoid new inside upgradeable code. Contracts created that way are deployed by Solidity, not the upgrade tooling, and will not be upgradeable. Pass existing instances in as parameters.
No selfdestruct or delegatecall. Either could destroy the logic contract and break every proxy that points to it.
Storage layout
A proxy reads the same slots before and after an upgrade. If the new code disagrees about which variable lives where, values get misread.
- Do not change the order or type of existing state variables.
- Do not remove variables or insert new ones before existing ones. Add new variables at the end.
- Renaming a variable keeps its stored value.
- Changing inheritance order, or adding variables to a base contract, can shift slots. Storage gaps reserve space in base contracts.
- Namespaced storage under ERC-7201 avoids many of these problems and needs Solidity 0.8.20 or higher.
A pre-upgrade checklist
- Diff the storage layout of the old and new versions with tooling, not by eye.
- Run tests against a fork that holds real state.
- Confirm the implementation is locked.
- Route the upgrade through a delay, as described in our timelock explainer.
- Check upgrade authority: who can sign, and is it a single key?
Where scanning helps
Contract libraries arrive through package managers like any other dependency. Safeguard's dependency scanning and SBOM generation can show which versions of those packages a repository pins. Try the free scan at gold.safeguard.sh/scan.
Quick check
Is renaming a state variable safe?
According to the documentation, renaming keeps the stored value. Changing order or type is what breaks things.
Why lock the implementation contract?
Without that, anyone can call the initializer on the logic contract directly and take ownership of it.
Sources
Last reviewed: 19 October 2026.