Browse Source

Add Proxies from OpenZeppelin SDK (#2335)

Francisco Giordano 5 năm trước cách đây
mục cha
commit
cb791a1b21

+ 20 - 0
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;
+  }
+}

+ 57 - 0
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";
+  }
+}

+ 36 - 0
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");
+  }
+
+}

+ 76 - 0
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;
+  }
+}

+ 66 - 0
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;
+  }
+}

+ 49 - 0
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;
+  }
+}

+ 62 - 0
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;
+    }
+}

+ 78 - 0
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());
+    }
+}

+ 70 - 0
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);
+    }
+}

+ 139 - 0
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();
+    }
+}

+ 81 - 0
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)
+        }
+    }
+}

+ 81 - 0
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);
+    });
+  });
+});

+ 119 - 0
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);
+        });
+      });
+    });
+  });
+});

+ 436 - 0
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');
+    });
+  });
+};

+ 17 - 0
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);
+});

+ 216 - 0
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,
+        });
+      });
+    });
+  });
+};

+ 15 - 0
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);
+});