透明可升级代理 (Transparent Upgradeable Proxy) 是一种用于升级代理合约的设计模式,它消除了发生函数选择器冲突 (function selector clash) 的可能性。
一个功能完备的 Ethereum 代理至少需要以下两个特性:
- 一个用于保存逻辑合约 (implementation contract) 地址的存储槽 (storage slot)
- 一个允许管理员 (admin) 更改该逻辑合约地址的机制
ERC-1967 标准规定了保存逻辑合约地址的存储槽应位于何处,从而将存储碰撞 (storage collision) 的可能性降至最低。然而,ERC-1967 标准并未规定应如何更改该逻辑合约的地址。
在代理合约内部添加一个额外的函数来更改逻辑合约(例如 updateImplementation(address _newImplementation))所带来的问题是,这个更新函数有不可忽视的几率与逻辑合约中的某个函数发生冲突。
函数选择器冲突
在代理合约内部声明 public 函数以更新逻辑合约地址,会引入 函数选择器 冲突的可能性。
这里有一个简单的示例:
contract ProxyUnsafe {
function changeImplementation(
address newImplementation
) public {
// some code...
}
fallback(bytes calldata data) external payable (bytes memory) {
(bool ok, bytes memory data) = getImplementation().delegatecall(data);
require(ok, "delegatecall failed");
return data;
}
}
contract Implementation {
// an identical function is declared here -- they will clash
function changeImplementation(
address newImplementation
) public {
}
//...
}
请记住,fallback 总是最后被检查的。 在调用 fallback 之前,Proxy 合约会检查 4 字节的函数选择器是否与 changeImplementation(或代理合约中的任何其他 public 函数)匹配。
因此,如果在代理合约中声明了 public 函数,可能会发生两种函数选择器冲突:
- 如果逻辑合约实现了具有相同签名的函数,该函数将变得无法调用,因为被调用的是代理合约中具有相同签名的 public 函数,而不是 fallback。如果不触发 fallback,就不会对逻辑合约发起 delegatecall。
- 如果逻辑合约中有一个函数与代理合约中的 public 函数拥有相同的函数选择器,同样出于上述原因,它也将变得无法调用。这种场景属于当 4 个字节碰巧匹配时随机发生的函数选择器冲突。两个不同函数具有相同选择器的概率是 42.9 亿分之一;因为函数选择器由 4 个字节组成,所以存在 42.9 亿种可能性。这个概率虽然很小,但绝不是可以忽略不计的。例如,
clash550254402()与proxyAdmin()的函数选择器就完全相同。
透明可升级代理模式完全阻止了函数选择器冲突
透明可升级代理模式是一种彻底消除函数选择器冲突可能性的设计模式。
具体而言,透明可升级代理模式规定代理合约上除了 fallback 外,不应存在任何 public 函数。
但是如果只有一个 fallback 函数,我们该如何调用函数来升级代理呢?
答案是检测 msg.sender 是否为 admin。
contract Proxy is ERC1967 {
address immutable admin;
constructor(address admin_) {
admin = admin_
}
fallback() external payable {
if (msg.sender == admin) {
// upgrade logic
} else {
// delegatecall to implementation
}
}
}
这意味着 admin 无法直接使用该代理合约,因为他们的调用总是会被重定向而绕过 delegatecall。然而,通过我们稍后将讨论的另一种机制,admin 依然可以调用代理合约,并让代理合约像普通交易一样向逻辑合约发起 delegatecall。
更改不可变的 admin
在上面的代码片段中,admin 是 immutable 的。这意味着从技术层面讲,该合约并不符合 ERC-1967 的要求,因为标准规定 admin 必须保存在存储槽 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103 或 bytes32(uint256(keccak256('eip1967.proxy.admin')) - 1) 中。
为了兼容 ERC-1967,透明可升级代理确实将 admin 的地址保存在该存储槽中,但它并不实际使用该存储变量。
该存储槽中存在地址,将向区块链浏览器发出信号,表明该合约是一个代理(这也是 ERC-1967 的意图之一)。但是,如果每次调用代理合约都要读取一次存储,会给调用带来额外的 2,100 gas 开销。因此,使用 immutable 变量是更可取的设计。
“更改” admin
尽管如此,我们依然希望能够更新 admin 地址——但这乍看之下似乎不可能,因为 Proxy 使用了 immutable 变量。
透明可升级代理为了实现代理合约 admin 的变更,采用了两步策略。首先,它指定另一个名为 ProxyAdmin 的合约作为代理合约的 admin。

智能合约的地址永远不会改变,因此这完全兼容透明可升级代理将 admin 地址存储在 immutable 变量中的做法。
其次,ProxyAdmin 的 owner 才是“真正的” admin。ProxyAdmin 仅仅是将来自 owner 的调用路由给 Proxy。“真正的” admin 调用 ProxyAdmin,然后 ProxyAdmin 调用透明代理。通过更改 ProxyAdmin 的 owner,我们就可以改变谁拥有升级透明代理的权限。
AdminProxy
下面是 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);
}
}
存在一个常见的误区:认为透明代理的 admin 无法使用合约,因为他们的调用会被转发至升级逻辑。然而,AdminProxy 的 owner 是可以毫无问题地使用 Proxy 的,如下面的图示所示。
事实上,正如 upgradeToAndCall() 这个函数名所暗示的那样,我们稍后将看到有一种机制允许 ProxyAdmin 对代理发起任意调用。

使代理变得不可升级
如果将 owner 更改为零地址,或者更改为另一个无法正确调用 upgradeAndCall()(或无法更改 owner)的智能合约,那么透明可升级代理将不再具备可升级性。例如,如果将 AdminProxy 的 owner 设置为一个不同的 AdminProxy 合约,就会出现这种情况。
实现细节
OpenZeppelin 的透明可升级代理通过以下三个合约来实现该标准:
- Proxy.sol
- ERC1967Proxy.sol(继承自 Proxy.sol)
- TransparentUpgradeableProxy.sol(继承自 ERC1967Proxy.sol)
最顶层父合约:Proxy.sol
基础合约是 Proxy.sol。当被提供一个逻辑合约地址时,它会向该逻辑合约发送 delegatecall。_implementation() 函数在 Proxy 中并未实际实现——它由其子合约 ERC1967Proxy 进行重写和实现,使其返回相应的存储槽数据。
abstract contract Proxy {
function _delegate(address implementation) internal virtual {
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 {
revert(0, returndatasize())
}
default {
return(0, returndatasize())
}
}
}
function _implementation() internal view virtual returns (address);
function _fallback() internal virtual {
_delegate(_implementation());
}
fallback() external payable virtual {
_fallback();
}
}
Proxy.sol 的子合约:ERC1967Proxy.sol
ERC1967Proxy.sol 继承自 Proxy.sol。它添加(并重写)了内部函数 _implementation(),该函数会返回存储在 ERC-1967 指定槽位中的逻辑合约地址。此合约的构造函数会将逻辑合约地址保存在 ERC-1967 所指定的存储槽中。不过,透明可升级代理不会使用此函数,而是使用其自身的 immutable 变量。
pragma solidity ^0.8.20;
import {Proxy} from "../Proxy.sol";
import {ERC1967Utils} from "./ERC1967Utils.sol";
contract ERC1967Proxy is Proxy {
constructor(address implementation, bytes memory _data) payable {
ERC1967Utils.upgradeToAndCall(implementation, _data);
}
// reads from bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1)
function _implementation() internal view virtual override returns (address) {
return ERC1967Utils.getImplementation();
}
}
ERC1967Proxy.sol 的子合约:TransparentUpgradeableProxy.sol
最后,TransparentUpgradeableProxy.sol 继承自 ERC1967Proxy.sol。在此合约的构造函数中,会部署 ProxyAdmin,并将不可变的 admin(合约中的第一个变量)设置为构造函数中生成的 ProxyAdmin 的地址。
contract TransparentUpgradeableProxy is ERC1967Proxy {
address private immutable _admin;
error ProxyDeniedAdminAccess();
constructor(address _logic, address initialOwner, bytes memory _data) payable ERC1967Proxy(_logic, _data) {
_admin = address(new ProxyAdmin(initialOwner));
// Set the storage value and emit an event for ERC-1967 compatibility
ERC1967Utils.changeAdmin(_proxyAdmin());
}
function _proxyAdmin() internal view virtual returns (address) {
return _admin;
}
function _fallback() internal virtual override {
if (msg.sender == _proxyAdmin()) {
if (msg.sig != ITransparentUpgradeableProxy.upgradeToAndCall.selector) {
revert ProxyDeniedAdminAccess();
} else {
_dispatchUpgradeToAndCall();
}
} else {
super._fallback();
}
}
function _dispatchUpgradeToAndCall() private {
(address newImplementation, bytes memory data) = abi.decode(msg.data[4:], (address, bytes));
ERC1967Utils.upgradeToAndCall(newImplementation, data);
}
}
让我们来看一下 msg.sender 是 _proxyAdmin 的情况。在此场景下,调用会被路由到 _dispatchUpgradeToAndCall(),但 _fallback() 会首先检查传入的函数选择器是否为 upgradeToAndCall。这里的“选择器”并非“真正的”选择器,因为透明可升级代理本身没有 public 函数。然而,为了允许 ProxyAdmin 发起 Solidity 接口调用 (高级调用),它需要接收来自 ProxyAdmin 且经过 ABI 编码的 calldata,即针对 upgradeToAndCall() 的调用数据。
回想一下,即使代理合约除了 fallback 之外没有其他任何 public 函数,ProxyAdmin 仍在对 Proxy 中的 upgradeToAndCall 进行接口调用(ProxyAdmin 代码见下):

下面的视频将这三个代码块并排显示,展示了继承链中不同合约(Proxy、ERC1967Proxy 和 TransparentUpgradeableProxy)相互交互的过程:
为什么使用 upgradeToAndCall() 而不仅是 upgradeTo()?
在升级逻辑合约时,可以像 ProxyAdmin 就是 msg.sender 一样向其发起调用,并让该交易以普通的代理交互方式对逻辑合约执行 delegatecall。当然,这一切并不会在 fallback 函数内部发生,因为来自 ProxyAdmin 的调用已被路由至升级逻辑。
以下代码取自 ERC1967Utils.sol,TransparentUpgradeableProxy 依赖于该库来更新逻辑合约槽位。该库提供了一个内部辅助函数,用以更新保存逻辑合约地址的存储槽。
/**
* @dev Performs implementation upgrade with additional setup call if data is nonempty.
* This function is payable only if the setup call is performed, otherwise `msg.value` is rejected
* to avoid stuck value in the contract.
*
* Emits an {IERC1967-Upgraded} event.
*/
function upgradeToAndCall(address newImplementation, bytes memory data) internal {
_setImplementation(newImplementation);
emit IERC1967.Upgraded(newImplementation);
if (data.length > 0) {
Address.functionDelegateCall(newImplementation, data);
} else {
_checkNonPayable();
}
}
只有当 data.length > 0 时,它才会对逻辑合约发起 delegatecall。
upgradeToAndCall() 会在同一次升级交易中实现由 Proxy 针对逻辑合约的 delegatecall。这等同于 ProxyAdmin 附带 data 中指定的特定 calldata 调用了代理合约,随后代理合约又对逻辑合约执行了 delegatecall。
通过这种方式,ProxyAdmin 就能够向 Proxy 发起任意调用。
请注意,upgradeToAndCall 并不强制要求被升级的合约必须具备不同的逻辑——你完全可以“升级”为同样的逻辑合约。
这使得 ProxyAdmin 合约能通过 Proxy 对逻辑合约发起任意的 delegatecall——虽然从透明代理的视角来看,其 msg.sender 是 ProxyAdmin。
ProxyAdmin 可以使用该合约并非一个“漏洞”——ProxyAdmin 拥有完全更改逻辑合约的能力,ProxyAdmin 的 owner 本身就已经具备了对 Proxy 的管理员控制权。
ProxyAdmin 在升级时受到的唯一限制是:他们不能升级到一个空合约(即没有字节码的地址)。_setImplementation 函数会检查新逻辑合约的 代码长度 (code length) 是否大于零。
/**
* @dev Stores a new address in the ERC-1967 implementation slot.
*/
function _setImplementation(address newImplementation) private {
if (newImplementation.code.length == 0) {
revert ERC1967InvalidImplementation(newImplementation);
}
StorageSlot.getAddressSlot(IMPLEMENTATION_SLOT).value = newImplementation;
}
透明可升级代理总结
- 透明可升级代理是一种设计模式,旨在防止代理合约与逻辑合约之间发生函数选择器冲突。
- fallback 是透明可升级代理上仅存的 public 函数。
- 升级功能只能由 admin 通过 fallback 触发。所有来自非管理员地址的调用均会在代理合约中转为 delegatecall。
- 透明可升级代理利用 immutable 变量来存储 admin 以达到节省 gas 的目的。为兼容 ERC-1967,它同时会将 admin 的地址存储在 ERC-1967 规定的
admin槽位中,尽管它自身从不读取该槽位。 - 由于该 admin 不可直接更改,因此 admin 会被配置为一个名为
AdminProxy的智能合约。AdminProxy对外暴露了单一函数upgradeAndCall(),且只有AdminProxy的 owner 可以调用它。通过更改AdminProxy的 owner,即可转移透明可升级代理中逻辑合约槽位的更新权限。
我们要感谢来自 OpenZeppelin 的 @ernestognw 审阅本文并提供有益的建议。
原文发布于 6 月 4 日