透明代理合约

本章讲解透明代理合约的产生背景、实现原理,以及如何编写透明代理合约。

视频Bilibili  |  Youtube
官网binschool.app
推特@BinSchool    DiscordBinDAO   微信:bkra50 

在智能合约编译成字节码的过程中,合约中定义的各个函数,在内部实际上都是使用函数签名来表示的。函数签名是一个 4 字节长度的序列,由函数名称和参数类型组合的哈希值,截取前 4 个字节组成。由于函数签名的长度比较短,只有 4 个字节,所以不同的函数可能会生成相同的签名,造成“函数签名冲突”。

关于函数签名的生成规则等相关内容,可以参考高级教程中的函数选择器章节。

在使用 Solidity 编写的单个智能合约内部,如果两个不同的函数声明产生了相同的函数签名,这将导致无法编译合约,因为合约内部无法区分这两个签名相同的函数。

我们看以下合约的例子:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract SelectorCollision {
  function burn(uint256) external pure{}
  function collate_propagate_storage(bytes16) external pure{}
}

两个不同的函数定义 burncollate_propagate_storage 就具有相同的函数签名。

我们将这个合约复制到 Remix 进行编译,就会报错:

TypeError: Function signature hash collision for collate_propagate_storage(bytes16)

在这种情况下,我们就必须调整函数的声明,确保每个函数都有一个独一无二的签名。

另一方面,如果这两个函数出现在两个不同的合约中,并不会构成问题。因为这两个合约是分开的实体,它们的内部函数签名冲突不会影响彼此的正常编译和运行。

不幸的是,在代理合约的模式下,虽然代理合约和逻辑合约是两个独立的合约,但是这两个合约中一旦出现函数签名冲突,也会导致非常严重的问题。

我们看一下合约的例子:

contract Porxy {
  function burn(uint256) external pure{}

  fallback external {
    //......
  }
}

contract Logic {
  function collate_propagate_storage(bytes16) external pure{}
}

其中,burncollate_propagate_storage 分别位于代理合约和逻辑合约中,它们的函数签名都是 0x42966c68

假设某个用户想要使用 proxy.call("0x42966c68"),调用逻辑合约中的函数 collate_propagate_storage,事实上却无法做到的,它会调用到代理合约中的函数 burn

所以,在代理合约模式下,函数签名冲突问题很难避免,这将会导致不可预料的结果。透明代理就是为了解决这类问题而产生的一种方案。

1. 透明代理的原理

在代理合约的架构中,主要涉及两种合约:代理合约 逻辑合约。逻辑合约承担着所有业务逻辑的实现,而代理合约则负责将调用者的请求转发给逻辑合约,并管理逻辑合约的升级。

这种架构创造了两种角色:普通用户(即普通合约交互者)和管理员(即所有者)。普通用户主要与逻辑合约交互,而管理员则负责维护和升级逻辑合约。为了清晰地界定这两种角色的职责,防止它们错误地使用不适当的功能,从而引发业务或安全问题,就需要对不同类型的用户进行权限划分。

比如,代理合约中的 upgrade 函数,只能由管理员使用,而不能由普通用户因为函数签名冲突问题而错误的调用。

透明代理(Transparent Proxy)正是在这样的背景下出现的。它是一种特殊的代理合约设计,能够区分普通用户和管理员的交互,并针对不同的用户采取不同的行为。

透明代理之所以被称为“透明”,是因为对于普通用户而言,代理的存在和行为基本上是不可见的。代理合约隐藏了实现细节,如逻辑合约的升级和更换。在透明代理模式下,用户与代理合约的交互就像是直接与实现合约进行交互一样,用户感知不到代理合约的存在。因此,“透明”在此指代理合约对普通用户而言的隐蔽性,这确保了用户体验的连续性和流畅性,同时为管理员提供了必要的合约维护和升级能力。

2. 透明代理的实现

透明代理合约能够通过 msg.sender 识别发起交易的是普通用户还是管理员。

对于管理员,只能使用代理合约中定义的升级管理函数,不能通过 fallback 转发交易请求给逻辑合约。

对于普通用户,只能通过代理合约将调用委托给逻辑合约,不能使用代理合约中定义的升级管理函数。

透明代理合约的实现代码:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

// 透明代理合约
contract TransparentProxy {
    // 逻辑合约地址
    address public logicContractAddress;
    // 合约管理员地址
    address public admin;

    // 构造函数
    constructor(address _logicContractAddress) {
        // 设置逻辑合约地址
        logicContractAddress = _logicContractAddress;
        // 合约管理员地址
        admin = msg.sender;
    }
    // 调用逻辑合约的 fallback 函数
    fallback() external {
        if (msg.sender == admin) {
            // 如果调用者是管理员,则返回
            return;
        }
        // 转发请求给逻辑合约
        (bool success, bytes memory result) = logicContractAddress.delegatecall(msg.data);
        require(success, "Forwarding request to logic contract failed");
        // 返回执行结果给调用者
        assembly {
            return(add(result, 0x20), mload(result))
        }
    }
    // 升级函数
    function upgrade(address _newLogicContractAddress) external {
        // 如果调用者是管理员则放行,否则拒绝
        require(msg.sender == admin, "Only admin can upgrade");
        // 设置新的逻辑合约地址
        logicContractAddress = _newLogicContractAddress;
    }
}

我们可以将上述合约使用 Remix 进行部署。测试时,通过切换合约部署者地址和另外一个地址,模拟管理员和普通用户,调用合约中不同的函数来检验权限划分情况。

3.使用 openzepplin 编写透明代理

我们可以使用 OpenZeppelin 的合约库来方便地实现一个透明代理合约,OpenZeppelin 的合约库为创建安全的智能合约提供了可靠的基础。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";

contract MyTransparentProxy is TransparentUpgradeableProxy {
    constructor(address _logic, address _admin, bytes memory _data)
        TransparentUpgradeableProxy(_logic, _admin, _data)
    {}
}

我们可以将上述合约使用 Remix 进行部署。测试时,通过切换合约部署者地址和另外一个地址,模拟管理员和普通用户,调用合约中不同的函数来检验权限划分情况。