代理模式与合约升级
概述
智能合约一旦部署就不可修改——这是区块链的核心特性。但实际开发中,我们需要修复 bug、添加功能。代理模式(Proxy Pattern)解决了这个矛盾。
| 章节 | 模式 | 核心思想 |
|---|---|---|
| 16 | 基础代理 | delegatecall 实现数据与逻辑分离 |
| 17 | 简单升级代理 | Proxy 中增加 upgrade 函数 |
| 18 | 透明代理 | 根据调用者身份路由,解决选择器冲突 |
| 19 | UUPS 代理 | 升级逻辑放在 Logic 合约中 |
前置知识:delegatecall
delegatecall 是理解所有代理模式的基础:
普通 call:
A → call → B
代码在 B 执行,数据存在 B,msg.sender = A
delegatecall:
A → delegatecall → B
代码在 B 执行,数据存在 A,msg.sender 保持不变
一句话理解:借用别人的代码,在自己的存储空间执行。
类比:你请了一个装修队(Logic)来你家(Proxy)干活。装修队带工具(代码),但改的是你家的墙(存储)。
第 16 章:基础代理合约
存储布局对齐(最重要的概念)
由于 delegatecall 操作的是 Proxy 的存储空间,Logic 合约中变量的 slot 位置和类型必须与 Proxy 完全对齐:
Proxy 存储: Logic 存储:
┌─────────────────────┐ ┌─────────────────────┐
│ Slot 0: implementation │ │ Slot 0: implementation │ ← 对齐!
│ Slot 1: x = 10 │ │ Slot 1: x = 99 │ ← 对齐!
└─────────────────────┘ └─────────────────────┘
delegatecall 执行 Logic.increment() 时:
x++ 实际操作的是 Proxy 的 Slot 1 → Proxy.x: 10 → 11
Logic 的 x 始终是 99,不受影响
代码实现
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Proxy {
address public implementation; // slot 0: 逻辑合约地址
uint256 public x = 10; // slot 1: 业务数据
constructor(address implementation_) {
implementation = implementation_;
}
/// @notice 所有未知函数调用都会进入 fallback,转发给 Logic
fallback() external payable {
_delegate();
}
receive() external payable {}
/// @notice 核心委托函数(用内联汇编实现透明转发)
function _delegate() internal {
assembly {
// 1. 从 slot 0 读取逻辑合约地址
let _implementation := sload(0)
// 2. 把完整的 calldata 复制到内存
calldatacopy(0, 0, calldatasize())
// 3. delegatecall: 用 Logic 的代码操作 Proxy 的 storage
let result := delegatecall(
gas(),
_implementation,
0, calldatasize(),
0, 0
)
// 4. 将返回数据复制到内存
returndatacopy(0, 0, returndatasize())
// 5. 根据结果 return 或 revert
switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}
}
/// @notice 逻辑合约(存储布局必须与 Proxy 对齐)
contract Logic {
address public implementation; // slot 0: 占位对齐
uint256 public x = 99; // slot 1: 对齐
event CallSuccess();
function increment() external returns (uint256) {
emit CallSuccess();
x++; // 实际修改 Proxy 的 slot 1
return x + 1;
}
}
调用链路
用户调用 Proxy.increment()
→ Proxy 没有 increment 函数
→ 触发 fallback()
→ delegatecall(Logic.increment())
→ Logic 的代码执行,但修改的是 Proxy 的存储
→ Proxy.x: 10 → 11
→ 返回 12
第 17 章:简单升级代理
在基础代理上增加什么?
基础代理的 Logic 地址是固定的。升级代理增加了 upgrade() 函数,让管理员可以更换逻辑合约地址。
升级前: Proxy → delegatecall → LogicV1.foo() → words = "Hello, kiki!"
↓ admin 调用 upgrade(LogicV2地址)
升级后: Proxy → delegatecall → LogicV2.foo() → words = "Hello, zed!"
Proxy 中的数据始终保留,只是执行逻辑变了!
代码实现
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SimpleUpgradeProxy {
address public implementation; // slot 0
address public admin; // slot 1
string public words; // slot 2: 业务数据
constructor(address implementation_) {
implementation = implementation_;
admin = msg.sender;
}
fallback() external payable {
(bool success, ) = implementation.delegatecall(msg.data);
require(success, "");
}
receive() external payable {}
/// @notice 管理员升级逻辑合约
function upgrade(address newImplementation) external {
require(msg.sender == admin, "only admin");
implementation = newImplementation;
}
}
/// @notice V1: foo() 设置 words 为 "Hello, kiki!"
contract LogicV1 {
address public implementation; // slot 0 占位
address public admin; // slot 1 占位
string public words; // slot 2 对齐
function foo() public {
words = "Hello, kiki!";
}
}
/// @notice V2: foo() 设置 words 为 "Hello, zed!"
contract LogicV2 {
address public implementation; // slot 0 占位
address public admin; // slot 1 占位
string public words; // slot 2 对齐
function foo() public {
words = "Hello, zed!";
}
}
升级操作流程
1. 部署 LogicV1
2. 部署 SimpleUpgradeProxy(LogicV1地址)
3. 用户调用 Proxy.foo() → words = "Hello, kiki!"
4. 部署 LogicV2
5. admin 调用 Proxy.upgrade(LogicV2地址)
6. 用户调用 Proxy.foo() → words = "Hello, zed!"
7. 数据保留:之前写入的 "Hello, kiki!" 虽然被覆盖了,但原理上 Proxy 的存储未丢失
第 18 章:透明代理(Transparent Proxy)
解决什么问题?——选择器冲突
问题场景:
Proxy 有函数: upgrade(address) → 选择器 = 0x0900f010
Logic 有函数: collide(uint256) → 选择器 = 0x0900f010(巧合相同!)
4 字节选择器只有约 42 亿种可能,函数越多越容易碰撞。如果用户调用 collide(),EVM 会先检查 Proxy 的函数列表,匹配到 upgrade 并执行——灾难性错误!
解决方案:根据调用者身份路由
┌──────────────────────────────────────────┐
│ TransparentProxy │
│ │
│ if (msg.sender == admin) │
│ → 只能调用 upgrade() │
│ → 不走 fallback,不会穿透到 Logic │
│ │
│ if (msg.sender != admin) │
│ → 一律走 fallback → delegatecall │
│ → 不经过 Proxy 的函数选择器 │
│ → 无论选择器是否冲突都安全 │
└──────────────────────────────────────────┘
代码实现
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract TransparentProxy {
address public implementation; // slot 0
address public admin; // slot 1
string public words; // slot 2
constructor(address _implementation) {
admin = msg.sender;
implementation = _implementation;
}
/// @notice 核心分流逻辑
fallback() external payable {
// admin 不能走 fallback(不能调业务函数)
require(msg.sender != admin);
(bool success, ) = implementation.delegatecall(msg.data);
require(success, "");
}
receive() external payable {}
/// @notice 只有 admin 能升级,非 admin 直接 revert
function upgrade(address newImplementation) external {
if (msg.sender != admin) revert();
implementation = newImplementation;
}
}
权限矩阵
| 调用者 | 调用 upgrade() | 调用业务函数 foo() |
|---|---|---|
| admin | ✅ 正常升级 | ❌ fallback 中 revert |
| 普通用户 | ❌ upgrade 中 revert | ✅ fallback → Logic |
缺点
每次普通用户调用都要多做一次 msg.sender != admin 的判断,浪费 gas。这就是 UUPS 出现的原因。
第 19 章:UUPS 代理(Universal Upgradeable Proxy Standard)
核心改进
把 upgrade() 函数从 Proxy 移到 Logic 中:
透明代理:
Proxy 有 upgrade() + fallback()
Logic 只有业务函数
UUPS:
Proxy 只有 fallback()(极简!)
Logic 有 upgrade() + 业务函数
为什么这样更好?
- Proxy 没有任何函数 → 不可能产生选择器冲突
- 省 gas:不需要每次判断 msg.sender 是否为 admin
- 更灵活:每个版本的 Logic 可以有不同的升级逻辑
升级原理
admin 调用 upgrade(新Logic地址)
→ Proxy 没有 upgrade 函数
→ fallback() → delegatecall → 当前Logic.upgrade(新地址)
→ Logic.upgrade 中: implementation = 新地址
→ 由于 delegatecall,实际修改的是 Proxy 的 slot 0
→ Proxy 的 implementation 指向新 Logic
→ 升级完成!
代码实现
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
/// @notice UUPS Proxy: 极简设计,只有 fallback
contract UUPSProxy {
address public implementation; // slot 0
address public admin; // slot 1
string public words; // slot 2
constructor(address _implementation) {
admin = msg.sender;
implementation = _implementation;
}
/// @notice 所有调用的唯一入口(无身份判断)
fallback() external payable {
(bool success, ) = implementation.delegatecall(msg.data);
require(success, "");
}
receive() external payable {}
}
/// @notice UUPS V1: 业务函数 + 升级函数都在这里
contract UUPS1 {
address public implementation; // slot 0 对齐
address public admin; // slot 1 对齐
string public words; // slot 2 对齐
function foo() public {
words = "old";
}
/// @notice 升级函数(UUPS 核心)
/// delegatecall 时 msg.sender 保持为原始调用者
/// implementation 写入 Proxy 的 slot 0
function upgrade(address newImplementation) external {
require(msg.sender == admin);
implementation = newImplementation;
}
}
/// @notice UUPS V2: 升级后的版本
contract UUPS2 {
address public implementation;
address public admin;
string public words;
function foo() public {
words = "new";
}
// ⚠️ 必须保留 upgrade 函数!否则永久无法再升级
function upgrade(address newImplementation) external {
require(msg.sender == admin);
implementation = newImplementation;
}
}
⚠️ UUPS 的致命风险
每个版本的 Logic 都必须包含 upgrade() 函数!
如果某个版本忘记写 upgrade():
- Proxy 自身没有 upgrade(UUPS 设计)
- Logic 也没有 upgrade
- 永远无法再更换 Logic 地址
- 合约升级能力永久丢失
四种模式横向对比
| 对比项 | 基础代理 | 简单升级 | 透明代理 | UUPS |
|---|---|---|---|---|
| upgrade 位置 | 无 | Proxy | Proxy | Logic |
| 选择器冲突 | 无(无管理函数) | ⚠️ 可能冲突 | ✅ 已解决 | ✅ 已解决 |
| Gas 开销 | 低 | 低 | 高(每次判断身份) | 低 |
| 安全风险 | 不可升级 | 选择器冲突 | 低 | 忘写upgrade则永久锁死 |
| 复杂度 | 简单 | 简单 | 中等 | 中等 |
| 代表项目 | - | - | OpenZeppelin | EIP-1822 |
升级注意事项
- 存储布局永远不能变:只能在末尾追加变量,不能删除或修改已有变量的顺序/类型
- Logic 的构造函数不会执行:delegatecall 不会触发 Logic 的 constructor,初始化需要用
initialize()函数 - 变量名不重要:EVM 只认 slot 编号,变量名和 public/private 都是编译器语法糖
正确的升级布局:
V1: [slot0: impl] [slot1: admin] [slot2: words]
V2: [slot0: impl] [slot1: admin] [slot2: words] [slot3: newVar] ← 只能追加
错误的升级布局:
V1: [slot0: impl] [slot1: admin] [slot2: words]
V2: [slot0: impl] [slot1: newVar] [slot2: admin] [slot3: words] ← 改了顺序!数据错乱!