diff options
Diffstat (limited to 'packages/order-utils')
-rw-r--r-- | packages/order-utils/package.json | 21 | ||||
-rw-r--r-- | packages/order-utils/src/artifacts.ts | 10 | ||||
-rw-r--r-- | packages/order-utils/src/assert.ts | 14 | ||||
-rw-r--r-- | packages/order-utils/src/asset_proxy_utils.ts | 151 | ||||
-rw-r--r-- | packages/order-utils/src/crypto.ts | 46 | ||||
-rw-r--r-- | packages/order-utils/src/formatters.ts | 23 | ||||
-rw-r--r-- | packages/order-utils/src/index.ts | 9 | ||||
-rw-r--r-- | packages/order-utils/src/order_factory.ts | 76 | ||||
-rw-r--r-- | packages/order-utils/src/order_hash.ts | 183 | ||||
-rw-r--r-- | packages/order-utils/src/order_state_utils.ts | 32 | ||||
-rw-r--r-- | packages/order-utils/src/remaining_fillable_calculator.ts | 12 | ||||
-rw-r--r-- | packages/order-utils/src/signature_utils.ts | 237 | ||||
-rw-r--r-- | packages/order-utils/src/types.ts | 22 | ||||
-rw-r--r-- | packages/order-utils/src/utils.ts | 9 | ||||
-rw-r--r-- | packages/order-utils/test/order_hash_test.ts | 47 | ||||
-rw-r--r-- | packages/order-utils/test/signature_utils_test.ts | 134 | ||||
-rw-r--r-- | packages/order-utils/test/utils/web3_wrapper.ts | 2 |
17 files changed, 778 insertions, 250 deletions
diff --git a/packages/order-utils/package.json b/packages/order-utils/package.json index ddb95ba3f..2d7e7ba66 100644 --- a/packages/order-utils/package.json +++ b/packages/order-utils/package.json @@ -1,6 +1,6 @@ { "name": "@0xproject/order-utils", - "version": "0.0.5", + "version": "0.0.6", "engines": { "node": ">=6.12" }, @@ -9,13 +9,17 @@ "types": "lib/src/index.d.ts", "scripts": { "watch": "tsc -w", - "build": "tsc && copyfiles -u 3 './lib/src/monorepo_scripts/**/*' ./scripts", - "test": "run-s clean build run_mocha", + "build": "run-s clean update_artifacts generate_contract_wrappers transpile copy_monorepo_scripts", + "transpile": "tsc", + "copy_monorepo_scripts": "copyfiles -u 3 './lib/src/monorepo_scripts/**/*' ./scripts", + "generate_contract_wrappers": "abi-gen --abis 'lib/src/artifacts/@(Exchange|IWallet|IValidator).json' --template ../contract_templates/contract.handlebars --partials '../contract_templates/partials/**/*.handlebars' --output src/generated_contract_wrappers --backend ethers && prettier --write 'src/generated_contract_wrappers/**.ts'", + "update_artifacts": "for i in ${npm_package_config_contracts}; do copyfiles -u 4 ../migrations/artifacts/2.0.0/$i.json lib/src/artifacts; done;", + "test": "run-s build run_mocha", "test:circleci": "yarn test:coverage", "run_mocha": "mocha lib/test/**/*_test.js --bail --exit", "test:coverage": "nyc npm run test --all && yarn coverage:report:lcov", "coverage:report:lcov": "nyc report --reporter=text-lcov > coverage/lcov.info", - "clean": "shx rm -rf lib scripts", + "clean": "shx rm -rf lib scripts lib/src/artifacts src/generated_contract_wrappers", "lint": "tslint --project .", "manual:postpublish": "yarn build; node ./scripts/postpublish.js", "docs:stage": "node scripts/stage_docs.js", @@ -23,6 +27,7 @@ "upload_docs_json": "aws s3 cp generated_docs/index.json $S3_URL --profile 0xproject --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers --content-type application/json" }, "config": { + "contracts": "IWallet IValidator Exchange", "postpublish": { "docPublishConfigs": { "extraFileIncludes": [ @@ -66,15 +71,19 @@ }, "dependencies": { "@0xproject/assert": "^0.2.10", - "@0xproject/json-schemas": "0.7.22", - "@0xproject/types": "0.7.0", + "@0xproject/base-contract": "^0.3.2", + "@0xproject/json-schemas": "1.0.0", + "@0xproject/sol-compiler": "^0.5.0", + "@0xproject/types": "^1.0.0", "@0xproject/typescript-typings": "^0.3.2", "@0xproject/utils": "^0.6.2", "@0xproject/web3-wrapper": "^0.6.4", "@types/node": "^8.0.53", "bn.js": "^4.11.8", + "ethereum-types": "^0.0.1", "ethereumjs-abi": "^0.6.4", "ethereumjs-util": "^5.1.1", + "ethers": "^3.0.15", "lodash": "^4.17.4" }, "publishConfig": { diff --git a/packages/order-utils/src/artifacts.ts b/packages/order-utils/src/artifacts.ts new file mode 100644 index 000000000..f6fd00472 --- /dev/null +++ b/packages/order-utils/src/artifacts.ts @@ -0,0 +1,10 @@ +import { Artifact } from '@0xproject/types'; + +import * as Exchange from './artifacts/Exchange.json'; +import * as IValidator from './artifacts/IValidator.json'; +import * as IWallet from './artifacts/IWallet.json'; +export const artifacts = { + Exchange: (Exchange as any) as Artifact, + IWallet: (IWallet as any) as Artifact, + IValidator: (IValidator as any) as Artifact, +}; diff --git a/packages/order-utils/src/assert.ts b/packages/order-utils/src/assert.ts index 5ac402e7e..a1318b9b8 100644 --- a/packages/order-utils/src/assert.ts +++ b/packages/order-utils/src/assert.ts @@ -3,12 +3,12 @@ import { assert as sharedAssert } from '@0xproject/assert'; // tslint:disable-next-line:no-unused-variable import { Schema } from '@0xproject/json-schemas'; // tslint:disable-next-line:no-unused-variable -import { ECSignature } from '@0xproject/types'; +import { ECSignature, SignatureType } from '@0xproject/types'; import { BigNumber } from '@0xproject/utils'; import { Web3Wrapper } from '@0xproject/web3-wrapper'; import * as _ from 'lodash'; -import { isValidSignature } from './signature_utils'; +import { utils } from './utils'; export const assert = { ...sharedAssert, @@ -24,4 +24,14 @@ export const assert = { `Specified ${variableName} ${senderAddressHex} isn't available through the supplied web3 provider`, ); }, + isOneOfExpectedSignatureTypes(signature: string, signatureTypes: SignatureType[]): void { + sharedAssert.isHexString('signature', signature); + const signatureTypeIndexIfExists = utils.getSignatureTypeIndexIfExists(signature); + const isExpectedSignatureType = _.includes(signatureTypes, signatureTypeIndexIfExists); + if (!isExpectedSignatureType) { + throw new Error( + `Unexpected signatureType: ${signatureTypeIndexIfExists}. Valid signature types: ${signatureTypes}`, + ); + } + }, }; diff --git a/packages/order-utils/src/asset_proxy_utils.ts b/packages/order-utils/src/asset_proxy_utils.ts new file mode 100644 index 000000000..55f2d56df --- /dev/null +++ b/packages/order-utils/src/asset_proxy_utils.ts @@ -0,0 +1,151 @@ +import { AssetProxyId, ERC20ProxyData, ERC721ProxyData, ProxyData } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import BN = require('bn.js'); +import ethUtil = require('ethereumjs-util'); + +const ERC20_PROXY_METADATA_BYTE_LENGTH = 21; +const ERC721_PROXY_METADATA_BYTE_LENGTH = 53; + +export const assetProxyUtils = { + encodeAssetProxyId(assetProxyId: AssetProxyId): Buffer { + return ethUtil.toBuffer(assetProxyId); + }, + decodeAssetProxyId(encodedAssetProxyId: Buffer): AssetProxyId { + return ethUtil.bufferToInt(encodedAssetProxyId); + }, + encodeAddress(address: string): Buffer { + if (!ethUtil.isValidAddress(address)) { + throw new Error(`Invalid Address: ${address}`); + } + const encodedAddress = ethUtil.toBuffer(address); + return encodedAddress; + }, + decodeAddress(encodedAddress: Buffer): string { + const address = ethUtil.bufferToHex(encodedAddress); + if (!ethUtil.isValidAddress(address)) { + throw new Error(`Invalid Address: ${address}`); + } + return address; + }, + encodeUint256(value: BigNumber): Buffer { + const base = 10; + const formattedValue = new BN(value.toString(base)); + const encodedValue = ethUtil.toBuffer(formattedValue); + // tslint:disable-next-line:custom-no-magic-numbers + const paddedValue = ethUtil.setLengthLeft(encodedValue, 32); + return paddedValue; + }, + decodeUint256(encodedValue: Buffer): BigNumber { + const formattedValue = ethUtil.bufferToHex(encodedValue); + const value = new BigNumber(formattedValue, 16); + return value; + }, + encodeERC20ProxyData(tokenAddress: string): string { + const encodedAssetProxyId = assetProxyUtils.encodeAssetProxyId(AssetProxyId.ERC20); + const encodedAddress = assetProxyUtils.encodeAddress(tokenAddress); + const encodedMetadata = Buffer.concat([encodedAddress, encodedAssetProxyId]); + const encodedMetadataHex = ethUtil.bufferToHex(encodedMetadata); + return encodedMetadataHex; + }, + decodeERC20ProxyData(proxyData: string): ERC20ProxyData { + const encodedProxyMetadata = ethUtil.toBuffer(proxyData); + if (encodedProxyMetadata.byteLength !== ERC20_PROXY_METADATA_BYTE_LENGTH) { + throw new Error( + `Could not decode ERC20 Proxy Data. Expected length of encoded data to be 21. Got ${ + encodedProxyMetadata.byteLength + }`, + ); + } + const encodedAssetProxyId = encodedProxyMetadata.slice(-1); + const assetProxyId = assetProxyUtils.decodeAssetProxyId(encodedAssetProxyId); + if (assetProxyId !== AssetProxyId.ERC20) { + throw new Error( + `Could not decode ERC20 Proxy Data. Expected Asset Proxy Id to be ERC20 (${ + AssetProxyId.ERC20 + }), but got ${assetProxyId}`, + ); + } + const addressOffset = ERC20_PROXY_METADATA_BYTE_LENGTH - 1; + const encodedTokenAddress = encodedProxyMetadata.slice(0, addressOffset); + const tokenAddress = assetProxyUtils.decodeAddress(encodedTokenAddress); + const erc20ProxyData = { + assetProxyId, + tokenAddress, + }; + return erc20ProxyData; + }, + encodeERC721ProxyData(tokenAddress: string, tokenId: BigNumber): string { + const encodedAssetProxyId = assetProxyUtils.encodeAssetProxyId(AssetProxyId.ERC721); + const encodedAddress = assetProxyUtils.encodeAddress(tokenAddress); + const encodedTokenId = assetProxyUtils.encodeUint256(tokenId); + const encodedMetadata = Buffer.concat([encodedAddress, encodedTokenId, encodedAssetProxyId]); + const encodedMetadataHex = ethUtil.bufferToHex(encodedMetadata); + return encodedMetadataHex; + }, + decodeERC721ProxyData(proxyData: string): ERC721ProxyData { + const encodedProxyMetadata = ethUtil.toBuffer(proxyData); + if (encodedProxyMetadata.byteLength !== ERC721_PROXY_METADATA_BYTE_LENGTH) { + throw new Error( + `Could not decode ERC20 Proxy Data. Expected length of encoded data to be 53. Got ${ + encodedProxyMetadata.byteLength + }`, + ); + } + const encodedAssetProxyId = encodedProxyMetadata.slice(-1); + const assetProxyId = assetProxyUtils.decodeAssetProxyId(encodedAssetProxyId); + if (assetProxyId !== AssetProxyId.ERC721) { + throw new Error( + `Could not decode ERC721 Proxy Data. Expected Asset Proxy Id to be ERC721 (${ + AssetProxyId.ERC721 + }), but got ${assetProxyId}`, + ); + } + const addressOffset = ERC20_PROXY_METADATA_BYTE_LENGTH - 1; + const encodedTokenAddress = encodedProxyMetadata.slice(0, addressOffset); + const tokenAddress = assetProxyUtils.decodeAddress(encodedTokenAddress); + const tokenIdOffset = ERC721_PROXY_METADATA_BYTE_LENGTH - 1; + const encodedTokenId = encodedProxyMetadata.slice(addressOffset, tokenIdOffset); + const tokenId = assetProxyUtils.decodeUint256(encodedTokenId); + const erc721ProxyData = { + assetProxyId, + tokenAddress, + tokenId, + }; + return erc721ProxyData; + }, + decodeProxyDataId(proxyData: string): AssetProxyId { + const encodedProxyMetadata = ethUtil.toBuffer(proxyData); + if (encodedProxyMetadata.byteLength < 1) { + throw new Error( + `Could not decode Proxy Data. Expected length of encoded data to be at least 1. Got ${ + encodedProxyMetadata.byteLength + }`, + ); + } + const encodedAssetProxyId = encodedProxyMetadata.slice(-1); + const assetProxyId = assetProxyUtils.decodeAssetProxyId(encodedAssetProxyId); + return assetProxyId; + }, + decodeProxyData(proxyData: string): ProxyData { + const assetProxyId = assetProxyUtils.decodeProxyDataId(proxyData); + switch (assetProxyId) { + case AssetProxyId.ERC20: + const erc20ProxyData = assetProxyUtils.decodeERC20ProxyData(proxyData); + const generalizedERC20ProxyData = { + assetProxyId, + tokenAddress: erc20ProxyData.tokenAddress, + }; + return generalizedERC20ProxyData; + case AssetProxyId.ERC721: + const erc721ProxyData = assetProxyUtils.decodeERC721ProxyData(proxyData); + const generaliedERC721ProxyData = { + assetProxyId, + tokenAddress: erc721ProxyData.tokenAddress, + data: erc721ProxyData.tokenId, + }; + return generaliedERC721ProxyData; + default: + throw new Error(`Unrecognized asset proxy id: ${assetProxyId}`); + } + }, +}; diff --git a/packages/order-utils/src/crypto.ts b/packages/order-utils/src/crypto.ts new file mode 100644 index 000000000..517ca2840 --- /dev/null +++ b/packages/order-utils/src/crypto.ts @@ -0,0 +1,46 @@ +import BN = require('bn.js'); +import ABI = require('ethereumjs-abi'); +import ethUtil = require('ethereumjs-util'); +import * as _ from 'lodash'; + +export const crypto = { + /** + * We convert types from JS to Solidity as follows: + * BigNumber -> uint256 + * number -> uint8 + * string -> string + * boolean -> bool + * valid Ethereum address -> address + */ + solSHA3(args: any[]): Buffer { + return crypto._solHash(args, ABI.soliditySHA3); + }, + solSHA256(args: any[]): Buffer { + return crypto._solHash(args, ABI.soliditySHA256); + }, + _solHash(args: any[], hashFunction: (types: string[], values: any[]) => Buffer): Buffer { + const argTypes: string[] = []; + _.each(args, (arg, i) => { + const isNumber = _.isFinite(arg); + if (isNumber) { + argTypes.push('uint8'); + } else if (arg.isBigNumber) { + argTypes.push('uint256'); + const base = 10; + args[i] = new BN(arg.toString(base), base); + } else if (ethUtil.isValidAddress(arg)) { + argTypes.push('address'); + } else if (_.isString(arg)) { + argTypes.push('string'); + } else if (_.isBuffer(arg)) { + argTypes.push('bytes'); + } else if (_.isBoolean(arg)) { + argTypes.push('bool'); + } else { + throw new Error(`Unable to guess arg type: ${arg}`); + } + }); + const hash = hashFunction(argTypes, args); + return hash; + }, +}; diff --git a/packages/order-utils/src/formatters.ts b/packages/order-utils/src/formatters.ts deleted file mode 100644 index 2b6f4ddb7..000000000 --- a/packages/order-utils/src/formatters.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Order, OrderAddresses, OrderValues } from '@0xproject/types'; -import { BigNumber } from '@0xproject/utils'; - -export const formatters = { - getOrderAddressesAndValues(order: Order): [OrderAddresses, OrderValues] { - const orderAddresses: OrderAddresses = [ - order.maker, - order.taker, - order.makerTokenAddress, - order.takerTokenAddress, - order.feeRecipient, - ]; - const orderValues: OrderValues = [ - order.makerTokenAmount, - order.takerTokenAmount, - order.makerFee, - order.takerFee, - order.expirationUnixTimestampSec, - order.salt, - ]; - return [orderAddresses, orderValues]; - }, -}; diff --git a/packages/order-utils/src/index.ts b/packages/order-utils/src/index.ts index e9cea95ed..b844fbfcb 100644 --- a/packages/order-utils/src/index.ts +++ b/packages/order-utils/src/index.ts @@ -1,11 +1,12 @@ -export { getOrderHashHex, isValidOrderHash } from './order_hash'; -export { isValidSignature, signOrderHashAsync } from './signature_utils'; +export { orderHashUtils } from './order_hash'; +export { isValidSignatureAsync, ecSignOrderHashAsync, addSignedMessagePrefix } from './signature_utils'; export { orderFactory } from './order_factory'; export { constants } from './constants'; +export { crypto } from './crypto'; export { generatePseudoRandomSalt } from './salt'; -export { OrderError } from './types'; -export { formatters } from './formatters'; +export { OrderError, MessagePrefixType, MessagePrefixOpts } from './types'; export { AbstractBalanceAndProxyAllowanceFetcher } from './abstract/abstract_balance_and_proxy_allowance_fetcher'; export { AbstractOrderFilledCancelledFetcher } from './abstract/abstract_order_filled_cancelled_fetcher'; export { RemainingFillableCalculator } from './remaining_fillable_calculator'; export { OrderStateUtils } from './order_state_utils'; +export { assetProxyUtils } from './asset_proxy_utils'; diff --git a/packages/order-utils/src/order_factory.ts b/packages/order-utils/src/order_factory.ts index 2759aac81..678336ac5 100644 --- a/packages/order-utils/src/order_factory.ts +++ b/packages/order-utils/src/order_factory.ts @@ -1,49 +1,69 @@ -import { Provider, SignedOrder } from '@0xproject/types'; +import { ECSignature, SignedOrder } from '@0xproject/types'; import { BigNumber } from '@0xproject/utils'; +import { Provider } from 'ethereum-types'; +import * as ethUtil from 'ethereumjs-util'; import * as _ from 'lodash'; -import { getOrderHashHex } from './order_hash'; +import { orderHashUtils } from './order_hash'; import { generatePseudoRandomSalt } from './salt'; -import { signOrderHashAsync } from './signature_utils'; - -const SHOULD_ADD_PERSONAL_MESSAGE_PREFIX = false; +import { ecSignOrderHashAsync } from './signature_utils'; +import { MessagePrefixType } from './types'; export const orderFactory = { async createSignedOrderAsync( provider: Provider, - maker: string, - taker: string, + makerAddress: string, + takerAddress: string, + senderAddress: string, makerFee: BigNumber, takerFee: BigNumber, - makerTokenAmount: BigNumber, - makerTokenAddress: string, - takerTokenAmount: BigNumber, - takerTokenAddress: string, - exchangeContractAddress: string, - feeRecipient: string, - expirationUnixTimestampSecIfExists?: BigNumber, + makerAssetAmount: BigNumber, + makerAssetData: string, + takerAssetAmount: BigNumber, + takerAssetData: string, + exchangeAddress: string, + feeRecipientAddress: string, + expirationTimeSecondsIfExists?: BigNumber, ): Promise<SignedOrder> { const defaultExpirationUnixTimestampSec = new BigNumber(2524604400); // Close to infinite - const expirationUnixTimestampSec = _.isUndefined(expirationUnixTimestampSecIfExists) + const expirationTimeSeconds = _.isUndefined(expirationTimeSecondsIfExists) ? defaultExpirationUnixTimestampSec - : expirationUnixTimestampSecIfExists; + : expirationTimeSecondsIfExists; const order = { - maker, - taker, + makerAddress, + takerAddress, + senderAddress, makerFee, takerFee, - makerTokenAmount, - takerTokenAmount, - makerTokenAddress, - takerTokenAddress, + makerAssetAmount, + takerAssetAmount, + makerAssetData, + takerAssetData, salt: generatePseudoRandomSalt(), - exchangeContractAddress, - feeRecipient, - expirationUnixTimestampSec, + exchangeAddress, + feeRecipientAddress, + expirationTimeSeconds, + }; + const orderHash = orderHashUtils.getOrderHashHex(order); + const messagePrefixOpts = { + prefixType: MessagePrefixType.EthSign, + shouldAddPrefixBeforeCallingEthSign: false, }; - const orderHash = getOrderHashHex(order); - const ecSignature = await signOrderHashAsync(provider, orderHash, maker, SHOULD_ADD_PERSONAL_MESSAGE_PREFIX); - const signedOrder: SignedOrder = _.assign(order, { ecSignature }); + const ecSignature = await ecSignOrderHashAsync(provider, orderHash, makerAddress, messagePrefixOpts); + const signature = getVRSHexString(ecSignature); + const signedOrder: SignedOrder = _.assign(order, { signature }); return signedOrder; }, }; + +function getVRSHexString(ecSignature: ECSignature): string { + const vrs = `0x${intToHex(ecSignature.v)}${ethUtil.stripHexPrefix(ecSignature.r)}${ethUtil.stripHexPrefix( + ecSignature.s, + )}`; + return vrs; +} + +function intToHex(i: number): string { + const hex = ethUtil.bufferToHex(ethUtil.toBuffer(i)); + return hex; +} diff --git a/packages/order-utils/src/order_hash.ts b/packages/order-utils/src/order_hash.ts index 108344a04..2ef746ef8 100644 --- a/packages/order-utils/src/order_hash.ts +++ b/packages/order-utils/src/order_hash.ts @@ -1,90 +1,117 @@ import { schemas, SchemaValidator } from '@0xproject/json-schemas'; -import { Order, SignedOrder, SolidityTypes } from '@0xproject/types'; +import { Order, SignedOrder } from '@0xproject/types'; import { BigNumber } from '@0xproject/utils'; import BN = require('bn.js'); +import { SolidityTypes } from 'ethereum-types'; import * as ethABI from 'ethereumjs-abi'; import * as ethUtil from 'ethereumjs-util'; import * as _ from 'lodash'; import { assert } from './assert'; +import { crypto } from './crypto'; -const INVALID_TAKER_FORMAT = 'instance.taker is not of a type(s) string'; +const INVALID_TAKER_FORMAT = 'instance.takerAddress is not of a type(s) string'; -/** - * Converts BigNumber instance to BN - * The only reason we convert to BN is to remain compatible with `ethABI.soliditySHA3` that - * expects values of Solidity type `uint` to be passed as type `BN`. - * We do not use BN anywhere else in the codebase. - */ -function bigNumberToBN(value: BigNumber): BN { - const base = 10; - return new BN(value.toString(), base); -} - -/** - * Computes the orderHash for a supplied order. - * @param order An object that conforms to the Order or SignedOrder interface definitions. - * @return The resulting orderHash from hashing the supplied order. - */ -export function getOrderHashHex(order: Order | SignedOrder): string { - try { - assert.doesConformToSchema('order', order, schemas.orderSchema); - } catch (error) { - if (_.includes(error.message, INVALID_TAKER_FORMAT)) { - const errMsg = - 'Order taker must be of type string. If you want anyone to be able to fill an order - pass ZeroEx.NULL_ADDRESS'; - throw new Error(errMsg); +export const orderHashUtils = { + /** + * Checks if the supplied hex encoded order hash is valid. + * Note: Valid means it has the expected format, not that an order with the orderHash exists. + * Use this method when processing orderHashes submitted as user input. + * @param orderHash Hex encoded orderHash. + * @return Whether the supplied orderHash has the expected format. + */ + isValidOrderHash(orderHash: string): boolean { + // Since this method can be called to check if any arbitrary string conforms to an orderHash's + // format, we only assert that we were indeed passed a string. + assert.isString('orderHash', orderHash); + const schemaValidator = new SchemaValidator(); + const isValid = schemaValidator.validate(orderHash, schemas.orderHashSchema).valid; + return isValid; + }, + /** + * Computes the orderHash for a supplied order. + * @param order An object that conforms to the Order or SignedOrder interface definitions. + * @return The resulting orderHash from hashing the supplied order. + */ + getOrderHashHex(order: SignedOrder | Order): string { + try { + assert.doesConformToSchema('order', order, schemas.orderSchema, [schemas.hexSchema]); + } catch (error) { + if (_.includes(error.message, INVALID_TAKER_FORMAT)) { + const errMsg = + 'Order taker must be of type string. If you want anyone to be able to fill an order - pass ZeroEx.NULL_ADDRESS'; + throw new Error(errMsg); + } + throw error; } - throw error; - } - const orderParts = [ - { value: order.exchangeContractAddress, type: SolidityTypes.Address }, - { value: order.maker, type: SolidityTypes.Address }, - { value: order.taker, type: SolidityTypes.Address }, - { value: order.makerTokenAddress, type: SolidityTypes.Address }, - { value: order.takerTokenAddress, type: SolidityTypes.Address }, - { value: order.feeRecipient, type: SolidityTypes.Address }, - { - value: bigNumberToBN(order.makerTokenAmount), - type: SolidityTypes.Uint256, - }, - { - value: bigNumberToBN(order.takerTokenAmount), - type: SolidityTypes.Uint256, - }, - { - value: bigNumberToBN(order.makerFee), - type: SolidityTypes.Uint256, - }, - { - value: bigNumberToBN(order.takerFee), - type: SolidityTypes.Uint256, - }, - { - value: bigNumberToBN(order.expirationUnixTimestampSec), - type: SolidityTypes.Uint256, - }, - { value: bigNumberToBN(order.salt), type: SolidityTypes.Uint256 }, - ]; - const types = _.map(orderParts, o => o.type); - const values = _.map(orderParts, o => o.value); - const hashBuff = ethABI.soliditySHA3(types, values); - const hashHex = ethUtil.bufferToHex(hashBuff); - return hashHex; -} -/** - * Checks if the supplied hex encoded order hash is valid. - * Note: Valid means it has the expected format, not that an order with the orderHash exists. - * Use this method when processing orderHashes submitted as user input. - * @param orderHash Hex encoded orderHash. - * @return Whether the supplied orderHash has the expected format. - */ -export function isValidOrderHash(orderHash: string): boolean { - // Since this method can be called to check if any arbitrary string conforms to an orderHash's - // format, we only assert that we were indeed passed a string. - assert.isString('orderHash', orderHash); - const schemaValidator = new SchemaValidator(); - const isValid = schemaValidator.validate(orderHash, schemas.orderHashSchema).valid; - return isValid; -} + const orderHashBuff = this.getOrderHashBuff(order); + const orderHashHex = `0x${orderHashBuff.toString('hex')}`; + return orderHashHex; + }, + /** + * Computes the orderHash for a supplied order and returns it as a Buffer + * @param order An object that conforms to the Order or SignedOrder interface definitions. + * @return The resulting orderHash from hashing the supplied order as a Buffer + */ + getOrderHashBuff(order: SignedOrder | Order): Buffer { + const makerAssetDataHash = crypto.solSHA3([ethUtil.toBuffer(order.makerAssetData)]); + const takerAssetDataHash = crypto.solSHA3([ethUtil.toBuffer(order.takerAssetData)]); + + const orderParamsHashBuff = crypto.solSHA3([ + order.makerAddress, + order.takerAddress, + order.feeRecipientAddress, + order.senderAddress, + order.makerAssetAmount, + order.takerAssetAmount, + order.makerFee, + order.takerFee, + order.expirationTimeSeconds, + order.salt, + makerAssetDataHash, + takerAssetDataHash, + ]); + const orderParamsHashHex = `0x${orderParamsHashBuff.toString('hex')}`; + const orderSchemaHashHex = this._getOrderSchemaHex(); + const domainSeparatorHashHex = this._getDomainSeparatorHashHex(order.exchangeAddress); + const domainSeparatorSchemaHex = this._getDomainSeparatorSchemaHex(); + const orderHashBuff = crypto.solSHA3([ + new BigNumber(domainSeparatorSchemaHex), + new BigNumber(domainSeparatorHashHex), + new BigNumber(orderSchemaHashHex), + new BigNumber(orderParamsHashHex), + ]); + return orderHashBuff; + }, + _getOrderSchemaHex(): string { + const orderSchemaHashBuff = crypto.solSHA3([ + 'Order(', + 'address makerAddress,', + 'address takerAddress,', + 'address feeRecipientAddress,', + 'address senderAddress,', + 'uint256 makerAssetAmount,', + 'uint256 takerAssetAmount,', + 'uint256 makerFee,', + 'uint256 takerFee,', + 'uint256 expirationTimeSeconds,', + 'uint256 salt,', + 'bytes makerAssetData,', + 'bytes takerAssetData,', + ')', + ]); + const schemaHashHex = `0x${orderSchemaHashBuff.toString('hex')}`; + return schemaHashHex; + }, + _getDomainSeparatorSchemaHex(): string { + const domainSeparatorSchemaHashBuff = crypto.solSHA3(['DomainSeparator(address contract)']); + const schemaHashHex = `0x${domainSeparatorSchemaHashBuff.toString('hex')}`; + return schemaHashHex; + }, + _getDomainSeparatorHashHex(exchangeAddress: string): string { + const domainSeparatorHashBuff = crypto.solSHA3([exchangeAddress]); + const domainSeparatorHashHex = `0x${domainSeparatorHashBuff.toString('hex')}`; + return domainSeparatorHashHex; + }, +}; diff --git a/packages/order-utils/src/order_state_utils.ts b/packages/order-utils/src/order_state_utils.ts index 36171f526..61050c9d6 100644 --- a/packages/order-utils/src/order_state_utils.ts +++ b/packages/order-utils/src/order_state_utils.ts @@ -11,7 +11,8 @@ import * as _ from 'lodash'; import { AbstractBalanceAndProxyAllowanceFetcher } from './abstract/abstract_balance_and_proxy_allowance_fetcher'; import { AbstractOrderFilledCancelledFetcher } from './abstract/abstract_order_filled_cancelled_fetcher'; -import { getOrderHashHex } from './order_hash'; +import { assetProxyUtils } from './asset_proxy_utils'; +import { orderHashUtils } from './order_hash'; import { RemainingFillableCalculator } from './remaining_fillable_calculator'; const ACCEPTABLE_RELATIVE_ROUNDING_ERROR = 0.0001; @@ -23,7 +24,7 @@ export class OrderStateUtils { const unavailableTakerTokenAmount = orderRelevantState.cancelledTakerTokenAmount.add( orderRelevantState.filledTakerTokenAmount, ); - const availableTakerTokenAmount = signedOrder.takerTokenAmount.minus(unavailableTakerTokenAmount); + const availableTakerTokenAmount = signedOrder.takerAssetAmount.minus(unavailableTakerTokenAmount); if (availableTakerTokenAmount.eq(0)) { throw new Error(ExchangeContractErrs.OrderRemainingFillAmountZero); } @@ -42,9 +43,9 @@ export class OrderStateUtils { throw new Error(ExchangeContractErrs.InsufficientMakerFeeAllowance); } } - const minFillableTakerTokenAmountWithinNoRoundingErrorRange = signedOrder.takerTokenAmount + const minFillableTakerTokenAmountWithinNoRoundingErrorRange = signedOrder.takerAssetAmount .dividedBy(ACCEPTABLE_RELATIVE_ROUNDING_ERROR) - .dividedBy(signedOrder.makerTokenAmount); + .dividedBy(signedOrder.makerAssetAmount); if ( orderRelevantState.remainingFillableTakerTokenAmount.lessThan( minFillableTakerTokenAmountWithinNoRoundingErrorRange, @@ -62,7 +63,7 @@ export class OrderStateUtils { } public async getOrderStateAsync(signedOrder: SignedOrder): Promise<OrderState> { const orderRelevantState = await this.getOrderRelevantStateAsync(signedOrder); - const orderHash = getOrderHashHex(signedOrder); + const orderHash = orderHashUtils.getOrderHashHex(signedOrder); try { OrderStateUtils._validateIfOrderIsValid(signedOrder, orderRelevantState); const orderState: OrderStateValid = { @@ -82,22 +83,22 @@ export class OrderStateUtils { } public async getOrderRelevantStateAsync(signedOrder: SignedOrder): Promise<OrderRelevantState> { const zrxTokenAddress = this._orderFilledCancelledFetcher.getZRXTokenAddress(); - const orderHash = getOrderHashHex(signedOrder); + const orderHash = orderHashUtils.getOrderHashHex(signedOrder); const makerBalance = await this._balanceAndProxyAllowanceFetcher.getBalanceAsync( - signedOrder.makerTokenAddress, - signedOrder.maker, + signedOrder.makerAssetData, + signedOrder.makerAddress, ); const makerProxyAllowance = await this._balanceAndProxyAllowanceFetcher.getProxyAllowanceAsync( - signedOrder.makerTokenAddress, - signedOrder.maker, + signedOrder.makerAssetData, + signedOrder.makerAddress, ); const makerFeeBalance = await this._balanceAndProxyAllowanceFetcher.getBalanceAsync( zrxTokenAddress, - signedOrder.maker, + signedOrder.makerAddress, ); const makerFeeProxyAllowance = await this._balanceAndProxyAllowanceFetcher.getProxyAllowanceAsync( zrxTokenAddress, - signedOrder.maker, + signedOrder.makerAddress, ); const filledTakerTokenAmount = await this._orderFilledCancelledFetcher.getFilledTakerAmountAsync(orderHash); const cancelledTakerTokenAmount = await this._orderFilledCancelledFetcher.getCancelledTakerAmountAsync( @@ -106,8 +107,8 @@ export class OrderStateUtils { const unavailableTakerTokenAmount = await this._orderFilledCancelledFetcher.getUnavailableTakerAmountAsync( orderHash, ); - const totalMakerTokenAmount = signedOrder.makerTokenAmount; - const totalTakerTokenAmount = signedOrder.takerTokenAmount; + const totalMakerTokenAmount = signedOrder.makerAssetAmount; + const totalTakerTokenAmount = signedOrder.takerAssetAmount; const remainingTakerTokenAmount = totalTakerTokenAmount.minus(unavailableTakerTokenAmount); const remainingMakerTokenAmount = remainingTakerTokenAmount .times(totalMakerTokenAmount) @@ -115,7 +116,8 @@ export class OrderStateUtils { const transferrableMakerTokenAmount = BigNumber.min([makerProxyAllowance, makerBalance]); const transferrableFeeTokenAmount = BigNumber.min([makerFeeProxyAllowance, makerFeeBalance]); - const isMakerTokenZRX = signedOrder.makerTokenAddress === zrxTokenAddress; + const zrxAssetData = assetProxyUtils.encodeERC20ProxyData(zrxTokenAddress); + const isMakerTokenZRX = signedOrder.makerAssetData === zrxAssetData; const remainingFillableCalculator = new RemainingFillableCalculator( signedOrder, isMakerTokenZRX, diff --git a/packages/order-utils/src/remaining_fillable_calculator.ts b/packages/order-utils/src/remaining_fillable_calculator.ts index 184c13aa4..b291d8ea9 100644 --- a/packages/order-utils/src/remaining_fillable_calculator.ts +++ b/packages/order-utils/src/remaining_fillable_calculator.ts @@ -23,7 +23,7 @@ export class RemainingFillableCalculator { this._remainingMakerTokenAmount = remainingMakerTokenAmount; this._remainingMakerFeeAmount = remainingMakerTokenAmount .times(signedOrder.makerFee) - .dividedToIntegerBy(signedOrder.makerTokenAmount); + .dividedToIntegerBy(signedOrder.makerAssetAmount); } public computeRemainingMakerFillable(): BigNumber { if (this._hasSufficientFundsForFeeAndTransferAmount()) { @@ -36,8 +36,8 @@ export class RemainingFillableCalculator { } public computeRemainingTakerFillable(): BigNumber { return this.computeRemainingMakerFillable() - .times(this._signedOrder.takerTokenAmount) - .dividedToIntegerBy(this._signedOrder.makerTokenAmount); + .times(this._signedOrder.takerAssetAmount) + .dividedToIntegerBy(this._signedOrder.makerAssetAmount); } private _hasSufficientFundsForFeeAndTransferAmount(): boolean { if (this._isMakerTokenZRX) { @@ -59,7 +59,7 @@ export class RemainingFillableCalculator { } private _calculatePartiallyFillableMakerTokenAmount(): BigNumber { // Given an order for 200 wei for 2 ZRXwei fee, find 100 wei for 1 ZRXwei. Order ratio is then 100:1 - const orderToFeeRatio = this._signedOrder.makerTokenAmount.dividedBy(this._signedOrder.makerFee); + const orderToFeeRatio = this._signedOrder.makerAssetAmount.dividedBy(this._signedOrder.makerFee); // The number of times the maker can fill the order, if each fill only required the transfer of a single // baseUnit of fee tokens. // Given 2 ZRXwei, the maximum amount of times Maker can fill this order, in terms of fees, is 2 @@ -81,10 +81,10 @@ export class RemainingFillableCalculator { // When Ratio is not fully divisible there can be remainders which cannot be represented, so they are floored. // This can result in a RoundingError being thrown by the Exchange Contract. const partiallyFillableMakerTokenAmount = fillableTimesInMakerTokenUnits - .times(this._signedOrder.makerTokenAmount) + .times(this._signedOrder.makerAssetAmount) .dividedToIntegerBy(this._signedOrder.makerFee); const partiallyFillableFeeTokenAmount = fillableTimesInFeeTokenBaseUnits - .times(this._signedOrder.makerTokenAmount) + .times(this._signedOrder.makerAssetAmount) .dividedToIntegerBy(this._signedOrder.makerFee); const partiallyFillableAmount = BigNumber.min( partiallyFillableMakerTokenAmount, diff --git a/packages/order-utils/src/signature_utils.ts b/packages/order-utils/src/signature_utils.ts index ebd636b20..c57699af0 100644 --- a/packages/order-utils/src/signature_utils.ts +++ b/packages/order-utils/src/signature_utils.ts @@ -1,28 +1,167 @@ import { schemas } from '@0xproject/json-schemas'; -import { ECSignature, Provider } from '@0xproject/types'; +import { ECSignature, SignatureType, ValidatorSignature } from '@0xproject/types'; import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import { Provider } from 'ethereum-types'; import * as ethUtil from 'ethereumjs-util'; import * as _ from 'lodash'; +import { artifacts } from './artifacts'; import { assert } from './assert'; -import { OrderError } from './types'; +import { ExchangeContract } from './generated_contract_wrappers/exchange'; +import { IValidatorContract } from './generated_contract_wrappers/i_validator'; +import { IWalletContract } from './generated_contract_wrappers/i_wallet'; +import { MessagePrefixOpts, MessagePrefixType, OrderError } from './types'; +import { utils } from './utils'; /** - * Verifies that the elliptic curve signature `signature` was generated - * by signing `data` with the private key corresponding to the `signerAddress` address. + * Verifies that the provided signature is valid according to the 0x Protocol smart contracts * @param data The hex encoded data signed by the supplied signature. - * @param signature An object containing the elliptic curve signature parameters. + * @param signature A hex encoded 0x Protocol signature made up of: [TypeSpecificData][SignatureType]. + * E.g [vrs][SignatureType.EIP712] * @param signerAddress The hex encoded address that signed the data, producing the supplied signature. * @return Whether the signature is valid for the supplied signerAddress and data. */ -export function isValidSignature(data: string, signature: ECSignature, signerAddress: string): boolean { +export async function isValidSignatureAsync( + provider: Provider, + data: string, + signature: string, + signerAddress: string, +): Promise<boolean> { + const signatureTypeIndexIfExists = utils.getSignatureTypeIndexIfExists(signature); + if (_.isUndefined(signatureTypeIndexIfExists)) { + throw new Error(`Unrecognized signatureType in signature: ${signature}`); + } + + switch (signatureTypeIndexIfExists) { + case SignatureType.Illegal: + case SignatureType.Invalid: + return false; + + case SignatureType.EIP712: { + const ecSignature = parseECSignature(signature); + return isValidECSignature(data, ecSignature, signerAddress); + } + + case SignatureType.EthSign: { + const ecSignature = parseECSignature(signature); + const prefixedMessageHex = addSignedMessagePrefix(data, MessagePrefixType.EthSign); + return isValidECSignature(prefixedMessageHex, ecSignature, signerAddress); + } + + case SignatureType.Caller: + // HACK: We currently do not "validate" the caller signature type. + // It can only be validated during Exchange contract execution. + throw new Error('Caller signature type cannot be validated off-chain'); + + case SignatureType.Wallet: { + const isValid = await isValidWalletSignatureAsync(provider, data, signature, signerAddress); + return isValid; + } + + case SignatureType.Validator: { + const isValid = await isValidValidatorSignatureAsync(provider, data, signature, signerAddress); + return isValid; + } + + case SignatureType.PreSigned: { + return isValidPresignedSignatureAsync(provider, data, signerAddress); + } + + case SignatureType.Trezor: { + const prefixedMessageHex = addSignedMessagePrefix(data, MessagePrefixType.Trezor); + const ecSignature = parseECSignature(signature); + return isValidECSignature(prefixedMessageHex, ecSignature, signerAddress); + } + + default: + throw new Error(`Unhandled SignatureType: ${signatureTypeIndexIfExists}`); + } +} + +/** + * Verifies that the provided presigned signature is valid according to the 0x Protocol smart contracts + * @param data The hex encoded data signed by the supplied signature. + * @param signature A hex encoded presigned 0x Protocol signature made up of: [SignatureType.Presigned] + * @param signerAddress The hex encoded address that signed the data, producing the supplied signature. + * @return Whether the data was preSigned by the supplied signerAddress. + */ +export async function isValidPresignedSignatureAsync( + provider: Provider, + data: string, + signerAddress: string, +): Promise<boolean> { + const exchangeContract = new ExchangeContract(artifacts.Exchange.abi, signerAddress, provider); + const isValid = await exchangeContract.preSigned.callAsync(data, signerAddress); + return isValid; +} + +/** + * Verifies that the provided wallet signature is valid according to the 0x Protocol smart contracts + * @param data The hex encoded data signed by the supplied signature. + * @param signature A hex encoded presigned 0x Protocol signature made up of: [SignatureType.Presigned] + * @param signerAddress The hex encoded address that signed the data, producing the supplied signature. + * @return Whether the data was preSigned by the supplied signerAddress. + */ +export async function isValidWalletSignatureAsync( + provider: Provider, + data: string, + signature: string, + signerAddress: string, +): Promise<boolean> { + // tslint:disable-next-line:custom-no-magic-numbers + const signatureWithoutType = signature.slice(-2); + const walletContract = new IWalletContract(artifacts.IWallet.abi, signerAddress, provider); + const isValid = await walletContract.isValidSignature.callAsync(data, signatureWithoutType); + return isValid; +} + +/** + * Verifies that the provided validator signature is valid according to the 0x Protocol smart contracts + * @param data The hex encoded data signed by the supplied signature. + * @param signature A hex encoded presigned 0x Protocol signature made up of: [SignatureType.Presigned] + * @param signerAddress The hex encoded address that signed the data, producing the supplied signature. + * @return Whether the data was preSigned by the supplied signerAddress. + */ +export async function isValidValidatorSignatureAsync( + provider: Provider, + data: string, + signature: string, + signerAddress: string, +): Promise<boolean> { + const validatorSignature = parseValidatorSignature(signature); + const exchangeContract = new ExchangeContract(artifacts.Exchange.abi, signerAddress, provider); + const isValidatorApproved = await exchangeContract.allowedValidators.callAsync( + signerAddress, + validatorSignature.validatorAddress, + ); + if (!isValidatorApproved) { + throw new Error(`Validator ${validatorSignature.validatorAddress} was not pre-approved by ${signerAddress}.`); + } + + const validatorContract = new IValidatorContract(artifacts.IValidator.abi, signerAddress, provider); + const isValid = await validatorContract.isValidSignature.callAsync( + data, + signerAddress, + validatorSignature.signature, + ); + return isValid; +} + +/** + * Checks if the supplied elliptic curve signature corresponds to signing `data` with + * the private key corresponding to `signerAddress` + * @param data The hex encoded data signed by the supplied signature. + * @param signature An object containing the elliptic curve signature parameters. + * @param signerAddress The hex encoded address that signed the data, producing the supplied signature. + * @return Whether the ECSignature is valid. + */ +export function isValidECSignature(data: string, signature: ECSignature, signerAddress: string): boolean { assert.isHexString('data', data); assert.doesConformToSchema('signature', signature, schemas.ecSignatureSchema); assert.isETHAddressHex('signerAddress', signerAddress); const normalizedSignerAddress = signerAddress.toLowerCase(); - const dataBuff = ethUtil.toBuffer(data); - const msgHashBuff = ethUtil.hashPersonalMessage(dataBuff); + const msgHashBuff = ethUtil.toBuffer(data); try { const pubKey = ethUtil.ecrecover( msgHashBuff, @@ -36,23 +175,23 @@ export function isValidSignature(data: string, signature: ECSignature, signerAdd return false; } } + /** * Signs an orderHash and returns it's elliptic curve signature. * This method currently supports TestRPC, Geth and Parity above and below V1.6.6 * @param orderHash Hex encoded orderHash to sign. * @param signerAddress The hex encoded Ethereum address you wish to sign it with. This address * must be available via the Provider supplied to 0x.js. - * @param shouldAddPersonalMessagePrefix Some signers add the personal message prefix `\x19Ethereum Signed Message` - * themselves (e.g Parity Signer, Ledger, TestRPC) and others expect it to already be done by the client - * (e.g Metamask). Depending on which signer this request is going to, decide on whether to add the prefix - * before sending the request. + * @param hashPrefixOpts Different signers add/require different prefixes be appended to the message being signed. + * Since we cannot know ahead of time which signer you are using, you must supply both a prefixType and + * whether it must be added before calling `eth_sign` (some signers add it themselves) * @return An object containing the Elliptic curve signature parameters generated by signing the orderHash. */ -export async function signOrderHashAsync( +export async function ecSignOrderHashAsync( provider: Provider, orderHash: string, signerAddress: string, - shouldAddPersonalMessagePrefix: boolean, + messagePrefixOpts: MessagePrefixOpts, ): Promise<ECSignature> { assert.isHexString('orderHash', orderHash); const web3Wrapper = new Web3Wrapper(provider); @@ -60,12 +199,10 @@ export async function signOrderHashAsync( const normalizedSignerAddress = signerAddress.toLowerCase(); let msgHashHex = orderHash; - if (shouldAddPersonalMessagePrefix) { - const orderHashBuff = ethUtil.toBuffer(orderHash); - const msgHashBuff = ethUtil.hashPersonalMessage(orderHashBuff); - msgHashHex = ethUtil.bufferToHex(msgHashBuff); + const prefixedMsgHashHex = addSignedMessagePrefix(orderHash, messagePrefixOpts.prefixType); + if (messagePrefixOpts.shouldAddPrefixBeforeCallingEthSign) { + msgHashHex = prefixedMsgHashHex; } - const signature = await web3Wrapper.signMessageAsync(normalizedSignerAddress, msgHashHex); // HACK: There is no consensus on whether the signatureHex string should be formatted as @@ -76,7 +213,7 @@ export async function signOrderHashAsync( const validVParamValues = [27, 28]; const ecSignatureVRS = parseSignatureHexAsVRS(signature); if (_.includes(validVParamValues, ecSignatureVRS.v)) { - const isValidVRSSignature = isValidSignature(orderHash, ecSignatureVRS, normalizedSignerAddress); + const isValidVRSSignature = isValidECSignature(prefixedMsgHashHex, ecSignatureVRS, normalizedSignerAddress); if (isValidVRSSignature) { return ecSignatureVRS; } @@ -84,7 +221,7 @@ export async function signOrderHashAsync( const ecSignatureRSV = parseSignatureHexAsRSV(signature); if (_.includes(validVParamValues, ecSignatureRSV.v)) { - const isValidRSVSignature = isValidSignature(orderHash, ecSignatureRSV, normalizedSignerAddress); + const isValidRSVSignature = isValidECSignature(prefixedMsgHashHex, ecSignatureRSV, normalizedSignerAddress); if (isValidRSVSignature) { return ecSignatureRSV; } @@ -93,6 +230,64 @@ export async function signOrderHashAsync( throw new Error(OrderError.InvalidSignature); } +/** + * Adds the relevant prefix to the message being signed. + * @param message Message to sign + * @param messagePrefixType The type of message prefix to add. Different signers expect + * specific message prefixes. + * @return Prefixed message + */ +export function addSignedMessagePrefix(message: string, messagePrefixType: MessagePrefixType): string { + switch (messagePrefixType) { + case MessagePrefixType.None: + return message; + + case MessagePrefixType.EthSign: { + const msgBuff = ethUtil.toBuffer(message); + const prefixedMsgBuff = ethUtil.hashPersonalMessage(msgBuff); + const prefixedMsgHex = ethUtil.bufferToHex(prefixedMsgBuff); + return prefixedMsgHex; + } + + case MessagePrefixType.Trezor: { + const msgBuff = ethUtil.toBuffer(message); + const prefixedMsgBuff = hashTrezorPersonalMessage(msgBuff); + const prefixedMsgHex = ethUtil.bufferToHex(prefixedMsgBuff); + return prefixedMsgHex; + } + + default: + throw new Error(`Unrecognized MessagePrefixType: ${messagePrefixType}`); + } +} + +function hashTrezorPersonalMessage(message: Buffer): Buffer { + const prefix = ethUtil.toBuffer('\x19Ethereum Signed Message:\n' + String.fromCharCode(message.length)); + return ethUtil.sha3(Buffer.concat([prefix, message])); +} + +function parseECSignature(signature: string): ECSignature { + const ecSignatureTypes = [SignatureType.EthSign, SignatureType.EIP712, SignatureType.Trezor]; + assert.isOneOfExpectedSignatureTypes(signature, ecSignatureTypes); + + // tslint:disable-next-line:custom-no-magic-numbers + const vrsHex = signature.slice(0, -2); + const ecSignature = parseSignatureHexAsVRS(vrsHex); + + return ecSignature; +} + +function parseValidatorSignature(signature: string): ValidatorSignature { + assert.isOneOfExpectedSignatureTypes(signature, [SignatureType.Validator]); + // tslint:disable:custom-no-magic-numbers + const validatorSignature = { + validatorAddress: signature.slice(-22, -2), + signature: signature.slice(0, -22), + }; + // tslint:enable:custom-no-magic-numbers + return validatorSignature; +} + function parseSignatureHexAsVRS(signatureHex: string): ECSignature { const signatureBuffer = ethUtil.toBuffer(signatureHex); let v = signatureBuffer[0]; diff --git a/packages/order-utils/src/types.ts b/packages/order-utils/src/types.ts index f79d52359..db0bfb249 100644 --- a/packages/order-utils/src/types.ts +++ b/packages/order-utils/src/types.ts @@ -1,3 +1,25 @@ export enum OrderError { InvalidSignature = 'INVALID_SIGNATURE', } + +/** + * The requisite message prefix (is any) to add to an `eth_sign` request. + */ +export enum MessagePrefixType { + None = 'NONE', + EthSign = 'ETH_SIGN', + Trezor = 'TREZOR', +} + +/** + * Options related to message prefixing of messages sent to `eth_sign` + * Some signers prepend a message prefix (e.g Parity Signer, Ledger, TestRPC), while + * others require it already be prepended (e.g Metamask). In addition, different signers + * expect slightly different prefixes (See: https://github.com/ethereum/go-ethereum/issues/14794). + * Depending on the signer that will receive your signing request, you must specify the + * desired prefix and whether it should be added before making the `eth_sign` request. + */ +export interface MessagePrefixOpts { + prefixType: MessagePrefixType; + shouldAddPrefixBeforeCallingEthSign: boolean; +} diff --git a/packages/order-utils/src/utils.ts b/packages/order-utils/src/utils.ts new file mode 100644 index 000000000..3b465cece --- /dev/null +++ b/packages/order-utils/src/utils.ts @@ -0,0 +1,9 @@ +export const utils = { + getSignatureTypeIndexIfExists(signature: string): number { + // tslint:disable-next-line:custom-no-magic-numbers + const signatureTypeHex = signature.slice(-2); + const base = 16; + const signatureTypeInt = parseInt(signatureTypeHex, base); + return signatureTypeInt; + }, +}; diff --git a/packages/order-utils/test/order_hash_test.ts b/packages/order-utils/test/order_hash_test.ts index db5489509..d571fc62a 100644 --- a/packages/order-utils/test/order_hash_test.ts +++ b/packages/order-utils/test/order_hash_test.ts @@ -1,10 +1,11 @@ import { web3Factory } from '@0xproject/dev-utils'; +import { Order } from '@0xproject/types'; import { BigNumber } from '@0xproject/utils'; import * as chai from 'chai'; import 'make-promises-safe'; import 'mocha'; -import { constants, getOrderHashHex } from '../src'; +import { constants, orderHashUtils } from '../src'; import { chaiSetup } from './utils/chai_setup'; import { web3Wrapper } from './utils/web3_wrapper'; @@ -14,34 +15,50 @@ const expect = chai.expect; describe('Order hashing', () => { describe('#getOrderHashHex', () => { - const expectedOrderHash = '0x39da987067a3c9e5f1617694f1301326ba8c8b0498ebef5df4863bed394e3c83'; + const expectedOrderHash = '0x367ad7730eb8b5feab8a9c9f47c6fcba77a2d4df125ee6a59cc26ac955710f7e'; const fakeExchangeContractAddress = '0xb69e673309512a9d726f87304c6984054f87a93b'; - const order = { - maker: constants.NULL_ADDRESS, - taker: constants.NULL_ADDRESS, - feeRecipient: constants.NULL_ADDRESS, - makerTokenAddress: constants.NULL_ADDRESS, - takerTokenAddress: constants.NULL_ADDRESS, - exchangeContractAddress: fakeExchangeContractAddress, + const order: Order = { + makerAddress: constants.NULL_ADDRESS, + takerAddress: constants.NULL_ADDRESS, + senderAddress: constants.NULL_ADDRESS, + feeRecipientAddress: constants.NULL_ADDRESS, + makerAssetData: constants.NULL_ADDRESS, + takerAssetData: constants.NULL_ADDRESS, + exchangeAddress: fakeExchangeContractAddress, salt: new BigNumber(0), makerFee: new BigNumber(0), takerFee: new BigNumber(0), - makerTokenAmount: new BigNumber(0), - takerTokenAmount: new BigNumber(0), - expirationUnixTimestampSec: new BigNumber(0), + makerAssetAmount: new BigNumber(0), + takerAssetAmount: new BigNumber(0), + expirationTimeSeconds: new BigNumber(0), }; it('calculates the order hash', async () => { - const orderHash = getOrderHashHex(order); + const orderHash = orderHashUtils.getOrderHashHex(order); expect(orderHash).to.be.equal(expectedOrderHash); }); it('throws a readable error message if taker format is invalid', async () => { const orderWithInvalidtakerFormat = { ...order, - taker: (null as any) as string, + takerAddress: (null as any) as string, }; const expectedErrorMessage = 'Order taker must be of type string. If you want anyone to be able to fill an order - pass ZeroEx.NULL_ADDRESS'; - expect(() => getOrderHashHex(orderWithInvalidtakerFormat)).to.throw(expectedErrorMessage); + expect(() => orderHashUtils.getOrderHashHex(orderWithInvalidtakerFormat)).to.throw(expectedErrorMessage); + }); + }); + describe('#isValidOrderHash', () => { + it('returns false if the value is not a hex string', () => { + const isValid = orderHashUtils.isValidOrderHash('not a hex'); + expect(isValid).to.be.false(); + }); + it('returns false if the length is wrong', () => { + const isValid = orderHashUtils.isValidOrderHash('0xdeadbeef'); + expect(isValid).to.be.false(); + }); + it('returns true if order hash is correct', () => { + const orderHashLength = 65; + const isValid = orderHashUtils.isValidOrderHash('0x' + Array(orderHashLength).join('0')); + expect(isValid).to.be.true(); }); }); }); diff --git a/packages/order-utils/test/signature_utils_test.ts b/packages/order-utils/test/signature_utils_test.ts index e24fa0ce5..c1a3cc7fb 100644 --- a/packages/order-utils/test/signature_utils_test.ts +++ b/packages/order-utils/test/signature_utils_test.ts @@ -1,14 +1,14 @@ import { web3Factory } from '@0xproject/dev-utils'; -import { JSONRPCErrorCallback, JSONRPCRequestPayload } from '@0xproject/types'; import { BigNumber } from '@0xproject/utils'; import * as chai from 'chai'; +import { JSONRPCErrorCallback, JSONRPCRequestPayload } from 'ethereum-types'; import * as _ from 'lodash'; import 'make-promises-safe'; import 'mocha'; import * as Sinon from 'sinon'; -import { generatePseudoRandomSalt, isValidOrderHash, isValidSignature, signOrderHashAsync } from '../src'; -import * as signatureUtils from '../src/signature_utils'; +import { ecSignOrderHashAsync, generatePseudoRandomSalt, MessagePrefixType, orderHashUtils } from '../src'; +import { isValidECSignature, isValidSignatureAsync } from '../src/signature_utils'; import { chaiSetup } from './utils/chai_setup'; import { provider, web3Wrapper } from './utils/web3_wrapper'; @@ -16,32 +16,82 @@ import { provider, web3Wrapper } from './utils/web3_wrapper'; chaiSetup.configure(); const expect = chai.expect; -const SHOULD_ADD_PERSONAL_MESSAGE_PREFIX = false; - describe('Signature utils', () => { describe('#isValidSignature', () => { - // The Exchange smart contract `isValidSignature` method only validates orderHashes and assumes - // the length of the data is exactly 32 bytes. Thus for these tests, we use data of this size. - const dataHex = '0x6927e990021d23b1eb7b8789f6a6feaf98fe104bb0cf8259421b79f9a34222b0'; + let dataHex = '0x6927e990021d23b1eb7b8789f6a6feaf98fe104bb0cf8259421b79f9a34222b0'; + const ethSignSignature = + '0x1B61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351bc3340349190569279751135161d22529dc25add4f6069af05be04cacbda2ace225403'; + let address = '0x5409ed021d9299bf6814279a6a1411a7e866a631'; + + it("should return false if the data doesn't pertain to the signature & address", async () => { + expect(await isValidSignatureAsync(provider, '0x0', ethSignSignature, address)).to.be.false(); + }); + it("should return false if the address doesn't pertain to the signature & data", async () => { + const validUnrelatedAddress = '0x8b0292b11a196601ed2ce54b665cafeca0347d42'; + expect( + await isValidSignatureAsync(provider, dataHex, ethSignSignature, validUnrelatedAddress), + ).to.be.false(); + }); + it("should return false if the signature doesn't pertain to the dataHex & address", async () => { + const signatureArray = ethSignSignature.split(''); + // tslint:disable-next-line:custom-no-magic-numbers + signatureArray[5] = 'C'; // V = 28, instead of 27 + const wrongSignature = signatureArray.join(''); + expect(await isValidSignatureAsync(provider, dataHex, wrongSignature, address)).to.be.false(); + }); + + it('should throw if signatureType is invalid', () => { + const signatureArray = ethSignSignature.split(''); + signatureArray[3] = '9'; // SignatureType w/ index 9 doesn't exist + const signatureWithInvalidType = signatureArray.join(''); + expect(isValidSignatureAsync(provider, dataHex, signatureWithInvalidType, address)).to.be.rejected(); + }); + + it('should return true for a valid Ecrecover (EthSign) signature', async () => { + const isValidSignatureLocal = await isValidSignatureAsync(provider, dataHex, ethSignSignature, address); + expect(isValidSignatureLocal).to.be.true(); + }); + + it('should return true for a valid EIP712 signature', async () => { + dataHex = '0xa1d7403bcbbcd75ec233cfd6584ff8dabed677d0e9bb32c2bea94e9dd8a109da'; + address = '0x6ecbe1db9ef729cbe972c83fb886247691fb6beb'; + const eip712Signature = + '0x1bdde07aac4bf12c12ddbb155919c43eba4146a2cfcf904a862950dbebe332554c6674975603eb5a4eaf8fd7f2e06350267e5b36cda9851a89f8bb49fe2fc9afe202'; + const isValidSignatureLocal = await isValidSignatureAsync(provider, dataHex, eip712Signature, address); + expect(isValidSignatureLocal).to.be.true(); + }); + + it('should return true for a valid Trezor signature', async () => { + dataHex = '0xd0d994e31c88f33fd8a572552a70ed339de579e5ba49ee1d17cc978bbe1cdd21'; + address = '0x6ecbe1db9ef729cbe972c83fb886247691fb6beb'; + const trezorSignature = + '0x1ce4760660e6495b5ae6723087bea073b3a99ce98ea81fdf00c240279c010e63d05b87bc34c4d67d4776e8d5aeb023a67484f4eaf0fd353b40893e5101e845cd9908'; + const isValidSignatureLocal = await isValidSignatureAsync(provider, dataHex, trezorSignature, address); + expect(isValidSignatureLocal).to.be.true(); + }); + }); + describe('#isValidECSignature', () => { const signature = { v: 27, - r: '0x61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351bc33', - s: '0x40349190569279751135161d22529dc25add4f6069af05be04cacbda2ace2254', + r: '0xaca7da997ad177f040240cdccf6905b71ab16b74434388c3a72f34fd25d64393', + s: '0x46b2bac274ff29b48b3ea6e2d04c1336eaceafda3c53ab483fc3ff12fac3ebf2', }; - const address = '0x5409ed021d9299bf6814279a6a1411a7e866a631'; + const data = '0x47173285a8d7341e5e972fc677286384f802f8ef42a5ec5f03bbfa254cb01fad'; + const address = '0x0e5cb767cce09a7f3ca594df118aa519be5e2b5a'; + it("should return false if the data doesn't pertain to the signature & address", async () => { - expect(isValidSignature('0x0', signature, address)).to.be.false(); + expect(isValidECSignature('0x0', signature, address)).to.be.false(); }); it("should return false if the address doesn't pertain to the signature & data", async () => { const validUnrelatedAddress = '0x8b0292b11a196601ed2ce54b665cafeca0347d42'; - expect(isValidSignature(dataHex, signature, validUnrelatedAddress)).to.be.false(); + expect(isValidECSignature(data, signature, validUnrelatedAddress)).to.be.false(); }); - it("should return false if the signature doesn't pertain to the dataHex & address", async () => { + it("should return false if the signature doesn't pertain to the data & address", async () => { const wrongSignature = _.assign({}, signature, { v: 28 }); - expect(isValidSignature(dataHex, wrongSignature, address)).to.be.false(); + expect(isValidECSignature(data, wrongSignature, address)).to.be.false(); }); - it('should return true if the signature does pertain to the dataHex & address', async () => { - const isValidSignatureLocal = isValidSignature(dataHex, signature, address); + it('should return true if the signature does pertain to the data & address', async () => { + const isValidSignatureLocal = isValidECSignature(data, signature, address); expect(isValidSignatureLocal).to.be.true(); }); }); @@ -58,22 +108,7 @@ describe('Signature utils', () => { expect(salt.lessThan(twoPow256)).to.be.true(); }); }); - describe('#isValidOrderHash', () => { - it('returns false if the value is not a hex string', () => { - const isValid = isValidOrderHash('not a hex'); - expect(isValid).to.be.false(); - }); - it('returns false if the length is wrong', () => { - const isValid = isValidOrderHash('0xdeadbeef'); - expect(isValid).to.be.false(); - }); - it('returns true if order hash is correct', () => { - const orderHashLength = 65; - const isValid = isValidOrderHash('0x' + Array(orderHashLength).join('0')); - expect(isValid).to.be.true(); - }); - }); - describe('#signOrderHashAsync', () => { + describe('#ecSignOrderHashAsync', () => { let stubs: Sinon.SinonStub[] = []; let makerAddress: string; before(async () => { @@ -92,12 +127,11 @@ describe('Signature utils', () => { r: '0x61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351bc33', s: '0x40349190569279751135161d22529dc25add4f6069af05be04cacbda2ace2254', }; - const ecSignature = await signOrderHashAsync( - provider, - orderHash, - makerAddress, - SHOULD_ADD_PERSONAL_MESSAGE_PREFIX, - ); + const messagePrefixOpts = { + prefixType: MessagePrefixType.EthSign, + shouldAddPrefixBeforeCallingEthSign: false, + }; + const ecSignature = await ecSignOrderHashAsync(provider, orderHash, makerAddress, messagePrefixOpts); expect(ecSignature).to.deep.equal(expectedECSignature); }); it('should return the correct ECSignature for signatureHex concatenated as R + S + V', async () => { @@ -126,12 +160,11 @@ describe('Signature utils', () => { }, }; - const ecSignature = await signOrderHashAsync( - fakeProvider, - orderHash, - makerAddress, - SHOULD_ADD_PERSONAL_MESSAGE_PREFIX, - ); + const messagePrefixOpts = { + prefixType: MessagePrefixType.EthSign, + shouldAddPrefixBeforeCallingEthSign: false, + }; + const ecSignature = await ecSignOrderHashAsync(fakeProvider, orderHash, makerAddress, messagePrefixOpts); expect(ecSignature).to.deep.equal(expectedECSignature); }); it('should return the correct ECSignature for signatureHex concatenated as V + R + S', async () => { @@ -157,12 +190,11 @@ describe('Signature utils', () => { }, }; - const ecSignature = await signOrderHashAsync( - fakeProvider, - orderHash, - makerAddress, - SHOULD_ADD_PERSONAL_MESSAGE_PREFIX, - ); + const messagePrefixOpts = { + prefixType: MessagePrefixType.EthSign, + shouldAddPrefixBeforeCallingEthSign: false, + }; + const ecSignature = await ecSignOrderHashAsync(fakeProvider, orderHash, makerAddress, messagePrefixOpts); expect(ecSignature).to.deep.equal(expectedECSignature); }); }); diff --git a/packages/order-utils/test/utils/web3_wrapper.ts b/packages/order-utils/test/utils/web3_wrapper.ts index 71a0dc1c2..419f76dde 100644 --- a/packages/order-utils/test/utils/web3_wrapper.ts +++ b/packages/order-utils/test/utils/web3_wrapper.ts @@ -1,6 +1,6 @@ import { devConstants, web3Factory } from '@0xproject/dev-utils'; -import { Provider } from '@0xproject/types'; import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import { Provider } from 'ethereum-types'; const provider: Provider = web3Factory.getRpcProvider({ shouldUseInProcessGanache: true }); const web3Wrapper = new Web3Wrapper(provider); |