编程限制

编写可升级合约并不是 free style,必须遵循一定的规矩。

限制 1:跟构造函数 say no

原因在于两点:

  1. 从语言限制上来讲,构造函数在合约部署后不属于合约的 runtime bytecode,可简单理解为部署后就消失不见了。
  2. 从逻辑上来讲,构造函数的执行应该只有一次,即使在升级的背景下,也应遵循这个原则。但是,升级合约的实质是“部署并替换”,这种情况下无法保证这一点。

因此,可以看到,在上面的例子中都没有使用构造函数,转而使用所谓的 initialize() 来完成初始化。同时,为了保证该函数只运行一次,还使用了 OpenZepplin 提供的 initializer modifier。

同理,也不要使用初始化声明,即类似下面的语句:

uint256 public hasInitialValue = 42; // X

但是,constant 例外,即以下语句没有问题:

uint256 public constant hasInitialValue = 42 // √

限制 2:initialize() 只能执行一次

原因:见上。代码实现的注意点:

  • 合约继承 Initializable
  • 使用 initializer modifier
  • 使用依赖注入来获得灵活性,上例就是如此,避免在该函数中使用硬编码。
  • 在合约构造函数中调用 _disableInitializers(),这主要是出于安全考虑。这时构造函数为:
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
    _disableInitializers();
}

限制 3:父合约的初始化也遵循 1

原因依旧同 1。

对于父合约,同样不能有构造函数,所有的初始化代码需挪到 initialize() 中,只是此时不能使用 initializer modifier,而需用 onlyInitializing modifier 来代替。原因也很简单:若是前者,一旦被子合约的初始化函数调用,父合约的初始化函数就只能执行一次,显然不合继承的语义。

OpenZepplin 提供了 @openzeppelin/contracts-upgradeable 来帮助已经熟悉了 @openzeppelin/contracts 的开发人员来编写可升级合约。前者提供了后者合约的可升级版,如 ERC721Upgradeable.sol 对应 ERC721.sol

限制 4:可兼容的存储布局

其中原因在于 solidity 的语言技术细节,未来会有专文细说。在此只需记住以下规则:相对于老版本合约,

  1. 新版本合约中的变量声明
    • 只增不删
    • 顺序不变
    • 类型不变
  2. 当继承多个合约时,新版本的继承顺序不变
  3. 父合约中的变量声明同样需要遵循:
    • 顺序不变
    • 类型不变

注意

规则 3 于 1 的区别:没有“只增不删”!

其原因很容易理解,因为在父合约中新增变量后会破坏子合约的存储布局。但问题是父合约本身也会演化,必然也有新增变量的需求。为了解决这个问题,可以使用 storage gap 的技巧来解决。说白了,就是:预留存储。

// v1
contract Base {
    uint256 base1;
    uint256[49] __gap;
}

// v2
contract Base {
    uint256 base1;
    uint256 base2;
    uint256[48] __gap;
}

上述代码中,v1 和 v2 的 Base 是存储布局兼容的。

注意

变量类型的长度关系重大,若使用 uint128,则可用两个。即:用连续两个 uint128 变量替代一个 uint256 变量。

限制 5:不要在子合约使用危险操作,如 delegatecall 和 selfdestruct

原因:当 implementation 地址已知后,其他第三方可以不通过 proxy 直接调用它。

虽然你可以在 implementation 里限制调用方的地址,但并不是所有情况下都可以这么做。因此避免危险操作是上策。

限制 6:确保使用可升级库

范围: import 的合约和 lib,确保它们可以正常工作于可升级场景。

除了 OpenZeppelin,还可以看看这个库 solidstate-solidity。正如其 readme 所言:Upgradeable-first Solidity smart contract development library . 未来或许有介绍它的专门文章。

验证升级

为了帮助确定新版本合约中适当的存储间隙大小,您可以简单地尝试使用升级upgradeProxy或运行验证validateUpgrade(参见HardhatTruffle的文档)。如果未正确减少存储间隙,您将看到一条错误消息,指示存储间隙的预期大小。

https://learnblockchain.cn/article/5348

https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable#modifying-your-contracts