Skip to content

Add a beacon proxy contract #2411

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 33 commits into from
Nov 26, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
c6f7a74
import beacon proxy files from f8n/openzeppelin-sdk
frangio Nov 13, 2020
dc003f8
rename UpgradeabilityBeacon -> BeaconUpgradeable
frangio Nov 13, 2020
47d93a4
get contracts to compile
frangio Nov 13, 2020
3ecf638
edit tests
frangio Nov 13, 2020
e7ef88c
refactor tests minimally to pass
frangio Nov 16, 2020
33c4257
use Address.functionDelegateCall
frangio Nov 16, 2020
00e3d30
use 4 space indentation
frangio Nov 16, 2020
d0a9b7c
inline behavior into test file
frangio Nov 18, 2020
68b56e0
adapt reason strings to convention and test for them
frangio Nov 18, 2020
1124059
Merge branch 'master' into beacon-proxy
frangio Nov 18, 2020
86d40ec
widen pragma for 0.7
frangio Nov 18, 2020
edbad75
rename clashing variable
frangio Nov 19, 2020
cbbdcd3
add documentation or edit to conform to our conventions
frangio Nov 20, 2020
f40ffc4
lint
frangio Nov 20, 2020
12b4314
fix UpgradeableProxy tests
frangio Nov 20, 2020
fe8037b
fix ProxyAdmin tests
frangio Nov 20, 2020
2c0b0f4
fix tests under solidity-coverage (ganache)
frangio Nov 20, 2020
3e821fe
rename BeaconUpgradeableProxy -> BeaconProxy, Beacon -> UpgradeableBe…
frangio Nov 20, 2020
3da41f5
fix tests under solidity-coverage (ganache)
frangio Nov 20, 2020
7b08c20
Update contracts/proxy/README.adoc
frangio Nov 25, 2020
b72bee0
Update contracts/proxy/UpgradeableBeacon.sol
frangio Nov 25, 2020
1f051c5
add positive test for available function
frangio Nov 25, 2020
1cd213d
remove unnecessary await
frangio Nov 25, 2020
56b1c45
refactor handling of beacon to avoid repeated getStorageAt
frangio Nov 25, 2020
08b7244
create separate test suite for UpgradeableBeacon
frangio Nov 25, 2020
4567c90
fix bad test description
frangio Nov 25, 2020
5c3e8c8
simplify test for shared beacon
frangio Nov 25, 2020
b4bdf1c
simplify initialization tests
frangio Nov 25, 2020
48c56ee
simplify upgrade tests
frangio Nov 25, 2020
88db9a4
simplify test description
frangio Nov 25, 2020
541b743
lint
frangio Nov 25, 2020
298f068
add more tests for bad beacon
frangio Nov 25, 2020
e72ead8
lint
frangio Nov 25, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions contracts/mocks/BadBeacon.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// SPDX-License-Identifier: MIT

pragma solidity >=0.6.0 <0.8.0;

import "../proxy/IBeacon.sol";

contract BadBeaconNoImpl {
}

contract BadBeaconNotContract is IBeacon {
function implementation() external view override returns (address) {
return address(0x1);
}
}
6 changes: 3 additions & 3 deletions contracts/mocks/DummyImplementation.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ contract DummyImplementation {
value = 100;
}

function initializeNonPayable(uint256 _value) public {
function initializeNonPayableWithValue(uint256 _value) public {
value = _value;
}

function initializePayable(uint256 _value) public payable {
function initializePayableWithValue(uint256 _value) public payable {
value = _value;
}

Expand All @@ -42,7 +42,7 @@ contract DummyImplementation {
}

function reverts() public pure {
require(false);
require(false, "DummyImplementation reverted");
}
}

Expand Down
86 changes: 86 additions & 0 deletions contracts/proxy/BeaconProxy.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// SPDX-License-Identifier: MIT

pragma solidity >=0.6.0 <0.8.0;

import "./Proxy.sol";
import "../utils/Address.sol";
import "./IBeacon.sol";

/**
* @dev This contract implements a proxy that gets the implementation address for each call from a {UpgradeableBeacon}.
*
* The beacon address is stored in storage slot `uint256(keccak256('eip1967.proxy.beacon')) - 1`, so that it doesn't
* conflict with the storage layout of the implementation behind the proxy.
*/
contract BeaconProxy is Proxy {
/**
* @dev The storage slot of the UpgradeableBeacon contract which defines the implementation for this proxy.
* This is bytes32(uint256(keccak256('eip1967.proxy.beacon')) - 1)) and is validated in the constructor.
*/
bytes32 private constant _BEACON_SLOT = 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50;

/**
* @dev Initializes the proxy with `beacon`.
*
* If `data` is nonempty, it's used as data in a delegate call to the implementation returned by the beacon. This
* will typically be an encoded function call, and allows initializating the storage of the proxy like a Solidity
* constructor.
*
* Requirements:
*
* - `beacon` must be a contract with the interface {IBeacon}.
*/
constructor(address beacon, bytes memory data) public payable {
assert(_BEACON_SLOT == bytes32(uint256(keccak256("eip1967.proxy.beacon")) - 1));
_setBeacon(beacon, data);
}

/**
* @dev Returns the current beacon address.
*/
function _beacon() internal view returns (address beacon) {
bytes32 slot = _BEACON_SLOT;
// solhint-disable-next-line no-inline-assembly
assembly {
beacon := sload(slot)
}
}

/**
* @dev Returns the current implementation address of the associated beacon.
*/
function _implementation() internal view override returns (address) {
return IBeacon(_beacon()).implementation();
}

/**
* @dev Changes the proxy to use a new beacon.
*
* If `data` is nonempty, it's used as data in a delegate call to the implementation returned by the beacon.
*
* Requirements:
*
* - `beacon` must be a contract.
* - The implementation returned by `beacon` must be a contract.
*/
function _setBeacon(address beacon, bytes memory data) internal {
require(
Address.isContract(beacon),
"BeaconProxy: beacon is not a contract"
);
require(
Address.isContract(IBeacon(beacon).implementation()),
"BeaconProxy: beacon implementation is not a contract"
);
bytes32 slot = _BEACON_SLOT;

// solhint-disable-next-line no-inline-assembly
assembly {
sstore(slot, beacon)
}

if (data.length > 0) {
Address.functionDelegateCall(_implementation(), data, "BeaconProxy: function call failed");
}
}
}
15 changes: 15 additions & 0 deletions contracts/proxy/IBeacon.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// SPDX-License-Identifier: MIT

pragma solidity >=0.6.0 <0.8.0;

/**
* @dev This is the interface that {BeaconProxy} expects of its beacon.
*/
interface IBeacon {
/**
* @dev Must return an address that can be used as a delegate call target.
*
* {BeaconProxy} will check that this address is a contract.
*/
function implementation() external view returns (address);
}
10 changes: 10 additions & 0 deletions contracts/proxy/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ The abstract {Proxy} contract implements the core delegation functionality. If t

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.

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.

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.

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

{{TransparentUpgradeableProxy}}

== UpgradeableBeacon

{{BeaconProxy}}

{{IBeacon}}

{{UpgradeableBeacon}}

== Utilities

{{Initializable}}
Expand Down
64 changes: 64 additions & 0 deletions contracts/proxy/UpgradeableBeacon.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// SPDX-License-Identifier: MIT

pragma solidity >=0.6.0 <0.8.0;

import "./IBeacon.sol";
import "../access/Ownable.sol";
import "../utils/Address.sol";

/**
* @dev This contract is used in conjunction with one or more instances of {BeaconProxy} to determine their
* implementation contract, which is where they will delegate all function calls.
*
* An owner is able to change the implementation the beacon points to, thus upgrading the proxies that use this beacon.
*/
contract UpgradeableBeacon is IBeacon, Ownable {
address private _implementation;

/**
* @dev Emitted when the implementation returned by the beacon is changed.
*/
event Upgraded(address indexed implementation);

/**
* @dev Sets the address of the initial implementation, and the deployer account as the owner who can upgrade the
* beacon.
*/
constructor(address implementation_) public {
_setImplementation(implementation_);
}

/**
* @dev Returns the current implementation address.
*/
function implementation() public view override returns (address) {
return _implementation;
}

/**
* @dev Upgrades the beacon to a new implementation.
*
* Emits an {Upgraded} event.
*
* Requirements:
*
* - msg.sender must be the owner of the contract.
* - `newImplementation` must be a contract.
*/
function upgradeTo(address newImplementation) public onlyOwner {
_setImplementation(newImplementation);
emit Upgraded(newImplementation);
}

/**
* @dev Sets the implementation contract address for this beacon
*
* Requirements:
*
* - `newImplementation` must be a contract.
*/
function _setImplementation(address newImplementation) private {
require(Address.isContract(newImplementation), "UpgradeableBeacon: implementation is not a contract");
_implementation = newImplementation;
}
}
163 changes: 163 additions & 0 deletions test/proxy/BeaconProxy.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
const { BN, expectRevert } = require('@openzeppelin/test-helpers');
const ethereumjsUtil = require('ethereumjs-util');
const { keccak256 } = ethereumjsUtil;

const { expect } = require('chai');

const UpgradeableBeacon = artifacts.require('UpgradeableBeacon');
const BeaconProxy = artifacts.require('BeaconProxy');
const DummyImplementation = artifacts.require('DummyImplementation');
const DummyImplementationV2 = artifacts.require('DummyImplementationV2');
const BadBeaconNoImpl = artifacts.require('BadBeaconNoImpl');
const BadBeaconNotContract = artifacts.require('BadBeaconNotContract');

function toChecksumAddress (address) {
return ethereumjsUtil.toChecksumAddress('0x' + address.replace(/^0x/, '').padStart(40, '0'));
}

const BEACON_LABEL = 'eip1967.proxy.beacon';
const BEACON_SLOT = '0x' + new BN(keccak256(Buffer.from(BEACON_LABEL))).subn(1).toString(16);

contract('BeaconProxy', function (accounts) {
const [anotherAccount] = accounts;

describe('bad beacon is not accepted', async function () {
it('non-contract beacon', async function () {
await expectRevert(
BeaconProxy.new(anotherAccount, '0x'),
'BeaconProxy: beacon is not a contract',
);
});

it('non-compliant beacon', async function () {
const beacon = await BadBeaconNoImpl.new();
await expectRevert.unspecified(
BeaconProxy.new(beacon.address, '0x'),
);
});

it('non-contract implementation', async function () {
const beacon = await BadBeaconNotContract.new();
await expectRevert(
BeaconProxy.new(beacon.address, '0x'),
'BeaconProxy: beacon implementation is not a contract',
);
});
});

before('deploy implementation', async function () {
this.implementationV0 = await DummyImplementation.new();
this.implementationV1 = await DummyImplementationV2.new();
});

describe('initialization', function () {
before(function () {
this.assertInitialized = async ({ value, balance }) => {
const beaconAddress = toChecksumAddress(await web3.eth.getStorageAt(this.proxy.address, BEACON_SLOT));
expect(beaconAddress).to.equal(this.beacon.address);

const dummy = new DummyImplementation(this.proxy.address);
expect(await dummy.value()).to.bignumber.eq(value);

expect(await web3.eth.getBalance(this.proxy.address)).to.bignumber.eq(balance);
};
});

beforeEach('deploy beacon', async function () {
this.beacon = await UpgradeableBeacon.new(this.implementationV0.address);
});

it('no initialization', async function () {
const data = Buffer.from('');
const balance = '10';
this.proxy = await BeaconProxy.new(this.beacon.address, data, { value: balance });
await this.assertInitialized({ value: '0', balance });
});

it('non-payable initialization', async function () {
const value = '55';
const data = this.implementationV0.contract.methods
.initializeNonPayableWithValue(value)
.encodeABI();
this.proxy = await BeaconProxy.new(this.beacon.address, data);
await this.assertInitialized({ value, balance: '0' });
});

it('payable initialization', async function () {
const value = '55';
const data = this.implementationV0.contract.methods
.initializePayableWithValue(value)
.encodeABI();
const balance = '100';
this.proxy = await BeaconProxy.new(this.beacon.address, data, { value: balance });
await this.assertInitialized({ value, balance });
});

it('reverting initialization', async function () {
const data = this.implementationV0.contract.methods.reverts().encodeABI();
await expectRevert(
BeaconProxy.new(this.beacon.address, data),
'DummyImplementation reverted',
);
});
});

it('upgrade a proxy by upgrading its beacon', async function () {
const beacon = await UpgradeableBeacon.new(this.implementationV0.address);

const value = '10';
const data = this.implementationV0.contract.methods
.initializeNonPayableWithValue(value)
.encodeABI();
const proxy = await BeaconProxy.new(beacon.address, data);

const dummy = new DummyImplementation(proxy.address);

// test initial values
expect(await dummy.value()).to.bignumber.eq(value);

// test initial version
expect(await dummy.version()).to.eq('V1');

// upgrade beacon
await beacon.upgradeTo(this.implementationV1.address);

// test upgraded version
expect(await dummy.version()).to.eq('V2');
});

it('upgrade 2 proxies by upgrading shared beacon', async function () {
const value1 = '10';
const value2 = '42';

const beacon = await UpgradeableBeacon.new(this.implementationV0.address);

const proxy1InitializeData = this.implementationV0.contract.methods
.initializeNonPayableWithValue(value1)
.encodeABI();
const proxy1 = await BeaconProxy.new(beacon.address, proxy1InitializeData);

const proxy2InitializeData = this.implementationV0.contract.methods
.initializeNonPayableWithValue(value2)
.encodeABI();
const proxy2 = await BeaconProxy.new(beacon.address, proxy2InitializeData);

const dummy1 = new DummyImplementation(proxy1.address);
const dummy2 = new DummyImplementation(proxy2.address);

// test initial values
expect(await dummy1.value()).to.bignumber.eq(value1);
expect(await dummy2.value()).to.bignumber.eq(value2);

// test initial version
expect(await dummy1.version()).to.eq('V1');
expect(await dummy2.version()).to.eq('V1');

// upgrade beacon
await beacon.upgradeTo(this.implementationV1.address);

// test upgraded version
expect(await dummy1.version()).to.eq('V2');
expect(await dummy2.version()).to.eq('V2');
});
});
Loading