Solidity Beacon Proxies
A beacon is a contract that holds one implementation address for many proxies to share. One update to the beacon changes the behaviour of every proxy that reads it.
One beacon update upgrades two proxies
Two proxies read the same beacon and hold different values. After one beacon update, both gain the new function and keep their own values.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract LogicV1 {
uint256 public value;
function setValue(uint256 v) public {
value = v;
}
}
contract LogicV2 {
uint256 public value;
function setValue(uint256 v) public {
value = v;
}
function increment() public {
value += 1;
}
}
contract Beacon {
address public implementation;
address public owner;
constructor(address impl) {
implementation = impl;
owner = msg.sender;
}
function upgradeTo(address impl) public {
require(msg.sender == owner, "only owner");
implementation = impl;
}
}
contract BeaconProxy {
bytes32 constant BEACON_SLOT = 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50;
constructor(address beacon) {
bytes32 slot = BEACON_SLOT;
assembly { sstore(slot, beacon) }
}
function beaconAddress() public view returns (address beacon) {
bytes32 slot = BEACON_SLOT;
assembly { beacon := sload(slot) }
}
fallback() external payable {
address impl = Beacon(beaconAddress()).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()) }
}
}
}
Only the beacon owner can update the beacon
The beacon guards its update with the stored owner. A stranger update reverts, while the owner update succeeds.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract LogicV1 {
uint256 public value;
function setValue(uint256 v) public {
value = v;
}
}
contract LogicV2 {
uint256 public value;
function setValue(uint256 v) public {
value = v;
}
function increment() public {
value += 1;
}
}
contract Beacon {
address public implementation;
address public owner;
constructor(address impl) {
implementation = impl;
owner = msg.sender;
}
function upgradeTo(address impl) public {
require(msg.sender == owner, "only owner");
implementation = impl;
}
}
contract BeaconProxy {
bytes32 constant BEACON_SLOT = 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50;
constructor(address beacon) {
bytes32 slot = BEACON_SLOT;
assembly { sstore(slot, beacon) }
}
function beaconAddress() public view returns (address beacon) {
bytes32 slot = BEACON_SLOT;
assembly { beacon := sload(slot) }
}
fallback() external payable {
address impl = Beacon(beaconAddress()).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()) }
}
}
}
A beacon proxy cannot be upgraded directly
The proxy holds no implementation of its own. An upgrade call sent to the proxy itself reverts, because the logic has no such function.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract LogicV1 {
uint256 public value;
function setValue(uint256 v) public {
value = v;
}
}
contract DirectUpgrade {
function upgradeTo(address) public {
}
}
contract Beacon {
address public implementation;
address public owner;
constructor(address impl) {
implementation = impl;
owner = msg.sender;
}
function upgradeTo(address impl) public {
require(msg.sender == owner, "only owner");
implementation = impl;
}
}
contract BeaconProxy {
bytes32 constant BEACON_SLOT = 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50;
constructor(address beacon) {
bytes32 slot = BEACON_SLOT;
assembly { sstore(slot, beacon) }
}
function beaconAddress() public view returns (address beacon) {
bytes32 slot = BEACON_SLOT;
assembly { beacon := sload(slot) }
}
fallback() external payable {
address impl = Beacon(beaconAddress()).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()) }
}
}
}
Example
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract VaultV1 {
uint256 public saved;
function addSaved(uint256 v) public {
saved += v;
}
}
contract VaultV2 {
uint256 public saved;
function addSaved(uint256 v) public {
saved += v;
}
function doubleSaved() public {
saved *= 2;
}
}
contract Beacon {
address public implementation;
address public owner;
constructor(address impl) {
implementation = impl;
owner = msg.sender;
}
function upgradeTo(address impl) public {
require(msg.sender == owner, "only owner");
implementation = impl;
}
}
contract BeaconProxy {
bytes32 constant BEACON_SLOT = 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50;
constructor(address beacon) {
bytes32 slot = BEACON_SLOT;
assembly { sstore(slot, beacon) }
}
function beaconAddress() public view returns (address beacon) {
bytes32 slot = BEACON_SLOT;
assembly { beacon := sload(slot) }
}
fallback() external payable {
address impl = Beacon(beaconAddress()).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()) }
}
}
}
Key points
- Beacon storage: the beacon holds the implementation address, and only its owner may change it.
- Proxy reading: each beacon proxy asks the beacon for the current implementation on every call.
- One update: changing the beacon once changes the behaviour of every proxy that reads it.
- Separate state: each proxy keeps its own values, even though the proxies share one implementation.
- Beacon slot: the proxy stores the beacon address in a fixed high slot, so it never collides with logic values.
In this example, a beacon points at VaultV1, and two beacon proxies read it. The first vault saves 30 and the second saves 40, then one beacon update points both proxies at VaultV2, and each vault doubles only its own savings, to 60 and 80.
Note
a beacon proxy never stores the implementation itself, so it cannot be upgraded on its own.
Try it Yourself
Edit the code, then press Run.
Running is simulated · Remix IDE
Exercise
How many beacon updates does it take to upgrade every proxy reading that beacon?