延續昨日 UUPS 的介紹,今天來了解可升級合約中初始化的考量。
初始化是為了實現邏輯合約的 constructor。
因為邏輯合約的 constructor 會將資料存在邏輯合約而不是代理合約,而我們做可升級合約是希望讓所有資料儲存在代理合約,邏輯合約只負責邏輯函式的實踐。
我們不能在代理合約的 constructor 初始化邏輯合約的 constructor 參數,因為會導致 Storage Collisions。
解決方法:在邏輯合約寫 initialize()
配上 initializer
modifier。藉由向代理合約呼叫 initialize()
來初始化邏輯合約,此時資料會儲存在代理合約。
初始化的設計,需要考量到被繼承的父合約也可能要初始化。
OZ 使用 _initialized
和 _initializing
兩個變數與三個 modifier: initializer
, onlyInitializing
, reinitializer
來處理以下的問題。
當 childmost 合約初始化時,initializer
會將 initialized
設為 true,導致父合約無法初始化的問題:
contract Initializable {
bool initialized = false;
modifier initializer() {
require(initialized == false, "Already initialized");
initialized = true;
_;
}
}
contract ParentNaive is Initializable {
function initializeParent() internal initializer {
// Initialize some state variables
}
}
contract ChildNaive is ParentNaive {
function initializeChild() public initializer {
super.initializeParent();
}
}
OZ 的解法,可參考以下這部短片,影片來自 RareSkills - The initializable smart contract design pattern
https://video.wixstatic.com/video/706568_ed24969c5dea40ee9d762eeeebc61c78/1080p/mp4/file.mp4
initializer
: 給第一個 childmost 邏輯合約的初始化函式使用。onlyInitializing
: 給父邏輯合約的初始化函式使用。reinitializer
: 給要升級的邏輯合約的初始化函式使用。問題出在 initialize
函式應該被代理合約呼叫,但可能直接被一個 EOA 呼叫,後者會導致初始化的資料儲存在邏輯合約。假設有 Ownable 的設計,owner 的地址會儲存在邏輯合約。
但真正的資料應該是儲存在代理合約,所以邏輯合約的資料就算有,也不是真的,也許可以不用管它?
若壞人在邏輯合約 initialize
並將 owner 設為自己,那麽邏輯合約上有 onlyOwner
限制的函式,也能夠被壞人呼叫,容易導致潛在的問題。
第二個可能的影響不是很懂,因此貼原文:
the owner could now delegatecall to a contract containing a selfdestruct opcode. This action would erase the implementation contract’s code, preventing the proxy from migrating to a new implementation.
在 childmost 邏輯合約寫上:
constructor() {
_disableInitializers();
}
其原理是設定 version 為 type(uint64).max 讓邏輯合約無法再被初始化。
為了防止代理合約部署後,被別人 frontrun 初始化,ERC1967Proxy 的 constructor 參數提供 _data
讓你在部署代理合約時,能夠同時執行初始化。
upgradeToAndCall
時請小心。_authorizeUpgrade
有 onlyOwner
或其他權限機制。delegatecall
or selfdestruct
。學習用途,未經審核僅供參考
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.27;
import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
// import "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol";
// import "@openzeppelin/contracts/proxy/utils/Initializable.sol";
import {UUPSUpgradeable} from "@openzeppelin-contracts-upgradeable/contracts/proxy/utils/UUPSUpgradeable.sol";
import {Initializable} from "@openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol";
import {OwnableUpgradeable} from "@openzeppelin-contracts-upgradeable/contracts/access/OwnableUpgradeable.sol";
contract UUPSProxy is ERC1967Proxy {
constructor(address _implementation, bytes memory _data) payable ERC1967Proxy(_implementation, _data) {}
}
contract LogicParent is Initializable {
uint256 public parentNumber;
function _parent_init(uint256 _parentNumber) internal onlyInitializing {
parentNumber = _parentNumber;
}
}
contract Logic is Initializable, UUPSUpgradeable, LogicParent, OwnableUpgradeable {
constructor() {
_disableInitializers();
}
function initialize(address owner) public initializer {
__Ownable_init(owner);
_parent_init(24);
}
function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
function myNumber() public view returns (uint256) {
return parentNumber + 1;
}
}
contract LogicV2 is Initializable, UUPSUpgradeable, LogicParent, OwnableUpgradeable {
function initialize(address owner) public reinitializer(2) {
__Ownable_init(owner);
_parent_init(42);
}
function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
function myNumber() public view returns (uint256) {
return parentNumber + 2;
}
}