代理模式与合约升级

代理模式与合约升级

代理模式与合约升级

概述

智能合约一旦部署就不可修改——这是区块链的核心特性。但实际开发中,我们需要修复 bug、添加功能。代理模式(Proxy Pattern)解决了这个矛盾。

章节模式核心思想
16基础代理delegatecall 实现数据与逻辑分离
17简单升级代理Proxy 中增加 upgrade 函数
18透明代理根据调用者身份路由,解决选择器冲突
19UUPS 代理升级逻辑放在 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() + 业务函数

为什么这样更好?

  1. Proxy 没有任何函数 → 不可能产生选择器冲突
  2. 省 gas:不需要每次判断 msg.sender 是否为 admin
  3. 更灵活:每个版本的 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 位置ProxyProxyLogic
选择器冲突无(无管理函数)⚠️ 可能冲突✅ 已解决✅ 已解决
Gas 开销高(每次判断身份)
安全风险不可升级选择器冲突忘写upgrade则永久锁死
复杂度简单简单中等中等
代表项目--OpenZeppelinEIP-1822

升级注意事项

  1. 存储布局永远不能变:只能在末尾追加变量,不能删除或修改已有变量的顺序/类型
  2. Logic 的构造函数不会执行:delegatecall 不会触发 Logic 的 constructor,初始化需要用 initialize() 函数
  3. 变量名不重要: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] ← 改了顺序!数据错乱!