Solidity合约升级:让你的区块链代码永葆青春的硬核攻略

Solidity里一个超硬核的技术——合约升级!区块链上的智能合约一旦部署,默认是“铁打不动”的,但现实中需求总在变,bug也得修,咋办?合约升级就是救星!它能让你在不换地址、不丢数据的情况下,悄悄把合约逻辑更新,简直像给代码换了个新皮肤!咱们会用大白话把合约升级的套路讲透,从最简单的代理模式到Op

Solidity里一个超硬核的技术——合约升级!区块链上的智能合约一旦部署,默认是“铁打不动”的,但现实中需求总在变,bug也得修,咋办?合约升级就是救星!它能让你在不换地址、不丢数据的情况下,悄悄把合约逻辑更新,简直像给代码换了个新皮肤!咱们会用大白话把合约升级的套路讲透,从最简单的代理模式到OpenZeppelin的透明代理和UUPS,再到多签控制升级,配合硬核代码和Hardhat测试,带你一步步实现安全的合约升级。重点是干货,废话少说,直接上技术细节,帮你把合约升级玩得贼6!

核心概念

先来搞明白几个关键点:

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

咱们用Solidity 0.8.20,结合OpenZeppelin和Hardhat,从基础代理到高级UUPS和多签升级,逐步实现安全的合约升级。

环境准备

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

mkdir upgrade-demo
cd upgrade-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

目录结构:

upgrade-demo/
├── contracts/
│   ├── SimpleProxy.sol
│   ├── LogicV1.sol
│   ├── LogicV2.sol
│   ├── TransparentProxy.sol
│   ├── UUPSProxy.sol
│   ├── MultiSigUpgrade.sol
├── scripts/
│   ├── deploy.ts
├── test/
│   ├── Upgrade.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;

    function initialize(address _owner) public {
        require(owner == address(0), "Already initialized");
        owner = _owner;
    }

    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;

    function initialize(address _owner) public {
        require(owner == address(0), "Already initialized");
        owner = _owner;
    }

    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;

    constructor(address _implementation) {
        owner = msg.sender;
        implementation = _implementation;
    }

    function upgrade(address _newImplementation) public {
        require(msg.sender == owner, "Only owner");
        implementation = _newImplementation;
    }

    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:基础逻辑合约,存ownervalue,提供initializesetValuegetValue
  • LogicV2:升级版,setValue将输入值翻倍,存储布局与V1一致。
  • SimpleProxy
    • implementation(逻辑合约地址)和owner
    • upgrade:更新逻辑合约地址,仅owner可调用。
    • fallback:用delegatecall转发调用到implementation,用汇编实现低级调用。
  • delegatecall:逻辑合约的代码在代理的存储上下文执行,ownervalue存在代理合约。
  • 安全特性
    • onlyOwner限制升级权限。
    • 检查implementation不为空。
    • 初始化检查防止重复设置。

测试

test/Upgrade.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);
  });

  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 proxy.upgrade(logicV2.address);
    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);
  });

  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,owner不变。
    • 权限:非owner无法升级。
  • 存储:代理保存ownervaluedelegatecall确保逻辑合约操作代理存储。

存储冲突的坑

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

错误示例

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)

    function initialize(address _owner) public {
        require(owner == address(0), "Already initialized");
        owner = _owner;
    }

    function setValue(uint256 _value) public {
        require(msg.sender == owner, "Only owner");
        value = _value * 2;
    }

    function getValue() public view returns (uint256) {
        return value;
    }
}

测试:

test/Upgrade.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),BadLogicV2value(slot 0), owner(slot 1),升级后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;

    function initialize(address _owner) public initializer {
        __Ownable_init(_owner);
    }

    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;

    function initialize(address _owner) public initializer {
        __Ownable_init(_owner);
    }

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

contract TransparentProxy is TransparentUpgradeableProxy {
    constructor(address logic, address admin, bytes memory data)
        TransparentUpgradeableProxy(logic, admin, data)
    {}
}

解析

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

测试

test/Upgrade.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 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 ProxyFactory = await ethers.getContractFactory("TransparentProxy");
    const initData = LogicV1Factory.interface.encodeFunctionData("initialize", [owner.address]);
    proxy = await ProxyFactory.deploy(logicV1.address, admin.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);
  });

  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);
    const ProxyAdminFactory = await ethers.getContractFactory("ProxyAdmin", admin);
    const proxyAdmin = await ProxyAdminFactory.deploy();
    await proxyAdmin.deployed();
    await proxyAdmin.connect(admin).upgrade(proxy.address, logicV2.address);
    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);
  });

  it("should restrict upgrade to admin", async function () {
    const ProxyAdminFactory = await ethers.getContractFactory("ProxyAdmin", admin);
    const proxyAdmin = await ProxyAdminFactory.deploy();
    await proxyAdmin.deployed();
    await expect(proxyAdmin.connect(addr1).upgrade(proxy.address, logicV2.address)).to.be.revertedWith("Ownable: caller is not the owner");
  });
});
  • 解析
    • 部署:LogicV1TransparentProxy,通过initData初始化。
    • 操作:设置value为42。
    • 升级:用ProxyAdmin切换到LogicV2setValue(10)返回20。
    • 权限:只有admin可升级。
  • 透明机制:管理员调用直接操作代理,普通用户调用转发到逻辑合约。

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;

    function initialize(address _owner) public initializer {
        __Ownable_init(_owner);
        __UUPSUpgradeable_init();
    }

    function setValue(uint256 _value) public onlyOwner {
        value = _value;
    }

    function getValue() public view returns (uint256) {
        return value;
    }

    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
}

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;

    function initialize(address _owner) public initializer {
        __Ownable_init(_owner);
        __UUPSUpgradeable_init();
    }

    function setValue(uint256 _value) public onlyOwner {
        value = _value * 2;
    }

    function getValue() public view returns (uint256) {
        return value;
    }

    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
}

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:限制升级权限。
  • UUPSProxy
    • 继承ERC1967Proxy,逻辑地址存标准槽位。
    • 构造函数:设置逻辑合约和初始化数据。
  • 优势
    • 代理合约轻量,升级逻辑在逻辑合约。
    • 可通过自毁移除升级功能。
  • 安全特性
    • onlyOwner控制升级。
    • Initializer防止重复初始化。
    • ERC1967标准槽位。

测试

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

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);
  });

  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 proxyAsLogicV1.upgradeTo(logicV2.address);
    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);
  });

  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。
    • 权限:非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;

    struct Transaction {
        address newImplementation;
        bool executed;
        uint256 confirmationCount;
    }

    event SubmitUpgrade(uint256 indexed txId, address indexed newImplementation);
    event ConfirmUpgrade(uint256 indexed txId, address indexed owner);
    event ExecuteUpgrade(uint256 indexed txId, address indexed newImplementation);
    event RevokeConfirmation(uint256 indexed txId, address indexed owner);

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

    function setValue(uint256 _value) public onlyOwner {
        value = _value;
    }

    function getValue() public view returns (uint256) {
        return value;
    }

    function submitUpgrade(address newImplementation) public onlyOwner {
        require(newImplementation != address(0), "Invalid implementation");
        uint256 txId = transactionCount++;
        transactions[txId] = Transaction({
            newImplementation: newImplementation,
            executed: false,
            confirmationCount: 0
        });
        emit SubmitUpgrade(txId, newImplementation);
    }

    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;
        _authorizeUpgrade(transaction.newImplementation);
        _upgradeTo(transaction.newImplementation);
        emit ExecuteUpgrade(txId, transaction.newImplementation);
    }

    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;

    struct Transaction {
        address newImplementation;
        bool executed;
        uint256 confirmationCount;
    }

    event SubmitUpgrade(uint256 indexed txId, address indexed newImplementation);
    event ConfirmUpgrade(uint256 indexed txId, address indexed owner);
    event ExecuteUpgrade(uint256 indexed txId, address indexed newImplementation);
    event RevokeConfirmation(uint256 indexed txId, address indexed owner);

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

    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) public onlyOwner {
        require(newImplementation != address(0), "Invalid implementation");
        uint256 txId = transactionCount++;
        transactions[txId] = Transaction({
            newImplementation: newImplementation,
            executed: false,
            confirmationCount: 0
        });
        emit SubmitUpgrade(txId, newImplementation);
    }

    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;
        _authorizeUpgrade(transaction.newImplementation);
        _upgradeTo(transaction.newImplementation);
        emit ExecuteUpgrade(txId, transaction.newImplementation);
    }

    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切换逻辑。
    • revokeConfirmation:撤销确认。
  • MultiSigLogicV2:升级版,setValue翻倍。
  • 安全特性
    • 多签防止单人误操作。
    • onlyOwner限制操作。
    • 检查newImplementation有效性。

测试

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

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);
  });

  it("should set and get value", async function () {
    const proxyAsLogicV1 = await ethers.getContractFactory("MultiSigUpgrade").then(f => f.attach(proxy.address));
    await proxyAsLogicV1.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.setValue(42);
    await proxyAsLogicV1.submitUpgrade(logicV2.address);
    await proxyAsLogicV1.connect(owner2).confirmUpgrade(0);
    await proxyAsLogicV1.connect(owner3).confirmUpgrade(0);
    const proxyAsLogicV2 = await ethers.getContractFactory("MultiSigLogicV2").then(f => f.attach(proxy.address));
    await proxyAsLogicV2.setValue(10);
    expect(await proxyAsLogicV2.getValue()).to.equal(20);
    expect(await proxyAsLogicV2.owners(0)).to.equal(owner1.address);
  });

  it("should not upgrade without enough confirmations", async function () {
    const proxyAsLogicV1 = await ethers.getContractFactory("MultiSigUpgrade").then(f => f.attach(proxy.address));
    await proxyAsLogicV1.submitUpgrade(logicV2.address);
    await proxyAsLogicV1.connect(owner2).confirmUpgrade(0);
    const proxyAsLogicV2 = await ethers.getContractFactory("MultiSigLogicV2").then(f => f.attach(proxy.address));
    await proxyAsLogicV2.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.submitUpgrade(logicV2.address);
    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)).to.be.revertedWith("Not owner");
  });
});
  • 解析
    • 部署:3个owners,需2人确认。
    • 操作:设置value为42。
    • 升级:提交提案,2人确认后切换到LogicV2setValue(10)返回20。
    • 撤销:撤销确认阻止升级。
  • 安全:多签机制防止单人误操作。

部署脚本

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 TransparentProxyFactory = await ethers.getContractFactory("TransparentProxy");
  const initData = LogicV1Factory.interface.encodeFunctionData("initialize", [owner.address]);
  const transparentProxy = await TransparentProxyFactory.deploy(logicV1.address, admin.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:~0.6M gas。
    • TransparentProxy:~1.2M gas。
    • UUPSProxy:~0.8M gas。
    • LogicV1/UUPSLogicV1:~1M gas。
    • MultiSigUpgrade:~1.5M gas。
  • 操作
    • 初始化:~100k gas。
    • 设置值:~50k gas。
    • 升级:60k gas(透明),40k gas(UUPS),~100k gas(多签)。

跑代码,体验Solidity合约升级的硬核玩法吧!

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

0 条评论

请先 登录 后评论
天涯学馆
天涯学馆
0x9d6d...50d5
资深大厂程序员,12年开发经验,致力于探索前沿技术!