DApp开发 阶段三 ✅ 验证通过
智能合约一旦部署,代码就不可修改。但现实是:
delegatecall = 在调用者的存储上下文中执行被调用者的代码
// delegatecall vs call 的区别
// call: 在被调用合约的上下文中执行
// - msg.sender = 调用者
// - storage = 被调用合约的存储
// - ETH传递: 可以
// delegatecall: 在调用者的上下文中执行
// - msg.sender = 原始调用者(不变)
// - storage = 调用者的存储 ← 这是代理模式的核心!
// - ETH传递: 不可以
// 简化版Proxy核心逻辑
contract SimpleProxy {
address public implementation;
address public admin;
fallback() external payable {
(bool success,) = implementation.delegatecall(msg.data);
require(success, "Delegatecall failed");
}
function upgradeTo(address _newImpl) external {
require(msg.sender == admin, "Not admin");
implementation = _newImpl;
}
}
// ✅ 正确的升级存储布局
// V1: 逻辑合约
contract LogicV1 {
uint256 public count; // slot 0
address public owner; // slot 1
function increment() external {
count += 1;
}
}
// V2: 升级后的逻辑合约
contract LogicV2 {
uint256 public count; // slot 0 — 不变!
address public owner; // slot 1 — 不变!
string public name; // slot 2 — ✅ 新变量加在末尾
function increment() external {
count += 2; // ✅ 修改逻辑
}
function setName(string memory _name) external {
name = _name; // ✅ 新增功能
}
}
| 特性 | Transparent Proxy | UUPS |
|---|---|---|
| 升级逻辑位置 | Proxy合约 | 实现合约 |
| 部署Gas | 较高(Proxy含升级逻辑) | 较低 |
| 每次调用Gas | 较高(需判断admin) | 较低(无admin判断) |
| 安全性 | 不会忘加升级函数 | 可能忘加升级函数(锁死) |
| 存储冲突 | 有冲突风险 | 无冲突(EIP-1822) |
| 推荐场景 | 入门、高安全需求 | Gas敏感、进阶项目 |
# 安装OpenZeppelin升级插件
npm install --save-dev @openzeppelin/hardhat-upgrades
npm install @openzeppelin/contracts-upgradeable
// hardhat.config.js
require("@openzeppelin/hardhat-upgrades");
// contracts/BoxV1.sol — 可升级的存储盒子
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
/**
* @title BoxV1
* @dev 可升级的存储合约(UUPS模式)
* 注意:不能使用constructor,用initializer代替
*/
contract BoxV1 is Initializable, OwnableUpgradeable, UUPSUpgradeable {
uint256 private _value;
string public version;
// ⚠️ 不能用constructor!用initializer代替
//initializer只在首次部署时调用(通过Proxy)
function initialize(uint256 initialValue) public initializer {
__Ownable_init(msg.sender); // 替代constructor中的owner设置
__UUPSUpgradeable_init();
_value = initialValue;
version = "v1";
}
function value() external view returns (uint256) {
return _value;
}
function setValue(uint256 newValue) external onlyOwner {
_value = newValue;
}
// UUPS模式:必须在实现合约中覆盖此函数
function _authorizeUpgrade(address newImplementation)
internal
override
onlyOwner
{}
}
// contracts/BoxV2.sol — 升级版本
contract BoxV2 is Initializable, OwnableUpgradeable, UUPSUpgradeable {
uint256 private _value; // slot不变
string public version; // slot不变
uint256 public multiplier; // ✅ 新变量加在末尾
// V2不再需要initialize的完整逻辑
// 但需要reinitializer来设置新变量
function initializeV2(uint256 _multiplier) public reinitializer(2) {
multiplier = _multiplier;
version = "v2";
}
function value() external view returns (uint256) {
return _value * multiplier; // ✅ 修改逻辑:加入乘数
}
function setValue(uint256 newValue) external onlyOwner {
_value = newValue;
}
// ✅ 新增功能
function increment() external {
_value += 1;
}
function _authorizeUpgrade(address newImplementation)
internal
override
onlyOwner
{}
}
// scripts/deploy-upgradeable.js
const { upgrades } = require("@openzeppelin/hardhat-upgrades");
async function main() {
const BoxV1 = await hre.ethers.getContractFactory("BoxV1");
console.log("部署可升级的BoxV1...");
// deployProxy会自动部署Proxy + Implementation
const box = await upgrades.deployProxy(BoxV1, [42], {
initializer: "initialize",
kind: "uups", // 使用UUPS模式
});
await box.waitForDeployment();
console.log("BoxV1 (Proxy) 部署到:", await box.getAddress());
console.log("初始值:", await box.value());
console.log("版本:", await box.version());
}
// scripts/upgrade-box.js
async function upgrade() {
const proxyAddress = "0x..."; // 填入Proxy地址
console.log("升级到BoxV2...");
const BoxV2 = await hre.ethers.getContractFactory("BoxV2");
// upgradeProxy会:1.部署新的Implementation 2.更新Proxy指针
const box = await upgrades.upgradeProxy(proxyAddress, BoxV2);
console.log("Box已升级!Proxy地址不变:", await box.getAddress());
// 调用V2的initializer
await box.initializeV2(10);
console.log("版本:", await box.version());
console.log("乘数:", await box.multiplier());
// 验证:旧值 * 新乘数
const val = await box.value();
console.log("新值 (旧值*乘数):", val);
}
// 验证升级安全性
async function validateUpgrade() {
const BoxV1 = await hre.ethers.getContractFactory("BoxV1");
const BoxV2 = await hre.ethers.getContractFactory("BoxV2");
// 验证存储布局是否兼容
await upgrades.validateUpgrade(BoxV1, BoxV2, {
kind: "uups",
});
console.log("✅ 升级验证通过!存储布局兼容");
}
1. delegatecall与call的核心区别是什么?
2. 为什么可升级合约不能用constructor?
3. 升级时在存储中间插入新变量会导致什么?
contract TransparentProxy {
address public impl;
address public admin;
function upgradeTo(address newImpl) public {
require(msg.sender == admin, "Not admin");
impl = newImpl;
}
fallback() external payable {
if (msg.sender == admin) revert("Admin cannot call logic");
address i = impl;
assembly {
calldatacopy(0, 0, calldatasize())
let r := delegatecall(gas(), i, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch r case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}
}
// Beacon模式: 多个代理共享同一个逻辑合约
contract Beacon {
address public implementation;
address public owner;
constructor(address impl) { implementation = impl; owner = msg.sender; }
function upgradeTo(address newImpl) public {
require(msg.sender == owner);
implementation = newImpl;
}
}
contract BeaconProxy {
address public beacon; // 指向Beacon而非直接指向implementation
constructor(address _beacon) { beacon = _beacon; }
fallback() external payable {
address impl = Beacon(beacon).implementation();
assembly {
calldatacopy(0, 0, calldatasize())
let r := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch r case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}
}
// ✅ 验证通过 - 升级Beacon即可同时升级所有代理!
你已掌握智能合约升级技术!从代理模式原理到UUPS实战,从存储布局到升级安全,你拥有了在不可变世界中迭代进化的能力。
关键收获:
✅ 代理模式原理(delegatecall + storage)
✅ 存储布局规则与升级限制
✅ Transparent Proxy vs UUPS模式对比
✅ OpenZeppelin Upgrades插件实战
✅ 升级安全检查清单