Beacon Proxy
Beacon Proxy is an advanced proxy pattern that allows multiple proxy contracts to share a single beacon contract that determines their implementation. This pattern is particularly useful for scenarios where you want to upgrade multiple proxy contracts simultaneously by updating a single beacon.
The OpenZeppelin Stylus Contracts provides a complete implementation of the Beacon Proxy pattern, including the BeaconProxy
contract and UpgradeableBeacon
contract.
Understanding Beacon Proxy
The Beacon Proxy pattern consists of three main components:
- Beacon Contract: A contract that stores the current implementation address.
- Beacon Proxy: Multiple proxy contracts that delegate to the beacon for their implementation.
- Implementation Contract: The actual logic contract that gets executed.
How It Works
- Multiple
BeaconProxy
contracts are deployed, each pointing to the sameUpgradeableBeacon
. - The
UpgradeableBeacon
stores the current implementation address. - When a call is made to any
BeaconProxy
, it queries the beacon for the current implementation. - The proxy then delegates the call to that implementation.
- To upgrade all proxies, you only need to update the beacon’s implementation.
Benefits
- Mass Upgrades: Upgrade multiple proxies with a single transaction.
- Gas Efficiency: Shared beacon reduces storage costs.
- Consistency: All proxies always use the same implementation.
- Centralized Control: Single point of control for upgrades.
- Trust Minimization: Proxies can verify the beacon’s implementation.
Basic Beacon Proxy Implementation
Here’s how to implement a basic beacon proxy:
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)
/// Get the beacon address
fn get_beacon(&self) -> Address
self.beacon_proxy.get_beacon()
/// Get the current implementation address from the beacon
fn implementation(&self) -> Result<Address, Vec<u8>>
self.beacon_proxy.implementation()
/// Fallback function that delegates all calls to the 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()
}
Upgradeable Beacon Implementation
The UpgradeableBeacon
contract manages the implementation address and provides upgrade functionality:
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)
/// Upgrade to a new implementation (only 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)?)
}
Custom Beacon Implementation
You can also implement your own beacon contract by implementing the 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);
/// Upgrade implementation (only 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())
}
Constructor Data
Like ERC-1967 proxies, beacon proxies support initialization data:
impl MyBeaconProxy
#[constructor]
fn constructor(
&mut self,
beacon: Address,
data: Bytes,
) -> Result<(), erc1967::utils::Error> {
// If data is provided, it will be passed to the implementation
// returned by the beacon during construction via delegatecall
self.beacon_proxy.constructor(beacon, &data)
}
The data
parameter can be used to:
- Initialize storage: Pass encoded function calls to set up initial state.
- Mint initial tokens: Call mint functions on token contracts.
- Set up permissions: Configure initial access control settings.
- Empty data: Pass empty bytes if no initialization is needed.
Example: Initializing with Data
use alloy_sol_macro::sol;
use alloy_sol_types::SolCall;
sol!
interface IERC20 {
function mint(address to, uint256 amount) external;
}
// In your deployment script or test
let beacon = deploy_beacon();
let implementation = deploy_implementation();
let initial_owner = alice;
let initial_supply = U256::from(1000000);
// Encode the mint call
let mint_data = IERC20::mintCall
to: initial_owner,
amount: initial_supply,
.abi_encode();
// Deploy beacon proxy with initialization data
let proxy = MyBeaconProxy::deploy(
beacon.address(),
mint_data.into(),
).expect("Failed to deploy beacon proxy");
Storage Layout Safety
Beacon proxies use ERC-1967 storage slots for safety:
Benefits
- No Storage Collisions: Implementation storage cannot conflict with proxy storage.
- Predictable Layout: Storage slots are standardized and well-documented.
- Upgrade Safety: New implementations can safely use any storage layout.
- Gas Efficiency: No need for complex storage gap patterns.
Implementation Storage
Your implementation contract can use any storage layout without worrying about conflicts:
#[entrypoint]
#[storage]
struct MyToken
// These fields are safe to use - they won't conflict with beacon proxy storage
balances: StorageMapping<Address, U256>,
allowances: StorageMapping<(Address, Address), U256>,
total_supply: StorageU256,
name: StorageString,
symbol: StorageString,
decimals: StorageU8,
// ... any other storage fields
Best Practices
- Trust the beacon: Ensure you control or trust the beacon contract, as it determines all proxy implementations.
- Use proper access control: Implement admin controls for beacon upgrade functions.
- Test mass upgrades: Ensure all proxies work correctly after beacon upgrades.
- Monitor beacon events: Track beacon upgrades for transparency.
- Handle initialization data carefully: Only send value when providing initialization data.
- Document beacon ownership: Clearly document who controls the beacon.
- Use standardized slots: Don’t override the ERC-1967 storage slots in your implementation.
- Consider beacon immutability: Beacon proxies cannot change their beacon address after deployment.
Common Pitfalls
- Untrusted beacon: Using a beacon you don’t control can lead to malicious upgrades.
- Beacon immutability: Beacon proxies cannot change their beacon address after deployment.
- Missing access control: Protect beacon upgrade functions with proper access control.
- Storage layout changes: Be careful when changing storage layout in new implementations.
- Incorrect initialization data: Ensure initialization data is properly encoded.
- Sending value without data: Beacon proxies prevent sending value without initialization data.
Use Cases
Beacon proxies are particularly useful for:
- Token Contracts: Multiple token instances sharing the same implementation.
- NFT Collections: Multiple NFT contracts with identical logic.
- DeFi Protocols: Multiple vault or pool contracts.
- DAO Governance: Multiple governance contracts.
- Cross-chain Bridges: Multiple bridge contracts on different chains.
Related Patterns
- Basic proxy: Basic proxy pattern using
delegate_call
for upgradeable contracts. - Beacon Proxy: Multiple proxies pointing to a single beacon contract for mass upgrades of the implementation contract address.
- UUPS Proxy: The Universal Upgradeable Proxy Standard (UUPS) that is a minimal and gas-efficient pattern for upgradeable contracts.