We manage your security to weather the AI storm →

← Resource Hub

OpenZeppelin Contracts v4 to v5: Can Live Proxies Upgrade?

By Owen Thurm Published 4 min read

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.

Watch: OpenZeppelin Contracts v4 to v5: Can Live Proxies Upgrade? 1:44
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 errorsUpdate tests and any offchain code that parses reverts
Ownable constructor takes initialOwnerPass the owner explicitly; address(0) reverts
Token hooks replaced by _updatePort every _beforeTokenTransfer override
Minimum compiler raised to 0.8.20Raise your pragma; some files need 0.8.24 since 5.5
SafeMath, Counters, ERC777, presets and more removedUse native checks or your own code
Upgradeable contracts use ERC-7201 namespaced storageTreat the layout as new (see below)
UUPS upgradeTo and ProxyAdmin.upgrade removedUse upgradeToAndCall or upgradeAndCall; empty data is allowed
No upgradeable copies of interfaces and librariesImport 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.
  • Initializable would read version 0, so anyone could call initialize again.
  • 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

  1. List every contract as new, immutable or live behind a proxy.
  2. Pin live v4 proxies to 4.9.x in the lockfile.
  3. Install matching v5 versions of @openzeppelin/contracts and contracts-upgradeable.
  4. Raise the pragma to at least 0.8.20, or 0.8.24 where files need it.
  5. Pass initialOwner to Ownable, or to __Ownable_init in initializers.
  6. Port hooks to _update, then test transfers, mints and burns.
  7. Replace removed contracts such as SafeMath and Counters.
  8. Update offchain code that matches revert strings or reads storage slots.
  9. Pass initialization data to every new ERC1967Proxy.
  10. Run the Upgrades Plugins validation on every later upgrade.
  11. 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

Get your v5 migration reviewed

Guardian reviews the storage layout, initializers and upgrade path of every contract you move to OpenZeppelin 5.