Beacon 代理(Beacon Proxy)详解:一次升级所有代理

理解信标合约如何集中存储实现地址,让海量代理通过一笔交易同时升级

7 分钟阅读
Beacon 代理(Beacon Proxy)详解:一次升级所有代理

Beacon 代理(Beacon Proxy)详解:一次升级所有代理

目录


一、Beacon 代理是什么

Beacon(信标)代理是一种可升级模式,特点是:多个代理共用同一个实现合约,且所有代理可以用一笔交易同时升级。它解决的是”管理海量可升级合约”的扩展性难题。

想象你要给 1000 个用户各部署一个相同逻辑的归属(vesting)合约。用透明代理/UUPS,升级要逐个调 1000 次。用 Beacon,只需改信标里的一个地址,1000 个代理瞬间全部切换到新逻辑。

二、核心架构

信标合约(Beacon):充当集中注册表。它存储当前实现地址,并通过一个公开函数提供给所有关联的代理。

信标是一个智能合约,通过公开函数把当前实现地址提供给各个代理。

多代理、单实现:不必为每个代理部署独立实现,所有代理都引用同一个信标——信标就是所有代理关于”当前实现地址”的唯一事实来源

三、工作流程

代理收到交易时:

  1. 代理调用信标的 implementation() view 函数;
  2. 信标返回当前实现地址;
  3. 代理 delegatecall 到该地址;
  4. 逻辑在代理自己的存储上下文执行。

每个代理都这样从同一个信标读地址。注意:和 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;
    }
}

五、一笔交易同时升级所有代理

升级全部代理的步骤:

  1. 部署新实现合约;
  2. 对信标调用 upgradeTo(新实现地址)
  3. 信标更新自己的存储;
  4. 所有代理在下一次调用时立刻读到新地址

改变存储插槽里的地址,会让所有代理 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);
    }
}

手动部署顺序

  1. 部署实现合约;
  2. 部署 UpgradeableBeacon(传实现地址 + 管理员);
  3. 部署工厂(可选但推荐);
  4. 用工厂按需创建代理。

九、真实案例: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:ownerdeposit()balance()
  • V2:布局兼容,新增 withdraw(uint)version() returns (2)

2. SimpleBeacon.sol(= UpgradeableBeacon 精简版)

  • Ownableaddress private _implementation;
  • implementation() viewupgradeTo(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)

  1. test_CreateMultipleProxies:工厂创建 3 个钱包代理,各自 initialize 不同 owner,互不干扰;
  2. test_AllProxiesShareImplementation:3 个代理的 _implementation()(或通过行为)都指向 WalletV1;
  3. test_MassUpgrade_OneTransaction(核心):对信标 upgradeTo(WalletV2) 一次,断言 3 个代理全部能调用 V2 新增的 version() 返回 2、withdraw 可用;
  4. test_StoragePreservedAfterUpgrade:升级后每个代理的 owner 和余额不变(存储在各自代理);
  5. test_BeaconSlot_ERC1967vm.load(proxy, 0xa3f0...3d50) == 信标地址;
  6. test_OnlyOwnerUpgradeBeacon:非 owner 调信标 upgradeTo 应 revert。

Sepolia 部署与验证步骤

  1. 部署 WalletV1、SimpleBeacon(传 V1 + 你的地址)、WalletFactory(传 beacon);
  2. createWallet 三次,得到 3 个代理地址,分别 deposit 一点 ETH;
  3. 部署 WalletV2,对信标调一次 upgradeTo(V2)
  4. 对 3 个代理地址分别调 version(),全部返回 2——一次升级,全员生效;
  5. cast storage <任一代理> 0xa3f0ad74...3d50 读 beacon 插槽核对。

进阶挑战(可选)

  • 把信标和工厂合并成 FactoryBeacon(继承 SimpleBeacon + 加 createWallet),复刻 Kwenta 的优化;
  • forge snapshot 对比 Beacon 代理每次调用 vs 普通 ERC-1967 代理的 Gas 差,量化”多一次信标查询”的成本,理解为何只有大量代理才划算。

💬 评论