Разработка смарт-контрактов с Beacon Proxy Pattern
Представьте, что ваш протокол создаёт сотни или тысячи proxy-контрактов через factory: lending позиции, per-user vaults, NFT-коллекции с индивидуальной логикой. Стандартный UUPS или Transparent Proxy означает, что обновление реализации требует отдельного вызова для каждого из них. При 500 proxy-контрактах это 500 транзакций, несколько ETH газа, и высокий риск ошибки.
Beacon Proxy решает это одним вызовом.
Как работает Beacon Proxy
Архитектура состоит из трёх уровней:
Beacon контракт — хранит единственное значение: адрес текущей реализации. Имеет функцию upgradeTo(address) с контролем доступа.
Proxy контракты — при каждом вызове обращаются к Beacon за адресом реализации, затем делают delegatecall. Все proxy читают адрес из одного beacon.
Implementation контракт — содержит бизнес-логику.
// BeaconProxy.sol (упрощённо)
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()) }
}
}
Обновление всех proxy: один вызов beacon.upgradeTo(newImplementation). Всё.
Когда нужен Beacon Proxy
Критерий прост: если создаётся N > 5 экземпляров одного контракта через factory, и они должны обновляться синхронно — Beacon Proxy.
Типичные случаи:
- Yield vaults — каждый vault по стратегии как отдельный proxy
- Lending positions — CDP-позиции в MakerDAO-like протоколах
- Per-user contracts — каждый пользователь получает свой изолированный контракт
- Game instances — отдельный контракт на каждую игровую сессию
Имплементация с OpenZeppelin
OpenZeppelin предоставляет BeaconProxy и UpgradeableBeacon. Вместе с 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);
}
// Обновление всех vault'ов одним вызовом
function upgradeImplementation(address newImpl) external onlyOwner {
beacon.upgradeTo(newImpl);
}
}
Важно: контракт реализации должен быть написан с учётом storage layout совместимости — те же правила, что для UUPS и Transparent Proxy. @openzeppelin/upgrades-plugins проверяет это автоматически.
Газ и производительность
Каждый вызов через BeaconProxy дороже прямого вызова: два SLOAD (beacon address + implementation address) — около 4200 gas overhead при cold access, ~400 при warm. Для большинства операций это незначимо. Для hot-path функций с высоким TPS — рассматриваем immutable beacon address в proxy constructor (экономит один SLOAD, теряем возможность смены beacon).
Сравнение паттернов
| Паттерн | Газ overhead | Обновление N proxy | Подходит для |
|---|---|---|---|
| Transparent Proxy | ~2100 gas | N транзакций | Единичные контракты |
| UUPS | ~400 gas | N транзакций | Единичные, gas-sensitive |
| Beacon Proxy | ~4200 gas (cold) | 1 транзакция | Factory, множественные экземпляры |
| Diamond | Зависит от facets | N транзакций | Большие контракты (>24KB) |
Сроки
Beacon Proxy setup с factory контрактом, initializer-логикой и тестами — 3-5 рабочих дней. Включает: Beacon + UpgradeableBeacon контракты, Factory с event emission, полный тест-сьют (deploy, upgrade, storage layout check), деплой на testnet.







