老道的学习日志。可升级合约的一个简单实践
老道的学习日志
众所周知,智能合约部署上链后代码不可改变,但总是会有需要加功能、bug维护、优化gas之类的时候,而且还要减少用户迁移带来的成本。因此有了'可升级'合约,而可升级并不是说修改旧的合约,而是通过将状态与逻辑分离,在保持同一地址对外服务的前提下替换逻辑合约。
fallback
中使用delegatecall
将调用委托给逻辑合约upgradeTo
)才会被执行。常与 ProxyAdmin 管理合约配合使用,优点是维护直观、生态成熟,但部署与运行略重。 upgradeTo
等),代理更轻;需谨慎实现授权钩子,确保只有预期角色可升级。相较 Transparent,UUPS 部署成本更低,但对实现者规范性要求更高。 一句话总结:新项目优先 UUPS(轻量、直观);若你需要与现有工具链/审计实践完全对齐,也可选 Transparent。
code/
├── contracts/
│ ├── MyUUPSV1.sol # V1实现合约
│ ├── MyUUPSV2.sol # V2实现合约(新功能)
│ └── SimpleProxy.sol # 简化的代理合约
├── scripts/
│ └── upgrade-demo.ts # 完整的升级演示脚本
├── test/
│ └── UUPS-Upgrade.test.ts # 全面的测试套件
└── ignition/modules/
├── MyUUPSV1.ts # V1部署模块
└── MyUUPSV2.ts # V2升级模块
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
contract MyUUPSV1 is Initializable, UUPSUpgradeable, OwnableUpgradeable {
uint256 public value;
function initialize(uint256 _value) public initializer {
__Ownable_init(msg.sender);
__UUPSUpgradeable_init();
value = _value;
}
function setValue(uint256 _value) public {
value = _value;
}
function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import "./MyUUPSV1.sol";
contract MyUUPSV2 is MyUUPSV1 {
// 新增状态变量(注意存储布局)
string public name;
uint256 public counter;
// V2的初始化函数
function initializeV2(string memory _name) public reinitializer(2) {
name = _name;
counter = 0;
}
// 新增功能:计数器操作
function increment() public {
counter++;
}
function decrement() public {
require(counter > 0, "Counter cannot be negative");
counter--;
}
// 新增功能:设置名称
function setName(string memory _name) public {
name = _name;
}
// 版本信息
function getVersion() public pure returns (string memory) {
return "v2.0.0";
}
}
reinitializer(2)
初始化新变量function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
// 部署实现合约
const MyUUPSV1 = await ethers.getContractFactory("MyUUPSV1");
const v1Implementation = await MyUUPSV1.deploy();
// 编码初始化数据
const initData = v1Implementation.interface.encodeFunctionData("initialize", [100]);
// 部署代理
const SimpleProxy = await ethers.getContractFactory("SimpleProxy");
const proxy = await SimpleProxy.deploy(v1Implementation.target, initData);
// 部署V2实现
const MyUUPSV2 = await ethers.getContractFactory("MyUUPSV2");
const v2Implementation = await MyUUPSV2.deploy();
// 编码V2初始化数据
const initV2Data = v2Implementation.interface.encodeFunctionData("initializeV2", ["UUPS Demo Contract"]);
// 执行升级
const v1Proxy = MyUUPSV1.attach(proxy.target);
await v1Proxy.upgradeToAndCall(v2Implementation.target, initV2Data);
describe("UUPS Upgrade Test", function () {
it("应该成功部署V1合约", async function () {
// 部署测试
});
it("应该能够使用V1功能", async function () {
// V1功能测试
});
it("应该成功升级到V2", async function () {
// 升级测试
});
it("应该能够使用V2的新功能", async function () {
// V2新功能测试
});
it("应该保持V1的原有功能", async function () {
// 向后兼容性测试
});
it("计数器不应该允许负数", async function () {
// 边界条件测试
});
});
🚀 开始UUPS可升级合约演示...
📦 部署V1实现合约... ✅
📦 部署代理合约... ✅
🔍 测试V1功能: 初始值: 100, 设置新值后: 200 ✅
📦 部署V2实现合约... ✅
⬆️ 升级到V2... ✅
🔧 初始化V2新功能... ✅
🔍 验证升级后的状态:
升级后的值 (应该保持不变): 200 ✅
版本信息: v2.0.0 ✅
合约名称: UUPS Demo Contract ✅
计数器初始值: 0 ✅
🆕 测试V2新功能:
增加计数器后: 2 ✅
减少计数器后: 1 ✅
更新后的名称: 升级成功的UUPS合约 ✅
🔄 验证V1功能仍然可用: 最终值: 500 ✅
reinitializer
为新版本初始化新变量💡 实战总结:UUPS模式通过将升级逻辑下沉到实现合约,实现了轻量级的可升级方案。关键在于正确处理存储布局、权限控制和状态迁移。
一句话总结:UUPS + 多签 + Timelock 是当前最实用的可升级治理组合:代码上用初始化器替代构造器、架构上用代理解耦状态与逻辑、流程上用多签与延时把风险“物理隔离”。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!