From 297ff10c14ca296143110eb541c071bcdb1078c2 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Thu, 18 Jun 2020 09:49:35 -0400 Subject: [PATCH] `@0x/contracts-zero-ex`: add `SignatureValidator` and `MetaTransactions` features. --- contracts/zero-ex/CHANGELOG.json | 4 + contracts/zero-ex/contracts/src/ZeroEx.sol | 7 +- .../errors/LibMetaTransactionsRichErrors.sol | 174 ++++++ .../src/errors/LibSignatureRichErrors.sol | 52 ++ .../src/features/IMetaTransactions.sol | 127 +++++ .../src/features/ISignatureValidator.sol | 63 +++ .../src/features/MetaTransactions.sol | 412 ++++++++++++++ .../contracts/src/features/Ownable.sol | 8 +- .../src/features/SignatureValidator.sol | 260 +++++++++ .../src/features/SimpleFunctionRegistry.sol | 8 +- .../contracts/src/features/TokenSpender.sol | 20 +- .../contracts/src/features/TransformERC20.sol | 32 +- .../contracts/src/fixins/FixinCommon.sol | 21 +- .../contracts/src/fixins/FixinEIP712.sol | 69 +++ .../src/migrations/FullMigration.sol | 48 +- .../src/migrations/InitialMigration.sol | 17 +- .../storage/LibMetaTransactionsStorage.sol | 44 ++ .../contracts/src/storage/LibStorage.sol | 3 +- .../src/storage/LibTransformERC20Storage.sol | 2 +- ...tMetaTransactionsTransformERC20Feature.sol | 71 +++ contracts/zero-ex/package.json | 4 +- contracts/zero-ex/src/artifacts.ts | 4 + contracts/zero-ex/src/migration.ts | 95 +++- contracts/zero-ex/src/wrappers.ts | 2 + contracts/zero-ex/test/artifacts.ts | 18 + .../test/features/meta_transactions_test.ts | 518 ++++++++++++++++++ .../test/features/signature_validator_test.ts | 232 ++++++++ .../test/features/token_spender_test.ts | 4 +- .../test/features/transform_erc20_test.ts | 4 +- contracts/zero-ex/test/full_migration_test.ts | 34 +- .../zero-ex/test/initial_migration_test.ts | 15 +- contracts/zero-ex/test/utils/migration.ts | 1 - contracts/zero-ex/test/wrappers.ts | 9 + contracts/zero-ex/tsconfig.json | 11 + packages/order-utils/src/eip712_utils.ts | 5 +- 35 files changed, 2297 insertions(+), 101 deletions(-) create mode 100644 contracts/zero-ex/contracts/src/errors/LibMetaTransactionsRichErrors.sol create mode 100644 contracts/zero-ex/contracts/src/errors/LibSignatureRichErrors.sol create mode 100644 contracts/zero-ex/contracts/src/features/IMetaTransactions.sol create mode 100644 contracts/zero-ex/contracts/src/features/ISignatureValidator.sol create mode 100644 contracts/zero-ex/contracts/src/features/MetaTransactions.sol create mode 100644 contracts/zero-ex/contracts/src/features/SignatureValidator.sol create mode 100644 contracts/zero-ex/contracts/src/fixins/FixinEIP712.sol create mode 100644 contracts/zero-ex/contracts/src/storage/LibMetaTransactionsStorage.sol create mode 100644 contracts/zero-ex/contracts/test/TestMetaTransactionsTransformERC20Feature.sol create mode 100644 contracts/zero-ex/test/features/meta_transactions_test.ts create mode 100644 contracts/zero-ex/test/features/signature_validator_test.ts diff --git a/contracts/zero-ex/CHANGELOG.json b/contracts/zero-ex/CHANGELOG.json index d567edd0fc..c02207c35a 100644 --- a/contracts/zero-ex/CHANGELOG.json +++ b/contracts/zero-ex/CHANGELOG.json @@ -9,6 +9,10 @@ { "note": "Export `AffiliateFeeTransformerContract`", "pr": 2622 + }, + { + "note": "Add `MetaTransactions` and `SignatureValidator` features", + "pr": 2610 } ] }, diff --git a/contracts/zero-ex/contracts/src/ZeroEx.sol b/contracts/zero-ex/contracts/src/ZeroEx.sol index 2e9b0f543b..16a1b9fcd0 100644 --- a/contracts/zero-ex/contracts/src/ZeroEx.sol +++ b/contracts/zero-ex/contracts/src/ZeroEx.sol @@ -34,11 +34,12 @@ contract ZeroEx { /// @dev Construct this contract and register the `Bootstrap` feature. /// After constructing this contract, `bootstrap()` should be called - /// to seed the initial feature set. - constructor() public { + /// by `bootstrap()` to seed the initial feature set. + /// @param bootstrapper Who can call `bootstrap()`. + 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); + Bootstrap bootstrap = new Bootstrap(bootstrapper); LibProxyStorage.getStorage().impls[bootstrap.bootstrap.selector] = address(bootstrap); } diff --git a/contracts/zero-ex/contracts/src/errors/LibMetaTransactionsRichErrors.sol b/contracts/zero-ex/contracts/src/errors/LibMetaTransactionsRichErrors.sol new file mode 100644 index 0000000000..6a8beb5016 --- /dev/null +++ b/contracts/zero-ex/contracts/src/errors/LibMetaTransactionsRichErrors.sol @@ -0,0 +1,174 @@ +/* + + 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; + + +library LibMetaTransactionsRichErrors { + + // solhint-disable func-name-mixedcase + + function InvalidMetaTransactionsArrayLengthsError( + uint256 mtxCount, + uint256 signatureCount + ) + internal + pure + returns (bytes memory) + { + return abi.encodeWithSelector( + bytes4(keccak256("InvalidMetaTransactionsArrayLengthsError(uint256,uint256)")), + mtxCount, + signatureCount + ); + } + + function MetaTransactionUnsupportedFunctionError( + bytes32 mtxHash, + bytes4 selector + ) + internal + pure + returns (bytes memory) + { + return abi.encodeWithSelector( + bytes4(keccak256("MetaTransactionUnsupportedFunctionError(bytes32,bytes4)")), + mtxHash, + selector + ); + } + + function MetaTransactionWrongSenderError( + bytes32 mtxHash, + address sender, + address expectedSender + ) + internal + pure + returns (bytes memory) + { + return abi.encodeWithSelector( + bytes4(keccak256("MetaTransactionWrongSenderError(bytes32,address,address)")), + mtxHash, + sender, + expectedSender + ); + } + + function MetaTransactionExpiredError( + bytes32 mtxHash, + uint256 time, + uint256 expirationTime + ) + internal + pure + returns (bytes memory) + { + return abi.encodeWithSelector( + bytes4(keccak256("MetaTransactionExpiredError(bytes32,uint256,uint256)")), + mtxHash, + time, + expirationTime + ); + } + + function MetaTransactionGasPriceError( + bytes32 mtxHash, + uint256 gasPrice, + uint256 minGasPrice, + uint256 maxGasPrice + ) + internal + pure + returns (bytes memory) + { + return abi.encodeWithSelector( + bytes4(keccak256("MetaTransactionGasPriceError(bytes32,uint256,uint256,uint256)")), + mtxHash, + gasPrice, + minGasPrice, + maxGasPrice + ); + } + + function MetaTransactionInsufficientEthError( + bytes32 mtxHash, + uint256 ethBalance, + uint256 ethRequired + ) + internal + pure + returns (bytes memory) + { + return abi.encodeWithSelector( + bytes4(keccak256("MetaTransactionInsufficientEthError(bytes32,uint256,uint256)")), + mtxHash, + ethBalance, + ethRequired + ); + } + + function MetaTransactionInvalidSignatureError( + bytes32 mtxHash, + bytes memory signature, + bytes memory errData + ) + internal + pure + returns (bytes memory) + { + return abi.encodeWithSelector( + bytes4(keccak256("MetaTransactionInvalidSignatureError(bytes32,bytes,bytes)")), + mtxHash, + signature, + errData + ); + } + + function MetaTransactionAlreadyExecutedError( + bytes32 mtxHash, + uint256 executedBlockNumber + ) + internal + pure + returns (bytes memory) + { + return abi.encodeWithSelector( + bytes4(keccak256("MetaTransactionAlreadyExecutedError(bytes32,uint256)")), + mtxHash, + executedBlockNumber + ); + } + + function MetaTransactionCallFailedError( + bytes32 mtxHash, + bytes memory callData, + bytes memory returnData + ) + internal + pure + returns (bytes memory) + { + return abi.encodeWithSelector( + bytes4(keccak256("MetaTransactionCallFailedError(bytes32,bytes,bytes)")), + mtxHash, + callData, + returnData + ); + } +} diff --git a/contracts/zero-ex/contracts/src/errors/LibSignatureRichErrors.sol b/contracts/zero-ex/contracts/src/errors/LibSignatureRichErrors.sol new file mode 100644 index 0000000000..6dcc9e41a4 --- /dev/null +++ b/contracts/zero-ex/contracts/src/errors/LibSignatureRichErrors.sol @@ -0,0 +1,52 @@ +/* + + 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; + + +library LibSignatureRichErrors { + + enum SignatureValidationErrorCodes { + ALWAYS_INVALID, + INVALID_LENGTH, + UNSUPPORTED, + ILLEGAL, + WRONG_SIGNER + } + + // solhint-disable func-name-mixedcase + + function SignatureValidationError( + SignatureValidationErrorCodes code, + bytes32 hash, + address signerAddress, + bytes memory signature + ) + internal + pure + returns (bytes memory) + { + return abi.encodeWithSelector( + bytes4(keccak256("SignatureValidationError(uint8,bytes32,address,bytes)")), + code, + hash, + signerAddress, + signature + ); + } +} diff --git a/contracts/zero-ex/contracts/src/features/IMetaTransactions.sol b/contracts/zero-ex/contracts/src/features/IMetaTransactions.sol new file mode 100644 index 0000000000..1052d048cc --- /dev/null +++ b/contracts/zero-ex/contracts/src/features/IMetaTransactions.sol @@ -0,0 +1,127 @@ +/* + + 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-erc20/contracts/src/v06/IERC20TokenV06.sol"; + + +/// @dev Meta-transactions feature. +interface IMetaTransactions { + + /// @dev Describes an exchange proxy meta transaction. + struct MetaTransactionData { + // Signer of meta-transaction. On whose behalf to execute the MTX. + address signer; + // Required sender, or NULL for anyone. + address sender; + // Minimum gas price. + uint256 minGasPrice; + // Maximum gas price. + uint256 maxGasPrice; + // MTX is invalid after this time. + uint256 expirationTime; + // Nonce to make this MTX unique. + uint256 salt; + // Encoded call data to a function on the exchange proxy. + bytes callData; + // Amount of ETH to attach to the call. + uint256 value; + // ERC20 fee `signer` pays `sender`. + IERC20TokenV06 feeToken; + // ERC20 fee amount. + uint256 feeAmount; + } + + /// @dev Emitted whenever a meta-transaction is executed via + /// `executeMetaTransaction()` or `executeMetaTransactions()`. + /// @param hash The meta-transaction hash. + /// @param selector The selector of the function being executed. + /// @param signer Who to execute the meta-transaction on behalf of. + /// @param sender Who executed the meta-transaction. + event MetaTransactionExecuted( + bytes32 hash, + bytes4 indexed selector, + address signer, + address sender + ); + + /// @dev Execute a single meta-transaction. + /// @param mtx The meta-transaction. + /// @param signature The signature by `mtx.signer`. + /// @return returnData The ABI-encoded result of the underlying call. + function executeMetaTransaction( + MetaTransactionData calldata mtx, + bytes calldata signature + ) + external + payable + returns (bytes memory returnData); + + /// @dev Execute multiple meta-transactions. + /// @param mtxs The meta-transactions. + /// @param signatures The signature by each respective `mtx.signer`. + /// @return returnDatas The ABI-encoded results of the underlying calls. + function executeMetaTransactions( + MetaTransactionData[] calldata mtxs, + bytes[] calldata signatures + ) + external + payable + returns (bytes[] memory returnDatas); + + /// @dev Execute a meta-transaction via `sender`. Privileged variant. + /// Only callable from within. + /// @param sender Who is executing the meta-transaction.. + /// @param mtx The meta-transaction. + /// @param signature The signature by `mtx.signer`. + /// @return returnData The ABI-encoded result of the underlying call. + function _executeMetaTransaction( + address sender, + MetaTransactionData calldata mtx, + bytes calldata signature + ) + external + payable + returns (bytes memory returnData); + + /// @dev Get the block at which a meta-transaction has been executed. + /// @param mtx The meta-transaction. + /// @return blockNumber The block height when the meta-transactioin was executed. + function getMetaTransactionExecutedBlock(MetaTransactionData calldata mtx) + external + view + returns (uint256 blockNumber); + + /// @dev Get the block at which a meta-transaction hash has been executed. + /// @param mtxHash The meta-transaction hash. + /// @return blockNumber The block height when the meta-transactioin was executed. + function getMetaTransactionHashExecutedBlock(bytes32 mtxHash) + external + view + returns (uint256 blockNumber); + + /// @dev Get the EIP712 hash of a meta-transaction. + /// @param mtx The meta-transaction. + /// @return mtxHash The EIP712 hash of `mtx`. + function getMetaTransactionHash(MetaTransactionData calldata mtx) + external + view + returns (bytes32 mtxHash); +} diff --git a/contracts/zero-ex/contracts/src/features/ISignatureValidator.sol b/contracts/zero-ex/contracts/src/features/ISignatureValidator.sol new file mode 100644 index 0000000000..121e27c033 --- /dev/null +++ b/contracts/zero-ex/contracts/src/features/ISignatureValidator.sol @@ -0,0 +1,63 @@ +/* + + 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 Feature for validating signatures. +interface ISignatureValidator { + + /// @dev Allowed signature types. + enum SignatureType { + Illegal, // 0x00, default value + Invalid, // 0x01 + EIP712, // 0x02 + EthSign, // 0x03 + NSignatureTypes // 0x04, number of signature types. Always leave at end. + } + + /// @dev Validate that `hash` was signed by `signer` given `signature`. + /// Reverts otherwise. + /// @param hash The hash that was signed. + /// @param signer The signer of the hash. + /// @param signature The signature. The last byte of this signature should + /// be a member of the `SignatureType` enum. + function validateHashSignature( + bytes32 hash, + address signer, + bytes calldata signature + ) + external + view; + + /// @dev Check that `hash` was signed by `signer` given `signature`. + /// @param hash The hash that was signed. + /// @param signer The signer of the hash. + /// @param signature The signature. The last byte of this signature should + /// be a member of the `SignatureType` enum. + /// @return isValid `true` on success. + function isValidHashSignature( + bytes32 hash, + address signer, + bytes calldata signature + ) + external + view + returns (bool isValid); +} diff --git a/contracts/zero-ex/contracts/src/features/MetaTransactions.sol b/contracts/zero-ex/contracts/src/features/MetaTransactions.sol new file mode 100644 index 0000000000..856b1831a8 --- /dev/null +++ b/contracts/zero-ex/contracts/src/features/MetaTransactions.sol @@ -0,0 +1,412 @@ +/* + + 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 "@0x/contracts-utils/contracts/src/v06/LibBytesV06.sol"; +import "../errors/LibMetaTransactionsRichErrors.sol"; +import "../fixins/FixinCommon.sol"; +import "../fixins/FixinEIP712.sol"; +import "../migrations/LibMigrate.sol"; +import "../storage/LibMetaTransactionsStorage.sol"; +import "./IMetaTransactions.sol"; +import "./ITransformERC20.sol"; +import "./ISignatureValidator.sol"; +import "./ITokenSpender.sol"; +import "./IFeature.sol"; + + +/// @dev MetaTransactions feature. +contract MetaTransactions is + IFeature, + IMetaTransactions, + FixinCommon, + FixinEIP712 +{ + using LibBytesV06 for bytes; + using LibRichErrorsV06 for bytes; + + /// @dev Intermediate state vars to avoid stack overflows. + struct ExecuteState { + address sender; + bytes32 hash; + MetaTransactionData mtx; + bytes signature; + bytes4 selector; + uint256 selfBalance; + uint256 executedBlockNumber; + } + + struct TransformERC20Args { + IERC20TokenV06 inputToken; + IERC20TokenV06 outputToken; + uint256 inputTokenAmount; + uint256 minOutputTokenAmount; + ITransformERC20.Transformation[] transformations; + } + + /// @dev Name of this feature. + string public constant override FEATURE_NAME = "MetaTransactions"; + /// @dev Version of this feature. + uint256 public immutable override FEATURE_VERSION = _encodeVersion(1, 0, 0); + /// @dev EIP712 typehash of the `MetaTransactionData` struct. + bytes32 public immutable MTX_EIP712_TYPEHASH = keccak256( + "MetaTransactionData(" + "address signer," + "address sender," + "uint256 minGasPrice," + "uint256 maxGasPrice," + "uint256 expirationTime," + "uint256 salt," + "bytes callData," + "uint256 value," + "address feeToken," + "uint256 feeAmount" + ")" + ); + + constructor(address zeroExAddress) + public + FixinCommon() + FixinEIP712(zeroExAddress) + { + // solhint-disable-next-line no-empty-blocks + } + + /// @dev Initialize and register this feature. + /// Should be delegatecalled by `Migrate.migrate()`. + /// @return success `LibMigrate.SUCCESS` on success. + function migrate() + external + returns (bytes4 success) + { + _registerFeatureFunction(this.executeMetaTransaction.selector); + _registerFeatureFunction(this.executeMetaTransactions.selector); + _registerFeatureFunction(this._executeMetaTransaction.selector); + _registerFeatureFunction(this.getMetaTransactionExecutedBlock.selector); + _registerFeatureFunction(this.getMetaTransactionHashExecutedBlock.selector); + _registerFeatureFunction(this.getMetaTransactionHash.selector); + return LibMigrate.MIGRATE_SUCCESS; + } + + /// @dev Execute a single meta-transaction. + /// @param mtx The meta-transaction. + /// @param signature The signature by `mtx.signer`. + /// @return returnData The ABI-encoded result of the underlying call. + function executeMetaTransaction( + MetaTransactionData memory mtx, + bytes memory signature + ) + public + payable + override + returns (bytes memory returnData) + { + return _executeMetaTransactionPrivate( + msg.sender, + mtx, + signature + ); + } + + /// @dev Execute multiple meta-transactions. + /// @param mtxs The meta-transactions. + /// @param signatures The signature by each respective `mtx.signer`. + /// @return returnDatas The ABI-encoded results of the underlying calls. + function executeMetaTransactions( + MetaTransactionData[] memory mtxs, + bytes[] memory signatures + ) + public + payable + override + returns (bytes[] memory returnDatas) + { + if (mtxs.length != signatures.length) { + LibMetaTransactionsRichErrors.InvalidMetaTransactionsArrayLengthsError( + mtxs.length, + signatures.length + ).rrevert(); + } + returnDatas = new bytes[](mtxs.length); + for (uint256 i = 0; i < mtxs.length; ++i) { + returnDatas[i] = _executeMetaTransactionPrivate( + msg.sender, + mtxs[i], + signatures[i] + ); + } + } + + /// @dev Execute a meta-transaction via `sender`. Privileged variant. + /// Only callable from within. + /// @param sender Who is executing the meta-transaction.. + /// @param mtx The meta-transaction. + /// @param signature The signature by `mtx.signer`. + /// @return returnData The ABI-encoded result of the underlying call. + function _executeMetaTransaction( + address sender, + MetaTransactionData memory mtx, + bytes memory signature + ) + public + payable + override + onlySelf + returns (bytes memory returnData) + { + return _executeMetaTransactionPrivate(sender, mtx, signature); + } + + /// @dev Get the block at which a meta-transaction has been executed. + /// @param mtx The meta-transaction. + /// @return blockNumber The block height when the meta-transactioin was executed. + function getMetaTransactionExecutedBlock(MetaTransactionData memory mtx) + public + override + view + returns (uint256 blockNumber) + { + return getMetaTransactionHashExecutedBlock(getMetaTransactionHash(mtx)); + } + + /// @dev Get the block at which a meta-transaction hash has been executed. + /// @param mtxHash The meta-transaction hash. + /// @return blockNumber The block height when the meta-transactioin was executed. + function getMetaTransactionHashExecutedBlock(bytes32 mtxHash) + public + override + view + returns (uint256 blockNumber) + { + return LibMetaTransactionsStorage.getStorage().mtxHashToExecutedBlockNumber[mtxHash]; + } + + /// @dev Get the EIP712 hash of a meta-transaction. + /// @param mtx The meta-transaction. + /// @return mtxHash The EIP712 hash of `mtx`. + function getMetaTransactionHash(MetaTransactionData memory mtx) + public + override + view + returns (bytes32 mtxHash) + { + return _getEIP712Hash(keccak256(abi.encode( + MTX_EIP712_TYPEHASH, + mtx.signer, + mtx.sender, + mtx.minGasPrice, + mtx.maxGasPrice, + mtx.expirationTime, + mtx.salt, + keccak256(mtx.callData), + mtx.value, + mtx.feeToken, + mtx.feeAmount + ))); + } + + /// @dev Execute a meta-transaction by `sender`. Low-level, hidden variant. + /// @param sender Who is executing the meta-transaction.. + /// @param mtx The meta-transaction. + /// @param signature The signature by `mtx.signer`. + /// @return returnData The ABI-encoded result of the underlying call. + function _executeMetaTransactionPrivate( + address sender, + MetaTransactionData memory mtx, + bytes memory signature + ) + private + returns (bytes memory returnData) + { + ExecuteState memory state; + state.sender = sender; + state.hash = getMetaTransactionHash(mtx); + state.mtx = mtx; + state.signature = signature; + + _validateMetaTransaction(state); + + // Mark the transaction executed. + assert(block.number > 0); + LibMetaTransactionsStorage.getStorage() + .mtxHashToExecutedBlockNumber[state.hash] = block.number; + + // Execute the call based on the selector. + state.selector = mtx.callData.readBytes4(0); + if (state.selector == ITransformERC20.transformERC20.selector) { + returnData = _executeTransformERC20Call(state); + } else { + LibMetaTransactionsRichErrors + .MetaTransactionUnsupportedFunctionError(state.hash, state.selector) + .rrevert(); + } + // Pay the fee to the sender. + if (mtx.feeAmount > 0) { + ITokenSpender(address(this))._spendERC20Tokens( + mtx.feeToken, + mtx.signer, // From the signer. + sender, // To the sender. + mtx.feeAmount + ); + } + emit MetaTransactionExecuted( + state.hash, + state.selector, + mtx.signer, + mtx.sender + ); + } + + /// @dev Validate that a meta-transaction is executable. + function _validateMetaTransaction(ExecuteState memory state) + private + view + { + // Must be from the required sender, if set. + if (state.mtx.sender != address(0) && state.mtx.sender != state.sender) { + LibMetaTransactionsRichErrors + .MetaTransactionWrongSenderError( + state.hash, + state.sender, + state.mtx.sender + ).rrevert(); + } + // Must not be expired. + if (state.mtx.expirationTime <= block.timestamp) { + LibMetaTransactionsRichErrors + .MetaTransactionExpiredError( + state.hash, + block.timestamp, + state.mtx.expirationTime + ).rrevert(); + } + // Must have a valid gas price. + if (state.mtx.minGasPrice > tx.gasprice || state.mtx.maxGasPrice < tx.gasprice) { + LibMetaTransactionsRichErrors + .MetaTransactionGasPriceError( + state.hash, + tx.gasprice, + state.mtx.minGasPrice, + state.mtx.maxGasPrice + ).rrevert(); + } + // Must have enough ETH. + state.selfBalance = address(this).balance; + if (state.mtx.value > state.selfBalance) { + LibMetaTransactionsRichErrors + .MetaTransactionInsufficientEthError( + state.hash, + state.selfBalance, + state.mtx.value + ).rrevert(); + } + // Must be signed by signer. + try + ISignatureValidator(address(this)) + .validateHashSignature(state.hash, state.mtx.signer, state.signature) + {} + catch (bytes memory err) { + LibMetaTransactionsRichErrors + .MetaTransactionInvalidSignatureError( + state.hash, + state.signature, + err + ).rrevert(); + } + // Transaction must not have been already executed. + state.executedBlockNumber = LibMetaTransactionsStorage + .getStorage().mtxHashToExecutedBlockNumber[state.hash]; + if (state.executedBlockNumber != 0) { + LibMetaTransactionsRichErrors + .MetaTransactionAlreadyExecutedError( + state.hash, + state.executedBlockNumber + ).rrevert(); + } + } + + /// @dev Execute a `ITransformERC20.transformERC20()` meta-transaction call + /// by decoding the call args and translating the call to the internal + /// `ITransformERC20._transformERC20()` variant, where we can override + /// the taker address. + function _executeTransformERC20Call(ExecuteState memory state) + private + returns (bytes memory returnData) + { + // HACK(dorothy-zbornak): `abi.decode()` with the individual args + // will cause a stack overflow. But we can prefix the call data with an + // offset to transform it into the encoding for the equivalent single struct arg. + // Decoding a single struct consumes far less stack space. + TransformERC20Args memory args; + { + bytes memory encodedStructArgs = new bytes(state.mtx.callData.length - 4 + 32); + // Copy the args data from the original, after the new struct offset prefix. + bytes memory fromCallData = state.mtx.callData; + assert(fromCallData.length >= 4); + uint256 fromMem; + uint256 toMem; + assembly { + // Prefix the original calldata with a struct offset, + // which is just one word over. + mstore(add(encodedStructArgs, 32), 32) + // Copy everything after the selector. + fromMem := add(fromCallData, 36) + // Start copying after the struct offset. + toMem := add(encodedStructArgs, 64) + } + LibBytesV06.memCopy(toMem, fromMem, fromCallData.length - 4); + // Decode call args for `ITransformERC20.transformERC20()` as a struct. + args = abi.decode(encodedStructArgs, (TransformERC20Args)); + } + // Call `ITransformERC20._transformERC20()` (internal variant). + return _callSelf( + state.hash, + abi.encodeWithSelector( + ITransformERC20._transformERC20.selector, + keccak256(state.mtx.callData), + state.mtx.signer, // taker is mtx signer + args.inputToken, + args.outputToken, + args.inputTokenAmount, + args.minOutputTokenAmount, + args.transformations + ), + state.mtx.value + ); + } + + /// @dev Make an arbitrary internal, meta-transaction call. + /// Warning: Do not let unadulerated `callData` into this function. + function _callSelf(bytes32 hash, bytes memory callData, uint256 value) + private + returns (bytes memory returnData) + { + bool success; + (success, returnData) = address(this).call{value: value}(callData); + if (!success) { + LibMetaTransactionsRichErrors.MetaTransactionCallFailedError( + hash, + callData, + returnData + ).rrevert(); + } + } +} diff --git a/contracts/zero-ex/contracts/src/features/Ownable.sol b/contracts/zero-ex/contracts/src/features/Ownable.sol index 230200d51f..e6ad228178 100644 --- a/contracts/zero-ex/contracts/src/features/Ownable.sol +++ b/contracts/zero-ex/contracts/src/features/Ownable.sol @@ -37,19 +37,15 @@ contract Ownable is FixinCommon { - // solhint-disable /// @dev Name of this feature. string public constant override FEATURE_NAME = "Ownable"; /// @dev Version of this feature. uint256 public immutable override FEATURE_VERSION = _encodeVersion(1, 0, 0); - /// @dev The deployed address of this contract. - address immutable private _implementation; - // solhint-enable using LibRichErrorsV06 for bytes; - constructor() public { - _implementation = address(this); + constructor() public FixinCommon() { + // solhint-disable-next-line no-empty-blocks } /// @dev Initializes this feature. The intial owner will be set to this (ZeroEx) diff --git a/contracts/zero-ex/contracts/src/features/SignatureValidator.sol b/contracts/zero-ex/contracts/src/features/SignatureValidator.sol new file mode 100644 index 0000000000..27a8251868 --- /dev/null +++ b/contracts/zero-ex/contracts/src/features/SignatureValidator.sol @@ -0,0 +1,260 @@ +/* + + 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 "@0x/contracts-utils/contracts/src/v06/LibBytesV06.sol"; +import "../errors/LibSignatureRichErrors.sol"; +import "../fixins/FixinCommon.sol"; +import "../migrations/LibMigrate.sol"; +import "./ISignatureValidator.sol"; +import "./IFeature.sol"; + + +/// @dev Feature for validating signatures. +contract SignatureValidator is + IFeature, + ISignatureValidator, + FixinCommon +{ + using LibBytesV06 for bytes; + using LibRichErrorsV06 for bytes; + + /// @dev Name of this feature. + string public constant override FEATURE_NAME = "SignatureValidator"; + /// @dev Version of this feature. + uint256 public immutable override FEATURE_VERSION = _encodeVersion(1, 0, 0); + + constructor() public FixinCommon() { + // solhint-disable-next-line no-empty-blocks + } + + /// @dev Initialize and register this feature. + /// Should be delegatecalled by `Migrate.migrate()`. + /// @return success `LibMigrate.SUCCESS` on success. + function migrate() + external + returns (bytes4 success) + { + _registerFeatureFunction(this.validateHashSignature.selector); + _registerFeatureFunction(this.isValidHashSignature.selector); + return LibMigrate.MIGRATE_SUCCESS; + } + + /// @dev Validate that `hash` was signed by `signer` given `signature`. + /// Reverts otherwise. + /// @param hash The hash that was signed. + /// @param signer The signer of the hash. + /// @param signature The signature. The last byte of this signature should + /// be a member of the `SignatureType` enum. + function validateHashSignature( + bytes32 hash, + address signer, + bytes memory signature + ) + public + override + view + { + SignatureType signatureType = _readValidSignatureType( + hash, + signer, + signature + ); + + // TODO: When we support non-hash signature types, assert that + // `signatureType` is only `EIP712` or `EthSign` here. + + _validateHashSignatureTypes( + signatureType, + hash, + signer, + signature + ); + } + + /// @dev Check that `hash` was signed by `signer` given `signature`. + /// @param hash The hash that was signed. + /// @param signer The signer of the hash. + /// @param signature The signature. The last byte of this signature should + /// be a member of the `SignatureType` enum. + /// @return isValid `true` on success. + function isValidHashSignature( + bytes32 hash, + address signer, + bytes calldata signature + ) + external + view + override + returns (bool isValid) + { + try this.validateHashSignature(hash, signer, signature) { + isValid = true; + } catch (bytes memory) { + isValid = false; + } + } + + /// @dev Validates a hash-only signature type. Low-level, hidden variant. + /// @param signatureType The type of signature to check. + /// @param hash The hash that was signed. + /// @param signer The signer of the hash. + /// @param signature The signature. The last byte of this signature should + /// be a member of the `SignatureType` enum. + function _validateHashSignatureTypes( + SignatureType signatureType, + bytes32 hash, + address signer, + bytes memory signature + ) + private + pure + { + address recovered = address(0); + if (signatureType == SignatureType.Invalid) { + // Always invalid signature. + // Like Illegal, this is always implicitly available and therefore + // offered explicitly. It can be implicitly created by providing + // a correctly formatted but incorrect signature. + LibSignatureRichErrors.SignatureValidationError( + LibSignatureRichErrors.SignatureValidationErrorCodes.ALWAYS_INVALID, + hash, + signer, + signature + ).rrevert(); + } else if (signatureType == SignatureType.EIP712) { + // Signature using EIP712 + if (signature.length != 66) { + LibSignatureRichErrors.SignatureValidationError( + LibSignatureRichErrors.SignatureValidationErrorCodes.INVALID_LENGTH, + hash, + signer, + signature + ).rrevert(); + } + uint8 v = uint8(signature[0]); + bytes32 r = signature.readBytes32(1); + bytes32 s = signature.readBytes32(33); + recovered = ecrecover( + hash, + v, + r, + s + ); + } else if (signatureType == SignatureType.EthSign) { + // Signed using `eth_sign` + if (signature.length != 66) { + LibSignatureRichErrors.SignatureValidationError( + LibSignatureRichErrors.SignatureValidationErrorCodes.INVALID_LENGTH, + hash, + signer, + signature + ).rrevert(); + } + uint8 v = uint8(signature[0]); + bytes32 r = signature.readBytes32(1); + bytes32 s = signature.readBytes32(33); + recovered = ecrecover( + keccak256(abi.encodePacked( + "\x19Ethereum Signed Message:\n32", + hash + )), + v, + r, + s + ); + } else { + // This should never happen. + revert('SignatureValidator/ILLEGAL_CODE_PATH'); + } + if (recovered == address(0) || signer != recovered) { + LibSignatureRichErrors.SignatureValidationError( + LibSignatureRichErrors.SignatureValidationErrorCodes.WRONG_SIGNER, + hash, + signer, + signature + ).rrevert(); + } + } + + /// @dev Reads the `SignatureType` from the end of a signature and validates it. + function _readValidSignatureType( + bytes32 hash, + address signer, + bytes memory signature + ) + private + pure + returns (SignatureType signatureType) + { + // Read the signatureType from the signature + signatureType = _readSignatureType( + hash, + signer, + signature + ); + + // Ensure signature is supported + if (uint8(signatureType) >= uint8(SignatureType.NSignatureTypes)) { + LibSignatureRichErrors.SignatureValidationError( + LibSignatureRichErrors.SignatureValidationErrorCodes.UNSUPPORTED, + hash, + signer, + signature + ).rrevert(); + } + + // Always illegal signature. + // This is always an implicit option since a signer can create a + // signature array with invalid type or length. We may as well make + // it an explicit option. This aids testing and analysis. It is + // also the initialization value for the enum type. + if (signatureType == SignatureType.Illegal) { + LibSignatureRichErrors.SignatureValidationError( + LibSignatureRichErrors.SignatureValidationErrorCodes.ILLEGAL, + hash, + signer, + signature + ).rrevert(); + } + } + + /// @dev Reads the `SignatureType` from the end of a signature. + function _readSignatureType( + bytes32 hash, + address signer, + bytes memory signature + ) + private + pure + returns (SignatureType sigType) + { + if (signature.length == 0) { + LibSignatureRichErrors.SignatureValidationError( + LibSignatureRichErrors.SignatureValidationErrorCodes.INVALID_LENGTH, + hash, + signer, + signature + ).rrevert(); + } + return SignatureType(uint8(signature[signature.length - 1])); + } +} diff --git a/contracts/zero-ex/contracts/src/features/SimpleFunctionRegistry.sol b/contracts/zero-ex/contracts/src/features/SimpleFunctionRegistry.sol index 283dc5ea5c..2979a945d0 100644 --- a/contracts/zero-ex/contracts/src/features/SimpleFunctionRegistry.sol +++ b/contracts/zero-ex/contracts/src/features/SimpleFunctionRegistry.sol @@ -36,18 +36,14 @@ contract SimpleFunctionRegistry is FixinCommon { // solhint-disable - /// @dev Name of this feature. string public constant override FEATURE_NAME = "SimpleFunctionRegistry"; /// @dev Version of this feature. uint256 public immutable override FEATURE_VERSION = _encodeVersion(1, 0, 0); - /// @dev The deployed address of this contract. - address private immutable _implementation; - // solhint-enable using LibRichErrorsV06 for bytes; - constructor() public { - _implementation = address(this); + constructor() public FixinCommon() { + // solhint-disable-next-line no-empty-blocks } /// @dev Initializes this feature, registering its own functions. diff --git a/contracts/zero-ex/contracts/src/features/TokenSpender.sol b/contracts/zero-ex/contracts/src/features/TokenSpender.sol index 7814ea396a..509dd24b95 100644 --- a/contracts/zero-ex/contracts/src/features/TokenSpender.sol +++ b/contracts/zero-ex/contracts/src/features/TokenSpender.sol @@ -44,14 +44,12 @@ contract TokenSpender is string public constant override FEATURE_NAME = "TokenSpender"; /// @dev Version of this feature. uint256 public immutable override FEATURE_VERSION = _encodeVersion(1, 0, 0); - /// @dev The implementation address of this feature. - address private immutable _implementation; // solhint-enable using LibRichErrorsV06 for bytes; - constructor() public { - _implementation = address(this); + constructor() public FixinCommon() { + // solhint-disable-next-line no-empty-blocks } /// @dev Initialize and register this feature. Should be delegatecalled @@ -59,14 +57,14 @@ contract TokenSpender is /// @param allowanceTarget An `allowanceTarget` instance, configured to have /// the ZeroeEx contract as an authority. /// @return success `MIGRATE_SUCCESS` on success. - function migrate(IAllowanceTarget allowanceTarget) external returns (bytes4 success) { + function migrate(IAllowanceTarget allowanceTarget) + external + returns (bytes4 success) + { LibTokenSpenderStorage.getStorage().allowanceTarget = allowanceTarget; - ISimpleFunctionRegistry(address(this)) - .extend(this.getAllowanceTarget.selector, _implementation); - ISimpleFunctionRegistry(address(this)) - .extend(this._spendERC20Tokens.selector, _implementation); - ISimpleFunctionRegistry(address(this)) - .extend(this.getSpendableERC20BalanceOf.selector, _implementation); + _registerFeatureFunction(this.getAllowanceTarget.selector); + _registerFeatureFunction(this._spendERC20Tokens.selector); + _registerFeatureFunction(this.getSpendableERC20BalanceOf.selector); return LibMigrate.MIGRATE_SUCCESS; } diff --git a/contracts/zero-ex/contracts/src/features/TransformERC20.sol b/contracts/zero-ex/contracts/src/features/TransformERC20.sol index 323522913b..85b4b1b651 100644 --- a/contracts/zero-ex/contracts/src/features/TransformERC20.sol +++ b/contracts/zero-ex/contracts/src/features/TransformERC20.sol @@ -52,39 +52,32 @@ contract TransformERC20 is uint256 takerOutputTokenBalanceAfter; } - // solhint-disable /// @dev Name of this feature. string public constant override FEATURE_NAME = "TransformERC20"; /// @dev Version of this feature. uint256 public immutable override FEATURE_VERSION = _encodeVersion(1, 1, 0); - /// @dev The implementation address of this feature. - address private immutable _implementation; - // solhint-enable using LibSafeMathV06 for uint256; using LibRichErrorsV06 for bytes; - constructor() public { - _implementation = address(this); + constructor() public FixinCommon() { + // solhint-disable-next-line no-empty-blocks } /// @dev Initialize and register this feature. /// Should be delegatecalled by `Migrate.migrate()`. /// @param transformerDeployer The trusted deployer for transformers. /// @return success `LibMigrate.SUCCESS` on success. - function migrate(address transformerDeployer) external returns (bytes4 success) { - ISimpleFunctionRegistry(address(this)) - .extend(this.getTransformerDeployer.selector, _implementation); - ISimpleFunctionRegistry(address(this)) - .extend(this.createTransformWallet.selector, _implementation); - ISimpleFunctionRegistry(address(this)) - .extend(this.getTransformWallet.selector, _implementation); - ISimpleFunctionRegistry(address(this)) - .extend(this.setTransformerDeployer.selector, _implementation); - ISimpleFunctionRegistry(address(this)) - .extend(this.transformERC20.selector, _implementation); - ISimpleFunctionRegistry(address(this)) - .extend(this._transformERC20.selector, _implementation); + function migrate(address transformerDeployer) + external + returns (bytes4 success) + { + _registerFeatureFunction(this.getTransformerDeployer.selector); + _registerFeatureFunction(this.createTransformWallet.selector); + _registerFeatureFunction(this.getTransformWallet.selector); + _registerFeatureFunction(this.setTransformerDeployer.selector); + _registerFeatureFunction(this.transformERC20.selector); + _registerFeatureFunction(this._transformERC20.selector); this.createTransformWallet(); LibTransformERC20Storage.getStorage().transformerDeployer = transformerDeployer; return LibMigrate.MIGRATE_SUCCESS; @@ -191,6 +184,7 @@ contract TransformERC20 is Transformation[] memory transformations ) public + virtual override payable onlySelf diff --git a/contracts/zero-ex/contracts/src/fixins/FixinCommon.sol b/contracts/zero-ex/contracts/src/fixins/FixinCommon.sol index e1b78fbbf3..46a0c0eaf0 100644 --- a/contracts/zero-ex/contracts/src/fixins/FixinCommon.sol +++ b/contracts/zero-ex/contracts/src/fixins/FixinCommon.sol @@ -23,13 +23,17 @@ import "@0x/contracts-utils/contracts/src/v06/errors/LibRichErrorsV06.sol"; import "../errors/LibCommonRichErrors.sol"; import "../errors/LibOwnableRichErrors.sol"; import "../features/IOwnable.sol"; +import "../features/ISimpleFunctionRegistry.sol"; /// @dev Common feature utilities. -contract FixinCommon { +abstract contract FixinCommon { using LibRichErrorsV06 for bytes; + /// @dev The implementation address of this feature. + address internal immutable _implementation; + /// @dev The caller must be this contract. modifier onlySelf() virtual { if (msg.sender != address(this)) { @@ -52,6 +56,21 @@ contract FixinCommon { _; } + constructor() internal { + // Remember this feature's original address. + _implementation = address(this); + } + + /// @dev Registers a function implemented by this feature at `_implementation`. + /// Can and should only be called within a `migrate()`. + /// @param selector The selector of the function whose implementation + /// is at `_implementation`. + function _registerFeatureFunction(bytes4 selector) + internal + { + ISimpleFunctionRegistry(address(this)).extend(selector, _implementation); + } + /// @dev Encode a feature version as a `uint256`. /// @param major The major version number of the feature. /// @param minor The minor version number of the feature. diff --git a/contracts/zero-ex/contracts/src/fixins/FixinEIP712.sol b/contracts/zero-ex/contracts/src/fixins/FixinEIP712.sol new file mode 100644 index 0000000000..fac8231066 --- /dev/null +++ b/contracts/zero-ex/contracts/src/fixins/FixinEIP712.sol @@ -0,0 +1,69 @@ +/* + + 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/LibCommonRichErrors.sol"; +import "../errors/LibOwnableRichErrors.sol"; +import "../features/IOwnable.sol"; + + +/// @dev EIP712 helpers for features. +abstract contract FixinEIP712 { + + /// @dev The domain hash separator for the entire exchange proxy. + bytes32 public immutable EIP712_DOMAIN_SEPARATOR; + + constructor(address zeroExAddress) internal { + // Compute `EIP712_DOMAIN_SEPARATOR` + { + uint256 chainId; + assembly { chainId := chainid() } + EIP712_DOMAIN_SEPARATOR = keccak256( + abi.encode( + keccak256( + "EIP712Domain(" + "string name," + "string version," + "uint256 chainId," + "address verifyingContract" + ")" + ), + keccak256("ZeroEx"), + keccak256("1.0.0"), + chainId, + zeroExAddress + ) + ); + } + } + + function _getEIP712Hash(bytes32 structHash) + internal + view + returns (bytes32 eip712Hash) + { + return keccak256(abi.encodePacked( + hex"1901", + EIP712_DOMAIN_SEPARATOR, + structHash + )); + } +} diff --git a/contracts/zero-ex/contracts/src/migrations/FullMigration.sol b/contracts/zero-ex/contracts/src/migrations/FullMigration.sol index b1a0e2246d..854a888f66 100644 --- a/contracts/zero-ex/contracts/src/migrations/FullMigration.sol +++ b/contracts/zero-ex/contracts/src/migrations/FullMigration.sol @@ -23,6 +23,8 @@ import "../ZeroEx.sol"; import "../features/IOwnable.sol"; import "../features/TokenSpender.sol"; import "../features/TransformERC20.sol"; +import "../features/SignatureValidator.sol"; +import "../features/MetaTransactions.sol"; import "../external/AllowanceTarget.sol"; import "./InitialMigration.sol"; @@ -38,6 +40,8 @@ contract FullMigration { Ownable ownable; TokenSpender tokenSpender; TransformERC20 transformERC20; + SignatureValidator signatureValidator; + MetaTransactions metaTransactions; } /// @dev Parameters needed to initialize features. @@ -62,25 +66,39 @@ contract FullMigration { _initialMigration = new InitialMigration(address(this)); } + /// @dev Retrieve the bootstrapper address to use when constructing `ZeroEx`. + /// @return bootstrapper The bootstrapper address. + function getBootstrapper() + external + view + returns (address bootstrapper) + { + return address(_initialMigration); + } + /// @dev Deploy the `ZeroEx` contract with the full feature set, /// transfer ownership to `owner`, then self-destruct. /// @param owner The owner of the contract. + /// @param zeroEx The instance of the ZeroEx contract. ZeroEx should + /// been constructed with this contract as the bootstrapper. /// @param features Features to add to the proxy. - /// @return zeroEx The deployed and configured `ZeroEx` contract. + /// @return _zeroEx The configured ZeroEx contract. Same as the `zeroEx` parameter. /// @param migrateOpts Parameters needed to initialize features. function deploy( address payable owner, + ZeroEx zeroEx, Features memory features, MigrateOpts memory migrateOpts ) public - returns (ZeroEx zeroEx) + returns (ZeroEx _zeroEx) { require(msg.sender == deployer, "FullMigration/INVALID_SENDER"); // Perform the initial migration with the owner set to this contract. - zeroEx = _initialMigration.deploy( + _initialMigration.deploy( address(uint160(address(this))), + zeroEx, InitialMigration.BootstrapFeatures({ registry: features.registry, ownable: features.ownable @@ -95,6 +113,8 @@ contract FullMigration { // Self-destruct. this.die(owner); + + return zeroEx; } /// @dev Destroy this contract. Only callable from ourselves (from `deploy()`). @@ -153,5 +173,27 @@ contract FullMigration { address(this) ); } + // SignatureValidator + { + // Register the feature. + ownable.migrate( + address(features.signatureValidator), + abi.encodeWithSelector( + SignatureValidator.migrate.selector + ), + address(this) + ); + } + // MetaTransactions + { + // Register the feature. + ownable.migrate( + address(features.metaTransactions), + abi.encodeWithSelector( + MetaTransactions.migrate.selector + ), + address(this) + ); + } } } diff --git a/contracts/zero-ex/contracts/src/migrations/InitialMigration.sol b/contracts/zero-ex/contracts/src/migrations/InitialMigration.sol index 50959cf946..404ccfe765 100644 --- a/contracts/zero-ex/contracts/src/migrations/InitialMigration.sol +++ b/contracts/zero-ex/contracts/src/migrations/InitialMigration.sol @@ -53,19 +53,22 @@ contract InitialMigration { /// transfers ownership to `owner`, then self-destructs. /// Only callable by `deployer` set in the contstructor. /// @param owner The owner of the contract. + /// @param zeroEx The instance of the ZeroEx contract. ZeroEx should + /// been constructed with this contract as the bootstrapper. /// @param features Features to bootstrap into the proxy. - /// @return zeroEx The deployed and configured `ZeroEx` contract. - function deploy(address payable owner, BootstrapFeatures memory features) + /// @return _zeroEx The configured ZeroEx contract. Same as the `zeroEx` parameter. + function deploy( + address payable owner, + ZeroEx zeroEx, + BootstrapFeatures memory features + ) public virtual - returns (ZeroEx zeroEx) + returns (ZeroEx _zeroEx) { // Must be called by the allowed deployer. require(msg.sender == deployer, "InitialMigration/INVALID_SENDER"); - // Deploy the ZeroEx contract, setting ourselves as the bootstrapper. - zeroEx = new ZeroEx(); - // Bootstrap the initial feature set. IBootstrap(address(zeroEx)).bootstrap( address(this), @@ -75,6 +78,8 @@ contract InitialMigration { // Self-destruct. This contract should not hold any funds but we send // them to the owner just in case. this.die(owner); + + return zeroEx; } /// @dev Sets up the initial state of the `ZeroEx` contract. diff --git a/contracts/zero-ex/contracts/src/storage/LibMetaTransactionsStorage.sol b/contracts/zero-ex/contracts/src/storage/LibMetaTransactionsStorage.sol new file mode 100644 index 0000000000..4f68b8cf74 --- /dev/null +++ b/contracts/zero-ex/contracts/src/storage/LibMetaTransactionsStorage.sol @@ -0,0 +1,44 @@ +/* + + 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 "./LibStorage.sol"; + + +/// @dev Storage helpers for the `MetaTransactions` feature. +library LibMetaTransactionsStorage { + + /// @dev Storage bucket for this feature. + struct Storage { + // The block number when a hash was executed. + mapping (bytes32 => uint256) mtxHashToExecutedBlockNumber; + } + + /// @dev Get the storage bucket for this contract. + function getStorage() internal pure returns (Storage storage stor) { + uint256 storageSlot = LibStorage.getStorageSlot( + LibStorage.StorageId.MetaTransactions + ); + // Dip into assembly to change the slot pointed to by the local + // variable `stor`. + // See https://solidity.readthedocs.io/en/v0.6.8/assembly.html?highlight=slot#access-to-external-variables-functions-and-libraries + assembly { stor_slot := storageSlot } + } +} diff --git a/contracts/zero-ex/contracts/src/storage/LibStorage.sol b/contracts/zero-ex/contracts/src/storage/LibStorage.sol index 9f0aa15c22..1af79e919f 100644 --- a/contracts/zero-ex/contracts/src/storage/LibStorage.sol +++ b/contracts/zero-ex/contracts/src/storage/LibStorage.sol @@ -34,7 +34,8 @@ library LibStorage { SimpleFunctionRegistry, Ownable, TokenSpender, - TransformERC20 + TransformERC20, + MetaTransactions } /// @dev Get the storage slot given a storage ID. We assign unique, well-spaced diff --git a/contracts/zero-ex/contracts/src/storage/LibTransformERC20Storage.sol b/contracts/zero-ex/contracts/src/storage/LibTransformERC20Storage.sol index 78740b171b..95f22ccbdc 100644 --- a/contracts/zero-ex/contracts/src/storage/LibTransformERC20Storage.sol +++ b/contracts/zero-ex/contracts/src/storage/LibTransformERC20Storage.sol @@ -23,7 +23,7 @@ import "./LibStorage.sol"; import "../external/IFlashWallet.sol"; -/// @dev Storage helpers for the `TokenSpender` feature. +/// @dev Storage helpers for the `TransformERC20` feature. library LibTransformERC20Storage { /// @dev Storage bucket for this feature. diff --git a/contracts/zero-ex/contracts/test/TestMetaTransactionsTransformERC20Feature.sol b/contracts/zero-ex/contracts/test/TestMetaTransactionsTransformERC20Feature.sol new file mode 100644 index 0000000000..7e653068b7 --- /dev/null +++ b/contracts/zero-ex/contracts/test/TestMetaTransactionsTransformERC20Feature.sol @@ -0,0 +1,71 @@ +/* + + 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/features/TransformERC20.sol"; + + +contract TestMetaTransactionsTransformERC20Feature is + TransformERC20 +{ + event TransformERC20Called( + address sender, + uint256 value, + bytes32 callDataHash, + address taker, + IERC20TokenV06 inputToken, + IERC20TokenV06 outputToken, + uint256 inputTokenAmount, + uint256 minOutputTokenAmount, + Transformation[] transformations + ); + + function _transformERC20( + bytes32 callDataHash, + address payable taker, + IERC20TokenV06 inputToken, + IERC20TokenV06 outputToken, + uint256 inputTokenAmount, + uint256 minOutputTokenAmount, + Transformation[] memory transformations + ) + public + override + payable + returns (uint256 outputTokenAmount) + { + if (msg.value == 666) { + revert('FAIL'); + } + + emit TransformERC20Called( + msg.sender, + msg.value, + callDataHash, + taker, + inputToken, + outputToken, + inputTokenAmount, + minOutputTokenAmount, + transformations + ); + return 1337; + } +} diff --git a/contracts/zero-ex/package.json b/contracts/zero-ex/package.json index 1a51a20b20..f9a83055c9 100644 --- a/contracts/zero-ex/package.json +++ b/contracts/zero-ex/package.json @@ -39,9 +39,9 @@ "publish:private": "yarn build && gitpkg publish" }, "config": { - "publicInterfaceContracts": "ZeroEx,FullMigration,InitialMigration,IFlashWallet,IAllowanceTarget,IERC20Transformer,IOwnable,ISimpleFunctionRegistry,ITokenSpender,ITransformERC20,FillQuoteTransformer,PayTakerTransformer,WethTransformer,Ownable,SimpleFunctionRegistry,TransformERC20,TokenSpender,AffiliateFeeTransformer", + "publicInterfaceContracts": "ZeroEx,FullMigration,InitialMigration,IFlashWallet,IAllowanceTarget,IERC20Transformer,IOwnable,ISimpleFunctionRegistry,ITokenSpender,ITransformERC20,FillQuoteTransformer,PayTakerTransformer,WethTransformer,Ownable,SimpleFunctionRegistry,TransformERC20,TokenSpender,AffiliateFeeTransformerSignatureValidator,MetaTransactions", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.", - "abis": "./test/generated-artifacts/@(AffiliateFeeTransformer|AllowanceTarget|Bootstrap|FillQuoteTransformer|FixinCommon|FixinGasToken|FlashWallet|FullMigration|IAllowanceTarget|IBootstrap|IERC20Bridge|IERC20Transformer|IExchange|IFeature|IFlashWallet|IGasToken|IOwnable|ISimpleFunctionRegistry|ITestSimpleFunctionRegistryFeature|ITokenSpender|ITransformERC20|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibMigrate|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibSpenderRichErrors|LibStorage|LibTokenSpenderStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|Ownable|PayTakerTransformer|SimpleFunctionRegistry|TestCallTarget|TestDelegateCaller|TestFillQuoteTransformerBridge|TestFillQuoteTransformerExchange|TestFillQuoteTransformerHost|TestFullMigration|TestInitialMigration|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestTokenSpender|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerBase|TestTransformerDeployerTransformer|TestTransformerHost|TestWeth|TestWethTransformerHost|TestZeroExFeature|TokenSpender|TransformERC20|Transformer|TransformerDeployer|WethTransformer|ZeroEx).json" + "abis": "./test/generated-artifacts/@(AffiliateFeeTransformer|AllowanceTarget|Bootstrap|FillQuoteTransformer|FixinCommon|FixinEIP712|FlashWallet|FullMigration|IAllowanceTarget|IBootstrap|IERC20Transformer|IExchange|IFeature|IFlashWallet|IMetaTransactions|IOwnable|ISignatureValidator|ISimpleFunctionRegistry|ITestSimpleFunctionRegistryFeature|ITokenSpender|ITransformERC20|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibMetaTransactionsRichErrors|LibMetaTransactionsStorage|LibMigrate|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibSignatureRichErrors|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibSpenderRichErrors|LibStorage|LibTokenSpenderStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|MetaTransactions|Ownable|PayTakerTransformer|SignatureValidator|SimpleFunctionRegistry|TestCallTarget|TestDelegateCaller|TestFillQuoteTransformerExchange|TestFillQuoteTransformerHost|TestFullMigration|TestInitialMigration|TestMetaTransactionsTransformERC20Feature|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestTokenSpender|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerBase|TestTransformerDeployerTransformer|TestTransformerHost|TestWeth|TestWethTransformerHost|TestZeroExFeature|TokenSpender|TransformERC20|Transformer|TransformerDeployer|WethTransformer|ZeroEx).json" }, "repository": { "type": "git", diff --git a/contracts/zero-ex/src/artifacts.ts b/contracts/zero-ex/src/artifacts.ts index 81d08589c8..0f8eaec40f 100644 --- a/contracts/zero-ex/src/artifacts.ts +++ b/contracts/zero-ex/src/artifacts.ts @@ -16,8 +16,10 @@ import * as IOwnable from '../generated-artifacts/IOwnable.json'; import * as ISimpleFunctionRegistry from '../generated-artifacts/ISimpleFunctionRegistry.json'; import * as ITokenSpender from '../generated-artifacts/ITokenSpender.json'; import * as ITransformERC20 from '../generated-artifacts/ITransformERC20.json'; +import * as MetaTransactions from '../generated-artifacts/MetaTransactions.json'; import * as Ownable from '../generated-artifacts/Ownable.json'; import * as PayTakerTransformer from '../generated-artifacts/PayTakerTransformer.json'; +import * as SignatureValidator from '../generated-artifacts/SignatureValidator.json'; import * as SimpleFunctionRegistry from '../generated-artifacts/SimpleFunctionRegistry.json'; import * as TokenSpender from '../generated-artifacts/TokenSpender.json'; import * as TransformERC20 from '../generated-artifacts/TransformERC20.json'; @@ -42,4 +44,6 @@ export const artifacts = { TransformERC20: TransformERC20 as ContractArtifact, TokenSpender: TokenSpender as ContractArtifact, AffiliateFeeTransformer: AffiliateFeeTransformer as ContractArtifact, + SignatureValidator: SignatureValidator as ContractArtifact, + MetaTransactions: MetaTransactions as ContractArtifact, }; diff --git a/contracts/zero-ex/src/migration.ts b/contracts/zero-ex/src/migration.ts index 7b8f7b490d..3ec41160fd 100644 --- a/contracts/zero-ex/src/migration.ts +++ b/contracts/zero-ex/src/migration.ts @@ -1,4 +1,3 @@ -import { BaseContract } from '@0x/base-contract'; import { SupportedProvider } from '@0x/subproviders'; import { TxData } from 'ethereum-types'; import * as _ from 'lodash'; @@ -7,7 +6,9 @@ import { artifacts } from './artifacts'; import { FullMigrationContract, InitialMigrationContract, + MetaTransactionsContract, OwnableContract, + SignatureValidatorContract, SimpleFunctionRegistryContract, TokenSpenderContract, TransformERC20Contract, @@ -16,11 +17,17 @@ import { // tslint:disable: completed-docs +/** + * Addresses of minimum features for a deployment of the Exchange Proxy. + */ export interface BootstrapFeatures { - registry: SimpleFunctionRegistryContract; - ownable: OwnableContract; + registry: string; + ownable: string; } +/** + * Deploy the minimum features of the Exchange Proxy. + */ export async function deployBootstrapFeaturesAsync( provider: SupportedProvider, txDefaults: Partial, @@ -34,20 +41,23 @@ export async function deployBootstrapFeaturesAsync( provider, txDefaults, artifacts, - )), + )).address, ownable: features.ownable || - (await OwnableContract.deployFrom0xArtifactAsync(artifacts.Ownable, provider, txDefaults, artifacts)), + (await OwnableContract.deployFrom0xArtifactAsync(artifacts.Ownable, provider, txDefaults, artifacts)) + .address, }; } +/** + * Migrate an instance of the Exchange proxy with minimum viable features. + */ export async function initialMigrateAsync( owner: string, provider: SupportedProvider, txDefaults: Partial, features: Partial = {}, ): Promise { - const _features = await deployBootstrapFeaturesAsync(provider, txDefaults, features); const migrator = await InitialMigrationContract.deployFrom0xArtifactAsync( artifacts.InitialMigration, provider, @@ -55,24 +65,42 @@ export async function initialMigrateAsync( artifacts, txDefaults.from as string, ); - const deployCall = migrator.deploy(owner, toFeatureAdddresses(_features)); - const zeroEx = new ZeroExContract(await deployCall.callAsync(), provider, {}); - await deployCall.awaitTransactionSuccessAsync(); + const zeroEx = await ZeroExContract.deployFrom0xArtifactAsync( + artifacts.ZeroEx, + provider, + txDefaults, + artifacts, + migrator.address, + ); + const _features = await deployBootstrapFeaturesAsync(provider, txDefaults, features); + await migrator.deploy(owner, zeroEx.address, _features).awaitTransactionSuccessAsync(); return zeroEx; } +/** + * Addresses of features for a full deployment of the Exchange Proxy. + */ export interface FullFeatures extends BootstrapFeatures { - tokenSpender: TokenSpenderContract; - transformERC20: TransformERC20Contract; + tokenSpender: string; + transformERC20: string; + signatureValidator: string; + metaTransactions: string; } +/** + * Extra configuration options for a full migration of the Exchange Proxy. + */ export interface FullMigrationOpts { transformerDeployer: string; } +/** + * Deploy all the features for a full Exchange Proxy. + */ export async function deployFullFeaturesAsync( provider: SupportedProvider, txDefaults: Partial, + zeroExAddress: string, features: Partial = {}, ): Promise { return { @@ -84,7 +112,7 @@ export async function deployFullFeaturesAsync( provider, txDefaults, artifacts, - )), + )).address, transformERC20: features.transformERC20 || (await TransformERC20Contract.deployFrom0xArtifactAsync( @@ -92,10 +120,30 @@ export async function deployFullFeaturesAsync( provider, txDefaults, artifacts, - )), + )).address, + signatureValidator: + features.signatureValidator || + (await SignatureValidatorContract.deployFrom0xArtifactAsync( + artifacts.SignatureValidator, + provider, + txDefaults, + artifacts, + )).address, + metaTransactions: + features.metaTransactions || + (await MetaTransactionsContract.deployFrom0xArtifactAsync( + artifacts.MetaTransactions, + provider, + txDefaults, + artifacts, + zeroExAddress, + )).address, }; } +/** + * Deploy a fully featured instance of the Exchange Proxy. + */ export async function fullMigrateAsync( owner: string, provider: SupportedProvider, @@ -103,7 +151,6 @@ export async function fullMigrateAsync( features: Partial = {}, opts: Partial = {}, ): Promise { - const _features = await deployFullFeaturesAsync(provider, txDefaults, features); const migrator = await FullMigrationContract.deployFrom0xArtifactAsync( artifacts.FullMigration, provider, @@ -111,20 +158,18 @@ export async function fullMigrateAsync( artifacts, txDefaults.from as string, ); + const zeroEx = await ZeroExContract.deployFrom0xArtifactAsync( + artifacts.ZeroEx, + provider, + txDefaults, + artifacts, + await migrator.getBootstrapper().callAsync(), + ); + const _features = await deployFullFeaturesAsync(provider, txDefaults, zeroEx.address, features); const _opts = { transformerDeployer: txDefaults.from as string, ...opts, }; - const deployCall = migrator.deploy(owner, toFeatureAdddresses(_features), _opts); - const zeroEx = new ZeroExContract(await deployCall.callAsync(), provider, {}); - await deployCall.awaitTransactionSuccessAsync(); + await migrator.deploy(owner, zeroEx.address, _features, _opts).awaitTransactionSuccessAsync(); return zeroEx; } - -// tslint:disable:space-before-function-parent one-line -export function toFeatureAdddresses( - features: T, -): { [name in keyof T]: string } { - // TS can't figure this out. - return _.mapValues(features, (c: BaseContract) => c.address) as any; -} diff --git a/contracts/zero-ex/src/wrappers.ts b/contracts/zero-ex/src/wrappers.ts index fa8857622c..d7da0ff07e 100644 --- a/contracts/zero-ex/src/wrappers.ts +++ b/contracts/zero-ex/src/wrappers.ts @@ -14,8 +14,10 @@ export * from '../generated-wrappers/i_simple_function_registry'; export * from '../generated-wrappers/i_token_spender'; export * from '../generated-wrappers/i_transform_erc20'; export * from '../generated-wrappers/initial_migration'; +export * from '../generated-wrappers/meta_transactions'; export * from '../generated-wrappers/ownable'; export * from '../generated-wrappers/pay_taker_transformer'; +export * from '../generated-wrappers/signature_validator'; export * from '../generated-wrappers/simple_function_registry'; export * from '../generated-wrappers/token_spender'; export * from '../generated-wrappers/transform_erc20'; diff --git a/contracts/zero-ex/test/artifacts.ts b/contracts/zero-ex/test/artifacts.ts index eee23af238..84d616b778 100644 --- a/contracts/zero-ex/test/artifacts.ts +++ b/contracts/zero-ex/test/artifacts.ts @@ -11,6 +11,7 @@ import * as Bootstrap from '../test/generated-artifacts/Bootstrap.json'; import * as FillQuoteTransformer from '../test/generated-artifacts/FillQuoteTransformer.json'; import * as FixinCommon from '../test/generated-artifacts/FixinCommon.json'; import * as FixinGasToken from '../test/generated-artifacts/FixinGasToken.json'; +import * as FixinEIP712 from '../test/generated-artifacts/FixinEIP712.json'; import * as FlashWallet from '../test/generated-artifacts/FlashWallet.json'; import * as FullMigration from '../test/generated-artifacts/FullMigration.json'; import * as IAllowanceTarget from '../test/generated-artifacts/IAllowanceTarget.json'; @@ -21,8 +22,10 @@ import * as IExchange from '../test/generated-artifacts/IExchange.json'; import * as IFeature from '../test/generated-artifacts/IFeature.json'; import * as IFlashWallet from '../test/generated-artifacts/IFlashWallet.json'; import * as IGasToken from '../test/generated-artifacts/IGasToken.json'; +import * as IMetaTransactions from '../test/generated-artifacts/IMetaTransactions.json'; import * as InitialMigration from '../test/generated-artifacts/InitialMigration.json'; import * as IOwnable from '../test/generated-artifacts/IOwnable.json'; +import * as ISignatureValidator from '../test/generated-artifacts/ISignatureValidator.json'; import * as ISimpleFunctionRegistry from '../test/generated-artifacts/ISimpleFunctionRegistry.json'; import * as ITestSimpleFunctionRegistryFeature from '../test/generated-artifacts/ITestSimpleFunctionRegistryFeature.json'; import * as ITokenSpender from '../test/generated-artifacts/ITokenSpender.json'; @@ -30,11 +33,14 @@ import * as ITransformERC20 from '../test/generated-artifacts/ITransformERC20.js import * as LibBootstrap from '../test/generated-artifacts/LibBootstrap.json'; import * as LibCommonRichErrors from '../test/generated-artifacts/LibCommonRichErrors.json'; import * as LibERC20Transformer from '../test/generated-artifacts/LibERC20Transformer.json'; +import * as LibMetaTransactionsRichErrors from '../test/generated-artifacts/LibMetaTransactionsRichErrors.json'; +import * as LibMetaTransactionsStorage from '../test/generated-artifacts/LibMetaTransactionsStorage.json'; import * as LibMigrate from '../test/generated-artifacts/LibMigrate.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 LibSignatureRichErrors from '../test/generated-artifacts/LibSignatureRichErrors.json'; import * as LibSimpleFunctionRegistryRichErrors from '../test/generated-artifacts/LibSimpleFunctionRegistryRichErrors.json'; import * as LibSimpleFunctionRegistryStorage from '../test/generated-artifacts/LibSimpleFunctionRegistryStorage.json'; import * as LibSpenderRichErrors from '../test/generated-artifacts/LibSpenderRichErrors.json'; @@ -43,8 +49,10 @@ import * as LibTokenSpenderStorage from '../test/generated-artifacts/LibTokenSpe import * as LibTransformERC20RichErrors from '../test/generated-artifacts/LibTransformERC20RichErrors.json'; import * as LibTransformERC20Storage from '../test/generated-artifacts/LibTransformERC20Storage.json'; import * as LibWalletRichErrors from '../test/generated-artifacts/LibWalletRichErrors.json'; +import * as MetaTransactions from '../test/generated-artifacts/MetaTransactions.json'; import * as Ownable from '../test/generated-artifacts/Ownable.json'; import * as PayTakerTransformer from '../test/generated-artifacts/PayTakerTransformer.json'; +import * as SignatureValidator from '../test/generated-artifacts/SignatureValidator.json'; import * as SimpleFunctionRegistry from '../test/generated-artifacts/SimpleFunctionRegistry.json'; import * as TestCallTarget from '../test/generated-artifacts/TestCallTarget.json'; import * as TestDelegateCaller from '../test/generated-artifacts/TestDelegateCaller.json'; @@ -53,6 +61,7 @@ import * as TestFillQuoteTransformerExchange from '../test/generated-artifacts/T import * as TestFillQuoteTransformerHost from '../test/generated-artifacts/TestFillQuoteTransformerHost.json'; import * as TestFullMigration from '../test/generated-artifacts/TestFullMigration.json'; import * as TestInitialMigration from '../test/generated-artifacts/TestInitialMigration.json'; +import * as TestMetaTransactionsTransformERC20Feature from '../test/generated-artifacts/TestMetaTransactionsTransformERC20Feature.json'; import * as TestMigrator from '../test/generated-artifacts/TestMigrator.json'; import * as TestMintableERC20Token from '../test/generated-artifacts/TestMintableERC20Token.json'; import * as TestMintTokenERC20Transformer from '../test/generated-artifacts/TestMintTokenERC20Transformer.json'; @@ -76,8 +85,10 @@ import * as ZeroEx from '../test/generated-artifacts/ZeroEx.json'; export const artifacts = { ZeroEx: ZeroEx as ContractArtifact, LibCommonRichErrors: LibCommonRichErrors as ContractArtifact, + LibMetaTransactionsRichErrors: LibMetaTransactionsRichErrors as ContractArtifact, LibOwnableRichErrors: LibOwnableRichErrors as ContractArtifact, LibProxyRichErrors: LibProxyRichErrors as ContractArtifact, + LibSignatureRichErrors: LibSignatureRichErrors as ContractArtifact, LibSimpleFunctionRegistryRichErrors: LibSimpleFunctionRegistryRichErrors as ContractArtifact, LibSpenderRichErrors: LibSpenderRichErrors as ContractArtifact, LibTransformERC20RichErrors: LibTransformERC20RichErrors as ContractArtifact, @@ -90,20 +101,26 @@ export const artifacts = { Bootstrap: Bootstrap as ContractArtifact, IBootstrap: IBootstrap as ContractArtifact, IFeature: IFeature as ContractArtifact, + IMetaTransactions: IMetaTransactions as ContractArtifact, IOwnable: IOwnable as ContractArtifact, + ISignatureValidator: ISignatureValidator as ContractArtifact, ISimpleFunctionRegistry: ISimpleFunctionRegistry as ContractArtifact, ITokenSpender: ITokenSpender as ContractArtifact, ITransformERC20: ITransformERC20 as ContractArtifact, + MetaTransactions: MetaTransactions as ContractArtifact, Ownable: Ownable as ContractArtifact, + SignatureValidator: SignatureValidator as ContractArtifact, SimpleFunctionRegistry: SimpleFunctionRegistry as ContractArtifact, TokenSpender: TokenSpender as ContractArtifact, TransformERC20: TransformERC20 as ContractArtifact, FixinCommon: FixinCommon as ContractArtifact, FixinGasToken: FixinGasToken as ContractArtifact, + FixinEIP712: FixinEIP712 as ContractArtifact, FullMigration: FullMigration as ContractArtifact, InitialMigration: InitialMigration as ContractArtifact, LibBootstrap: LibBootstrap as ContractArtifact, LibMigrate: LibMigrate as ContractArtifact, + LibMetaTransactionsStorage: LibMetaTransactionsStorage as ContractArtifact, LibOwnableStorage: LibOwnableStorage as ContractArtifact, LibProxyStorage: LibProxyStorage as ContractArtifact, LibSimpleFunctionRegistryStorage: LibSimpleFunctionRegistryStorage as ContractArtifact, @@ -128,6 +145,7 @@ export const artifacts = { TestFillQuoteTransformerHost: TestFillQuoteTransformerHost as ContractArtifact, TestFullMigration: TestFullMigration as ContractArtifact, TestInitialMigration: TestInitialMigration as ContractArtifact, + TestMetaTransactionsTransformERC20Feature: TestMetaTransactionsTransformERC20Feature as ContractArtifact, TestMigrator: TestMigrator as ContractArtifact, TestMintTokenERC20Transformer: TestMintTokenERC20Transformer as ContractArtifact, TestMintableERC20Token: TestMintableERC20Token as ContractArtifact, diff --git a/contracts/zero-ex/test/features/meta_transactions_test.ts b/contracts/zero-ex/test/features/meta_transactions_test.ts new file mode 100644 index 0000000000..65bfb88ff6 --- /dev/null +++ b/contracts/zero-ex/test/features/meta_transactions_test.ts @@ -0,0 +1,518 @@ +import { + blockchainTests, + constants, + expect, + getRandomInteger, + randomAddress, + verifyEventsFromLogs, +} from '@0x/contracts-test-utils'; +import { getExchangeProxyMetaTransactionHash, signatureUtils } from '@0x/order-utils'; +import { ExchangeProxyMetaTransaction } from '@0x/types'; +import { BigNumber, hexUtils, StringRevertError, ZeroExRevertErrors } from '@0x/utils'; +import * as _ from 'lodash'; + +import { MetaTransactionsContract, ZeroExContract } from '../../src/wrappers'; +import { artifacts } from '../artifacts'; +import { abis } from '../utils/abis'; +import { fullMigrateAsync } from '../utils/migration'; +import { + ITokenSpenderContract, + TestMetaTransactionsTransformERC20FeatureContract, + TestMetaTransactionsTransformERC20FeatureEvents, + TestMintableERC20TokenContract, +} from '../wrappers'; + +const { NULL_ADDRESS, ZERO_AMOUNT } = constants; + +blockchainTests.resets('MetaTransactions feature', env => { + let owner: string; + let sender: string; + let signers: string[]; + let zeroEx: ZeroExContract; + let feature: MetaTransactionsContract; + let feeToken: TestMintableERC20TokenContract; + let transformERC20Feature: TestMetaTransactionsTransformERC20FeatureContract; + let allowanceTarget: string; + + const MAX_FEE_AMOUNT = new BigNumber('1e18'); + + before(async () => { + [owner, sender, ...signers] = await env.getAccountAddressesAsync(); + transformERC20Feature = await TestMetaTransactionsTransformERC20FeatureContract.deployFrom0xArtifactAsync( + artifacts.TestMetaTransactionsTransformERC20Feature, + env.provider, + env.txDefaults, + {}, + ); + zeroEx = await fullMigrateAsync(owner, env.provider, env.txDefaults, { + transformERC20: transformERC20Feature.address, + }); + feature = new MetaTransactionsContract(zeroEx.address, env.provider, { ...env.txDefaults, from: sender }, abis); + feeToken = await TestMintableERC20TokenContract.deployFrom0xArtifactAsync( + artifacts.TestMintableERC20Token, + env.provider, + env.txDefaults, + {}, + ); + allowanceTarget = await new ITokenSpenderContract(zeroEx.address, env.provider, env.txDefaults) + .getAllowanceTarget() + .callAsync(); + // Fund signers with fee tokens. + await Promise.all( + signers.map(async signer => { + await feeToken.mint(signer, MAX_FEE_AMOUNT).awaitTransactionSuccessAsync(); + await feeToken.approve(allowanceTarget, MAX_FEE_AMOUNT).awaitTransactionSuccessAsync({ from: signer }); + }), + ); + }); + + function getRandomMetaTransaction( + fields: Partial = {}, + ): ExchangeProxyMetaTransaction { + return { + signer: _.sampleSize(signers)[0], + sender, + minGasPrice: getRandomInteger('2', '1e9'), + maxGasPrice: getRandomInteger('1e9', '100e9'), + expirationTime: new BigNumber(Math.floor(_.now() / 1000) + 360), + salt: new BigNumber(hexUtils.random()), + callData: hexUtils.random(4), + value: getRandomInteger(1, '1e18'), + feeToken: feeToken.address, + feeAmount: getRandomInteger(1, MAX_FEE_AMOUNT), + domain: { + chainId: 1, // Ganache's `chainid` opcode is hardcoded as 1 + verifyingContract: zeroEx.address, + }, + ...fields, + }; + } + + async function signMetaTransactionAsync(mtx: ExchangeProxyMetaTransaction, signer?: string): Promise { + return signatureUtils.ecSignHashAsync( + env.provider, + getExchangeProxyMetaTransactionHash(mtx), + signer || mtx.signer, + ); + } + + describe('getMetaTransactionHash()', () => { + it('generates the correct hash', async () => { + const mtx = getRandomMetaTransaction(); + const expected = getExchangeProxyMetaTransactionHash(mtx); + const actual = await feature.getMetaTransactionHash(mtx).callAsync(); + expect(actual).to.eq(expected); + }); + }); + + interface TransformERC20Args { + inputToken: string; + outputToken: string; + inputTokenAmount: BigNumber; + minOutputTokenAmount: BigNumber; + transformations: Array<{ deploymentNonce: BigNumber; data: string }>; + } + + function getRandomTransformERC20Args(fields: Partial = {}): TransformERC20Args { + return { + inputToken: randomAddress(), + outputToken: randomAddress(), + inputTokenAmount: getRandomInteger(1, '1e18'), + minOutputTokenAmount: getRandomInteger(1, '1e18'), + transformations: [{ deploymentNonce: new BigNumber(123), data: hexUtils.random() }], + ...fields, + }; + } + + const RAW_SUCCESS_RESULT = hexUtils.leftPad(1337); + + describe('executeMetaTransaction()', () => { + it('can call `TransformERC20.transformERC20()`', async () => { + const args = getRandomTransformERC20Args(); + const mtx = getRandomMetaTransaction({ + callData: transformERC20Feature + .transformERC20( + args.inputToken, + args.outputToken, + args.inputTokenAmount, + args.minOutputTokenAmount, + args.transformations, + ) + .getABIEncodedTransactionData(), + }); + const signature = await signMetaTransactionAsync(mtx); + const callOpts = { + gasPrice: mtx.minGasPrice, + value: mtx.value, + }; + const rawResult = await feature.executeMetaTransaction(mtx, signature).callAsync(callOpts); + expect(rawResult).to.eq(RAW_SUCCESS_RESULT); + const receipt = await feature.executeMetaTransaction(mtx, signature).awaitTransactionSuccessAsync(callOpts); + verifyEventsFromLogs( + receipt.logs, + [ + { + inputToken: args.inputToken, + outputToken: args.outputToken, + inputTokenAmount: args.inputTokenAmount, + minOutputTokenAmount: args.minOutputTokenAmount, + transformations: args.transformations, + sender: zeroEx.address, + value: mtx.value, + callDataHash: hexUtils.hash(mtx.callData), + taker: mtx.signer, + }, + ], + TestMetaTransactionsTransformERC20FeatureEvents.TransformERC20Called, + ); + }); + + it('can call with any sender if `sender == 0`', async () => { + const args = getRandomTransformERC20Args(); + const mtx = getRandomMetaTransaction({ + sender: NULL_ADDRESS, + callData: transformERC20Feature + .transformERC20( + args.inputToken, + args.outputToken, + args.inputTokenAmount, + args.minOutputTokenAmount, + args.transformations, + ) + .getABIEncodedTransactionData(), + }); + const signature = await signMetaTransactionAsync(mtx); + const callOpts = { + gasPrice: mtx.minGasPrice, + value: mtx.value, + from: randomAddress(), + }; + const rawResult = await feature.executeMetaTransaction(mtx, signature).callAsync(callOpts); + expect(rawResult).to.eq(RAW_SUCCESS_RESULT); + }); + + it('works without fee', async () => { + const args = getRandomTransformERC20Args(); + const mtx = getRandomMetaTransaction({ + feeAmount: ZERO_AMOUNT, + feeToken: randomAddress(), + callData: transformERC20Feature + .transformERC20( + args.inputToken, + args.outputToken, + args.inputTokenAmount, + args.minOutputTokenAmount, + args.transformations, + ) + .getABIEncodedTransactionData(), + }); + const signature = await signMetaTransactionAsync(mtx); + const callOpts = { + gasPrice: mtx.minGasPrice, + value: mtx.value, + }; + const rawResult = await feature.executeMetaTransaction(mtx, signature).callAsync(callOpts); + expect(rawResult).to.eq(RAW_SUCCESS_RESULT); + }); + + it('fails if the translated call fails', async () => { + const args = getRandomTransformERC20Args(); + const mtx = getRandomMetaTransaction({ + value: new BigNumber(666), + callData: transformERC20Feature + .transformERC20( + args.inputToken, + args.outputToken, + args.inputTokenAmount, + args.minOutputTokenAmount, + args.transformations, + ) + .getABIEncodedTransactionData(), + }); + const mtxHash = getExchangeProxyMetaTransactionHash(mtx); + const signature = await signMetaTransactionAsync(mtx); + const callOpts = { + gasPrice: mtx.minGasPrice, + value: mtx.value, + }; + const tx = feature.executeMetaTransaction(mtx, signature).callAsync(callOpts); + const actualCallData = transformERC20Feature + ._transformERC20( + hexUtils.hash(mtx.callData), + mtx.signer, + args.inputToken, + args.outputToken, + args.inputTokenAmount, + args.minOutputTokenAmount, + args.transformations, + ) + .getABIEncodedTransactionData(); + return expect(tx).to.revertWith( + new ZeroExRevertErrors.MetaTransactions.MetaTransactionCallFailedError( + mtxHash, + actualCallData, + new StringRevertError('FAIL').encode(), + ), + ); + }); + + it('fails with unsupported function', async () => { + const mtx = getRandomMetaTransaction({ + callData: transformERC20Feature.createTransformWallet().getABIEncodedTransactionData(), + }); + const mtxHash = getExchangeProxyMetaTransactionHash(mtx); + const signature = await signMetaTransactionAsync(mtx); + const callOpts = { + gasPrice: mtx.minGasPrice, + value: mtx.value, + }; + const tx = feature.executeMetaTransaction(mtx, signature).awaitTransactionSuccessAsync(callOpts); + return expect(tx).to.revertWith( + new ZeroExRevertErrors.MetaTransactions.MetaTransactionUnsupportedFunctionError( + mtxHash, + hexUtils.slice(mtx.callData, 0, 4), + ), + ); + }); + + it('cannot execute the same mtx twice', async () => { + const args = getRandomTransformERC20Args(); + const mtx = getRandomMetaTransaction({ + callData: transformERC20Feature + .transformERC20( + args.inputToken, + args.outputToken, + args.inputTokenAmount, + args.minOutputTokenAmount, + args.transformations, + ) + .getABIEncodedTransactionData(), + }); + const mtxHash = getExchangeProxyMetaTransactionHash(mtx); + const signature = await signMetaTransactionAsync(mtx); + const callOpts = { + gasPrice: mtx.minGasPrice, + value: mtx.value, + }; + const receipt = await feature.executeMetaTransaction(mtx, signature).awaitTransactionSuccessAsync(callOpts); + const tx = feature.executeMetaTransaction(mtx, signature).awaitTransactionSuccessAsync(callOpts); + return expect(tx).to.revertWith( + new ZeroExRevertErrors.MetaTransactions.MetaTransactionAlreadyExecutedError( + mtxHash, + receipt.blockNumber, + ), + ); + }); + + it('fails if not enough ETH provided', async () => { + const mtx = getRandomMetaTransaction(); + const mtxHash = getExchangeProxyMetaTransactionHash(mtx); + const signature = await signMetaTransactionAsync(mtx); + const callOpts = { + gasPrice: mtx.minGasPrice, + value: mtx.value.minus(1), + }; + const tx = feature.executeMetaTransaction(mtx, signature).awaitTransactionSuccessAsync(callOpts); + return expect(tx).to.revertWith( + new ZeroExRevertErrors.MetaTransactions.MetaTransactionInsufficientEthError( + mtxHash, + callOpts.value, + mtx.value, + ), + ); + }); + + it('fails if gas price too low', async () => { + const mtx = getRandomMetaTransaction(); + const mtxHash = getExchangeProxyMetaTransactionHash(mtx); + const signature = await signMetaTransactionAsync(mtx); + const callOpts = { + gasPrice: mtx.minGasPrice.minus(1), + value: mtx.value, + }; + const tx = feature.executeMetaTransaction(mtx, signature).awaitTransactionSuccessAsync(callOpts); + return expect(tx).to.revertWith( + new ZeroExRevertErrors.MetaTransactions.MetaTransactionGasPriceError( + mtxHash, + callOpts.gasPrice, + mtx.minGasPrice, + mtx.maxGasPrice, + ), + ); + }); + + it('fails if gas price too high', async () => { + const mtx = getRandomMetaTransaction(); + const mtxHash = getExchangeProxyMetaTransactionHash(mtx); + const signature = await signMetaTransactionAsync(mtx); + const callOpts = { + gasPrice: mtx.maxGasPrice.plus(1), + value: mtx.value, + }; + const tx = feature.executeMetaTransaction(mtx, signature).awaitTransactionSuccessAsync(callOpts); + return expect(tx).to.revertWith( + new ZeroExRevertErrors.MetaTransactions.MetaTransactionGasPriceError( + mtxHash, + callOpts.gasPrice, + mtx.minGasPrice, + mtx.maxGasPrice, + ), + ); + }); + + it('fails if expired', async () => { + const mtx = getRandomMetaTransaction({ + expirationTime: new BigNumber(Math.floor(_.now() / 1000 - 60)), + }); + const mtxHash = getExchangeProxyMetaTransactionHash(mtx); + const signature = await signMetaTransactionAsync(mtx); + const callOpts = { + gasPrice: mtx.maxGasPrice, + value: mtx.value, + }; + const tx = feature.executeMetaTransaction(mtx, signature).awaitTransactionSuccessAsync(callOpts); + return expect(tx).to.revertWith( + new ZeroExRevertErrors.MetaTransactions.MetaTransactionExpiredError( + mtxHash, + undefined, + mtx.expirationTime, + ), + ); + }); + + it('fails if wrong sender', async () => { + const requiredSender = randomAddress(); + const mtx = getRandomMetaTransaction({ + sender: requiredSender, + }); + const mtxHash = getExchangeProxyMetaTransactionHash(mtx); + const signature = await signMetaTransactionAsync(mtx); + const callOpts = { + gasPrice: mtx.maxGasPrice, + value: mtx.value, + }; + const tx = feature.executeMetaTransaction(mtx, signature).awaitTransactionSuccessAsync(callOpts); + return expect(tx).to.revertWith( + new ZeroExRevertErrors.MetaTransactions.MetaTransactionWrongSenderError( + mtxHash, + sender, + requiredSender, + ), + ); + }); + + it('fails if signature is wrong', async () => { + const mtx = getRandomMetaTransaction({ signer: signers[0] }); + const mtxHash = getExchangeProxyMetaTransactionHash(mtx); + const signature = await signMetaTransactionAsync(mtx, signers[1]); + const callOpts = { + gasPrice: mtx.maxGasPrice, + value: mtx.value, + }; + const tx = feature.executeMetaTransaction(mtx, signature).awaitTransactionSuccessAsync(callOpts); + return expect(tx).to.revertWith( + new ZeroExRevertErrors.MetaTransactions.MetaTransactionInvalidSignatureError( + mtxHash, + signature, + new ZeroExRevertErrors.SignatureValidator.SignatureValidationError( + ZeroExRevertErrors.SignatureValidator.SignatureValidationErrorCodes.WrongSigner, + mtxHash, + signers[0], + signature, + ).encode(), + ), + ); + }); + }); + + describe('executeMetaTransactions()', () => { + it('can execute multiple transactions', async () => { + const mtxs = _.times(2, i => { + const args = getRandomTransformERC20Args(); + return getRandomMetaTransaction({ + signer: signers[i], + callData: transformERC20Feature + .transformERC20( + args.inputToken, + args.outputToken, + args.inputTokenAmount, + args.minOutputTokenAmount, + args.transformations, + ) + .getABIEncodedTransactionData(), + }); + }); + const signatures = await Promise.all(mtxs.map(async mtx => signMetaTransactionAsync(mtx))); + const callOpts = { + gasPrice: BigNumber.max(...mtxs.map(mtx => mtx.minGasPrice)), + value: BigNumber.sum(...mtxs.map(mtx => mtx.value)), + }; + const rawResults = await feature.executeMetaTransactions(mtxs, signatures).callAsync(callOpts); + expect(rawResults).to.eql(mtxs.map(() => RAW_SUCCESS_RESULT)); + }); + }); + + describe('getMetaTransactionExecutedBlock()', () => { + it('returns zero for an unexecuted mtx', async () => { + const mtx = getRandomMetaTransaction(); + const block = await feature.getMetaTransactionExecutedBlock(mtx).callAsync(); + expect(block).to.bignumber.eq(0); + }); + + it('returns the block it was executed in', async () => { + const args = getRandomTransformERC20Args(); + const mtx = getRandomMetaTransaction({ + callData: transformERC20Feature + .transformERC20( + args.inputToken, + args.outputToken, + args.inputTokenAmount, + args.minOutputTokenAmount, + args.transformations, + ) + .getABIEncodedTransactionData(), + }); + const signature = await signMetaTransactionAsync(mtx); + const callOpts = { + gasPrice: mtx.minGasPrice, + value: mtx.value, + }; + const receipt = await feature.executeMetaTransaction(mtx, signature).awaitTransactionSuccessAsync(callOpts); + const block = await feature.getMetaTransactionExecutedBlock(mtx).callAsync(); + expect(block).to.bignumber.eq(receipt.blockNumber); + }); + }); + + describe('getMetaTransactionHashExecutedBlock()', () => { + it('returns zero for an unexecuted mtx', async () => { + const mtx = getRandomMetaTransaction(); + const mtxHash = getExchangeProxyMetaTransactionHash(mtx); + const block = await feature.getMetaTransactionHashExecutedBlock(mtxHash).callAsync(); + expect(block).to.bignumber.eq(0); + }); + + it('returns the block it was executed in', async () => { + const args = getRandomTransformERC20Args(); + const mtx = getRandomMetaTransaction({ + callData: transformERC20Feature + .transformERC20( + args.inputToken, + args.outputToken, + args.inputTokenAmount, + args.minOutputTokenAmount, + args.transformations, + ) + .getABIEncodedTransactionData(), + }); + const signature = await signMetaTransactionAsync(mtx); + const callOpts = { + gasPrice: mtx.minGasPrice, + value: mtx.value, + }; + const receipt = await feature.executeMetaTransaction(mtx, signature).awaitTransactionSuccessAsync(callOpts); + const mtxHash = getExchangeProxyMetaTransactionHash(mtx); + const block = await feature.getMetaTransactionHashExecutedBlock(mtxHash).callAsync(); + expect(block).to.bignumber.eq(receipt.blockNumber); + }); + }); +}); diff --git a/contracts/zero-ex/test/features/signature_validator_test.ts b/contracts/zero-ex/test/features/signature_validator_test.ts new file mode 100644 index 0000000000..5a2387bf43 --- /dev/null +++ b/contracts/zero-ex/test/features/signature_validator_test.ts @@ -0,0 +1,232 @@ +import { blockchainTests, constants, expect, randomAddress, signingUtils } from '@0x/contracts-test-utils'; +import { signatureUtils } from '@0x/order-utils'; +import { SignatureType } from '@0x/types'; +import { hexUtils, ZeroExRevertErrors } from '@0x/utils'; +import * as ethjs from 'ethereumjs-util'; +import * as _ from 'lodash'; + +import { SignatureValidatorContract, ZeroExContract } from '../../src/wrappers'; +import { abis } from '../utils/abis'; +import { fullMigrateAsync } from '../utils/migration'; + +const { NULL_BYTES } = constants; + +blockchainTests.resets('SignatureValidator feature', env => { + let owner: string; + let signers: string[]; + let zeroEx: ZeroExContract; + let feature: SignatureValidatorContract; + + before(async () => { + [owner, ...signers] = await env.getAccountAddressesAsync(); + zeroEx = await fullMigrateAsync(owner, env.provider, env.txDefaults); + feature = new SignatureValidatorContract(zeroEx.address, env.provider, env.txDefaults, abis); + }); + + describe('validateHashSignature()', () => { + it('can validate an eth_sign signature', async () => { + const hash = hexUtils.random(); + const signer = _.sampleSize(signers, 1)[0]; + const signature = await signatureUtils.ecSignHashAsync(env.provider, hash, signer); + await feature.validateHashSignature(hash, signer, signature).callAsync(); + }); + + it('rejects a wrong eth_sign signature', async () => { + const hash = hexUtils.random(); + const signer = _.sampleSize(signers, 1)[0]; + const signature = await signatureUtils.ecSignHashAsync(env.provider, hash, signer); + const notSigner = randomAddress(); + const tx = feature.validateHashSignature(hash, notSigner, signature).callAsync(); + return expect(tx).to.revertWith( + new ZeroExRevertErrors.SignatureValidator.SignatureValidationError( + ZeroExRevertErrors.SignatureValidator.SignatureValidationErrorCodes.WrongSigner, + hash, + notSigner, + signature, + ), + ); + }); + + it('rejects an eth_sign if ecrecover() fails', async () => { + const hash = hexUtils.random(); + const signer = _.sampleSize(signers, 1)[0]; + const signature = hexUtils.concat(hexUtils.random(65), SignatureType.EthSign); + const tx = feature.validateHashSignature(hash, signer, signature).callAsync(); + return expect(tx).to.revertWith( + new ZeroExRevertErrors.SignatureValidator.SignatureValidationError( + ZeroExRevertErrors.SignatureValidator.SignatureValidationErrorCodes.WrongSigner, + hash, + signer, + signature, + ), + ); + }); + + it('rejects a too short eth_sign signature', async () => { + const hash = hexUtils.random(); + const signer = _.sampleSize(signers, 1)[0]; + const signature = hexUtils.slice(await signatureUtils.ecSignHashAsync(env.provider, hash, signer), 1); + const notSigner = randomAddress(); + const tx = feature.validateHashSignature(hash, notSigner, signature).callAsync(); + return expect(tx).to.revertWith( + new ZeroExRevertErrors.SignatureValidator.SignatureValidationError( + ZeroExRevertErrors.SignatureValidator.SignatureValidationErrorCodes.InvalidLength, + hash, + notSigner, + signature, + ), + ); + }); + + it('can validate an eip712 signature', async () => { + const privateKey = hexUtils.random(); + const signer = hexUtils.toHex(ethjs.privateToAddress(ethjs.toBuffer(privateKey))); + const hash = hexUtils.random(); + const signature = hexUtils.toHex( + signingUtils.signMessage(ethjs.toBuffer(hash), ethjs.toBuffer(privateKey), SignatureType.EIP712), + ); + await feature.validateHashSignature(hash, signer, signature).callAsync(); + }); + + it('rejects a wrong eip712 signature', async () => { + const privateKey = hexUtils.random(); + const hash = hexUtils.random(); + const signature = hexUtils.toHex( + signingUtils.signMessage(ethjs.toBuffer(hash), ethjs.toBuffer(privateKey), SignatureType.EIP712), + ); + const notSigner = randomAddress(); + const tx = feature.validateHashSignature(hash, notSigner, signature).callAsync(); + return expect(tx).to.revertWith( + new ZeroExRevertErrors.SignatureValidator.SignatureValidationError( + ZeroExRevertErrors.SignatureValidator.SignatureValidationErrorCodes.WrongSigner, + hash, + notSigner, + signature, + ), + ); + }); + + it('rejects an eip712 if ecrecover() fails', async () => { + const hash = hexUtils.random(); + const signer = _.sampleSize(signers, 1)[0]; + const signature = hexUtils.concat(hexUtils.random(65), SignatureType.EIP712); + const tx = feature.validateHashSignature(hash, signer, signature).callAsync(); + return expect(tx).to.revertWith( + new ZeroExRevertErrors.SignatureValidator.SignatureValidationError( + ZeroExRevertErrors.SignatureValidator.SignatureValidationErrorCodes.WrongSigner, + hash, + signer, + signature, + ), + ); + }); + + it('rejects a too short eip712 signature', async () => { + const privateKey = hexUtils.random(); + const signer = hexUtils.toHex(ethjs.privateToAddress(ethjs.toBuffer(privateKey))); + const hash = hexUtils.random(); + const signature = hexUtils.slice( + hexUtils.toHex( + signingUtils.signMessage(ethjs.toBuffer(hash), ethjs.toBuffer(privateKey), SignatureType.EIP712), + ), + 1, + ); + const tx = feature.validateHashSignature(hash, signer, signature).callAsync(); + return expect(tx).to.revertWith( + new ZeroExRevertErrors.SignatureValidator.SignatureValidationError( + ZeroExRevertErrors.SignatureValidator.SignatureValidationErrorCodes.InvalidLength, + hash, + signer, + signature, + ), + ); + }); + + it('rejects an INVALID signature type', async () => { + const hash = hexUtils.random(); + const signer = _.sampleSize(signers, 1)[0]; + const signature = hexUtils.concat( + hexUtils.slice(await signatureUtils.ecSignHashAsync(env.provider, hash, signer), 0, -1), + SignatureType.Invalid, + ); + const tx = feature.validateHashSignature(hash, signer, signature).callAsync(); + return expect(tx).to.revertWith( + new ZeroExRevertErrors.SignatureValidator.SignatureValidationError( + ZeroExRevertErrors.SignatureValidator.SignatureValidationErrorCodes.AlwaysInvalid, + hash, + signer, + signature, + ), + ); + }); + + it('rejects an ILLEGAL signature type', async () => { + const hash = hexUtils.random(); + const signer = _.sampleSize(signers, 1)[0]; + const signature = hexUtils.concat( + hexUtils.slice(await signatureUtils.ecSignHashAsync(env.provider, hash, signer), 0, -1), + SignatureType.Illegal, + ); + const tx = feature.validateHashSignature(hash, signer, signature).callAsync(); + return expect(tx).to.revertWith( + new ZeroExRevertErrors.SignatureValidator.SignatureValidationError( + ZeroExRevertErrors.SignatureValidator.SignatureValidationErrorCodes.Illegal, + hash, + signer, + signature, + ), + ); + }); + + it('rejects an unsupported signature type', async () => { + const hash = hexUtils.random(); + const signer = _.sampleSize(signers, 1)[0]; + const signature = hexUtils.concat( + hexUtils.slice(await signatureUtils.ecSignHashAsync(env.provider, hash, signer), 0, -1), + SignatureType.Wallet, + ); + const tx = feature.validateHashSignature(hash, signer, signature).callAsync(); + return expect(tx).to.revertWith( + new ZeroExRevertErrors.SignatureValidator.SignatureValidationError( + ZeroExRevertErrors.SignatureValidator.SignatureValidationErrorCodes.Unsupported, + hash, + signer, + signature, + ), + ); + }); + + it('rejects an empty signature type', async () => { + const hash = hexUtils.random(); + const signer = _.sampleSize(signers, 1)[0]; + const signature = NULL_BYTES; + const tx = feature.validateHashSignature(hash, signer, signature).callAsync(); + return expect(tx).to.revertWith( + new ZeroExRevertErrors.SignatureValidator.SignatureValidationError( + ZeroExRevertErrors.SignatureValidator.SignatureValidationErrorCodes.InvalidLength, + hash, + signer, + signature, + ), + ); + }); + }); + + describe('isValidHashSignature()', () => { + it('returns true on valid signature', async () => { + const hash = hexUtils.random(); + const signer = _.sampleSize(signers, 1)[0]; + const signature = await signatureUtils.ecSignHashAsync(env.provider, hash, signer); + const r = await feature.isValidHashSignature(hash, signer, signature).callAsync(); + expect(r).to.eq(true); + }); + + it('returns false on invalid signature', async () => { + const hash = hexUtils.random(); + const signer = _.sampleSize(signers, 1)[0]; + const signature = await signatureUtils.ecSignHashAsync(env.provider, hash, signer); + const r = await feature.isValidHashSignature(hash, randomAddress(), signature).callAsync(); + expect(r).to.eq(false); + }); + }); +}); diff --git a/contracts/zero-ex/test/features/token_spender_test.ts b/contracts/zero-ex/test/features/token_spender_test.ts index 3d3b626aaf..dcb6592cde 100644 --- a/contracts/zero-ex/test/features/token_spender_test.ts +++ b/contracts/zero-ex/test/features/token_spender_test.ts @@ -22,12 +22,12 @@ blockchainTests.resets('TokenSpender feature', env => { before(async () => { const [owner] = await env.getAccountAddressesAsync(); zeroEx = await fullMigrateAsync(owner, env.provider, env.txDefaults, { - tokenSpender: await TokenSpenderContract.deployFrom0xArtifactAsync( + tokenSpender: (await TokenSpenderContract.deployFrom0xArtifactAsync( artifacts.TestTokenSpender, env.provider, env.txDefaults, artifacts, - ), + )).address, }); feature = new TokenSpenderContract(zeroEx.address, env.provider, env.txDefaults, abis); token = await TestTokenSpenderERC20TokenContract.deployFrom0xArtifactAsync( diff --git a/contracts/zero-ex/test/features/transform_erc20_test.ts b/contracts/zero-ex/test/features/transform_erc20_test.ts index 10732448a7..a7af7ecd96 100644 --- a/contracts/zero-ex/test/features/transform_erc20_test.ts +++ b/contracts/zero-ex/test/features/transform_erc20_test.ts @@ -40,12 +40,12 @@ blockchainTests.resets('TransformERC20 feature', env => { env.provider, env.txDefaults, { - transformERC20: await TransformERC20Contract.deployFrom0xArtifactAsync( + transformERC20: (await TransformERC20Contract.deployFrom0xArtifactAsync( artifacts.TestTransformERC20, env.provider, env.txDefaults, artifacts, - ), + )).address, }, { transformerDeployer }, ); diff --git a/contracts/zero-ex/test/full_migration_test.ts b/contracts/zero-ex/test/full_migration_test.ts index b45b7d9a5c..ffb4dc8231 100644 --- a/contracts/zero-ex/test/full_migration_test.ts +++ b/contracts/zero-ex/test/full_migration_test.ts @@ -6,10 +6,12 @@ import * as _ from 'lodash'; import { artifacts } from './artifacts'; import { abis } from './utils/abis'; -import { deployFullFeaturesAsync, FullFeatures, toFeatureAdddresses } from './utils/migration'; +import { deployFullFeaturesAsync, FullFeatures } from './utils/migration'; import { AllowanceTargetContract, + IMetaTransactionsContract, IOwnableContract, + ISignatureValidatorContract, ITokenSpenderContract, ITransformERC20Contract, TestFullMigrationContract, @@ -27,7 +29,6 @@ blockchainTests.resets('Full migration', env => { before(async () => { [owner] = await env.getAccountAddressesAsync(); - features = await deployFullFeaturesAsync(env.provider, env.txDefaults); migrator = await TestFullMigrationContract.deployFrom0xArtifactAsync( artifacts.TestFullMigration, env.provider, @@ -35,9 +36,15 @@ blockchainTests.resets('Full migration', env => { artifacts, env.txDefaults.from as string, ); - const deployCall = migrator.deploy(owner, toFeatureAdddresses(features), { transformerDeployer }); - zeroEx = new ZeroExContract(await deployCall.callAsync(), env.provider, env.txDefaults); - await deployCall.awaitTransactionSuccessAsync(); + zeroEx = await ZeroExContract.deployFrom0xArtifactAsync( + artifacts.ZeroEx, + env.provider, + env.txDefaults, + artifacts, + await migrator.getBootstrapper().callAsync(), + ); + features = await deployFullFeaturesAsync(env.provider, env.txDefaults, zeroEx.address); + await migrator.deploy(owner, zeroEx.address, features, { transformerDeployer }).awaitTransactionSuccessAsync(); }); it('ZeroEx has the correct owner', async () => { @@ -54,7 +61,7 @@ blockchainTests.resets('Full migration', env => { it('Non-deployer cannot call deploy()', async () => { const notDeployer = randomAddress(); const tx = migrator - .deploy(owner, toFeatureAdddresses(features), { transformerDeployer }) + .deploy(owner, zeroEx.address, features, { transformerDeployer }) .callAsync({ from: notDeployer }); return expect(tx).to.revertWith('FullMigration/INVALID_SENDER'); }); @@ -74,6 +81,21 @@ blockchainTests.resets('Full migration', env => { 'setTransformerDeployer', ], }, + SignatureValidator: { + contractType: ISignatureValidatorContract, + fns: ['isValidHashSignature', 'validateHashSignature'], + }, + MetaTransactions: { + contractType: IMetaTransactionsContract, + fns: [ + 'executeMetaTransaction', + 'executeMetaTransactions', + '_executeMetaTransaction', + 'getMetaTransactionExecutedBlock', + 'getMetaTransactionHashExecutedBlock', + 'getMetaTransactionHash', + ], + }, }; function createFakeInputs(inputs: DataItem[] | DataItem): any | any[] { diff --git a/contracts/zero-ex/test/initial_migration_test.ts b/contracts/zero-ex/test/initial_migration_test.ts index 0121cb0514..442a9bfbd9 100644 --- a/contracts/zero-ex/test/initial_migration_test.ts +++ b/contracts/zero-ex/test/initial_migration_test.ts @@ -2,7 +2,7 @@ import { blockchainTests, expect, randomAddress } from '@0x/contracts-test-utils import { hexUtils, ZeroExRevertErrors } from '@0x/utils'; import { artifacts } from './artifacts'; -import { BootstrapFeatures, deployBootstrapFeaturesAsync, toFeatureAdddresses } from './utils/migration'; +import { BootstrapFeatures, deployBootstrapFeaturesAsync } from './utils/migration'; import { IBootstrapContract, InitialMigrationContract, @@ -35,9 +35,14 @@ blockchainTests.resets('Initial migration', env => { env.txDefaults, {}, ); - const deployCall = migrator.deploy(owner, toFeatureAdddresses(features)); - zeroEx = new ZeroExContract(await deployCall.callAsync(), env.provider, env.txDefaults); - await deployCall.awaitTransactionSuccessAsync(); + zeroEx = await ZeroExContract.deployFrom0xArtifactAsync( + artifacts.ZeroEx, + env.provider, + env.txDefaults, + artifacts, + migrator.address, + ); + await migrator.deploy(owner, zeroEx.address, features).awaitTransactionSuccessAsync(); }); it('Self-destructs after deployment', async () => { @@ -47,7 +52,7 @@ blockchainTests.resets('Initial migration', env => { it('Non-deployer cannot call deploy()', async () => { const notDeployer = randomAddress(); - const tx = migrator.deploy(owner, toFeatureAdddresses(features)).callAsync({ from: notDeployer }); + const tx = migrator.deploy(owner, zeroEx.address, features).callAsync({ from: notDeployer }); return expect(tx).to.revertWith('InitialMigration/INVALID_SENDER'); }); diff --git a/contracts/zero-ex/test/utils/migration.ts b/contracts/zero-ex/test/utils/migration.ts index 4b70ec0aa0..566a32db72 100644 --- a/contracts/zero-ex/test/utils/migration.ts +++ b/contracts/zero-ex/test/utils/migration.ts @@ -4,7 +4,6 @@ export { deployFullFeaturesAsync, initialMigrateAsync, fullMigrateAsync, - toFeatureAdddresses, FullMigrationOpts, FullFeatures, } from '../../src/migration'; diff --git a/contracts/zero-ex/test/wrappers.ts b/contracts/zero-ex/test/wrappers.ts index 803fdf09c9..96d6706e23 100644 --- a/contracts/zero-ex/test/wrappers.ts +++ b/contracts/zero-ex/test/wrappers.ts @@ -9,6 +9,7 @@ export * from '../test/generated-wrappers/bootstrap'; export * from '../test/generated-wrappers/fill_quote_transformer'; export * from '../test/generated-wrappers/fixin_common'; export * from '../test/generated-wrappers/fixin_gas_token'; +export * from '../test/generated-wrappers/fixin_e_i_p712'; export * from '../test/generated-wrappers/flash_wallet'; export * from '../test/generated-wrappers/full_migration'; export * from '../test/generated-wrappers/i_allowance_target'; @@ -19,7 +20,9 @@ export * from '../test/generated-wrappers/i_exchange'; export * from '../test/generated-wrappers/i_feature'; export * from '../test/generated-wrappers/i_flash_wallet'; export * from '../test/generated-wrappers/i_gas_token'; +export * from '../test/generated-wrappers/i_meta_transactions'; export * from '../test/generated-wrappers/i_ownable'; +export * from '../test/generated-wrappers/i_signature_validator'; 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_token_spender'; @@ -28,11 +31,14 @@ 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_erc20_transformer'; +export * from '../test/generated-wrappers/lib_meta_transactions_rich_errors'; +export * from '../test/generated-wrappers/lib_meta_transactions_storage'; export * from '../test/generated-wrappers/lib_migrate'; 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_signature_rich_errors'; 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/lib_spender_rich_errors'; @@ -41,8 +47,10 @@ export * from '../test/generated-wrappers/lib_token_spender_storage'; export * from '../test/generated-wrappers/lib_transform_erc20_rich_errors'; export * from '../test/generated-wrappers/lib_transform_erc20_storage'; export * from '../test/generated-wrappers/lib_wallet_rich_errors'; +export * from '../test/generated-wrappers/meta_transactions'; export * from '../test/generated-wrappers/ownable'; export * from '../test/generated-wrappers/pay_taker_transformer'; +export * from '../test/generated-wrappers/signature_validator'; export * from '../test/generated-wrappers/simple_function_registry'; export * from '../test/generated-wrappers/test_call_target'; export * from '../test/generated-wrappers/test_delegate_caller'; @@ -51,6 +59,7 @@ export * from '../test/generated-wrappers/test_fill_quote_transformer_exchange'; export * from '../test/generated-wrappers/test_fill_quote_transformer_host'; export * from '../test/generated-wrappers/test_full_migration'; export * from '../test/generated-wrappers/test_initial_migration'; +export * from '../test/generated-wrappers/test_meta_transactions_transform_erc20_feature'; export * from '../test/generated-wrappers/test_migrator'; export * from '../test/generated-wrappers/test_mint_token_erc20_transformer'; export * from '../test/generated-wrappers/test_mintable_erc20_token'; diff --git a/contracts/zero-ex/tsconfig.json b/contracts/zero-ex/tsconfig.json index c161d5ff74..11585e644f 100644 --- a/contracts/zero-ex/tsconfig.json +++ b/contracts/zero-ex/tsconfig.json @@ -14,8 +14,10 @@ "generated-artifacts/ITokenSpender.json", "generated-artifacts/ITransformERC20.json", "generated-artifacts/InitialMigration.json", + "generated-artifacts/MetaTransactions.json", "generated-artifacts/Ownable.json", "generated-artifacts/PayTakerTransformer.json", + "generated-artifacts/SignatureValidator.json", "generated-artifacts/SimpleFunctionRegistry.json", "generated-artifacts/TokenSpender.json", "generated-artifacts/TransformERC20.json", @@ -27,6 +29,7 @@ "test/generated-artifacts/FillQuoteTransformer.json", "test/generated-artifacts/FixinCommon.json", "test/generated-artifacts/FixinGasToken.json", + "test/generated-artifacts/FixinEIP712.json", "test/generated-artifacts/FlashWallet.json", "test/generated-artifacts/FullMigration.json", "test/generated-artifacts/IAllowanceTarget.json", @@ -37,7 +40,9 @@ "test/generated-artifacts/IFeature.json", "test/generated-artifacts/IFlashWallet.json", "test/generated-artifacts/IGasToken.json", + "test/generated-artifacts/IMetaTransactions.json", "test/generated-artifacts/IOwnable.json", + "test/generated-artifacts/ISignatureValidator.json", "test/generated-artifacts/ISimpleFunctionRegistry.json", "test/generated-artifacts/ITestSimpleFunctionRegistryFeature.json", "test/generated-artifacts/ITokenSpender.json", @@ -46,11 +51,14 @@ "test/generated-artifacts/LibBootstrap.json", "test/generated-artifacts/LibCommonRichErrors.json", "test/generated-artifacts/LibERC20Transformer.json", + "test/generated-artifacts/LibMetaTransactionsRichErrors.json", + "test/generated-artifacts/LibMetaTransactionsStorage.json", "test/generated-artifacts/LibMigrate.json", "test/generated-artifacts/LibOwnableRichErrors.json", "test/generated-artifacts/LibOwnableStorage.json", "test/generated-artifacts/LibProxyRichErrors.json", "test/generated-artifacts/LibProxyStorage.json", + "test/generated-artifacts/LibSignatureRichErrors.json", "test/generated-artifacts/LibSimpleFunctionRegistryRichErrors.json", "test/generated-artifacts/LibSimpleFunctionRegistryStorage.json", "test/generated-artifacts/LibSpenderRichErrors.json", @@ -59,8 +67,10 @@ "test/generated-artifacts/LibTransformERC20RichErrors.json", "test/generated-artifacts/LibTransformERC20Storage.json", "test/generated-artifacts/LibWalletRichErrors.json", + "test/generated-artifacts/MetaTransactions.json", "test/generated-artifacts/Ownable.json", "test/generated-artifacts/PayTakerTransformer.json", + "test/generated-artifacts/SignatureValidator.json", "test/generated-artifacts/SimpleFunctionRegistry.json", "test/generated-artifacts/TestCallTarget.json", "test/generated-artifacts/TestDelegateCaller.json", @@ -69,6 +79,7 @@ "test/generated-artifacts/TestFillQuoteTransformerHost.json", "test/generated-artifacts/TestFullMigration.json", "test/generated-artifacts/TestInitialMigration.json", + "test/generated-artifacts/TestMetaTransactionsTransformERC20Feature.json", "test/generated-artifacts/TestMigrator.json", "test/generated-artifacts/TestMintTokenERC20Transformer.json", "test/generated-artifacts/TestMintableERC20Token.json", diff --git a/packages/order-utils/src/eip712_utils.ts b/packages/order-utils/src/eip712_utils.ts index 089bfc9cb2..1ab3576cdc 100644 --- a/packages/order-utils/src/eip712_utils.ts +++ b/packages/order-utils/src/eip712_utils.ts @@ -143,7 +143,10 @@ export const eip712Utils = { // tslint:disable-next-line: custom-no-magic-numbers v => (BigNumber.isBigNumber(v) ? v.toString(10) : v), ) as EIP712Object, - _domain, + { + ...constants.MAINNET_EXCHANGE_PROXY_DOMAIN, + ...mtx.domain, + }, ); }, };