合约升级

微信扫码分享

合约升级 (Contract Upgrade)

概念简介

合约升级是指在以太坊等区块链上,通过特定的设计模式改变智能合约的逻辑或修复漏洞,同时保留原有合约状态(数据)和地址的过程。这是区块链开发中最具挑战性的技术之一,因为它试图在不可变(Immutable)的环境中实现可变性(Mutability)。

核心矛盾:

区块链的本质:
- 代码一旦部署,永久不可更改
- 保证信任和确定性

现实需求:
- 修复安全漏洞
- 添加新功能
- 适应业务需求变化
- 优化性能和降低Gas成本

合约升级:在不可变性和灵活性之间的妥协方案

历史演进:

  • 2016年 DAO 事件:暴露了无法升级合约的严重问题,最终导致以太坊硬分叉
  • 2017年 Parity 多签钱包漏洞:由于无法升级,超过50万ETH被永久冻结
  • 2018年后:代理模式成为行业标准,OpenZeppelin等提供升级框架
  • 2020年后:UUPS、BeaconProxy、Diamond等高级模式成熟

市场规模:

据统计,截至2024年:

  • 可升级合约占比:Top 100 DeFi协议中,85%+使用可升级合约
  • 代理合约数量以太坊主网上超过10万个代理合约
  • 管理的资产:超过$50B的资产由可升级合约管理
  • 安全事件:30%+的合约漏洞与升级机制相关

核心特性

代理模式 (Proxy Pattern)

基本原理:

代理模式是实现合约升级的核心机制,通过分离"存储"和"逻辑"来实现升级。

传统合约:
┌─────────────────────┐
│   智能合约           │
│  ├─ 业务逻辑        │
│  └─ 状态数据        │
└─────────────────────┘
不可变:无法修改

代理模式:
┌──────────────┐      delegatecall      ┌──────────────┐
│ 代理合约      │ ───────────────────>  │ 逻辑合约      │
│ (Proxy)      │                        │ (Logic)      │
│              │                        │              │
│ - 存储数据    │                        │ - 业务逻辑    │
│ - 资产余额    │                        │ - 函数代码    │
│ - 用户交互    │                        │ - 无状态      │
└──────────────┘                        └──────────────┘
     ↓
  可升级:更换逻辑合约地址

delegatecall 机制:

// Proxy Contract
contract Proxy {
    address public implementation;  // 逻辑合约地址

    fallback() external payable {
        address _impl = implementation;

        assembly {
            // 复制calldata
            calldatacopy(0, 0, calldatasize())

            // delegatecall调用逻辑合约
            let result := delegatecall(
                gas(),
                _impl,
                0,
                calldatasize(),
                0,
                0
            )

            // 复制返回数据
            returndatacopy(0, 0, returndatasize())

            // 根据结果返回或revert
            switch result
            case 0 { revert(0, returndatasize()) }
            default { return(0, returndatasize()) }
        }
    }
}

// Logic Contract
contract Logic {
    uint256 public value;  // 实际存储在Proxy的slot 0

    function setValue(uint256 _value) public {
        value = _value;  // 修改的是Proxy的存储
    }
}

关键特性:

delegatecall vs call:

call:
- 在被调用合约的上下文中执行
- msg.sender = 调用者
- 修改被调用合约的存储

delegatecall:
- 在调用者合约的上下文中执行
- msg.sender = 原始调用者(穿透代理)
- 修改调用者合约的存储
- 完美适合代理模式

存储冲突 (Storage Collision)

最严重的升级风险:

// ❌ 错误示例:存储冲突

// Proxy Contract
contract Proxy {
    address public implementation;  // slot 0
    address public admin;           // slot 1

    // ... delegatecall logic
}

// Logic Contract V1
contract LogicV1 {
    uint256 public value;  // slot 0 ❌ 冲突!

    function setValue(uint256 _value) public {
        value = _value;  // 实际写入 Proxy 的 slot 0
        // 覆盖了 implementation 地址!
    }
}

结果:
- 执行 setValue 后,implementation 被覆盖
- 代理合约永久损坏
- 资金被锁定

解决方案:ERC-1967 标准

// ✅ 正确示例:使用ERC-1967

contract Proxy {
    // 使用伪随机slot,避免冲突
    bytes32 private constant IMPLEMENTATION_SLOT =
        bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1);

    function _getImplementation() internal view returns (address impl) {
        bytes32 slot = IMPLEMENTATION_SLOT;
        assembly {
            impl := sload(slot)
        }
    }

    function _setImplementation(address newImplementation) internal {
        bytes32 slot = IMPLEMENTATION_SLOT;
        assembly {
            sstore(slot, newImplementation)
        }
    }
}

// Logic Contract
contract Logic {
    uint256 public value;  // slot 0
    // 不会冲突,因为 Proxy 使用特殊 slot
}

ERC-1967 标准slot:

Implementation Slot:
0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc

Admin Slot:
0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103

Beacon Slot:
0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50

计算公式:
bytes32(uint256(keccak256('eip1967.proxy.[类型]')) - 1)

升级模式

透明代理 (Transparent Proxy)

问题背景:

函数选择器冲突:

Proxy Contract:
- upgradeTo(address) - 升级函数

Logic Contract:
- upgradeTo(address) - 业务函数(碰巧同名)

用户调用 upgradeTo:
- 应该调用哪个?
- 如果调用错误,可能导致意外升级

解决方案:

// OpenZeppelin Transparent Proxy
contract TransparentUpgradeableProxy {
    address private _admin;

    modifier ifAdmin() {
        if (msg.sender == _getAdmin()) {
            _;
        } else {
            _fallback();
        }
    }

    // 管理员调用:执行代理函数
    function upgradeTo(address newImplementation) external ifAdmin {
        _upgradeTo(newImplementation);
    }

    // 普通用户调用:转发到逻辑合约
    fallback() external payable {
        require(msg.sender != _getAdmin(), "Admin cannot fallback");
        _fallback();
    }

    function _fallback() internal {
        _delegate(_getImplementation());
    }
}

工作原理:

角色分离:

Admin(管理员):
- 调用代理管理函数(upgradeTo、changeAdmin)
- 无法调用逻辑合约函数
- 通常是 MultiSig 或 DAO

User(普通用户):
- 调用逻辑合约函数
- 无法调用代理管理函数
- 通过 fallback 转发

示例:
Admin 调用 proxy.upgradeTo(newLogic):
  → 执行代理的升级函数

User 调用 proxy.setValue(100):
  → fallback → delegatecall → Logic.setValue(100)

优缺点:

优点:
+ 安全:彻底隔离管理和业务调用
+ 简单:用户无需关心升级机制
+ 成熟:OpenZeppelin 经过充分审计

缺点:
- Gas 高:每次调用都需检查 msg.sender
- 管理员无法调用业务函数(需要另一个地址)

UUPS代理 (Universal Upgradeable Proxy Standard)

EIP-1822 标准:

UUPS 是一种更高效的升级模式,将升级逻辑放在逻辑合约中而非代理合约。

核心思想:

传统代理:
- 升级逻辑在 Proxy 中
- Proxy 代码复杂、Gas 高

UUPS:
- 升级逻辑在 Logic 中
- Proxy 极简、Gas 低
- 通过 delegatecall 执行升级

实现:

// UUPS Proxy(极简)
contract UUPSProxy {
    bytes32 private constant IMPLEMENTATION_SLOT =
        bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1);

    constructor(address _logic, bytes memory _data) {
        _setImplementation(_logic);
        if (_data.length > 0) {
            (bool success,) = _logic.delegatecall(_data);
            require(success);
        }
    }

    fallback() external payable {
        _delegate(_getImplementation());
    }

    function _delegate(address impl) internal {
        assembly {
            calldatacopy(0, 0, calldatasize())
            let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
            returndatacopy(0, 0, returndatasize())
            switch result
            case 0 { revert(0, returndatasize()) }
            default { return(0, returndatasize()) }
        }
    }

    function _getImplementation() internal view returns (address impl) {
        bytes32 slot = IMPLEMENTATION_SLOT;
        assembly { impl := sload(slot) }
    }

    function _setImplementation(address newImplementation) internal {
        bytes32 slot = IMPLEMENTATION_SLOT;
        assembly { sstore(slot, newImplementation) }
    }
}

// UUPS Logic Contract(包含升级逻辑)
abstract contract UUPSUpgradeable {
    bytes32 private constant IMPLEMENTATION_SLOT =
        bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1);

    event Upgraded(address indexed implementation);

    // 升级函数(在逻辑合约中)
    function upgradeTo(address newImplementation) public virtual {
        _authorizeUpgrade(newImplementation);
        _upgradeToAndCall(newImplementation, bytes(""));
    }

    function _upgradeToAndCall(
        address newImplementation,
        bytes memory data
    ) internal {
        // 1. 更新 implementation slot
        bytes32 slot = IMPLEMENTATION_SLOT;
        assembly { sstore(slot, newImplementation) }

        emit Upgraded(newImplementation);

        // 2. 可选:调用新逻辑的初始化函数
        if (data.length > 0) {
            (bool success,) = newImplementation.delegatecall(data);
            require(success);
        }
    }

    // 子类必须实现权限检查
    function _authorizeUpgrade(address newImplementation) internal virtual;
}

// 实际业务合约
contract MyContract is UUPSUpgradeable {
    address public owner;
    uint256 public value;

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

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

    // 实现权限检查
    function _authorizeUpgrade(address) internal override {
        require(msg.sender == owner, "Not owner");
    }
}

升级流程:

1. 部署新逻辑合约 LogicV2
2. 调用 proxy.upgradeTo(LogicV2)
   ↓
3. Proxy fallback → delegatecall → LogicV1.upgradeTo()
   ↓
4. LogicV1.upgradeTo() 执行:
   - _authorizeUpgrade(LogicV2)  // 权限检查
   - sstore(IMPLEMENTATION_SLOT, LogicV2)  // 更新地址
   ↓
5. 后续调用都转发到 LogicV2

优缺点:

优点:
+ Gas 效率高:Proxy 极简,每次调用省 Gas
+ 灵活:每个逻辑合约可定制升级逻辑
+ 无管理员限制:管理员可正常调用业务函数

缺点:
- 风险高:如果逻辑合约忘记继承 UUPSUpgradeable
  → 无法升级,永久锁定
- 复杂:开发者必须正确实现升级逻辑

安全检查:

// OpenZeppelin 的 UUPS 安全检查
contract UUPSUpgradeable {
    address private immutable __self = address(this);

    function upgradeTo(address newImplementation) public virtual {
        require(address(this) != __self, "Must be called through proxy");
        // 防止直接调用逻辑合约的升级函数

        _authorizeUpgrade(newImplementation);
        _upgradeToAndCall(newImplementation, bytes(""));
    }
}

Beacon代理 (Beacon Proxy)

应用场景:

问题:
部署了 1,000 个相同逻辑的代理合约
(例如:1,000 个用户的个人钱包合约)

升级需求:
- 修复逻辑合约的漏洞
- 需要升级所有 1,000 个代理

传统方案:
- 调用 1,000 次 upgradeTo
- Gas 成本:1,000 × 100,000 gas = 极高
- 操作复杂度:高

Beacon 方案:
- 所有代理指向同一个 Beacon
- 升级 Beacon 一次
- 所有代理自动升级

架构:

┌─────────────┐
│ BeaconProxy │ ──┐
│   #1        │   │
└─────────────┘   │
                  │
┌─────────────┐   │      ┌─────────────┐      ┌─────────────┐
│ BeaconProxy │ ──┼─────>│   Beacon    │────> │    Logic    │
│   #2        │   │      │             │      │             │
└─────────────┘   │      └─────────────┘      └─────────────┘
                  │
┌─────────────┐   │
│ BeaconProxy │ ──┘
│   #1000     │
└─────────────┘

升级:仅需更新 Beacon 中的 Logic 地址

实现:

// Beacon Contract
contract UpgradeableBeacon {
    address private _implementation;
    address private _owner;

    event Upgraded(address indexed implementation);

    constructor(address implementation_) {
        _setImplementation(implementation_);
        _owner = msg.sender;
    }

    // 获取当前逻辑合约地址
    function implementation() public view returns (address) {
        return _implementation;
    }

    // 升级逻辑合约
    function upgradeTo(address newImplementation) public {
        require(msg.sender == _owner, "Not owner");
        _setImplementation(newImplementation);
    }

    function _setImplementation(address newImplementation) private {
        require(newImplementation.code.length > 0, "Not a contract");
        _implementation = newImplementation;
        emit Upgraded(newImplementation);
    }
}

// Beacon Proxy
contract BeaconProxy {
    address private immutable _beacon;

    constructor(address beacon, bytes memory data) {
        _beacon = beacon;
        if (data.length > 0) {
            (bool success,) = _implementation().delegatecall(data);
            require(success);
        }
    }

    // 从 Beacon 获取逻辑合约地址
    function _implementation() internal view returns (address) {
        return IBeacon(_beacon).implementation();
    }

    fallback() external payable {
        _delegate(_implementation());
    }

    function _delegate(address impl) internal {
        assembly {
            calldatacopy(0, 0, calldatasize())
            let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
            returndatacopy(0, 0, returndatasize())
            switch result
            case 0 { revert(0, returndatasize()) }
            default { return(0, returndatasize()) }
        }
    }
}

使用示例:

// 1. 部署逻辑合约
Logic logic = new Logic();

// 2. 部署 Beacon
UpgradeableBeacon beacon = new UpgradeableBeacon(address(logic));

// 3. 部署多个 Beacon Proxy
for (uint i = 0; i < 1000; i++) {
    new BeaconProxy(address(beacon), initData);
}

// 4. 升级:仅需一次
LogicV2 logicV2 = new LogicV2();
beacon.upgradeTo(address(logicV2));
// 所有 1000 个代理自动使用新逻辑

优缺点:

优点:
+ 批量升级:一次升级影响所有代理
+ Gas 高效:升级成本 O(1)
+ 管理简单:中心化控制

缺点:
- 单点故障:Beacon 被攻击影响所有代理
- 灵活性低:无法单独升级某个代理
- 中心化:所有代理强制统一版本

Diamond代理 (Diamond Standard / EIP-2535)

突破24KB限制:

以太坊合约大小限制:
- Spurious Dragon 升级后:24 KB
- 复杂 DeFi 协议经常超出

传统解决方案:
- 拆分多个合约,手动协调
- 复杂且易出错

Diamond 方案:
- 一个代理委托给多个 Facet(逻辑合约)
- 突破大小限制
- 模块化升级

架构:

                     DiamondProxy
                          │
         ┌────────────────┼────────────────┐
         │                │                │
         ↓                ↓                ↓
    ┌─────────┐      ┌─────────┐      ┌─────────┐
    │ Facet A │      │ Facet B │      │ Facet C │
    │         │      │         │      │         │
    │ func1() │      │ func3() │      │ func5() │
    │ func2() │      │ func4() │      │ func6() │
    └─────────┘      └─────────┘      └─────────┘

函数选择器映射:
func1 → Facet A
func2 → Facet A
func3 → Facet B
func4 → Facet B
func5 → Facet C
func6 → Facet C

核心数据结构:

// Diamond Storage
library LibDiamond {
    bytes32 constant DIAMOND_STORAGE_POSITION =
        keccak256("diamond.standard.diamond.storage");

    struct FacetAddressAndSelectorPosition {
        address facetAddress;
        uint16 selectorPosition;
    }

    struct DiamondStorage {
        // 函数选择器 → Facet 地址映射
        mapping(bytes4 => FacetAddressAndSelectorPosition) selectorToFacet;
        // Facet 地址 → 函数选择器数组
        mapping(address => bytes4[]) facetToSelectors;
    }

    function diamondStorage() internal pure returns (DiamondStorage storage ds) {
        bytes32 position = DIAMOND_STORAGE_POSITION;
        assembly {
            ds.slot := position
        }
    }
}

// Diamond Proxy
contract Diamond {
    constructor(address _diamondCutFacet) {
        // 初始化 DiamondCut Facet(用于管理升级)
        LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage();
        ds.selectorToFacet[IDiamondCut.diamondCut.selector].facetAddress = _diamondCutFacet;
    }

    fallback() external payable {
        LibDiamond.DiamondStorage storage ds = LibDiamond.diamondStorage();
        // 根据函数选择器找到对应的 Facet
        address facet = ds.selectorToFacet[msg.sig].facetAddress;
        require(facet != address(0), "Function does not exist");

        // delegatecall 到 Facet
        assembly {
            calldatacopy(0, 0, calldatasize())
            let result := delegatecall(gas(), facet, 0, calldatasize(), 0, 0)
            returndatacopy(0, 0, returndatasize())
            switch result
            case 0 { revert(0, returndatasize()) }
            default { return(0, returndatasize()) }
        }
    }
}

升级操作:

interface IDiamondCut {
    enum FacetCutAction { Add, Replace, Remove }

    struct FacetCut {
        address facetAddress;
        FacetCutAction action;
        bytes4[] functionSelectors;
    }

    function diamondCut(
        FacetCut[] calldata _diamondCut,
        address _init,
        bytes calldata _calldata
    ) external;
}

// 使用示例
FacetCut[] memory cuts = new FacetCut[](3);

// 1. 添加新 Facet
cuts[0] = FacetCut({
    facetAddress: address(newFacet),
    action: FacetCutAction.Add,
    functionSelectors: [newFunc1.selector, newFunc2.selector]
});

// 2. 替换现有函数
cuts[1] = FacetCut({
    facetAddress: address(updatedFacet),
    action: FacetCutAction.Replace,
    functionSelectors: [existingFunc.selector]
});

// 3. 移除函数
cuts[2] = FacetCut({
    facetAddress: address(0),
    action: FacetCutAction.Remove,
    functionSelectors: [oldFunc.selector]
});

diamond.diamondCut(cuts, address(0), "");

优缺点:

优点:
+ 突破 24KB 限制:无限扩展
+ 模块化升级:细粒度控制
+ 代码复用:Facet 可共享
+ 灵活性:添加/替换/删除函数

缺点:
- 复杂度极高:学习和维护成本
- Gas 开销:函数查找额外成本
- 安全风险:复杂性带来更多攻击面
- 采用率低:生态支持不如传统代理

初始化模式

构造函数问题

为什么不能用构造函数?

// ❌ 错误:逻辑合约使用构造函数
contract Logic {
    address public owner;

    constructor() {
        owner = msg.sender;  // ❌ 只在 Logic 合约部署时执行
                             // Proxy 调用时 owner 为 0
    }
}

问题:
- 构造函数在合约部署时执行
- Logic 部署时,owner = Logic部署者
- Proxy 调用 Logic 时,owner 未初始化

解决方案:initialize 函数

// ✅ 正确:使用 initialize
contract Logic {
    address public owner;
    bool private initialized;

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

// Proxy 部署时调用
proxy = new Proxy(logic, abi.encodeWithSelector(
    Logic.initialize.selector,
    msg.sender
));

Initializable 模式

OpenZeppelin 实现:

abstract contract Initializable {
    uint8 private _initialized;
    bool private _initializing;

    modifier initializer() {
        bool isTopLevelCall = !_initializing;
        require(
            (isTopLevelCall && _initialized < 1) ||
            (!Address.isContract(address(this)) && _initialized == 1),
            "Initializable: contract is already initialized"
        );
        _initialized = 1;
        if (isTopLevelCall) {
            _initializing = true;
        }
        _;
        if (isTopLevelCall) {
            _initializing = false;
        }
    }

    modifier reinitializer(uint8 version) {
        require(!_initializing && _initialized < version, "Initializable: contract is already initialized");
        _initialized = version;
        _initializing = true;
        _;
        _initializing = false;
    }
}

// 使用
contract MyContract is Initializable {
    uint256 public value;

    function initialize(uint256 _value) public initializer {
        value = _value;
    }

    // V2 升级后的重新初始化
    function initializeV2(uint256 _newValue) public reinitializer(2) {
        value = _newValue * 2;
    }
}

存储间隔 (Storage Gap)

升级兼容性:

// ❌ 危险:升级导致存储冲突

// V1
contract LogicV1 {
    uint256 public value;
    // slot 0: value
}

// V2(添加新变量)
contract LogicV2 {
    address public owner;  // slot 0: owner ❌ 覆盖了 value!
    uint256 public value;  // slot 1
}

结果:
- V1 的 value 数据被解释为 address
- 数据损坏

正确做法:

// ✅ 正确:使用 storage gap

// V1
contract LogicV1 {
    uint256 public value;

    // 预留 50 个 slot
    uint256[49] private __gap;
}

// V2(安全添加新变量)
contract LogicV2 {
    uint256 public value;  // slot 0
    address public owner;  // slot 1(使用 gap 空间)

    // 减少 gap
    uint256[48] private __gap;  // 49 - 1 = 48
}

计算 gap 大小:

目标:未来可能添加的最大变量数量

例如:
- 预留 50 个 slot
- 当前使用 5 个
- gap = 50 - 5 = 45

每次添加 N 个变量:
- gap = gap - N

安全考虑

函数选择器冲突

问题:

// Proxy 和 Logic 同名函数
contract Proxy {
    function admin() public view returns (address);
}

contract Logic {
    function admin() public view returns (address);  // 冲突!
}

// 用户调用 proxy.admin()
// 应该执行哪个?

透明代理解决:

  • 管理员调用 Proxy 版本
  • 普通用户调用 Logic 版本

UUPS 解决:

  • 升级函数在 Logic 中,避免冲突

Delegatecall 安全

selfdestruct 攻击:

// ❌ 危险逻辑合约
contract MaliciousLogic {
    function destroy() public {
        selfdestruct(payable(msg.sender));
        // 通过 delegatecall 执行
        // → 销毁 Proxy 合约!
    }
}

防护:
- 永远不要在逻辑合约中使用 selfdestruct
- 审计升级的新逻辑

构造函数陷阱:

// ❌ 逻辑合约的构造函数不会被 Proxy 执行
contract Logic {
    uint256 public constant IMPORTANT = 100;

    constructor() {
        // 这里的代码只在 Logic 部署时执行
        // Proxy 调用时不会执行!
    }
}

时间锁 (Timelock)

延迟升级:

contract TimelockProxy {
    address public pendingImplementation;
    uint256 public upgradeTime;
    uint256 public constant DELAY = 2 days;

    // 提出升级
    function proposeUpgrade(address newImpl) public onlyAdmin {
        pendingImplementation = newImpl;
        upgradeTime = block.timestamp + DELAY;
        emit UpgradeProposed(newImpl, upgradeTime);
    }

    // 执行升级(必须等待)
    function executeUpgrade() public {
        require(block.timestamp >= upgradeTime, "Too early");
        require(pendingImplementation != address(0), "No pending upgrade");

        _setImplementation(pendingImplementation);
        pendingImplementation = address(0);

        emit Upgraded(pendingImplementation);
    }

    // 取消升级
    function cancelUpgrade() public onlyAdmin {
        pendingImplementation = address(0);
        emit UpgradeCancelled();
    }
}

Compound Timelock 示例:

// Compound 的 Timelock
contract Timelock {
    uint public constant GRACE_PERIOD = 14 days;
    uint public constant MINIMUM_DELAY = 2 days;
    uint public constant MAXIMUM_DELAY = 30 days;

    mapping (bytes32 => bool) public queuedTransactions;

    function queueTransaction(
        address target,
        uint value,
        string memory signature,
        bytes memory data,
        uint eta
    ) public returns (bytes32) {
        require(msg.sender == admin, "Unauthorized");
        require(eta >= getBlockTimestamp() + delay, "ETA must exceed delay");

        bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta));
        queuedTransactions[txHash] = true;

        emit QueueTransaction(txHash, target, value, signature, data, eta);
        return txHash;
    }

    function executeTransaction(
        address target,
        uint value,
        string memory signature,
        bytes memory data,
        uint eta
    ) public payable returns (bytes memory) {
        require(msg.sender == admin, "Unauthorized");

        bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta));
        require(queuedTransactions[txHash], "Not queued");
        require(getBlockTimestamp() >= eta, "Not yet");
        require(getBlockTimestamp() <= eta + GRACE_PERIOD, "Expired");

        queuedTransactions[txHash] = false;

        bytes memory callData = abi.encodePacked(bytes4(keccak256(bytes(signature))), data);
        (bool success, bytes memory returnData) = target.call{value: value}(callData);
        require(success, "Execution failed");

        emit ExecuteTransaction(txHash, target, value, signature, data, eta);
        return returnData;
    }
}

最佳实践

开发流程

1. 设计阶段:

决策树:

是否需要升级?
│
├─ 否 → 不可升级合约
│      优点:简单、安全、Gas 低
│      适用:简单工具合约、Token
│
└─ 是 → 选择升级模式
       │
       ├─ 单个合约 → Transparent 或 UUPS
       │           推荐:UUPS(Gas 低)
       │
       ├─ 批量合约 → Beacon Proxy
       │           例如:用户钱包、NFT
       │
       └─ 超大合约 → Diamond
                   例如:复杂 DeFi 协议

2. 实现阶段:

// 推荐结构

// 1. 导入 OpenZeppelin
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";

// 2. 继承顺序很重要
contract MyContract is
    Initializable,        // 第一个
    OwnableUpgradeable,
    UUPSUpgradeable       // 最后一个
{
    // 3. 状态变量
    uint256 public value;

    // 4. Storage Gap
    uint256[49] private __gap;

    // 5. 禁用构造函数
    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() {
        _disableInitializers();
    }

    // 6. Initialize 函数
    function initialize(uint256 _value) public initializer {
        __Ownable_init();
        __UUPSUpgradeable_init();
        value = _value;
    }

    // 7. 业务逻辑
    function setValue(uint256 _value) public onlyOwner {
        value = _value;
    }

    // 8. 升级授权
    function _authorizeUpgrade(address newImplementation)
        internal
        override
        onlyOwner
    {}
}

3. 测试阶段:

// Hardhat 测试示例
const { ethers, upgrades } = require("hardhat");

describe("Upgrade", function () {
    it("Should upgrade correctly", async function () {
        // 部署 V1
        const V1 = await ethers.getContractFactory("MyContractV1");
        const proxy = await upgrades.deployProxy(V1, [100], {
            kind: "uups"
        });

        // 验证初始状态
        expect(await proxy.value()).to.equal(100);

        // 升级到 V2
        const V2 = await ethers.getContractFactory("MyContractV2");
        const upgraded = await upgrades.upgradeProxy(proxy.address, V2);

        // 验证状态保留
        expect(await upgraded.value()).to.equal(100);

        // 验证新功能
        await upgraded.newFunction();
    });
});

工具支持

Hardhat Upgrades Plugin:

// hardhat.config.js
require("@openzeppelin/hardhat-upgrades");

module.exports = {
    solidity: "0.8.20",
};

// 部署脚本
async function main() {
    const MyContract = await ethers.getContractFactory("MyContract");

    // 自动处理代理部署
    const proxy = await upgrades.deployProxy(MyContract, [100], {
        initializer: "initialize",
        kind: "uups"  // 或 "transparent", "beacon"
    });

    console.log("Proxy deployed to:", proxy.address);

    // 验证升级安全性
    await upgrades.validateImplementation(MyContract);
}

Foundry 升级测试:

// test/Upgrade.t.sol
contract UpgradeTest is Test {
    Proxy proxy;
    LogicV1 logicV1;
    LogicV2 logicV2;

    function setUp() public {
        logicV1 = new LogicV1();
        proxy = new Proxy(address(logicV1), abi.encodeWithSelector(
            LogicV1.initialize.selector,
            100
        ));
    }

    function testUpgrade() public {
        LogicV1 proxyAsV1 = LogicV1(address(proxy));
        assertEq(proxyAsV1.value(), 100);

        // 升级
        logicV2 = new LogicV2();
        proxyAsV1.upgradeTo(address(logicV2));

        // 转换接口
        LogicV2 proxyAsV2 = LogicV2(address(proxy));

        // 验证状态保留
        assertEq(proxyAsV2.value(), 100);

        // 测试新功能
        proxyAsV2.newFunction();
    }
}

推荐阅读

相关概念

  • Delegatecall:实现代理模式的核心 EVM 操作码
  • Storage Layout:合约状态变量在存储中的排列方式
  • Initializable:可初始化合约基类
  • Timelock:延迟执行升级的治理机制
  • MultiSig:多签钱包,常用于管理升级权限
  • ERC-1967:代理存储槽标准
  • Function Selector:函数选择器,用于路由调用
  • Storage Collision:存储冲突
  • Immutable:不可变性
  • Diamond Pattern:钻石模式