背景:在开发或维护 solidity 语言的智能合约时,经常会因为业务逻辑变动而变动合约内的逻辑,这就要考虑在不影响以前智能合约中已上链的数据的同时,修改或扩展新的业务逻辑,所以合约第一次开发时就需要考虑其本身支持可升级功能
目的:本篇文章是为了让读者快速上手使用 hardhat 并搭配 openZeppelin 的 uups 升级模式对合约进行可升级适配以及指导后续如何进行合约升级
适用对象:适用于 BSN 开放联盟链武汉链(基于 ETH),也可适用于其他支持 solidity 语言的链框架
id:BSN_2021
公众号:BSN 研习社
如何使用 hardhat 进行合约 uups 模式升级
- 安装 hardhat 
- 初始化项目 
- 编写合约 
- 编写测试文件 
- 编辑配置文件 
- 执行项目 
- 参考 
安装 hardhat
1.创建空的文件夹 demo-uups 并进入文件夹里面,执行 npm init
2.执行安装命令 npm install --save-dev hardhat
3.安装依赖 npm install --save-dev @nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers ethers
4.安装 openZeppelin 的 uups 可升级合约包 npm install --save-dev @openzeppelin/contracts-upgradeable
5.安装测试工具 mocha npm install --save-dev mocha
初始化项目
执行 npx hardhat 选择 Create a basic sample project
初始化完成后的目录结构为
编写合约
在 contracts 目录下面新建两个名称分别为 MyLogicV1 和 MyLogicV2 的.sol 文件并进行代码编写。实现 openZeppelin 的 uups 升级模式必须要引入 openZeppelin 相关库文件,且必须在 constructor()方法上加 @custom:oz-upgrades-unsafe-allow constructor 字样注释
 
// contract/MyLogicV1.sol
// SPDX-License-Identifier: MITpragma solidity ^0.8.0;
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
contract MyLogicV1 is Initializable, UUPSUpgradeable, OwnableUpgradeable {    function initialize() initializer public {      __Ownable_init();      __UUPSUpgradeable_init();    }
    /// @custom:oz-upgrades-unsafe-allow constructor    constructor() initializer {}
    function _authorizeUpgrade(address) internal override onlyOwner {}
    mapping(string => uint256) private logic;
    event logicSetted(string indexed _key, uint256 _value);
    function SetLogic(string memory _key, uint256 _value) external {        logic[_key] = _value;        emit logicSetted(_key, _value);    }
    function GetLogic(string memory _key) public view returns (uint256){        return logic[_key];    }}
   复制代码
 
 
// contract/MyLogicV2.sol
// SPDX-License-Identifier: MITpragma solidity ^0.8.0;
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
contract MyLogicV2 is Initializable, UUPSUpgradeable, OwnableUpgradeable {    function initialize() initializer public {      __Ownable_init();      __UUPSUpgradeable_init();    }
    /// @custom:oz-upgrades-unsafe-allow constructor    constructor() initializer {}
    function _authorizeUpgrade(address) internal override onlyOwner {}
    mapping(string => uint256) private logic;
    event logicSetted(string indexed _key, uint256 _value);
    function SetLogic(string memory _key, uint256 _value) external {        logic[_key] = _value;        emit logicSetted(_key, _value);    }
    function GetLogic(string memory _key) public view returns (uint256){        return logic[_key]+100;    }}
   复制代码
 MyLogicV1 实现了一个对于 logic 的 map 的写入方法和读取方法,MyLogicV2 只是改写了 MyLogicV1 的 GetLogic 方法中的处理逻辑,使得每次返回的值加了 100
编写测试文件在 test 目录下创建生成名称为 update-test 的.js 文件并进行代码编写
 
// test/update-test.tsconst { expect } = require('chai');const { ethers, upgrades } = require('hardhat');
let myLogicV1;let myLogicV2;
describe('uups mode upgrade', function () {  it('deploys', async function () {    const MyLogicV1 = await ethers.getContractFactory('MyLogicV1');      myLogicV1 = (await upgrades.deployProxy(MyLogicV1, {kind: 'uups'}));      console.log(myLogicV1.address);  })  it('myLogicV1 set', async function () {    await myLogicV1.SetLogic("aa", 1);    expect((await myLogicV1.GetLogic("aa")).toString()).to.equal('1');  })  it('upgrades', async function () {    const MyLogicV2 = await ethers.getContractFactory('MyLogicV2');      myLogicV2 = (await upgrades.upgradeProxy(myLogicV1, MyLogicV2));      console.log(myLogicV2.address);  })  it('myLogicV2 get', async function () {      expect((await myLogicV2.GetLogic("aa")).toString()).to.equal('101');  })})
   复制代码
 测试文件步骤详解
- deploys: 调用 upgrades 中的 deployProxy 方法以 uups 的方式部署 MyLogicV1 合约 
- myLogicV1 set:调用 MyLogicV1 合约中的 SetLogic 方法将值传入,并调用 GetLogic 方法确认返回值为 1 
- upgrades:调用 upgrades 中的 upgradeProxy 方法升级 
- myLogicV2 get:调用 MyLogicV2 合约中的 GetLogic 方法确认返回值为 101 
编辑配置文件编辑 hardhat.config.js 文件
 
require("@nomiclabs/hardhat-waffle");require('@openzeppelin/hardhat-upgrades');
// This is a sample Hardhat task. To learn how to create your own go to// https://hardhat.org/guides/create-task.htmltask("accounts", "Prints the list of accounts", async (taskArgs, hre) => {  const accounts = await hre.ethers.getSigners();
  for (const account of accounts) {    console.log(account.address);  }});
// You need to export an object to set up your config// Go to https://hardhat.org/config/ to learn more
/** * @type import('hardhat/config').HardhatUserConfig */module.exports = {  solidity: {    version: "0.8.4",    settings: {      optimizer: {        enabled: true,        runs: 200      }    }  },  mocha: { timeout: 60000 }};
   复制代码
 执行项目
1.执行编译合约
npx hardhat compile
2.启动本地测试节点
npx hardhat node
3.新开一个窗口运行测试文件
npx hardhat test test/update-test.js --network localhost
测试通过,合约地址都为代理合约地址,测试说明成功升级改写了 GetLogic 中的处理逻辑,使得每次返回的值都在原来的基础上多加了 100
参考
Hardhat 官方文档:Overview | Hardhat | Ethereum development environment for professionals by Nomic Foundation[1]
OpenZeppelin 官方文档:Using with Upgrades - OpenZeppelin Docs[2]
References
[1] Overview | Hardhat | Ethereum development environment for professionals by Nomic Foundation: https://hardhat.org/getting-started/
[2] Using with Upgrades - OpenZeppelin Docs: https://docs.openzeppelin.com/contracts/4.x/upgradeable
评论