hardhat 可升级合约

admin
admin 2024年11月09日
  • 在其它设备中阅读本文章

基于 hardhat 框架进行合约开发,使用 OpenZeppelin 库实现可升级的智能合约

搭建开发环境

  1. 操作系统:Linux
  2. node:23.1.0
  3. yarn:1.22.22
  4. hardhat:2.22.17

1、创建空白项目

mkdir demo && cd demo
yarn init -y

可更改 yarn 的源(默认的太慢还可能导致安装包失败):

yarn config set registry https://registry.npmmirror.com

2、添加 hardhat

yarn add -D hardhat

选择创建 JavaScript 项目,并按照提示创建相关文件和安装依赖包

888    888                      888 888               888
888    888                      888 888               888
888    888                      888 888               888
8888888888  8888b.  888d888 .d88888 88888b.   8888b.  888888
888    888     "88b 888P"  d88" 888 888 "88b     "88b 888
888    888 .d888888 888    888  888 888  888 .d888888 888
888    888 888  888 888    Y88b 888 888  888 888  888 Y88b.
888    888 "Y888888 888     "Y88888 888  888 "Y888888  "Y888

Welcome to Hardhat v2.22.17

✔ What do you want to do? · Create a JavaScript project
✔ Hardhat project root: · /home/swwx/demo
✔ Do you want to add a .gitignore? (Y/n) · y
✔ Do you want to install this sample project's dependencies with yarn (@nomicfoundation/hardhat-network-helpers @nomicfoundation/hardhat-verify chai hardhat-gas-reporter solidity-coverage @nomicfoundation/hardhat-ignition @nomicfoundation/hardhat-toolbox @nomicfoundation/hardhat-chai-matchers @nomicfoundation/hardhat-ethers ethers @typechain/hardhat typechain @typechain/ethers-v6 @nomicfoundation/hardhat-ignition-ethers)? (Y/n) · y

初始化时自动配置了一个示例合约,可以运行以下命令查看效果

yarn hardhat compile:编译智能合约
yarn hardhat test:执行合约测试
yarn hardhat node:启动一个虚拟的本地测试节点
yarn hardhat help:查看帮助信息

3、安装 openzeppelin 升级插件和合约库

yarn add -D @openzeppelin/hardhat-upgrades 
yarn add @openzeppelin/contracts @openzeppelin/contracts-upgradeable

修改 hardhat 配置文件hardhat.config.js,导入插件

require("@nomicfoundation/hardhat-toolbox");
require('@openzeppelin/hardhat-upgrades');

/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
  solidity: "0.8.28",
};

开发升级合约

部署到 EVM 的合约代码是不能修改的,一般可以使用代理合约的方式实现合约升级,代理合约保存数据,逻辑合约保存可执行代码,并使用 delegatecall 字节码连接在一起。
一般来说,升级代码实现有两种方式,一是在将升级管理代码放在代理合约中,但这样会增加代理合约的大小,增加部署代理的成本,二是将升级代码放到实现合约中,由代理合约使用 delegatecall 字节码进行调用,这样会增加逻辑合约的大小,但是可以在将来升级的时候禁用升级接口,实现不再更新合约的功能。

初始的 Hardhat 项目默认有一个 Lock 合约及其测试,可以参考修改或者删除

1、透明代理(代理合约管理升级)

编写可升级的智能合约,创建代币合约MyToken.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28;

import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";

contract MyToken is ERC20Upgradeable {
    function initialize() public initializer {
        __ERC20_init("MyToken", "MT");
    }
}

创建部署脚本scripts/deploy.js

const { ethers, upgrades } = require("hardhat");

async function main() {
  const MyToken = await ethers.getContractFactory("MyToken");
  const myToken = await upgrades.deployProxy(MyToken);
  await myToken.waitForDeployment();
  console.log("MyToken deployed to:", await myToken.getAddress());
}

main();

启动本地虚拟节点并部署合约

yarn hardhat node

# 打开新的终端后执行部署命令
yarn hardhat run scripts/deploy.js --network localhost

增加 V2 版本的合约MyTokenV2.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28;

import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";

contract MyTokenV2 is ERC20Upgradeable {
    function name() public view override returns (string memory) {
        return "MyTokenV2";
    }
}

创建升级脚本scripts/upgrade.js

const { ethers, upgrades } = require("hardhat");

async function main() {
  // 这里填入之前部署的代理合约地址(上一步部署时控制台输出的)
  const V1_ADDRESS = "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512";
  const MyTokenV2 = await ethers.getContractFactory("MyTokenV2");
  await upgrades.upgradeProxy(V1_ADDRESS, MyTokenV2);
  console.log("MyToken upgraded");
}

main();

执行升级合约脚本

yarn hardhat run scripts/upgrade.js --network localhost

验证合约升级的效果

# 打开hardhat本地节点控制台
yarn hardhat console --network localhost

# 在控制台输入以下命令进行确认
> const MyToken = await ethers.getContractFactory("MyToken");
undefined
> const myToken = await MyToken.attach("0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512");
undefined
> await myToken.name();
'MyTokenV2'

可以看到输出的代币名称为 MyTokenV2,表示合约升级成功(V1 版本的代币名称为 MyToken)

参考文档

  1. openzeppelin 合约文档:https://docs.openzeppelin.com/contracts
  2. 升级插件文档:https://docs.openzeppelin.com/upgrades-plugins
  3. 代理(升级)合约:https://docs.openzeppelin.com/contracts/api/proxy#transparent-vs-uups
  4. 完整的开发流程指南:https://docs.openzeppelin.com/learn
  5. 升级合约总结:https://blog.openzeppelin.com/the-state-of-smart-contract-upgrades