Solidity合约版本控制:让你的区块链代码随心升级不翻车

Solidity里一个超硬核的主题——合约版本控制!以太坊智能合约一旦部署,默认是“铁打不动”,但现实中业务需求总在变,bug也得修,咋办?版本控制就是救星!它能让你在不换地址、不丢数据的情况下,把合约逻辑升级得像换皮肤一样丝滑!这篇干货会用大白话把Solidity的版本控制技巧讲得明明白白,从基础代理模式到OpenZeppelin的透明代理、UUPS,再到多签控制和事件追踪,配合Hardhat测试和完整代码,带你一步步打造稳如老狗的版本控制方案。重点是硬核知识点,废话少说,直接上技术细节,帮你把合约升级玩得贼6!

核心概念

先搞清楚几个关键点:

  • 不可变性:以太坊智能合约部署后,字节码和存储通常不可更改。
  • 代理模式:通过代理合约(Proxy)分离存储和逻辑,代理存数据,逻辑合约存代码,升级时只换逻辑合约地址。
  • 委托调用(delegatecall):代理通过delegatecall调用逻辑合约,逻辑合约的代码在代理的存储上下文执行。
  • 存储布局:代理和逻辑合约的存储槽必须一致,否则数据会乱套。
  • 版本控制机制:更新代理指向的逻辑合约地址,实现功能升级,同时保留版本历史。
  • 安全风险
    • 存储冲突:新逻辑合约的存储布局变化可能覆盖旧数据。
    • 权限控制:谁能升级?没限制可能被黑客搞乱。
    • 初始化:新逻辑合约需正确初始化。
    • 版本追踪:记录每次升级的版本号和地址。
  • 工具
    • Solidity 0.8.x:自带溢出/下溢检查,安全可靠。
    • OpenZeppelin:提供透明代理(Transparent Proxy)和UUPS(Universal Upgradeable Proxy Standard)。
    • Hardhat:开发和测试工具,方便部署和验证。
  • 事件:通过事件记录版本变更,便于链上追踪。

咱们用Solidity 0.8.20,结合OpenZeppelin和Hardhat,从简单代理到多签升级,逐步实现安全的版本控制。

环境准备

用Hardhat搭建开发环境,写和测试合约。

mkdir version-control-demo cd version-control-demo npm init -y npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox @openzeppelin/contracts @openzeppelin/contracts-upgradeable npm install ethers

初始化Hardhat:

npx hardhat init

选择TypeScript项目,安装依赖:

npm install --save-dev ts-node typescript @types/node @types/mocha

目录结构:

version-control-demo/ ├── contracts/ │ ├── SimpleProxy.sol │ ├── LogicV1.sol │ ├── LogicV2.sol │ ├── TransparentProxy.sol │ ├── UUPSProxy.sol │ ├── MultiSigUpgrade.sol ├── scripts/ │ ├── deploy.ts ├── test/ │ ├── VersionControl.test.ts ├── hardhat.config.ts ├── tsconfig.json ├── package.json

tsconfig.json

{ "compilerOptions": { "target": "ES2020", "module": "CommonJS", "strict": true, "esModuleInterop": true, "moduleResolution": "node", "resolveJsonModule": true, "outDir": "./dist", "rootDir": "./" }, "include": ["hardhat.config.ts", "scripts", "test"] }

hardhat.config.ts

import { HardhatUserConfig } from "hardhat/config"; import "@nomicfoundation/hardhat-toolbox"; const config: HardhatUserConfig = { solidity: "0.8.20", networks: { hardhat: { chainId: 1337, }, }, }; export default config;

跑本地节点:

npx hardhat node

基础代理合约

先搞一个简单的代理合约,弄清楚delegatecall和版本控制基础。

合约代码

contracts/LogicV1.sol

// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; contract LogicV1 { address public owner; uint256 public value; uint256 public version; event Upgraded(address indexed implementation, uint256 version); function initialize(address _owner) public { require(owner == address(0), "Already initialized"); owner = _owner; version = 1; } function setValue(uint256 _value) public { require(msg.sender == owner, "Only owner"); value = _value; } function getValue() public view returns (uint256) { return value; } }

contracts/LogicV2.sol

// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; contract LogicV2 { address public owner; uint256 public value; uint256 public version; event Upgraded(address indexed implementation, uint256 version); function initialize(address _owner) public { require(owner == address(0), "Already initialized"); owner = _owner; version = 2; } function setValue(uint256 _value) public { require(msg.sender == owner, "Only owner"); value = _value * 2; // V2 doubles the value } function getValue() public view returns (uint256) { return value; } }

contracts/SimpleProxy.sol

// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; contract SimpleProxy { address public implementation; address public owner; uint256 public version; mapping(uint256 => address) public versionHistory; event Upgraded(address indexed implementation, uint256 version); constructor(address _implementation) { owner = msg.sender; implementation = _implementation; version = 1; versionHistory[1] = _implementation; emit Upgraded(_implementation, 1); } function upgrade(address _newImplementation) public { require(msg.sender == owner, "Only owner"); implementation = _newImplementation; version++; versionHistory[version] = _newImplementation; emit Upgraded(_newImplementation, version); } fallback() external payable { address impl = implementation; require(impl != address(0), "Implementation not set"); assembly { let ptr := mload(0x40) calldatacopy(ptr, 0, calldatasize()) let result := delegatecall(gas(), impl, ptr, calldatasize(), 0, 0) let size := returndatasize() returndatacopy(ptr, 0, size) switch result case 0 { revert(ptr, size) } default { return(ptr, size) } } } receive() external payable {} }

解析

  • LogicV1:基础逻辑合约,存ownervalueversion,提供initializesetValuegetValue,通过Upgraded事件记录版本。
  • LogicV2:升级版,setValue将输入值翻倍,存储布局与V1一致,version设为2。
  • SimpleProxy
    • implementation(逻辑合约地址)、ownerversionversionHistory(版本历史)。
    • upgrade:更新逻辑合约地址,增加版本号,记录历史,仅owner可调用。
    • fallback:用delegatecall转发调用到implementation,用汇编实现低级调用。
    • Upgraded事件:记录每次升级的地址和版本号。
  • delegatecall:逻辑合约的代码在代理的存储上下文执行,ownervalueversion存在代理合约。
  • 安全特性
    • onlyOwner限制升级权限。
    • 检查implementation不为空。
    • 初始化检查防止重复设置。
    • 版本历史记录便于追踪。

测试

test/VersionControl.test.ts

import { ethers } from "hardhat"; import { expect } from "chai"; import { SimpleProxy, LogicV1, LogicV2 } from "../typechain-types"; describe("SimpleProxy", function () { let proxy: SimpleProxy; let logicV1: LogicV1; let logicV2: LogicV2; let owner: any, addr1: any; beforeEach(async function () { [owner, addr1] = await ethers.getSigners(); const LogicV1Factory = await ethers.getContractFactory("LogicV1"); logicV1 = await LogicV1Factory.deploy(); await logicV1.deployed(); const ProxyFactory = await ethers.getContractFactory("SimpleProxy"); proxy = await ProxyFactory.deploy(logicV1.address); await proxy.deployed(); const LogicV2Factory = await ethers.getContractFactory("LogicV2"); logicV2 = await LogicV2Factory.deploy(); await logicV2.deployed(); const proxyAsLogicV1 = LogicV1Factory.attach(proxy.address); await proxyAsLogicV1.initialize(owner.address); }); it("should initialize correctly", async function () { const proxyAsLogicV1 = await ethers.getContractFactory("LogicV1").then(f => f.attach(proxy.address)); expect(await proxyAsLogicV1.owner()).to.equal(owner.address); expect(await proxyAsLogicV1.version()).to.equal(1); expect(await proxy.version()).to.equal(1); expect(await proxy.versionHistory(1)).to.equal(logicV1.address); }); it("should set and get value", async function () { const proxyAsLogicV1 = await ethers.getContractFactory("LogicV1").then(f => f.attach(proxy.address)); await proxyAsLogicV1.setValue(42); expect(await proxyAsLogicV1.getValue()).to.equal(42); }); it("should restrict setValue to owner", async function () { const proxyAsLogicV1 = await ethers.getContractFactory("LogicV1").then(f => f.attach(proxy.address)); await expect(proxyAsLogicV1.connect(addr1).setValue(42)).to.be.revertedWith("Only owner"); }); it("should upgrade to LogicV2", async function () { const proxyAsLogicV1 = await ethers.getContractFactory("LogicV1").then(f => f.attach(proxy.address)); await proxyAsLogicV1.setValue(42); await expect(proxy.upgrade(logicV2.address)) .to.emit(proxy, "Upgraded") .withArgs(logicV2.address, 2); const proxyAsLogicV2 = await ethers.getContractFactory("LogicV2").then(f => f.attach(proxy.address)); await proxyAsLogicV2.setValue(10); expect(await proxyAsLogicV2.getValue()).to.equal(20); expect(await proxyAsLogicV2.owner()).to.equal(owner.address); expect(await proxyAsLogicV2.version()).to.equal(2); expect(await proxy.versionHistory(2)).to.equal(logicV2.address); }); it("should restrict upgrade to owner", async function () { await expect(proxy.connect(addr1).upgrade(logicV2.address)).to.be.revertedWith("Only owner"); }); });

跑测试:

npx hardhat test
  • 解析
    • 部署:LogicV1SimpleProxy,通过代理调用initialize
    • 操作:设置value为42,验证存储在代理。
    • 升级:切换到LogicV2setValue(10)返回20,ownerversion保持正确。
    • 版本控制:versionHistory记录逻辑合约地址,Upgraded事件追踪升级。
    • 权限:非owner无法升级。
  • 存储:代理保存ownervalueversiondelegatecall确保逻辑合约操作代理存储。

存储冲突的坑

存储布局不一致会导致数据错乱,来看个错误例子。

错误示例

contracts/BadLogicV2.sol

// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; contract BadLogicV2 { uint256 public value; // Slot 0 (wrong order) address public owner; // Slot 1 (wrong order) uint256 public version; event Upgraded(address indexed implementation, uint256 version); function initialize(address _owner) public { require(owner == address(0), "Already initialized"); owner = _owner; version = 2; } function setValue(uint256 _value) public { require(msg.sender == owner, "Only owner"); value = _value * 2; } function getValue() public view returns (uint256) { return value; } }

测试:

test/VersionControl.test.ts(添加):

it("should fail with storage collision", async function () { const proxyAsLogicV1 = await ethers.getContractFactory("LogicV1").then(f => f.attach(proxy.address)); await proxyAsLogicV1.setValue(42); const BadLogicV2Factory = await ethers.getContractFactory("BadLogicV2"); const badLogicV2 = await BadLogicV2Factory.deploy(); await badLogicV2.deployed(); await proxy.upgrade(badLogicV2.address); const proxyAsBadLogicV2 = BadLogicV2Factory.attach(proxy.address); expect(await proxyAsBadLogicV2.owner()).to.not.equal(owner.address); });
  • 问题LogicV1owner(slot 0), value(slot 1), version(slot 2),BadLogicV2value(slot 0), owner(slot 1), version(slot 2),升级后owner被覆盖为value的值。
  • 解决:新逻辑合约必须保持存储布局一致,或用OpenZeppelin规范化。

透明代理(Transparent Proxy)

用OpenZeppelin的透明代理,解决管理员和用户调用冲突。

contracts/LogicV1.sol(更新):

// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; contract LogicV1 is Initializable, OwnableUpgradeable { uint256 public value; uint256 public version; event Upgraded(address indexed implementation, uint256 version); function initialize(address _owner) public initializer { __Ownable_init(_owner); version = 1; emit Upgraded(address(this), 1); } function setValue(uint256 _value) public onlyOwner { value = _value; } function getValue() public view returns (uint256) { return value; } }

contracts/LogicV2.sol(更新):

// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; contract LogicV2 is Initializable, OwnableUpgradeable { uint256 public value; uint256 public version; event Upgraded(address indexed implementation, uint256 version); function initialize(address _owner) public initializer { __Ownable_init(_owner); version = 2; emit Upgraded(address(this), 2); } function setValue(uint256 _value) public onlyOwner { value = _value * 2; } function getValue() public view returns (uint256) { return value; } }

contracts/TransparentProxy.sol

// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; contract TransparentProxy is TransparentUpgradeableProxy { constructor(address logic, address admin, bytes memory data) TransparentUpgradeableProxy(logic, admin, data) {} }

解析

  • LogicV1/V2
    • InitializableOwnableUpgradeable,支持代理初始化。
    • initializer确保初始化只执行一次。
    • Upgraded事件记录版本和逻辑地址。
  • TransparentProxy
    • 继承TransparentUpgradeableProxy
    • 构造函数设置逻辑合约、管理员地址、初始化数据。
    • 透明机制:管理员调用操作代理(如升级),普通用户调用转发到逻辑合约。
  • 安全特性
    • 防止管理员误调用逻辑合约。
    • OwnableUpgradeable管理权限。
    • 标准化的存储布局。
    • 版本通过事件和存储追踪。

测试

test/VersionControl.test.ts(更新):

import { ethers } from "hardhat"; import { expect } from "chai"; import { TransparentProxy, LogicV1, LogicV2 } from "../typechain-types"; describe("TransparentProxy", function () { let proxy: TransparentProxy; let logicV1: LogicV1; let logicV2: LogicV2; let proxyAdmin: any; let owner: any, addr1: any, admin: any; beforeEach(async function () { [owner, addr1, admin] = await ethers.getSigners(); const LogicV1Factory = await ethers.getContractFactory("LogicV1"); logicV1 = await LogicV1Factory.deploy(); await logicV1.deployed(); const ProxyAdminFactory = await ethers.getContractFactory("ProxyAdmin", admin); proxyAdmin = await ProxyAdminFactory.deploy(); await proxyAdmin.deployed(); const ProxyFactory = await ethers.getContractFactory("TransparentProxy"); const initData = LogicV1Factory.interface.encodeFunctionData("initialize", [owner.address]); proxy = await ProxyFactory.deploy(logicV1.address, proxyAdmin.address, initData); await proxy.deployed(); const LogicV2Factory = await ethers.getContractFactory("LogicV2"); logicV2 = await LogicV2Factory.deploy(); await logicV2.deployed(); }); it("should initialize correctly", async function () { const proxyAsLogicV1 = await ethers.getContractFactory("LogicV1").then(f => f.attach(proxy.address)); expect(await proxyAsLogicV1.owner()).to.equal(owner.address); expect(await proxyAsLogicV1.version()).to.equal(1); }); it("should set and get value", async function () { const proxyAsLogicV1 = await ethers.getContractFactory("LogicV1").then(f => f.attach(proxy.address)); await proxyAsLogicV1.setValue(42); expect(await proxyAsLogicV1.getValue()).to.equal(42); }); it("should restrict setValue to owner", async function () { const proxyAsLogicV1 = await ethers.getContractFactory("LogicV1").then(f => f.attach(proxy.address)); await expect(proxyAsLogicV1.connect(addr1).setValue(42)).to.be.revertedWith("Ownable: caller is not the owner"); }); it("should upgrade to LogicV2", async function () { const proxyAsLogicV1 = await ethers.getContractFactory("LogicV1").then(f => f.attach(proxy.address)); await proxyAsLogicV1.setValue(42); await expect(proxyAdmin.connect(admin).upgrade(proxy.address, logicV2.address)) .to.emit(proxy, "Upgraded") .withArgs(logicV2.address, 2); const proxyAsLogicV2 = await ethers.getContractFactory("LogicV2").then(f => f.attach(proxy.address)); await proxyAsLogicV2.setValue(10); expect(await proxyAsLogicV2.getValue()).to.equal(20); expect(await proxyAsLogicV2.owner()).to.equal(owner.address); expect(await proxyAsLogicV2.version()).to.equal(2); }); it("should restrict upgrade to admin", async function () { await expect(proxyAdmin.connect(addr1).upgrade(proxy.address, logicV2.address)).to.be.revertedWith("Ownable: caller is not the owner"); }); });
  • 解析
    • 部署:LogicV1ProxyAdminTransparentProxy,通过initData初始化。
    • 操作:设置value为42。
    • 升级:用ProxyAdmin切换到LogicV2setValue(10)返回20,version更新为2。
    • 权限:只有admin可升级。
    • 版本控制:Upgraded事件记录升级历史。
  • 透明机制:管理员调用直接操作代理,普通用户调用转发到逻辑合约。

UUPS代理(Universal Upgradeable Proxy Standard)

UUPS把升级逻辑放逻辑合约,代理更轻量。

contracts/UUPSLogicV1.sol

// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; contract UUPSLogicV1 is Initializable, OwnableUpgradeable, UUPSUpgradeable { uint256 public value; uint256 public version; mapping(uint256 => address) public versionHistory; event Upgraded(address indexed implementation, uint256 version); function initialize(address _owner) public initializer { __Ownable_init(_owner); __UUPSUpgradeable_init(); version = 1; versionHistory[1] = address(this); emit Upgraded(address(this), 1); } function setValue(uint256 _value) public onlyOwner { value = _value; } function getValue() public view returns (uint256) { return value; } function _authorizeUpgrade(address newImplementation) internal override onlyOwner { version++; versionHistory[version] = newImplementation; emit Upgraded(newImplementation, version); } }

contracts/UUPSLogicV2.sol

// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; contract UUPSLogicV2 is Initializable, OwnableUpgradeable, UUPSUpgradeable { uint256 public value; uint256 public version; mapping(uint256 => address) public versionHistory; event Upgraded(address indexed implementation, uint256 version); function initialize(address _owner) public initializer { __Ownable_init(_owner); __UUPSUpgradeable_init(); version = 2; versionHistory[2] = address(this); emit Upgraded(address(this), 2); } function setValue(uint256 _value) public onlyOwner { value = _value * 2; } function getValue() public view returns (uint256) { return value; } function _authorizeUpgrade(address newImplementation) internal override onlyOwner { version++; versionHistory[version] = newImplementation; emit Upgraded(newImplementation, version); } }

contracts/UUPSProxy.sol

// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; contract UUPSProxy is ERC1967Proxy { constructor(address logic, bytes memory data) ERC1967Proxy(logic, data) {} }

解析

  • UUPSLogicV1/V2
    • 继承UUPSUpgradeable,包含升级逻辑和版本控制。
    • _authorizeUpgrade:限制升级权限,更新versionversionHistory
    • Upgraded事件记录版本历史。
  • UUPSProxy
    • 继承ERC1967Proxy,逻辑地址存标准槽位。
    • 构造函数:设置逻辑合约和初始化数据。
  • 优势
    • 代理合约轻量,升级逻辑在逻辑合约。
    • 可通过自毁移除升级功能。
  • 安全特性
    • onlyOwner控制升级。
    • Initializer防止重复初始化。
    • ERC1967标准槽位。
    • 版本历史和事件追踪。

测试

test/VersionControl.test.ts(update):

import { ethers } from "hardhat"; import { expect } from "chai"; import { UUPSProxy, UUPSLogicV1, UUPSLogicV2 } from "../typechain-types"; describe("UUPSProxy", function () { let proxy: UUPSProxy; let logicV1: UUPSLogicV1; let logicV2: UUPSLogicV2; let owner: any, addr1: any; beforeEach(async function () { [owner, addr1] = await ethers.getSigners(); const LogicV1Factory = await ethers.getContractFactory("UUPSLogicV1"); logicV1 = await LogicV1Factory.deploy(); await logicV1.deployed(); const ProxyFactory = await ethers.getContractFactory("UUPSProxy"); const initData = LogicV1Factory.interface.encodeFunctionData("initialize", [owner.address]); proxy = await ProxyFactory.deploy(logicV1.address, initData); await proxy.deployed(); const LogicV2Factory = await ethers.getContractFactory("UUPSLogicV2"); logicV2 = await LogicV2Factory.deploy(); await logicV2.deployed(); }); it("should initialize correctly", async function () { const proxyAsLogicV1 = await ethers.getContractFactory("UUPSLogicV1").then(f => f.attach(proxy.address)); expect(await proxyAsLogicV1.owner()).to.equal(owner.address); expect(await proxyAsLogicV1.version()).to.equal(1); expect(await proxyAsLogicV1.versionHistory(1)).to.equal(logicV1.address); }); it("should set and get value", async function () { const proxyAsLogicV1 = await ethers.getContractFactory("UUPSLogicV1").then(f => f.attach(proxy.address)); await proxyAsLogicV1.setValue(42); expect(await proxyAsLogicV1.getValue()).to.equal(42); }); it("should upgrade to LogicV2", async function () { const proxyAsLogicV1 = await ethers.getContractFactory("UUPSLogicV1").then(f => f.attach(proxy.address)); await proxyAsLogicV1.setValue(42); await expect(proxyAsLogicV1.upgradeTo(logicV2.address)) .to.emit(proxyAsLogicV1, "Upgraded") .withArgs(logicV2.address, 2); const proxyAsLogicV2 = await ethers.getContractFactory("UUPSLogicV2").then(f => f.attach(proxy.address)); await proxyAsLogicV2.setValue(10); expect(await proxyAsLogicV2.getValue()).to.equal(20); expect(await proxyAsLogicV2.owner()).to.equal(owner.address); expect(await proxyAsLogicV2.version()).to.equal(2); expect(await proxyAsLogicV2.versionHistory(2)).to.equal(logicV2.address); }); it("should restrict upgrade to owner", async function () { const proxyAsLogicV1 = await ethers.getContractFactory("UUPSLogicV1").then(f => f.attach(proxy.address)); await expect(proxyAsLogicV1.connect(addr1).upgradeTo(logicV2.address)).to.be.revertedWith("Ownable: caller is not the owner"); }); });
  • 解析
    • 部署:UUPSLogicV1UUPSProxy,通过initData初始化。
    • 操作:设置value为42。
    • 升级:调用upgradeTo切换到LogicV2setValue(10)返回20,version更新。
    • 版本控制:versionHistoryUpgraded事件记录升级历史。
    • 权限:非owner无法升级。
  • UUPS特点:升级逻辑在逻辑合约,代理更轻量。

多签控制版本升级

为升级加多签机制,需多人同意。

contracts/MultiSigUpgrade.sol

// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; contract MultiSigUpgrade is Initializable, UUPSUpgradeable, OwnableUpgradeable { address[] public owners; uint256 public required; uint256 public transactionCount; mapping(uint256 => Transaction) public transactions; mapping(uint256 => mapping(address => bool)) public confirmations; uint256 public value; uint256 public version; mapping(uint256 => address) public versionHistory; struct Transaction { address newImplementation; bool executed; uint256 confirmationCount; uint256 version; } event SubmitUpgrade(uint256 indexed txId, address indexed newImplementation, uint256 version); event ConfirmUpgrade(uint256 indexed txId, address indexed owner); event ExecuteUpgrade(uint256 indexed txId, address indexed newImplementation, uint256 version); event RevokeConfirmation(uint256 indexed txId, address indexed owner); event Upgraded(address indexed implementation, uint256 version); modifier onlyOwner() { bool isOwner = false; for (uint256 i = 0; i < owners.length; i++) { if (owners[i] == msg.sender) { isOwner = true; break; } } require(isOwner, "Not owner"); _; } function initialize(address[] memory _owners, uint256 _required) public initializer { __Ownable_init(msg.sender); __UUPSUpgradeable_init(); require(_owners.length > 0, "Owners required"); require(_required > 0 && _required <= _owners.length, "Invalid required"); owners = _owners; required = _required; version = 1; versionHistory[1] = address(this); emit Upgraded(address(this), 1); } function setValue(uint256 _value) public onlyOwner { value = _value; } function getValue() public view returns (uint256) { return value; } function submitUpgrade(address newImplementation, uint256 newVersion) public onlyOwner { require(newImplementation != address(0), "Invalid implementation"); uint256 txId = transactionCount++; transactions[txId] = Transaction({ newImplementation: newImplementation, executed: false, confirmationCount: 0, version: newVersion }); emit SubmitUpgrade(txId, newImplementation, newVersion); } function confirmUpgrade(uint256 txId) public onlyOwner { Transaction storage transaction = transactions[txId]; require(!transaction.executed, "Transaction already executed"); require(!confirmations[txId][msg.sender], "Already confirmed"); confirmations[txId][msg.sender] = true; transaction.confirmationCount++; emit ConfirmUpgrade(txId, msg.sender); if (transaction.confirmationCount >= required) { executeUpgrade(txId); } } function executeUpgrade(uint256 txId) internal { Transaction storage transaction = transactions[txId]; require(!transaction.executed, "Transaction already executed"); require(transaction.confirmationCount >= required, "Insufficient confirmations"); transaction.executed = true; version = transaction.version; versionHistory[version] = transaction.newImplementation; _authorizeUpgrade(transaction.newImplementation); _upgradeTo(transaction.newImplementation); emit ExecuteUpgrade(txId, transaction.newImplementation, version); emit Upgraded(transaction.newImplementation, version); } function revokeConfirmation(uint256 txId) public onlyOwner { Transaction storage transaction = transactions[txId]; require(!transaction.executed, "Transaction already executed"); require(confirmations[txId][msg.sender], "Not confirmed"); confirmations[txId][msg.sender] = false; transaction.confirmationCount--; emit RevokeConfirmation(txId, msg.sender); } function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} }

contracts/MultiSigLogicV2.sol

// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; contract MultiSigLogicV2 is Initializable, UUPSUpgradeable, OwnableUpgradeable { address[] public owners; uint256 public required; uint256 public transactionCount; mapping(uint256 => Transaction) public transactions; mapping(uint256 => mapping(address => bool)) public confirmations; uint256 public value; uint256 public version; mapping(uint256 => address) public versionHistory; struct Transaction { address newImplementation; bool executed; uint256 confirmationCount; uint256 version; } event SubmitUpgrade(uint256 indexed txId, address indexed newImplementation, uint256 version); event ConfirmUpgrade(uint256 indexed txId, address indexed owner); event ExecuteUpgrade(uint256 indexed txId, address indexed newImplementation, uint256 version); event RevokeConfirmation(uint256 indexed txId, address indexed owner); event Upgraded(address indexed implementation, uint256 version); modifier onlyOwner() { bool isOwner = false; for (uint256 i = 0; i < owners.length; i++) { if (owners[i] == msg.sender) { isOwner = true; break; } } require(isOwner, "Not owner"); _; } function initialize(address[] memory _owners, uint256 _required) public initializer { __Ownable_init(msg.sender); __UUPSUpgradeable_init(); require(_owners.length > 0, "Owners required"); require(_required > 0 && _required <= _owners.length, "Invalid required"); owners = _owners; required = _required; version = 2; versionHistory[2] = address(this); emit Upgraded(address(this), 2); } function setValue(uint256 _value) public onlyOwner { value = _value * 2; // V2 doubles the value } function getValue() public view returns (uint256) { return value; } function submitUpgrade(address newImplementation, uint256 newVersion) public onlyOwner { require(newImplementation != address(0), "Invalid implementation"); uint256 txId = transactionCount++; transactions[txId] = Transaction({ newImplementation: newImplementation, executed: false, confirmationCount: 0, version: newVersion }); emit SubmitUpgrade(txId, newImplementation, newVersion); } function confirmUpgrade(uint256 txId) public onlyOwner { Transaction storage transaction = transactions[txId]; require(!transaction.executed, "Transaction already executed"); require(!confirmations[txId][msg.sender], "Already confirmed"); confirmations[txId][msg.sender] = true; transaction.confirmationCount++; emit ConfirmUpgrade(txId, msg.sender); if (transaction.confirmationCount >= required) { executeUpgrade(txId); } } function executeUpgrade(uint256 txId) internal { Transaction storage transaction = transactions[txId]; require(!transaction.executed, "Transaction already executed"); require(transaction.confirmationCount >= required, "Insufficient confirmations"); transaction.executed = true; version = transaction.version; versionHistory[version] = transaction.newImplementation; _authorizeUpgrade(transaction.newImplementation); _upgradeTo(transaction.newImplementation); emit ExecuteUpgrade(txId, transaction.newImplementation, version); emit Upgraded(transaction.newImplementation, version); } function revokeConfirmation(uint256 txId) public onlyOwner { Transaction storage transaction = transactions[txId]; require(!transaction.executed, "Transaction already executed"); require(confirmations[txId][msg.sender], "Not confirmed"); confirmations[txId][msg.sender] = false; transaction.confirmationCount--; emit RevokeConfirmation(txId, msg.sender); } function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} }

解析

  • MultiSigUpgrade
    • 继承UUPSUpgradeable,支持多签升级。
    • ownersrequired控制多签。
    • submitUpgrade:提交升级提案,指定新版本号。
    • confirmUpgrade:确认提案,达标后执行。
    • executeUpgrade:调用_upgradeTo切换逻辑,更新versionversionHistory
    • revokeConfirmation:撤销确认。
    • Upgraded事件记录版本历史。
  • MultiSigLogicV2:升级版,setValue翻倍,保持多签逻辑。
  • 安全特性
    • 多签防止单人误操作。
    • onlyOwner限制操作。
    • 检查newImplementation有效性。
    • 版本历史和事件追踪。

测试

test/VersionControl.test.ts(add):

import { ethers } from "hardhat"; import { expect } from "chai"; import { UUPSProxy, MultiSigUpgrade, MultiSigLogicV2 } from "../typechain-types"; describe("MultiSigUpgrade", function () { let proxy: UUPSProxy; let logicV1: MultiSigUpgrade; let logicV2: MultiSigLogicV2; let owner1: any, owner2: any, owner3: any, nonOwner: any; beforeEach(async function () { [owner1, owner2, owner3, nonOwner] = await ethers.getSigners(); const LogicV1Factory = await ethers.getContractFactory("MultiSigUpgrade"); logicV1 = await LogicV1Factory.deploy(); await logicV1.deployed(); const ProxyFactory = await ethers.getContractFactory("UUPSProxy"); const initData = LogicV1Factory.interface.encodeFunctionData("initialize", [ [owner1.address, owner2.address, owner3.address], 2, ]); proxy = await ProxyFactory.deploy(logicV1.address, initData); await proxy.deployed(); const LogicV2Factory = await ethers.getContractFactory("MultiSigLogicV2"); logicV2 = await LogicV2Factory.deploy(); await logicV2.deployed(); }); it("should initialize correctly", async function () { const proxyAsLogicV1 = await ethers.getContractFactory("MultiSigUpgrade").then(f => f.attach(proxy.address)); expect(await proxyAsLogicV1.owners(0)).to.equal(owner1.address); expect(await proxyAsLogicV1.required()).to.equal(2); expect(await proxyAsLogicV1.version()).to.equal(1); expect(await proxyAsLogicV1.versionHistory(1)).to.equal(logicV1.address); }); it("should set and get value", async function () { const proxyAsLogicV1 = await ethers.getContractFactory("MultiSigUpgrade").then(f => f.attach(proxy.address)); await proxyAsLogicV1.connect(owner1).setValue(42); expect(await proxyAsLogicV1.getValue()).to.equal(42); }); it("should upgrade with multi-sig", async function () { const proxyAsLogicV1 = await ethers.getContractFactory("MultiSigUpgrade").then(f => f.attach(proxy.address)); await proxyAsLogicV1.connect(owner1).setValue(42); await proxyAsLogicV1.connect(owner1).submitUpgrade(logicV2.address, 2); await proxyAsLogicV1.connect(owner2).confirmUpgrade(0); await expect(proxyAsLogicV1.connect(owner3).confirmUpgrade(0)) .to.emit(proxyAsLogicV1, "Upgraded") .withArgs(logicV2.address, 2); const proxyAsLogicV2 = await ethers.getContractFactory("MultiSigLogicV2").then(f => f.attach(proxy.address)); await proxyAsLogicV2.connect(owner1).setValue(10); expect(await proxyAsLogicV2.getValue()).to.equal(20); expect(await proxyAsLogicV2.owners(0)).to.equal(owner1.address); expect(await proxyAsLogicV2.version()).to.equal(2); expect(await proxyAsLogicV2.versionHistory(2)).to.equal(logicV2.address); }); it("should not upgrade without enough confirmations", async function () { const proxyAsLogicV1 = await ethers.getContractFactory("MultiSigUpgrade").then(f => f.attach(proxy.address)); await proxyAsLogicV1.connect(owner1).submitUpgrade(logicV2.address, 2); await proxyAsLogicV1.connect(owner2).confirmUpgrade(0); const proxyAsLogicV2 = await ethers.getContractFactory("MultiSigLogicV2").then(f => f.attach(proxy.address)); await proxyAsLogicV2.connect(owner1).setValue(10); expect(await proxyAsLogicV2.getValue()).to.equal(10); // Still V1 }); it("should allow revoking confirmation", async function () { const proxyAsLogicV1 = await ethers.getContractFactory("MultiSigUpgrade").then(f => f.attach(proxy.address)); await proxyAsLogicV1.connect(owner1).submitUpgrade(logicV2.address, 2); await proxyAsLogicV1.connect(owner2).confirmUpgrade(0); await proxyAsLogicV1.connect(owner2).revokeConfirmation(0); await proxyAsLogicV1.connect(owner3).confirmUpgrade(0); const proxyAsLogicV2 = await ethers.getContractFactory("MultiSigLogicV2").then(f => f.attach(proxy.address)); expect(await proxyAsLogicV2.getValue()).to.equal(0); // Still V1 }); it("should restrict upgrade to owners", async function () { const proxyAsLogicV1 = await ethers.getContractFactory("MultiSigUpgrade").then(f => f.attach(proxy.address)); await expect(proxyAsLogicV1.connect(nonOwner).submitUpgrade(logicV2.address, 2)).to.be.revertedWith("Not owner"); }); });
  • 解析
    • 部署:3个owners,需2人确认。
    • 操作:设置value为42。
    • 升级:提交提案,2人确认后切换到LogicV2setValue(10)返回20,version更新。
    • 撤销:撤销确认阻止升级。
    • 版本控制:versionHistoryUpgraded事件记录历史。
  • 安全:多签机制防止单人误操作。

部署脚本

scripts/deploy.ts

import { ethers } from "hardhat"; async function main() { const [owner, admin, owner1, owner2, owner3] = await ethers.getSigners(); const LogicV1Factory = await ethers.getContractFactory("LogicV1"); const logicV1 = await LogicV1Factory.deploy(); await logicV1.deployed(); console.log(`LogicV1 deployed to: ${logicV1.address}`); const ProxyAdminFactory = await ethers.getContractFactory("ProxyAdmin", admin); const proxyAdmin = await ProxyAdminFactory.deploy(); await proxyAdmin.deployed(); console.log(`ProxyAdmin deployed to: ${proxyAdmin.address}`); const TransparentProxyFactory = await ethers.getContractFactory("TransparentProxy"); const initData = LogicV1Factory.interface.encodeFunctionData("initialize", [owner.address]); const transparentProxy = await TransparentProxyFactory.deploy(logicV1.address, proxyAdmin.address, initData); await transparentProxy.deployed(); console.log(`TransparentProxy deployed to: ${transparentProxy.address}`); const UUPSLogicV1Factory = await ethers.getContractFactory("UUPSLogicV1"); const uupsLogicV1 = await UUPSLogicV1Factory.deploy(); await uupsLogicV1.deployed(); console.log(`UUPSLogicV1 deployed to: ${uupsLogicV1.address}`); const UUPSProxyFactory = await ethers.getContractFactory("UUPSProxy"); const uupsProxy = await UUPSProxyFactory.deploy(uupsLogicV1.address, initData); await uupsProxy.deployed(); console.log(`UUPSProxy deployed to: ${uupsProxy.address}`); const MultiSigLogicV1Factory = await ethers.getContractFactory("MultiSigUpgrade"); const multiSigLogicV1 = await MultiSigLogicV1Factory.deploy(); await multiSigLogicV1.deployed(); console.log(`MultiSigUpgrade deployed to: ${multiSigLogicV1.address}`); const multiSigInitData = MultiSigLogicV1Factory.interface.encodeFunctionData("initialize", [ [owner1.address, owner2.address, owner3.address], 2, ]); const multiSigProxy = await UUPSProxyFactory.deploy(multiSigLogicV1.address, multiSigInitData); await multiSigProxy.deployed(); console.log(`MultiSigProxy deployed to: ${multiSigProxy.address}`); } main().catch((error) => { console.error(error); process.exitCode = 1; });

跑部署:

npx hardhat run scripts/deploy.ts --network hardhat
  • 解析:部署所有代理和逻辑合约,记录地址。

版本追踪

  • SimpleProxy:用versionHistoryUpgraded事件记录版本。
  • TransparentProxy:通过ProxyAdmin和事件追踪。
  • UUPSProxy:逻辑合约内记录versionHistory和事件。
  • MultiSigUpgrade:多签提案和事件记录版本历史。

跑代码,体验Solidity版本控制的硬核玩法吧!

点赞 0
收藏 0
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论