Add Proxies from OpenZeppelin SDK (#2335)
parent
0b489f4d79
commit
cb791a1b21
@ -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; |
||||
} |
||||
} |
@ -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"; |
||||
} |
||||
} |
@ -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"); |
||||
} |
||||
|
||||
} |
@ -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; |
||||
} |
||||
} |
@ -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; |
||||
} |
||||
} |
@ -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; |
||||
} |
||||
} |
@ -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; |
||||
} |
||||
} |
@ -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()); |
||||
} |
||||
} |
@ -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); |
||||
} |
||||
} |
@ -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(); |
||||
} |
||||
} |
@ -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) |
||||
} |
||||
} |
||||
} |
@ -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); |
||||
}); |
||||
}); |
||||
}); |
@ -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); |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
@ -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'); |
||||
}); |
||||
}); |
||||
}; |
@ -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); |
||||
}); |
@ -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, |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
||||
}; |
@ -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); |
||||
}); |
Loading…
Reference in new issue