diff --git a/contracts/mocks/ClashingImplementation.sol b/contracts/mocks/ClashingImplementation.sol new file mode 100644 index 00000000000..b75621351f8 --- /dev/null +++ b/contracts/mocks/ClashingImplementation.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + + +/** + * @dev Implementation contract with an admin() function made to clash with + * @dev TransparentUpgradeableProxy's to test correct functioning of the + * @dev Transparent Proxy feature. + */ +contract ClashingImplementation { + + function admin() external pure returns (address) { + return 0x0000000000000000000000000000000011111142; + } + + function delegatedFunction() external pure returns (bool) { + return true; + } +} diff --git a/contracts/mocks/DummyImplementation.sol b/contracts/mocks/DummyImplementation.sol new file mode 100644 index 00000000000..e186920345e --- /dev/null +++ b/contracts/mocks/DummyImplementation.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +abstract contract Impl { + function version() public pure virtual returns (string memory); +} + +contract DummyImplementation { + uint256 public value; + string public text; + uint256[] public values; + + function initializeNonPayable() public { + value = 10; + } + + function initializePayable() payable public { + value = 100; + } + + function initializeNonPayable(uint256 _value) public { + value = _value; + } + + function initializePayable(uint256 _value) payable public { + value = _value; + } + + function initialize(uint256 _value, string memory _text, uint256[] memory _values) public { + value = _value; + text = _text; + values = _values; + } + + function get() public pure returns (bool) { + return true; + } + + function version() public pure virtual returns (string memory) { + return "V1"; + } + + function reverts() public pure { + require(false); + } +} + +contract DummyImplementationV2 is DummyImplementation { + function migrate(uint256 newVal) payable public { + value = newVal; + } + + function version() public pure override returns (string memory) { + return "V2"; + } +} diff --git a/contracts/mocks/InitializableMock.sol b/contracts/mocks/InitializableMock.sol new file mode 100644 index 00000000000..a3f53687ccb --- /dev/null +++ b/contracts/mocks/InitializableMock.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +import "../proxy/Initializable.sol"; + +/** + * @title InitializableMock + * @dev This contract is a mock to test initializable functionality + */ +contract InitializableMock is Initializable { + + bool public initializerRan; + uint256 public x; + + function initialize() public initializer { + initializerRan = true; + } + + function initializeNested() public initializer { + initialize(); + } + + function initializeWithX(uint256 _x) public payable initializer { + x = _x; + } + + function nonInitializable(uint256 _x) public payable { + x = _x; + } + + function fail() public pure { + require(false, "InitializableMock forced failure"); + } + +} diff --git a/contracts/mocks/MultipleInheritanceInitializableMocks.sol b/contracts/mocks/MultipleInheritanceInitializableMocks.sol new file mode 100644 index 00000000000..e7e82c57fa4 --- /dev/null +++ b/contracts/mocks/MultipleInheritanceInitializableMocks.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +import "../proxy/Initializable.sol"; + +// Sample contracts showing upgradeability with multiple inheritance. +// Child contract inherits from Father and Mother contracts, and Father extends from Gramps. +// +// Human +// / \ +// | Gramps +// | | +// Mother Father +// | | +// -- Child -- + +/** + * Sample base intializable contract that is a human + */ +contract SampleHuman is Initializable { + bool public isHuman; + + function initialize() public initializer { + isHuman = true; + } +} + +/** + * Sample base intializable contract that defines a field mother + */ +contract SampleMother is Initializable, SampleHuman { + uint256 public mother; + + function initialize(uint256 value) public initializer virtual { + SampleHuman.initialize(); + mother = value; + } +} + +/** + * Sample base intializable contract that defines a field gramps + */ +contract SampleGramps is Initializable, SampleHuman { + string public gramps; + + function initialize(string memory value) public initializer virtual { + SampleHuman.initialize(); + gramps = value; + } +} + +/** + * Sample base intializable contract that defines a field father and extends from gramps + */ +contract SampleFather is Initializable, SampleGramps { + uint256 public father; + + function initialize(string memory _gramps, uint256 _father) public initializer { + SampleGramps.initialize(_gramps); + father = _father; + } +} + +/** + * Child extends from mother, father (gramps) + */ +contract SampleChild is Initializable, SampleMother, SampleFather { + uint256 public child; + + function initialize(uint256 _mother, string memory _gramps, uint256 _father, uint256 _child) public initializer { + SampleMother.initialize(_mother); + SampleFather.initialize(_gramps, _father); + child = _child; + } +} diff --git a/contracts/mocks/RegressionImplementation.sol b/contracts/mocks/RegressionImplementation.sol new file mode 100644 index 00000000000..1db26ac969a --- /dev/null +++ b/contracts/mocks/RegressionImplementation.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +import "../proxy/Initializable.sol"; + +contract Implementation1 is Initializable { + uint internal _value; + + function initialize() public initializer { + } + + function setValue(uint _number) public { + _value = _number; + } +} + +contract Implementation2 is Initializable { + uint internal _value; + + function initialize() public initializer { + } + + function setValue(uint _number) public { + _value = _number; + } + + function getValue() public view returns (uint) { + return _value; + } +} + +contract Implementation3 is Initializable { + uint internal _value; + + function initialize() public initializer { + } + + function setValue(uint _number) public { + _value = _number; + } + + function getValue(uint _number) public view returns (uint) { + return _value + _number; + } +} + +contract Implementation4 is Initializable { + uint internal _value; + + function initialize() public initializer { + } + + function setValue(uint _number) public { + _value = _number; + } + + function getValue() public view returns (uint) { + return _value; + } + + // solhint-disable-next-line payable-fallback + fallback() external { + _value = 1; + } +} diff --git a/contracts/mocks/SingleInheritanceInitializableMocks.sol b/contracts/mocks/SingleInheritanceInitializableMocks.sol new file mode 100644 index 00000000000..a9fcbce2de7 --- /dev/null +++ b/contracts/mocks/SingleInheritanceInitializableMocks.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +import "../proxy/Initializable.sol"; + +/** + * @title MigratableMockV1 + * @dev This contract is a mock to test initializable functionality through migrations + */ +contract MigratableMockV1 is Initializable { + uint256 public x; + + function initialize(uint256 value) public payable initializer { + x = value; + } +} + +/** + * @title MigratableMockV2 + * @dev This contract is a mock to test migratable functionality with params + */ +contract MigratableMockV2 is MigratableMockV1 { + bool internal _migratedV2; + uint256 public y; + + function migrate(uint256 value, uint256 anotherValue) public payable { + require(!_migratedV2); + x = value; + y = anotherValue; + _migratedV2 = true; + } +} + +/** + * @title MigratableMockV3 + * @dev This contract is a mock to test migratable functionality without params + */ +contract MigratableMockV3 is MigratableMockV2 { + bool internal _migratedV3; + + function migrate() public payable { + require(!_migratedV3); + uint256 oldX = x; + x = y; + y = oldX; + _migratedV3 = true; + } +} diff --git a/contracts/proxy/Initializable.sol b/contracts/proxy/Initializable.sol new file mode 100644 index 00000000000..449a625406e --- /dev/null +++ b/contracts/proxy/Initializable.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.4.24 <0.7.0; + + +/** + * @title Initializable + * + * @dev Helper contract to support initializer functions. To use it, replace + * the constructor with a function that has the `initializer` modifier. + * WARNING: Unlike constructors, initializer functions must be manually + * invoked. This applies both to deploying an Initializable contract, as well + * as extending an Initializable contract via inheritance. + * WARNING: When used with inheritance, manual care must be taken to not invoke + * a parent initializer twice, or ensure that all initializers are idempotent, + * because this is not dealt with automatically as with constructors. + */ +contract Initializable { + + /** + * @dev Indicates that the contract has been initialized. + */ + bool private _initialized; + + /** + * @dev Indicates that the contract is in the process of being initialized. + */ + bool private _initializing; + + /** + * @dev Modifier to use in the initializer function of a contract. + */ + modifier initializer() { + require(_initializing || _isConstructor() || !_initialized, "Initializable: contract is already initialized"); + + bool isTopLevelCall = !_initializing; + if (isTopLevelCall) { + _initializing = true; + _initialized = true; + } + + _; + + if (isTopLevelCall) { + _initializing = false; + } + } + + /// @dev Returns true if and only if the function is running in the constructor + function _isConstructor() private view returns (bool) { + // extcodesize checks the size of the code stored in an address, and + // address returns the current address. Since the code is still not + // deployed when running a constructor, any checks on its code size will + // yield zero, making it an effective way to detect if a contract is + // under construction or not. + address self = address(this); + uint256 cs; + // solhint-disable-next-line no-inline-assembly + assembly { cs := extcodesize(self) } + return cs == 0; + } +} diff --git a/contracts/proxy/Proxy.sol b/contracts/proxy/Proxy.sol new file mode 100644 index 00000000000..e68622f6d8a --- /dev/null +++ b/contracts/proxy/Proxy.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +/** + * @title Proxy + * @dev Implements delegation of calls to other contracts, with proper + * forwarding of return values and bubbling of failures. + * It defines a fallback function that delegates all calls to the address + * returned by the abstract _implementation() internal function. + */ +abstract contract Proxy { + /** + * @dev Fallback function. + * Implemented entirely in `_fallback`. + */ + fallback () payable external { + _fallback(); + } + + /** + * @dev Receive function. + * Implemented entirely in `_fallback`. + */ + receive () payable external { + _fallback(); + } + + /** + * @return The Address of the implementation. + */ + function _implementation() internal virtual view returns (address); + + /** + * @dev Delegates execution to an implementation contract. + * This is a low level function that doesn't return to its internal call site. + * It will return to the external caller whatever the implementation returns. + * @param implementation Address to delegate. + */ + function _delegate(address implementation) internal { + // solhint-disable-next-line no-inline-assembly + assembly { + // Copy msg.data. We take full control of memory in this inline assembly + // block because it will not return to Solidity code. We overwrite the + // Solidity scratch pad at memory position 0. + calldatacopy(0, 0, calldatasize()) + + // Call the implementation. + // out and outsize are 0 because we don't know the size yet. + let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0) + + // Copy the returned data. + returndatacopy(0, 0, returndatasize()) + + switch result + // delegatecall returns 0 on error. + case 0 { revert(0, returndatasize()) } + default { return(0, returndatasize()) } + } + } + + /** + * @dev Function that is run as the first thing in the fallback function. + * Can be redefined in derived contracts to add functionality. + * Redefinitions must call super._willFallback(). + */ + function _willFallback() internal virtual { + } + + /** + * @dev fallback implementation. + * Extracted to enable manual triggering. + */ + function _fallback() internal { + _willFallback(); + _delegate(_implementation()); + } +} diff --git a/contracts/proxy/ProxyAdmin.sol b/contracts/proxy/ProxyAdmin.sol new file mode 100644 index 00000000000..35ce8298e50 --- /dev/null +++ b/contracts/proxy/ProxyAdmin.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +import "../access/Ownable.sol"; +import "./TransparentUpgradeableProxy.sol"; + +/** + * @title ProxyAdmin + * @dev This contract is the admin of a proxy, and is in charge + * of upgrading it as well as transferring it to another admin. + */ +contract ProxyAdmin is Ownable { + + /** + * @dev Returns the current implementation of a proxy. + * This is needed because only the proxy admin can query it. + * @return The address of the current implementation of the proxy. + */ + function getProxyImplementation(TransparentUpgradeableProxy proxy) public view returns (address) { + // We need to manually run the static call since the getter cannot be flagged as view + // bytes4(keccak256("implementation()")) == 0x5c60da1b + (bool success, bytes memory returndata) = address(proxy).staticcall(hex"5c60da1b"); + require(success); + return abi.decode(returndata, (address)); + } + + /** + * @dev Returns the admin of a proxy. Only the admin can query it. + * @return The address of the current admin of the proxy. + */ + function getProxyAdmin(TransparentUpgradeableProxy proxy) public view returns (address) { + // We need to manually run the static call since the getter cannot be flagged as view + // bytes4(keccak256("admin()")) == 0xf851a440 + (bool success, bytes memory returndata) = address(proxy).staticcall(hex"f851a440"); + require(success); + return abi.decode(returndata, (address)); + } + + /** + * @dev Changes the admin of a proxy. + * @param proxy Proxy to change admin. + * @param newAdmin Address to transfer proxy administration to. + */ + function changeProxyAdmin(TransparentUpgradeableProxy proxy, address newAdmin) public onlyOwner { + proxy.changeAdmin(newAdmin); + } + + /** + * @dev Upgrades a proxy to the newest implementation of a contract. + * @param proxy Proxy to be upgraded. + * @param implementation the address of the Implementation. + */ + function upgrade(TransparentUpgradeableProxy proxy, address implementation) public onlyOwner { + proxy.upgradeTo(implementation); + } + + /** + * @dev Upgrades a proxy to the newest implementation of a contract and forwards a function call to it. + * This is useful to initialize the proxied contract. + * @param proxy Proxy to be upgraded. + * @param implementation Address of the Implementation. + * @param data Data to send as msg.data in the low level call. + * It should include the signature and the parameters of the function to be called, as described in + * https://solidity.readthedocs.io/en/v0.4.24/abi-spec.html#function-selector-and-argument-encoding. + */ + function upgradeAndCall(TransparentUpgradeableProxy proxy, address implementation, bytes memory data) public payable onlyOwner { + proxy.upgradeToAndCall{value: msg.value}(implementation, data); + } +} diff --git a/contracts/proxy/TransparentUpgradeableProxy.sol b/contracts/proxy/TransparentUpgradeableProxy.sol new file mode 100644 index 00000000000..fbd431f697e --- /dev/null +++ b/contracts/proxy/TransparentUpgradeableProxy.sol @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +import "./UpgradeableProxy.sol"; + +/** + * @title TransparentUpgradeableProxy + * @dev This contract combines an upgradeability proxy with an authorization + * mechanism for administrative tasks. + * All external functions in this contract must be guarded by the + * `ifAdmin` modifier. See ethereum/solidity#3864 for a Solidity + * feature proposal that would enable this to be done automatically. + */ +contract TransparentUpgradeableProxy is UpgradeableProxy { + /** + * Contract constructor. + * @param _logic address of the initial implementation. + * @param _admin Address of the proxy administrator. + * @param _data Data to send as msg.data to the implementation to initialize the proxied contract. + * It should include the signature and the parameters of the function to be called, as described in + * https://solidity.readthedocs.io/en/v0.4.24/abi-spec.html#function-selector-and-argument-encoding. + * This parameter is optional, if no data is given the initialization call to proxied contract will be skipped. + */ + constructor(address _logic, address _admin, bytes memory _data) public payable UpgradeableProxy(_logic, _data) { + assert(_ADMIN_SLOT == bytes32(uint256(keccak256("eip1967.proxy.admin")) - 1)); + _setAdmin(_admin); + } + + /** + * @dev Emitted when the administration has been transferred. + * @param previousAdmin Address of the previous admin. + * @param newAdmin Address of the new admin. + */ + event AdminChanged(address previousAdmin, address newAdmin); + + /** + * @dev Storage slot with the admin of the contract. + * This is the keccak-256 hash of "eip1967.proxy.admin" subtracted by 1, and is + * validated in the constructor. + */ + + bytes32 private constant _ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; + + /** + * @dev Modifier to check whether the `msg.sender` is the admin. + * If it is, it will run the function. Otherwise, it will delegate the call + * to the implementation. + */ + modifier ifAdmin() { + if (msg.sender == _admin()) { + _; + } else { + _fallback(); + } + } + + /** + * @return The address of the proxy admin. + */ + function admin() external ifAdmin returns (address) { + return _admin(); + } + + /** + * @return The address of the implementation. + */ + function implementation() external ifAdmin returns (address) { + return _implementation(); + } + + /** + * @dev Changes the admin of the proxy. + * Only the current admin can call this function. + * @param newAdmin Address to transfer proxy administration to. + */ + function changeAdmin(address newAdmin) external ifAdmin { + require(newAdmin != address(0), "TransparentUpgradeableProxy: new admin is the zero address"); + emit AdminChanged(_admin(), newAdmin); + _setAdmin(newAdmin); + } + + /** + * @dev Upgrade the backing implementation of the proxy. + * Only the admin can call this function. + * @param newImplementation Address of the new implementation. + */ + function upgradeTo(address newImplementation) external ifAdmin { + _upgradeTo(newImplementation); + } + + /** + * @dev Upgrade the backing implementation of the proxy and call a function + * on the new implementation. + * This is useful to initialize the proxied contract. + * @param newImplementation Address of the new implementation. + * @param data Data to send as msg.data in the low level call. + * It should include the signature and the parameters of the function to be called, as described in + * https://solidity.readthedocs.io/en/v0.4.24/abi-spec.html#function-selector-and-argument-encoding. + */ + function upgradeToAndCall(address newImplementation, bytes calldata data) external payable ifAdmin { + _upgradeTo(newImplementation); + // solhint-disable-next-line avoid-low-level-calls + (bool success,) = newImplementation.delegatecall(data); + require(success); + } + + /** + * @return adm The admin slot. + */ + function _admin() internal view returns (address adm) { + bytes32 slot = _ADMIN_SLOT; + // solhint-disable-next-line no-inline-assembly + assembly { + adm := sload(slot) + } + } + + /** + * @dev Sets the address of the proxy admin. + * @param newAdmin Address of the new proxy admin. + */ + function _setAdmin(address newAdmin) internal { + bytes32 slot = _ADMIN_SLOT; + + // solhint-disable-next-line no-inline-assembly + assembly { + sstore(slot, newAdmin) + } + } + + /** + * @dev Only fallback when the sender is not the admin. + */ + function _willFallback() internal override virtual { + require(msg.sender != _admin(), "TransparentUpgradeableProxy: admin cannot fallback to proxy target"); + super._willFallback(); + } +} diff --git a/contracts/proxy/UpgradeableProxy.sol b/contracts/proxy/UpgradeableProxy.sol new file mode 100644 index 00000000000..8b120c069f7 --- /dev/null +++ b/contracts/proxy/UpgradeableProxy.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +import "./Proxy.sol"; +import "../utils/Address.sol"; + +/** + * @title UpgradeableProxy + * @dev This contract implements a proxy that allows to change the + * implementation address to which it will delegate. + * Such a change is called an implementation upgrade. + */ +contract UpgradeableProxy is Proxy { + /** + * @dev Contract constructor. + * @param _logic Address of the initial implementation. + * @param _data Data to send as msg.data to the implementation to initialize the proxied contract. + * It should include the signature and the parameters of the function to be called, as described in + * https://solidity.readthedocs.io/en/v0.4.24/abi-spec.html#function-selector-and-argument-encoding. + * This parameter is optional, if no data is given the initialization call to proxied contract will be skipped. + */ + constructor(address _logic, bytes memory _data) public payable { + assert(_IMPLEMENTATION_SLOT == bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1)); + _setImplementation(_logic); + if(_data.length > 0) { + // solhint-disable-next-line avoid-low-level-calls + (bool success,) = _logic.delegatecall(_data); + require(success); + } + } + + /** + * @dev Emitted when the implementation is upgraded. + * @param implementation Address of the new implementation. + */ + event Upgraded(address indexed implementation); + + /** + * @dev Storage slot with the address of the current implementation. + * This is the keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1, and is + * validated in the constructor. + */ + bytes32 private constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + + /** + * @dev Returns the current implementation. + * @return impl Address of the current implementation + */ + function _implementation() internal override view returns (address impl) { + bytes32 slot = _IMPLEMENTATION_SLOT; + // solhint-disable-next-line no-inline-assembly + assembly { + impl := sload(slot) + } + } + + /** + * @dev Upgrades the proxy to a new implementation. + * @param newImplementation Address of the new implementation. + */ + function _upgradeTo(address newImplementation) internal { + _setImplementation(newImplementation); + emit Upgraded(newImplementation); + } + + /** + * @dev Sets the implementation address of the proxy. + * @param newImplementation Address of the new implementation. + */ + function _setImplementation(address newImplementation) internal { + require(Address.isContract(newImplementation), "UpgradeableProxy: new implementation is not a contract"); + + bytes32 slot = _IMPLEMENTATION_SLOT; + + // solhint-disable-next-line no-inline-assembly + assembly { + sstore(slot, newImplementation) + } + } +} diff --git a/test/proxy/Initializable.test.js b/test/proxy/Initializable.test.js new file mode 100644 index 00000000000..c20356c4dc0 --- /dev/null +++ b/test/proxy/Initializable.test.js @@ -0,0 +1,81 @@ +const { contract } = require('@openzeppelin/test-environment'); + +const { expectRevert } = require('@openzeppelin/test-helpers'); + +const { assert } = require('chai'); + +const InitializableMock = contract.fromArtifact('InitializableMock'); +const SampleChild = contract.fromArtifact('SampleChild'); + +describe('Initializable', function () { + describe('basic testing without inheritance', function () { + beforeEach('deploying', async function () { + this.contract = await InitializableMock.new(); + }); + + context('before initialize', function () { + it('initializer has not run', async function () { + assert.isFalse(await this.contract.initializerRan()); + }); + }); + + context('after initialize', function () { + beforeEach('initializing', async function () { + await this.contract.initialize(); + }); + + it('initializer has run', async function () { + assert.isTrue(await this.contract.initializerRan()); + }); + + it('initializer does not run again', async function () { + await expectRevert(this.contract.initialize(), 'Initializable: contract is already initialized'); + }); + }); + + context('after nested initialize', function () { + beforeEach('initializing', async function () { + await this.contract.initializeNested(); + }); + + it('initializer has run', async function () { + assert.isTrue(await this.contract.initializerRan()); + }); + }); + }); + + describe('complex testing with inheritance', function () { + const mother = 12; + const gramps = '56'; + const father = 34; + const child = 78; + + beforeEach('deploying', async function () { + this.contract = await SampleChild.new(); + }); + + beforeEach('initializing', async function () { + await this.contract.initialize(mother, gramps, father, child); + }); + + it('initializes human', async function () { + assert.equal(await this.contract.isHuman(), true); + }); + + it('initializes mother', async function () { + assert.equal(await this.contract.mother(), mother); + }); + + it('initializes gramps', async function () { + assert.equal(await this.contract.gramps(), gramps); + }); + + it('initializes father', async function () { + assert.equal(await this.contract.father(), father); + }); + + it('initializes child', async function () { + assert.equal(await this.contract.child(), child); + }); + }); +}); diff --git a/test/proxy/ProxyAdmin.test.js b/test/proxy/ProxyAdmin.test.js new file mode 100644 index 00000000000..991c9f653b8 --- /dev/null +++ b/test/proxy/ProxyAdmin.test.js @@ -0,0 +1,119 @@ +const { accounts, contract } = require('@openzeppelin/test-environment'); + +const { expectRevert } = require('@openzeppelin/test-helpers'); + +const { expect } = require('chai'); + +const ImplV1 = contract.fromArtifact('DummyImplementation'); +const ImplV2 = contract.fromArtifact('DummyImplementationV2'); +const ProxyAdmin = contract.fromArtifact('ProxyAdmin'); +const TransparentUpgradeableProxy = contract.fromArtifact('TransparentUpgradeableProxy'); + +describe('ProxyAdmin', function () { + const [proxyAdminOwner, newAdmin, anotherAccount] = accounts; + + before('set implementations', async function () { + this.implementationV1 = await ImplV1.new(); + this.implementationV2 = await ImplV2.new(); + }); + + beforeEach(async function () { + const initializeData = Buffer.from(''); + this.proxyAdmin = await ProxyAdmin.new({ from: proxyAdminOwner }); + this.proxy = await TransparentUpgradeableProxy.new( + this.implementationV1.address, + this.proxyAdmin.address, + initializeData, + { from: proxyAdminOwner }, + ); + }); + + it('has an owner', async function () { + expect(await this.proxyAdmin.owner()).to.equal(proxyAdminOwner); + }); + + describe('#getProxyAdmin', function () { + it('returns proxyAdmin as admin of the proxy', async function () { + const admin = await this.proxyAdmin.getProxyAdmin(this.proxy.address); + expect(admin).to.be.equal(this.proxyAdmin.address); + }); + }); + + describe('#changeProxyAdmin', function () { + it('fails to change proxy admin if its not the proxy owner', async function () { + await expectRevert( + this.proxyAdmin.changeProxyAdmin(this.proxy.address, newAdmin, { from: anotherAccount }), + 'caller is not the owner', + ); + }); + + it('changes proxy admin', async function () { + await this.proxyAdmin.changeProxyAdmin(this.proxy.address, newAdmin, { from: proxyAdminOwner }); + expect(await this.proxy.admin.call({ from: newAdmin })).to.eq(newAdmin); + }); + }); + + describe('#getProxyImplementation', function () { + it('returns proxy implementation address', async function () { + const implementationAddress = await this.proxyAdmin.getProxyImplementation(this.proxy.address); + expect(implementationAddress).to.be.equal(this.implementationV1.address); + }); + }); + + describe('#upgrade', function () { + context('with unauthorized account', function () { + it('fails to upgrade', async function () { + await expectRevert( + this.proxyAdmin.upgrade(this.proxy.address, this.implementationV2.address, { from: anotherAccount }), + 'caller is not the owner', + ); + }); + }); + + context('with authorized account', function () { + it('upgrades implementation', async function () { + await this.proxyAdmin.upgrade(this.proxy.address, this.implementationV2.address, { from: proxyAdminOwner }); + const implementationAddress = await this.proxyAdmin.getProxyImplementation(this.proxy.address); + expect(implementationAddress).to.be.equal(this.implementationV2.address); + }); + }); + }); + + describe('#upgradeAndCall', function () { + context('with unauthorized account', function () { + it('fails to upgrade', async function () { + const callData = new ImplV1('').contract.methods['initializeNonPayable(uint256)'](1337).encodeABI(); + await expectRevert( + this.proxyAdmin.upgradeAndCall(this.proxy.address, this.implementationV2.address, callData, + { from: anotherAccount } + ), + 'caller is not the owner', + ); + }); + }); + + context('with authorized account', function () { + context('with invalid callData', function () { + it('fails to upgrade', async function () { + const callData = '0x12345678'; + await expectRevert.unspecified( + this.proxyAdmin.upgradeAndCall(this.proxy.address, this.implementationV2.address, callData, + { from: proxyAdminOwner } + ), + ); + }); + }); + + context('with valid callData', function () { + it('upgrades implementation', async function () { + const callData = new ImplV1('').contract.methods['initializeNonPayable(uint256)'](1337).encodeABI(); + await this.proxyAdmin.upgradeAndCall(this.proxy.address, this.implementationV2.address, callData, + { from: proxyAdminOwner } + ); + const implementationAddress = await this.proxyAdmin.getProxyImplementation(this.proxy.address); + expect(implementationAddress).to.be.equal(this.implementationV2.address); + }); + }); + }); + }); +}); diff --git a/test/proxy/TransparentUpgradeableProxy.behaviour.js b/test/proxy/TransparentUpgradeableProxy.behaviour.js new file mode 100644 index 00000000000..0107680a23b --- /dev/null +++ b/test/proxy/TransparentUpgradeableProxy.behaviour.js @@ -0,0 +1,436 @@ +const { contract, web3 } = require('@openzeppelin/test-environment'); + +const { BN, expectRevert, expectEvent, constants } = require('@openzeppelin/test-helpers'); +const { ZERO_ADDRESS } = constants; +const { toChecksumAddress, keccak256 } = require('ethereumjs-util'); + +const { expect } = require('chai'); + +const Proxy = contract.fromArtifact('Proxy'); +const Implementation1 = contract.fromArtifact('Implementation1'); +const Implementation2 = contract.fromArtifact('Implementation2'); +const Implementation3 = contract.fromArtifact('Implementation3'); +const Implementation4 = contract.fromArtifact('Implementation4'); +const MigratableMockV1 = contract.fromArtifact('MigratableMockV1'); +const MigratableMockV2 = contract.fromArtifact('MigratableMockV2'); +const MigratableMockV3 = contract.fromArtifact('MigratableMockV3'); +const InitializableMock = contract.fromArtifact('InitializableMock'); +const DummyImplementation = contract.fromArtifact('DummyImplementation'); +const ClashingImplementation = contract.fromArtifact('ClashingImplementation'); + +const IMPLEMENTATION_LABEL = 'eip1967.proxy.implementation'; +const ADMIN_LABEL = 'eip1967.proxy.admin'; + +module.exports = function shouldBehaveLikeTransparentUpgradeableProxy (createProxy, accounts) { + const [proxyAdminAddress, proxyAdminOwner, anotherAccount] = accounts; + + before(async function () { + this.implementationV0 = (await DummyImplementation.new()).address; + this.implementationV1 = (await DummyImplementation.new()).address; + }); + + beforeEach(async function () { + const initializeData = Buffer.from(''); + this.proxy = await createProxy(this.implementationV0, proxyAdminAddress, initializeData, { + from: proxyAdminOwner, + }); + this.proxyAddress = this.proxy.address; + }); + + describe('implementation', function () { + it('returns the current implementation address', async function () { + const implementation = await this.proxy.implementation.call({ from: proxyAdminAddress }); + + expect(implementation).to.be.equal(this.implementationV0); + }); + + it('delegates to the implementation', async function () { + const dummy = new DummyImplementation(this.proxyAddress); + const value = await dummy.get(); + + expect(value).to.equal(true); + }); + }); + + describe('upgradeTo', function () { + describe('when the sender is the admin', function () { + const from = proxyAdminAddress; + + describe('when the given implementation is different from the current one', function () { + it('upgrades to the requested implementation', async function () { + await this.proxy.upgradeTo(this.implementationV1, { from }); + + const implementation = await this.proxy.implementation.call({ from: proxyAdminAddress }); + expect(implementation).to.be.equal(this.implementationV1); + }); + + it('emits an event', async function () { + expectEvent( + await this.proxy.upgradeTo(this.implementationV1, { from }), + 'Upgraded', { + implementation: this.implementationV1, + }, + ); + }); + }); + + describe('when the given implementation is the zero address', function () { + it('reverts', async function () { + await expectRevert( + this.proxy.upgradeTo(ZERO_ADDRESS, { from }), + 'UpgradeableProxy: new implementation is not a contract', + ); + }); + }); + }); + + describe('when the sender is not the admin', function () { + const from = anotherAccount; + + it('reverts', async function () { + await expectRevert.unspecified( + this.proxy.upgradeTo(this.implementationV1, { from }) + ); + }); + }); + }); + + describe('upgradeToAndCall', function () { + describe('without migrations', function () { + beforeEach(async function () { + this.behavior = await InitializableMock.new(); + }); + + describe('when the call does not fail', function () { + const initializeData = new InitializableMock('').contract.methods['initializeWithX(uint256)'](42).encodeABI(); + + describe('when the sender is the admin', function () { + const from = proxyAdminAddress; + const value = 1e5; + + beforeEach(async function () { + this.receipt = await this.proxy.upgradeToAndCall(this.behavior.address, initializeData, { from, value }); + }); + + it('upgrades to the requested implementation', async function () { + const implementation = await this.proxy.implementation.call({ from: proxyAdminAddress }); + expect(implementation).to.be.equal(this.behavior.address); + }); + + it('emits an event', function () { + expectEvent(this.receipt, 'Upgraded', { implementation: this.behavior.address }); + }); + + it('calls the initializer function', async function () { + const migratable = new InitializableMock(this.proxyAddress); + const x = await migratable.x(); + expect(x).to.be.bignumber.equal('42'); + }); + + it('sends given value to the proxy', async function () { + const balance = await web3.eth.getBalance(this.proxyAddress); + expect(balance.toString()).to.be.bignumber.equal(value.toString()); + }); + + it.skip('uses the storage of the proxy', async function () { + // storage layout should look as follows: + // - 0: Initializable storage + // - 1-50: Initailizable reserved storage (50 slots) + // - 51: initializerRan + // - 52: x + const storedValue = await Proxy.at(this.proxyAddress).getStorageAt(52); + expect(parseInt(storedValue)).to.eq(42); + }); + }); + + describe('when the sender is not the admin', function () { + it('reverts', async function () { + await expectRevert.unspecified( + this.proxy.upgradeToAndCall(this.behavior.address, initializeData, { from: anotherAccount }), + ); + }); + }); + }); + + describe('when the call does fail', function () { + const initializeData = new InitializableMock('').contract.methods.fail().encodeABI(); + + it('reverts', async function () { + await expectRevert.unspecified( + this.proxy.upgradeToAndCall(this.behavior.address, initializeData, { from: proxyAdminAddress }), + ); + }); + }); + }); + + describe('with migrations', function () { + describe('when the sender is the admin', function () { + const from = proxyAdminAddress; + const value = 1e5; + + describe('when upgrading to V1', function () { + const v1MigrationData = new MigratableMockV1('').contract.methods.initialize(42).encodeABI(); + + beforeEach(async function () { + this.behaviorV1 = await MigratableMockV1.new(); + this.balancePreviousV1 = new BN(await web3.eth.getBalance(this.proxyAddress)); + this.receipt = await this.proxy.upgradeToAndCall(this.behaviorV1.address, v1MigrationData, { from, value }); + }); + + it('upgrades to the requested version and emits an event', async function () { + const implementation = await this.proxy.implementation.call({ from: proxyAdminAddress }); + expect(implementation).to.be.equal(this.behaviorV1.address); + expectEvent(this.receipt, 'Upgraded', { implementation: this.behaviorV1.address }); + }); + + it('calls the \'initialize\' function and sends given value to the proxy', async function () { + const migratable = new MigratableMockV1(this.proxyAddress); + + const x = await migratable.x(); + expect(x).to.be.bignumber.equal('42'); + + const balance = await web3.eth.getBalance(this.proxyAddress); + expect(new BN(balance)).to.be.bignumber.equal(this.balancePreviousV1.addn(value)); + }); + + describe('when upgrading to V2', function () { + const v2MigrationData = new MigratableMockV2('').contract.methods.migrate(10, 42).encodeABI(); + + beforeEach(async function () { + this.behaviorV2 = await MigratableMockV2.new(); + this.balancePreviousV2 = new BN(await web3.eth.getBalance(this.proxyAddress)); + this.receipt = + await this.proxy.upgradeToAndCall(this.behaviorV2.address, v2MigrationData, { from, value }); + }); + + it('upgrades to the requested version and emits an event', async function () { + const implementation = await this.proxy.implementation.call({ from: proxyAdminAddress }); + expect(implementation).to.be.equal(this.behaviorV2.address); + expectEvent(this.receipt, 'Upgraded', { implementation: this.behaviorV2.address }); + }); + + it('calls the \'migrate\' function and sends given value to the proxy', async function () { + const migratable = new MigratableMockV2(this.proxyAddress); + + const x = await migratable.x(); + expect(x).to.be.bignumber.equal('10'); + + const y = await migratable.y(); + expect(y).to.be.bignumber.equal('42'); + + const balance = new BN(await web3.eth.getBalance(this.proxyAddress)); + expect(balance).to.be.bignumber.equal(this.balancePreviousV2.addn(value)); + }); + + describe('when upgrading to V3', function () { + const v3MigrationData = new MigratableMockV3('').contract.methods['migrate()']().encodeABI(); + + beforeEach(async function () { + this.behaviorV3 = await MigratableMockV3.new(); + this.balancePreviousV3 = new BN(await web3.eth.getBalance(this.proxyAddress)); + this.receipt = + await this.proxy.upgradeToAndCall(this.behaviorV3.address, v3MigrationData, { from, value }); + }); + + it('upgrades to the requested version and emits an event', async function () { + const implementation = await this.proxy.implementation.call({ from: proxyAdminAddress }); + expect(implementation).to.be.equal(this.behaviorV3.address); + expectEvent(this.receipt, 'Upgraded', { implementation: this.behaviorV3.address }); + }); + + it('calls the \'migrate\' function and sends given value to the proxy', async function () { + const migratable = new MigratableMockV3(this.proxyAddress); + + const x = await migratable.x(); + expect(x).to.be.bignumber.equal('42'); + + const y = await migratable.y(); + expect(y).to.be.bignumber.equal('10'); + + const balance = new BN(await web3.eth.getBalance(this.proxyAddress)); + expect(balance).to.be.bignumber.equal(this.balancePreviousV3.addn(value)); + }); + }); + }); + }); + }); + + describe('when the sender is not the admin', function () { + const from = anotherAccount; + + it('reverts', async function () { + const behaviorV1 = await MigratableMockV1.new(); + const v1MigrationData = new MigratableMockV1('').contract.methods.initialize(42).encodeABI(); + await expectRevert.unspecified( + this.proxy.upgradeToAndCall(behaviorV1.address, v1MigrationData, { from }), + ); + }); + }); + }); + }); + + describe('changeAdmin', function () { + describe('when the new proposed admin is not the zero address', function () { + const newAdmin = anotherAccount; + + describe('when the sender is the admin', function () { + beforeEach('transferring', async function () { + this.receipt = await this.proxy.changeAdmin(newAdmin, { from: proxyAdminAddress }); + }); + + it('assigns new proxy admin', async function () { + const newProxyAdmin = await this.proxy.admin.call({ from: newAdmin }); + expect(newProxyAdmin).to.be.equal(anotherAccount); + }); + + it('emits an event', function () { + expectEvent(this.receipt, 'AdminChanged', { + previousAdmin: proxyAdminAddress, + newAdmin: newAdmin, + }); + }); + }); + + describe('when the sender is not the admin', function () { + it('reverts', async function () { + await expectRevert.unspecified(this.proxy.changeAdmin(newAdmin, { from: anotherAccount })); + }); + }); + }); + + describe('when the new proposed admin is the zero address', function () { + it('reverts', async function () { + await expectRevert( + this.proxy.changeAdmin(ZERO_ADDRESS, { from: proxyAdminAddress }), + 'TransparentUpgradeableProxy: new admin is the zero address', + ); + }); + }); + }); + + describe('storage', function () { + it('should store the implementation address in specified location', async function () { + const slot = '0x' + new BN(keccak256(Buffer.from(IMPLEMENTATION_LABEL))).subn(1).toString(16); + const implementation = toChecksumAddress(await web3.eth.getStorageAt(this.proxyAddress, slot)); + expect(implementation).to.be.equal(this.implementationV0); + }); + + it('should store the admin proxy in specified location', async function () { + const slot = '0x' + new BN(keccak256(Buffer.from(ADMIN_LABEL))).subn(1).toString(16); + const proxyAdmin = toChecksumAddress(await web3.eth.getStorageAt(this.proxyAddress, slot)); + expect(proxyAdmin).to.be.equal(proxyAdminAddress); + }); + }); + + describe('transparent proxy', function () { + beforeEach('creating proxy', async function () { + const initializeData = Buffer.from(''); + this.impl = await ClashingImplementation.new(); + this.proxy = await createProxy(this.impl.address, proxyAdminAddress, initializeData, { from: proxyAdminOwner }); + + this.clashing = new ClashingImplementation(this.proxy.address); + }); + + it('proxy admin cannot call delegated functions', async function () { + await expectRevert( + this.clashing.delegatedFunction({ from: proxyAdminAddress }), + 'TransparentUpgradeableProxy: admin cannot fallback to proxy target', + ); + }); + + context('when function names clash', function () { + it('when sender is proxy admin should run the proxy function', async function () { + const value = await this.proxy.admin.call({ from: proxyAdminAddress }); + expect(value).to.be.equal(proxyAdminAddress); + }); + + it('when sender is other should delegate to implementation', async function () { + const value = await this.proxy.admin.call({ from: anotherAccount }); + expect(value).to.be.equal('0x0000000000000000000000000000000011111142'); + }); + }); + }); + + describe('regression', () => { + const initializeData = Buffer.from(''); + + it('should add new function', async () => { + const instance1 = await Implementation1.new(); + const proxy = await createProxy(instance1.address, proxyAdminAddress, initializeData, { from: proxyAdminOwner }); + + const proxyInstance1 = new Implementation1(proxy.address); + await proxyInstance1.setValue(42); + + const instance2 = await Implementation2.new(); + await proxy.upgradeTo(instance2.address, { from: proxyAdminAddress }); + + const proxyInstance2 = new Implementation2(proxy.address); + const res = await proxyInstance2.getValue(); + expect(res.toString()).to.eq('42'); + }); + + it('should remove function', async () => { + const instance2 = await Implementation2.new(); + const proxy = await createProxy(instance2.address, proxyAdminAddress, initializeData, { from: proxyAdminOwner }); + + const proxyInstance2 = new Implementation2(proxy.address); + await proxyInstance2.setValue(42); + const res = await proxyInstance2.getValue(); + expect(res.toString()).to.eq('42'); + + const instance1 = await Implementation1.new(); + await proxy.upgradeTo(instance1.address, { from: proxyAdminAddress }); + + const proxyInstance1 = new Implementation2(proxy.address); + await expectRevert.unspecified(proxyInstance1.getValue()); + }); + + it('should change function signature', async () => { + const instance1 = await Implementation1.new(); + const proxy = await createProxy(instance1.address, proxyAdminAddress, initializeData, { from: proxyAdminOwner }); + + const proxyInstance1 = new Implementation1(proxy.address); + await proxyInstance1.setValue(42); + + const instance3 = await Implementation3.new(); + await proxy.upgradeTo(instance3.address, { from: proxyAdminAddress }); + const proxyInstance3 = new Implementation3(proxy.address); + + const res = await proxyInstance3.getValue(8); + expect(res.toString()).to.eq('50'); + }); + + it('should add fallback function', async () => { + const initializeData = Buffer.from(''); + const instance1 = await Implementation1.new(); + const proxy = await createProxy(instance1.address, proxyAdminAddress, initializeData, { from: proxyAdminOwner }); + + const instance4 = await Implementation4.new(); + await proxy.upgradeTo(instance4.address, { from: proxyAdminAddress }); + const proxyInstance4 = new Implementation4(proxy.address); + + const data = ''; + await web3.eth.sendTransaction({ to: proxy.address, from: anotherAccount, data }); + + const res = await proxyInstance4.getValue(); + expect(res.toString()).to.eq('1'); + }); + + it('should remove fallback function', async () => { + const instance4 = await Implementation4.new(); + const proxy = await createProxy(instance4.address, proxyAdminAddress, initializeData, { from: proxyAdminOwner }); + + const instance2 = await Implementation2.new(); + await proxy.upgradeTo(instance2.address, { from: proxyAdminAddress }); + + const data = ''; + await expectRevert.unspecified( + web3.eth.sendTransaction({ to: proxy.address, from: anotherAccount, data }), + ); + + const proxyInstance2 = new Implementation2(proxy.address); + const res = await proxyInstance2.getValue(); + expect(res.toString()).to.eq('0'); + }); + }); +}; diff --git a/test/proxy/TransparentUpgradeableProxy.test.js b/test/proxy/TransparentUpgradeableProxy.test.js new file mode 100644 index 00000000000..ea7613bbc6f --- /dev/null +++ b/test/proxy/TransparentUpgradeableProxy.test.js @@ -0,0 +1,17 @@ +const { accounts, contract } = require('@openzeppelin/test-environment'); + +const shouldBehaveLikeUpgradeableProxy = require('./UpgradeableProxy.behaviour'); +const shouldBehaveLikeTransparentUpgradeableProxy = require('./TransparentUpgradeableProxy.behaviour'); + +const TransparentUpgradeableProxy = contract.fromArtifact('TransparentUpgradeableProxy'); + +describe('TransparentUpgradeableProxy', function () { + const [proxyAdminAddress, proxyAdminOwner] = accounts; + + const createProxy = async function (logic, admin, initData, opts) { + return TransparentUpgradeableProxy.new(logic, admin, initData, opts); + }; + + shouldBehaveLikeUpgradeableProxy(createProxy, proxyAdminAddress, proxyAdminOwner); + shouldBehaveLikeTransparentUpgradeableProxy(createProxy, accounts); +}); diff --git a/test/proxy/UpgradeableProxy.behaviour.js b/test/proxy/UpgradeableProxy.behaviour.js new file mode 100644 index 00000000000..72e5c258acf --- /dev/null +++ b/test/proxy/UpgradeableProxy.behaviour.js @@ -0,0 +1,216 @@ +const { contract, web3 } = require('@openzeppelin/test-environment'); + +const { BN, expectRevert } = require('@openzeppelin/test-helpers'); +const { toChecksumAddress, keccak256 } = require('ethereumjs-util'); + +const { expect } = require('chai'); + +const DummyImplementation = contract.fromArtifact('DummyImplementation'); + +const IMPLEMENTATION_LABEL = 'eip1967.proxy.implementation'; + +module.exports = function shouldBehaveLikeUpgradeableProxy (createProxy, proxyAdminAddress, proxyCreator) { + it('cannot be initialized with a non-contract address', async function () { + const nonContractAddress = proxyCreator; + const initializeData = Buffer.from(''); + await expectRevert.unspecified( + createProxy(nonContractAddress, proxyAdminAddress, initializeData, { + from: proxyCreator, + }), + ); + }); + + before('deploy implementation', async function () { + this.implementation = web3.utils.toChecksumAddress((await DummyImplementation.new()).address); + }); + + const assertProxyInitialization = function ({ value, balance }) { + it('sets the implementation address', async function () { + const slot = '0x' + new BN(keccak256(Buffer.from(IMPLEMENTATION_LABEL))).subn(1).toString(16); + const implementation = toChecksumAddress(await web3.eth.getStorageAt(this.proxy, slot)); + expect(implementation).to.be.equal(this.implementation); + }); + + it('initializes the proxy', async function () { + const dummy = new DummyImplementation(this.proxy); + expect(await dummy.value()).to.be.bignumber.equal(value.toString()); + }); + + it('has expected balance', async function () { + expect(await web3.eth.getBalance(this.proxy)).to.be.bignumber.equal(balance.toString()); + }); + }; + + describe('without initialization', function () { + const initializeData = Buffer.from(''); + + describe('when not sending balance', function () { + beforeEach('creating proxy', async function () { + this.proxy = ( + await createProxy(this.implementation, proxyAdminAddress, initializeData, { + from: proxyCreator, + }) + ).address; + }); + + assertProxyInitialization({ value: 0, balance: 0 }); + }); + + describe('when sending some balance', function () { + const value = 10e5; + + beforeEach('creating proxy', async function () { + this.proxy = ( + await createProxy(this.implementation, proxyAdminAddress, initializeData, { + from: proxyCreator, + value, + }) + ).address; + }); + + assertProxyInitialization({ value: 0, balance: value }); + }); + }); + + describe('initialization without parameters', function () { + describe('non payable', function () { + const expectedInitializedValue = 10; + const initializeData = new DummyImplementation('').contract.methods['initializeNonPayable()']().encodeABI(); + + describe('when not sending balance', function () { + beforeEach('creating proxy', async function () { + this.proxy = ( + await createProxy(this.implementation, proxyAdminAddress, initializeData, { + from: proxyCreator, + }) + ).address; + }); + + assertProxyInitialization({ + value: expectedInitializedValue, + balance: 0, + }); + }); + + describe('when sending some balance', function () { + const value = 10e5; + + it('reverts', async function () { + await expectRevert.unspecified( + createProxy(this.implementation, proxyAdminAddress, initializeData, { from: proxyCreator, value }), + ); + }); + }); + }); + + describe('payable', function () { + const expectedInitializedValue = 100; + const initializeData = new DummyImplementation('').contract.methods['initializePayable()']().encodeABI(); + + describe('when not sending balance', function () { + beforeEach('creating proxy', async function () { + this.proxy = ( + await createProxy(this.implementation, proxyAdminAddress, initializeData, { + from: proxyCreator, + }) + ).address; + }); + + assertProxyInitialization({ + value: expectedInitializedValue, + balance: 0, + }); + }); + + describe('when sending some balance', function () { + const value = 10e5; + + beforeEach('creating proxy', async function () { + this.proxy = ( + await createProxy(this.implementation, proxyAdminAddress, initializeData, { + from: proxyCreator, + value, + }) + ).address; + }); + + assertProxyInitialization({ + value: expectedInitializedValue, + balance: value, + }); + }); + }); + }); + + describe('initialization with parameters', function () { + describe('non payable', function () { + const expectedInitializedValue = 10; + const initializeData = new DummyImplementation('').contract + .methods['initializeNonPayable(uint256)'](expectedInitializedValue).encodeABI(); + + describe('when not sending balance', function () { + beforeEach('creating proxy', async function () { + this.proxy = ( + await createProxy(this.implementation, proxyAdminAddress, initializeData, { + from: proxyCreator, + }) + ).address; + }); + + assertProxyInitialization({ + value: expectedInitializedValue, + balance: 0, + }); + }); + + describe('when sending some balance', function () { + const value = 10e5; + + it('reverts', async function () { + await expectRevert.unspecified( + createProxy(this.implementation, proxyAdminAddress, initializeData, { from: proxyCreator, value }), + ); + }); + }); + }); + + describe('payable', function () { + const expectedInitializedValue = 42; + const initializeData = new DummyImplementation('').contract + .methods['initializePayable(uint256)'](expectedInitializedValue).encodeABI(); + + describe('when not sending balance', function () { + beforeEach('creating proxy', async function () { + this.proxy = ( + await createProxy(this.implementation, proxyAdminAddress, initializeData, { + from: proxyCreator, + }) + ).address; + }); + + assertProxyInitialization({ + value: expectedInitializedValue, + balance: 0, + }); + }); + + describe('when sending some balance', function () { + const value = 10e5; + + beforeEach('creating proxy', async function () { + this.proxy = ( + await createProxy(this.implementation, proxyAdminAddress, initializeData, { + from: proxyCreator, + value, + }) + ).address; + }); + + assertProxyInitialization({ + value: expectedInitializedValue, + balance: value, + }); + }); + }); + }); +}; diff --git a/test/proxy/UpgradeableProxy.test.js b/test/proxy/UpgradeableProxy.test.js new file mode 100644 index 00000000000..e9d70bdf402 --- /dev/null +++ b/test/proxy/UpgradeableProxy.test.js @@ -0,0 +1,15 @@ +const { accounts, contract } = require('@openzeppelin/test-environment'); + +const shouldBehaveLikeUpgradeableProxy = require('./UpgradeableProxy.behaviour'); + +const UpgradeableProxy = contract.fromArtifact('UpgradeableProxy'); + +describe('UpgradeableProxy', function () { + const [proxyAdminOwner] = accounts; + + const createProxy = async function (implementation, _admin, initData, opts) { + return UpgradeableProxy.new(implementation, initData, opts); + }; + + shouldBehaveLikeUpgradeableProxy(createProxy, undefined, proxyAdminOwner); +});