Skip to content

Commit 951e946

Browse files
frangiospalladino
andauthored
Add a beacon proxy contract (#2411)
Co-authored-by: Santiago Palladino <[email protected]>
1 parent 520bf7a commit 951e946

10 files changed

+409
-7
lines changed

contracts/mocks/BadBeacon.sol

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity >=0.6.0 <0.8.0;
4+
5+
import "../proxy/IBeacon.sol";
6+
7+
contract BadBeaconNoImpl {
8+
}
9+
10+
contract BadBeaconNotContract is IBeacon {
11+
function implementation() external view override returns (address) {
12+
return address(0x1);
13+
}
14+
}

contracts/mocks/DummyImplementation.sol

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@ contract DummyImplementation {
1919
value = 100;
2020
}
2121

22-
function initializeNonPayable(uint256 _value) public {
22+
function initializeNonPayableWithValue(uint256 _value) public {
2323
value = _value;
2424
}
2525

26-
function initializePayable(uint256 _value) public payable {
26+
function initializePayableWithValue(uint256 _value) public payable {
2727
value = _value;
2828
}
2929

@@ -42,7 +42,7 @@ contract DummyImplementation {
4242
}
4343

4444
function reverts() public pure {
45-
require(false);
45+
require(false, "DummyImplementation reverted");
4646
}
4747
}
4848

contracts/proxy/BeaconProxy.sol

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity >=0.6.0 <0.8.0;
4+
5+
import "./Proxy.sol";
6+
import "../utils/Address.sol";
7+
import "./IBeacon.sol";
8+
9+
/**
10+
* @dev This contract implements a proxy that gets the implementation address for each call from a {UpgradeableBeacon}.
11+
*
12+
* The beacon address is stored in storage slot `uint256(keccak256('eip1967.proxy.beacon')) - 1`, so that it doesn't
13+
* conflict with the storage layout of the implementation behind the proxy.
14+
*/
15+
contract BeaconProxy is Proxy {
16+
/**
17+
* @dev The storage slot of the UpgradeableBeacon contract which defines the implementation for this proxy.
18+
* This is bytes32(uint256(keccak256('eip1967.proxy.beacon')) - 1)) and is validated in the constructor.
19+
*/
20+
bytes32 private constant _BEACON_SLOT = 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50;
21+
22+
/**
23+
* @dev Initializes the proxy with `beacon`.
24+
*
25+
* If `data` is nonempty, it's used as data in a delegate call to the implementation returned by the beacon. This
26+
* will typically be an encoded function call, and allows initializating the storage of the proxy like a Solidity
27+
* constructor.
28+
*
29+
* Requirements:
30+
*
31+
* - `beacon` must be a contract with the interface {IBeacon}.
32+
*/
33+
constructor(address beacon, bytes memory data) public payable {
34+
assert(_BEACON_SLOT == bytes32(uint256(keccak256("eip1967.proxy.beacon")) - 1));
35+
_setBeacon(beacon, data);
36+
}
37+
38+
/**
39+
* @dev Returns the current beacon address.
40+
*/
41+
function _beacon() internal view returns (address beacon) {
42+
bytes32 slot = _BEACON_SLOT;
43+
// solhint-disable-next-line no-inline-assembly
44+
assembly {
45+
beacon := sload(slot)
46+
}
47+
}
48+
49+
/**
50+
* @dev Returns the current implementation address of the associated beacon.
51+
*/
52+
function _implementation() internal view override returns (address) {
53+
return IBeacon(_beacon()).implementation();
54+
}
55+
56+
/**
57+
* @dev Changes the proxy to use a new beacon.
58+
*
59+
* If `data` is nonempty, it's used as data in a delegate call to the implementation returned by the beacon.
60+
*
61+
* Requirements:
62+
*
63+
* - `beacon` must be a contract.
64+
* - The implementation returned by `beacon` must be a contract.
65+
*/
66+
function _setBeacon(address beacon, bytes memory data) internal {
67+
require(
68+
Address.isContract(beacon),
69+
"BeaconProxy: beacon is not a contract"
70+
);
71+
require(
72+
Address.isContract(IBeacon(beacon).implementation()),
73+
"BeaconProxy: beacon implementation is not a contract"
74+
);
75+
bytes32 slot = _BEACON_SLOT;
76+
77+
// solhint-disable-next-line no-inline-assembly
78+
assembly {
79+
sstore(slot, beacon)
80+
}
81+
82+
if (data.length > 0) {
83+
Address.functionDelegateCall(_implementation(), data, "BeaconProxy: function call failed");
84+
}
85+
}
86+
}

contracts/proxy/IBeacon.sol

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity >=0.6.0 <0.8.0;
4+
5+
/**
6+
* @dev This is the interface that {BeaconProxy} expects of its beacon.
7+
*/
8+
interface IBeacon {
9+
/**
10+
* @dev Must return an address that can be used as a delegate call target.
11+
*
12+
* {BeaconProxy} will check that this address is a contract.
13+
*/
14+
function implementation() external view returns (address);
15+
}

contracts/proxy/README.adoc

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ The abstract {Proxy} contract implements the core delegation functionality. If t
99

1010
Upgradeability is implemented in the {UpgradeableProxy} contract, although it provides only an internal upgrade interface. For an upgrade interface exposed externally to an admin, we provide {TransparentUpgradeableProxy}. Both of these contracts use the storage slots specified in https://eips.ethereum.org/EIPS/eip-1967[EIP1967] to avoid clashes with the storage of the implementation contract behind the proxy.
1111

12+
An alternative upgradeability mechanism is provided in <<UpgradeableBeacon>>. This pattern, popularized by Dharma, allows multiple proxies to be upgraded to a different implementation in a single transaction. In this pattern, the proxy contract doesn't hold the implementation address in storage like {UpgradeableProxy}, but the address of a {UpgradeableBeacon} contract, which is where the implementation address is actually stored and retrieved from. The `upgrade` operations that change the implementation contract address are then sent to the beacon instead of to the proxy contract, and all proxies that follow that beacon are automatically upgraded.
13+
1214
CAUTION: Using upgradeable proxies correctly and securely is a difficult task that requires deep knowledge of the proxy pattern, Solidity, and the EVM. Unless you want a lot of low level control, we recommend using the xref:upgrades-plugins::index.adoc[OpenZeppelin Upgrades Plugins] for Truffle and Buidler.
1315

1416
== Core
@@ -19,6 +21,14 @@ CAUTION: Using upgradeable proxies correctly and securely is a difficult task th
1921

2022
{{TransparentUpgradeableProxy}}
2123

24+
== UpgradeableBeacon
25+
26+
{{BeaconProxy}}
27+
28+
{{IBeacon}}
29+
30+
{{UpgradeableBeacon}}
31+
2232
== Utilities
2333

2434
{{Initializable}}

contracts/proxy/UpgradeableBeacon.sol

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity >=0.6.0 <0.8.0;
4+
5+
import "./IBeacon.sol";
6+
import "../access/Ownable.sol";
7+
import "../utils/Address.sol";
8+
9+
/**
10+
* @dev This contract is used in conjunction with one or more instances of {BeaconProxy} to determine their
11+
* implementation contract, which is where they will delegate all function calls.
12+
*
13+
* An owner is able to change the implementation the beacon points to, thus upgrading the proxies that use this beacon.
14+
*/
15+
contract UpgradeableBeacon is IBeacon, Ownable {
16+
address private _implementation;
17+
18+
/**
19+
* @dev Emitted when the implementation returned by the beacon is changed.
20+
*/
21+
event Upgraded(address indexed implementation);
22+
23+
/**
24+
* @dev Sets the address of the initial implementation, and the deployer account as the owner who can upgrade the
25+
* beacon.
26+
*/
27+
constructor(address implementation_) public {
28+
_setImplementation(implementation_);
29+
}
30+
31+
/**
32+
* @dev Returns the current implementation address.
33+
*/
34+
function implementation() public view override returns (address) {
35+
return _implementation;
36+
}
37+
38+
/**
39+
* @dev Upgrades the beacon to a new implementation.
40+
*
41+
* Emits an {Upgraded} event.
42+
*
43+
* Requirements:
44+
*
45+
* - msg.sender must be the owner of the contract.
46+
* - `newImplementation` must be a contract.
47+
*/
48+
function upgradeTo(address newImplementation) public onlyOwner {
49+
_setImplementation(newImplementation);
50+
emit Upgraded(newImplementation);
51+
}
52+
53+
/**
54+
* @dev Sets the implementation contract address for this beacon
55+
*
56+
* Requirements:
57+
*
58+
* - `newImplementation` must be a contract.
59+
*/
60+
function _setImplementation(address newImplementation) private {
61+
require(Address.isContract(newImplementation), "UpgradeableBeacon: implementation is not a contract");
62+
_implementation = newImplementation;
63+
}
64+
}

test/proxy/BeaconProxy.test.js

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
const { BN, expectRevert } = require('@openzeppelin/test-helpers');
2+
const ethereumjsUtil = require('ethereumjs-util');
3+
const { keccak256 } = ethereumjsUtil;
4+
5+
const { expect } = require('chai');
6+
7+
const UpgradeableBeacon = artifacts.require('UpgradeableBeacon');
8+
const BeaconProxy = artifacts.require('BeaconProxy');
9+
const DummyImplementation = artifacts.require('DummyImplementation');
10+
const DummyImplementationV2 = artifacts.require('DummyImplementationV2');
11+
const BadBeaconNoImpl = artifacts.require('BadBeaconNoImpl');
12+
const BadBeaconNotContract = artifacts.require('BadBeaconNotContract');
13+
14+
function toChecksumAddress (address) {
15+
return ethereumjsUtil.toChecksumAddress('0x' + address.replace(/^0x/, '').padStart(40, '0'));
16+
}
17+
18+
const BEACON_LABEL = 'eip1967.proxy.beacon';
19+
const BEACON_SLOT = '0x' + new BN(keccak256(Buffer.from(BEACON_LABEL))).subn(1).toString(16);
20+
21+
contract('BeaconProxy', function (accounts) {
22+
const [anotherAccount] = accounts;
23+
24+
describe('bad beacon is not accepted', async function () {
25+
it('non-contract beacon', async function () {
26+
await expectRevert(
27+
BeaconProxy.new(anotherAccount, '0x'),
28+
'BeaconProxy: beacon is not a contract',
29+
);
30+
});
31+
32+
it('non-compliant beacon', async function () {
33+
const beacon = await BadBeaconNoImpl.new();
34+
await expectRevert.unspecified(
35+
BeaconProxy.new(beacon.address, '0x'),
36+
);
37+
});
38+
39+
it('non-contract implementation', async function () {
40+
const beacon = await BadBeaconNotContract.new();
41+
await expectRevert(
42+
BeaconProxy.new(beacon.address, '0x'),
43+
'BeaconProxy: beacon implementation is not a contract',
44+
);
45+
});
46+
});
47+
48+
before('deploy implementation', async function () {
49+
this.implementationV0 = await DummyImplementation.new();
50+
this.implementationV1 = await DummyImplementationV2.new();
51+
});
52+
53+
describe('initialization', function () {
54+
before(function () {
55+
this.assertInitialized = async ({ value, balance }) => {
56+
const beaconAddress = toChecksumAddress(await web3.eth.getStorageAt(this.proxy.address, BEACON_SLOT));
57+
expect(beaconAddress).to.equal(this.beacon.address);
58+
59+
const dummy = new DummyImplementation(this.proxy.address);
60+
expect(await dummy.value()).to.bignumber.eq(value);
61+
62+
expect(await web3.eth.getBalance(this.proxy.address)).to.bignumber.eq(balance);
63+
};
64+
});
65+
66+
beforeEach('deploy beacon', async function () {
67+
this.beacon = await UpgradeableBeacon.new(this.implementationV0.address);
68+
});
69+
70+
it('no initialization', async function () {
71+
const data = Buffer.from('');
72+
const balance = '10';
73+
this.proxy = await BeaconProxy.new(this.beacon.address, data, { value: balance });
74+
await this.assertInitialized({ value: '0', balance });
75+
});
76+
77+
it('non-payable initialization', async function () {
78+
const value = '55';
79+
const data = this.implementationV0.contract.methods
80+
.initializeNonPayableWithValue(value)
81+
.encodeABI();
82+
this.proxy = await BeaconProxy.new(this.beacon.address, data);
83+
await this.assertInitialized({ value, balance: '0' });
84+
});
85+
86+
it('payable initialization', async function () {
87+
const value = '55';
88+
const data = this.implementationV0.contract.methods
89+
.initializePayableWithValue(value)
90+
.encodeABI();
91+
const balance = '100';
92+
this.proxy = await BeaconProxy.new(this.beacon.address, data, { value: balance });
93+
await this.assertInitialized({ value, balance });
94+
});
95+
96+
it('reverting initialization', async function () {
97+
const data = this.implementationV0.contract.methods.reverts().encodeABI();
98+
await expectRevert(
99+
BeaconProxy.new(this.beacon.address, data),
100+
'DummyImplementation reverted',
101+
);
102+
});
103+
});
104+
105+
it('upgrade a proxy by upgrading its beacon', async function () {
106+
const beacon = await UpgradeableBeacon.new(this.implementationV0.address);
107+
108+
const value = '10';
109+
const data = this.implementationV0.contract.methods
110+
.initializeNonPayableWithValue(value)
111+
.encodeABI();
112+
const proxy = await BeaconProxy.new(beacon.address, data);
113+
114+
const dummy = new DummyImplementation(proxy.address);
115+
116+
// test initial values
117+
expect(await dummy.value()).to.bignumber.eq(value);
118+
119+
// test initial version
120+
expect(await dummy.version()).to.eq('V1');
121+
122+
// upgrade beacon
123+
await beacon.upgradeTo(this.implementationV1.address);
124+
125+
// test upgraded version
126+
expect(await dummy.version()).to.eq('V2');
127+
});
128+
129+
it('upgrade 2 proxies by upgrading shared beacon', async function () {
130+
const value1 = '10';
131+
const value2 = '42';
132+
133+
const beacon = await UpgradeableBeacon.new(this.implementationV0.address);
134+
135+
const proxy1InitializeData = this.implementationV0.contract.methods
136+
.initializeNonPayableWithValue(value1)
137+
.encodeABI();
138+
const proxy1 = await BeaconProxy.new(beacon.address, proxy1InitializeData);
139+
140+
const proxy2InitializeData = this.implementationV0.contract.methods
141+
.initializeNonPayableWithValue(value2)
142+
.encodeABI();
143+
const proxy2 = await BeaconProxy.new(beacon.address, proxy2InitializeData);
144+
145+
const dummy1 = new DummyImplementation(proxy1.address);
146+
const dummy2 = new DummyImplementation(proxy2.address);
147+
148+
// test initial values
149+
expect(await dummy1.value()).to.bignumber.eq(value1);
150+
expect(await dummy2.value()).to.bignumber.eq(value2);
151+
152+
// test initial version
153+
expect(await dummy1.version()).to.eq('V1');
154+
expect(await dummy2.version()).to.eq('V1');
155+
156+
// upgrade beacon
157+
await beacon.upgradeTo(this.implementationV1.address);
158+
159+
// test upgraded version
160+
expect(await dummy1.version()).to.eq('V2');
161+
expect(await dummy2.version()).to.eq('V2');
162+
});
163+
});

0 commit comments

Comments
 (0)