透明代理合约
本章讲解透明代理合约的产生背景、实现原理,以及如何编写透明代理合约。
在智能合约编译成字节码的过程中,合约中定义的各个函数,在内部实际上都是使用函数签名来表示的。函数签名是一个 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{} }
两个不同的函数定义 burn 和 collate_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{} }
其中,burn 和 collate_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
进行部署。测试时,通过切换合约部署者地址和另外一个地址,模拟管理员和普通用户,调用合约中不同的函数来检验权限划分情况。