From 2a82ff48c061eacb3b6f9fb36eeae7f515b6d11d Mon Sep 17 00:00:00 2001 From: Jacob Evans Date: Thu, 4 Oct 2018 17:12:35 +1000 Subject: Move SignTypedData to utils package --- packages/order-utils/src/constants.ts | 12 +++ packages/order-utils/src/eip712_utils.ts | 109 ---------------------- packages/order-utils/src/index.ts | 4 - packages/order-utils/src/order_hash.ts | 52 +++++++---- packages/order-utils/src/signature_utils.ts | 2 +- packages/order-utils/src/types.ts | 18 ---- packages/utils/src/index.ts | 1 + packages/utils/src/sign_typed_data_utils.ts | 81 ++++++++++++++++ packages/utils/test/sign_typed_data_utils_test.ts | 107 +++++++++++++++++++++ 9 files changed, 234 insertions(+), 152 deletions(-) delete mode 100644 packages/order-utils/src/eip712_utils.ts create mode 100644 packages/utils/src/sign_typed_data_utils.ts create mode 100644 packages/utils/test/sign_typed_data_utils_test.ts diff --git a/packages/order-utils/src/constants.ts b/packages/order-utils/src/constants.ts index c23578c20..cc03755c3 100644 --- a/packages/order-utils/src/constants.ts +++ b/packages/order-utils/src/constants.ts @@ -14,3 +14,15 @@ export const constants = { INFINITE_TIMESTAMP_SEC: new BigNumber(2524604400), // Close to infinite ZERO_AMOUNT: new BigNumber(0), }; + +export const EIP712_DOMAIN_NAME = '0x Protocol'; +export const EIP712_DOMAIN_VERSION = '2'; + +export const EIP712_DOMAIN_SCHEMA = { + name: 'EIP712Domain', + parameters: [ + { name: 'name', type: 'string' }, + { name: 'version', type: 'string ' }, + { name: 'verifyingContract', type: 'address' }, + ], +}; diff --git a/packages/order-utils/src/eip712_utils.ts b/packages/order-utils/src/eip712_utils.ts deleted file mode 100644 index 01f1e930e..000000000 --- a/packages/order-utils/src/eip712_utils.ts +++ /dev/null @@ -1,109 +0,0 @@ -import ethUtil = require('ethereumjs-util'); -import * as _ from 'lodash'; - -import { crypto } from './crypto'; -import { EIP712Schema, EIP712Types } from './types'; - -const EIP191_PREFIX = '\x19\x01'; -const EIP712_VALUE_LENGTH = 32; -export const EIP712_DOMAIN_NAME = '0x Protocol'; -export const EIP712_DOMAIN_VERSION = '2'; - -export const EIP712_DOMAIN_SCHEMA: EIP712Schema = { - name: 'EIP712Domain', - parameters: [ - { name: 'name', type: EIP712Types.String }, - { name: 'version', type: EIP712Types.String }, - { name: 'verifyingContract', type: EIP712Types.Address }, - ], -}; - -export const eip712Utils = { - /** - * Compiles the EIP712Schema and returns the hash of the schema. - * @param schema The EIP712 schema. - * @return The hash of the compiled schema - */ - compileSchema(schema: EIP712Schema): Buffer { - const eip712Schema = eip712Utils._encodeType(schema); - const eip712SchemaHashBuffer = crypto.solSHA3([eip712Schema]); - return eip712SchemaHashBuffer; - }, - /** - * Merges the EIP712 hash of a struct with the DomainSeparator for 0x v2. - * @param hashStruct the EIP712 hash of a struct - * @param contractAddress the exchange contract address - * @return The hash of an EIP712 message with domain separator prefixed - */ - createEIP712Message(hashStruct: Buffer, contractAddress: string): Buffer { - const domainSeparatorHashBuffer = eip712Utils._getDomainSeparatorHashBuffer(contractAddress); - const messageBuff = crypto.solSHA3([EIP191_PREFIX, domainSeparatorHashBuffer, hashStruct]); - return messageBuff; - }, - /** - * Pad an address to 32 bytes - * @param address Address to pad - * @return padded address - */ - pad32Address(address: string): Buffer { - const addressBuffer = ethUtil.toBuffer(address); - const addressPadded = eip712Utils.pad32Buffer(addressBuffer); - return addressPadded; - }, - /** - * Pad an buffer to 32 bytes - * @param buffer Address to pad - * @return padded buffer - */ - pad32Buffer(buffer: Buffer): Buffer { - const bufferPadded = ethUtil.setLengthLeft(buffer, EIP712_VALUE_LENGTH); - return bufferPadded; - }, - /** - * Hash together a EIP712 schema with the corresponding data - * @param schema EIP712-compliant schema - * @param data Data the complies to the schema - * @return A buffer containing the SHA256 hash of the schema and encoded data - */ - structHash(schema: EIP712Schema, data: { [key: string]: any }): Buffer { - const encodedData = eip712Utils._encodeData(schema, data); - const schemaHash = eip712Utils.compileSchema(schema); - const hashBuffer = crypto.solSHA3([schemaHash, ...encodedData]); - return hashBuffer; - }, - _getDomainSeparatorSchemaBuffer(): Buffer { - return eip712Utils.compileSchema(EIP712_DOMAIN_SCHEMA); - }, - _getDomainSeparatorHashBuffer(exchangeAddress: string): Buffer { - const domainSeparatorSchemaBuffer = eip712Utils._getDomainSeparatorSchemaBuffer(); - const encodedData = eip712Utils._encodeData(EIP712_DOMAIN_SCHEMA, { - name: EIP712_DOMAIN_NAME, - version: EIP712_DOMAIN_VERSION, - verifyingContract: exchangeAddress, - }); - const domainSeparatorHashBuff2 = crypto.solSHA3([domainSeparatorSchemaBuffer, ...encodedData]); - return domainSeparatorHashBuff2; - }, - _encodeType(schema: EIP712Schema): string { - const namedTypes = _.map(schema.parameters, ({ name, type }) => `${type} ${name}`); - const namedTypesJoined = namedTypes.join(','); - const encodedType = `${schema.name}(${namedTypesJoined})`; - return encodedType; - }, - _encodeData(schema: EIP712Schema, data: { [key: string]: any }): any { - const encodedValues = []; - for (const parameter of schema.parameters) { - const value = data[parameter.name]; - if (parameter.type === EIP712Types.String || parameter.type === EIP712Types.Bytes) { - encodedValues.push(crypto.solSHA3([ethUtil.toBuffer(value)])); - } else if (parameter.type === EIP712Types.Uint256) { - encodedValues.push(value); - } else if (parameter.type === EIP712Types.Address) { - encodedValues.push(eip712Utils.pad32Address(value)); - } else { - throw new Error(`Unable to encode ${parameter.type}`); - } - } - return encodedValues; - }, -}; diff --git a/packages/order-utils/src/index.ts b/packages/order-utils/src/index.ts index e7a23682c..7194b9780 100644 --- a/packages/order-utils/src/index.ts +++ b/packages/order-utils/src/index.ts @@ -2,7 +2,6 @@ export { orderHashUtils } from './order_hash'; export { signatureUtils } from './signature_utils'; export { generatePseudoRandomSalt } from './salt'; export { assetDataUtils } from './asset_data_utils'; -export { eip712Utils } from './eip712_utils'; export { marketUtils } from './market_utils'; export { rateUtils } from './rate_utils'; export { sortingUtils } from './sorting_utils'; @@ -36,9 +35,6 @@ export { } from '@0xproject/types'; export { OrderError, - EIP712Parameter, - EIP712Schema, - EIP712Types, TradeSide, TransferType, FindFeeOrdersThatCoverFeesForTargetOrdersOpts, diff --git a/packages/order-utils/src/order_hash.ts b/packages/order-utils/src/order_hash.ts index 151c1f801..37b9da811 100644 --- a/packages/order-utils/src/order_hash.ts +++ b/packages/order-utils/src/order_hash.ts @@ -1,28 +1,28 @@ import { schemas, SchemaValidator } from '@0xproject/json-schemas'; import { Order, SignedOrder } from '@0xproject/types'; +import { signTypedDataUtils } from '@0xproject/utils'; import * as _ from 'lodash'; import { assert } from './assert'; -import { eip712Utils } from './eip712_utils'; -import { EIP712Schema, EIP712Types } from './types'; +import { EIP712_DOMAIN_NAME, EIP712_DOMAIN_SCHEMA, EIP712_DOMAIN_VERSION } from './constants'; const INVALID_TAKER_FORMAT = 'instance.takerAddress is not of a type(s) string'; -export const EIP712_ORDER_SCHEMA: EIP712Schema = { +export const EIP712_ORDER_SCHEMA = { name: 'Order', parameters: [ - { name: 'makerAddress', type: EIP712Types.Address }, - { name: 'takerAddress', type: EIP712Types.Address }, - { name: 'feeRecipientAddress', type: EIP712Types.Address }, - { name: 'senderAddress', type: EIP712Types.Address }, - { name: 'makerAssetAmount', type: EIP712Types.Uint256 }, - { name: 'takerAssetAmount', type: EIP712Types.Uint256 }, - { name: 'makerFee', type: EIP712Types.Uint256 }, - { name: 'takerFee', type: EIP712Types.Uint256 }, - { name: 'expirationTimeSeconds', type: EIP712Types.Uint256 }, - { name: 'salt', type: EIP712Types.Uint256 }, - { name: 'makerAssetData', type: EIP712Types.Bytes }, - { name: 'takerAssetData', type: EIP712Types.Bytes }, + { name: 'makerAddress', type: 'address' }, + { name: 'takerAddress', type: 'address' }, + { name: 'feeRecipientAddress', type: 'address' }, + { name: 'senderAddress', type: 'address' }, + { name: 'makerAssetAmount', type: 'uint256' }, + { name: 'takerAssetAmount', type: 'uint256' }, + { name: 'makerFee', type: 'uint256' }, + { name: 'takerFee', type: 'uint256' }, + { name: 'expirationTimeSeconds', type: 'uint256' }, + { name: 'salt', type: 'uint256' }, + { name: 'makerAssetData', type: 'bytes' }, + { name: 'takerAssetData', type: 'bytes' }, ], }; @@ -69,11 +69,23 @@ export const orderHashUtils = { * @return The resulting orderHash from hashing the supplied order as a Buffer */ getOrderHashBuffer(order: SignedOrder | Order): Buffer { - const orderParamsHashBuff = eip712Utils.structHash(EIP712_ORDER_SCHEMA, order); - const orderHashBuff = eip712Utils.createEIP712Message(orderParamsHashBuff, order.exchangeAddress); + const normalizedOrder = _.mapValues(order, value => { + return _.isObject(value) ? value.toString() : value; + }); + const typedData = { + types: { + EIP712Domain: EIP712_DOMAIN_SCHEMA.parameters, + Order: EIP712_ORDER_SCHEMA.parameters, + }, + domain: { + name: EIP712_DOMAIN_NAME, + version: EIP712_DOMAIN_VERSION, + verifyingContract: order.exchangeAddress, + }, + message: normalizedOrder, + primaryType: EIP712_ORDER_SCHEMA.name, + }; + const orderHashBuff = signTypedDataUtils.signTypedDataHash(typedData); return orderHashBuff; }, - _getOrderSchemaBuffer(): Buffer { - return eip712Utils.compileSchema(EIP712_ORDER_SCHEMA); - }, }; diff --git a/packages/order-utils/src/signature_utils.ts b/packages/order-utils/src/signature_utils.ts index 8e0fd702b..2d7fcfc9e 100644 --- a/packages/order-utils/src/signature_utils.ts +++ b/packages/order-utils/src/signature_utils.ts @@ -7,7 +7,7 @@ import * as _ from 'lodash'; import { artifacts } from './artifacts'; import { assert } from './assert'; -import { EIP712_DOMAIN_NAME, EIP712_DOMAIN_SCHEMA, EIP712_DOMAIN_VERSION } from './eip712_utils'; +import { EIP712_DOMAIN_NAME, EIP712_DOMAIN_SCHEMA, EIP712_DOMAIN_VERSION } from './constants'; import { ExchangeContract } from './generated_contract_wrappers/exchange'; import { IValidatorContract } from './generated_contract_wrappers/i_validator'; import { IWalletContract } from './generated_contract_wrappers/i_wallet'; diff --git a/packages/order-utils/src/types.ts b/packages/order-utils/src/types.ts index a843efaa4..80075270e 100644 --- a/packages/order-utils/src/types.ts +++ b/packages/order-utils/src/types.ts @@ -14,24 +14,6 @@ export enum TransferType { Fee = 'fee', } -export interface EIP712Parameter { - name: string; - type: EIP712Types; -} - -export interface EIP712Schema { - name: string; - parameters: EIP712Parameter[]; -} - -export enum EIP712Types { - Address = 'address', - Bytes = 'bytes', - Bytes32 = 'bytes32', - String = 'string', - Uint256 = 'uint256', -} - export interface CreateOrderOpts { takerAddress?: string; senderAddress?: string; diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 9d01e5bc5..0723e5788 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -9,3 +9,4 @@ export { abiUtils } from './abi_utils'; export { NULL_BYTES } from './constants'; export { errorUtils } from './error_utils'; export { fetchAsync } from './fetch_async'; +export { signTypedDataUtils } from './sign_typed_data_utils'; diff --git a/packages/utils/src/sign_typed_data_utils.ts b/packages/utils/src/sign_typed_data_utils.ts new file mode 100644 index 000000000..902d8530c --- /dev/null +++ b/packages/utils/src/sign_typed_data_utils.ts @@ -0,0 +1,81 @@ +import * as ethUtil from 'ethereumjs-util'; +import * as ethers from 'ethers'; + +export interface EIP712Parameter { + name: string; + type: string; +} + +export interface EIP712Types { + [key: string]: EIP712Parameter[]; +} +export interface EIP712TypedData { + types: EIP712Types; + domain: any; + message: any; + primaryType: string; +} + +export const signTypedDataUtils = { + findDependencies(primaryType: string, types: EIP712Types, found: string[] = []): string[] { + if (found.includes(primaryType) || types[primaryType] === undefined) { + return found; + } + found.push(primaryType); + for (const field of types[primaryType]) { + for (const dep of signTypedDataUtils.findDependencies(field.type, types, found)) { + if (!found.includes(dep)) { + found.push(dep); + } + } + } + return found; + }, + encodeType(primaryType: string, types: EIP712Types): string { + let deps = signTypedDataUtils.findDependencies(primaryType, types); + deps = deps.filter(d => d !== primaryType); + deps = [primaryType].concat(deps.sort()); + let result = ''; + for (const dep of deps) { + result += `${dep}(${types[dep].map(({ name, type }) => `${type} ${name}`).join(',')})`; + } + return result; + }, + encodeData(primaryType: string, data: any, types: EIP712Types): string { + const encodedTypes = ['bytes32']; + const encodedValues = [signTypedDataUtils.typeHash(primaryType, types)]; + for (const field of types[primaryType]) { + let value = data[field.name]; + if (field.type === 'string' || field.type === 'bytes') { + value = ethUtil.sha3(value); + encodedTypes.push('bytes32'); + encodedValues.push(value); + } else if (types[field.type] !== undefined) { + encodedTypes.push('bytes32'); + value = ethUtil.sha3(signTypedDataUtils.encodeData(field.type, value, types)); + encodedValues.push(value); + } else if (field.type.lastIndexOf(']') === field.type.length - 1) { + throw new Error('Arrays currently unimplemented in encodeData'); + } else { + encodedTypes.push(field.type); + encodedValues.push(value); + } + } + return ethers.utils.defaultAbiCoder.encode(encodedTypes, encodedValues); + }, + typeHash(primaryType: string, types: EIP712Types): Buffer { + return ethUtil.sha3(signTypedDataUtils.encodeType(primaryType, types)); + }, + structHash(primaryType: string, data: any, types: EIP712Types): Buffer { + return ethUtil.sha3(signTypedDataUtils.encodeData(primaryType, data, types)); + }, + signTypedDataHash(typedData: EIP712TypedData): Buffer { + return ethUtil.sha3( + Buffer.concat([ + Buffer.from('1901', 'hex'), + signTypedDataUtils.structHash('EIP712Domain', typedData.domain, typedData.types), + signTypedDataUtils.structHash(typedData.primaryType, typedData.message, typedData.types), + ]), + ); + }, +}; diff --git a/packages/utils/test/sign_typed_data_utils_test.ts b/packages/utils/test/sign_typed_data_utils_test.ts new file mode 100644 index 000000000..b21ffefa0 --- /dev/null +++ b/packages/utils/test/sign_typed_data_utils_test.ts @@ -0,0 +1,107 @@ +import * as chai from 'chai'; +import 'mocha'; + +import { signTypedDataUtils } from '../src/sign_typed_data_utils'; + +const expect = chai.expect; + +describe('signTypedDataUtils', () => { + describe('signTypedDataHash', () => { + const signTypedDataHashHex = '0x55eaa6ec02f3224d30873577e9ddd069a288c16d6fb407210eecbc501fa76692'; + const signTypedData = { + types: { + EIP712Domain: [ + { + name: 'name', + type: 'string', + }, + { + name: 'version', + type: 'string', + }, + { + name: 'verifyingContract', + type: 'address', + }, + ], + Order: [ + { + name: 'makerAddress', + type: 'address', + }, + { + name: 'takerAddress', + type: 'address', + }, + { + name: 'feeRecipientAddress', + type: 'address', + }, + { + name: 'senderAddress', + type: 'address', + }, + { + name: 'makerAssetAmount', + type: 'uint256', + }, + { + name: 'takerAssetAmount', + type: 'uint256', + }, + { + name: 'makerFee', + type: 'uint256', + }, + { + name: 'takerFee', + type: 'uint256', + }, + { + name: 'expirationTimeSeconds', + type: 'uint256', + }, + { + name: 'salt', + type: 'uint256', + }, + { + name: 'makerAssetData', + type: 'bytes', + }, + { + name: 'takerAssetData', + type: 'bytes', + }, + ], + }, + domain: { + name: '0x Protocol', + version: '2', + verifyingContract: '0x0000000000000000000000000000000000000000', + }, + message: { + makerAddress: '0x0000000000000000000000000000000000000000', + takerAddress: '0x0000000000000000000000000000000000000000', + makerAssetAmount: '1000000000000000000', + takerAssetAmount: '1000000000000000000', + expirationTimeSeconds: '12345', + makerFee: '0', + takerFee: '0', + feeRecipientAddress: '0x0000000000000000000000000000000000000000', + senderAddress: '0x0000000000000000000000000000000000000000', + salt: '12345', + makerAssetData: '0x0000000000000000000000000000000000000000', + takerAssetData: '0x0000000000000000000000000000000000000000', + exchangeAddress: '0x0000000000000000000000000000000000000000', + }, + primaryType: 'Order', + }; + it.only('creates a known hash of the sign typed data', () => { + const hash = signTypedDataUtils.signTypedDataHash(signTypedData).toString('hex'); + const hashHex = `0x${hash}`; + expect(hashHex).to.be.eq(signTypedDataHashHex); + console.log(hash); + }); + }); +}); -- cgit v1.2.3