可升级合约编程限制
编程限制
编写可升级合约并不是 free style,必须遵循一定的规矩。
限制 1:跟构造函数 say no
原因在于两点:
- 从语言限制上来讲,构造函数在合约部署后不属于合约的 runtime bytecode,可简单理解为部署后就消失不见了。
- 从逻辑上来讲,构造函数的执行应该只有一次,即使在升级的背景下,也应遵循这个原则。但是,升级合约的实质是“部署并替换”,这种情况下无法保证这一点。
因此,可以看到,在上面的例子中都没有使用构造函数,转而使用所谓的 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 的语言技术细节,未来会有专文细说。在此只需记住以下规则:相对于老版本合约,
- 新版本合约中的变量声明
- 只增不删
- 顺序不变
- 类型不变
- 当继承多个合约时,新版本的继承顺序不变
- 父合约中的变量声明同样需要遵循:
- 顺序不变
- 类型不变
注意
规则 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
(参见Hardhat或Truffle的文档)。如果未正确减少存储间隙,您将看到一条错误消息,指示存储间隙的预期大小。
https://learnblockchain.cn/article/5348
https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable#modifying-your-contracts