OpenZeppelin Contracts v4 to v5: Can Live Proxies Upgrade?
Quick answer. No: OpenZeppelin says it "will never be safe for a live contract to upgrade from one major release to another." Version 5 moved upgradeable state to ERC-7201 slots, so keep live proxies on 4.9 or redeploy.
Video transcript
Can a live OpenZeppelin v4 proxy upgrade to version 5? No. OpenZeppelin says it is never safe to upgrade a live contract across major releases.
Version 5 swapped revert strings for custom errors, made Ownable take an explicit owner, replaced token hooks and redesigned upgradeable storage.
Storage is the blocker. Version 4 stored variables in order with gap arrays. Version 5 puts each contract's state at a hashed ERC 7201 location.
We expect a v4 proxy on v5 code to break three ways. The owner reads as the zero address. Anyone could call initialize again. And your own variables shift slots.
OpenZeppelin is blunt: major releases should be assumed incompatible. Guardian saw this in a client codebase, where a renamed namespace meant an upgraded proxy would appear uninitialized.
Sort contracts into three cases. New contracts use the latest v5. Immutable v4 contracts have nothing to upgrade. Live v4 proxies stay on four point nine, or you redeploy and migrate users.
Token hooks are gone. Transfers, mints and burns now all run through one update function, so test all three.
Then work the checklist. Classify every contract, pin live proxies, raise the pragma, port hooks, and get the full diff reviewed.
A v5 migration is a new codebase. Guardian's Vanguard puts two independent teams on it. Read the full checklist, or get a quote at guardian.security.
OpenZeppelin Contracts 5.0 shipped on October 5, 2023. The current release is v5.7.0, from July 29, 2026.
For new code, v5 is the obvious choice. For a proxy already live on v4, the answer changes.
Here is what v5 breaks, why live proxies cannot follow, and a checklist for migrating safely.
What Changed in v5
The 5.0.0 changelog lists the breaking changes. These are the ones that touch most codebases:
| Change | What to do |
|---|---|
| Revert strings replaced by custom errors | Update tests and any offchain code that parses reverts |
Ownable constructor takes initialOwner | Pass the owner explicitly; address(0) reverts |
Token hooks replaced by _update | Port every _beforeTokenTransfer override |
| Minimum compiler raised to 0.8.20 | Raise your pragma; some files need 0.8.24 since 5.5 |
SafeMath, Counters, ERC777, presets and more removed | Use native checks or your own code |
| Upgradeable contracts use ERC-7201 namespaced storage | Treat the layout as new (see below) |
UUPS upgradeTo and ProxyAdmin.upgrade removed | Use upgradeToAndCall or upgradeAndCall; empty data is allowed |
| No upgradeable copies of interfaces and libraries | Import them from @openzeppelin/contracts |
Other removals include Address.isContract, ERC20Snapshot, PaymentSplitter and ERC20's increaseAllowance.
Minor 5.x releases add breaking changes too. Since 5.6.0, ERC1967Proxy reverts if deployed without initialization data.
Why the Storage Layout Breaks
In v4, upgradeable contracts stored variables in order, padded with __gap arrays. OwnableUpgradeable 4.9.6 holds _owner, then a 49-slot gap.
Initializable kept its _initialized and _initializing flags in the very first slot.
In v5, each contract keeps its variables in a struct at a hashed ERC-7201 location, such as erc7201:openzeppelin.storage.Ownable. The gaps are gone.
What would that do to a live proxy? Comparing the 4.9.6 and 5.7.0 source, we expect a typical v4 proxy pointed at v5 code to break three ways:
- The owner would read as
address(0): v5 looks in a slot the proxy never wrote. Initializablewould read version 0, so anyone could callinitializeagain.- Your own variables would shift to lower slots, reading old parent data or empty gaps.
OpenZeppelin's policy is blunt: "Major releases should be assumed incompatible." Minor and patch updates, by contrast, "always preserve storage layout compatibility".
Guardian found the same mechanism in a client codebase. In Valantis I-01, a renamed ERC-7201 namespace meant a proxy upgraded in place "will appear uninitialized".
New Deployments Versus Live Proxies
Sort every contract into one of three cases.
New contracts
Use the latest v5. Later 5.x upgrades keep the storage layout, and OpenZeppelin recommends its Upgrades Plugins or CLI to check each one.
Immutable v4 contracts
Nothing to upgrade. Any new version is a new deployment, so build it on v5.
Live v4 proxies
Keep the proxy on @openzeppelin/contracts-upgradeable 4.9.x. OpenZeppelin's security policy still backports critical fixes to 4.9; other security fixes ship only in 5.x.
To reach v5, deploy new contracts and move users, balances and roles to them. That is a new launch, with its own review.
Moving From Hooks to _update
v5 deletes _beforeTokenTransfer and _afterTokenTransfer from ERC20, ERC721 and ERC1155. Customizations now override one internal function, _update.
// v4
function _beforeTokenTransfer(address from, address to, uint256 amount) internal override {
super._beforeTokenTransfer(from, to, amount);
require(!blocked[from], "blocked");
}
// v5 (with error Blocked(address account) declared)
function _update(address from, address to, uint256 value) internal override {
if (blocked[from]) revert Blocked(from);
super._update(from, to, value);
}
Mints and burns now run through _update too, and _transfer, _mint and _burn are no longer virtual.
So logic you move from a _transfer override into _update now also runs on mint and burn. Skip it with from == address(0) or to == address(0) checks where it should not.
Order matters. "Before" checks go above super._update; "after" logic goes below it.
ERC721 changed more. _update takes to, tokenId and auth and returns the previous owner. _isAuthorized replaces _isApprovedOrOwner, and _exists is gone.
Migration Checklist
- List every contract as new, immutable or live behind a proxy.
- Pin live v4 proxies to 4.9.x in the lockfile.
- Install matching v5 versions of
@openzeppelin/contractsandcontracts-upgradeable. - Raise the pragma to at least 0.8.20, or 0.8.24 where files need it.
- Pass
initialOwnertoOwnable, or to__Ownable_initin initializers. - Port hooks to
_update, then test transfers, mints and burns. - Replace removed contracts such as
SafeMathandCounters. - Update offchain code that matches revert strings or reads storage slots.
- Pass initialization data to every new
ERC1967Proxy. - Run the Upgrades Plugins validation on every later upgrade.
- Get the full diff reviewed before mainnet.
For the release process around each upgrade, see smart contract upgrade change management. For initializer mistakes, see initializer vulnerabilities.
How Guardian Helps
A Guardian smart contract audit reviews governance, roles and upgrade paths in design, then attacks upgrade risk in the code.
A v5 migration is a new codebase. Vanguard puts two independent teams on it and reviews updates for six months after production.
For the proxy bugs we check first, see five upgradeable contract vulnerabilities.
Frequently Asked Questions
Can I upgrade my OpenZeppelin v4 proxy to v5?
No. OpenZeppelin says it is never safe for a live contract to upgrade across major releases. Version 5's upgradeable contracts keep state at ERC-7201 locations, so a v4 proxy's owner, initialized flag and other variables would no longer line up.
Is OpenZeppelin Contracts 4.9 still supported?
Partly. OpenZeppelin's security policy backports critical security fixes to 4.9, while other security fixes ship only in 5.x. The last 4.9 release in the changelog is 4.9.6, from February 29, 2024.
What replaced _beforeTokenTransfer in OpenZeppelin 5?
The internal _update function. ERC20, ERC721 and ERC1155 route transfers, mints and burns through it, so override _update and call super._update. The _transfer, _mint and _burn functions are no longer virtual.
Can a live proxy move between v5 minor versions?
Yes. OpenZeppelin says minor and patch updates always preserve storage layout compatibility. Still read each release's breaking changes, and check every upgrade with the OpenZeppelin Upgrades Plugins or CLI.
Sources
- OpenZeppelin Docs: Backwards Compatibility, Contracts 5.x
- OpenZeppelin Docs: Using with Upgrades, Contracts 5.x
- OpenZeppelin Contracts (GitHub): CHANGELOG, including "How to migrate from 4.x" (5.0.0, October 5, 2023)
- OpenZeppelin Contracts (GitHub): Release v5.0.0 (October 5, 2023)
- OpenZeppelin Contracts (GitHub): Release v5.7.0 (July 29, 2026)
- OpenZeppelin Contracts (GitHub): Security Policy, supported versions
- OpenZeppelin Contracts Upgradeable (GitHub): OwnableUpgradeable.sol, v4.9.6
- OpenZeppelin Contracts Upgradeable (GitHub): OwnableUpgradeable.sol, v5.7.0
- OpenZeppelin Contracts (GitHub): Initializable.sol, v5.7.0
- OpenZeppelin Contracts (GitHub): ERC1967Proxy.sol, v5.7.0
- Ethereum Improvement Proposals: ERC-7201, Namespaced Storage Layout
- Guardian: Valantis Overseer (January 21, 2026)
Get your v5 migration reviewed
Guardian reviews the storage layout, initializers and upgrade path of every contract you move to OpenZeppelin 5.