OpenZeppelin 可昇級合約插件 5/9 寫可昇級的合約

以太坊 ethereum andy 4年前 (2020-10-26) 1146次浏览 已收录 0个评论 扫描二维码
文章目录[隐藏]

OpenZeppelin 可昇級合約插件 5/9 寫可昇級的合約

撰寫可昇級的合約

在使用 OpenZeppelin 的可昇級合約插件來寫 Solidity 合約之前,有一些注意事項要先記得。
值得一提的是,這些限制來自於以太坊虛擬機的工作原理。這些限制適用於所有的可昇級合約項目,而不只是 OpenZeppelin 的可昇級合約。

初始化

可以在OpenZeppelin可昇級合約插件內直接使用原來的 Solidity 寫的合約,只需要修改建構函數即可。由於這種基於代理的可昇級系統,不能在可昇級的合約內使用建構函數。想了解這個限制背後的理由,可以參考 https://docs.openzeppelin.com/upgrades-plugins/1.x/proxies#the-constructor-caveat

這表示,在 OpenZeppelin 可昇級合約插件裡使用智能合約,必需把合約的建構函數修改成一般函數,一般命名為 initialize ,如下例

// NOTE: Do not use this code snippet, it's incomplete and has a critical vulnerability!

pragma solidity ^0.6.0;


contract MyContract {
    uint256 public x;

    function initialize(uint256 _x) public {
        x = _x;
    }
}

然而,Solidity 會確保 constructor 在合約的生命週期內只被調用一次,一般函數可以被調用多次。為了預防合約被初始化多次,必需在 initialize 函數內加入檢查,確保只被調用一次。

// contracts/MyContract.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;


contract MyContract {
    uint256 public x;
    bool private initialized;

    function initialize(uint256 _x) public {
        require(!initialized, "Contract instance has already been initialized");
        initialized = true;
        x = _x;
    }
}

由於這種模式在可昇級合約中非常通用,OpenZeppelin 可昇級合約插件提供 Initializable 基礎合約,使用 initializer 修飾符來完成這個模式

// contracts/MyContract.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import "@openzeppelin/upgrades/contracts/Initializable.sol";


contract MyContract is Initializable {
    uint256 public x;

    function initialize(uint256 _x) public initializer {
        x = _x;
    }
}

建構函數 constructor 和一般函數之間的另一個不同點,就是 Solidity 會自動調用本合約的所有父合約的建構函數。當寫 initializer 的時候,要手動去調用父合約的 initializer 函數

// contracts/MyContract.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import "@openzeppelin/upgrades/contracts/Initializable.sol";


contract BaseContract is Initializable {
    uint256 public y;

    function initialize() public initializer {
        y = 42;
    }
}


contract MyContract is BaseContract {
    uint256 public x;

    function initialize(uint256 _x) public initializer {
        BaseContract.initialize(); // Do not forget this call!
        x = _x;
    }
}

使用可昇級智能合約程式庫

在 initial 函數初始化的限制,不止在合約裡要注意,連引入的程式庫也是如此。舉個例子,OpenZeppelin 的 ERC20 合約,這個合約在建構函數裡面初始化代幣的名稱,符號和小數位。

// @openzeppelin/contracts/token/ERC20/ERC20.sol
pragma solidity ^0.6.0;

  ...

contract ERC20 is Context, IERC20 {

  ...

    string private _name;
    string private _symbol;
    uint8 private _decimals;

    constructor (string memory name, string memory symbol) public {
        _name = name;
        _symbol = symbol;
        _decimals = 18;
    }

  ...
}

以上表示這些合約不適用在可昇級合約插件的專案內使用。替代的方案是使用 @openzeppelin/contracts-ethereum-package , 這是官方分支的 OpenZeppelin合約,修改建構函數成為 initializer ,看一下 ERC20 合約在 @openzeppelin/contracts-ethereum-package 長什麼樣子

// @openzeppelin/contracts-ethereum-package/contracts/token/ERC20/ERC20.sol 
pragma solidity ^0.6.0; 
... 
contract ERC20UpgradeSafe is Initializable, ContextUpgradeSafe, IERC20 { 
... 
    string private _name; 
    string private _symbol; 
    uint8 private _decimals; 
    function __ERC20_init(string memory name, string memory symbol) internal initializer { 
        __Context_init_unchained(); 
        __ERC20_init_unchained(name, symbol); 
    } 
    
    function __ERC20_init_unchained(string memory name, string memory symbol) internal initializer { 
        _name = name; 
        _symbol = symbol; 
        _decimals = 18; 
    } 
    ... 
}

不論是使用 OpenZeppelin 合約或是其他合約程式庫,記得確定這些程式套件是不是可以符合可昇級合約的條件。

避免在欄位聲明時使用初始值

Solidity 在合約內總是在宣告變數時就定義了初始值

contract MyContract { 
    uint256 public constant hasInitialValue = 42; 
}

這種方式等同於在建構函數內設定這些值,像這樣的方式也不能用在可昇級合約裡。確定所有的初始值寫在 initializer 函數之中,不然的話可昇級的合約實體就沒有這些變數的設定值。

contract MyContract is Initializable { 
    uint256 public hasInitialValue; 
    function initialize() public initializer { 
        hasInitialValue = 42; 
    } 
}

注意到這種方式來定義常量也是很好,因為編譯器不會預先留下存儲空間,而是在每次使用時,透過運算常量運算式來取得這個常量。有關常量可以參考 Solidity 官網文件 https://solidity.readthedocs.io/en/latest/contracts.html#constant-and-immutable-state-variables 。所以以下的常量定義可以用在可昇級合約。

contract MyContract { 
    uint256 public constant hasInitialValue = 42; 
}

從智能合約代碼建立新的合約實體
直接從合約代碼建立一個新的合約實體時,這些創建過程是由 Solidity 來掌握,而不是 OpenZeppelin 可昇級合約插件,這表示這些合約實體不是可昇級的。

例如以下的例子,就算 MyContract 是部署為可昇級合約,但是 token 合約實體仍然不是可昇級的。

// contracts/MyContract.sol 
// SPDX-License-Identifier: MIT 

pragma solidity ^0.6.0; 

import "@openzeppelin/upgrades/contracts/Initializable.sol"; 
import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 

contract MyContract is Initializable { 
    ERC20 public token; 
    
    function initialize() public initializer { 
        token = new ERC20("Test", "TST"); // This contract will not be upgradeable 
    } 
}

如果想讓 ERC20 實體是可昇級的,最簡單達成的方式就是在建立實體完成之後,把實體當成參數注入合約內。代碼如下

// contracts/MyContract.sol 
// SPDX-License-Identifier: MIT 
pragma solidity ^0.6.0; 

import "@openzeppelin/contracts-ethereum-package/contracts/Initializable.sol"; 
import "@openzeppelin/contracts-ethereum-package/contracts/token/ERC20/IERC20.sol"; 

contract MyContract is Initializable { 
    IERC20 public token; 

    function initialize(IERC20 _token) public initializer { 
        token = _token; 
    } 
}

潛在的不安全操作

使用可昇級的合約時,通常是跟上層的合約實體交互,而不是跟底層的邏輯合約交互。但是不能防止惡意的人送交易給邏輯合約。這不構成威脅,因為底層的邏輯合約上的狀態,並不會被上層的合約實體使用,所以不會影響上層的合約實體。

但還是有例外,如果直接調用邏輯合約的 selfdestruct 操作,邏輯合約就會被刪除。所有使用這個邏輯合約的上層實體合約,將會把調用委託給這個沒有任何代碼的地址。這就會中斷專案中所有調用到的合約。

另外一個類似效果的操作,就是邏輯合約內使用了 delegatecall 操作,如果可以讓合約內的 delegatecall ,調用一個內含 selfdestruct 操作的惡意合約,也會刪除調用的合約。

因此,不允許在合約內使用 selfdestruct 或是 delegatecall 。

修改合約

在寫合約的新版本時,不論是增加新功能或是修改問題,有一些額外的限制需要注意,就是不能更改已經宣告的合約狀態變數的順序,或是類型。這些限制背後的原因,可以參考 https://docs.openzeppelin.com/upgrades-plugins/1.x/proxies (這是我們的下一篇,代理昇級模式 Proxy Upgrade Pattern)

特別注意,違反任何這些合約狀態變數的順序及類型限制,會導致合約的昇級版本把這些狀態變數值混用,而導致應用程式嚴重錯誤。

假設已經宣告的合約是以下的順序宣告合約變數

contract MyContract { 
    uint256 private x;
    string private y; 
}

以下是因為 x 的類型被修改了,所以產生錯誤

contract MyContract { 
    string private x; 
    string private y; 
}

以下是因為 x 跟 y 的順序被修改了,所以會產生錯誤

contract MyContract { 
    string private y; 
    uint256 private x; 
}

以下的代碼是因為 a 變數宣告時放在 x 及 y 之前,影響了順序所以產生錯誤

contract MyContract { 
    bytes private a; 
    uint256 private x; 
    string private y; 
}

以下的代碼則是因為刪除了 x 變數,影響了順序所以產生錯誤

contract MyContract { 
    string private y; 
}

如果需要增加新變數,可以加在最後,如下的 z 變數,加在所有已宣告的變數之後

contract MyContract { 
    uint256 private x; 
    string private y; 
    bytes private z; 
}

請記得如果重命名變數的話,那變數內容跟昇級之前會相同,

contract MyContract { 
    uint256 private x; 
    string private z; // 內容跟昇級前的 y 相同 
}

如果從後面刪除變數的話,如果之後再新增一個變數,那這個變數會跟原來被刪除的內容一樣。
以下是在昇級時刪除 y 變數

contract MyContract { 
    uint256 private x; 
}

如果未來又再昇級,增加了 z 變數,那z 的值就是 y 刪除前的值。

contract MyContract { 
    uint256 private x; 
    string private z; // z 的值是之前刪的 y 變數的值
}

有可能在修改繼承關時,不經意的更改了變數的順序及類型,以下 MyContract 是繼承 A,B 合約

contract A { 
    uint256 a; 
} 
contract B { 
    uint256 b; 
} 
contract MyContract is A, B {}

把繼承順序改為 B,A 時,也是修改了變數的順序及類型。

contract MyContract is B, A {}

如果子合約已經宣告變數了,就不能在基礎合約上增加新的變數,想一下以下的場景

contract Base { 
    uint256 base1; 
} 
contract Child is Base { 
    uint256 child; 
}

當 Base 合約新增了一個 base2 變數,base2變數的值就變成了 Child 合約的 child 變數

contract Base {
    uint256 base1;
    uint256 base2;
}

這個現象的解決方案,是在基礎合約內預先宣告幾個未使用的變數,佔用變數的位置,以便未來擴展。這個方式不會增燃料的使用量。

參考

https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable


神隊友學長Andy , 版权所有丨如未注明 , 均为原创丨本网站采用BY-NC-SA协议进行授权
转载请注明原文链接:OpenZeppelin 可昇級合約插件 5/9 寫可昇級的合約
喜欢 (0)
[[email protected]]
分享 (0)
andy
关于作者:
中年大叔,打拼 like young students.
发表我的评论
取消评论
表情 贴图 加粗 删除线 居中 斜体 签到

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址