如何使用OpenZeppelin的可升级合约
如何使用OpenZeppelin的可升级合约
已经部署的智能合约可以使用OpenZeppelin升级插件通过修改代码但保留原合约地址、状态和余额来进行升级。这点允许我们迭代自己的项目来添加新功能或修复发现的Bug。
正常情况下以太坊的智能合约是不能改变的。合约一旦创建就没办法改变。
但是,在很多场景下,人们还是希望可以修改合约。想象一下传统合约的参与双方,如果双方都同意改变,他们就可以改变合约。在以太坊上,人们也会希望通过修改合约来修复他们发现的Bug或添加其他功能。
如果不升级合约,可以通过以下方法来修复合约中发现的Bug。
- 重新部署一个新合约;
- 手工迁移所有的老合约到新合约,这个成本很高需要消耗gas费;
- 使用新合约地址更新所有老的合约;
- 通知所有用户并且说服他们开始使用新部署的合约。
使用升级插件进行升级
无论何时通过OpenZeppelin升级插件的deployProxy部署的新合约,后面都可以升级该合约实例。默认情况下,只有原来部署合约的地址有权升级它。
deployProxy 会做以下工作:
- 部署合约实现;
- 部署ProxyAdmin 合约;
- 部署proxy合约并且执行初始化函数。
// contracts/Box.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
contract Box {
uint256 private _value;
// Emitted when the stored value changes
event ValueChanged(uint256 value);
// Stores a new value in the contract
function store(uint256 value) public {
_value = value;
emit ValueChanged(value);
}
// Reads the last stored value
function retrieve() public view returns (uint256) {
return _value;
}
}
首先我们需要安装升级插件。
npm install --save-dev @openzeppelin/hardhat-upgrades
然后配置Hardhat使用@openzeppelin/hardhat-upgrades插件,在hardhat.config.js中添加以下代码:
// hardhat.config.js
...
require('@nomiclabs/hardhat-ethers');
require('@openzeppelin/hardhat-upgrades');
...
module.exports = {
...
};
为了升级合约我们首先需要将它部署为一个可升级合约,和以前合约部署步骤不同。我们将通过值42调用store初始化合约。
Hardhat当前还没有本地化部署系统,我们可以使用脚本来部署合约。
以下是使用deployProxy部署和升级合约的脚本,保存该文件路径为scripts/deploy_upgradeable_box.js
// scripts/deploy_upgradeable_box.js
const { ethers, upgrades } = require('hardhat');
async function main () {
const Box = await ethers.getContractFactory('Box');
console.log('Deploying Box...');
const box = await upgrades.deployProxy(Box, [42], { initializer: 'store' });
await box.deployed();
console.log('Box deployed to:', box.address);
}
main();
可以使用以下命令部署Box合约到开发者网络上。
npx hardhat run --network localhost scripts/deploy_upgradeable_box.js
可以使用Hardhat console来和Box合约交互。
npx hardhat console --network localhost
> const Box = await ethers.getContractFactory('Box');
> const box = await Box.attach('0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0');
> (await box.retrieve()).toString();
本示例中,我们想要给合约增加一个功能点:可以增加值的函数。
// contracts/BoxV2.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
contract BoxV2 {
// ... code from Box.sol
// Increments the stored value by 1
function increment() public {
_value = _value + 1;
emit ValueChanged(_value);
}
}
这个Solidity文件创建好以后,就可以使用upgradeProxy函数来升级已经部署的合约实例。
upgradeProxy 会做以下操作:
- 部署新合约BoxV2;
- 调用ProxyAdmin 更新proxy合约以使用BoxV2。
我们可以创建脚本使用upgradeProxy升级合约,将这个脚本存储为scripts/upgrade_box.js。脚本中需要列出proxy合约的地址。
然后使用run命令部署可升级合约Box到开发者网络上。
npx hardhat run --network localhost scripts/upgrade_box.js
命令执行完成后,Box实例就升级为最新版本,当然状态和地址与以前合约相同。我们不需要在一个新地址上部署一个新合约,也不需要手动复制旧合约的值到新合约中。
现在调用新合约中新增的increment 函数,当然需要指出proxy合约的地址。
npx hardhat console --network localhost
> const BoxV2 = await ethers.getContractFactory('BoxV2');
> const box = await BoxV2.attach('0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0');
> await box.increment();
> (await box.retrieve()).toString();
执行完成后会发现值42变成了43。
关于可升级合约原理可以稍作了解:
当我们创建可升级合约时,OpenZeppelin升级插件实际上部署了三个合约:
- 正常我们所写的合约实例;
- ProxyAdmin 合约;
- proxy 合约,就是我们我们与之交互的合约;
可升级合约的局限性:
Initialization:可升级合约不能有自己的constructor。OpenZeppelin提供了一个Initializable基础合约以标识一个方法为initializer,确保它只能运行一次。
现在试着写一个Box合约的新版本,存储能改变合约内容的管理员的地址。
// contracts/AdminBox.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
contract AdminBox is Initializable {
uint256 private _value;
address private _admin;
// Emitted when the stored value changes
event ValueChanged(uint256 value);
function initialize(address admin) public initializer {
_admin = admin;
}
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() initializer {}
// Stores a new value in the contract
function store(uint256 value) public {
require(msg.sender == _admin, "AdminBox: not admin");
_value = value;
emit ValueChanged(value);
}
// Reads the last stored value
function retrieve() public view returns (uint256) {
return _value;
}
}
当部署这个合约时,需要描述initializer 函数名(当函数名不是默认的initialize时)并提供我们想要使用的管理员地址。
由于可升级合约的局限性,如果你已经在合约中声明了一个状态变量,就不能删除它,也不能改变它的类型,或者在它之前声明另一个变量。在Box合约实例中,我们只能在value变量之后添加一个新的状态变量。
// contracts/Box.sol
contract Box {
uint256 private _value;
// We can safely add a new variable after the ones we had declared
address private _owner;
// ...
}
幸运的是,这种局限性仅仅影响状态变量。我们可以按自己的需要修改合约的函数和事件。