Beacon 代理(Beacon Proxy)详解:一次升级所有代理
目录
- 一、Beacon 代理是什么
- 二、核心架构
- 三、工作流程
- 四、UpgradeableBeacon 合约
- 五、一笔交易同时升级所有代理
- 六、BeaconProxy 合约细节
- 七、ERC-1967 beacon 插槽
- 八、工厂模式批量部署
- 九、真实案例:Kwenta 归属合约
- 十、Gas 考量与适用场景
- 十一、与其他模式对比
- 十二、总结
- 十三、动手练习项目:Beacon 批量代理工厂 BeaconFactory
一、Beacon 代理是什么
Beacon(信标)代理是一种可升级模式,特点是:多个代理共用同一个实现合约,且所有代理可以用一笔交易同时升级。它解决的是”管理海量可升级合约”的扩展性难题。
想象你要给 1000 个用户各部署一个相同逻辑的归属(vesting)合约。用透明代理/UUPS,升级要逐个调 1000 次。用 Beacon,只需改信标里的一个地址,1000 个代理瞬间全部切换到新逻辑。
二、核心架构
信标合约(Beacon):充当集中注册表。它存储当前实现地址,并通过一个公开函数提供给所有关联的代理。
信标是一个智能合约,通过公开函数把当前实现地址提供给各个代理。
多代理、单实现:不必为每个代理部署独立实现,所有代理都引用同一个信标——信标就是所有代理关于”当前实现地址”的唯一事实来源。
三、工作流程
代理收到交易时:
- 代理调用信标的
implementation()view 函数; - 信标返回当前实现地址;
- 代理 delegatecall 到该地址;
- 逻辑在代理自己的存储上下文执行。
每个代理都这样从同一个信标读地址。注意:和 ERC-1967 代理”自己存实现地址”不同,Beacon 代理自己只存信标地址,实现地址向信标问——这是它能集中升级的关键。
四、UpgradeableBeacon 合约
OpenZeppelin 的 UpgradeableBeacon 有两个核心功能:
contract UpgradeableBeacon {
address private _implementation;
// 返回当前实现地址
function implementation() public view returns (address) { return _implementation; }
// 仅 owner 可更新实现(这一个调用就升级了所有代理)
function upgradeTo(address newImplementation) public onlyOwner {
// 校验是合约地址后更新
_implementation = newImplementation;
}
}
五、一笔交易同时升级所有代理
升级全部代理的步骤:
- 部署新实现合约;
- 对信标调用
upgradeTo(新实现地址); - 信标更新自己的存储;
- 所有代理在下一次调用时立刻读到新地址。
改变存储插槽里的地址,会让所有代理 delegatecall 到新地址,瞬间把它们全部”重新路由”。
这就是 Beacon 模式最大的卖点:O(1) 的升级成本,与代理数量无关。
六、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;
}
}
要点:
_beacon是 immutable 变量,存信标地址(省 Gas);_implementation()重写:向信标查询当前实现,而不是自己存;- 构造函数的
data参数:用于初始化——它会被 delegatecall 到实现,让实现逻辑配置代理自己的存储变量(比如设 owner、vesting 参数)。
七、ERC-1967 beacon 插槽
为了让区块浏览器识别 Beacon 代理,信标地址要存在 ERC-1967 标准 beacon 插槽:
0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50
= bytes32(uint256(keccak256('eip1967.proxy.beacon')) - 1)
注意:实际运行时信标地址存在 immutable 变量里(省 Gas),ERC-1967 插槽只作为给浏览器的信号,和透明代理里 admin 插槽的处理思路一样。
八、工厂模式批量部署
手动逐个部署代理太低效,通常用工厂:
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);
}
}
手动部署顺序:
- 部署实现合约;
- 部署 UpgradeableBeacon(传实现地址 + 管理员);
- 部署工厂(可选但推荐);
- 用工厂按需创建代理。
九、真实案例:Kwenta 归属合约
Kwenta 在 Optimism 上用 Beacon 代理管理代币归属包,TVL 超 2000 万美元。选择理由:
- 升级需求:归属包要和可升级的 Kwenta 质押系统保持兼容,Beacon 能系统性地统一更新所有包;
- 存储隔离:每个归属包逻辑相同但参数不同(代币量、时长),独立合约简化奖励追踪、避免状态串扰;
- 所有权转移:单个包可转给不同地址或多签,方便归属权重新分配;
- 避免冷却期阻塞:质押系统有两周解质押冷却,独立代理使一个人的归属操作不会阻塞他人。
Kwenta 还把信标和工厂合并成一个 FactoryBeacon(继承 UpgradeableBeacon 再加创建代理的方法),降低部署复杂度。
十、Gas 考量与适用场景
Beacon 模式比 UUPS/透明代理 单次调用 Gas 更高,因为:
- 要额外部署信标(和工厂)合约;
- 每次代理调用都多一次合约调用(向信标问实现地址)。
但是:
Beacon 代理模式只有在你需要大量代理时才划算——届时”一笔交易升级全部”的收益远超额外开销。
优化技巧:代理调用信标读其存储时,可用 EIP-2930 访问列表(access list)交易降低 Gas。
十一、与其他模式对比
| 模式 | 可升级 | 升级成本 | 部署成本 | 适用 |
|---|---|---|---|---|
| 最小代理 Clone (EIP-1167) | 否 | —(不可升级) | 极低 | 大量不可升级实例 |
| Transparent / UUPS | 是 | 每个代理单独升级 | 中 | 少量代理 |
| Beacon | 是 | 一笔升级全部 | 较高(多信标+工厂) | 大量需统一升级的代理 |
| Diamond | 是 | 灵活 | 高、复杂 | 复杂大型系统 |
十二、总结
- Beacon 代理 = 多代理共享一个信标,信标存实现地址;
- 代理每次调用向信标问地址再 delegatecall;
- 升级只需对信标
upgradeTo一次,所有代理瞬间切换; - 代理自存信标地址(immutable)+ ERC-1967 beacon 插槽供浏览器识别;
- 适合”大量、需统一升级”的场景(如 Kwenta 归属包),少量代理用它不划算。
十三、动手练习项目:Beacon 批量代理工厂 BeaconFactory
项目目标
实现完整 Beacon 体系(信标 + BeaconProxy + 工厂),部署多个代理,用一笔交易把它们全部升级,亲眼验证 O(1) 升级。部署到 Sepolia。
合约要求
1. WalletV1.sol / WalletV2.sol(共享实现)
- 用 initializer 模式:
initialize(address owner); - V1:
owner、deposit()、balance(); - V2:布局兼容,新增
withdraw(uint)和version() returns (2)。
2. SimpleBeacon.sol(= UpgradeableBeacon 精简版)
Ownable,address private _implementation;implementation() view、upgradeTo(address) onlyOwner(校验newImpl.code.length > 0,发事件Upgraded)。
3. MiniBeaconProxy.sol
address immutable beacon;,同时写 ERC-1967 beacon 插槽;- 构造函数
(address _beacon, bytes memory data):存 beacon,若 data 非空则 delegatecall 当前实现做初始化; _implementation():IBeacon(beacon).implementation();fallback():delegatecall 到_implementation(),冒泡返回/revert。
4. WalletFactory.sol
immutable beacon;createWallet(address owner) external returns (address):用abi.encodeCall(WalletV1.initialize,(owner))作为 data 部署 MiniBeaconProxy,记录到address[] public wallets。
测试要求(Foundry)
test_CreateMultipleProxies:工厂创建 3 个钱包代理,各自 initialize 不同 owner,互不干扰;test_AllProxiesShareImplementation:3 个代理的_implementation()(或通过行为)都指向 WalletV1;test_MassUpgrade_OneTransaction(核心):对信标upgradeTo(WalletV2)一次,断言 3 个代理全部能调用 V2 新增的version()返回 2、withdraw可用;test_StoragePreservedAfterUpgrade:升级后每个代理的 owner 和余额不变(存储在各自代理);test_BeaconSlot_ERC1967:vm.load(proxy, 0xa3f0...3d50)== 信标地址;test_OnlyOwnerUpgradeBeacon:非 owner 调信标 upgradeTo 应 revert。
Sepolia 部署与验证步骤
- 部署 WalletV1、SimpleBeacon(传 V1 + 你的地址)、WalletFactory(传 beacon);
- 调
createWallet三次,得到 3 个代理地址,分别 deposit 一点 ETH; - 部署 WalletV2,对信标调一次
upgradeTo(V2); - 对 3 个代理地址分别调
version(),全部返回 2——一次升级,全员生效; cast storage <任一代理> 0xa3f0ad74...3d50读 beacon 插槽核对。
进阶挑战(可选)
- 把信标和工厂合并成
FactoryBeacon(继承 SimpleBeacon + 加 createWallet),复刻 Kwenta 的优化; - 用
forge snapshot对比 Beacon 代理每次调用 vs 普通 ERC-1967 代理的 Gas 差,量化”多一次信标查询”的成本,理解为何只有大量代理才划算。