openzeppelin 的Proxy
有两种:
- 透明代理 Transparent proxy
- UUPS proxy
两者的最主要的区别是, upgradeTo
函数逻辑的位置。在透明代理中, upgradeTo
函数逻辑在proxy合约中;而在 UUPS 代理中, upgradeTo
函数逻辑在实现合约中。
漏洞
openzeppelin 的代理合约通常都由几个合约组成。每个可升级的部署都包括一个实现合约,实现合约中是可升级合约的逻辑;一个代理合约,保存合约的状态(也就是存储)。当代理合约升级时,将代理合约的实现地址指向行的实现地址即可。
代理合约升级示意图:
在版本4.1.0-4.3.1中,UUPSUpgradeable 合约的 upgradeTo 函数没有设置权限,任何人都可以调用该函数。因此,可以构造一个攻击合约,在攻击合约的 upgradeTo
中调用 SELFDESTRUCT
, 然后调用实现合约的 upgradeTo
, 参数为我们的攻击合约,这将导致实现合约销毁自己(这里很重要的一点是,调用 SELFDESTRUCT 不会导致失败!),因此,代理合约的逻辑代码被销毁,导致整个合约宕机!
之前版本的 upgradeTo
函数的实现:
function upgradeTo(address newImplementation) external virtual {
_authorizeUpgrade(newImplementation);
_upgradeToAndCallSecure(newImplementation, bytes(""), false);
}
4.3.2 版本 upgradeTo
函数的实现:
modifier onlyProxy() {
require(address(this) != __self, "Function must be called through delegatecall");
require(_getImplementation() == __self, "Function must be called through active proxy");
_;
}
function upgradeTo(address newImplementation) external virtual onlyProxy {
_authorizeUpgrade(newImplementation);
_upgradeToAndCallSecure(newImplementation, new bytes(0), false);
}
可以看到,现在实现合约的 upgradeTo 只有 proxy 合约可以调用。
UUPS 代理合约 upgradeTo 原理
UUPS合约的部署步骤如下:
首先,部署实现合约, 实现合约需继承合约 ERC1967UpgradeUpgradeable
;
其次,部署 ERC1967Proxy 合约,在这个合约的初始化方法中,指定实现合约的地址为上一步部署的实现合约地址;
最后,调用 ERC1967Proxy 的 upgradeTo 函数。
upgradeTo 函数的代码:
function upgradeTo(address newImplementation) external virtual onlyProxy {
_authorizeUpgrade(newImplementation);
_upgradeToAndCallSecure(newImplementation, new bytes(0), false);
}
function _upgradeToAndCallSecure(
address newImplementation,
bytes memory data,
bool forceCall
) internal {
address oldImplementation = _getImplementation();
// Initial upgrade and setup call
_setImplementation(newImplementation);
if (data.length > 0 || forceCall) {
_functionDelegateCall(newImplementation, data);
}
// Perform rollback test if not already in progress
StorageSlotUpgradeable.BooleanSlot storage rollbackTesting = StorageSlotUpgradeable.getBooleanSlot(_ROLLBACK_SLOT);
if (!rollbackTesting.value) {
// Trigger rollback using upgradeTo from the new implementation
rollbackTesting.value = true;
_functionDelegateCall(
newImplementation,
abi.encodeWithSignature("upgradeTo(address)", oldImplementation)
);
rollbackTesting.value = false;
// Check rollback was effective
require(oldImplementation == _getImplementation(), "ERC1967Upgrade: upgrade breaks further upgrades");
// Finally reset to the new implementation and log the upgrade
_upgradeTo(newImplementation);
}
}
function _upgradeTo(address newImplementation) internal {
_setImplementation(newImplementation);
emit Upgraded(newImplementation);
}
function _setImplementation(address newImplementation) private {
require(AddressUpgradeable.isContract(newImplementation), "ERC1967: new implementation is not a contract");
StorageSlotUpgradeable.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation;
}
其中, _authorizeUpgrade 是实现合约的权限验证,任何继承 ERC1967UpgradeUpgradeable 的合约都必须实现这个函数。这个函数通常是限制只有owner才能调用。
在 _upgradeToAndCallSecure 中,对新的实现合约执行了回滚测试 (rollbackTesting),也就是,执行新的执行合约升级逻辑,使proxy升级到现在的实现合约,以确保新的实现合约的 upgradeTo 没有问题,确保新的实现合约可以继续升级。
当上述回滚测试没有问题时,将 _IMPLEMENTATION_SLOT
的地址设置为新的实现地址。
https://forum.openzeppelin.com/t/uupsupgradeable-vulnerability-post-mortem/15680