OpenZeppelin 5에서 __Ownable2Step_init은 owner를 설정하지 않는다
OpenZeppelin Contracts Upgradeable 4.x에서는 __Ownable2Step_init()을 호출하면 호출자가 owner가 됐지만, 5.x에서는 아무 일도 하지 않습니다. 초기화 코드를 고치지 않고 의존성만 올리면, 새로 배포하는 프록시에 owner가 아예 없을 수 있습니다.
리뷰하다 보면 자주 마주치는 OpenZeppelin 버전 간 차이가 있습니다. __Ownable2Step_init()은 4.x에서는 초기 owner를 설정하지만 5.x에서는 설정하지 않습니다.
4.9: 호출자가 owner가 된다
@openzeppelin/contracts-upgradeable 4.9에서 __Ownable2Step_init()은 Ownable을 거쳐 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: 아무 일도 일어나지 않는다
5.0부터 Ownable은 초기 owner를 명시적으로 받아야 합니다. __Ownable2Step_init()은 빈 함수가 됐습니다.
solidity
// Ownable2StepUpgradeable (5.x)
function __Ownable2Step_init() internal onlyInitializing {
}
// OwnableUpgradeable (5.x)
function __Ownable_init(address initialOwner) internal onlyInitializing {
__Ownable_init_unchained(initialOwner);
}왜 문제가 되나
4.x 기준으로 작성된 코드는 보통 이렇게 생겼습니다.
solidity
function initialize() external initializer {
__Ownable2Step_init();
// ...
}5.x에서도 컴파일은 잘 됩니다. 하지만 이 코드로 배포하고 초기화한 프록시는 owner()가 address(0)이 됩니다. onlyOwner 함수는 하나도 호출할 수 없게 됩니다. initialize는 한 번만 실행되므로 복구하려면 reinitializer를 넣어 업그레이드해야 합니다. 그런데 UUPS처럼 업그레이드 자체가 onlyOwner로 막혀 있다면 되돌릴 방법이 없습니다.
| 버전 | __Ownable2Step_init() |
initialize() 이후 owner |
|---|---|---|
| 4.9 | __Ownable_init_unchained() 호출 |
msg.sender |
| 5.x | 빈 함수 | address(0) |
고치는 법
원하는 owner로 Ownable을 명시적으로 초기화하고 Ownable2Step 호출도 함께 둡니다.
solidity
function initialize(address initialOwner) external initializer {
__Ownable_init(initialOwner);
__Ownable2Step_init();
// ...
}리뷰할 때 확인하는 것
- 프로젝트가 실제로 어떤 OpenZeppelin 버전을 쓰는지 (
package.json이나foundry.toml만이 아니라 lockfile까지). - 5.x에서 모든
Ownable/Ownable2Step컨트랙트가__Ownable_init(initialOwner)를 호출하는지. - 배포와 초기화 직후
owner()를 확인하는 테스트. 거의 공짜인데도 이 문제를 바로 잡아냅니다.



