Beacon Proxy 是一种智能合约升级模式,在这种模式下,多个代理(proxies)使用同一个实现合约(implementation contract),并且所有代理可以在单笔交易中完成升级。本文将详细解释该代理模式的工作原理。
前置知识
我们假设你已经了解 minimal proxy 的工作原理,甚至可能了解 UUPS 或 Transparent 代理。
使用 Beacon Proxy 的动机
通常,代理模式使用单个实现合约和单个代理合约。然而,也允许多个代理使用同一个实现。

为了理解为什么我们需要这种模式,让我们想象一款全链游戏。这款游戏希望将每个用户账户存储为一个单独的合约,这样账户就可以轻松转移到不同的钱包,而且一个钱包可以拥有多个账户。每个代理都在其各自的存储变量中保存账户信息。
有几种方法可以实现这一点:
- 使用 Minimal Proxy Standard (EIP1167) 并将每个账户部署为一个克隆(clone)
- 使用 UUPS 或 Transparent 代理模式,并为每个账户部署一个代理
在大多数情况下,这两种选择都可行,但如果你想为账户添加新功能该怎么办?
在 minimal proxy 标准的情况下,你将不得不重新部署整个系统,并通过社区共识让所有人进行迁移,因为克隆是不可升级的。
传统的代理是可升级的,但你必须逐一升级每个代理。随着账户数量的增加,这将变得非常昂贵。
当数量众多时,无论是克隆还是传统的代理,升级起来都非常麻烦。
beacon 模式正是为了解决这个问题而设计的:它允许你部署一个新的实现合约,并同时升级所有代理。
这意味着 beacon 模式将允许你为账户部署一个新的实现,并一次性升级所有代理。
从宏观层面来看,该标准允许你为每个实现合约创建无限数量的代理,并且仍然能够轻松进行升级。
beacon 的工作原理
顾名思义,该标准需要一个 beacon(信标),OpenZeppelin 将其称为 “UpgradeableBeacon”,并在 UpgradeableBeacon.sol 中实现。
beacon 是一个智能合约,它通过一个公共函数向代理提供当前的实现地址。 对于代理而言,beacon 是当前实现地址的事实来源(source of truth),这就是为什么它被称为 “beacon” 的原因。
当代理收到传入的交易时,代理首先调用 beacon 上的 view 函数 implementation() 以获取当前的实现地址,然后代理向该地址发起 delegatecalls。这正是 beacon 能够作为实现地址事实来源的原因。

任何额外的代理都将遵循相同的模式:它们首先使用 implementation() 从 beacon 获取实现地址,然后再 delegatecall 到该地址。
注意:代理知道要在哪里调用 implementation(),因为它们将 beacon 的地址存储在一个不可变(immutable)变量中。稍后我们将进一步解释这一机制。
这种模式具有高度可扩展性,因为每个额外的代理只需从 beacon 读取实现地址,然后使用 delegatecall 即可。

尽管 Beacon Proxy 模式涉及更多的合约,但代理本身比 UUPS 或 Transparent Upgradeable Proxies 更简单。
beacon proxies 总是调用相同的 beacon 地址来获取当前的实现地址,因此它们不需要关注管理员是谁或如何更改实现地址等细节。
同时升级多个代理
既然所有代理都从 beacon 的存储中获取实现地址,那么更改该存储槽(storage slot)中的地址就会导致所有代理 delegatecall 到新的地址,从而瞬间完成对它们的“重新路由”。
要同时升级所有代理:
- 部署新的实现合约
- 在 beacon 的存储中设置新的实现地址
设置新的实现地址是通过在 beacon 上调用 upgradeTo(address newImplementation) 并将新地址作为参数传入来完成的。upgradeTo() 是 UpgradeableBeacon.sol(即 beacon)上的两个公共函数之一。另一个公共(视图)函数是我们前面提到的 implementation()。
注意:upgradeTo() 具有 onlyOwner 修饰符,该修饰符是在 UpgradeableBeacon.sol(beacon)的构造函数中设置的。
/**
* @dev Upgrades the beacon to a new implementation.
*
* Emits an {Upgraded} event.
*
* Requirements:
*
* - msg.sender must be the owner of the contract.
* - `newImplementation` must be a contract.
*/
function upgradeTo(address newImplementation) public virtual onlyOwner {
_setImplementation(newImplementation);
}
upgradeTo() 调用了一个内部函数 _setImplementation(address newImplementation)(同样在 beacon 上),该函数检查新的实现地址是否为合约,然后将 beacon 中的地址存储变量 _implementation 设置为新的实现地址。
/**
* @dev Sets the implementation contract address for this beacon
*
* Requirements:
*
* - `newImplementation` must be a contract.
*/
function _setImplementation(address newImplementation) private {
require(Address.isContract(newImplementation), "UpgradeableBeacon: implementation is not a contract");
_implementation = newImplementation;
}
现在,beacon 存储中的实现地址已经改变,所有代理都会读取 beacon 中的新地址,并将其 delegatecall 路由到这个新的实现。
这种升级方式非常简单,因为你只需将 beacon(进而包括所有代理)“指向”一个新的实现即可。如果需要撤销更改,你甚至可以将实现重新指向以前的版本(但要注意存储碰撞问题)。

代理合约代码解析
为了避免混淆,我们使用术语 “BeaconProxy” 来指代智能合约代理,使用 “beacon proxy” 来指代设计模式。我们现在将讨论 OpenZeppelin 称为 “BeaconProxy” 并在 BeaconProxy.sol 中实现的代理合约。
OpenZeppelin 的 BeaconProxy 继承自 Proxy.sol 并添加了更多功能:
- 它将 beacon 合约的地址存储在
_beacon中 - 增加了一个
_getBeacon()函数来返回_beacon变量 - 重写了
_implementation()函数以在_beacon地址上调用.implementation() - 增加了一个构造函数来设置
_beacon变量,并通过data参数初始化代理
以下是删除了注释的 OpenZeppelin 版本的 BeaconProxy 实现
contract BeaconProxy is Proxy {
address private immutable _beacon;
constructor(address beacon, bytes memory data) payable {
ERC1967Utils.upgradeBeaconToAndCall(beacon, data);
_beacon = beacon;
}
function _implementation() internal view virtual override returns (address) {
return IBeacon(_getBeacon()).implementation();
}
function _getBeacon() internal view virtual returns (address) {
return _beacon;
}
}
重写 _implementation() 函数是因为 Proxy.sol 会在执行 delegatecall 之前调用该函数来获取实现地址。
BeaconProxy 的构造函数有两个作用:
- 设置
_beacon地址 - 使用
data初始化代理
这个可选的 data 被用于对实现合约发起 delegatecall,从而完成代理存储的初始化。在我们的游戏示例中,这可能意味着使用玩家的初始统计数据来初始化账户(代理)。从本质上讲,data 参数充当了代理的 Solidity 构造函数:该 data 被用于对实现发起 delegatecall,以便实现逻辑可以配置代理的存储变量。
function upgradeBeaconToAndCall(address newBeacon, bytes memory data) internal {
_setBeacon(newBeacon);
emit IERC1967.BeaconUpgraded(newBeacon);
if (data.length > 0) {
Address.functionDelegateCall(IBeacon(newBeacon).implementation(), data);
} else {
_checkNonPayable();
}
}
ERC1967 与 BeaconProxy.sol
为了让区块浏览器知道 BeaconProxy 是一个代理,它需要遵守 ERC-1967 规范。由于它具体是一个 beacon proxy,它需要将 Beacon 的地址存储在存储槽 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50 中,该槽位由 bytes32(uint256(keccak256('eip1967.proxy.beacon')) - 1) 计算得出。
与 Transparent Upgradeable Proxy 类似,这个存储地址实际上并没有被 BeaconProxy 使用。它只是向区块浏览器发出一个信号,表明该合约是一个 Beacon Proxy。为了 gas optimization 的目的,实际的实现地址被存储在一个不可变变量中;beacon 的地址永远不会改变。
EIP2930
在这种模式下,应始终使用 access list 交易,因为在进行跨合约调用和访问另一个合约的存储时可以节省 gas。具体来说,代理正在调用 beacon 并从存储中获取实现地址。有关 Beacon Proxy 的访问列表(Access list)基准测试可以参见这里。
部署多个 BeaconProxies
手动部署多个 BeaconProxies 会很麻烦。这正是工厂合约(factory contract)发挥作用的地方。工厂可以部署新的代理,并在它们的构造函数中设置 beacon 地址。
OpenZeppelin 并没有在他们的 beacon 模式中要求或提供一个标准的工厂合约。然而在实际操作中,工厂合约有助于部署新代理。
下面提供了一个工厂合约的示例。该工厂存储了 beacon 的地址,并包含一个用于创建使用该 beacon 的新代理的函数。createBeaconProxy() 函数接收 data 作为输入,以传递给 BeaconProxy 的构造函数。部署代理后,它会返回该代理的地址。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {BeaconProxy} from "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol";
/// @dev THIS CONTRACT IS FOR TEACHING PURPOSES ONLY
contract ExampleFactory {
address public immutable beacon;
constructor(address Beacon) {
beacon = Beacon;
}
function createBeaconProxy(bytes memory data) external returns (address) {
BeaconProxy proxy = new BeaconProxy(beacon, data);
return address(proxy);
}
}
现在我们已经了解了如何使用工厂合约来部署代理,让我们看看它如何融入整体架构。

这就是设计一个 beacon 模式所需要的所有合约:
- 实现(Implementation)
- 信标(Beacon)
- 工厂(Factory)(可选)
- 代理(Proxy)
部署
好了,但我们要如何部署这整个系统呢?它其实并没有看起来那么可怕。
OpenZeppelin 为 Hardhat 和 Foundry 都提供了一个 Upgrades 插件。你只需安装该库,然后使用 beacon 合约的参数调用 deployBeacon() 即可。接下来,可以通过调用 deployBeaconProxy() 来部署 BeaconProxies。升级过程也很类似:使用新实现的参数调用 upgradeBeacon() 函数即可。
该系统也可以手动部署:
- 部署实现合约
- 部署 beacon 合约并在构造函数中,输入实现地址以及被允许升级实现地址的人的地址
- 部署工厂合约
- 使用工厂部署所需数量的代理

一个真实世界的例子
在现实生活中什么时候会使用 beacon proxy?我为 Kwenta 编写了一个 beacon proxy,该合约已在 Optimism 上线,TVL 超过 2000 万美元。
该 beacon proxy 是为 Kwenta 归属包(vesting packages)编写的。“归属包”是一个智能合约,它将代币(KWENTA)缓慢释放给协议的特定利益方和核心贡献者。每个人都会获得一个归属包,其代币数量和期限(通常为 1-4 年)各不相同。想了解更多关于加密货币中的代币归属机制,请参阅这里。
为什么非要用 beacon proxy?
-
它必须容易升级。归属包必须是可升级的,因为它们会调用同样是可升级的 Kwenta 质押系统上的函数。如果质押系统在未来进行升级,那么归属包上的功能可能就不再有效了。使归属包可升级能够让它们具备面向未来的适应性。
-
每个归属包都有相同的归属逻辑(
vest()、stake()等),但初始化的参数(代币数量、归属期限)不同。为了满足这种要求,必须将归属包做成独立的合约或“隔离(siloed)”,因为:a. 更简单的开发:每人拥有一个可初始化的合约比拥有一个包含复杂映射、用于追踪每个人不同归属包的庞大合约要简单得多。此外,每个归属包中的 KWENTA 在创建时就会自动进行质押,这意味着每个人都在不断累积奖励。如果所有人的包都在同一个合约中,奖励就会混合在一起,变得极其混乱。
b. 归属包的所有权可以很轻松地转移到其他地址或多签钱包(multi-sig)。
c. 归属(Vesting)意味着要在 Kwenta 质押合约上调用
unstake()。该质押合约有 2 周的unstake()冷却期。所以如果所有人的包都在同一个合约里,一旦有一个人执行了归属操作(进而触发了解除质押),那么至少在 2 周内其他人都无法进行归属操作。将归属包隔离成独立的合约可以避免这个 bug。 -
归属包必须支持超过 10 人。这意味着需要 10 个以上的代理
beacon proxy 能够在不牺牲任何特性的情况下做到所有这些。
克隆技术(Clones)固然可以轻易部署 10 个以上的可初始化合约,但它们不可升级。
Transparent 和 UUPS 是可升级的,但需要将每个归属包逐一升级,这将非常耗时且消耗更多 gas。
也曾考虑过 diamond proxy(钻石代理),但对于这种架构来说它过于复杂了。
Kwenta 的 FactoryBeacon
作为一种优化,FactoryBeacon 结合了 UpgradeableBeacon.sol 和 Factory 合约。这种结合简化了设置过程,并减少了攻击面(surface area)。

这之所以可行,是因为工厂不需要作为一个独立的合约存在:它只是几行代码,用于部署一个新的 BeaconProxy 并设置其 beacon 地址及初始化 data。
这里是一个工厂和 beacon 合约结合的示例。通过继承 UpgradeableBeacon,该合约保留了与常规 beacon 相同的功能,而 createBeaconProxy() 函数则添加了工厂功能。此外,不再需要存储 beacon 的地址,因为现在可以直接使用 address(this)。

尽管如此,整体的 “beacon 架构” 依然是一致的。
每个人都调用自己的 BeaconProxy,该代理存放着他们特定归属包的所有存储数据(归属数量、期限等)。
BeaconProxy 接着从 FactoryBeacon 获取实现地址,FactoryBeacon 仍然具备与常规 beacon 相同的功能。
从 FactoryBeacon 获取实现地址后,BeaconProxy 会通过 delegatecalls 调用 VestingBaseV2,即具体的实现逻辑。
需要注意的是,唯一能够调用 FactoryBeacon 的是 adminDAO(一个管理员多签钱包)。管理员是唯一能创建新归属包(BeaconProxy)以及将代理升级到新实现的人。
结论
Beacon Proxy 模式允许为一个实现创建多个代理,并能够一次性对它们进行全面升级。工厂部署新代理,这些代理使用 delegatecall 调用从 beacon 获取到的地址。beacon 则充当实现的单一真实来源(source of truth)。
需要注意的是,与 UUPS 或 Transparent 等其他模式相比,Beacon Proxy 模式在设置阶段会产生更高的 gas 成本,因为除了代理本身之外,还必须部署工厂和 beacon。此外,对代理的每次调用都会因为要额外调用 beacon 而产生额外的成本。这种额外的 gas 成本是其主要缺点。不过如果你需要多个代理,这并不一定是劣势,因为这正是 Beacon Proxy 模式最能发挥优势的地方。较高的 gas 成本也正是为什么通常不会看到在只有一个代理的情况下使用 Beacon Proxy 模式的原因。
虽然 beacon 允许同时升级多个代理,但它的设置更为复杂和昂贵。它需要更多的 gas,并且涉及设置额外的合约,这使得在开发和审计方面成本更高。因此,只有当你需要大量的代理时,Beacon Proxy 模式才具有优势。
作者信息
本文作者为 Andrew Chiaramonte(LinkedIn,Twitter)。
最初发表于 2024 年 7 月 20 日