iT邦幫忙

2024 iThome 鐵人賽

DAY 24
0
Modern Web

web3 短篇集系列 第 24

可升級合約的初始化

  • 分享至 

  • xImage
  •  

延續昨日 UUPS 的介紹,今天來了解可升級合約中初始化的考量。

初始化是為了實現邏輯合約的 constructor。

因為邏輯合約的 constructor 會將資料存在邏輯合約而不是代理合約,而我們做可升級合約是希望讓所有資料儲存在代理合約,邏輯合約只負責邏輯函式的實踐。

我們不能在代理合約的 constructor 初始化邏輯合約的 constructor 參數,因為會導致 Storage Collisions。

解決方法:在邏輯合約寫 initialize() 配上 initializer modifier。藉由向代理合約呼叫 initialize() 來初始化邏輯合約,此時資料會儲存在代理合約。

繼承 (Inheritance) 的問題

初始化的設計,需要考量到被繼承的父合約也可能要初始化。

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.

解決方法

  1. 在壞人對邏輯合約初始化之前先對邏輯合約初始化。
  2. 禁止任何人對邏輯合約初始化,這是 OZ 的解法:

在 childmost 邏輯合約寫上:

constructor() {
    _disableInitializers();
}

其原理是設定 version 為 type(uint64).max 讓邏輯合約無法再被初始化。

預防代理合約執行初始化時遭 frontrun

為了防止代理合約部署後,被別人 frontrun 初始化,ERC1967Proxy 的 constructor 參數提供 _data 讓你在部署代理合約時,能夠同時執行初始化。

其他注意事項

  • 如果有多張父合約與多層次繼承關係,小心不要初始化父合約兩次,其中一個方法是確保初始化函式為 idempotent,亦即不管執行幾次都帶來相同的結果。
  • 當你要 override upgradeToAndCall 時請小心。
  • 確保 _authorizeUpgradeonlyOwner 或其他權限機制。
  • 升級時,要小心邏輯函式的 authorization schema。
  • 不要在邏輯合約使用 delegatecall or selfdestruct

Reference

程式碼參考

學習用途,未經審核僅供參考

// 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;
    }
}


上一篇
UUPS Proxy Pattern (ERC-1822)
下一篇
Commit-Reveal Scheme
系列文
web3 短篇集30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言