Open Zeppelin 透明可升级代理部署问题

  • vinicaboy
  • 发布于 2024-12-06 17:21
  • 阅读 31

文章详细讨论了在使用Open Zeppelin的透明可升级代理模式时遇到的问题,特别是在部署和管理代理合约时,代理管理员之间的交互问题。

这是系列文章的第一篇,我将分享在公有和私有审计期间遇到的问题。我将重点关注那些不特定于某个特定代码库,而是在不同项目中普遍存在的通用问题。

问题发现的背景

问题是在 CodeHawks 上的 One World Project 审计竞赛中发现的。一个相对较小的代码库(约 500 行),涉及 DAO 创建、成员管理、KYC 代币和奖励分配机制。

我们在此不讨论的先决条件

  • 什么是代理模式?
  • delegate 调用是如何工作的?

关于 Open Zeppelin 透明可升级代理的实现

为了理解这个问题及其根本原因,我们首先需要了解透明可升级代理的工作方式。

在透明代理中,msg.sender 用于区分操作是管理员尝试升级代理还是普通用户交互,如 Open Zeppelin 实现 中所示。如果调用者是代理的管理员,代理将不会委托任何调用,只会响应它理解的管理消息。如果调用者是其他地址,代理将始终委托调用,无论它是否与代理的某个函数匹配:

function _fallback() internal virtual override {
    if (msg.sender == _proxyAdmin()) {
       if (msg.sig != ITransparentUpgradeableProxy.upgradeToAndCall.selector) {
           revert ProxyDeniedAdminAccess();
       } else {
           _dispatchUpgradeToAndCall();
       }
    } else {
        super._fallback();
    }
 }

直观上,_proxyAdmin() 只是一个简单的地址,通常设置为部署合约的地址。但在这里并非如此:

constructor(address _logic, address initialOwner, bytes memory _data) payable ERC1967Proxy(_logic, _data) {
    _admin = address(new ProxyAdmin(initialOwner));
    // 设置存储值并发出事件以兼容 ERC-1967
    ERC1967Utils.changeAdmin(_proxyAdmin());
}
function _proxyAdmin() internal view virtual returns (address) {
    return _admin;
}

TransparentUpgradeableProxy 构造函数中,部署了一个 ProxyAdmin 合约,允许指定的 initialOwner 管理它。ProxyAdmin 只有一个 upgradeAndCall() 函数,用于将调用转发给代理:

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

代理的 upgradeToAndCall() 如下所示:

function upgradeToAndCall(address newImplementation, bytes memory data) internal {
    _setImplementation(newImplementation);
    emit IERC1967.Upgraded(newImplementation);

    if (data.length > 0) {
        Address.functionDelegateCall(newImplementation, data);
    } else {
        _checkNonPayable();
    }
}

下图展示了用户和代理所有者之间的交互流程:

现在,了解了 OZ 如何实现 TransparentUpgradeableProxy 后,我们可以深入探讨这个问题。

问题所在

问题出现在一个工厂合约中,该合约的功能是部署新的成员合约。成员合约是通过 TransparentUpgradeableProxy 部署的。

工厂合约有一个状态变量 ProxyAdmin proxyAdmin,它在构造函数中初始化。以下是用于创建新 TransparentUpgradeableProxy 的函数的简化版本:

function createNewDAOMembership(){

    TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(
        membershipImplementation,
        address(proxyAdmin), // 初始所有者将是状态变量 proxyAdmin
        abi.encodeWithSignature("initialize()")
    );
 }

你发现问题了吗?让我们探讨一下在这种场景下合约之间的交互预期是如何发生的。为了简化引用,我们将不同的代理管理员命名为:

  • 作为代理状态变量存储的代理管理员将被称作 ProxyAdminA
  • 新部署的代理管理员在 TransparentUpgradeableProxy 内部将被称作 ProxyAdminB

根据上述图表的预期交互流程,ProxyAdminA(初始所有者)应该与 ProxyAdminB(新部署的代理管理员)进行交互,然后代理管理员与代理进行通信。然而,这种交互是不可行的,因为 ProxyAdmin 合约只有一个函数:

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

问题在于函数签名不匹配。ProxyAdmin 函数接受三个参数(proxyimplementationdata),但它内部调用的函数只接受两个参数(implementationdata)。这种不匹配使得两个 ProxyAdmin 合约之间的直接通信变得不可能。

这个问题的后果是什么?管理员将无法升级合约。

结束语

始终确保代理的部署处理正确。这类问题通常出现在智能合约中,直到管理员尝试升级代理时才会被发现。

感谢阅读!如果你想联系我,请在 X 上联系:@vinica_boy

  • 原文链接: medium.com/@vinicaboy/op...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
vinicaboy
vinicaboy
江湖只有他的大名,没有他的介绍。