我们公司在做网站推广,网站简介 title,wordpress怎么发布文章带图片,wordpress怎么登录界面openzeppelin可升级模板库中合约初始化详解 我们知道#xff0c;在openzeppelin提供的可升级模板库中#xff0c;合约初始化一般会涉及到下面三个元素#xff1a;initializer,initialize,onlyInitializing 。它们的功能分别为顶级初始化修饰符#xff0c;约定的初始化函数和…openzeppelin可升级模板库中合约初始化详解 我们知道在openzeppelin提供的可升级模板库中合约初始化一般会涉及到下面三个元素initializer,initialize,onlyInitializing 。它们的功能分别为顶级初始化修饰符约定的初始化函数和内部初始化修饰符。可是你真的了解他们吗本文就带你认真学习三这个元素。 我们知道在使用Solidity编写的以太坊智能合约中代码即法律意思是代码不能更改了。但是这里的更改是指代码编译后部署的字节码无法更改了并不是存储内容或者代码执行逻辑绝对无法更改。
例如我们重新设置某个参数它也可能更改代码的执行逻辑。历为假定这个参数是个外部合约的话外部合约的地址不同执行的外部合约逻辑也不相同的。
利用这个特性和Solidity中委托调用delegatcall在智能合约中可以采用代理/实现模式来实现合约的可升级。在openzeppelin模板库中提供了详尽的不同功能实现的多种实现示例。然而不管每种示例是为了实现什么功能它本质还是一个合约是合约就需要初始化虽然初始化可能什么都不做。本文详细讲述了代理/实现模式时数据初始化的几种方式及相关元素。
1、构造器与initialize
构造器常用于合约部署时初始化合约状态它只会调用一次。在代理/实现模式中实现合约的构造器是没有任何用处的因为代理合约和实现合约是两个不同的合约代理合约调用的只是实现合约的逻辑而非采用实现合约的数据。所以那么构造器没有用那怎么实现初始化呢
openzeppelin 就约定俗成采用了一个函数叫initialize来进行代理/实现模式中的合约初始化。为什么叫约定俗成呢因为你完全可以定义一个别的函数来进行初始化比如叫init这都是可以的。
因为构造器只能调用一次因此我们的初始化函数initialize也只有调用一次。有人说这很容易合约里设置一个初始化状态例如一个布尔值。初始化时检查该状态如果没有初始化过该值为false初始化时将值设置为true。那么第二次初始化时会因为状态检查通不过而失败。完全正确的确就是这么简单。但考虑合约的灵活性和兼容性openzeppelin 把它放在一个叫initializer的修饰符里进行实现并且专门用来修饰initialize函数使之只能调用一次这样就达到了类似构造器只能调用一次的效果。
2、实现合约的两种模式。
在代理/实现模式中实现合约为一个单独的合约它有两种不同的应用场景
应用在代理/实现中此时它仅提供逻辑自身数据不参与任何调用。但是必须有数据否则无法编写代码正如变量x不存在你就无法写一个setX函数那样。在初始化时是调用代理合约的initialize函数进行实现的此时代理合约委托调用了实现合约的initialize函数将代理合约的数据进行初始化。不应用在代理/实现场景中作为一个独立的实例合约存在。此时该合约就是一个正常的合约操作的是自己的数据只不过将初始化由构造器改为调用自己的initialize函数来实现。
作为开发者你可以自由的选择这两种模式。但一般为了不混淆如果采用代理/实现模式请不要写构造器虽然写了构造器也没有什么副作用。如果采用单独实例不可升级尽量不要采用openzeppelin的升极模板库而是采用普通合约加构造器的方式。openzeppelin分别提供了openzeppelin/contracts库和openzeppelin/contracts-upgradeable库来对应不同的场景。
这里有一点要注意openzeppelin/contracts-upgradeable中的合约基本上都可以应用于这两种格式中。
3、继承与onlyInitializing
当初始化涉及到继承时又稍有不同。为了保证父类合约初始化函数只能初始化一次并且只能在初始化的时候调用openzeppelin使用了一个onlyInitializing修饰符来进行验证。
ps:我们父合约的初始化内部非顶级初始化你也可以使用 initializer虽然不推荐并且有可能引发一些冲突。但我们学习的目的是为了随心所欲不逾矩。适当灵活应用也是可行的。
4、initializer修饰符详解
我们先看该修饰符的定义这里以openzeppelin/contracts-upgradeable 4.6.7 为例具体代码在openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol合约源文件中
/**
* dev Indicates that the contract has been initialized.
* custom:oz-retyped-from bool
*/
uint8 private _initialized;/**
* dev Indicates that the contract is in the process of being initialized.
*/
bool private _initializing;/*** dev A modifier that defines a protected initializer function that can be invoked at most once. In its scope,* onlyInitializing functions can be used to initialize parent contracts. Equivalent to reinitializer(1).*/
modifier initializer() {bool isTopLevelCall !_initializing;require((isTopLevelCall _initialized 1) ||(!AddressUpgradeable.isContract(address(this)) _initialized 1),Initializable: contract is already initialized);_initialized 1;if (isTopLevelCall) {_initializing true;}_;if (isTopLevelCall) {_initializing false;emit Initialized(1);}
}我们首先来看涉及到的相关变量
_initialized代表合约是否初始化过注意它是uint8类型其可能的值为1因为openzeppelin这里还实现了一个版本升级后重新初始化功能所以为了记录不同的版本号采用了uin8类型。_initializing代表合约是否正在初始化很显然它是个布尔类型。
我们来看一个正常的合约使用初始化的代码片断
function initialize() external initializer {}我们来一步一步查看调用过程 。
当用户调用initialize函数时它首先执行initializer修饰符。在该修饰符里具体执行为
bool isTopLevelCall !_initializing; 构造了一个临时变量名字叫是否顶级调用。显然没有初始化时肯定是顶级调用只有顶级调用才能调用initialize函数来初始化。进行一个require认证。条件1是顶级调用时必须初始化次数小于1也就是未初始化过条件2是初始化次数为1时必须在构造器中。这里必须在构造器中是哪得来的呢!AddressUpgradeable.isContract(address(this)) ,这行代码的意思为本地址为非合约那么本地址在什么情况下为非合约呢只有在构造器中isContract才会返回false。参考文章https://despos1to.medium.com/carefully-use-openzeppelins-address-iscontract-msg-sender-4136cc6ff66d 。这两个条件满足一个就行。等一等不是刚才说代理/实现模式这种初始化不是不调用构造器而是使用initialize函数么那么这里条件2是什么鬼这个不要急代理/实现模式里是实现合约不需要构造器代理合约是可以有构造器的。并且为了将初始化一次做到极致可以在代理合约的构造器里调用实现合约initialize函数进行初始化的。这里的条件2就是对应的这种情况。_initialized 1很好理解初始化开始了将初始化的版本号设置为1代表开始了。注意设置为1后require的第一个条件就不会再满足了也就是接下来的再次调用必须是从构造器中进行了。接下来三行也好理解如果是顶级调用的话那么将正在初始化设置为true。这里我们可以将顶级调用理解为外部调用这样可能更容易理解一些。接下来一行代码_;很关键它的意思是执行initialize函数体在我们上面的示例里函数体为空也就是什么都不做。接下来如果是顶级调用将正在初始化设置为false也就是结束了正在初始化状态并抛出一个事件进行追踪。 那么如果是非顶级外部调用就不用结束正在初始化状态吗答案是就是这样。如果是非顶级调用那么证明是内部调用。此时最外部的调用并未结束因此还是在初始化中。所有的内部调用结束后顶级调用才会结束此时才能结束正在初始化状态。这里的顶级调用和内部调用类似函数调用层级顶级调用必然是外部发起的它可以调用多级内部调用。调用其实为函数调用也就是遵循入栈出栈的一般规律所以顶级调用是最先调用最后退出的。如果我们初始化结束之后第二次调用initialize函数。此时isTopLevelCall为true而_initialized为1。如果不在构造器里的话没有人无聊到在构造器里多次相同初始化。require的两个条件均不会满足因此会抛出异常并给出Initializable: contract is already initialized错误。所以我们只能初始化一次如果一个交易算一次的话。
5、onlyInitializing修饰符详解
看完了initializer我们再看onlyInitializing代码片断仍然在该文件中如下
/*** dev Modifier to protect an initialization function so that it can only be invoked by functions with the* {initializer} and {reinitializer} modifiers, directly or indirectly.*/
modifier onlyInitializing() {require(_initializing, Initializable: contract is not initializing);_;
}这里很简单注释中提到用来保护某个初始化函数只能被initializer或者reinitializer修饰符修饰的函数调用一次。代码也很简单只有一个判断条件
_initializing为true也就是正在初始化中。这个在上面可以看到只有在initializer修饰符中_initializing才可能设置为true。并且顶级调用退出后该值重置为false也就是无法再次初始化了。
ps这里提到了reinitializer通过在相同的源文件中查询它的注释可以看到它用来版本升级后重新初始化。因为我们initializer初始化时版本号为1所以它可以升级版本号并重新初始化。 注意它是一个修饰符而非函数所以正常情况下是用不到它的。只有在极特殊的情况下才会用到我们这里暂时先略过它。我们只考虑初始化一次的情况。
看到这里我们都已经明白了通常最外部初始化调用使用initializer修饰符在内部的初始化比如父合约的初始化一般使用onlyInitializing修饰符保证内部的初始化只在initializer作用域下调用一次。是不是So Easy!
但是事情往往比想象的要复杂因为有以下几种情况
内部初始化你也可以使用initializer修饰符虽然不推荐并且我们有时也需要在非构造器中进行初始化例如代理合约部署时还未决定好初始化参数等。有时我们不采用代理/实现模式而作为一个单独的实例合约初始化。
这三种模式相互组合可以得到 至少6种场景让我们测试其中几种场景。
6、一个示例合约
我们来看一个简单的示例合约通过该合约及单元测试我们来演示上面提到的6种组合。
我们使用hardhat来做单元测试。
pragma solidity ^0.8.0;import openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol;
import openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol;contract CustomProxy is TransparentUpgradeableProxy {constructor(address _logic,address admin_, bytes memory data) TransparentUpgradeableProxy(_logic,admin_,data) {// 这里父构造器最后传入的data参数其实相当于执行了如下代码:// _logic.delegatecall(data);}
}contract A is Initializable {uint public x;uint public y;// 不推荐initializer限定条件较多如果用于内部初始化// 可能存在多个 initializer 同时起作用而引发冲突此时必须在代理合约的构造器中执行初始化function __init_X() internal initializer {x 5;}// 推荐可兼容多种情况例如不采用代理/实现模式和不在构造器中初始化function __init_Y() internal onlyInitializing {y 10;}
}contract B is A {uint public z;function initialize(uint _z) external initializer {__init_X();__init_Y();z _z;}// 演示 onlyInitializing 只能由 initializer修饰的函数调用function failedCall() external {__init_Y();}// 演示一种不好用法可以在外部任意函数中调用内部 initializer 修饰的函数// 这样容易混淆出错function initCall() external {__init_X();}
}// 推荐做法这样即可以从构造器中初始化又可以从构造器外初始化还可以单独作为一个实例。
contract C is A {uint public z;function initialize(uint _z) external initializer {__init_Y();z _z;}
}合约代码很简单CustomProxy为代理合约它继承了TransparentUpgradeableProxy合约但是没有增加任何代码原因是我们只想方便的使用TransparentUpgradeableProxy合约否则hardhat不编译它。
合约 A 定义了两个初始化函数不同的是一个使用了initializer作为修饰符一个使用了onlyInitializing作修饰符。我们推荐使用onlyInitializing原因最后讲。
合约B继承了合约A定义了一个正常的initialize初始化函数和几个测试函数。
合约C继承了合约A只定义了一个正常的initialize初始化函数。
注意一般合约只会有一个内部初始化函数我们为了演示所以在合约A中定义了两个初始化函数__init_X与__init_Y。
7、单元测试
详细的单元测试如下大家注意看注释。
const { expect } require(chai);
const { ethers } require(hardhat);describe(Proxy/Impl init test, function () {let impl;let proxy;let owner,user1,user2,users;beforeEach(async () {[owner, user1, user2,...users] await ethers.getSigners();const B await ethers.getContractFactory(B);impl await B.deploy();});describe(initializer in internal call test, () {// 在构造器中调用带有initializer的内部初始化函数可以成功it(Should be successful while call initialize in constructor while it revoke an inner function with initializer, async () {let CustomProxy await ethers.getContractFactory(TransparentUpgradeableProxy);let data impl.interface.encodeFunctionData(initialize,[100]);proxy await CustomProxy.deploy(impl.address,user1.address,data);await proxy.deployed();proxy impl.attach(proxy.address);//check stateexpect(await proxy.x()).to.be.equal(5);expect(await proxy.y()).to.be.equal(10);expect(await proxy.z()).to.be.equal(100);});// 在构造器外进行初始化时如果父类合约初始化包含有 initializer则会冲突失败。it(should be failed while call initialize out constructor while it revoke an inner function with initializer, async () {let CustomProxy await ethers.getContractFactory(TransparentUpgradeableProxy);proxy await CustomProxy.deploy(impl.address,user1.address,0x);await proxy.deployed();proxy impl.attach(proxy.address);// 未初始化expect(await proxy.x()).to.be.equal(0);// 这里失败的原因是 __init_X 也使用了initializer, 这里会重复开启两次初始化状态而又不在构造器中所以失败。await expect(proxy.initialize(100)).to.be.revertedWith(Initializable: contract is already initialized)});// 在不采用代理/实现模式的情况下如果内部初始化也包含 initializer 那么外部initialize 会失败,原因同上it(should be failed, async () {await expect(impl.initialize(100)).to.be.revertedWith(Initializable: contract is already initialized);});// 正常函数里调用 onlyInitializing 修饰的内部函数会失败因为不在初始化过程中。it(should be failed while call an inner function with onlyInitializing, async () {let CustomProxy await ethers.getContractFactory(TransparentUpgradeableProxy);proxy await CustomProxy.deploy(impl.address,user1.address,0x);await proxy.deployed();proxy impl.attach(proxy.address);await expect(proxy.failedCall()).to.be.revertedWith(Initializable: contract is not initializing);});// 正常函数调用 initializer 的内部函数。it(should be successful while call a inner function with initializer, async () {let CustomProxy await ethers.getContractFactory(TransparentUpgradeableProxy);proxy await CustomProxy.deploy(impl.address,user1.address,0x);await proxy.deployed();proxy impl.attach(proxy.address);await proxy.initCall();//check stateexpect(await proxy.x()).to.be.equal(5);// still zeroexpect(await proxy.y()).to.be.equal(0);expect(await proxy.z()).to.be.equal(0);});// 从 initialize 函数中调用内部 onlyInitializing 函数无论初始化是否发生在构造器中均可以成功it(should be successful ,async () {let C await ethers.getContractFactory(C);let c await C.deploy();let CustomProxy await ethers.getContractFactory(CustomProxy);proxy await CustomProxy.deploy(c.address,user1.address,0x);await proxy.deployed();proxy c.attach(proxy.address);// call initializeawait proxy.initialize(100);//check stateexpect(await proxy.x()).to.be.equal(0);expect(await proxy.y()).to.be.equal(10);expect(await proxy.z()).to.be.equal(100);});// 如果不采用代理/升级模式作为单独的合约it(should be successful while implement as a instance, async () {let C await ethers.getContractFactory(C);let c await C.deploy();await c.initialize(100);//check stateexpect(await proxy.x()).to.be.equal(0);expect(await proxy.y()).to.be.equal(10);expect(await proxy.z()).to.be.equal(100);});});
});8、openzeppelin/hardhat-upgrades 插件
我们从上面的单元测试可以看到代理/实现模式的一般步骤就是
部署实现合约部署代理合约调用初始化函数
因为这个步骤几乎是固定的所以有一个hardhat-upgrades库来帮我们这件事。它把这三个步骤打包成一步了我们只需要提供初始化参数即可。
我们可以简单示例一下
安装 openzeppelin/hardhat-upgrades 这里使用npm安装就不多说了hardhat.config.js配置文件中在顶部添加这么一行:require(openzeppelin/hardhat-upgrades);。编写单元测试文件内容如下
const { expect } require(chai);
const { ethers,upgrades } require(hardhat);describe(hardhat upgrades test, function () {it(can depoly proxy/impl by hardhat upgrades module, async () {const B await ethers.getContractFactory(B);let proxy await upgrades.deployProxy(B,[100]);await proxy.deployed();proxy B.attach(proxy.address);//check stateexpect(await proxy.x()).to.be.equal(5);expect(await proxy.y()).to.be.equal(10);expect(await proxy.z()).to.be.equal(100);});
});可以看到我们只需要一个deployProxy操作就完成了上面三个步骤其实还有个设置管理员的步骤这里没有讲。