Notes

__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.

OZ 5.0 changelog

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.

solidity
// 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.

solidity
// 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:

solidity
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:

solidity
function initialize(address initialOwner) external initializer {
    __Ownable_init(initialOwner);
    __Ownable2Step_init();
    // ...
}

What I check in reviews

  1. Which OpenZeppelin version the project resolves (lockfile, not just package.json or foundry.toml).
  2. Whether every Ownable/Ownable2Step contract calls __Ownable_init(initialOwner) on 5.x.
  3. A test that asserts owner() right after deployment and initialization. It's cheap and catches this immediately.

Keep reading

View all