NamedBeacon and Proxy: Optimized Implementation for Upgradable Solidity Contracts
Developers often face inefficiencies with the standard BeaconProxy: a separate contract just to store one implementation address, duplicated references in immutable and storage, and redundant admin logic in every proxy. The proposed pair of NamedBeacon and NamedBeaconProxy eliminates these issues, minimizing gas costs and bytecode size.
NamedBeacon centralizes management of implementation addresses using string-like bytes32 identifiers. The proxy stores only immutable references to the beacon and referenceId, dynamically fetching the current implementation at runtime.
NamedBeacon: Centralized Implementation Registry
This contract implements a mapping(bytes32 => address) to store addresses. Two key functions:
registerImplementation(bytes32 _referenceId, address _implementation)— registration, available only to the owner.getImplementation(bytes32 _referenceId)— retrieves the address by ID, returns address(0) if not found.
Administration is centralized: owner checks are enforced in registerImplementation. Implementation validation prevents zero addresses without code. For compatibility with OpenZeppelin, an implementation() function is included that returns address(this).
Storage usage is ideal for immutable scenarios after deployment. The absence of SlotStorage simplifies the code but limits beacon upgrades.
Full NamedBeacon code:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.33;
interface INamedBeacon {
function getImplementation(bytes32 _referenceId) external view returns (address _implementation);
function registerImplementation(bytes32 _referenceId, address _implementation) external;
}
contract NamedBeacon {
/// @dev This must be overwritten by your custom auth logic
address public owner;
/// @notice Main storage of implementations
mapping(bytes32 implId => address implAddress) internal implementations;
error InvalidImplementation(address implementation);
error UnauthorizedAccess();
// ctor
constructor (address _owner) {
/// @dev This must be overwritten by your custom auth logic
require (_owner != address(0));
owner = _owner;
}
/// @notice Returns an address of an implementation, registered under _referenceId. If no reference is found, returns address(0).
/// @param _referenceId Unique ref id of the referenced contract
function getImplementation(bytes32 _referenceId) external view returns (address _implementation) {
return implementations[_referenceId];
}
/// @notice Registers implementation _implementation under given _referenceId.
/// @param _referenceId Unique ref id of the referenced contract
/// @param _implementation Address of implementation
function registerImplementation(bytes32 _referenceId, address _implementation) external {
require (msg.sender == owner, UnauthorizedAccess());
if (_implementation != address(0) && _implementation.code.length == 0) {
revert InvalidImplementation(_implementation);
}
implementations[_referenceId] = _implementation;
}
/// @notice Special function to mimic beacon functionality from OZ
/// @dev Exists only to be compatible with OZ BeaconProxy
/// @dev BeaconProxy.ctor() => ERC1967Utils.upgradeBeaconToAndCall(beacon, data) => _setBeacon(address newBeacon) => address beaconImplementation = IBeacon(newBeacon).implementation();
/// @dev 1. this should return a valid address
/// @dev 2. AND it should be a contract: if (beaconImplementation.code.length == 0) { revert }
/// @dev Similar issue is in ERC1967Utils.upgradeBeaconToAndCall(address newBeacon, bytes memory data),
/// @dev on line `Address.functionDelegateCall(IBeacon(newBeacon).implementation(), data);`
/// @dev Given this,
function implementation() external view returns (address _current) {
return address(this);
}
}
NamedBeaconProxy: Minimalist Upgradable Proxy
Extends Proxy and uses Address from OpenZeppelin. Immutable fields: beacon and implementationReferenceId — no storage slots used.
In the constructor:
- Validates beacon (code.length > 0).
- Fetches implementation from beacon via referenceId, checks code.length.
- Optional delegatecall to initialize with _data.
Overridden _implementation() returns INamedBeacon(beacon).getImplementation(implementationReferenceId).
Supports receive() for payable, throws NonPayable error if msg.value > 0 without _data.
Full NamedBeaconProxy code:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.33;
import { Address } from "@openzeppelin/contracts/utils/Address.sol";
import { Proxy } from "@openzeppelin/contracts/proxy/Proxy.sol";
import { INamedBeacon } from "./NamedBeacon.sol";
contract NamedBeaconProxy is Proxy {
address private immutable beacon;
bytes32 private immutable implementationReferenceId;
error NonPayable();
error InvalidBeacon(address beacon);
error InvalidImplementation(address implementation);
error ImplementationNotFound(bytes32 implementationReferenceId);
constructor(address _beacon, bytes32 _implementationReferenceId, bytes memory _data) payable {
// set beacon
if (_beacon.code.length == 0) {
revert InvalidBeacon(_beacon);
}
beacon = _beacon;
// set implementation ref id
address implementationFromBeacon = INamedBeacon(_beacon).getImplementation(_implementationReferenceId);
if (implementationFromBeacon.code.length == 0) {
revert InvalidImplementation(implementationFromBeacon);
}
implementationReferenceId = _implementationReferenceId;
// initialize if data is provided
if (_data.length > 0) {
Address.functionDelegateCall(implementationFromBeacon, _data);
} else {
_checkNonPayable();
}
}
function _implementation() internal view virtual override returns (address) {
return INamedBeacon(beacon).getImplementation(implementationReferenceId);
}
receive() external payable {}
function _checkNonPayable() private {
if (msg.value > 0) {
revert NonPayable();
}
}
}
Deployment and Usage Workflow
- Deploy NamedBeacon with an owner.
- Deploy implementation contracts.
- Register:
registerImplementation(keccak256('v1'), implementationAddress). - Deploy NamedBeaconProxy(beacon, referenceId, initData).
Validation checks in the proxy can be disabled to save gas on calls. Owner logic can be customized in a derived NamedBeacon contract.
Key Benefits
- Centralized storage of implementation addresses via bytes32 IDs reduces code duplication.
- Immutable references in the proxy eliminate storage slot usage.
- Minimal proxy size: no admin logic, just delegatecall to the beacon.
- Full OpenZeppelin compatibility: supports
implementation()for upgradeBeaconToAndCall. - Built for production: Solidity 0.8.33, Hardhat 3.1.6, with comprehensive test coverage.
— Editorial Team
No comments yet.