Smart Contract Development with Beacon Proxy Pattern
Imagine your protocol creates hundreds or thousands of proxy contracts through a factory: lending positions, per-user vaults, NFT collections with individual logic. Standard UUPS or Transparent Proxy means that updating the implementation requires a separate call for each. With 500 proxy contracts that's 500 transactions, several ETH in gas, and a high risk of error.
Beacon Proxy solves this in one call.
How Beacon Proxy Works
The architecture consists of three levels:
Beacon contract — stores a single value: the address of the current implementation. Has an upgradeTo(address) function with access control.
Proxy contracts — on each call they query the Beacon for the implementation address, then make a delegatecall. All proxies read the address from one beacon.
Implementation contract — contains business logic.
// BeaconProxy.sol (simplified)
fallback() external payable {
address impl = IBeacon(beacon).implementation();
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}
Upgrade all proxies: one call to beacon.upgradeTo(newImplementation). Done.
When Beacon Proxy is Needed
The criterion is simple: if N > 5 instances of one contract are created through a factory, and they should be upgraded synchronously — use Beacon Proxy.
Typical cases:
- Yield vaults — each vault by strategy as a separate proxy
- Lending positions — CDP positions in MakerDAO-like protocols
- Per-user contracts — each user gets their own isolated contract
- Game instances — separate contract for each game session
Implementation with OpenZeppelin
OpenZeppelin provides BeaconProxy and UpgradeableBeacon. Combined with a factory:
contract VaultFactory {
UpgradeableBeacon public immutable beacon;
constructor(address initialImplementation) {
beacon = new UpgradeableBeacon(initialImplementation);
beacon.transferOwnership(msg.sender);
}
function createVault(address owner) external returns (address) {
BeaconProxy proxy = new BeaconProxy(
address(beacon),
abi.encodeWithSignature("initialize(address)", owner)
);
return address(proxy);
}
// Upgrade all vaults in one call
function upgradeImplementation(address newImpl) external onlyOwner {
beacon.upgradeTo(newImpl);
}
}
Important: the implementation contract must be written with storage layout compatibility in mind — same rules as for UUPS and Transparent Proxy. @openzeppelin/upgrades-plugins checks this automatically.
Gas and Performance
Each call through BeaconProxy is more expensive than a direct call: two SLOADs (beacon address + implementation address) — about 4200 gas overhead on cold access, ~400 on warm. For most operations this is negligible. For hot-path functions with high TPS — consider an immutable beacon address in proxy constructor (saves one SLOAD, loses ability to change beacon).
Pattern Comparison
| Pattern | Gas Overhead | Upgrade N proxies | Suitable for |
|---|---|---|---|
| Transparent Proxy | ~2100 gas | N transactions | Single contracts |
| UUPS | ~400 gas | N transactions | Single, gas-sensitive |
| Beacon Proxy | ~4200 gas (cold) | 1 transaction | Factory, multiple instances |
| Diamond | Depends on facets | N transactions | Large contracts (>24KB) |
Timeline
Beacon Proxy setup with factory contract, initializer logic and tests — 3-5 business days. Includes: Beacon + UpgradeableBeacon contracts, Factory with event emission, full test suite (deploy, upgrade, storage layout check), testnet deployment.







