__Ownable2Step_init no longer sets an owner in OpenZeppelin 5
In OpenZeppelin Contracts Upgradeable 4.x, calling __Ownable2Step_init() made the caller the owner. In 5.x it does nothing. Bump the dependency without updating your initializers, and new deployments end up with no owner at all.
A small difference between OpenZeppelin versions that I keep seeing in reviews: __Ownable2Step_init() sets the initial owner in 4.x, but not in 5.x.
4.9: the caller becomes the owner
In @openzeppelin/contracts-upgradeable 4.9, __Ownable2Step_init() calls into Ownable and transfers ownership to msg.sender.
// Ownable2StepUpgradeable (4.9)
function __Ownable2Step_init() internal onlyInitializing {
__Ownable_init_unchained();
}
// OwnableUpgradeable (4.9)
function __Ownable_init_unchained() internal onlyInitializing {
_transferOwnership(_msgSender());
}5.x: nothing happens
In 5.0, Ownable requires an explicit initial owner, and __Ownable2Step_init() became an empty function.
// Ownable2StepUpgradeable (5.x)
function __Ownable2Step_init() internal onlyInitializing {
}
// OwnableUpgradeable (5.x)
function __Ownable_init(address initialOwner) internal onlyInitializing {
__Ownable_init_unchained(initialOwner);
}What breaks
Code written for 4.x often looks like this:
function initialize() external initializer {
__Ownable2Step_init();
// ...
}It still compiles against 5.x. But any proxy deployed and initialized with this code ends up with owner() equal to address(0), so every onlyOwner function is unreachable. Because initialize can only run once, recovery needs an upgrade with a reinitializer. If upgrades are themselves restricted to onlyOwner, as with UUPS, there is no way back.
| Version | __Ownable2Step_init() |
Owner after initialize() |
|---|---|---|
| 4.9 | Calls __Ownable_init_unchained() |
msg.sender |
| 5.x | Empty | address(0) |
The fix
Initialize Ownable explicitly with the owner you want, and keep the Ownable2Step call for completeness:
function initialize(address initialOwner) external initializer {
__Ownable_init(initialOwner);
__Ownable2Step_init();
// ...
}What I check in reviews
- Which OpenZeppelin version the project resolves (lockfile, not just
package.jsonorfoundry.toml). - Whether every
Ownable/Ownable2Stepcontract calls__Ownable_init(initialOwner)on 5.x. - A test that asserts
owner()right after deployment and initialization. It's cheap and catches this immediately.



