From 829a353b143d74195f6d2c0845b1f80f3beb71c4 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Mon, 22 Jun 2020 15:36:53 -0400 Subject: [PATCH 01/11] `@0x/order-utils`: Add `getOrderHash()`, `getExchangeTransactionHash()`, `getExchangeProxyTransactionHash()` --- packages/order-utils/CHANGELOG.json | 4 + packages/order-utils/package.json | 1 + packages/order-utils/src/constants.ts | 22 +++++ packages/order-utils/src/eip712_utils.ts | 17 +++- packages/order-utils/src/hash_utils.ts | 29 ++++++ packages/order-utils/src/index.ts | 4 + packages/order-utils/src/signature_utils.ts | 102 ++++++++++++++++++-- 7 files changed, 171 insertions(+), 8 deletions(-) create mode 100644 packages/order-utils/src/hash_utils.ts diff --git a/packages/order-utils/CHANGELOG.json b/packages/order-utils/CHANGELOG.json index dd530319af..07ecbd87e5 100644 --- a/packages/order-utils/CHANGELOG.json +++ b/packages/order-utils/CHANGELOG.json @@ -5,6 +5,10 @@ { "note": "Add ERC20 Transformer utils and export useful constants.", "pr": 2604 + }, + { + "note": "Add `getOrderHash()`, `getExchangeTransactionHash()`, `getExchangeProxyTransactionHash()`", + "pr": 2610 } ] }, diff --git a/packages/order-utils/package.json b/packages/order-utils/package.json index 2f66f35e0e..fe4784ed27 100644 --- a/packages/order-utils/package.json +++ b/packages/order-utils/package.json @@ -64,6 +64,7 @@ }, "dependencies": { "@0x/assert": "^3.0.8", + "@0x/contract-addresses": "^4.10.0", "@0x/contract-wrappers": "^13.7.0", "@0x/json-schemas": "^5.0.8", "@0x/utils": "^5.5.0", diff --git a/packages/order-utils/src/constants.ts b/packages/order-utils/src/constants.ts index 7ef3cec026..e8f52deea8 100644 --- a/packages/order-utils/src/constants.ts +++ b/packages/order-utils/src/constants.ts @@ -1,3 +1,4 @@ +import { getContractAddressesForChainOrThrow } from '@0x/contract-addresses'; import { BigNumber, NULL_ADDRESS, NULL_BYTES } from '@0x/utils'; import { MethodAbi } from 'ethereum-types'; @@ -150,6 +151,27 @@ export const constants = { { name: 'transactionSignature', type: 'bytes' }, ], }, + MAINNET_EXCHANGE_PROXY_DOMAIN: { + name: 'ZeroEx', + version: '1.0.0', + chainId: 1, + verifyingContract: getContractAddressesForChainOrThrow(1).exchangeProxy, + }, + EXCHANGE_PROXY_MTX_SCEHMA: { + name: 'MetaTransactionData', + parameters: [ + { name: 'signer', type: 'address' }, + { name: 'sender', type: 'address' }, + { name: 'minGasPrice', type: 'uint256' }, + { name: 'maxGasPrice', type: 'uint256' }, + { name: 'expirationTime', type: 'uint256' }, + { name: 'salt', type: 'uint256' }, + { name: 'callData', type: 'bytes' }, + { name: 'value', type: 'uint256' }, + { name: 'feeToken', type: 'address' }, + { name: 'feeAmount', type: 'uint256' }, + ], + }, ERC20_METHOD_ABI, ERC721_METHOD_ABI, MULTI_ASSET_METHOD_ABI, diff --git a/packages/order-utils/src/eip712_utils.ts b/packages/order-utils/src/eip712_utils.ts index 57fbe89815..089bfc9cb2 100644 --- a/packages/order-utils/src/eip712_utils.ts +++ b/packages/order-utils/src/eip712_utils.ts @@ -5,11 +5,12 @@ import { EIP712Object, EIP712TypedData, EIP712Types, + ExchangeProxyMetaTransaction, Order, SignedZeroExTransaction, ZeroExTransaction, } from '@0x/types'; -import { hexUtils, signTypedDataUtils } from '@0x/utils'; +import { BigNumber, hexUtils, signTypedDataUtils } from '@0x/utils'; import * as _ from 'lodash'; import { constants } from './constants'; @@ -131,4 +132,18 @@ export const eip712Utils = { ); return typedData; }, + createExchangeProxyMetaTransactionTypedData(mtx: ExchangeProxyMetaTransaction): EIP712TypedData { + return eip712Utils.createTypedData( + constants.EXCHANGE_PROXY_MTX_SCEHMA.name, + { + MetaTransactionData: constants.EXCHANGE_PROXY_MTX_SCEHMA.parameters, + }, + _.mapValues( + _.omit(mtx, 'domain'), + // tslint:disable-next-line: custom-no-magic-numbers + v => (BigNumber.isBigNumber(v) ? v.toString(10) : v), + ) as EIP712Object, + _domain, + ); + }, }; diff --git a/packages/order-utils/src/hash_utils.ts b/packages/order-utils/src/hash_utils.ts new file mode 100644 index 0000000000..57859f0843 --- /dev/null +++ b/packages/order-utils/src/hash_utils.ts @@ -0,0 +1,29 @@ +import { ExchangeProxyMetaTransaction, Order, ZeroExTransaction } from '@0x/types'; +import { hexUtils, signTypedDataUtils } from '@0x/utils'; + +import { eip712Utils } from './eip712_utils'; +import { orderHashUtils } from './order_hash_utils'; +import { transactionHashUtils } from './transaction_hash_utils'; + +/** + * Compute the EIP712 hash of an order. + */ +export function getOrderHash(order: Order): string { + return orderHashUtils.getOrderHash(order); +} + +/** + * Compute the EIP712 hash of an Exchange meta-transaction. + */ +export function getExchangeMetaTransactionHash(tx: ZeroExTransaction): string { + return transactionHashUtils.getTransactionHash(tx); +} + +/** + * Compute the EIP712 hash of an Exchange Proxy meta-transaction. + */ +export function getExchangeProxyMetaTransactionHash(mtx: ExchangeProxyMetaTransaction): string { + return hexUtils.toHex( + signTypedDataUtils.generateTypedDataHash(eip712Utils.createExchangeProxyMetaTransactionTypedData(mtx)), + ); +} diff --git a/packages/order-utils/src/index.ts b/packages/order-utils/src/index.ts index b35b070c69..bfc0d9506a 100644 --- a/packages/order-utils/src/index.ts +++ b/packages/order-utils/src/index.ts @@ -49,6 +49,8 @@ export { ZeroExTransaction, SignedZeroExTransaction, ValidatorSignature, + ExchangeProxyMetaTransaction, + SignedExchangeProxyMetaTransaction, } from '@0x/types'; export { @@ -77,6 +79,8 @@ export { decodeAffiliateFeeTransformerData, } from './transformer_data_encoders'; +export { getOrderHash, getExchangeMetaTransactionHash, getExchangeProxyMetaTransactionHash } from './hash_utils'; + import { constants } from './constants'; export const NULL_ADDRESS = constants.NULL_ADDRESS; export const NULL_BYTES = constants.NULL_BYTES; diff --git a/packages/order-utils/src/signature_utils.ts b/packages/order-utils/src/signature_utils.ts index a0c55179c9..8058a2cbf2 100644 --- a/packages/order-utils/src/signature_utils.ts +++ b/packages/order-utils/src/signature_utils.ts @@ -1,14 +1,16 @@ import { schemas } from '@0x/json-schemas'; import { ECSignature, + ExchangeProxyMetaTransaction, Order, SignatureType, + SignedExchangeProxyMetaTransaction, SignedOrder, SignedZeroExTransaction, ValidatorSignature, ZeroExTransaction, } from '@0x/types'; -import { providerUtils } from '@0x/utils'; +import { hexUtils, providerUtils } from '@0x/utils'; import { Web3Wrapper } from '@0x/web3-wrapper'; import { SupportedProvider } from 'ethereum-types'; import * as ethUtil from 'ethereumjs-util'; @@ -16,6 +18,7 @@ import * as _ from 'lodash'; import { assert } from './assert'; import { eip712Utils } from './eip712_utils'; +import { getExchangeProxyMetaTransactionHash } from './hash_utils'; import { orderHashUtils } from './order_hash_utils'; import { transactionHashUtils } from './transaction_hash_utils'; import { TypedDataError } from './types'; @@ -187,6 +190,96 @@ export const signatureUtils = { } } }, + /** + * Signs an Exchange Proxy meta-transaction and returns a SignedExchangeProxyMetaTransaction. + * First `eth_signTypedData` is requested then a fallback to `eth_sign` if not + * available on the supplied provider. + * @param supportedProvider Web3 provider to use for all JSON RPC requests + * @param transaction The ExchangeProxyMetaTransaction to sign. + * @param signerAddress The hex encoded Ethereum address you wish to sign it with. This address + * must be available via the supplied Provider. + * @return A SignedExchangeProxyMetaTransaction containing the order and + * elliptic curve signature with Signature Type. + */ + async ecSignExchangeProxyMetaTransactionAsync( + supportedProvider: SupportedProvider, + transaction: ExchangeProxyMetaTransaction, + signerAddress: string, + ): Promise { + assert.doesConformToSchema('transaction', transaction, schemas.zeroExTransactionSchema, [schemas.hexSchema]); + try { + const signedTransaction = await signatureUtils.ecSignTypedDataExchangeProxyMetaTransactionAsync( + supportedProvider, + transaction, + signerAddress, + ); + return signedTransaction; + } catch (err) { + // HACK: We are unable to handle specific errors thrown since provider is not an object + // under our control. It could be Metamask Web3, Ethers, or any general RPC provider. + // We check for a user denying the signature request in a way that supports Metamask and + // Coinbase Wallet. Unfortunately for signers with a different error message, + // they will receive two signature requests. + if (err.message.includes('User denied message signature')) { + throw err; + } + const transactionHash = getExchangeProxyMetaTransactionHash(transaction); + const signatureHex = await signatureUtils.ecSignHashAsync( + supportedProvider, + transactionHash, + signerAddress, + ); + const signedTransaction = { + ...transaction, + signature: signatureHex, + }; + return signedTransaction; + } + }, + /** + * Signs an Exchange Proxy meta-transaction using `eth_signTypedData` and + * returns a SignedZeroExTransaction. + * @param supportedProvider Web3 provider to use for all JSON RPC requests + * @param transaction The Exchange Proxy transaction to sign. + * @param signerAddress The hex encoded Ethereum address you wish + * to sign it with. This address must be available via the supplied Provider. + * @return A SignedExchangeProxyMetaTransaction containing the + * ExchangeProxyMetaTransaction and elliptic curve signature with Signature Type. + */ + async ecSignTypedDataExchangeProxyMetaTransactionAsync( + supportedProvider: SupportedProvider, + transaction: ExchangeProxyMetaTransaction, + signerAddress: string, + ): Promise { + const provider = providerUtils.standardizeOrThrow(supportedProvider); + assert.isETHAddressHex('signerAddress', signerAddress); + assert.doesConformToSchema('transaction', transaction, schemas.zeroExTransactionSchema, [schemas.hexSchema]); + const web3Wrapper = new Web3Wrapper(provider); + await assert.isSenderAddressAsync('signerAddress', signerAddress, web3Wrapper); + const normalizedSignerAddress = signerAddress.toLowerCase(); + const typedData = eip712Utils.createExchangeProxyMetaTransactionTypedData(transaction); + try { + const signature = await web3Wrapper.signTypedDataAsync(normalizedSignerAddress, typedData); + const ecSignatureRSV = parseSignatureHexAsRSV(signature); + const signatureHex = hexUtils.concat( + ecSignatureRSV.v, + ecSignatureRSV.r, + ecSignatureRSV.s, + SignatureType.EIP712, + ); + return { + ...transaction, + signature: signatureHex, + }; + } catch (err) { + // Detect if Metamask to transition users to the MetamaskSubprovider + if ((provider as any).isMetaMask) { + throw new Error(TypedDataError.InvalidMetamaskSigner); + } else { + throw err; + } + } + }, /** * Signs a hash using `eth_sign` and returns its elliptic curve signature and signature type. * @param supportedProvider Web3 provider to use for all JSON RPC requests @@ -245,12 +338,7 @@ export const signatureUtils = { * @return Hex encoded string of signature (v,r,s) with Signature Type */ convertECSignatureToSignatureHex(ecSignature: ECSignature): string { - const signatureBuffer = Buffer.concat([ - ethUtil.toBuffer(ecSignature.v), - ethUtil.toBuffer(ecSignature.r), - ethUtil.toBuffer(ecSignature.s), - ]); - const signatureHex = `0x${signatureBuffer.toString('hex')}`; + const signatureHex = hexUtils.concat(ecSignature.v, ecSignature.r, ecSignature.s); const signatureWithType = signatureUtils.convertToSignatureWithType(signatureHex, SignatureType.EthSign); return signatureWithType; }, From 687c79ae81ac5f660defd90c2c560521920cf947 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Wed, 24 Jun 2020 01:44:41 -0400 Subject: [PATCH 02/11] `@0x/utils`: Add more revert errors to `ZeroExRevertErrors` --- packages/utils/CHANGELOG.json | 4 + packages/utils/src/index.ts | 2 + .../zero-ex/meta_transaction_revert_errors.ts | 144 ++++++++++++++++++ .../signature_validator_revert_errors.ts | 33 ++++ 4 files changed, 183 insertions(+) create mode 100644 packages/utils/src/revert_errors/zero-ex/meta_transaction_revert_errors.ts create mode 100644 packages/utils/src/revert_errors/zero-ex/signature_validator_revert_errors.ts diff --git a/packages/utils/CHANGELOG.json b/packages/utils/CHANGELOG.json index 1ea711fca0..e50f836c67 100644 --- a/packages/utils/CHANGELOG.json +++ b/packages/utils/CHANGELOG.json @@ -29,6 +29,10 @@ { "note": "Update `ZeroExRevertErrors`", "pr": 2597 + }, + { + "note": "Add more revert errors to `ZeroExRevertErrors`", + "pr": 2610 } ], "timestamp": 1592969527 diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index e311c57da5..52c2565aa3 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -52,4 +52,6 @@ export const ZeroExRevertErrors = { Spender: require('./revert_errors/zero-ex/spender_revert_errors'), TransformERC20: require('./revert_errors/zero-ex/transform_erc20_revert_errors'), Wallet: require('./revert_errors/zero-ex/wallet_revert_errors'), + MetaTransactions: require('./revert_errors/zero-ex/meta_transaction_revert_errors'), + SignatureValidator: require('./revert_errors/zero-ex/signature_validator_revert_errors'), }; diff --git a/packages/utils/src/revert_errors/zero-ex/meta_transaction_revert_errors.ts b/packages/utils/src/revert_errors/zero-ex/meta_transaction_revert_errors.ts new file mode 100644 index 0000000000..86ad905cda --- /dev/null +++ b/packages/utils/src/revert_errors/zero-ex/meta_transaction_revert_errors.ts @@ -0,0 +1,144 @@ +import { RevertError } from '../../revert_error'; +import { Numberish } from '../../types'; + +// tslint:disable:max-classes-per-file +export class InvalidMetaTransactionsArrayLengthsError extends RevertError { + constructor(mtxCount?: Numberish, signatureCount?: Numberish) { + super( + 'InvalidMetaTransactionsArrayLengthsError', + 'InvalidMetaTransactionsArrayLengthsError(uint256 mtxCount, uint256 signatureCount)', + { + mtxCount, + signatureCount, + }, + ); + } +} + +export class MetaTransactionAlreadyExecutedError extends RevertError { + constructor(mtxHash?: string, executedBlockNumber?: Numberish) { + super( + 'MetaTransactionAlreadyExecutedError', + 'MetaTransactionAlreadyExecutedError(bytes32 mtxHash, uint256 executedBlockNumber)', + { + mtxHash, + executedBlockNumber, + }, + ); + } +} + +export class MetaTransactionUnsupportedFunctionError extends RevertError { + constructor(mtxHash?: string, selector?: string) { + super( + 'MetaTransactionUnsupportedFunctionError', + 'MetaTransactionUnsupportedFunctionError(bytes32 mtxHash, bytes4 selector)', + { + mtxHash, + selector, + }, + ); + } +} + +export class MetaTransactionWrongSenderError extends RevertError { + constructor(mtxHash?: string, sender?: string, expectedSender?: string) { + super( + 'MetaTransactionWrongSenderError', + 'MetaTransactionWrongSenderError(bytes32 mtxHash, address sender, address expectedSender)', + { + mtxHash, + sender, + expectedSender, + }, + ); + } +} + +export class MetaTransactionExpiredError extends RevertError { + constructor(mtxHash?: string, time?: Numberish, expirationTime?: Numberish) { + super( + 'MetaTransactionExpiredError', + 'MetaTransactionExpiredError(bytes32 mtxHash, uint256 time, uint256 expirationTime)', + { + mtxHash, + time, + expirationTime, + }, + ); + } +} + +export class MetaTransactionGasPriceError extends RevertError { + constructor(mtxHash?: string, gasPrice?: Numberish, minGasPrice?: Numberish, maxGasPrice?: Numberish) { + super( + 'MetaTransactionGasPriceError', + 'MetaTransactionGasPriceError(bytes32 mtxHash, uint256 gasPrice, uint256 minGasPrice, uint256 maxGasPrice)', + { + mtxHash, + gasPrice, + minGasPrice, + maxGasPrice, + }, + ); + } +} + +export class MetaTransactionInsufficientEthError extends RevertError { + constructor(mtxHash?: string, ethBalance?: Numberish, ethRequired?: Numberish) { + super( + 'MetaTransactionInsufficientEthError', + 'MetaTransactionInsufficientEthError(bytes32 mtxHash, uint256 ethBalance, uint256 ethRequired)', + { + mtxHash, + ethBalance, + ethRequired, + }, + ); + } +} + +export class MetaTransactionInvalidSignatureError extends RevertError { + constructor(mtxHash?: string, signature?: string, errData?: string) { + super( + 'MetaTransactionInvalidSignatureError', + 'MetaTransactionInvalidSignatureError(bytes32 mtxHash, bytes signature, bytes errData)', + { + mtxHash, + signature, + errData, + }, + ); + } +} + +export class MetaTransactionCallFailedError extends RevertError { + constructor(mtxHash?: string, callData?: string, returnData?: string) { + super( + 'MetaTransactionCallFailedError', + 'MetaTransactionCallFailedError(bytes32 mtxHash, bytes callData, bytes returnData)', + { + mtxHash, + callData, + returnData, + }, + ); + } +} + +const types = [ + InvalidMetaTransactionsArrayLengthsError, + MetaTransactionAlreadyExecutedError, + MetaTransactionUnsupportedFunctionError, + MetaTransactionWrongSenderError, + MetaTransactionExpiredError, + MetaTransactionGasPriceError, + MetaTransactionInsufficientEthError, + MetaTransactionInvalidSignatureError, + MetaTransactionCallFailedError, +]; + +// Register the types we've defined. +for (const type of types) { + RevertError.registerType(type); +} diff --git a/packages/utils/src/revert_errors/zero-ex/signature_validator_revert_errors.ts b/packages/utils/src/revert_errors/zero-ex/signature_validator_revert_errors.ts new file mode 100644 index 0000000000..69d2283f75 --- /dev/null +++ b/packages/utils/src/revert_errors/zero-ex/signature_validator_revert_errors.ts @@ -0,0 +1,33 @@ +import { RevertError } from '../../revert_error'; + +// tslint:disable:max-classes-per-file + +export enum SignatureValidationErrorCodes { + AlwaysInvalid = 0, + InvalidLength = 1, + Unsupported = 2, + Illegal = 3, + WrongSigner = 4, +} + +export class SignatureValidationError extends RevertError { + constructor(code?: SignatureValidationErrorCodes, hash?: string, signerAddress?: string, signature?: string) { + super( + 'SignatureValidationError', + 'SignatureValidationError(uint8 code, bytes32 hash, address signerAddress, bytes signature)', + { + code, + hash, + signerAddress, + signature, + }, + ); + } +} + +const types = [SignatureValidationError]; + +// Register the types we've defined. +for (const type of types) { + RevertError.registerType(type); +} From 55474c059977bcd5fb3ccdf0254d1689dc2361c7 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Mon, 22 Jun 2020 15:37:21 -0400 Subject: [PATCH 03/11] `@0x/types`: Add `ExchangeProxyMetaTransactionHash()`. --- packages/types/CHANGELOG.json | 9 +++++++++ packages/types/src/index.ts | 24 ++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/packages/types/CHANGELOG.json b/packages/types/CHANGELOG.json index 50fc417d7e..172164dd13 100644 --- a/packages/types/CHANGELOG.json +++ b/packages/types/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "version": "3.2.0", + "changes": [ + { + "note": "Add `ExchangeProxyMetaTransaction` and `SignedExchangeProxyMetaTransaction`", + "pr": 2610 + } + ] + }, { "timestamp": 1592969527, "version": "3.1.3", diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 5322971d33..f151959295 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -54,6 +54,30 @@ export interface SignedZeroExTransaction extends ZeroExTransaction { signature: string; } +/** + * Exchange Proxy meta transaction struct. + */ +export interface ExchangeProxyMetaTransaction { + signer: string; + sender: string; + minGasPrice: BigNumber; + maxGasPrice: BigNumber; + expirationTime: BigNumber; + salt: BigNumber; + callData: string; + value: BigNumber; + feeToken: string; + feeAmount: BigNumber; + domain: EIP712DomainWithDefaultSchema; +} + +/** + * `ExchangeProxyMetaTransaction` with `signature` field. + */ +export interface SignedExchangeProxyMetaTransaction extends ExchangeProxyMetaTransaction { + signature: string; +} + /** * Elliptic Curve signature */ From 1305f4314d5d2d2f617925dedf202cbedd9cee95 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Wed, 24 Jun 2020 12:46:02 -0400 Subject: [PATCH 04/11] `@0x/0x.js`: Export `ExchangeProxyMetaTransaction` and `SignedExchangeProxyMetaTransaction` --- packages/0x.js/CHANGELOG.json | 4 ++++ packages/0x.js/src/index.ts | 2 ++ 2 files changed, 6 insertions(+) diff --git a/packages/0x.js/CHANGELOG.json b/packages/0x.js/CHANGELOG.json index b2faa8050e..65102343b2 100644 --- a/packages/0x.js/CHANGELOG.json +++ b/packages/0x.js/CHANGELOG.json @@ -5,6 +5,10 @@ { "note": "Export `GethCallOverrides` type", "pr": 2620 + }, + { + "note": "Export `ExchangeProxyMetaTransaction` and `SignedExchangeProxyMetaTransaction`", + "pr": 2610 } ] }, diff --git a/packages/0x.js/src/index.ts b/packages/0x.js/src/index.ts index 8dccdf4561..00582a0310 100644 --- a/packages/0x.js/src/index.ts +++ b/packages/0x.js/src/index.ts @@ -82,6 +82,8 @@ export { EventCallback, IndexedFilterValues, DecodedLogEvent, + ExchangeProxyMetaTransaction, + SignedExchangeProxyMetaTransaction, } from '@0x/types'; export { From 297ff10c14ca296143110eb541c071bcdb1078c2 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Thu, 18 Jun 2020 09:49:35 -0400 Subject: [PATCH 05/11] `@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, + }, ); }, }; From 7da9ec2c75bf49cea18702735296d473d9c34b7e Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Thu, 25 Jun 2020 01:05:02 -0400 Subject: [PATCH 06/11] `@0x/contract-addresses`: Update ganache snapshot addresses for the Exchange Proxy. --- packages/contract-addresses/CHANGELOG.json | 4 ++++ packages/contract-addresses/addresses.json | 14 +++++++------- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/contract-addresses/CHANGELOG.json b/packages/contract-addresses/CHANGELOG.json index 35456d19e5..5bf60560a1 100644 --- a/packages/contract-addresses/CHANGELOG.json +++ b/packages/contract-addresses/CHANGELOG.json @@ -21,6 +21,10 @@ { "note": "Redeploy DFB on kovan", "pr": 2628 + }, + { + "note": "Update ganache snapshot Exchange Proxy addresses for MetaTransactions", + "pr": 2610 } ] }, diff --git a/packages/contract-addresses/addresses.json b/packages/contract-addresses/addresses.json index 4218f34553..3aa6bccc5b 100644 --- a/packages/contract-addresses/addresses.json +++ b/packages/contract-addresses/addresses.json @@ -218,15 +218,15 @@ "dexForwarderBridge": "0x0000000000000000000000000000000000000000", "multiBridge": "0x0000000000000000000000000000000000000000", "exchangeProxyGovernor": "0x0000000000000000000000000000000000000000", - "exchangeProxy": "0x4b8ce0fa221284de4aaa09be3e7bf6193444b786", - "exchangeProxyAllowanceTarget": "0xd6724bf180441a89d08ea3aeded2c995180b9a04", + "exchangeProxy": "0x2ebb94cc79d7d0f1195300aaf191d118f53292a8", + "exchangeProxyAllowanceTarget": "0x3eab3df72fd584b50184ff7d988a0d8f9328c866", "exchangeProxyTransformerDeployer": "0x5409ed021d9299bf6814279a6a1411a7e866a631", - "exchangeProxyFlashWallet": "0xdec8629610e2f4087bd9cc441d10ca8be0c6f6c5", + "exchangeProxyFlashWallet": "0x8362c3ebd90041b30ec45908332e592721642637", "transformers": { - "wethTransformer": "0xb125995f5a4766c451cd8c34c4f5cac89b724571", - "payTakerTransformer": "0x10a736a7b223f1fe1050264249d1abb975741e75", - "fillQuoteTransformer": "0x33def1aa867be09809f3a01ce41d5ec1888846c9", - "affiliateFeeTransformer": "0xc7124963ab16c33e5bf421d4c0090116622b3074" + "wethTransformer": "0x7209185959d7227fb77274e1e88151d7c4c368d3", + "payTakerTransformer": "0xc6b0d3c45a6b5092808196cb00df5c357d55e1d5", + "fillQuoteTransformer": "0xc7124963ab16c33e5bf421d4c0090116622b3074", + "affiliateFeeTransformer": "0x3f16ca81691dab9184cb4606c361d73c4fd2510a" } } } From 06eeb2088be2945c9c78b0fd2da3f6d3f65453a0 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Mon, 6 Jul 2020 11:43:37 -0400 Subject: [PATCH 07/11] `@0x/types`: Address review feedback. --- packages/types/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index f151959295..e5583f8a00 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -62,7 +62,7 @@ export interface ExchangeProxyMetaTransaction { sender: string; minGasPrice: BigNumber; maxGasPrice: BigNumber; - expirationTime: BigNumber; + expirationTimeSeconds: BigNumber; salt: BigNumber; callData: string; value: BigNumber; From bdc1dbf08afbedc054480ae01f9504de2aba04d2 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Mon, 6 Jul 2020 11:44:51 -0400 Subject: [PATCH 08/11] `@0x/order-utils`: Address review feedback. --- packages/order-utils/src/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/order-utils/src/constants.ts b/packages/order-utils/src/constants.ts index e8f52deea8..d4295a75f5 100644 --- a/packages/order-utils/src/constants.ts +++ b/packages/order-utils/src/constants.ts @@ -164,7 +164,7 @@ export const constants = { { name: 'sender', type: 'address' }, { name: 'minGasPrice', type: 'uint256' }, { name: 'maxGasPrice', type: 'uint256' }, - { name: 'expirationTime', type: 'uint256' }, + { name: 'expirationTimeSeconds', type: 'uint256' }, { name: 'salt', type: 'uint256' }, { name: 'callData', type: 'bytes' }, { name: 'value', type: 'uint256' }, From e8106f04b55846dcd10c5e87b2a9ff750a5e37bc Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Mon, 6 Jul 2020 12:29:23 -0400 Subject: [PATCH 09/11] `@0x/contracts-zero-ex`: Address review feedback. --- .../src/features/IMetaTransactions.sol | 16 ++--- .../src/features/MetaTransactions.sol | 70 +++++++++++++------ .../src/features/SimpleFunctionRegistry.sol | 2 +- .../src/migrations/FullMigration.sol | 26 +++---- .../src/migrations/InitialMigration.sol | 24 +++---- contracts/zero-ex/package.json | 4 +- contracts/zero-ex/src/migration.ts | 4 +- contracts/zero-ex/test/artifacts.ts | 4 +- .../test/features/meta_transactions_test.ts | 40 +++++++++-- .../test/features/signature_validator_test.ts | 5 +- contracts/zero-ex/test/full_migration_test.ts | 10 +-- .../zero-ex/test/initial_migration_test.ts | 6 +- contracts/zero-ex/test/wrappers.ts | 2 +- contracts/zero-ex/tsconfig.json | 2 +- 14 files changed, 135 insertions(+), 80 deletions(-) diff --git a/contracts/zero-ex/contracts/src/features/IMetaTransactions.sol b/contracts/zero-ex/contracts/src/features/IMetaTransactions.sol index 1052d048cc..16be3986b8 100644 --- a/contracts/zero-ex/contracts/src/features/IMetaTransactions.sol +++ b/contracts/zero-ex/contracts/src/features/IMetaTransactions.sol @@ -36,7 +36,7 @@ interface IMetaTransactions { // Maximum gas price. uint256 maxGasPrice; // MTX is invalid after this time. - uint256 expirationTime; + uint256 expirationTimeSeconds; // Nonce to make this MTX unique. uint256 salt; // Encoded call data to a function on the exchange proxy. @@ -65,33 +65,33 @@ interface IMetaTransactions { /// @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. + /// @return returnResult The ABI-encoded result of the underlying call. function executeMetaTransaction( MetaTransactionData calldata mtx, bytes calldata signature ) external payable - returns (bytes memory returnData); + returns (bytes memory returnResult); /// @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( + /// @return returnResults The ABI-encoded results of the underlying calls. + function batchExecuteMetaTransactions( MetaTransactionData[] calldata mtxs, bytes[] calldata signatures ) external payable - returns (bytes[] memory returnDatas); + returns (bytes[] memory returnResults); /// @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. + /// @return returnResult The ABI-encoded result of the underlying call. function _executeMetaTransaction( address sender, MetaTransactionData calldata mtx, @@ -99,7 +99,7 @@ interface IMetaTransactions { ) external payable - returns (bytes memory returnData); + returns (bytes memory returnResult); /// @dev Get the block at which a meta-transaction has been executed. /// @param mtx The meta-transaction. diff --git a/contracts/zero-ex/contracts/src/features/MetaTransactions.sol b/contracts/zero-ex/contracts/src/features/MetaTransactions.sol index 856b1831a8..55b73ed92d 100644 --- a/contracts/zero-ex/contracts/src/features/MetaTransactions.sol +++ b/contracts/zero-ex/contracts/src/features/MetaTransactions.sol @@ -73,7 +73,7 @@ contract MetaTransactions is "address sender," "uint256 minGasPrice," "uint256 maxGasPrice," - "uint256 expirationTime," + "uint256 expirationTimeSeconds," "uint256 salt," "bytes callData," "uint256 value," @@ -98,7 +98,7 @@ contract MetaTransactions is returns (bytes4 success) { _registerFeatureFunction(this.executeMetaTransaction.selector); - _registerFeatureFunction(this.executeMetaTransactions.selector); + _registerFeatureFunction(this.batchExecuteMetaTransactions.selector); _registerFeatureFunction(this._executeMetaTransaction.selector); _registerFeatureFunction(this.getMetaTransactionExecutedBlock.selector); _registerFeatureFunction(this.getMetaTransactionHashExecutedBlock.selector); @@ -109,7 +109,7 @@ contract MetaTransactions is /// @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. + /// @return returnResult The ABI-encoded result of the underlying call. function executeMetaTransaction( MetaTransactionData memory mtx, bytes memory signature @@ -117,7 +117,7 @@ contract MetaTransactions is public payable override - returns (bytes memory returnData) + returns (bytes memory returnResult) { return _executeMetaTransactionPrivate( msg.sender, @@ -129,15 +129,15 @@ contract MetaTransactions is /// @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( + /// @return returnResults The ABI-encoded results of the underlying calls. + function batchExecuteMetaTransactions( MetaTransactionData[] memory mtxs, bytes[] memory signatures ) public payable override - returns (bytes[] memory returnDatas) + returns (bytes[] memory returnResults) { if (mtxs.length != signatures.length) { LibMetaTransactionsRichErrors.InvalidMetaTransactionsArrayLengthsError( @@ -145,9 +145,9 @@ contract MetaTransactions is signatures.length ).rrevert(); } - returnDatas = new bytes[](mtxs.length); + returnResults = new bytes[](mtxs.length); for (uint256 i = 0; i < mtxs.length; ++i) { - returnDatas[i] = _executeMetaTransactionPrivate( + returnResults[i] = _executeMetaTransactionPrivate( msg.sender, mtxs[i], signatures[i] @@ -160,7 +160,7 @@ contract MetaTransactions is /// @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. + /// @return returnResult The ABI-encoded result of the underlying call. function _executeMetaTransaction( address sender, MetaTransactionData memory mtx, @@ -170,7 +170,7 @@ contract MetaTransactions is payable override onlySelf - returns (bytes memory returnData) + returns (bytes memory returnResult) { return _executeMetaTransactionPrivate(sender, mtx, signature); } @@ -214,7 +214,7 @@ contract MetaTransactions is mtx.sender, mtx.minGasPrice, mtx.maxGasPrice, - mtx.expirationTime, + mtx.expirationTimeSeconds, mtx.salt, keccak256(mtx.callData), mtx.value, @@ -227,14 +227,14 @@ contract MetaTransactions is /// @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. + /// @return returnResult The ABI-encoded result of the underlying call. function _executeMetaTransactionPrivate( address sender, MetaTransactionData memory mtx, bytes memory signature ) private - returns (bytes memory returnData) + returns (bytes memory returnResult) { ExecuteState memory state; state.sender = sender; @@ -252,7 +252,7 @@ contract MetaTransactions is // Execute the call based on the selector. state.selector = mtx.callData.readBytes4(0); if (state.selector == ITransformERC20.transformERC20.selector) { - returnData = _executeTransformERC20Call(state); + returnResult = _executeTransformERC20Call(state); } else { LibMetaTransactionsRichErrors .MetaTransactionUnsupportedFunctionError(state.hash, state.selector) @@ -290,12 +290,12 @@ contract MetaTransactions is ).rrevert(); } // Must not be expired. - if (state.mtx.expirationTime <= block.timestamp) { + if (state.mtx.expirationTimeSeconds <= block.timestamp) { LibMetaTransactionsRichErrors .MetaTransactionExpiredError( state.hash, block.timestamp, - state.mtx.expirationTime + state.mtx.expirationTimeSeconds ).rrevert(); } // Must have a valid gas price. @@ -349,12 +349,36 @@ contract MetaTransactions is /// the taker address. function _executeTransformERC20Call(ExecuteState memory state) private - returns (bytes memory returnData) + returns (bytes memory returnResult) { // 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. + // offset to transform it into the encoding for the equivalent single struct arg, + // since decoding a single struct arg consumes far less stack space than + // decoding multiple struct args. + + // Where the encoding for multiple args (with the seleector ommitted) + // would typically look like: + // | argument | offset | + // |--------------------------|---------| + // | inputToken | 0 | + // | outputToken | 32 | + // | inputTokenAmount | 64 | + // | minOutputTokenAmount | 96 | + // | transformations (offset) | 128 | = 32 + // | transformations (data) | 160 | + + // We will ABI-decode a single struct arg copy with the layout: + // | argument | offset | + // |--------------------------|---------| + // | (arg 1 offset) | 0 | = 32 + // | inputToken | 32 | + // | outputToken | 64 | + // | inputTokenAmount | 96 | + // | minOutputTokenAmount | 128 | + // | transformations (offset) | 160 | = 32 + // | transformations (data) | 192 | + TransformERC20Args memory args; { bytes memory encodedStructArgs = new bytes(state.mtx.callData.length - 4 + 32); @@ -397,15 +421,15 @@ contract MetaTransactions is /// Warning: Do not let unadulerated `callData` into this function. function _callSelf(bytes32 hash, bytes memory callData, uint256 value) private - returns (bytes memory returnData) + returns (bytes memory returnResult) { bool success; - (success, returnData) = address(this).call{value: value}(callData); + (success, returnResult) = address(this).call{value: value}(callData); if (!success) { LibMetaTransactionsRichErrors.MetaTransactionCallFailedError( hash, callData, - returnData + returnResult ).rrevert(); } } diff --git a/contracts/zero-ex/contracts/src/features/SimpleFunctionRegistry.sol b/contracts/zero-ex/contracts/src/features/SimpleFunctionRegistry.sol index 2979a945d0..f9da65102f 100644 --- a/contracts/zero-ex/contracts/src/features/SimpleFunctionRegistry.sol +++ b/contracts/zero-ex/contracts/src/features/SimpleFunctionRegistry.sol @@ -35,7 +35,7 @@ contract SimpleFunctionRegistry is ISimpleFunctionRegistry, 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); diff --git a/contracts/zero-ex/contracts/src/migrations/FullMigration.sol b/contracts/zero-ex/contracts/src/migrations/FullMigration.sol index 854a888f66..5cefc83643 100644 --- a/contracts/zero-ex/contracts/src/migrations/FullMigration.sol +++ b/contracts/zero-ex/contracts/src/migrations/FullMigration.sol @@ -49,20 +49,20 @@ contract FullMigration { address transformerDeployer; } - /// @dev The allowed caller of `deploy()`. - address public immutable deployer; + /// @dev The allowed caller of `initializeZeroEx()`. + address public immutable initializeCaller; /// @dev The initial migration contract. InitialMigration private _initialMigration; - /// @dev Instantiate this contract and set the allowed caller of `deploy()` - /// to `deployer`. - /// @param deployer_ The allowed caller of `deploy()`. - constructor(address payable deployer_) + /// @dev Instantiate this contract and set the allowed caller of `initializeZeroEx()` + /// to `initializeCaller`. + /// @param initializeCaller_ The allowed caller of `initializeZeroEx()`. + constructor(address payable initializeCaller_) public { - deployer = deployer_; + initializeCaller = initializeCaller_; // Create an initial migration contract with this contract set to the - // allowed deployer. + // allowed `initializeCaller`. _initialMigration = new InitialMigration(address(this)); } @@ -76,7 +76,7 @@ contract FullMigration { return address(_initialMigration); } - /// @dev Deploy the `ZeroEx` contract with the full feature set, + /// @dev Initialize 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 @@ -84,7 +84,7 @@ contract FullMigration { /// @param features Features to add to the proxy. /// @return _zeroEx The configured ZeroEx contract. Same as the `zeroEx` parameter. /// @param migrateOpts Parameters needed to initialize features. - function deploy( + function initializeZeroEx( address payable owner, ZeroEx zeroEx, Features memory features, @@ -93,10 +93,10 @@ contract FullMigration { public returns (ZeroEx _zeroEx) { - require(msg.sender == deployer, "FullMigration/INVALID_SENDER"); + require(msg.sender == initializeCaller, "FullMigration/INVALID_SENDER"); // Perform the initial migration with the owner set to this contract. - _initialMigration.deploy( + _initialMigration.initializeZeroEx( address(uint160(address(this))), zeroEx, InitialMigration.BootstrapFeatures({ @@ -117,7 +117,7 @@ contract FullMigration { return zeroEx; } - /// @dev Destroy this contract. Only callable from ourselves (from `deploy()`). + /// @dev Destroy this contract. Only callable from ourselves (from `initializeZeroEx()`). /// @param ethRecipient Receiver of any ETH in this contract. function die(address payable ethRecipient) external diff --git a/contracts/zero-ex/contracts/src/migrations/InitialMigration.sol b/contracts/zero-ex/contracts/src/migrations/InitialMigration.sol index 404ccfe765..8868a8a08c 100644 --- a/contracts/zero-ex/contracts/src/migrations/InitialMigration.sol +++ b/contracts/zero-ex/contracts/src/migrations/InitialMigration.sol @@ -35,29 +35,29 @@ contract InitialMigration { Ownable ownable; } - /// @dev The allowed caller of `deploy()`. In production, this would be + /// @dev The allowed caller of `initializeZeroEx()`. In production, this would be /// the governor. - address public immutable deployer; + address public immutable initializeCaller; /// @dev The real address of this contract. address private immutable _implementation; - /// @dev Instantiate this contract and set the allowed caller of `deploy()` - /// to `deployer_`. - /// @param deployer_ The allowed caller of `deploy()`. - constructor(address deployer_) public { - deployer = deployer_; + /// @dev Instantiate this contract and set the allowed caller of `initializeZeroEx()` + /// to `initializeCaller_`. + /// @param initializeCaller_ The allowed caller of `initializeZeroEx()`. + constructor(address initializeCaller_) public { + initializeCaller = initializeCaller_; _implementation = address(this); } - /// @dev Deploy the `ZeroEx` contract with the minimum feature set, + /// @dev Initialize the `ZeroEx` contract with the minimum feature set, /// transfers ownership to `owner`, then self-destructs. - /// Only callable by `deployer` set in the contstructor. + /// Only callable by `initializeCaller` 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 configured ZeroEx contract. Same as the `zeroEx` parameter. - function deploy( + function initializeZeroEx( address payable owner, ZeroEx zeroEx, BootstrapFeatures memory features @@ -66,8 +66,8 @@ contract InitialMigration { virtual returns (ZeroEx _zeroEx) { - // Must be called by the allowed deployer. - require(msg.sender == deployer, "InitialMigration/INVALID_SENDER"); + // Must be called by the allowed initializeCaller. + require(msg.sender == initializeCaller, "InitialMigration/INVALID_SENDER"); // Bootstrap the initial feature set. IBootstrap(address(zeroEx)).bootstrap( diff --git a/contracts/zero-ex/package.json b/contracts/zero-ex/package.json index f9a83055c9..19a77e7620 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,AffiliateFeeTransformerSignatureValidator,MetaTransactions", + "publicInterfaceContracts": "ZeroEx,FullMigration,InitialMigration,IFlashWallet,IAllowanceTarget,IERC20Transformer,IOwnable,ISimpleFunctionRegistry,ITokenSpender,ITransformERC20,FillQuoteTransformer,PayTakerTransformer,WethTransformer,Ownable,SimpleFunctionRegistry,TransformERC20,TokenSpender,AffiliateFeeTransformer,SignatureValidator,MetaTransactions", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.", - "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" + "abis": "./test/generated-artifacts/@(AffiliateFeeTransformer|AllowanceTarget|Bootstrap|FillQuoteTransformer|FixinCommon|FixinEIP712|FixinGasToken|FlashWallet|FullMigration|IAllowanceTarget|IBootstrap|IERC20Bridge|IERC20Transformer|IExchange|IFeature|IFlashWallet|IGasToken|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|TestFillQuoteTransformerBridge|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/migration.ts b/contracts/zero-ex/src/migration.ts index 3ec41160fd..8d211800e0 100644 --- a/contracts/zero-ex/src/migration.ts +++ b/contracts/zero-ex/src/migration.ts @@ -73,7 +73,7 @@ export async function initialMigrateAsync( migrator.address, ); const _features = await deployBootstrapFeaturesAsync(provider, txDefaults, features); - await migrator.deploy(owner, zeroEx.address, _features).awaitTransactionSuccessAsync(); + await migrator.initializeZeroEx(owner, zeroEx.address, _features).awaitTransactionSuccessAsync(); return zeroEx; } @@ -170,6 +170,6 @@ export async function fullMigrateAsync( transformerDeployer: txDefaults.from as string, ...opts, }; - await migrator.deploy(owner, zeroEx.address, _features, _opts).awaitTransactionSuccessAsync(); + await migrator.initializeZeroEx(owner, zeroEx.address, _features, _opts).awaitTransactionSuccessAsync(); return zeroEx; } diff --git a/contracts/zero-ex/test/artifacts.ts b/contracts/zero-ex/test/artifacts.ts index 84d616b778..6750466ef4 100644 --- a/contracts/zero-ex/test/artifacts.ts +++ b/contracts/zero-ex/test/artifacts.ts @@ -10,8 +10,8 @@ import * as AllowanceTarget from '../test/generated-artifacts/AllowanceTarget.js 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 FixinGasToken from '../test/generated-artifacts/FixinGasToken.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'; @@ -114,8 +114,8 @@ export const artifacts = { TokenSpender: TokenSpender as ContractArtifact, TransformERC20: TransformERC20 as ContractArtifact, FixinCommon: FixinCommon as ContractArtifact, - FixinGasToken: FixinGasToken as ContractArtifact, FixinEIP712: FixinEIP712 as ContractArtifact, + FixinGasToken: FixinGasToken as ContractArtifact, FullMigration: FullMigration as ContractArtifact, InitialMigration: InitialMigration as ContractArtifact, LibBootstrap: LibBootstrap as ContractArtifact, diff --git a/contracts/zero-ex/test/features/meta_transactions_test.ts b/contracts/zero-ex/test/features/meta_transactions_test.ts index 65bfb88ff6..3205822ea7 100644 --- a/contracts/zero-ex/test/features/meta_transactions_test.ts +++ b/contracts/zero-ex/test/features/meta_transactions_test.ts @@ -74,7 +74,7 @@ blockchainTests.resets('MetaTransactions feature', env => { sender, minGasPrice: getRandomInteger('2', '1e9'), maxGasPrice: getRandomInteger('1e9', '100e9'), - expirationTime: new BigNumber(Math.floor(_.now() / 1000) + 360), + expirationTimeSeconds: new BigNumber(Math.floor(_.now() / 1000) + 360), salt: new BigNumber(hexUtils.random()), callData: hexUtils.random(4), value: getRandomInteger(1, '1e18'), @@ -362,7 +362,7 @@ blockchainTests.resets('MetaTransactions feature', env => { it('fails if expired', async () => { const mtx = getRandomMetaTransaction({ - expirationTime: new BigNumber(Math.floor(_.now() / 1000 - 60)), + expirationTimeSeconds: new BigNumber(Math.floor(_.now() / 1000 - 60)), }); const mtxHash = getExchangeProxyMetaTransactionHash(mtx); const signature = await signMetaTransactionAsync(mtx); @@ -375,7 +375,7 @@ blockchainTests.resets('MetaTransactions feature', env => { new ZeroExRevertErrors.MetaTransactions.MetaTransactionExpiredError( mtxHash, undefined, - mtx.expirationTime, + mtx.expirationTimeSeconds, ), ); }); @@ -425,7 +425,7 @@ blockchainTests.resets('MetaTransactions feature', env => { }); }); - describe('executeMetaTransactions()', () => { + describe('batchExecuteMetaTransactions()', () => { it('can execute multiple transactions', async () => { const mtxs = _.times(2, i => { const args = getRandomTransformERC20Args(); @@ -447,9 +447,39 @@ blockchainTests.resets('MetaTransactions feature', env => { 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); + const rawResults = await feature.batchExecuteMetaTransactions(mtxs, signatures).callAsync(callOpts); expect(rawResults).to.eql(mtxs.map(() => RAW_SUCCESS_RESULT)); }); + + it('cannot execute the same transaction twice', async () => { + const mtx = (() => { + const args = getRandomTransformERC20Args(); + return getRandomMetaTransaction({ + signer: _.sampleSize(signers, 1)[0], + callData: transformERC20Feature + .transformERC20( + args.inputToken, + args.outputToken, + args.inputTokenAmount, + args.minOutputTokenAmount, + args.transformations, + ) + .getABIEncodedTransactionData(), + }); + })(); + const mtxHash = getExchangeProxyMetaTransactionHash(mtx); + const mtxs = _.times(2, () => mtx); + 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 block = await env.web3Wrapper.getBlockNumberAsync(); + const tx = feature.batchExecuteMetaTransactions(mtxs, signatures).callAsync(callOpts); + return expect(tx).to.revertWith( + new ZeroExRevertErrors.MetaTransactions.MetaTransactionAlreadyExecutedError(mtxHash, block), + ); + }); }); describe('getMetaTransactionExecutedBlock()', () => { diff --git a/contracts/zero-ex/test/features/signature_validator_test.ts b/contracts/zero-ex/test/features/signature_validator_test.ts index 5a2387bf43..99fdd79e9b 100644 --- a/contracts/zero-ex/test/features/signature_validator_test.ts +++ b/contracts/zero-ex/test/features/signature_validator_test.ts @@ -66,13 +66,12 @@ blockchainTests.resets('SignatureValidator feature', env => { 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(); + const tx = feature.validateHashSignature(hash, signer, signature).callAsync(); return expect(tx).to.revertWith( new ZeroExRevertErrors.SignatureValidator.SignatureValidationError( ZeroExRevertErrors.SignatureValidator.SignatureValidationErrorCodes.InvalidLength, hash, - notSigner, + signer, signature, ), ); diff --git a/contracts/zero-ex/test/full_migration_test.ts b/contracts/zero-ex/test/full_migration_test.ts index ffb4dc8231..107e4f6b28 100644 --- a/contracts/zero-ex/test/full_migration_test.ts +++ b/contracts/zero-ex/test/full_migration_test.ts @@ -44,7 +44,9 @@ blockchainTests.resets('Full migration', env => { await migrator.getBootstrapper().callAsync(), ); features = await deployFullFeaturesAsync(env.provider, env.txDefaults, zeroEx.address); - await migrator.deploy(owner, zeroEx.address, features, { transformerDeployer }).awaitTransactionSuccessAsync(); + await migrator + .initializeZeroEx(owner, zeroEx.address, features, { transformerDeployer }) + .awaitTransactionSuccessAsync(); }); it('ZeroEx has the correct owner', async () => { @@ -58,10 +60,10 @@ blockchainTests.resets('Full migration', env => { expect(dieRecipient).to.eq(owner); }); - it('Non-deployer cannot call deploy()', async () => { + it('Non-deployer cannot call initializeZeroEx()', async () => { const notDeployer = randomAddress(); const tx = migrator - .deploy(owner, zeroEx.address, features, { transformerDeployer }) + .initializeZeroEx(owner, zeroEx.address, features, { transformerDeployer }) .callAsync({ from: notDeployer }); return expect(tx).to.revertWith('FullMigration/INVALID_SENDER'); }); @@ -89,7 +91,7 @@ blockchainTests.resets('Full migration', env => { contractType: IMetaTransactionsContract, fns: [ 'executeMetaTransaction', - 'executeMetaTransactions', + 'batchExecuteMetaTransactions', '_executeMetaTransaction', 'getMetaTransactionExecutedBlock', 'getMetaTransactionHashExecutedBlock', diff --git a/contracts/zero-ex/test/initial_migration_test.ts b/contracts/zero-ex/test/initial_migration_test.ts index 442a9bfbd9..f9985ea4c2 100644 --- a/contracts/zero-ex/test/initial_migration_test.ts +++ b/contracts/zero-ex/test/initial_migration_test.ts @@ -42,7 +42,7 @@ blockchainTests.resets('Initial migration', env => { artifacts, migrator.address, ); - await migrator.deploy(owner, zeroEx.address, features).awaitTransactionSuccessAsync(); + await migrator.initializeZeroEx(owner, zeroEx.address, features).awaitTransactionSuccessAsync(); }); it('Self-destructs after deployment', async () => { @@ -50,9 +50,9 @@ blockchainTests.resets('Initial migration', env => { expect(dieRecipient).to.eq(owner); }); - it('Non-deployer cannot call deploy()', async () => { + it('Non-deployer cannot call initializeZeroEx()', async () => { const notDeployer = randomAddress(); - const tx = migrator.deploy(owner, zeroEx.address, features).callAsync({ from: notDeployer }); + const tx = migrator.initializeZeroEx(owner, zeroEx.address, features).callAsync({ from: notDeployer }); return expect(tx).to.revertWith('InitialMigration/INVALID_SENDER'); }); diff --git a/contracts/zero-ex/test/wrappers.ts b/contracts/zero-ex/test/wrappers.ts index 96d6706e23..b5827399e8 100644 --- a/contracts/zero-ex/test/wrappers.ts +++ b/contracts/zero-ex/test/wrappers.ts @@ -8,8 +8,8 @@ export * from '../test/generated-wrappers/allowance_target'; 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/fixin_gas_token'; export * from '../test/generated-wrappers/flash_wallet'; export * from '../test/generated-wrappers/full_migration'; export * from '../test/generated-wrappers/i_allowance_target'; diff --git a/contracts/zero-ex/tsconfig.json b/contracts/zero-ex/tsconfig.json index 11585e644f..caa5d8fca0 100644 --- a/contracts/zero-ex/tsconfig.json +++ b/contracts/zero-ex/tsconfig.json @@ -28,8 +28,8 @@ "test/generated-artifacts/Bootstrap.json", "test/generated-artifacts/FillQuoteTransformer.json", "test/generated-artifacts/FixinCommon.json", - "test/generated-artifacts/FixinGasToken.json", "test/generated-artifacts/FixinEIP712.json", + "test/generated-artifacts/FixinGasToken.json", "test/generated-artifacts/FlashWallet.json", "test/generated-artifacts/FullMigration.json", "test/generated-artifacts/IAllowanceTarget.json", From a55e8b268c7e8b6c219c9f5c5cbf61d34d522cb2 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Mon, 6 Jul 2020 14:32:51 -0400 Subject: [PATCH 10/11] `@0x/contracts-zero-ex`: Fix linter errors. --- contracts/zero-ex/test/features/meta_transactions_test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/zero-ex/test/features/meta_transactions_test.ts b/contracts/zero-ex/test/features/meta_transactions_test.ts index 3205822ea7..35020bf632 100644 --- a/contracts/zero-ex/test/features/meta_transactions_test.ts +++ b/contracts/zero-ex/test/features/meta_transactions_test.ts @@ -469,10 +469,10 @@ blockchainTests.resets('MetaTransactions feature', env => { })(); const mtxHash = getExchangeProxyMetaTransactionHash(mtx); const mtxs = _.times(2, () => mtx); - const signatures = await Promise.all(mtxs.map(async mtx => signMetaTransactionAsync(mtx))); + const signatures = await Promise.all(mtxs.map(async m => signMetaTransactionAsync(m))); const callOpts = { - gasPrice: BigNumber.max(...mtxs.map(mtx => mtx.minGasPrice)), - value: BigNumber.sum(...mtxs.map(mtx => mtx.value)), + gasPrice: BigNumber.max(...mtxs.map(m => m.minGasPrice)), + value: BigNumber.sum(...mtxs.map(m => m.value)), }; const block = await env.web3Wrapper.getBlockNumberAsync(); const tx = feature.batchExecuteMetaTransactions(mtxs, signatures).callAsync(callOpts); From a47795fd3f8a175b9dd9b52249050d854609f294 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Fri, 10 Jul 2020 03:05:56 -0400 Subject: [PATCH 11/11] `@0x/contract-wrappers-test`: Increase test timeout. --- packages/contract-wrappers-test/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/contract-wrappers-test/package.json b/packages/contract-wrappers-test/package.json index e0bb212f49..6ea45dab7f 100644 --- a/packages/contract-wrappers-test/package.json +++ b/packages/contract-wrappers-test/package.json @@ -15,7 +15,7 @@ "fix": "tslint --fix --format stylish --project .--exclude **/lib/**/*", "test:circleci": "run-s test:coverage", "test": "yarn run_mocha", - "run_mocha": "mocha --require source-map-support/register --require make-promises-safe lib/test/**/*_test.js lib/test/global_hooks.js --timeout 20000 --bail --exit", + "run_mocha": "mocha --require source-map-support/register --require make-promises-safe lib/test/**/*_test.js lib/test/global_hooks.js --timeout 30000 --bail --exit", "test:coverage": "nyc npm run test --all && yarn coverage:report:lcov", "coverage:report:lcov": "nyc report --reporter=text-lcov > coverage/lcov.info", "prettier": "prettier --write **/* --config ../../.prettierrc",