基于 Openzeppelin 的可升级合约解决方案的注意事项

基于 Openzeppelin 的可升级合约解决方案的注意事项

注意事项

构造函数

在编写可升级合约时请不要使用构造函数contructor(),我们知道可升级合约运行时逻辑与数据分离的,合约数据保存在代理合约中,我们编写的合约是逻辑合约,当合约部署时,逻辑合约调用contructor()初始化的数据是逻辑合约的,代理合约中的数据并没有被初始化,所以是无效的。
包括全局变量声明时赋值初始值,因为这种做法相当于在构造函数contructor()中设置这些值。

父类合约初始化

如果MyContract继承自合约 BaseContract, 那么BaseContract合约的初始化函数 initialize() 的modifier(修饰器) 必须使用 onlyInitializing,比如:

contract BaseContract is Initializable {
    uint256 public y;

    function initialize() public onlyInitializing {
        y = 42;
    }
}
// BaseContract 继承自 Initializable,这里无需重复显式继承 Initializable
contract MyContract is BaseContract {
    int storageValue;

    // modifier(修饰器) initializer 可以确保initialize只会被调用一次
    function initialize(int initValue) public initializer {
        BaseContract.initialize();
        storageValue = initValue;
    }
    ......
}

声明状态变量

  • 声明状态变量时,不能对其赋值 初始值

像这样做是错误的

contract MyContract is Initializable {
    int storageValue = 666; //初始值无效

    function initialize() public initializer {
    }
    ......
}

因为这种做法相当于在构造函数contructor()中设置这些值,因此不适用于可升级的约定。

  • 定义常量状态变量仍然可以

像这样是可以的

contract MyContract is Initializable {
    // constant 常量关键字
    int public constant storageValue = 666; //有效

    function initialize() public initializer {
    }
    ......
}

在合约代码中创建新合约实例

  • 不要在合约中创建新的合约实例,创建出的合约是不可升级的

像这样,即使MyContract是可升级的,但 ERC20 实例不可升级

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

contract MyContract is Initializable {
    ERC20 public token;
    
    function initialize() public initializer {
        token = new ERC20("Test", "TST"); // 这个合约是不可升级的
    }
    ......
}
  • 如果你希望在MyContract引用其他可升级合约,那么将已部署好的可升级合约实例注入到MyContract中,是很好的解决方案

像这样

import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol";

contract MyContract is Initializable {
    IERC20Upgradeable public token;

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

升级合约时声明状态变量

在编写合约的新版本时,无论是由于新功能还是 bug 修复,还有一个额外的限制需要遵守:不能更改合约状态变量的声明顺序,也不能更改其类型。

比如当前版本(V1)合约状态变量布局

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

那么在编写新版本合约时,请避免一下错误操作:

  • 更改变量的类型

这是错误的

contract MyContractV2 {
    string private x;
    string private y;
}
  • 更改声明它们的顺序

这是错误的

contract MyContractV2 {
    string private y;
    uint256 private x;
}
  • 在现有变量之前引入一个新变量

这是错误的

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

这是正确的

contract MyContractV2 {
    uint256 private x;
    string private y;
    bytes private a;
}
  • 删除现有变量

这是错误的

contract MyContractV2 {
    string private y;
}

参考文档:
Openzeppelin编写可升级合约:https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable


版权声明:本文为Lyon_Nee原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。