透明可升级代理

  • RareSkills
  • 发布于 2024-06-06 18:37
  • 阅读 223

文章详细解释了透明可升级代理模式,该模式旨在升级代理时消除函数选择器冲突的可能性。文章介绍了代理合约的基本需求、函数选择器冲突问题及其解决方案,并通过代码示例和图表深入探讨了OpenZeppelin的实现细节。

透明可升级代理模式详细解释

透明可升级代理是一种设计模式,用于在升级代理的同时消除函数选择器冲突的可能性。

一个功能齐全的以太坊代理至少需要以下两个特性:

  • 一个存储槽,保存实现合约的地址
  • 一个机制,允许管理员更改实现地址

ERC-1967 标准规定了实现地址应存储的位置,以最小化存储碰撞的机会。然而,ERC-1967 标准并没有规定如何更改实现地址。

将一个额外的函数放置在代理中以更改实现(例如 updateImplementation(address _newImplementation))的问题在于,更新函数有非忽略的机会与实现中的某个函数发生冲突。

函数选择器冲突

在代理中声明公共函数来更新实现地址会引入函数选择器冲突的可能性。

这里是一个简单的例子:

contract ProxyUnsafe {

    function changeImplementation(
            address newImplementation
    ) public {
        // 一些代码...
    }

    fallback(bytes calldata data) external payable (bytes memory) {
        (bool ok, bytes memory data) = getImplementation().delegatecall(data);
        require(ok, "delegatecall 失败");
        return data;
    }
}

contract Implementation {
    // 这里声明了一个相同的函数 -- 它们将发生冲突
    function changeImplementation(
            address newImplementation
    ) public {

    }
    //...
}

请记住,fallback 始终最后检查。 在调用 fallback 之前,代理合约会检查 4 字节函数选择器是否匹配 changeImplementation(或代理中的任何其他公共函数)。

因此,如果在代理中声明了一个公共函数,则可能发生两种类型的函数选择器冲突:

  1. 如果实现合约实现了一个具有相同签名的函数,则该函数将无法调用,因为代理的公共函数会被调用,而不是 fallback。如果不触发 fallback,就不会有对实现的 delegatecall
  2. 如果实现合约有一个与代理中的公共函数相同的函数选择器,它也将无法调用,原因相同。这种情况是一种随机的函数选择器冲突,当四个字节匹配时可能会发生。两个不同的函数具有相同选择器的概率是 1 在 42.9 亿中;函数选择器由 4 个字节组成,因此有 42.9 亿种可能性。这是一个小概率,但不是可忽略的。例如,clash550254402()proxyAdmin() 具有相同的函数选择器。

透明可升级代理模式完全防止函数选择器冲突

透明可升级代理模式是一种设计模式,旨在完全消除函数选择器冲突的可能性。

具体来说,透明可升级代理模式规定代理上不应该有公共函数,除了 fallback

但是只有一个 fallback 函数,我们如何调用用于升级代理的函数呢?

答案是检测 msg.sender 是否是管理员。

contract Proxy is ERC1967 {
    address immutable admin;

    constructor(address admin_) {
        admin = admin_;
    }

    fallback() external payable {
        if (msg.sender == admin) {
            // 升级逻辑
        } else {
            // delegatecall 到实现
        }
    }
}

这意味着管理员无法直接使用代理,因为他们的调用总是被路由到升级逻辑。然而,使用我们稍后会讨论的不同机制,管理员仍然可以调用代理,代理作为普通交易进行到实现的 delegatecall。

更改不可变的管理员

在上述代码片段中,管理员是不可变的。这意味着合约在技术上不符合 ERC-1967 的要求,后者规定管理员必须保存在存储槽 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103 或者 bytes32(uint256(keccak256('eip1967.proxy.admin')) - 1) 中。

为了兼容 ERC-1967,透明可升级代理在该存储槽中存储管理员的地址,但并不使用该存储变量。

该存储槽中地址的存在将向区块探测器发出信号,表明合约是一个代理(这是 ERC-1967 的一个意图)。然而,每次对代理的调用都从存储中读取,增加了额外的 2100 Gas成本。因此,使用不可变变量是可取的。

“更改”管理员

然而,仍然希望能够更新管理员地址 — 但最初这似乎是不可能的,因为代理使用了一个不可变变量。

透明可升级代理允许更改代理合约管理员的方式有两个方面。首先,它指定另一个合约,称为 ProxyAdmin,作为代理合约的管理员。

展示代理、代理管理员和所有者在透明可升级代理中的关系的图

智能合约的地址永远不会改变,因此这与透明可升级代理将管理员地址存储在不可变变量中是兼容的。

第二,ProxyAdmin所有者是真正的管理员。ProxyAdmin 仅将调用从 owner 路由到 Proxy。真正的管理员调用 ProxyAdmin,然后 ProxyAdmin 调用透明代理。通过更改 ProxyAdmin 的所有者,我们可以更改谁有能力升级透明代理。

AdminProxy

以下是 OpenZeppelin AdminProxy 中的代码(已删除评论)。请注意,只有一个函数 upgradeAndCall(),它只能调用 Proxy 上的 upgradeToAndCall() 方法。

pragma solidity ^0.8.20;

import {ITransparentUpgradeableProxy} from "./TransparentUpgradeableProxy.sol";
import {Ownable} from "../../access/Ownable.sol";

contract ProxyAdmin is Ownable {
    string public constant UPGRADE_INTERFACE_VERSION = "5.0.0";

    constructor(address initialOwner) Ownable(initialOwner) {}

    function upgradeAndCall(
        ITransparentUpgradeableProxy proxy,
        address implementation,
        bytes memory data
    ) public payable virtual onlyOwner {
        proxy.upgradeToAndCall{value: msg.value}(implementation, data);
    }
}

有一种常见误解,认为透明代理的管理员无法使用合约,因为他们的调用被转发到升级逻辑。然而,AdminProxyowner 可以毫无问题地使用 Proxy,如下图所示。

实际上,我们稍后会看到,ProxyAdmin 有机制可以对代理进行任意调用,就像 upgradeToAndCall() 函数名称所暗示的那样。

透明可升级代理中所有者和用户可能的调用路径图

使代理不可升级

如果 owner 被更改为...

剩余50%的内容订阅专栏后可查看

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
RareSkills
RareSkills
https://www.rareskills.io/