hardhat 可升级合约
基于 hardhat 框架进行合约开发,使用 OpenZeppelin 库实现可升级的智能合约
搭建开发环境
- 操作系统:Linux
- node:23.1.0
- yarn:1.22.22
- 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
初始化环境
yarn 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、Transparent(透明代理)(代理合约管理升级)
编写可升级的智能合约,创建代币合约MyTokenV1.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28;
import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
contract MyTokenV1 is ERC20Upgradeable {
function initialize() public initializer {
__ERC20_init("MyTokenV1", "MTV1");
}
}
创建部署脚本scripts/deployV1.js
const { ethers, upgrades } = require("hardhat");
async function main() {
const MyTokenV1 = await ethers.getContractFactory("MyTokenV1");
const myTokenV1 = await upgrades.deployProxy(MyTokenV1);
await myTokenV1.waitForDeployment();
console.log("MyToken V1 deployed to:", await myToken.getAddress());
}
main();
启动本地虚拟节点并部署合约
yarn hardhat node
# 打开新的终端后执行部署命令
yarn hardhat run scripts/deployV1.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 initialize() external initializer {
__ERC20_init("MyTokenV2", "MTV2");
}
function owner() public pure returns (address) {
return address(0);
}
function name() public pure override returns (string memory) {
return "MyTokenV2";
}
function symbol() public pure override returns (string memory) {
return "MTV2";
}
}
创建升级脚本scripts/upgradeV2.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 to V2");
}
main();
执行升级合约脚本
yarn hardhat run scripts/upgradeV2.js --network localhost
验证合约升级的效果
# 打开hardhat本地节点控制台
yarn hardhat console --network localhost
# 在控制台输入以下命令进行确认
> const MyToken = await ethers.getContractFactory("MyTokenV2");
undefined
> const myToken = await MyToken.attach("0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512");
undefined
> await myToken.name();
'MyTokenV2'
可以看到输出的代币名称为 MyTokenV2,表示合约升级成功(V1 版本的代币名称为 MyTokenV1)
2、UUPS(通用可升级代理标准)(实现合约管理升级)
合约需要继承 UUPSUpgradeable 合约,部署和升级的脚本和透明代理的一样无需改动,openzeppelin 升级工具会自动选择合适的代理部署。
编写可升级的智能合约,创建代币合约MyTokenV1.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28;
import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
contract MyTokenV1 is
Initializable,
OwnableUpgradeable,
UUPSUpgradeable,
ERC20Upgradeable
{
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
function initialize(address initialOwner) external initializer {
__Ownable_init(initialOwner);
__UUPSUpgradeable_init();
__ERC20_init("MyTokenV1", "MTV1");
}
function _authorizeUpgrade(
address newImplementation
) internal override onlyOwner {}
}
增加 V2 版本的合约MyTokenV2.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28;
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
contract MyTokenV2 is OwnableUpgradeable, UUPSUpgradeable, ERC20Upgradeable {
function initialize(address initialOwner) external initializer {
__Ownable_init(initialOwner);
__ERC20_init("MyTokenV2", "MTV2");
}
function _authorizeUpgrade(address) internal pure override {
assert(false);
}
function owner() public pure override returns (address) {
return address(0);
}
function name() public pure override returns (string memory) {
return "MyTokenV2";
}
function symbol() public pure override returns (string memory) {
return "MTV2";
}
}
验证合约升级的效果
# 打开hardhat本地节点控制台
yarn hardhat console --network localhost
# 在控制台输入以下命令进行确认
> const MyToken = await ethers.getContractFactory("MyTokenV2");
undefined
> const myToken = await MyToken.attach("0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512");
undefined
> await myToken.name();
'MyTokenV2'
> await myToken.owner();
'0x0000000000000000000000000000000000000000'
可以看到输出的代币名称为 MyTokenV2,表示合约升级成功(V1 版本的代币名称为 MyTokenV1)
3、 Beacon(信标代理)(批量升级合约)
编写可升级的智能合约,创建代币合约MyTokenV1.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28;
import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
contract MyTokenV1 is ERC20Upgradeable {
function initialize() public initializer {
__ERC20_init("MyTokenV1", "MTV1");
}
}
创建部署脚本scripts/deployV1.js
const { ethers, upgrades } = require("hardhat");
async function main() {
const MyTokenV1 = await ethers.getContractFactory("MyTokenV1");
const beacon = await upgrades.deployBeacon(MyTokenV1);
await beacon.waitForDeployment();
console.log("Beacon deployed to:", await beacon.getAddress());
const myTokenV1 = await upgrades.deployBeaconProxy(beacon, MyTokenV1);
await myTokenV1.waitForDeployment();
console.log("MyToken V1 deployed to:", await myTokenV1.getAddress());
}
main();
启动本地虚拟节点并部署合约
yarn hardhat node
# 打开新的终端后执行部署命令
yarn hardhat run scripts/deployV1.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 initialize() external initializer {
__ERC20_init("MyTokenV2", "MTV2");
}
function owner() public pure returns (address) {
return address(0);
}
function name() public pure override returns (string memory) {
return "MyTokenV2";
}
function symbol() public pure override returns (string memory) {
return "MTV2";
}
}
创建升级脚本scripts/upgradeV2.js
const { ethers, upgrades } = require("hardhat");
async function main() {
// 这里填入之前部署的信标合约地址(上一步部署时控制台输出的)
const BEACON_ADDRESS = "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0";
const MyTokenV2 = await ethers.getContractFactory("MyTokenV2");
await upgrades.upgradeBeacon(BEACON_ADDRESS, MyTokenV2);
console.log("Beacon upgraded");
}
main();
执行升级合约脚本
yarn hardhat run scripts/upgradeV2.js --network localhost
验证合约升级的效果
# 打开hardhat本地节点控制台
yarn hardhat console --network localhost
# 在控制台输入以下命令进行确认
> const MyToken = await ethers.getContractFactory("MyTokenV2");
undefined
> const myToken = await MyToken.attach("0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9");
undefined
> await myToken.name();
'MyTokenV2'
可以看到输出的代币名称为 MyTokenV2,表示合约升级成功(V1 版本的代币名称为 MyTokenV1)
参考文档
- openzeppelin 合约文档:https://docs.openzeppelin.com/contracts
- 升级插件文档:https://docs.openzeppelin.com/upgrades-plugins
- 代理(升级)合约:https://docs.openzeppelin.com/contracts/api/proxy#transparent-vs-uups
- 完整的开发流程指南:https://docs.openzeppelin.com/learn
- 升级合约总结:https://blog.openzeppelin.com/the-state-of-smart-contract-upgrades