diff --git a/contracts/exchange-libs/contracts/test/TestLibs.sol b/contracts/exchange-libs/contracts/test/TestLibs.sol index 43beae3161..0665793140 100644 --- a/contracts/exchange-libs/contracts/test/TestLibs.sol +++ b/contracts/exchange-libs/contracts/test/TestLibs.sol @@ -173,4 +173,28 @@ contract TestLibs is _addFillResults(totalFillResults, singleFillResults); return totalFillResults; } + + function hashOrder(Order memory order) + public + pure + returns (bytes32) + { + return _hashOrder(order); + } + + function hashZeroExTransaction(ZeroExTransaction memory transaction) + public + pure + returns (bytes32) + { + return _hashZeroExTransaction(transaction); + } + + function hashEIP712ExchangeMessage(bytes32 hashStruct) + public + view + returns (bytes32) + { + return _hashEIP712ExchangeMessage(hashStruct); + } } diff --git a/contracts/exchange-libs/test/lib_eip712_exchange_domain.ts b/contracts/exchange-libs/test/lib_eip712_exchange_domain.ts new file mode 100644 index 0000000000..df511da977 --- /dev/null +++ b/contracts/exchange-libs/test/lib_eip712_exchange_domain.ts @@ -0,0 +1,68 @@ +import { blockchainTests, constants, describe, expect, hexRandom } from '@0x/contracts-test-utils'; +import { eip712Utils, orderHashUtils } from '@0x/order-utils'; +import { Order } from '@0x/types'; +import { BigNumber, signTypedDataUtils } from '@0x/utils'; +import * as ethUtil from 'ethereumjs-util'; +import * as _ from 'lodash'; + +import { artifacts, TestLibsContract } from '../src'; + +blockchainTests('LibEIP712ExchangeDomain', env => { + let libsContract: TestLibsContract; + let exchangeDomainHash: string; + const CHAIN_ID = 1337; + + // Random generator functions + const randomAddress = () => hexRandom(constants.ADDRESS_LENGTH); + const randomHash = () => hexRandom(constants.WORD_LENGTH); + + /** + * Tests a specific instance of EIP712 message hashing. + * @param lib The LibEIP712 contract to call. + * @param domainHash The hash of the EIP712 domain of this instance. + * @param hashStruct The hash of the struct of this instance. + */ + async function testHashEIP712MessageAsync(hashStruct: string): Promise { + // Remove the hex-prefix from the exchangeDomainHash and the hashStruct + const unprefixedHashStruct = hashStruct.slice(2, hashStruct.length); + + // Hash the provided input to get the expected hash + const input = '0x1901'.concat(exchangeDomainHash, unprefixedHashStruct); + const expectedHash = '0x'.concat(ethUtil.sha3(input).toString('hex')); + + // Get the actual hash by calling the smart contract + const actualHash = await libsContract.hashEIP712ExchangeMessage.callAsync(hashStruct); + + // Verify that the actual hash matches the expected hash + expect(actualHash).to.be.eq(expectedHash); + } + + before(async () => { + libsContract = await TestLibsContract.deployFrom0xArtifactAsync( + artifacts.TestLibs, + env.provider, + env.txDefaults, + new BigNumber(CHAIN_ID), + ); + + // Generate the domain hash of 0x Exchange V3 + exchangeDomainHash = signTypedDataUtils + .generateDomainHash({ + name: '0x Protocol', + version: '3.0.0', + chainId: CHAIN_ID, + verifyingContractAddress: libsContract.address, + }) + .toString('hex'); + }); + + describe('hashEIP712ExchangeMessage', () => { + it('should correctly match an empty hash', async () => { + await testHashEIP712MessageAsync(constants.NULL_BYTES32); + }); + + it('should correctly match a non-empty hash', async () => { + await testHashEIP712MessageAsync(randomHash()); + }); + }); +}); diff --git a/contracts/exchange-libs/test/lib_order.ts b/contracts/exchange-libs/test/lib_order.ts new file mode 100644 index 0000000000..517f996dc5 --- /dev/null +++ b/contracts/exchange-libs/test/lib_order.ts @@ -0,0 +1,133 @@ +import { blockchainTests, constants, describe, expect, hexRandom } from '@0x/contracts-test-utils'; +import { eip712Utils, orderHashUtils } from '@0x/order-utils'; +import { Order } from '@0x/types'; +import { BigNumber, signTypedDataUtils } from '@0x/utils'; +import * as _ from 'lodash'; + +import { artifacts, TestLibsContract } from '../src'; + +blockchainTests('LibOrder', env => { + const CHAIN_ID = 1337; + let libsContract: TestLibsContract; + + const randomAddress = () => hexRandom(constants.ADDRESS_LENGTH); + const randomHash = () => hexRandom(constants.WORD_LENGTH); + const randomUint256 = () => new BigNumber(randomHash()); + const randomAssetData = () => hexRandom(36); + + const EMPTY_ORDER: Order = { + domain: { + verifyingContractAddress: constants.NULL_ADDRESS, + chainId: 0, + }, + senderAddress: constants.NULL_ADDRESS, + makerAddress: constants.NULL_ADDRESS, + takerAddress: constants.NULL_ADDRESS, + makerFee: constants.ZERO_AMOUNT, + takerFee: constants.ZERO_AMOUNT, + makerAssetAmount: constants.ZERO_AMOUNT, + takerAssetAmount: constants.ZERO_AMOUNT, + makerAssetData: constants.NULL_BYTES, + takerAssetData: constants.NULL_BYTES, + makerFeeAssetData: constants.NULL_BYTES, + takerFeeAssetData: constants.NULL_BYTES, + salt: constants.ZERO_AMOUNT, + feeRecipientAddress: constants.NULL_ADDRESS, + expirationTimeSeconds: constants.ZERO_AMOUNT, + }; + + /** + * Tests the `_hashOrder()` function against a reference hash. + */ + async function testHashOrderAsync(order: Order): Promise { + const typedData = eip712Utils.createOrderTypedData(order); + const expectedHash = '0x'.concat( + signTypedDataUtils.generateTypedDataHashWithoutDomain(typedData).toString('hex'), + ); + const actualHash = await libsContract.hashOrder.callAsync(order); + expect(actualHash).to.be.eq(expectedHash); + } + + /** + * Tests the `getOrderHash()` function against a reference hash. + */ + async function testGetOrderHashAsync(order: Order): Promise { + const expectedHash = orderHashUtils.getOrderHashHex(order); + const actualHash = await libsContract.getOrderHash.callAsync(order); + expect(actualHash).to.be.eq(expectedHash); + } + + before(async () => { + libsContract = await TestLibsContract.deployFrom0xArtifactAsync( + artifacts.TestLibs, + env.provider, + env.txDefaults, + new BigNumber(CHAIN_ID), + ); + }); + + describe('getOrderHash', () => { + it('should correctly hash an empty order', async () => { + await testGetOrderHashAsync({ + ...EMPTY_ORDER, + domain: { + verifyingContractAddress: libsContract.address, + chainId: 1337, + }, + }); + }); + + it('should correctly hash a non-empty order', async () => { + await testGetOrderHashAsync({ + domain: { + verifyingContractAddress: libsContract.address, + chainId: 1337, + }, + senderAddress: randomAddress(), + makerAddress: randomAddress(), + takerAddress: randomAddress(), + makerFee: randomUint256(), + takerFee: randomUint256(), + makerAssetAmount: randomUint256(), + takerAssetAmount: randomUint256(), + makerAssetData: randomAssetData(), + takerAssetData: randomAssetData(), + makerFeeAssetData: randomAssetData(), + takerFeeAssetData: randomAssetData(), + salt: randomUint256(), + feeRecipientAddress: randomAddress(), + expirationTimeSeconds: randomUint256(), + }); + }); + }); + + describe('hashOrder', () => { + it('should correctly hash an empty order', async () => { + await testHashOrderAsync(EMPTY_ORDER); + }); + + it('should correctly hash a non-empty order', async () => { + await testHashOrderAsync({ + // The domain is not used in this test, so it's okay if it is left empty. + domain: { + verifyingContractAddress: constants.NULL_ADDRESS, + chainId: 0, + }, + senderAddress: randomAddress(), + makerAddress: randomAddress(), + takerAddress: randomAddress(), + makerFee: randomUint256(), + takerFee: randomUint256(), + makerAssetAmount: randomUint256(), + takerAssetAmount: randomUint256(), + makerAssetData: randomAssetData(), + takerAssetData: randomAssetData(), + makerFeeAssetData: randomAssetData(), + takerFeeAssetData: randomAssetData(), + salt: randomUint256(), + feeRecipientAddress: randomAddress(), + expirationTimeSeconds: randomUint256(), + }); + }); + }); +}); diff --git a/contracts/exchange-libs/test/lib_zero_ex_transaction.ts b/contracts/exchange-libs/test/lib_zero_ex_transaction.ts new file mode 100644 index 0000000000..c119f61f92 --- /dev/null +++ b/contracts/exchange-libs/test/lib_zero_ex_transaction.ts @@ -0,0 +1,104 @@ +import { blockchainTests, constants, describe, expect, hexRandom } from '@0x/contracts-test-utils'; +import { eip712Utils } from '@0x/order-utils'; +import { ZeroExTransaction } from '@0x/types'; +import { BigNumber, signTypedDataUtils } from '@0x/utils'; +import * as _ from 'lodash'; + +import { artifacts, TestLibsContract } from '../src'; + +blockchainTests('LibZeroExTransaction', env => { + const CHAIN_ID = 1337; + let libsContract: TestLibsContract; + + const randomAddress = () => hexRandom(constants.ADDRESS_LENGTH); + const randomHash = () => hexRandom(constants.WORD_LENGTH); + const randomUint256 = () => new BigNumber(randomHash()); + const randomAssetData = () => hexRandom(36); + + const EMPTY_TRANSACTION: ZeroExTransaction = { + salt: constants.ZERO_AMOUNT, + expirationTimeSeconds: constants.ZERO_AMOUNT, + signerAddress: constants.NULL_ADDRESS, + data: constants.NULL_BYTES, + domain: { + verifyingContractAddress: constants.NULL_ADDRESS, + chainId: 0, + }, + }; + + /** + * Tests the `_hashZeroExTransaction()` function against a reference hash. + */ + async function testHashZeroExTransactionAsync(transaction: ZeroExTransaction): Promise { + const typedData = eip712Utils.createZeroExTransactionTypedData(transaction); + const expectedHash = '0x'.concat( + signTypedDataUtils.generateTypedDataHashWithoutDomain(typedData).toString('hex'), + ); + const actualHash = await libsContract.hashZeroExTransaction.callAsync(transaction); + expect(actualHash).to.be.eq(expectedHash); + } + + /** + * Tests the `getTransactionHash()` function against a reference hash. + */ + async function testGetTransactionHashAsync(transaction: ZeroExTransaction): Promise { + const typedData = eip712Utils.createZeroExTransactionTypedData(transaction); + const expectedHash = '0x'.concat(signTypedDataUtils.generateTypedDataHash(typedData).toString('hex')); + const actualHash = await libsContract.getTransactionHash.callAsync(transaction); + expect(actualHash).to.be.eq(expectedHash); + } + + before(async () => { + libsContract = await TestLibsContract.deployFrom0xArtifactAsync( + artifacts.TestLibs, + env.provider, + env.txDefaults, + new BigNumber(CHAIN_ID), + ); + }); + + describe('getTransactionHash', () => { + it('should correctly hash an empty transaction', async () => { + await testGetTransactionHashAsync({ + ...EMPTY_TRANSACTION, + domain: { + verifyingContractAddress: libsContract.address, + chainId: 1337, + }, + }); + }); + + it('should correctly hash a non-empty order', async () => { + await testGetTransactionHashAsync({ + salt: randomUint256(), + expirationTimeSeconds: randomUint256(), + signerAddress: randomAddress(), + data: randomAssetData(), + domain: { + verifyingContractAddress: libsContract.address, + chainId: 1337, + }, + }); + }); + }); + + describe('hashOrder', () => { + it('should correctly hash an empty order', async () => { + await testHashZeroExTransactionAsync(EMPTY_TRANSACTION); + }); + + it('should correctly hash a non-empty order', async () => { + await testHashZeroExTransactionAsync({ + salt: randomUint256(), + expirationTimeSeconds: randomUint256(), + signerAddress: randomAddress(), + data: randomAssetData(), + // The domain is not used in this test, so it's okay if it is left empty. + domain: { + verifyingContractAddress: constants.NULL_ADDRESS, + chainId: 0, + }, + }); + }); + }); +}); diff --git a/contracts/utils/test/lib_eip712.ts b/contracts/utils/test/lib_eip712.ts index a2a0b7218a..0a458e1061 100644 --- a/contracts/utils/test/lib_eip712.ts +++ b/contracts/utils/test/lib_eip712.ts @@ -41,25 +41,6 @@ async function testHashEIP712DomainAsync( expect(actualHash).to.be.eq(hexConcat(expectedHash)); } -/** - * Tests a specific instance of EIP712 message hashing. - * @param lib The LibEIP712 contract to call. - * @param domainHash The hash of the EIP712 domain of this instance. - * @param hashStruct The hash of the struct of this instance. - */ -async function testHashEIP712MessageAsync( - lib: TestLibEIP712Contract, - domainHash: string, - hashStruct: string, -): Promise { - const input = '0x1901'.concat( - domainHash.slice(2, domainHash.length).concat(hashStruct.slice(2, hashStruct.length)), - ); - const expectedHash = '0x'.concat(ethUtil.sha3(input).toString('hex')); - const actualHash = await lib.externalHashEIP712Message.callAsync(domainHash, hashStruct); - expect(actualHash).to.be.eq(expectedHash); -} - describe('LibEIP712', () => { let lib: TestLibEIP712Contract; @@ -73,6 +54,32 @@ describe('LibEIP712', () => { await blockchainLifecycle.revertAsync(); }); + /** + * Tests a specific instance of EIP712 message hashing. + * @param lib The LibEIP712 contract to call. + * @param domainHash The hash of the EIP712 domain of this instance. + * @param hashStruct The hash of the struct of this instance. + */ + async function testHashEIP712MessageAsync( + lib: TestLibEIP712Contract, + domainHash: string, + hashStruct: string, + ): Promise { + // Remove the hex prefix from the domain hash and the hash struct + const unprefixedDomainHash = domainHash.slice(2, domainHash.length); + const unprefixedHashStruct = hashStruct.slice(2, hashStruct.length); + + // Hash the provided input to get the expected hash + const input = '0x1901'.concat(unprefixedDomainHash.concat(unprefixedHashStruct)); + const expectedHash = '0x'.concat(ethUtil.sha3(input).toString('hex')); + + // Get the actual hash by calling the smart contract + const actualHash = await lib.externalHashEIP712Message.callAsync(domainHash, hashStruct); + + // Verify that the actual hash matches the expected hash + expect(actualHash).to.be.eq(expectedHash); + } + describe('_hashEIP712Domain', async () => { it('should correctly hash empty input', async () => { await testHashEIP712DomainAsync(lib, '', '', 0, constants.NULL_ADDRESS); diff --git a/packages/utils/src/sign_typed_data_utils.ts b/packages/utils/src/sign_typed_data_utils.ts index b9b9502a84..d1df23228a 100644 --- a/packages/utils/src/sign_typed_data_utils.ts +++ b/packages/utils/src/sign_typed_data_utils.ts @@ -20,6 +20,15 @@ export const signTypedDataUtils = { ]), ); }, + /** + * Generates the EIP712 Typed Data hash for a typed data object without using the domain field. This + * makes hashing easier for non-EIP712 data. + * @param typedData An object that conforms to the EIP712TypedData interface + * @return A Buffer containing the hash of the typed data. + */ + generateTypedDataHashWithoutDomain(typedData: EIP712TypedData): Buffer { + return signTypedDataUtils._structHash(typedData.primaryType, typedData.message, typedData.types); + }, /** * Generates the hash of a EIP712 Domain with the default schema * @param domain An EIP712 domain with the default schema containing a name, version, chain id,