diff --git a/contracts/zero-ex/contracts/src/ZeroEx.sol b/contracts/zero-ex/contracts/src/ZeroEx.sol index ed3965abe9..25fa11ac65 100644 --- a/contracts/zero-ex/contracts/src/ZeroEx.sol +++ b/contracts/zero-ex/contracts/src/ZeroEx.sol @@ -20,7 +20,8 @@ pragma solidity ^0.6.5; pragma experimental ABIEncoderV2; import "@0x/contracts-utils/contracts/src/v06/LibBytesV06.sol"; -import "./interfaces/IZeroExBootstrapper.sol"; +import "./migrations/LibBootstrap.sol"; +import "./features/Bootstrap.sol"; import "./storage/LibProxyStorage.sol"; import "./errors/LibProxyRichErrors.sol"; @@ -28,17 +29,23 @@ import "./errors/LibProxyRichErrors.sol"; /// @dev An extensible proxy contract that serves as a universal entry point for /// interacting with the 0x protocol. contract ZeroEx { - // solhint-disable separate-by-one-line-in-contract,indent,var-name-mixedcase using LibBytesV06 for bytes; - /// @dev Construct this contract. - /// After constructing this contract, the deployer should call - /// `bootstrap()` to seed the initial feature set. - constructor() public { - // Set the `bootstrap()` caller to the deployer. - LibProxyStorage.getStorage().bootstrapCaller = msg.sender; + /// @dev Magic bytes returned by the bootstrapper to indicate success. + bytes4 internal constant BOOTSTRAP_SUCCESS = 0xd150751b; + + /// @dev Construct this contract and set the bootstrap migration contract. + /// After constructing this contract, `bootstrap()` should be called + /// to seed the initial feature set. + /// @param bootstrapper The bootstrap migration contract. + constructor(address bootstrapper) public { + // Temporarily create and register the bootstrap feature. + // It will deregister itself after `bootstrap()` has been called. + Bootstrap bootstrap = new Bootstrap(msg.sender, bootstrapper); + LibProxyStorage.getStorage().impls[bootstrap.bootstrap.selector] = + address(bootstrap); } // solhint-disable state-visibility @@ -63,40 +70,6 @@ contract ZeroEx { // solhint-enable state-visibility - /// @dev Bootstrap the initial feature set of this contract. - /// This can only be called once by the deployer of this contract. - /// @param bootstrappers Array of bootstrapping contracts to delegatecall into. - function bootstrap(IZeroExBootstrapper[] calldata bootstrappers) external { - LibProxyStorage.Storage storage stor = LibProxyStorage.getStorage(); - - // If `bootstrapCaller` is zero, the contract has already been bootstrapped. - address bootstrapCaller = stor.bootstrapCaller; - if (bootstrapCaller == address(0)) { - _revertWithData(LibProxyRichErrors.AlreadyBootstrappedError()); - } - // Only the deployer caller can call this function. - if (bootstrapCaller != msg.sender) { - _revertWithData( - LibProxyRichErrors.InvalidBootstrapCallerError(msg.sender, bootstrapCaller) - ); - } - // Prevent calling `bootstrap()` again. - stor.bootstrapCaller = address(0); - - // Call the bootstrap contracts. - for (uint256 i = 0; i < bootstrappers.length; ++i) { - // Delegatecall into the bootstrap contract. - (bool success, bytes memory resultData) = address(bootstrappers[i]) - .delegatecall(abi.encodeWithSelector( - IZeroExBootstrapper.bootstrap.selector, - address(bootstrappers[i]) - )); - if (!success) { - _revertWithData(resultData); - } - } - } - /// @dev Get the implementation contract of a registered function. /// @param selector The function selector. /// @return impl The implementation contract address. diff --git a/contracts/zero-ex/contracts/test/TestBasicMigration.sol b/contracts/zero-ex/contracts/src/errors/LibMigrateRichErrors.sol similarity index 50% rename from contracts/zero-ex/contracts/test/TestBasicMigration.sol rename to contracts/zero-ex/contracts/src/errors/LibMigrateRichErrors.sol index 2a7a4e2229..0400c7c371 100644 --- a/contracts/zero-ex/contracts/test/TestBasicMigration.sol +++ b/contracts/zero-ex/contracts/src/errors/LibMigrateRichErrors.sol @@ -17,17 +17,31 @@ */ pragma solidity ^0.6.5; -pragma experimental ABIEncoderV2; - -import "../src/migrations/BasicMigration.sol"; -import "../src/interfaces/IZeroExBootstrapper.sol"; -contract TestBasicMigration is - BasicMigration -{ - function callBootstrap(ZeroEx zeroEx) external { - IZeroExBootstrapper[] memory bootstrappers; - zeroEx.bootstrap(bootstrappers); +library LibMigrateRichErrors { + + // solhint-disable func-name-mixedcase + + function AlreadyMigratingError() + internal + pure + returns (bytes memory) + { + return abi.encodeWithSelector( + bytes4(keccak256("AlreadyMigratingError()")) + ); + } + + function MigrateCallFailedError(address target, bytes memory resultData) + internal + pure + returns (bytes memory) + { + return abi.encodeWithSelector( + bytes4(keccak256("MigrateCallFailedError(address,bytes)")), + target, + resultData + ); } } diff --git a/contracts/zero-ex/contracts/src/errors/LibProxyRichErrors.sol b/contracts/zero-ex/contracts/src/errors/LibProxyRichErrors.sol index 062e325ab5..5c8e9242c3 100644 --- a/contracts/zero-ex/contracts/src/errors/LibProxyRichErrors.sol +++ b/contracts/zero-ex/contracts/src/errors/LibProxyRichErrors.sol @@ -34,25 +34,39 @@ library LibProxyRichErrors { ); } - function AlreadyBootstrappedError() - internal - pure - returns (bytes memory) - { - return abi.encodeWithSelector( - bytes4(keccak256("AlreadyBootstrappedError()")) - ); - } - - function InvalidBootstrapCallerError(address caller, address expectedCaller) + function InvalidBootstrapCallerError(address actual, address expected) internal pure returns (bytes memory) { return abi.encodeWithSelector( bytes4(keccak256("InvalidBootstrapCallerError(address,address)")), - caller, - expectedCaller + actual, + expected + ); + } + + function InvalidDieCallerError(address actual, address expected) + internal + pure + returns (bytes memory) + { + return abi.encodeWithSelector( + bytes4(keccak256("InvalidDieCallerError(address,address)")), + actual, + expected + ); + } + + function BootstrapCallFailedError(address target, bytes memory resultData) + internal + pure + returns (bytes memory) + { + return abi.encodeWithSelector( + bytes4(keccak256("BootstrapCallFailedError(address,bytes)")), + target, + resultData ); } } diff --git a/contracts/zero-ex/contracts/src/features/Bootstrap.sol b/contracts/zero-ex/contracts/src/features/Bootstrap.sol new file mode 100644 index 0000000000..0dd60eba66 --- /dev/null +++ b/contracts/zero-ex/contracts/src/features/Bootstrap.sol @@ -0,0 +1,87 @@ +/* + + Copyright 2020 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.6.5; +pragma experimental ABIEncoderV2; + +import "../migrations/LibBootstrap.sol"; +import "../fixins/FixinCommon.sol"; +import "../storage/LibProxyStorage.sol"; +import "./IBootstrap.sol"; + + +/// @dev Detachable `bootstrap()` feature. +contract Bootstrap is + IBootstrap, + FixinCommon +{ + // solhint-disable state-visibility,indent + /// @dev The ZeroEx contract. + /// This has to be immutable to persist across delegatecalls. + address immutable private _deployer; + /// @dev The implementation address of this contract. + /// This has to be immutable to persist across delegatecalls. + address immutable private _implementation; + /// @dev The deployer. + /// This has to be immutable to persist across delegatecalls. + address immutable private _bootstrapCaller; + /// @dev The bootstrap migrator. + /// This has to be immutable to persist across delegatecalls. + address immutable private _bootstrapper; + // solhint-enable state-visibility,indent + + /// @dev Construct this contract and set the bootstrap migration contract. + /// After constructing this contract, `bootstrap()` should be called + /// to seed the initial feature set. + /// @param bootstrapCaller The allowed caller of `bootstrap()`. + /// @param bootstrapper The bootstrap migration contract. + constructor(address bootstrapCaller, address bootstrapper) public { + _deployer = msg.sender; + _implementation = address(this); + _bootstrapCaller = bootstrapCaller; + _bootstrapper = bootstrapper; + } + + /// @dev Bootstrap the initial feature set of this contract by delegatecalling + /// into `_bootstrapper`. Before exiting the `bootstrap()` function will + /// deregister itself from the proxy to prevent being called again. + /// @param callData The call data to execute on `_bootstrapper`. + function bootstrap(bytes calldata callData) external override { + // Only the bootstrap caller can call this function. + if (msg.sender != _bootstrapCaller) { + _rrevert(LibProxyRichErrors.InvalidBootstrapCallerError( + msg.sender, + _bootstrapCaller + )); + } + LibBootstrap.delegatecallBootstrapFunction(_bootstrapper, callData); + // Deregister. + LibProxyStorage.getStorage().impls[this.bootstrap.selector] = address(0); + // Self-destruct. + Bootstrap(_implementation).die(); + } + + /// @dev Self-destructs this contract. + /// Can only be called by the ZeroEx contract. + function die() external { + if (msg.sender != _deployer) { + _rrevert(LibProxyRichErrors.InvalidDieCallerError(msg.sender, _deployer)); + } + selfdestruct(msg.sender); + } +} diff --git a/contracts/zero-ex/contracts/src/interfaces/IZeroExBootstrapper.sol b/contracts/zero-ex/contracts/src/features/IBootstrap.sol similarity index 59% rename from contracts/zero-ex/contracts/src/interfaces/IZeroExBootstrapper.sol rename to contracts/zero-ex/contracts/src/features/IBootstrap.sol index 4965ce22e9..87bc766700 100644 --- a/contracts/zero-ex/contracts/src/interfaces/IZeroExBootstrapper.sol +++ b/contracts/zero-ex/contracts/src/features/IBootstrap.sol @@ -20,13 +20,12 @@ pragma solidity ^0.6.5; pragma experimental ABIEncoderV2; -/// @dev Interface for a bootstrapping contract that the `ZeroEx` proxy. -interface IZeroExBootstrapper { +/// @dev Detachable `bootstrap()` feature. +interface IBootstrap { - /// @dev Sets up the initial state of the `ZeroEx` contract. - /// The `ZeroEx` contract will delegatecall this function so the - /// bootstrapper should use this function to register initial - /// features. - /// @param impl The implementation contract. - function bootstrap(address impl) external; + /// @dev Bootstrap the initial feature set of this contract by delegatecalling + /// into `_bootstrapper`. Before exiting the `bootstrap()` function will + /// deregister itself from the proxy to prevent being called again. + /// @param callData The call data to execute on `_bootstrapper`. + function bootstrap(bytes calldata callData) external; } diff --git a/contracts/zero-ex/contracts/src/interfaces/IFeature.sol b/contracts/zero-ex/contracts/src/features/IFeature.sol similarity index 100% rename from contracts/zero-ex/contracts/src/interfaces/IFeature.sol rename to contracts/zero-ex/contracts/src/features/IFeature.sol diff --git a/contracts/zero-ex/contracts/src/features/IMigrate.sol b/contracts/zero-ex/contracts/src/features/IMigrate.sol new file mode 100644 index 0000000000..76c69a9501 --- /dev/null +++ b/contracts/zero-ex/contracts/src/features/IMigrate.sol @@ -0,0 +1,43 @@ +/* + + Copyright 2020 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.6.5; +pragma experimental ABIEncoderV2; + + +/// @dev Migration features. +interface IMigrate { + + /// @dev Emitted when `migrate()` is called. + /// @param caller The caller of `migrate()`. + /// @param migrator The migration contract. + event Migrated(address caller, address migrator); + + /// @dev Execute a migration function in the context of the ZeroEx contract. + /// The result of the function being called should be the magic bytes + /// 0x2c64c5ef. Only callable by the owner. + /// The owner will be temporarily set to `address(this)` inside the call. + /// The original owner can be retrieved through `getMigrationOwner()`.` + /// @param target The migrator contract address. + /// @param data The call data. + function migrate(address target, bytes calldata data) external; + + /// @dev Get the true owner of this contract during a migration. + /// @return owner The true owner of this contract. + function getMigrationOwner() external view returns (address owner); +} diff --git a/contracts/zero-ex/contracts/src/interfaces/IOwnable.sol b/contracts/zero-ex/contracts/src/features/IOwnable.sol similarity index 100% rename from contracts/zero-ex/contracts/src/interfaces/IOwnable.sol rename to contracts/zero-ex/contracts/src/features/IOwnable.sol diff --git a/contracts/zero-ex/contracts/src/interfaces/ISimpleFunctionRegistry.sol b/contracts/zero-ex/contracts/src/features/ISimpleFunctionRegistry.sol similarity index 88% rename from contracts/zero-ex/contracts/src/interfaces/ISimpleFunctionRegistry.sol rename to contracts/zero-ex/contracts/src/features/ISimpleFunctionRegistry.sol index 4ac7be8e89..6aabc17889 100644 --- a/contracts/zero-ex/contracts/src/interfaces/ISimpleFunctionRegistry.sol +++ b/contracts/zero-ex/contracts/src/features/ISimpleFunctionRegistry.sol @@ -19,8 +19,6 @@ pragma solidity ^0.6.5; pragma experimental ABIEncoderV2; -import "../interfaces/IZeroExBootstrapper.sol"; - /// @dev Basic registry management features. interface ISimpleFunctionRegistry { @@ -41,12 +39,6 @@ interface ISimpleFunctionRegistry { /// @param impl The implementation contract for the function. function extend(bytes4 selector, address impl) external; - /// @dev Register or replace a function. - /// Only callable from within. - /// @param selector The function selector. - /// @param impl The implementation contract for the function. - function extendSelf(bytes4 selector, address impl) external; - /// @dev Retrieve the length of the rollback history for a function. /// @param selector The function selector. /// @return rollbackLength The number of items in the rollback history for diff --git a/contracts/zero-ex/contracts/src/features/Migrate.sol b/contracts/zero-ex/contracts/src/features/Migrate.sol new file mode 100644 index 0000000000..f5ff916c19 --- /dev/null +++ b/contracts/zero-ex/contracts/src/features/Migrate.sol @@ -0,0 +1,94 @@ +/* + + Copyright 2020 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.6.5; +pragma experimental ABIEncoderV2; + +import "../fixins/FixinOwnable.sol"; +import "../errors/LibOwnableRichErrors.sol"; +import "../storage/LibOwnableStorage.sol"; +import "../storage/LibMigrateStorage.sol"; +import "../migrations/LibMigrate.sol"; +import "../migrations/LibBootstrap.sol"; +import "./IFeature.sol"; +import "./IMigrate.sol"; +import "./ISimpleFunctionRegistry.sol"; + + +/// @dev Migration features. +contract Migrate is + IFeature, + IMigrate, + FixinOwnable +{ + // solhint-disable const-name-snakecase + + /// @dev Name of this feature. + string constant public override FEATURE_NAME = "Migrate"; + /// @dev Version of this feature. + uint256 constant public override FEATURE_VERSION = (1 << 64) | (0 << 32) | (0); + + /// @dev Initializes this feature. + /// @param impl The actual address of this feature contract. + /// @return success Magic bytes if successful. + function bootstrap(address impl) external returns (bytes4 success) { + // Register feature functions. + ISimpleFunctionRegistry(address(this)).extend(this.migrate.selector, impl); + ISimpleFunctionRegistry(address(this)).extend(this.getMigrationOwner.selector, impl); + return LibBootstrap.BOOTSTRAP_SUCCESS; + } + + /// @dev Execute a migration function in the context of the ZeroEx contract. + /// The result of the function being called should be the magic bytes + /// 0x2c64c5ef. Only callable by the owner. + /// The owner will be temporarily set to `address(this)` inside the call. + /// The original owner can be retrieved through `getMigrationOwner()`.` + /// @param target The migrator contract address. + /// @param data The call data. + function migrate(address target, bytes calldata data) + external + override + onlyOwner + { + LibOwnableStorage.Storage storage ownableStor = LibOwnableStorage.getStorage(); + LibMigrateStorage.Storage storage stor = LibMigrateStorage.getStorage(); + address prevOwner = ownableStor.owner; + if (prevOwner == address(this)) { + // If the owner is already set to ourselves then we've reentered. + _rrevert(LibMigrateRichErrors.AlreadyMigratingError()); + } + // Temporarily set the owner to ourselves. + ownableStor.owner = address(this); + stor.migrationOwner = prevOwner; + + // Perform the migration. + LibMigrate.delegatecallMigrateFunction(target, data); + + // Restore the owner. + ownableStor.owner = prevOwner; + stor.migrationOwner = address(0); + + emit Migrated(msg.sender, target); + } + + /// @dev Get the true owner of this contract during a migration. + /// @return owner The true owner of this contract. + function getMigrationOwner() external override view returns (address owner) { + return LibMigrateStorage.getStorage().migrationOwner; + } +} diff --git a/contracts/zero-ex/contracts/src/features/Ownable.sol b/contracts/zero-ex/contracts/src/features/Ownable.sol index 5a77ba7b56..609aecbfc6 100644 --- a/contracts/zero-ex/contracts/src/features/Ownable.sol +++ b/contracts/zero-ex/contracts/src/features/Ownable.sol @@ -20,19 +20,18 @@ pragma solidity ^0.6.5; pragma experimental ABIEncoderV2; import "../fixins/FixinOwnable.sol"; -import "../interfaces/IFeature.sol"; -import "../interfaces/IOwnable.sol"; -import "../interfaces/ISimpleFunctionRegistry.sol"; -import "../interfaces/IZeroExBootstrapper.sol"; import "../errors/LibOwnableRichErrors.sol"; import "../storage/LibOwnableStorage.sol"; +import "../migrations/LibBootstrap.sol"; +import "./IFeature.sol"; +import "./IOwnable.sol"; +import "./ISimpleFunctionRegistry.sol"; /// @dev Owner management features. contract Ownable is IFeature, IOwnable, - IZeroExBootstrapper, FixinOwnable { // solhint-disable const-name-snakecase @@ -42,15 +41,20 @@ contract Ownable is /// @dev Version of this feature. uint256 constant public override FEATURE_VERSION = (1 << 64) | (0 << 32) | (0); - /// @dev Initializes the authority feature. - /// @param impl The actual address of this feature contract. - function bootstrap(address impl) external override { + /// @dev Initializes this feature. The intial owner will be set to this (ZeroEx) + /// to allow the bootstrappers to call `extend()`. Ownership should be + /// transferred to the real owner by the bootstrapper after + /// bootstrapping is complete. + /// @param impl the actual address of this feature contract. + /// @return success Magic bytes if successful. + function bootstrap(address impl) external returns (bytes4 success) { // Set the owner. - LibOwnableStorage.getStorage().owner = msg.sender; + LibOwnableStorage.getStorage().owner = address(this); // Register feature functions. - ISimpleFunctionRegistry(address(this)).extendSelf(this.transferOwnership.selector, impl); - ISimpleFunctionRegistry(address(this)).extendSelf(this.getOwner.selector, impl); + ISimpleFunctionRegistry(address(this)).extend(this.transferOwnership.selector, impl); + ISimpleFunctionRegistry(address(this)).extend(this.getOwner.selector, impl); + return LibBootstrap.BOOTSTRAP_SUCCESS; } /// @dev Change the owner of this contract. diff --git a/contracts/zero-ex/contracts/src/features/SimpleFunctionRegistry.sol b/contracts/zero-ex/contracts/src/features/SimpleFunctionRegistry.sol index 633340a781..0ce160a134 100644 --- a/contracts/zero-ex/contracts/src/features/SimpleFunctionRegistry.sol +++ b/contracts/zero-ex/contracts/src/features/SimpleFunctionRegistry.sol @@ -19,20 +19,19 @@ pragma solidity ^0.6.5; pragma experimental ABIEncoderV2; -import "../interfaces/IFeature.sol"; -import "../interfaces/ISimpleFunctionRegistry.sol"; -import "../interfaces/IZeroExBootstrapper.sol"; import "../fixins/FixinOwnable.sol"; import "../storage/LibProxyStorage.sol"; import "../storage/LibSimpleFunctionRegistryStorage.sol"; import "../errors/LibSimpleFunctionRegistryRichErrors.sol"; +import "../migrations/LibBootstrap.sol"; +import "./IFeature.sol"; +import "./ISimpleFunctionRegistry.sol"; /// @dev Basic registry management features. contract SimpleFunctionRegistry is IFeature, ISimpleFunctionRegistry, - IZeroExBootstrapper, FixinOwnable { // solhint-disable const-name-snakecase @@ -42,17 +41,18 @@ contract SimpleFunctionRegistry is /// @dev Version of this feature. uint256 constant public override FEATURE_VERSION = (1 << 64) | (0 << 32) | (0); - /// @dev Initializes the feature implementation registry. + /// @dev Initializes this feature. /// @param impl The actual address of this feature contract. - function bootstrap(address impl) external override { + /// @return success Magic bytes if successful. + function bootstrap(address impl) external returns (bytes4 success) { // Register the registration functions (inception vibes). _extend(this.extend.selector, impl); - _extend(this.extendSelf.selector, impl); // Register the rollback function. _extend(this.rollback.selector, impl); // Register getters. _extend(this.getRollbackLength.selector, impl); _extend(this.getRollbackEntryAtIndex.selector, impl); + return LibBootstrap.BOOTSTRAP_SUCCESS; } /// @dev Roll back to a prior implementation of a function. @@ -108,18 +108,6 @@ contract SimpleFunctionRegistry is _extend(selector, impl); } - /// @dev Register or replace a function. - /// Only callable from within. - /// @param selector The function selector. - /// @param impl The implementation contract for the function. - function extendSelf(bytes4 selector, address impl) - external - override - onlySelf - { - _extend(selector, impl); - } - /// @dev Retrieve the length of the rollback history for a function. /// @param selector The function selector. /// @return rollbackLength The number of items in the rollback history for diff --git a/contracts/zero-ex/contracts/src/migrations/BasicMigration.sol b/contracts/zero-ex/contracts/src/migrations/BasicMigration.sol deleted file mode 100644 index 201d0ef3c6..0000000000 --- a/contracts/zero-ex/contracts/src/migrations/BasicMigration.sol +++ /dev/null @@ -1,70 +0,0 @@ -/* - - Copyright 2020 ZeroEx Intl. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - -*/ - -pragma solidity ^0.6.5; -pragma experimental ABIEncoderV2; - -import "../ZeroEx.sol"; -import "../features/SimpleFunctionRegistry.sol"; -import "../features/Ownable.sol"; -import "../interfaces/IOwnable.sol"; -import "../interfaces/IZeroExBootstrapper.sol"; -import "../interfaces/ISimpleFunctionRegistry.sol"; - - -/// @dev A contract for deploying and configuring a minimal ZeroEx contract. -contract BasicMigration { - - /// @dev Deploy the `ZeroEx` contract with the minimum feature set. - /// @param owner The owner of the contract. - /// @return zeroEx The deployed and configured `ZeroEx` contract. - function migrate(address owner) external returns (ZeroEx zeroEx) { - // Deploy the ZeroEx contract. - zeroEx = new ZeroEx(); - - // Bootstrap the initial feature set. - zeroEx.bootstrap(_createBootstrappers()); - - // Disable the `extendSelf()` function by rolling it back to zero. - ISimpleFunctionRegistry(address(zeroEx)) - .rollback(ISimpleFunctionRegistry.extendSelf.selector, address(0)); - - // Call the _postInitialize hook. - _postInitialize(zeroEx); - - // Transfer ownership. - if (owner != address(this)) { - IOwnable(address(zeroEx)).transferOwnership(owner); - } - } - - function _createBootstrappers() - internal - virtual - returns (IZeroExBootstrapper[] memory bootstrappers) - { - bootstrappers = new IZeroExBootstrapper[](2); - bootstrappers[0] = IZeroExBootstrapper(address(new SimpleFunctionRegistry())); - bootstrappers[1] = IZeroExBootstrapper(address(new Ownable())); - } - - // solhint-disable no-empty-blocks - function _postInitialize(ZeroEx zeroEx) internal virtual { - // Override me. - } -} diff --git a/contracts/zero-ex/contracts/src/migrations/InitialMigration.sol b/contracts/zero-ex/contracts/src/migrations/InitialMigration.sol new file mode 100644 index 0000000000..1281a246ca --- /dev/null +++ b/contracts/zero-ex/contracts/src/migrations/InitialMigration.sol @@ -0,0 +1,87 @@ +/* + + Copyright 2020 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.6.5; +pragma experimental ABIEncoderV2; + +import "../ZeroEx.sol"; +import "../features/IBootstrap.sol"; +import "../features/SimpleFunctionRegistry.sol"; +import "../features/Ownable.sol"; +import "../features/Migrate.sol"; +import "./LibBootstrap.sol"; + + +/// @dev A contract for deploying and configuring a minimal ZeroEx contract. +contract InitialMigration { + /// @dev The deployed ZereEx instance. + ZeroEx private _zeroEx; + + /// @dev Deploy the `ZeroEx` contract with the minimum feature set, + /// transfers ownership to `owner`, then self-destructs. + /// @param owner The owner of the contract. + /// @return zeroEx The deployed and configured `ZeroEx` contract. + function deploy(address owner) public virtual returns (ZeroEx zeroEx) { + // Must not have already been run. + require(address(_zeroEx) == address(0), "InitialMigration/ALREADY_DEPLOYED"); + + // Deploy the ZeroEx contract, setting ourselves as the bootstrapper. + zeroEx = _zeroEx = new ZeroEx(address(this)); + + // Bootstrap the initial feature set. + IBootstrap(address(zeroEx)).bootstrap(abi.encodeWithSelector( + this.bootstrap.selector, + owner + )); + } + + /// @dev Sets up the initial state of the `ZeroEx` contract. + /// The `ZeroEx` contract will delegatecall into this function. + /// @param owner The new owner of the ZeroEx contract. + /// @return success Magic bytes if successful. + function bootstrap(address owner) public virtual returns (bytes4 success) { + // Deploy and migrate the initial features. + // Order matters here. + + // Initialize Registry. + SimpleFunctionRegistry registry = new SimpleFunctionRegistry(); + LibBootstrap.delegatecallBootstrapFunction( + address(registry), + abi.encodeWithSelector(registry.bootstrap.selector, address(registry)) + ); + + // Initialize Ownable. + Ownable ownable = new Ownable(); + LibBootstrap.delegatecallBootstrapFunction( + address(ownable), + abi.encodeWithSelector(ownable.bootstrap.selector, address(ownable)) + ); + + // Initialize Migrate. + Migrate migrate = new Migrate(); + LibBootstrap.delegatecallBootstrapFunction( + address(migrate), + abi.encodeWithSelector(migrate.bootstrap.selector, address(migrate)) + ); + + // Transfer ownership to the real owner. + Ownable(address(this)).transferOwnership(owner); + + success = LibBootstrap.BOOTSTRAP_SUCCESS; + } +} diff --git a/contracts/zero-ex/contracts/src/migrations/LibBootstrap.sol b/contracts/zero-ex/contracts/src/migrations/LibBootstrap.sol new file mode 100644 index 0000000000..e2a1744517 --- /dev/null +++ b/contracts/zero-ex/contracts/src/migrations/LibBootstrap.sol @@ -0,0 +1,50 @@ +/* + + Copyright 2020 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.6.5; +pragma experimental ABIEncoderV2; + +import "@0x/contracts-utils/contracts/src/v06/errors/LibRichErrorsV06.sol"; +import "../errors/LibProxyRichErrors.sol"; + + +library LibBootstrap { + + /// @dev Magic bytes returned by the bootstrapper to indicate success. + bytes4 internal constant BOOTSTRAP_SUCCESS = 0xd150751b; + + /// @dev Perform a delegatecall and ensure it returns the magic bytes. + /// @param target The call target. + /// @param data The call data. + function delegatecallBootstrapFunction( + address target, + bytes memory data + ) + internal + { + (bool success, bytes memory resultData) = target.delegatecall(data); + if (!success || + resultData.length != 32 || + abi.decode(resultData, (bytes4)) != BOOTSTRAP_SUCCESS) + { + LibRichErrorsV06.rrevert( + LibProxyRichErrors.BootstrapCallFailedError(target, resultData) + ); + } + } +} diff --git a/contracts/zero-ex/contracts/src/migrations/LibMigrate.sol b/contracts/zero-ex/contracts/src/migrations/LibMigrate.sol new file mode 100644 index 0000000000..ebc71edc98 --- /dev/null +++ b/contracts/zero-ex/contracts/src/migrations/LibMigrate.sol @@ -0,0 +1,50 @@ +/* + + Copyright 2020 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.6.5; +pragma experimental ABIEncoderV2; + +import "@0x/contracts-utils/contracts/src/v06/errors/LibRichErrorsV06.sol"; +import "../errors/LibMigrateRichErrors.sol"; + + +library LibMigrate { + + /// @dev Magic bytes returned by a migrator to indicate success. + bytes4 internal constant MIGRATE_SUCCESS = 0x2c64c5ef; + + /// @dev Perform a delegatecall and ensure it returns the magic bytes. + /// @param target The call target. + /// @param data The call data. + function delegatecallMigrateFunction( + address target, + bytes memory data + ) + internal + { + (bool success, bytes memory resultData) = target.delegatecall(data); + if (!success || + resultData.length != 32 || + abi.decode(resultData, (bytes4)) != MIGRATE_SUCCESS) + { + LibRichErrorsV06.rrevert( + LibMigrateRichErrors.MigrateCallFailedError(target, resultData) + ); + } + } +} diff --git a/contracts/zero-ex/contracts/src/storage/LibMigrateStorage.sol b/contracts/zero-ex/contracts/src/storage/LibMigrateStorage.sol new file mode 100644 index 0000000000..ff2aa3b4b8 --- /dev/null +++ b/contracts/zero-ex/contracts/src/storage/LibMigrateStorage.sol @@ -0,0 +1,41 @@ +/* + + Copyright 2020 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.6.5; +pragma experimental ABIEncoderV2; + + +/// @dev Storage helpers for the `Migrate` feature. +library LibMigrateStorage { + + /// @dev Globally unique offset for the storage bucket. + bytes32 constant internal STORAGE_ID = + 0x0ed67b719caa0e9bebb7147a4de9fdb6f1c82a984b2297d741a9888432214d5c; + + /// @dev Storage bucket for this feature. + struct Storage { + // The owner of this contract prior to the `migrate()` call. + address migrationOwner; + } + + /// @dev Get the storage bucket for this contract. + function getStorage() internal pure returns (Storage storage stor) { + bytes32 storageId = STORAGE_ID; + assembly { stor_slot := storageId } + } +} diff --git a/contracts/zero-ex/contracts/src/storage/LibProxyStorage.sol b/contracts/zero-ex/contracts/src/storage/LibProxyStorage.sol index c579f3af1a..ce878d17fc 100644 --- a/contracts/zero-ex/contracts/src/storage/LibProxyStorage.sol +++ b/contracts/zero-ex/contracts/src/storage/LibProxyStorage.sol @@ -29,8 +29,6 @@ library LibProxyStorage { /// @dev Storage bucket for proxy contract. struct Storage { - // The allowed caller for `bootstrap()`. - address bootstrapCaller; // Mapping of function selector -> function implementation mapping(bytes4 => address) impls; } diff --git a/contracts/zero-ex/contracts/test/TestInitialMigration.sol b/contracts/zero-ex/contracts/test/TestInitialMigration.sol new file mode 100644 index 0000000000..e3d60eb50b --- /dev/null +++ b/contracts/zero-ex/contracts/test/TestInitialMigration.sol @@ -0,0 +1,46 @@ +/* + + Copyright 2020 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.6.5; +pragma experimental ABIEncoderV2; + +import "../src/ZeroEx.sol"; +import "../src/features/IBootstrap.sol"; +import "../src/migrations/InitialMigration.sol"; + + +contract TestInitialMigration is + InitialMigration +{ + address public bootstrapFeature; + + function callBootstrap(ZeroEx zeroEx) external { + IBootstrap(address(zeroEx)).bootstrap(new bytes(0)); + } + + function getCodeSizeOf(address target) external view returns (uint256 codeSize) { + assembly { codeSize := extcodesize(target) } + } + + function bootstrap(address owner) public override returns (bytes4 success) { + success = InitialMigration.bootstrap(owner); + // Snoop the bootstrap feature contract. + bootstrapFeature = ZeroEx(address(uint160(address(this)))) + .getFunctionImplementation(IBootstrap.bootstrap.selector); + } +} diff --git a/contracts/zero-ex/contracts/test/TestMigrator.sol b/contracts/zero-ex/contracts/test/TestMigrator.sol new file mode 100644 index 0000000000..8aeb6ad6e9 --- /dev/null +++ b/contracts/zero-ex/contracts/test/TestMigrator.sol @@ -0,0 +1,55 @@ +/* + + Copyright 2020 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.6.5; +pragma experimental ABIEncoderV2; + +import "../src/migrations/LibMigrate.sol"; +import "../src/features/IOwnable.sol"; +import "../src/features/IMigrate.sol"; + + +contract TestMigrator { + event TestMigrateCalled( + bytes callData, + address owner, + address actualOwner + ); + + function succeedingMigrate() external returns (bytes4 success) { + emit TestMigrateCalled( + msg.data, + IOwnable(address(this)).getOwner(), + IMigrate(address(this)).getMigrationOwner() + ); + return LibMigrate.MIGRATE_SUCCESS; + } + + function failingMigrate() external returns (bytes4 success) { + emit TestMigrateCalled( + msg.data, + IOwnable(address(this)).getOwner(), + IMigrate(address(this)).getMigrationOwner() + ); + return 0xdeadbeef; + } + + function revertingMigrate() external pure { + revert("OOPSIE"); + } +} diff --git a/contracts/zero-ex/package.json b/contracts/zero-ex/package.json index 75b856a79e..1e365a432c 100644 --- a/contracts/zero-ex/package.json +++ b/contracts/zero-ex/package.json @@ -38,9 +38,9 @@ "docs:json": "typedoc --excludePrivate --excludeExternals --excludeProtected --ignoreCompilerErrors --target ES5 --tsconfig typedoc-tsconfig.json --json $JSON_FILE_PATH $PROJECT_FILES" }, "config": { - "publicInterfaceContracts": "ZeroEx,IOwnable,ISimpleFunctionRegistry", + "publicInterfaceContracts": "ZeroEx,IMigrate,IOwnable,ISimpleFunctionRegistry", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.", - "abis": "./test/generated-artifacts/@(BasicMigration|FixinCommon|FixinOwnable|IFeature|IOwnable|ISimpleFunctionRegistry|ITestSimpleFunctionRegistryFeature|IZeroExBootstrapper|LibCommonRichErrors|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|Ownable|SimpleFunctionRegistry|TestBasicMigration|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestZeroExFeature|ZeroEx).json" + "abis": "./test/generated-artifacts/@(Bootstrap|FixinCommon|FixinOwnable|IBootstrap|IFeature|IMigrate|IOwnable|ISimpleFunctionRegistry|ITestSimpleFunctionRegistryFeature|InitialMigration|LibBootstrap|LibCommonRichErrors|LibMigrate|LibMigrateRichErrors|LibMigrateStorage|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|Migrate|Ownable|SimpleFunctionRegistry|TestInitialMigration|TestMigrator|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestZeroExFeature|ZeroEx).json" }, "repository": { "type": "git", diff --git a/contracts/zero-ex/src/artifacts.ts b/contracts/zero-ex/src/artifacts.ts index aa3ab07a57..47b300d3af 100644 --- a/contracts/zero-ex/src/artifacts.ts +++ b/contracts/zero-ex/src/artifacts.ts @@ -5,11 +5,13 @@ */ import { ContractArtifact } from 'ethereum-types'; +import * as IMigrate from '../generated-artifacts/IMigrate.json'; import * as IOwnable from '../generated-artifacts/IOwnable.json'; import * as ISimpleFunctionRegistry from '../generated-artifacts/ISimpleFunctionRegistry.json'; import * as ZeroEx from '../generated-artifacts/ZeroEx.json'; export const artifacts = { ZeroEx: ZeroEx as ContractArtifact, + IMigrate: IMigrate as ContractArtifact, IOwnable: IOwnable as ContractArtifact, ISimpleFunctionRegistry: ISimpleFunctionRegistry as ContractArtifact, }; diff --git a/contracts/zero-ex/src/wrappers.ts b/contracts/zero-ex/src/wrappers.ts index b7b12cc29d..32e226aa5c 100644 --- a/contracts/zero-ex/src/wrappers.ts +++ b/contracts/zero-ex/src/wrappers.ts @@ -3,6 +3,7 @@ * Warning: This file is auto-generated by contracts-gen. Don't edit manually. * ----------------------------------------------------------------------------- */ +export * from '../generated-wrappers/i_migrate'; export * from '../generated-wrappers/i_ownable'; export * from '../generated-wrappers/i_simple_function_registry'; export * from '../generated-wrappers/zero_ex'; diff --git a/contracts/zero-ex/test/artifacts.ts b/contracts/zero-ex/test/artifacts.ts index f11d91e878..ddc546676f 100644 --- a/contracts/zero-ex/test/artifacts.ts +++ b/contracts/zero-ex/test/artifacts.ts @@ -5,24 +5,32 @@ */ import { ContractArtifact } from 'ethereum-types'; -import * as BasicMigration from '../test/generated-artifacts/BasicMigration.json'; +import * as Bootstrap from '../test/generated-artifacts/Bootstrap.json'; import * as FixinCommon from '../test/generated-artifacts/FixinCommon.json'; import * as FixinOwnable from '../test/generated-artifacts/FixinOwnable.json'; +import * as IBootstrap from '../test/generated-artifacts/IBootstrap.json'; import * as IFeature from '../test/generated-artifacts/IFeature.json'; +import * as IMigrate from '../test/generated-artifacts/IMigrate.json'; +import * as InitialMigration from '../test/generated-artifacts/InitialMigration.json'; import * as IOwnable from '../test/generated-artifacts/IOwnable.json'; import * as ISimpleFunctionRegistry from '../test/generated-artifacts/ISimpleFunctionRegistry.json'; import * as ITestSimpleFunctionRegistryFeature from '../test/generated-artifacts/ITestSimpleFunctionRegistryFeature.json'; -import * as IZeroExBootstrapper from '../test/generated-artifacts/IZeroExBootstrapper.json'; +import * as LibBootstrap from '../test/generated-artifacts/LibBootstrap.json'; import * as LibCommonRichErrors from '../test/generated-artifacts/LibCommonRichErrors.json'; +import * as LibMigrate from '../test/generated-artifacts/LibMigrate.json'; +import * as LibMigrateRichErrors from '../test/generated-artifacts/LibMigrateRichErrors.json'; +import * as LibMigrateStorage from '../test/generated-artifacts/LibMigrateStorage.json'; import * as LibOwnableRichErrors from '../test/generated-artifacts/LibOwnableRichErrors.json'; import * as LibOwnableStorage from '../test/generated-artifacts/LibOwnableStorage.json'; import * as LibProxyRichErrors from '../test/generated-artifacts/LibProxyRichErrors.json'; import * as LibProxyStorage from '../test/generated-artifacts/LibProxyStorage.json'; import * as LibSimpleFunctionRegistryRichErrors from '../test/generated-artifacts/LibSimpleFunctionRegistryRichErrors.json'; import * as LibSimpleFunctionRegistryStorage from '../test/generated-artifacts/LibSimpleFunctionRegistryStorage.json'; +import * as Migrate from '../test/generated-artifacts/Migrate.json'; import * as Ownable from '../test/generated-artifacts/Ownable.json'; import * as SimpleFunctionRegistry from '../test/generated-artifacts/SimpleFunctionRegistry.json'; -import * as TestBasicMigration from '../test/generated-artifacts/TestBasicMigration.json'; +import * as TestInitialMigration from '../test/generated-artifacts/TestInitialMigration.json'; +import * as TestMigrator from '../test/generated-artifacts/TestMigrator.json'; import * as TestSimpleFunctionRegistryFeatureImpl1 from '../test/generated-artifacts/TestSimpleFunctionRegistryFeatureImpl1.json'; import * as TestSimpleFunctionRegistryFeatureImpl2 from '../test/generated-artifacts/TestSimpleFunctionRegistryFeatureImpl2.json'; import * as TestZeroExFeature from '../test/generated-artifacts/TestZeroExFeature.json'; @@ -30,23 +38,31 @@ import * as ZeroEx from '../test/generated-artifacts/ZeroEx.json'; export const artifacts = { ZeroEx: ZeroEx as ContractArtifact, LibCommonRichErrors: LibCommonRichErrors as ContractArtifact, + LibMigrateRichErrors: LibMigrateRichErrors as ContractArtifact, LibOwnableRichErrors: LibOwnableRichErrors as ContractArtifact, LibProxyRichErrors: LibProxyRichErrors as ContractArtifact, LibSimpleFunctionRegistryRichErrors: LibSimpleFunctionRegistryRichErrors as ContractArtifact, + Bootstrap: Bootstrap as ContractArtifact, + IBootstrap: IBootstrap as ContractArtifact, + IFeature: IFeature as ContractArtifact, + IMigrate: IMigrate as ContractArtifact, + IOwnable: IOwnable as ContractArtifact, + ISimpleFunctionRegistry: ISimpleFunctionRegistry as ContractArtifact, + Migrate: Migrate as ContractArtifact, Ownable: Ownable as ContractArtifact, SimpleFunctionRegistry: SimpleFunctionRegistry as ContractArtifact, FixinCommon: FixinCommon as ContractArtifact, FixinOwnable: FixinOwnable as ContractArtifact, - IFeature: IFeature as ContractArtifact, - IOwnable: IOwnable as ContractArtifact, - ISimpleFunctionRegistry: ISimpleFunctionRegistry as ContractArtifact, - IZeroExBootstrapper: IZeroExBootstrapper as ContractArtifact, - BasicMigration: BasicMigration as ContractArtifact, + InitialMigration: InitialMigration as ContractArtifact, + LibBootstrap: LibBootstrap as ContractArtifact, + LibMigrate: LibMigrate as ContractArtifact, + LibMigrateStorage: LibMigrateStorage as ContractArtifact, LibOwnableStorage: LibOwnableStorage as ContractArtifact, LibProxyStorage: LibProxyStorage as ContractArtifact, LibSimpleFunctionRegistryStorage: LibSimpleFunctionRegistryStorage as ContractArtifact, ITestSimpleFunctionRegistryFeature: ITestSimpleFunctionRegistryFeature as ContractArtifact, - TestBasicMigration: TestBasicMigration as ContractArtifact, + TestInitialMigration: TestInitialMigration as ContractArtifact, + TestMigrator: TestMigrator as ContractArtifact, TestSimpleFunctionRegistryFeatureImpl1: TestSimpleFunctionRegistryFeatureImpl1 as ContractArtifact, TestSimpleFunctionRegistryFeatureImpl2: TestSimpleFunctionRegistryFeatureImpl2 as ContractArtifact, TestZeroExFeature: TestZeroExFeature as ContractArtifact, diff --git a/contracts/zero-ex/test/basic_migration_test.ts b/contracts/zero-ex/test/basic_migration_test.ts deleted file mode 100644 index 66a9fb92c4..0000000000 --- a/contracts/zero-ex/test/basic_migration_test.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { blockchainTests, expect, randomAddress } from '@0x/contracts-test-utils'; -import { hexUtils, ZeroExRevertErrors } from '@0x/utils'; - -import { artifacts } from './artifacts'; -import { - IOwnableContract, - ISimpleFunctionRegistryContract, - TestBasicMigrationContract, - ZeroExContract, -} from './wrappers'; - -blockchainTests.resets('Basic migration', env => { - let owner: string; - let zeroEx: ZeroExContract; - let migrator: TestBasicMigrationContract; - - before(async () => { - [owner] = await env.getAccountAddressesAsync(); - migrator = await TestBasicMigrationContract.deployFrom0xArtifactAsync( - artifacts.TestBasicMigration, - env.provider, - env.txDefaults, - artifacts, - ); - const migrateCall = migrator.migrate(owner); - zeroEx = new ZeroExContract(await migrateCall.callAsync(), env.provider, env.txDefaults); - await migrateCall.awaitTransactionSuccessAsync(); - }); - - describe('bootstrapping', () => { - it('Migrator cannot call bootstrap() again', async () => { - const tx = migrator.callBootstrap(zeroEx.address).awaitTransactionSuccessAsync(); - return expect(tx).to.revertWith(new ZeroExRevertErrors.Proxy.AlreadyBootstrappedError()); - }); - }); - - describe('Ownable feature', () => { - let ownable: IOwnableContract; - - before(async () => { - ownable = new IOwnableContract(zeroEx.address, env.provider, env.txDefaults); - }); - - it('has the correct owner', async () => { - const actualOwner = await ownable.getOwner().callAsync(); - expect(actualOwner).to.eq(owner); - }); - }); - - describe('Registry feature', () => { - let registry: ISimpleFunctionRegistryContract; - - before(async () => { - registry = new ISimpleFunctionRegistryContract(zeroEx.address, env.provider, env.txDefaults); - }); - - it('`extendSelf()` is unregistered', async () => { - const tx = registry.extendSelf(hexUtils.random(4), randomAddress()).callAsync(); - return expect(tx).to.revertWith(new ZeroExRevertErrors.Proxy.NotImplementedError()); - }); - }); -}); diff --git a/contracts/zero-ex/test/features/migrate_test.ts b/contracts/zero-ex/test/features/migrate_test.ts new file mode 100644 index 0000000000..51bfb242fd --- /dev/null +++ b/contracts/zero-ex/test/features/migrate_test.ts @@ -0,0 +1,94 @@ +import { blockchainTests, expect, LogDecoder, randomAddress, verifyEventsFromLogs } from '@0x/contracts-test-utils'; +import { hexUtils, OwnableRevertErrors, StringRevertError, ZeroExRevertErrors } from '@0x/utils'; + +import { artifacts } from '../artifacts'; +import { initialMigrateAsync } from '../utils/migration'; +import { IMigrateContract, IOwnableContract, TestMigratorContract, TestMigratorEvents } from '../wrappers'; + +blockchainTests.resets('Migrate feature', env => { + let owner: string; + let ownable: IOwnableContract; + let migrate: IMigrateContract; + let testMigrator: TestMigratorContract; + let succeedingMigrateFnCallData: string; + let failingMigrateFnCallData: string; + let revertingMigrateFnCallData: string; + let logDecoder: LogDecoder; + + before(async () => { + logDecoder = new LogDecoder(env.web3Wrapper, artifacts); + [owner] = await env.getAccountAddressesAsync(); + const zeroEx = await initialMigrateAsync(owner, env.provider, env.txDefaults); + ownable = new IOwnableContract(zeroEx.address, env.provider, env.txDefaults); + migrate = new IMigrateContract(zeroEx.address, env.provider, env.txDefaults); + testMigrator = await TestMigratorContract.deployFrom0xArtifactAsync( + artifacts.TestMigrator, + env.provider, + env.txDefaults, + artifacts, + ); + succeedingMigrateFnCallData = testMigrator.succeedingMigrate().getABIEncodedTransactionData(); + failingMigrateFnCallData = testMigrator.failingMigrate().getABIEncodedTransactionData(); + revertingMigrateFnCallData = testMigrator.revertingMigrate().getABIEncodedTransactionData(); + }); + + describe('migrate()', () => { + it('non-owner cannot call migrate()', async () => { + const notOwner = randomAddress(); + const tx = migrate + .migrate(testMigrator.address, succeedingMigrateFnCallData) + .awaitTransactionSuccessAsync({ from: notOwner }); + return expect(tx).to.revertWith(new OwnableRevertErrors.OnlyOwnerError(notOwner, owner)); + }); + + it('can successfully execute a migration', async () => { + const receipt = await migrate + .migrate(testMigrator.address, succeedingMigrateFnCallData) + .awaitTransactionSuccessAsync({ from: owner }); + const { logs } = logDecoder.decodeReceiptLogs(receipt); + verifyEventsFromLogs( + logs, + [ + { + callData: succeedingMigrateFnCallData, + owner: migrate.address, + actualOwner: owner, + }, + ], + TestMigratorEvents.TestMigrateCalled, + ); + }); + + it('owner is restored after a migration', async () => { + await migrate + .migrate(testMigrator.address, succeedingMigrateFnCallData) + .awaitTransactionSuccessAsync({ from: owner }); + const currentOwner = await ownable.getOwner().callAsync(); + expect(currentOwner).to.eq(owner); + }); + + it('failing migration reverts', async () => { + const tx = migrate + .migrate(testMigrator.address, failingMigrateFnCallData) + .awaitTransactionSuccessAsync({ from: owner }); + return expect(tx).to.revertWith( + new ZeroExRevertErrors.Migrate.MigrateCallFailedError( + testMigrator.address, + hexUtils.rightPad('0xdeadbeef'), + ), + ); + }); + + it('reverting migration reverts', async () => { + const tx = migrate + .migrate(testMigrator.address, revertingMigrateFnCallData) + .awaitTransactionSuccessAsync({ from: owner }); + return expect(tx).to.revertWith( + new ZeroExRevertErrors.Migrate.MigrateCallFailedError( + testMigrator.address, + new StringRevertError('OOPSIE').encode(), + ), + ); + }); + }); +}); diff --git a/contracts/zero-ex/test/features/ownable_test.ts b/contracts/zero-ex/test/features/ownable_test.ts index 2e68b5116a..1609d90a21 100644 --- a/contracts/zero-ex/test/features/ownable_test.ts +++ b/contracts/zero-ex/test/features/ownable_test.ts @@ -1,30 +1,30 @@ import { blockchainTests, expect, randomAddress, verifyEventsFromLogs } from '@0x/contracts-test-utils'; import { OwnableRevertErrors } from '@0x/utils'; -import { basicMigrateAsync } from '../utils/migration'; +import { initialMigrateAsync } from '../utils/migration'; import { IOwnableContract, IOwnableEvents } from '../wrappers'; blockchainTests.resets('Ownable feature', env => { const notOwner = randomAddress(); let owner: string; - let auth: IOwnableContract; + let ownable: IOwnableContract; before(async () => { [owner] = await env.getAccountAddressesAsync(); - const zeroEx = await basicMigrateAsync(owner, env.provider, env.txDefaults); - auth = new IOwnableContract(zeroEx.address, env.provider, env.txDefaults); + const zeroEx = await initialMigrateAsync(owner, env.provider, env.txDefaults); + ownable = new IOwnableContract(zeroEx.address, env.provider, env.txDefaults); }); describe('transferOwnership()', () => { it('non-owner cannot transfer ownership', async () => { const newOwner = randomAddress(); - const tx = auth.transferOwnership(newOwner).callAsync({ from: notOwner }); + const tx = ownable.transferOwnership(newOwner).callAsync({ from: notOwner }); return expect(tx).to.revertWith(new OwnableRevertErrors.OnlyOwnerError(notOwner, owner)); }); it('owner can transfer ownership', async () => { const newOwner = randomAddress(); - const receipt = await auth.transferOwnership(newOwner).awaitTransactionSuccessAsync({ from: owner }); + const receipt = await ownable.transferOwnership(newOwner).awaitTransactionSuccessAsync({ from: owner }); verifyEventsFromLogs( receipt.logs, [ @@ -35,7 +35,7 @@ blockchainTests.resets('Ownable feature', env => { ], IOwnableEvents.OwnershipTransferred, ); - expect(await auth.getOwner().callAsync()).to.eq(newOwner); + expect(await ownable.getOwner().callAsync()).to.eq(newOwner); }); }); }); diff --git a/contracts/zero-ex/test/features/simple_function_registry_test.ts b/contracts/zero-ex/test/features/simple_function_registry_test.ts index d18dd5be17..60670b03b8 100644 --- a/contracts/zero-ex/test/features/simple_function_registry_test.ts +++ b/contracts/zero-ex/test/features/simple_function_registry_test.ts @@ -2,7 +2,7 @@ import { blockchainTests, constants, expect, randomAddress, verifyEventsFromLogs import { BigNumber, hexUtils, OwnableRevertErrors, ZeroExRevertErrors } from '@0x/utils'; import { artifacts } from '../artifacts'; -import { basicMigrateAsync } from '../utils/migration'; +import { initialMigrateAsync } from '../utils/migration'; import { ISimpleFunctionRegistryContract, ISimpleFunctionRegistryEvents, @@ -25,7 +25,7 @@ blockchainTests.resets('SimpleFunctionRegistry feature', env => { before(async () => { [owner] = await env.getAccountAddressesAsync(); - zeroEx = await basicMigrateAsync(owner, env.provider, env.txDefaults); + zeroEx = await initialMigrateAsync(owner, env.provider, env.txDefaults); registry = new ISimpleFunctionRegistryContract(zeroEx.address, env.provider, { ...env.txDefaults, from: owner, diff --git a/contracts/zero-ex/test/initial_migration_test.ts b/contracts/zero-ex/test/initial_migration_test.ts new file mode 100644 index 0000000000..defbad5c20 --- /dev/null +++ b/contracts/zero-ex/test/initial_migration_test.ts @@ -0,0 +1,57 @@ +import { blockchainTests, expect } from '@0x/contracts-test-utils'; +import { ZeroExRevertErrors } from '@0x/utils'; + +import { artifacts } from './artifacts'; +import { IBootstrapContract, IOwnableContract, TestInitialMigrationContract, ZeroExContract } from './wrappers'; + +blockchainTests.resets('Initial migration', env => { + let owner: string; + let zeroEx: ZeroExContract; + let migrator: TestInitialMigrationContract; + let bootstrapFeature: IBootstrapContract; + + before(async () => { + [owner] = await env.getAccountAddressesAsync(); + migrator = await TestInitialMigrationContract.deployFrom0xArtifactAsync( + artifacts.TestInitialMigration, + env.provider, + env.txDefaults, + artifacts, + ); + bootstrapFeature = new IBootstrapContract( + await migrator.bootstrapFeature().callAsync(), + env.provider, + env.txDefaults, + {}, + ); + const deployCall = migrator.deploy(owner); + zeroEx = new ZeroExContract(await deployCall.callAsync(), env.provider, env.txDefaults); + await deployCall.awaitTransactionSuccessAsync(); + }); + + describe('bootstrapping', () => { + it('Migrator cannot call bootstrap() again', async () => { + const tx = migrator.callBootstrap(zeroEx.address).awaitTransactionSuccessAsync(); + const selector = bootstrapFeature.getSelector('bootstrap'); + return expect(tx).to.revertWith(new ZeroExRevertErrors.Proxy.NotImplementedError(selector)); + }); + }); + + describe('Ownable feature', () => { + let ownable: IOwnableContract; + + before(async () => { + ownable = new IOwnableContract(zeroEx.address, env.provider, env.txDefaults); + }); + + it('has the correct owner', async () => { + const actualOwner = await ownable.getOwner().callAsync(); + expect(actualOwner).to.eq(owner); + }); + }); + + it('bootstrap feature self destructs after deployment', async () => { + const codeSize = await migrator.getCodeSizeOf(bootstrapFeature.address).callAsync(); + expect(codeSize).to.bignumber.eq(0); + }); +}); diff --git a/contracts/zero-ex/test/utils/migration.ts b/contracts/zero-ex/test/utils/migration.ts index 8612b55b02..07578e1004 100644 --- a/contracts/zero-ex/test/utils/migration.ts +++ b/contracts/zero-ex/test/utils/migration.ts @@ -2,22 +2,22 @@ import { SupportedProvider } from '@0x/subproviders'; import { TxData } from 'ethereum-types'; import { artifacts } from '../artifacts'; -import { BasicMigrationContract, ZeroExContract } from '../wrappers'; +import { InitialMigrationContract, ZeroExContract } from '../wrappers'; // tslint:disable: completed-docs -export async function basicMigrateAsync( +export async function initialMigrateAsync( owner: string, provider: SupportedProvider, txDefaults: Partial, ): Promise { - const migrator = await BasicMigrationContract.deployFrom0xArtifactAsync( - artifacts.BasicMigration, + const migrator = await InitialMigrationContract.deployFrom0xArtifactAsync( + artifacts.InitialMigration, provider, txDefaults, artifacts, ); - const migrateCall = migrator.migrate(owner); - const zeroEx = new ZeroExContract(await migrateCall.callAsync(), provider, {}); - await migrateCall.awaitTransactionSuccessAsync(); + const deployCall = migrator.deploy(owner); + const zeroEx = new ZeroExContract(await deployCall.callAsync(), provider, {}); + await deployCall.awaitTransactionSuccessAsync(); return zeroEx; } diff --git a/contracts/zero-ex/test/wrappers.ts b/contracts/zero-ex/test/wrappers.ts index 9da5bfd0b8..64ec73da86 100644 --- a/contracts/zero-ex/test/wrappers.ts +++ b/contracts/zero-ex/test/wrappers.ts @@ -3,24 +3,32 @@ * Warning: This file is auto-generated by contracts-gen. Don't edit manually. * ----------------------------------------------------------------------------- */ -export * from '../test/generated-wrappers/basic_migration'; +export * from '../test/generated-wrappers/bootstrap'; export * from '../test/generated-wrappers/fixin_common'; export * from '../test/generated-wrappers/fixin_ownable'; +export * from '../test/generated-wrappers/i_bootstrap'; export * from '../test/generated-wrappers/i_feature'; +export * from '../test/generated-wrappers/i_migrate'; export * from '../test/generated-wrappers/i_ownable'; export * from '../test/generated-wrappers/i_simple_function_registry'; export * from '../test/generated-wrappers/i_test_simple_function_registry_feature'; -export * from '../test/generated-wrappers/i_zero_ex_bootstrapper'; +export * from '../test/generated-wrappers/initial_migration'; +export * from '../test/generated-wrappers/lib_bootstrap'; export * from '../test/generated-wrappers/lib_common_rich_errors'; +export * from '../test/generated-wrappers/lib_migrate'; +export * from '../test/generated-wrappers/lib_migrate_rich_errors'; +export * from '../test/generated-wrappers/lib_migrate_storage'; export * from '../test/generated-wrappers/lib_ownable_rich_errors'; export * from '../test/generated-wrappers/lib_ownable_storage'; export * from '../test/generated-wrappers/lib_proxy_rich_errors'; export * from '../test/generated-wrappers/lib_proxy_storage'; export * from '../test/generated-wrappers/lib_simple_function_registry_rich_errors'; export * from '../test/generated-wrappers/lib_simple_function_registry_storage'; +export * from '../test/generated-wrappers/migrate'; export * from '../test/generated-wrappers/ownable'; export * from '../test/generated-wrappers/simple_function_registry'; -export * from '../test/generated-wrappers/test_basic_migration'; +export * from '../test/generated-wrappers/test_initial_migration'; +export * from '../test/generated-wrappers/test_migrator'; export * from '../test/generated-wrappers/test_simple_function_registry_feature_impl1'; export * from '../test/generated-wrappers/test_simple_function_registry_feature_impl2'; export * from '../test/generated-wrappers/test_zero_ex_feature'; diff --git a/contracts/zero-ex/test/zero_ex_test.ts b/contracts/zero-ex/test/zero_ex_test.ts index 55e768d3d7..cd9ed8460c 100644 --- a/contracts/zero-ex/test/zero_ex_test.ts +++ b/contracts/zero-ex/test/zero_ex_test.ts @@ -2,7 +2,7 @@ import { blockchainTests, constants, expect, verifyEventsFromLogs } from '@0x/co import { BigNumber, ZeroExRevertErrors } from '@0x/utils'; import { artifacts } from './artifacts'; -import { basicMigrateAsync } from './utils/migration'; +import { initialMigrateAsync } from './utils/migration'; import { IFeatureContract, IOwnableContract, @@ -21,7 +21,7 @@ blockchainTests.resets('ZeroEx contract', env => { before(async () => { [owner] = await env.getAccountAddressesAsync(); - zeroEx = await basicMigrateAsync(owner, env.provider, env.txDefaults); + zeroEx = await initialMigrateAsync(owner, env.provider, env.txDefaults); ownable = new IOwnableContract(zeroEx.address, env.provider, env.txDefaults); registry = new ISimpleFunctionRegistryContract(zeroEx.address, env.provider, env.txDefaults); testFeature = new TestZeroExFeatureContract(zeroEx.address, env.provider, env.txDefaults); diff --git a/contracts/zero-ex/tsconfig.json b/contracts/zero-ex/tsconfig.json index f1364e47ae..64221788c1 100644 --- a/contracts/zero-ex/tsconfig.json +++ b/contracts/zero-ex/tsconfig.json @@ -3,27 +3,36 @@ "compilerOptions": { "outDir": "lib", "rootDir": ".", "resolveJsonModule": true }, "include": ["./src/**/*", "./test/**/*", "./generated-wrappers/**/*"], "files": [ + "generated-artifacts/IMigrate.json", "generated-artifacts/IOwnable.json", "generated-artifacts/ISimpleFunctionRegistry.json", "generated-artifacts/ZeroEx.json", - "test/generated-artifacts/BasicMigration.json", + "test/generated-artifacts/Bootstrap.json", "test/generated-artifacts/FixinCommon.json", "test/generated-artifacts/FixinOwnable.json", + "test/generated-artifacts/IBootstrap.json", "test/generated-artifacts/IFeature.json", + "test/generated-artifacts/IMigrate.json", "test/generated-artifacts/IOwnable.json", "test/generated-artifacts/ISimpleFunctionRegistry.json", "test/generated-artifacts/ITestSimpleFunctionRegistryFeature.json", - "test/generated-artifacts/IZeroExBootstrapper.json", + "test/generated-artifacts/InitialMigration.json", + "test/generated-artifacts/LibBootstrap.json", "test/generated-artifacts/LibCommonRichErrors.json", + "test/generated-artifacts/LibMigrate.json", + "test/generated-artifacts/LibMigrateRichErrors.json", + "test/generated-artifacts/LibMigrateStorage.json", "test/generated-artifacts/LibOwnableRichErrors.json", "test/generated-artifacts/LibOwnableStorage.json", "test/generated-artifacts/LibProxyRichErrors.json", "test/generated-artifacts/LibProxyStorage.json", "test/generated-artifacts/LibSimpleFunctionRegistryRichErrors.json", "test/generated-artifacts/LibSimpleFunctionRegistryStorage.json", + "test/generated-artifacts/Migrate.json", "test/generated-artifacts/Ownable.json", "test/generated-artifacts/SimpleFunctionRegistry.json", - "test/generated-artifacts/TestBasicMigration.json", + "test/generated-artifacts/TestInitialMigration.json", + "test/generated-artifacts/TestMigrator.json", "test/generated-artifacts/TestSimpleFunctionRegistryFeatureImpl1.json", "test/generated-artifacts/TestSimpleFunctionRegistryFeatureImpl2.json", "test/generated-artifacts/TestZeroExFeature.json",