文章详细解释了透明可升级代理模式,该模式旨在升级代理时消除函数选择器冲突的可能性。文章介绍了代理合约的基本需求、函数选择器冲突问题及其解决方案,并通过代码示例和图表深入探讨了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
(或代理中的任何其他公共函数)。
因此,如果在代理中声明了一个公共函数,则可能发生两种类型的函数选择器冲突:
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
的所有者,我们可以更改谁有能力升级透明代理。
以下是 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);
}
}
有一种常见误解,认为透明代理的管理员无法使用合约,因为他们的调用被转发到升级逻辑。然而,AdminProxy
的 owner
可以毫无问题地使用 Proxy
,如下图所示。
实际上,我们稍后会看到,ProxyAdmin
有机制可以对代理进行任意调用,就像 upgradeToAndCall()
函数名称所暗示的那样。
如果 owner
被更改为...
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!