文章目录
- 前言
-
- [智能合约 -透明可升级合约[ hardhat、openzeppelin 、ethers ]的演示 demo](#智能合约 -透明可升级合约[ hardhat、openzeppelin 、ethers ]的演示 demo)
-
- [1. 测试内容](#1. 测试内容)
- [2. 合约源码](#2. 合约源码)
- [3. 脚本部署测试](#3. 脚本部署测试)
-
- [3.1. 执行代理合约[逻辑合约BoxV1]的部署脚本](#3.1. 执行代理合约[逻辑合约BoxV1]的部署脚本)
- [3.2. 执行代理合约[逻辑合约BoxV2]的升级脚本:](#3.2. 执行代理合约[逻辑合约BoxV2]的升级脚本:)
前言
如果您觉得有用的话,记得给博主点个赞,评论,收藏一键三连啊,写作不易啊^ _ ^。
而且听说点赞的人每天的运气都不会太差,实在白嫖的话,那欢迎常来啊!!!
智能合约 -透明可升级合约[ hardhat、openzeppelin 、ethers ]的演示 demo
版本:
solidity ^0.8.28、hardhat 2.28.4、openzeppelin 5.6.1 ethers 6.16.0
1. 测试内容
测试内容: BoxV2合约升级内容为覆盖BoxV1合约的call()函数。
注意的是升级实现合约时,不需要重新部署代理合约,代理合约地址始终不变,仅需将代理指向新的实现合约地址即可。
所以针对 TPUProxy 这类可升级代理,必须写两个核心脚本:初次部署脚本(代理 + 实现合约 V1 的绑定初始化)和升级脚本(部署实现合约 V2 + 让代理指向新实现)。
2. 合约源码
下面是两个逻辑合约和代理合约的源码:
BoxV1 合约:
bash
// 声明代码遵循 MIT 开源许可证
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import "@openzeppelin/contracts/proxy/utils/Initializable.sol";
contract BoxV1 is Initializable{
uint public x;
// initialize 替代constructor
function initialize(uint _val) external initializer{
x = _val;
}
function call() external {
x=x+1;
}
// 逻辑合约的初始化数据:供代理合约使用
function showInvoke() external pure returns (bytes memory){
return abi.encodeWithSelector(this.initialize.selector, 1);
}
}
BoxV2 合约:
bash
// 声明代码遵循 MIT 开源许可证
// SPDX-License-Identifier: MIT
import "@openzeppelin/contracts/proxy/utils/Initializable.sol";
pragma solidity ^0.8.28;
contract BoxV2 is Initializable{
uint public x;
// initialize 替代constructor
function initialize(uint _val) external initializer{
x = _val;
}
function call() external {
x=x+2;
}
// 逻辑合约的初始化数据:供代理合约使用
function showInvoke() external pure returns (bytes memory){
return abi.encodeWithSelector(this.initialize.selector, 1);
}
}
TPUProxy 自定义透明可升级代理合约:
bash
// 声明代码遵循 MIT 开源许可证,SPDX 标准许可证标识
// SPDX-License-Identifier: MIT
// 透明可升级代理合约,继承 OpenZeppelin 官方透明代理基础合约
pragma solidity ^0.8.28; // 限定 Solidity 编译器版本为 0.8.28 及兼容版本
// 引入 OpenZeppelin 官方透明可升级代理核心合约,提供代理底层逻辑
import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
// 自定义透明可升级代理合约,继承 TransparentUpgradeableProxy 实现功能扩展
contract TPUProxy is TransparentUpgradeableProxy {
/**
* @dev 构造函数,继承并调用父类 TransparentUpgradeableProxy 构造函数
* @param _logic 实现合约地址(代理指向的业务逻辑合约)
* @param initialOwner 代理合约初始管理员地址-区块链账户地址(拥有代理升级、管理权限)
* @param _data 部署时传递给实现合约的初始化数据(通常是初始化函数的编码数据)
*/
constructor(address _logic, address initialOwner, bytes memory _data) payable TransparentUpgradeableProxy(_logic, initialOwner, _data) {
// 无自定义逻辑,仅透传参数调用父类构造函数完成代理初始化
}
/**
* @dev 外部只读方法,查询代理合约的管理员地址
* @return address 代理管理员地址(拥有 proxyAdmin 权限的账户)
* 封装父类内部的 _proxyAdmin() 方法,对外提供查询接口
*/
function proxyAdmin() external view returns(address) {
return _proxyAdmin();
}
/**
* @dev 外部只读方法,查询代理当前指向的实现合约地址
* @return address 业务逻辑实现合约的当前地址
* 封装父类内部的 _implementation() 方法,对外提供查询接口
*/
function getImplementation() external view returns(address) {
return _implementation();
}
receive() external payable {
revert("TPUProxy: do not send ETH directly"); // 回滚交易并抛出明确错误信息
}
}
3. 脚本部署测试
3.1. 执行代理合约[逻辑合约BoxV1]的部署脚本
源码:
bash
// Hardhat 和 ethers.js 的「桥梁」,负责注册插件,让两者能协同工作;
import hre from "hardhat";
import "@nomicfoundation/hardhat-ethers";
import "@openzeppelin/hardhat-upgrades";
const CONTRACT_NAME = "BoxV1";
// 部署合约
async function contract(){
console.log(`开始部署 ${CONTRACT_NAME} 代理合约...`);
// 获取合约工厂
const factory = await hre.ethers.getContractFactory("BoxV1");
const proxy = await hre.upgrades.deployProxy(factory,[1],{
initializer: "initialize"
});
// 获取代理合约地址方式
const contractAddress = await proxy.getAddress();
return { contractInstance: proxy, contractAddress }; // 同时返回实例和地址,方便后续调用方法
}
async function deploy() {
const { contractInstance, contractAddress } = await contract();
console.log(`${CONTRACT_NAME} 部署完成!`);
console.log(`代理合约地址: ${contractAddress}`);
console.log(`验证: 调用代理合约-${CONTRACT_NAME} 的 call 方法`);
try {
await contractInstance.call();
console.log(`验证: x:`, await contractInstance.x());
} catch (error) {
console.error(`代理合约-${CONTRACT_NAME}的 方法调用失败:`, error);
process.exit(1);
}
}
// 启动 本地网络 npx hardhat node
deploy()
.then(() => process.exit(0))
.catch((error) => {
console.error("部署失败:", error);
process.exit(1);
});
执行脚本。
npx hardhat run scripts/upgradable/deploy-BoxV1Proxy.ts --network localhost

0x0165878A594ca255338adfa4d48449f69242Eb8F 是合约代理地址,记下来,这个会在升级脚本的时候会用到。
下面是call合约的方法:
bash
function call() external {
x=x+1;
}
初始化的x为1,调用call函数后,加1,最后得出x为2,正确。
3.2. 执行代理合约[逻辑合约BoxV2]的升级脚本:
源码:
bash
// Hardhat 和 ethers.js 的「桥梁」,负责注册插件,让两者能协同工作;
import hre from "hardhat";
import "@nomicfoundation/hardhat-ethers";
import "@openzeppelin/hardhat-upgrades";
const CONTRACT_NAME = "BoxV2";
const PROXY_ADDRESS = "0x0165878A594ca255338adfa4d48449f69242Eb8F";
// 部署合约
async function contract(){
console.log(`开始升级 ${CONTRACT_NAME} 代理合约...`);
// 获取合约工厂
const factory = await hre.ethers.getContractFactory("BoxV2");
const proxy = await hre.upgrades.upgradeProxy(PROXY_ADDRESS,factory);
// 获取代理合约地址方式
const contractAddress = await proxy.getAddress();
return { contractInstance: proxy, contractAddress }; // 同时返回实例和地址,方便后续调用方法
}
async function deploy() {
const { contractInstance, contractAddress } = await contract();
console.log(`${CONTRACT_NAME} 部署完成!`);
console.log(`代理合约地址: ${contractAddress}`);
console.log(`验证: 调用代理合约-${CONTRACT_NAME} 的 call 方法`);
try {
await contractInstance.call();
console.log(`验证: x:`, await contractInstance.x());
} catch (error) {
console.error(`代理合约-${CONTRACT_NAME}的 方法调用失败:`, error);
process.exit(1);
}
}
// 启动 本地网络 npx hardhat node
deploy()
.then(() => process.exit(0))
.catch((error) => {
console.error("部署失败:", error);
process.exit(1);
});
执行脚本:
npx hardhat run scripts/upgradable/upgrade-TPUProxy.ts --network localhost

因为原来的x=2,看一下BoxV2合约的call方法:
bash
function call() external {
x=x+2;
}
调用call()函数后,加2,最后得出x=4,验证通过。