今天咱们来聊聊Solidity里一个超硬核的技术------合约升级!区块链上的智能合约一旦部署,默认是"铁打不动"的,但现实中需求总在变,bug也得修,咋办?合约升级就是救星!它能让你在不换地址、不丢数据的情况下,悄悄把合约逻辑更新,简直像给代码换了个新皮肤!咱们会用大白话把合约升级的套路讲透,从最简单的代理模式到OpenZeppelin的透明代理和UUPS,再到多签控制升级,配合硬核代码和Hardhat测试,带你一步步实现安全的合约升级。
核心概念
先来搞明白几个关键点:
- 不可变性:以太坊智能合约部署后,字节码和存储通常不可更改。
- 代理模式:通过代理合约(Proxy)分离存储和逻辑,代理保存数据,逻辑合约提供代码,升级时只换逻辑合约。
- 委托调用(delegatecall) :代理通过
delegatecall调用逻辑合约,逻辑合约的代码在代理的存储上下文执行。 - 存储布局:代理和逻辑合约的存储槽必须一致,否则数据会乱套。
- 升级机制:更新代理指向的逻辑合约地址,实现功能升级。
- 安全风险 :
- 存储冲突:新逻辑合约的存储布局变化可能覆盖旧数据。
- 权限控制:谁能升级?没限制可能被黑客搞乱。
- 初始化:新逻辑合约需要正确初始化。
- OpenZeppelin:提供透明代理(Transparent Proxy)和UUPS(Universal Upgradeable Proxy Standard)实现。
- Solidity 0.8.x:自带溢出/下溢检查,安全可靠。
- Hardhat:开发和测试工具,方便部署和验证。
咱们用Solidity 0.8.20,结合OpenZeppelin和Hardhat,从基础代理到高级UUPS和多签升级,逐步实现安全的合约升级。
环境准备
用Hardhat搭建开发环境,写和测试合约。
bash
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:
bash
npx hardhat init
选择TypeScript项目,安装依赖:
bash
npm install --save-dev ts-node typescript @types/node @types/mocha
目录结构:
lua
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:
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:
typescript
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
const config: HardhatUserConfig = {
solidity: "0.8.20",
networks: {
hardhat: {
chainId: 1337,
},
},
};
export default config;
跑本地节点:
bash
npx hardhat node
基础代理合约
先搞一个简单的代理合约,弄清楚delegatecall和升级的套路。
合约代码
contracts/LogicV1.sol:
solidity
// 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:
solidity
// 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:
solidity
// 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 :基础逻辑合约,存
owner和value,提供initialize、setValue、getValue。 - LogicV2 :升级版,
setValue将输入值翻倍,存储布局与V1一致。 - SimpleProxy :
- 存
implementation(逻辑合约地址)和owner。 upgrade:更新逻辑合约地址,仅owner可调用。fallback:用delegatecall转发调用到implementation,用汇编实现低级调用。
- 存
- delegatecall :逻辑合约的代码在代理的存储上下文执行,
owner和value存在代理合约。 - 安全特性 :
onlyOwner限制升级权限。- 检查
implementation不为空。 - 初始化检查防止重复设置。
测试
test/Upgrade.test.ts:
typescript
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");
});
});
跑测试:
bash
npx hardhat test
- 解析 :
- 部署:
LogicV1和SimpleProxy,通过代理调用initialize。 - 操作:设置
value为42,验证存储在代理。 - 升级:切换到
LogicV2,setValue(10)返回20,owner不变。 - 权限:非
owner无法升级。
- 部署:
- 存储 :代理保存
owner和value,delegatecall确保逻辑合约操作代理存储。
存储冲突的坑
存储布局不一致会导致数据错乱,来看个错误例子。
错误示例
contracts/BadLogicV2.sol:
solidity
// 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(添加):
typescript
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);
});
- 问题 :
LogicV1的owner(slot 0),value(slot 1),BadLogicV2的value(slot 0),owner(slot 1),升级后owner被覆盖为value的值。 - 解决:新逻辑合约必须保持存储布局一致,或用OpenZeppelin的工具规范化。
透明代理(Transparent Proxy)
用OpenZeppelin的透明代理,解决管理员和用户调用冲突。
contracts/LogicV1.sol(更新):
solidity
// 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(更新):
solidity
// 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:
solidity
// 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 :
- 用
Initializable和OwnableUpgradeable,支持代理初始化。 initializer确保初始化只执行一次。
- 用
- TransparentProxy :
- 继承
TransparentUpgradeableProxy。 - 构造函数设置逻辑合约、管理员地址、初始化数据。
- 透明机制:管理员调用操作代理(如升级),普通用户调用转发到逻辑合约。
- 继承
- 安全特性 :
- 防止管理员误调用逻辑合约。
OwnableUpgradeable管理权限。- 标准化的存储布局。
测试
test/Upgrade.test.ts(更新):
typescript
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");
});
});
- 解析 :
- 部署:
LogicV1和TransparentProxy,通过initData初始化。 - 操作:设置
value为42。 - 升级:用
ProxyAdmin切换到LogicV2,setValue(10)返回20。 - 权限:只有
admin可升级。
- 部署:
- 透明机制:管理员调用直接操作代理,普通用户调用转发到逻辑合约。
UUPS代理(Universal Upgradeable Proxy Standard)
UUPS把升级逻辑放逻辑合约,代理更轻量。
contracts/UUPSLogicV1.sol:
solidity
// 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:
solidity
// 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:
solidity
// 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(更新):
typescript
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");
});
});
- 解析 :
- 部署:
UUPSLogicV1和UUPSProxy,通过initData初始化。 - 操作:设置
value为42。 - 升级:调用
upgradeTo切换到LogicV2,setValue(10)返回20。 - 权限:非
owner无法升级。
- 部署:
- UUPS特点:升级逻辑在逻辑合约,代理更轻量。
多签控制升级
为升级加多签机制,需多人同意。
contracts/MultiSigUpgrade.sol:
solidity
// 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:
solidity
// 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,支持多签升级。 owners和required控制多签。submitUpgrade:提交升级提案。confirmUpgrade:确认提案,达标后执行。executeUpgrade:调用_upgradeTo切换逻辑。revokeConfirmation:撤销确认。
- 继承
- MultiSigLogicV2 :升级版,
setValue翻倍。 - 安全特性 :
- 多签防止单人误操作。
onlyOwner限制操作。- 检查
newImplementation有效性。
测试
test/Upgrade.test.ts(添加):
typescript
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人确认后切换到
LogicV2,setValue(10)返回20。 - 撤销:撤销确认阻止升级。
- 部署:3个
- 安全:多签机制防止单人误操作。
部署脚本
scripts/deploy.ts:
typescript
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;
});
跑部署:
bash
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合约升级的硬核玩法吧!