Notes

OpenZeppelin 5에서 __Ownable2Step_init은 owner를 설정하지 않는다

OpenZeppelin Contracts Upgradeable 4.x에서는 __Ownable2Step_init()을 호출하면 호출자가 owner가 됐지만, 5.x에서는 아무 일도 하지 않습니다. 초기화 코드를 고치지 않고 의존성만 올리면, 새로 배포하는 프록시에 owner가 아예 없을 수 있습니다.

OZ 5.0 변경 내역

리뷰하다 보면 자주 마주치는 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();
    // ...
}

리뷰할 때 확인하는 것

  1. 프로젝트가 실제로 어떤 OpenZeppelin 버전을 쓰는지 (package.json이나 foundry.toml만이 아니라 lockfile까지).
  2. 5.x에서 모든 Ownable/Ownable2Step 컨트랙트가 __Ownable_init(initialOwner)를 호출하는지.
  3. 배포와 초기화 직후 owner()를 확인하는 테스트. 거의 공짜인데도 이 문제를 바로 잡아냅니다.

계속 읽어보기

모두 보기