本文最后更新于 2024-08-13,本文发布时间距今超过 90 天, 文章内容可能已经过时。最新内容请以官方内容为准

UUPS 代理合约

引言 🌐

在区块链开发的多变领域中,合约的可升级性是一个关键特性,它允许开发者随着时间推移更新和改进他们的合约。**UUPS(通用可升级代理标准)**是一个关键的标准,它在 Solidity 中实现了合约的可升级性。这个标准特别有趣,因为它通过将升级函数直接放在逻辑合约中,解决了选择器冲突问题。

UUPS 是什么?🤔

UUPS 代理模式是用于创建可升级智能合约的设计。它建立在透明代理模式之上,但将升级合约的责任转移到逻辑合约本身。这种方法确保只有授权实体可以升级合约,并且简化了管理代理和逻辑合约的过程。

关键组件:

  • 代理合约:保存实现地址,并将所有调用委托给逻辑合约。
  • 逻辑合约(实现):包含业务逻辑和升级函数。
  • 升级函数:由逻辑合约管理,允许在保留状态的同时更新实现合约。

为什么使用 UUPS?🚀

UUPS 旨在高效灵活,允许以最小的燃气成本无缝升级。实现合约中的升级逻辑还提供了更清晰的架构,避免了与函数选择器相关的潜在安全问题。

实现细节 🔧

UUPS 的逻辑流程

让我们将 UUPS 模式分解为其核心组件:

  1. 代理合约

    • 该合约存储实现地址。
    • 它将所有调用委托给实现合约。
  2. 逻辑合约

    • 包含升级函数。
    • 处理核心业务逻辑。
  3. 升级过程

    • 代理合约将调用委托给逻辑合约。
    • 逻辑合约中的升级函数更新代理中的实现地址。

流程图 🎨

graph TD;
    A[用户调用] --> B[代理合约];
    B --> C[委托给逻辑合约];
    C --> D[执行逻辑];
    B --> E[升级逻辑];
    E --> F[更改实现地址];

合约调用逻辑:
upgradableContract.png

代码实现 📝

让我们通过一个完整的示例,包括代理部署,来增强初始实现。

UUPSCallGraph

代理合约

UUPS 的代理合约看起来像是个不可升级的代理合约,非常简单,因为升级函数被放在了逻辑合约中。它包含 3 个变量:

  • implementation:逻辑合约地址。
  • admin:admin 地址。
    -words:字符串,可以通过逻辑合约的函数改变。

它包含 2 个函数

  • 构造函数:初始化 admin 和逻辑合约地址。
  • fallback():回调函数,将调用委托给逻辑合约。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract UUPSProxy {
    address public implementation; // 逻辑合约地址
    address public admin; // admin地址
    string public words; // 字符串,可以通过逻辑合约的函数改变

    // 构造函数,初始化admin和逻辑合约地址
    constructor(address _implementation){
        admin = msg.sender;
        implementation = _implementation;
    }

    // fallback函数,将调用委托给逻辑合约
    fallback() external payable {
        (bool success, bytes memory data) = implementation.delegatecall(msg.data);
    }
}

逻辑合约

UUPS 的逻辑合约与透明代理中的不同是多了个升级函数。UUPS 逻辑合约包含 3 个状态变量,与保持代理合约一致,防止插槽冲突。它包含 2 个

  • upgrade():升级函数,将改变逻辑合约地址 implementation,只能由 admin 调用。
  • foo():旧 UUPS 逻辑合约会将 words 的值改为"old",新的会改为"new"。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

// UUPS逻辑合约(升级函数写在逻辑合约内)
contract UUPS1{
    // 状态变量和proxy合约一致,防止插槽冲突
    address public implementation; 
    address public admin; 
    string public words; // 字符串,可以通过逻辑合约的函数改变

    // 改变proxy中状态变量,选择器: 0xc2985578
    function foo() public{
        words = "old";
    }

    // 升级函数,改变逻辑合约地址,只能由admin调用。选择器:0x0900f010
    // UUPS中,逻辑函数中必须包含升级函数,不然就不能再升级了。
    function upgrade(address newImplementation) external {
        require(msg.sender == admin);
        implementation = newImplementation;
    }
}

// 新的UUPS逻辑合约
contract UUPS2{
    // 状态变量和proxy合约一致,防止插槽冲突
    address public implementation; 
    address public admin; 
    string public words; // 字符串,可以通过逻辑合约的函数改变

    // 改变proxy中状态变量,选择器: 0xc2985578
    function foo() public{
        words = "new";
    }

    // 升级函数,改变逻辑合约地址,只能由admin调用。选择器:0x0900f010
    // UUPS中,逻辑函数中必须包含升级函数,不然就不能再升级了。
    function upgrade(address newImplementation) external {
        require(msg.sender == admin);
        implementation = newImplementation;
    }
}

示例用法 🛠️

以下是在真实场景中如何使用这些合约的示例:

  1. 部署 old 逻辑合约:这是您的初始逻辑合约。
  2. 部署 代理合约:这个代理将委托调用给逻辑合约
  3. 通过代理初始化:您将通过代理调用initialize函数来设置初始状态。

当需要升级时:

  1. 部署 new 逻辑合约:这是您的逻辑合约的新版本。
  2. 升级代理:在代理上调用upgradeTo函数,该函数反过来调用_authorizeUpgrade方法来执行升级。

安全考虑

  • 总是在_authorizeUpgrade函数中包含访问控制。
  • _canUpgrade方法保持谨慎,因为它定义了谁可以触发升级。

结论

UUPS 代理提供了一种强大的解决方案,用于创建可升级的智能合约。通过理解模式并仔细实施,开发者可以确保他们的合约长期保持灵活性和安全性。

参考资料 📚

这个增强版教程为实现和理解 UUPS 代理合约提供了全面的指南,包括详细的代码示例和安全考虑。