可初始化的智能合约设计模式
初始化器是可升级合约实现构造函数行为的方式。
在部署合约时,通常会调用构造函数来初始化存储变量。例如,构造函数可能会设置代币的名称或最大供应量。在可升级合约中,这些信息需要存储在代理(合约)的存储变量中,而不是实现(合约)的存储变量中。在代理中添加一个构造函数,如下所示:
constructor(
string memory name_,
string memory symbol_,
uint256 maxSupply_
) {
name = name_;
symbol = symbol_;
maxSupply = maxSupply_;
}
这不是一个好的解决方案,因为在代理和实现之间对齐存储变量位置容易出错。在实现中创建构造函数也不起作用,因为它会设置实现(合约)中的存储变量。
解决上述所有问题的方法是在实现中创建一个 initializer()
函数,该函数以与构造函数相同的方式设置存储变量,并让代理通过委托调用 initialize()
实现。将 initializer()
放在实现中可确保存储变量对齐将自动正确。为了模仿构造函数,关键是这个函数只能被代理委托调用一次。
OpenZeppelin 的 Initializable.sol 合约的目的是提供这种初始化模式的稳健实现。目前,Initializable.sol
在 OpenZeppelin 的可升级合约中使用,如 ERC20Upgradeable.sol
。
本文的目的是详细解释 Initializable.sol 的工作原理。但在此之前,让我们展示如何天真地实现这种模式以及为什么天真的实现仅在最简单的情况下有效。
初始化器过程的高级示意图如下动画所示: video
可能会尝试编写如下合约,其中设计了一个修饰符来限制函数的执行仅一次且不再执行。
contract NaiveInitialization {
// **initialized** 表示合约是否已初始化
bool initialized = false;
// 限制函数仅执行一次
modifier initializer() {
require(initialized == false, "Already initialized");
initialized = true;
_;
}
// 仅可执行一次
function initialize() public initializer {
// 初始化必要的存储变量
}
}
上述代码适用于此特定合约,确保 initialize()
函数只能执行一次。然而,当与继承一起使用时,相同模式在失败。
上述模式的问题在于,当合约使用继承且父合约也必须初始化时,它不支持。让我们在以下代码中查看此问题的示例。
contract Initializable {
// **initialized** 表示合约是否已初始化
bool initialized = false;
// 限制函数仅执行一次
modifier initializer() {
require(initialized == false, "Already initialized");
initialized = true;
_;
}
}
contract ParentNaive is Initializable {
// 初始化父合约
function initializeParent() internal initializer {
// 初始化一些状态变量
}
}
contract ChildNaive is ParentNaive {
// 初始化子合约
function initializeChild() public initializer {
super.initializeParent();
// 初始化其他状态变量
}
}
上述合约的预期执行顺序如下:
调用 initializeChild()
函数。使用 initializer
修饰符,它将 initialized
变量更新为 true
。此变量被继承链中的 所有 合约使用。
接下来,在 initializeChild()
中调用 initializeParent()
函数。initializeParent()
也具有 initializer
修饰符,因此它要求变量 initialized
为 false
。
但是,当 initializeChild
运行时,initialized
变量已经设置为 true
,因此在调用 initializeParent()
时事务将回滚。
OpenZeppelin 的 Initializable.sol
合约通过允许在继承链中的所有合约进行初始化来解决了这个问题,同时防止在初始化事务之后调用初始化器。
Initializable.sol
的核心包括三个修饰符:initializer
、reinitializer
和 onlyInitializing
,以及两个状态变量:_initializing
和 _initialized
。
每个修饰符仅在特定场景中使用,具有不同的目的。它们的使用概述如下:
initializer
修饰符应在可升级合约的初始部署期间使用,且仅在最子级合约中使用。
reinitializer
修饰符应用于初始化实现合约的新版本,同样仅在最子级合约中使用。
onlyInitializing
修饰符与父级初始化器一起使用,在初始化期间运行,并防止在以后的事务中调用这些初始化器。这解决了前一节提到的问题,即由于最子级初始化器禁用了父级初始化器,导致父级初始化器无法运行的问题。通过这种方案,可以初始化所有父级合约,以及最子级合约。
下面是说明这些场景的可视化图表。这些修饰符的使用将在接下来的部分中提供更详细的解释。
Initializable.sol 实现了 ERC-7201 模式,其中状态变量在结构体中声明。如果你不了解此模式,请将 _initializing
和 _initialized
视为状态变量。
_initializing
变量是一个布尔值,指示合约是否处于初始化过程中,而 _initialized
变量存储合约的当前版本。它从值 0
开始,在第一次初始化后将为 1
。如果开发人员选择部署新实现并希望“重新初始化”存储变量以新值,则它可能会更高。
视频演示了这些部分如何协同工作:
initializer
修饰符如下所示。稍后将详细解释代码的一些部分。
由于需要解决与先前版本的向后兼容性问题,上面的代码并不直观。然而,主要思想是双重的:
将_initialized
变量设置为1
,以防止函数再次执行(绿色框)。
在_initializing
为真时,临时允许使用onlyInitializing
修改的父级初始化程序运行。如上面的代码所示,当合约尚未...
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!