可升级合约从入门到上线:UUPS + 多签 + Timelock 一把梭

老道的学习日志。可升级合约的一个简单实践

老道的学习日志

是什么

众所周知,智能合约部署上链后代码不可改变,但总是会有需要加功能、bug维护、优化gas之类的时候,而且还要减少用户迁移带来的成本。因此有了'可升级'合约,而可升级并不是说修改旧的合约,而是通过将状态与逻辑分离,在保持同一地址对外服务的前提下替换逻辑合约。

image.png

怎么搞

  • 代理模式:
    • 用户与代理合约进行交互
    • 代理在fallback中使用delegatecall将调用委托给逻辑合约
    • 逻辑代码在代理的执行上下文里运行,因此状态(存储)保存在代理,而逻辑可替换,实现“状态-逻辑解耦”。设计时要特别注意存储冲突与函数选择器冲突(尤其是管理函数与业务函数的选择器重叠问题)

主流模式

  • Transparent 透明代理:用户调用都转发给逻辑合约;只有管理员地址调用时,代理自身的管理函数(如 upgradeTo)才会被执行。常与 ProxyAdmin 管理合约配合使用,优点是维护直观、生态成熟,但部署与运行略重。

image-1.png

  • UUPS(EIP-1822):升级逻辑下沉到逻辑合约自身(实现 upgradeTo 等),代理更轻;需谨慎实现授权钩子,确保只有预期角色可升级。相较 Transparent,UUPS 部署成本更低,但对实现者规范性要求更高。

一句话总结:新项目优先 UUPS(轻量、直观);若你需要与现有工具链/审计实践完全对齐,也可选 Transparent。

上手

UUPS 实战项目

项目结构

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升级模块

核心合约实现

MyUUPSV1.sol - 初始版本

// 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 {}
}

MyUUPSV2.sol - 升级版本

// 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";
    }
}

关键实现要点

1. 存储布局兼容性

  • V2继承V1,确保存储槽位置不变
  • 新状态变量添加在原有变量之后
  • 使用reinitializer(2)初始化新变量

2. 升级授权

function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
  • 只有合约所有者可以执行升级
  • 在V1和V2中都需要实现此函数

3. 代理部署

// 部署实现合约
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);

4. 升级流程

// 部署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 ✅

测试结果

  • ✅ 6个测试全部通过
  • ✅ 状态完美保持
  • ✅ 新功能正常工作
  • ✅ 向后兼容性良好

核心优势

  1. 状态保持 - 升级后V1的状态完全保持
  2. 新功能添加 - V2成功添加了计数器和名称功能
  3. 向后兼容 - V1的所有功能在升级后依然可用
  4. 安全升级 - 只有合约所有者可以执行升级
  5. 完整测试 - 覆盖了所有升级场景和边界条件

最佳实践

  1. 存储布局 - 新版本只能在末尾添加状态变量
  2. 初始化器 - 使用reinitializer为新版本初始化新变量
  3. 权限控制 - 严格控制升级权限,通常限制为合约所有者
  4. 测试覆盖 - 全面测试升级前后的功能和状态保持
  5. 渐进升级 - 分步骤验证每个升级环节

💡 实战总结:UUPS模式通过将升级逻辑下沉到实现合约,实现了轻量级的可升级方案。关键在于正确处理存储布局、权限控制和状态迁移。

一句话总结UUPS + 多签 + Timelock 是当前最实用的可升级治理组合:代码上用初始化器替代构造器、架构上用代理解耦状态与逻辑、流程上用多签与延时把风险“物理隔离”

参考文献

  1. 如何部署和使用可升级的智能合约
  2. https://mirror.xyz/0x59749c32329F8B52A4Ef7a1b0A83Aa94B9D20f2F/zbn1gLdKPED6V7V3C5Ik6BO1UR1GpvD-xHDAUzFTVU0 (感谢图片,这个图片真的很清晰)
  3. https://learnblockchain.cn/article/4880
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
坐忘道
坐忘道
0xbf65...58d8
江湖只有他的大名,没有他的介绍。