文章详细讨论了在使用Open Zeppelin的透明可升级代理模式时遇到的问题,特别是在部署和管理代理合约时,代理管理员之间的交互问题。
这是系列文章的第一篇,我将分享在公有和私有审计期间遇到的问题。我将重点关注那些不特定于某个特定代码库,而是在不同项目中普遍存在的通用问题。
问题是在 CodeHawks 上的 One World Project 审计竞赛中发现的。一个相对较小的代码库(约 500 行),涉及 DAO 创建、成员管理、KYC 代币和奖励分配机制。
为了理解这个问题及其根本原因,我们首先需要了解透明可升级代理的工作方式。
在透明代理中,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
函数接受三个参数(proxy
、implementation
和 data
),但它内部调用的函数只接受两个参数(implementation
和 data
)。这种不匹配使得两个 ProxyAdmin
合约之间的直接通信变得不可能。
这个问题的后果是什么?管理员将无法升级合约。
始终确保代理的部署处理正确。这类问题通常出现在智能合约中,直到管理员尝试升级代理时才会被发现。
感谢阅读!如果你想联系我,请在 X 上联系:@vinica_boy
- 原文链接: medium.com/@vinicaboy/op...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!