OpenZeppelin的Initializable合约中_initializing与isTopLevelCall作用是什么?
可升级合约Initializable逻辑设计疑问解答
首先给出问题语境下的完整合约代码:
abstract contract Initializable { bool private _initialized; bool private _initializing; modifier initializer() { require(_initializing || !_initialized, "Initializable: contract is already initialized"); bool isTopLevelCall = !_initializing; if (isTopLevelCall) { _initializing = true; _initialized = true; } _; if (isTopLevelCall) { _initializing = false; } } } contract UpgradebleTest1 is Initializable { uint public x; function initialize(uint _x) public initializer { x = _x; } }
为什么不能仅用_initialized做状态控制
仅用_initialized确实可以实现禁止重复初始化的基础能力,但完全无法处理初始化流程中嵌套调用其他带initializer修饰符函数的场景,这也是_initializing和isTopLevelCall存在的核心意义。
可升级合约的常规开发规范中,父子合约通常都会给各自的初始化函数加上initializer修饰符,子合约初始化时需要先调用父合约的初始化函数,如果只保留_initialized变量,流程会直接报错:
- 调用子合约
initialize,触发修饰符检查,此时_initialized为false,校验通过 - 修饰符立刻将
_initialized设为true - 子合约初始化逻辑中调用父合约的
initialize,触发父合约的initializer修饰符检查,发现_initialized已经为true,直接回滚,初始化失败
_initializing与isTopLevelCall的作用
_initializing用来标记当前正处于初始化执行流程中isTopLevelCall用来判断当前触发修饰符的调用是不是最顶层的初始化调用(而非初始化过程中嵌套的内部调用)
我们走一遍正常的嵌套初始化流程就能清晰理解逻辑:
- 外部调用子合约
initialize,触发修饰符,此时_initializing为false、_initialized为false,通过require校验 isTopLevelCall判断为true,将_initializing设为true、_initialized设为true- 进入子合约初始化逻辑,调用父合约的
initialize - 父合约的
initializer修饰符触发,此时_initializing为true,直接通过require校验 - 父合约的
isTopLevelCall判断为false,不会修改任何状态变量 - 父合约初始化逻辑执行完成,回到子合约逻辑继续执行
- 子合约所有逻辑执行完成,回到修饰符的后置逻辑,因为
isTopLevelCall为true,将_initializing设为false - 整个初始化流程结束,后续任何调用带
initializer修饰符的函数,都会因为_initializing为false、_initialized为true被require拦截,避免重复初始化
如果你的业务场景中初始化逻辑完全没有嵌套调用其他带initializer修饰符的函数,仅用_initialized确实可以正常运行,但通用的可升级合约框架需要覆盖所有常规开发场景,因此这两个变量是必不可少的设计。
内容的提问来源于stack exchange,提问作者Cengo
相关产品推荐
相关产品推荐

