信标代理
信标代理是一种高级代理模式,允许多个代理合约共享一个信标合约,该信标合约决定了它们的实现。 此模式对于希望通过更新单个信标同时升级多个代理合约的场景特别有用。
OpenZeppelin Stylus Contracts 提供了信标代理模式的完整实现,包括 BeaconProxy
合约和 UpgradeableBeacon
合约。
理解信标代理
基本信标代理实现
以下是如何实现基本信标代理:
use openzeppelin_stylus::proxy::{
beacon::{proxy::BeaconProxy, IBeacon},
erc1967,
IProxy,
};
use stylus_sdk::{
abi::Bytes,
alloy_primitives::Address,
prelude::*,
ArbResult,
};
#[entrypoint]
#[storage]
struct MyBeaconProxy {
beacon_proxy: BeaconProxy,
}
#[public]
impl MyBeaconProxy {
#[constructor]
fn constructor(
&mut self,
beacon: Address,
data: Bytes,
) -> Result<(), erc1967::utils::Error> {
self.beacon_proxy.constructor(beacon, &data)
}
/// 获取信标地址
fn get_beacon(&self) -> Address {
self.beacon_proxy.get_beacon()
}
/// 从信标获取当前实现地址
fn implementation(&self) -> Result<Address, Vec<u8>> {
self.beacon_proxy.implementation()
}
/// 回退函数,将所有调用委托给实现
#[fallback]
fn fallback(&mut self, calldata: &[u8]) -> ArbResult {
unsafe { self.do_fallback(calldata) }
}
}
unsafe impl IProxy for MyBeaconProxy {
fn implementation(&self) -> Result<Address, Vec<u8>> {
self.beacon_proxy.implementation()
}
}
可升级信标的实现
UpgradeableBeacon
合约管理实现地址并提供升级功能:
use openzeppelin_stylus::{
access::ownable::{IOwnable, Ownable},
proxy::beacon::{IBeacon, IUpgradeableBeacon, UpgradeableBeacon},
};
use stylus_sdk::{
alloy_primitives::Address,
prelude::*,
};
#[entrypoint]
#[storage]
struct MyUpgradeableBeacon {
beacon: UpgradeableBeacon,
}
#[public]
impl MyUpgradeableBeacon {
#[constructor]
fn constructor(
&mut self,
implementation: Address,
initial_owner: Address,
) -> Result<(), beacon::Error> {
self.beacon.constructor(implementation, initial_owner)
}
/// 升级到新的实现 (仅所有者)
fn upgrade_to(
&mut self,
new_implementation: Address,
) -> Result<(), beacon::Error> {
self.beacon.upgrade_to(new_implementation)
}
}
#[public]
impl IBeacon for MyUpgradeableBeacon {
fn implementation(&self) -> Result<Address, Vec<u8>> {
self.beacon.implementation()
}
}
#[public]
impl IOwnable for MyUpgradeableBeacon {
fn owner(&self) -> Address {
self.beacon.owner()
}
fn transfer_ownership(&mut self, new_owner: Address) -> Result<(), Vec<u8>> {
self.beacon.transfer_ownership(new_owner)
}
fn renounce_ownership(&mut self) -> Result<(), Vec<u8>> {
self.beacon.renounce_ownership()
}
}
#[public]
impl IUpgradeableBeacon for MyUpgradeableBeacon {
fn upgrade_to(&mut self, new_implementation: Address) -> Result<(), Vec<u8>> {
Ok(self.beacon.upgrade_to(new_implementation)?)
}
}
自定义信标实现
您还可以通过实现 IBeacon
trait 来实现您自己的信标合约:
use openzeppelin_stylus::proxy::beacon::IBeacon;
use stylus_sdk::{
alloy_primitives::Address,
prelude::*,
storage::StorageAddress,
};
#[entrypoint]
#[storage]
struct MyCustomBeacon {
implementation: StorageAddress,
admin: StorageAddress,
}
#[public]
impl MyCustomBeacon {
#[constructor]
fn constructor(&mut self, implementation: Address, admin: Address) {
self.implementation.set(implementation);
self.admin.set(admin);
}
/// 升级实现 (仅管理员)
fn upgrade_implementation(&mut self, new_implementation: Address) -> Result<(), Vec<u8>> {
if self.admin.get() != msg::sender() {
return Err("Only admin can upgrade".abi_encode());
}
if !new_implementation.has_code() {
return Err("Invalid implementation".abi_encode());
}
self.implementation.set(new_implementation);
Ok(())
}
}
#[public]
impl IBeacon for MyCustomBeacon {
fn implementation(&self) -> Result<Address, Vec<u8>> {
Ok(self.implementation.get())
}
}
构造函数数据
与 ERC-1967 代理一样,信标代理支持初始化数据:
impl MyBeaconProxy {
#[constructor]
fn constructor(
&mut self,
beacon: Address,
data: Bytes,
) -> Result<(), erc1967::utils::Error> {
// 如果提供了数据,它将通过 delegatecall 传递给信标返回的实现
// 在构造期间
self.beacon_proxy.constructor(beacon, &data)
}
}
data
参数可以用于:
-
初始化存储 (Initialize storage):传递编码的函数调用来设置初始状态。
-
铸造初始 token (Mint initial tokens):调用 token 合约上的铸造函数。
-
设置权限 (Set up permissions):配置初始访问控制设置。
-
空数据 (Empty data):如果不需要初始化,则传递空字节。
示例:使用数据初始化
use alloy_sol_macro::sol;
use alloy_sol_types::SolCall;
sol! {
interface IERC20 {
function mint(address to, uint256 amount) external;
}
}
// 在您的部署脚本或测试中
let beacon = deploy_beacon();
let implementation = deploy_implementation();
let initial_owner = alice;
let initial_supply = U256::from(1000000);
// 对铸造调用进行编码
let mint_data = IERC20::mintCall {
to: initial_owner,
amount: initial_supply,
}.abi_encode();
// 使用初始化数据部署信标代理
let proxy = MyBeaconProxy::deploy(
beacon.address(),
mint_data.into(),
).expect("Failed to deploy beacon proxy");
存储布局安全性
信标代理使用 ERC-1967 存储槽来实现安全性:
优势
-
无存储冲突 (No Storage Collisions):实现存储不会与代理存储冲突。
-
可预测的布局 (Predictable Layout):存储槽是标准化的并且有详细的文档记录。
-
升级安全 (Upgrade Safety):新的实现可以安全地使用任何存储布局。
-
Gas 效率 (Gas Efficiency):无需复杂的存储间隙模式。
实现存储
您的实现合约可以使用任何存储布局,而无需担心冲突:
#[entrypoint]
#[storage]
struct MyToken {
// 这些字段可以安全使用 - 它们不会与信标代理存储冲突
balances: StorageMapping<Address, U256>,
allowances: StorageMapping<(Address, Address), U256>,
total_supply: StorageU256,
name: StorageString,
symbol: StorageString,
decimals: StorageU8,
// ... 任何其他存储字段
}
最佳实践
-
信任信标 (Trust the beacon):确保您控制或信任信标合约,因为它决定了所有代理的实现。
-
使用适当的访问控制 (Use proper access control):为信标升级功能实现管理控制。
-
测试批量升级 (Test mass upgrades):确保所有代理在信标升级后都能正常工作。
-
监控信标事件 (Monitor beacon events):跟踪信标升级以提高透明度。
-
小心处理初始化数据 (Handle initialization data carefully):仅在提供初始化数据时才发送 value。
-
记录信标所有权 (Document beacon ownership):清楚地记录谁控制信标。
-
使用标准化插槽 (Use standardized slots):不要在您的实现中覆盖 ERC-1967 存储插槽。
-
考虑信标不变性 (Consider beacon immutability):信标代理在部署后无法更改其信标地址。
常见陷阱
-
不受信任的信标 (Untrusted beacon):使用您不控制的信标可能会导致恶意升级。
-
信标不变性 (Beacon immutability):信标代理在部署后无法更改其信标地址。
-
缺少访问控制 (Missing access control):使用适当的访问控制来保护信标升级功能。
-
存储布局更改 (Storage layout changes):在新实现中更改存储布局时要小心。
-
不正确的初始化数据 (Incorrect initialization data):确保初始化数据已正确编码。
-
在没有数据的情况下发送 value (Sending value without data):信标代理会阻止在没有初始化数据的情况下发送 value。
用例
信标代理特别适用于:
-
Token 合约 (Token Contracts):共享相同实现的多个 token 实例。
-
NFT 合集 (NFT Collections):具有相同逻辑的多个 NFT 合约。
-
DeFi 协议 (DeFi Protocols):多个 vault 或 pool 合约。
-
DAO 治理 (DAO Governance):多个治理合约。
-
跨链桥 (Cross-chain Bridges):不同链上的多个桥合约。
相关模式
-
基本代理 (Basic proxy):使用
delegate_call
的基本代理模式,用于可升级合约。 -
信标代理 (Beacon Proxy):多个代理指向单个信标合约,用于批量升级实现合约地址。