diff --git a/contracts/zero-ex/CHANGELOG.json b/contracts/zero-ex/CHANGELOG.json index 54f97fee9d..6a43a3a8aa 100644 --- a/contracts/zero-ex/CHANGELOG.json +++ b/contracts/zero-ex/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "version": "0.14.0", + "changes": [ + { + "note": "Use the `MetaTransaction` class from `@0x/protocol-utils` in tests.", + "pr": 90 + } + ] + }, { "version": "0.13.0", "changes": [ diff --git a/contracts/zero-ex/package.json b/contracts/zero-ex/package.json index 85ade7f0cd..205867380c 100644 --- a/contracts/zero-ex/package.json +++ b/contracts/zero-ex/package.json @@ -61,6 +61,7 @@ "@0x/contracts-gen": "^2.0.24", "@0x/contracts-test-utils": "^5.3.15", "@0x/dev-utils": "^4.1.3", + "@0x/order-utils": "^10.4.10", "@0x/sol-compiler": "^4.4.1", "@0x/ts-doc-gen": "^0.0.28", "@0x/tslint-config": "^4.1.3", @@ -82,7 +83,6 @@ }, "dependencies": { "@0x/base-contract": "^6.2.14", - "@0x/order-utils": "^10.4.10", "@0x/protocol-utils": "^1.0.1", "@0x/subproviders": "^6.2.3", "@0x/types": "^3.3.1", diff --git a/contracts/zero-ex/test/features/meta_transactions_test.ts b/contracts/zero-ex/test/features/meta_transactions_test.ts index 8ae5170ee4..dc7e4e7f41 100644 --- a/contracts/zero-ex/test/features/meta_transactions_test.ts +++ b/contracts/zero-ex/test/features/meta_transactions_test.ts @@ -6,9 +6,7 @@ import { randomAddress, verifyEventsFromLogs, } from '@0x/contracts-test-utils'; -import { getExchangeProxyMetaTransactionHash, signatureUtils } from '@0x/order-utils'; -import { Signature } from '@0x/protocol-utils'; -import { ExchangeProxyMetaTransaction } from '@0x/types'; +import { MetaTransaction, MetaTransactionFields } from '@0x/protocol-utils'; import { BigNumber, hexUtils, StringRevertError, ZeroExRevertErrors } from '@0x/utils'; import * as _ from 'lodash'; @@ -31,6 +29,7 @@ blockchainTests.resets('MetaTransactions feature', env => { let owner: string; let maker: string; let sender: string; + let notSigner: string; let signers: string[]; let zeroEx: IZeroExContract; let feature: MetaTransactionsFeatureContract; @@ -45,7 +44,7 @@ blockchainTests.resets('MetaTransactions feature', env => { const REENTRANCY_FLAG_MTX = 0x1; before(async () => { - [owner, maker, sender, ...signers] = await env.getAccountAddressesAsync(); + [owner, maker, sender, notSigner, ...signers] = await env.getAccountAddressesAsync(); transformERC20Feature = await TestMetaTransactionsTransformERC20FeatureContract.deployFrom0xArtifactAsync( artifacts.TestMetaTransactionsTransformERC20Feature, env.provider, @@ -83,19 +82,8 @@ blockchainTests.resets('MetaTransactions feature', env => { ); }); - function sigstruct(signature: string): Signature { - return { - v: parseInt(hexUtils.slice(signature, 0, 1), 16), - signatureType: parseInt(hexUtils.slice(signature, 65, 66), 16), - r: hexUtils.slice(signature, 1, 33), - s: hexUtils.slice(signature, 33, 65), - }; - } - - function getRandomMetaTransaction( - fields: Partial = {}, - ): ExchangeProxyMetaTransaction { - return { + function getRandomMetaTransaction(fields: Partial = {}): MetaTransaction { + return new MetaTransaction({ signer: _.sampleSize(signers)[0], sender, minGasPrice: getRandomInteger('2', '1e9'), @@ -106,28 +94,16 @@ blockchainTests.resets('MetaTransactions feature', env => { value: getRandomInteger(1, '1e18'), feeToken: feeToken.address, feeAmount: getRandomInteger(1, MAX_FEE_AMOUNT), - domain: { - chainId: 1, // Ganache's `chainid` opcode is hardcoded as 1 - verifyingContract: zeroEx.address, - }, + chainId: 1, // Ganache's `chainid` opcode is hardcoded as 1 + verifyingContract: zeroEx.address, ...fields, - }; - } - - async function signMetaTransactionAsync(mtx: ExchangeProxyMetaTransaction, signer?: string): Promise { - return sigstruct( - await signatureUtils.ecSignHashAsync( - env.provider, - getExchangeProxyMetaTransactionHash(mtx), - signer || mtx.signer, - ), - ); + }); } describe('getMetaTransactionHash()', () => { it('generates the correct hash', async () => { const mtx = getRandomMetaTransaction(); - const expected = getExchangeProxyMetaTransactionHash(mtx); + const expected = mtx.getHash(); const actual = await feature.getMetaTransactionHash(mtx).callAsync(); expect(actual).to.eq(expected); }); @@ -163,7 +139,7 @@ blockchainTests.resets('MetaTransactions feature', env => { const mtx = getRandomMetaTransaction({ callData: nativeOrdersFeature.fillLimitOrder(order, sig, fillAmount).getABIEncodedTransactionData(), }); - const signature = await signMetaTransactionAsync(mtx); + const signature = await mtx.getSignatureWithProviderAsync(env.provider); const callOpts = { gasPrice: mtx.minGasPrice, value: mtx.value, @@ -198,7 +174,7 @@ blockchainTests.resets('MetaTransactions feature', env => { callData: nativeOrdersFeature.fillRfqOrder(order, sig, fillAmount).getABIEncodedTransactionData(), value: ZERO_AMOUNT, }); - const signature = await signMetaTransactionAsync(mtx); + const signature = await mtx.getSignatureWithProviderAsync(env.provider); const callOpts = { gasPrice: mtx.minGasPrice, value: 0, @@ -237,7 +213,7 @@ blockchainTests.resets('MetaTransactions feature', env => { ) .getABIEncodedTransactionData(), }); - const signature = await signMetaTransactionAsync(mtx); + const signature = await mtx.getSignatureWithProviderAsync(env.provider); const callOpts = { gasPrice: mtx.minGasPrice, value: mtx.value, @@ -275,7 +251,7 @@ blockchainTests.resets('MetaTransactions feature', env => { ) .getABIEncodedTransactionData(); const mtx = getRandomMetaTransaction({ callData }); - const signature = await signMetaTransactionAsync(mtx); + const signature = await mtx.getSignatureWithProviderAsync(env.provider); const callOpts = { gasPrice: mtx.minGasPrice, value: mtx.value, @@ -315,7 +291,7 @@ blockchainTests.resets('MetaTransactions feature', env => { ) .getABIEncodedTransactionData(), }); - const signature = await signMetaTransactionAsync(mtx); + const signature = await mtx.getSignatureWithProviderAsync(env.provider); const callOpts = { gasPrice: mtx.minGasPrice, value: mtx.value, @@ -340,7 +316,7 @@ blockchainTests.resets('MetaTransactions feature', env => { ) .getABIEncodedTransactionData(), }); - const signature = await signMetaTransactionAsync(mtx); + const signature = await mtx.getSignatureWithProviderAsync(env.provider); const callOpts = { gasPrice: mtx.minGasPrice, value: mtx.value, @@ -363,8 +339,8 @@ blockchainTests.resets('MetaTransactions feature', env => { ) .getABIEncodedTransactionData(), }); - const mtxHash = getExchangeProxyMetaTransactionHash(mtx); - const signature = await signMetaTransactionAsync(mtx); + const mtxHash = mtx.getHash(); + const signature = await mtx.getSignatureWithProviderAsync(env.provider); const callOpts = { gasPrice: mtx.minGasPrice, value: mtx.value, @@ -393,8 +369,8 @@ blockchainTests.resets('MetaTransactions feature', env => { const mtx = getRandomMetaTransaction({ callData: transformERC20Feature.createTransformWallet().getABIEncodedTransactionData(), }); - const mtxHash = getExchangeProxyMetaTransactionHash(mtx); - const signature = await signMetaTransactionAsync(mtx); + const mtxHash = mtx.getHash(); + const signature = await mtx.getSignatureWithProviderAsync(env.provider); const callOpts = { gasPrice: mtx.minGasPrice, value: mtx.value, @@ -421,8 +397,8 @@ blockchainTests.resets('MetaTransactions feature', env => { ) .getABIEncodedTransactionData(), }); - const mtxHash = getExchangeProxyMetaTransactionHash(mtx); - const signature = await signMetaTransactionAsync(mtx); + const mtxHash = mtx.getHash(); + const signature = await mtx.getSignatureWithProviderAsync(env.provider); const callOpts = { gasPrice: mtx.minGasPrice, value: mtx.value, @@ -439,8 +415,8 @@ blockchainTests.resets('MetaTransactions feature', env => { it('fails if not enough ETH provided', async () => { const mtx = getRandomMetaTransaction(); - const mtxHash = getExchangeProxyMetaTransactionHash(mtx); - const signature = await signMetaTransactionAsync(mtx); + const mtxHash = mtx.getHash(); + const signature = await mtx.getSignatureWithProviderAsync(env.provider); const callOpts = { gasPrice: mtx.minGasPrice, value: mtx.value.minus(1), @@ -457,8 +433,8 @@ blockchainTests.resets('MetaTransactions feature', env => { it('fails if gas price too low', async () => { const mtx = getRandomMetaTransaction(); - const mtxHash = getExchangeProxyMetaTransactionHash(mtx); - const signature = await signMetaTransactionAsync(mtx); + const mtxHash = mtx.getHash(); + const signature = await mtx.getSignatureWithProviderAsync(env.provider); const callOpts = { gasPrice: mtx.minGasPrice.minus(1), value: mtx.value, @@ -476,8 +452,8 @@ blockchainTests.resets('MetaTransactions feature', env => { it('fails if gas price too high', async () => { const mtx = getRandomMetaTransaction(); - const mtxHash = getExchangeProxyMetaTransactionHash(mtx); - const signature = await signMetaTransactionAsync(mtx); + const mtxHash = mtx.getHash(); + const signature = await mtx.getSignatureWithProviderAsync(env.provider); const callOpts = { gasPrice: mtx.maxGasPrice.plus(1), value: mtx.value, @@ -497,8 +473,8 @@ blockchainTests.resets('MetaTransactions feature', env => { const mtx = getRandomMetaTransaction({ expirationTimeSeconds: new BigNumber(Math.floor(_.now() / 1000 - 60)), }); - const mtxHash = getExchangeProxyMetaTransactionHash(mtx); - const signature = await signMetaTransactionAsync(mtx); + const mtxHash = mtx.getHash(); + const signature = await mtx.getSignatureWithProviderAsync(env.provider); const callOpts = { gasPrice: mtx.maxGasPrice, value: mtx.value, @@ -518,8 +494,8 @@ blockchainTests.resets('MetaTransactions feature', env => { const mtx = getRandomMetaTransaction({ sender: requiredSender, }); - const mtxHash = getExchangeProxyMetaTransactionHash(mtx); - const signature = await signMetaTransactionAsync(mtx); + const mtxHash = mtx.getHash(); + const signature = await mtx.getSignatureWithProviderAsync(env.provider); const callOpts = { gasPrice: mtx.maxGasPrice, value: mtx.value, @@ -536,8 +512,8 @@ blockchainTests.resets('MetaTransactions feature', env => { it('fails if signature is wrong', async () => { const mtx = getRandomMetaTransaction({ signer: signers[0] }); - const mtxHash = getExchangeProxyMetaTransactionHash(mtx); - const signature = await signMetaTransactionAsync(mtx, signers[1]); + const mtxHash = mtx.getHash(); + const signature = await mtx.clone({ signer: notSigner }).getSignatureWithProviderAsync(env.provider); const callOpts = { gasPrice: mtx.maxGasPrice, value: mtx.value, @@ -567,8 +543,8 @@ blockchainTests.resets('MetaTransactions feature', env => { .getABIEncodedTransactionData(), value: TRANSFORM_ERC20_REENTER_VALUE, }); - const mtxHash = getExchangeProxyMetaTransactionHash(mtx); - const signature = await signMetaTransactionAsync(mtx); + const mtxHash = mtx.getHash(); + const signature = await mtx.getSignatureWithProviderAsync(env.provider); const callOpts = { gasPrice: mtx.maxGasPrice, value: mtx.value, @@ -600,8 +576,8 @@ blockchainTests.resets('MetaTransactions feature', env => { .getABIEncodedTransactionData(), value: TRANSFORM_ERC20_BATCH_REENTER_VALUE, }); - const mtxHash = getExchangeProxyMetaTransactionHash(mtx); - const signature = await signMetaTransactionAsync(mtx); + const mtxHash = mtx.getHash(); + const signature = await mtx.getSignatureWithProviderAsync(env.provider); const callOpts = { gasPrice: mtx.maxGasPrice, value: mtx.value, @@ -633,8 +609,8 @@ blockchainTests.resets('MetaTransactions feature', env => { .getABIEncodedTransactionData(), value: TRANSFORM_ERC20_REENTER_VALUE, }); - const mtxHash = getExchangeProxyMetaTransactionHash(mtx); - const signature = await signMetaTransactionAsync(mtx); + const mtxHash = mtx.getHash(); + const signature = await mtx.getSignatureWithProviderAsync(env.provider); const callOpts = { gasPrice: mtx.maxGasPrice, value: mtx.value, @@ -666,8 +642,8 @@ blockchainTests.resets('MetaTransactions feature', env => { .getABIEncodedTransactionData(), value: TRANSFORM_ERC20_BATCH_REENTER_VALUE, }); - const mtxHash = getExchangeProxyMetaTransactionHash(mtx); - const signature = await signMetaTransactionAsync(mtx); + const mtxHash = mtx.getHash(); + const signature = await mtx.getSignatureWithProviderAsync(env.provider); const callOpts = { gasPrice: mtx.maxGasPrice, value: mtx.value, @@ -703,7 +679,9 @@ blockchainTests.resets('MetaTransactions feature', env => { .getABIEncodedTransactionData(), }); }); - const signatures = await Promise.all(mtxs.map(async mtx => signMetaTransactionAsync(mtx))); + const signatures = await Promise.all( + mtxs.map(async mtx => mtx.getSignatureWithProviderAsync(env.provider)), + ); const callOpts = { gasPrice: BigNumber.max(...mtxs.map(mtx => mtx.minGasPrice)), value: BigNumber.sum(...mtxs.map(mtx => mtx.value)), @@ -728,9 +706,9 @@ blockchainTests.resets('MetaTransactions feature', env => { .getABIEncodedTransactionData(), }); })(); - const mtxHash = getExchangeProxyMetaTransactionHash(mtx); + const mtxHash = mtx.getHash(); const mtxs = _.times(2, () => mtx); - const signatures = await Promise.all(mtxs.map(async m => signMetaTransactionAsync(m))); + const signatures = await Promise.all(mtxs.map(async m => m.getSignatureWithProviderAsync(env.provider))); const callOpts = { gasPrice: BigNumber.max(...mtxs.map(m => m.minGasPrice)), value: BigNumber.sum(...mtxs.map(m => m.value)), @@ -756,8 +734,8 @@ blockchainTests.resets('MetaTransactions feature', env => { ) .getABIEncodedTransactionData(), }); - const mtxHash = getExchangeProxyMetaTransactionHash(mtx); - const signature = await signMetaTransactionAsync(mtx); + const mtxHash = mtx.getHash(); + const signature = await mtx.getSignatureWithProviderAsync(env.provider); const callOpts = { gasPrice: mtx.minGasPrice, value: mtx.value, @@ -786,8 +764,8 @@ blockchainTests.resets('MetaTransactions feature', env => { .getABIEncodedTransactionData(), value: TRANSFORM_ERC20_REENTER_VALUE, }); - const mtxHash = getExchangeProxyMetaTransactionHash(mtx); - const signature = await signMetaTransactionAsync(mtx); + const mtxHash = mtx.getHash(); + const signature = await mtx.getSignatureWithProviderAsync(env.provider); const callOpts = { gasPrice: mtx.maxGasPrice, value: mtx.value, @@ -819,8 +797,8 @@ blockchainTests.resets('MetaTransactions feature', env => { .getABIEncodedTransactionData(), value: TRANSFORM_ERC20_BATCH_REENTER_VALUE, }); - const mtxHash = getExchangeProxyMetaTransactionHash(mtx); - const signature = await signMetaTransactionAsync(mtx); + const mtxHash = mtx.getHash(); + const signature = await mtx.getSignatureWithProviderAsync(env.provider); const callOpts = { gasPrice: mtx.maxGasPrice, value: mtx.value, @@ -859,7 +837,7 @@ blockchainTests.resets('MetaTransactions feature', env => { ) .getABIEncodedTransactionData(), }); - const signature = await signMetaTransactionAsync(mtx); + const signature = await mtx.getSignatureWithProviderAsync(env.provider); const callOpts = { gasPrice: mtx.minGasPrice, value: mtx.value, @@ -873,7 +851,7 @@ blockchainTests.resets('MetaTransactions feature', env => { describe('getMetaTransactionHashExecutedBlock()', () => { it('returns zero for an unexecuted mtx', async () => { const mtx = getRandomMetaTransaction(); - const mtxHash = getExchangeProxyMetaTransactionHash(mtx); + const mtxHash = mtx.getHash(); const block = await feature.getMetaTransactionHashExecutedBlock(mtxHash).callAsync(); expect(block).to.bignumber.eq(0); }); @@ -891,13 +869,13 @@ blockchainTests.resets('MetaTransactions feature', env => { ) .getABIEncodedTransactionData(), }); - const signature = await signMetaTransactionAsync(mtx); + const signature = await mtx.getSignatureWithProviderAsync(env.provider); const callOpts = { gasPrice: mtx.minGasPrice, value: mtx.value, }; const receipt = await feature.executeMetaTransaction(mtx, signature).awaitTransactionSuccessAsync(callOpts); - const mtxHash = getExchangeProxyMetaTransactionHash(mtx); + const mtxHash = mtx.getHash(); const block = await feature.getMetaTransactionHashExecutedBlock(mtxHash).callAsync(); expect(block).to.bignumber.eq(receipt.blockNumber); }); diff --git a/packages/protocol-utils/CHANGELOG.json b/packages/protocol-utils/CHANGELOG.json index 0dbf2f00e4..b142e958f5 100644 --- a/packages/protocol-utils/CHANGELOG.json +++ b/packages/protocol-utils/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "version": "1.1.0", + "changes": [ + { + "note": "Add the `MetaTransaction` class for EP mtxs", + "pr": 90 + } + ] + }, { "timestamp": 1607485227, "version": "1.0.1", diff --git a/packages/protocol-utils/src/constants.ts b/packages/protocol-utils/src/constants.ts index 5e17e10027..5a12b968ce 100644 --- a/packages/protocol-utils/src/constants.ts +++ b/packages/protocol-utils/src/constants.ts @@ -1 +1,4 @@ +import { BigNumber } from '@0x/utils'; + export const ETH_TOKEN_ADDRESS = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'; +export const ZERO = new BigNumber(0); diff --git a/packages/protocol-utils/src/eip712_utils.ts b/packages/protocol-utils/src/eip712_utils.ts index 3cbc5e9675..ebe2303c4a 100644 --- a/packages/protocol-utils/src/eip712_utils.ts +++ b/packages/protocol-utils/src/eip712_utils.ts @@ -7,6 +7,8 @@ export interface EIP712Domain { verifyingContract: string; } +export type EIP712_STRUCT_ABI = Array<{ type: string; name: string }>; + export const EIP712_DOMAIN_PARAMETERS = [ { name: 'name', type: 'string' }, { name: 'version', type: 'string' }, @@ -68,3 +70,12 @@ export function getExchangeProxyEIP712Hash(structHash: string, chainId?: number, hexUtils.concat('0x1901', getExchangeProxyEIP712DomainHash(chainId, verifyingContract), structHash), ); } + +/** + * Compute the type hash of an EIP712 struct given its ABI. + */ +export function getTypeHash(structName: string, abi: EIP712_STRUCT_ABI): string { + return hexUtils.hash( + hexUtils.toHex(Buffer.from([`${structName}(`, abi.map(a => `${a.type} ${a.name}`).join(','), ')'].join(''))), + ); +} diff --git a/packages/protocol-utils/src/index.ts b/packages/protocol-utils/src/index.ts index dd9b7f7714..15466ec4f3 100644 --- a/packages/protocol-utils/src/index.ts +++ b/packages/protocol-utils/src/index.ts @@ -4,6 +4,7 @@ export const RevertError = _RevertErrors.RevertError; export * from './eip712_utils'; export * from './orders'; +export * from './meta_transactions'; export * from './signature_utils'; export * from './transformer_utils'; export * from './constants'; diff --git a/packages/protocol-utils/src/meta_transactions.ts b/packages/protocol-utils/src/meta_transactions.ts new file mode 100644 index 0000000000..d201ad9707 --- /dev/null +++ b/packages/protocol-utils/src/meta_transactions.ts @@ -0,0 +1,171 @@ +import { getContractAddressesForChainOrThrow } from '@0x/contract-addresses'; +import { SupportedProvider } from '@0x/subproviders'; +import { EIP712TypedData } from '@0x/types'; +import { BigNumber, hexUtils, NULL_ADDRESS } from '@0x/utils'; + +import { ZERO } from './constants'; +import { + createExchangeProxyEIP712Domain, + EIP712_DOMAIN_PARAMETERS, + getExchangeProxyEIP712Hash, + getTypeHash, +} from './eip712_utils'; +import { + eip712SignTypedDataWithKey, + eip712SignTypedDataWithProviderAsync, + ethSignHashWithKey, + ethSignHashWithProviderAsync, + Signature, + SignatureType, +} from './signature_utils'; + +const MTX_DEFAULT_VALUES = { + signer: NULL_ADDRESS, + sender: NULL_ADDRESS, + minGasPrice: ZERO, + maxGasPrice: ZERO, + expirationTimeSeconds: ZERO, + salt: ZERO, + callData: hexUtils.leftPad(0), + value: ZERO, + feeToken: NULL_ADDRESS, + feeAmount: ZERO, + chainId: 1, + verifyingContract: getContractAddressesForChainOrThrow(1).exchangeProxy, +}; + +export type MetaTransactionFields = typeof MTX_DEFAULT_VALUES; + +export class MetaTransaction { + public static readonly STRUCT_NAME = 'MetaTransactionData'; + public static readonly STRUCT_ABI = [ + { type: 'address', name: 'signer' }, + { type: 'address', name: 'sender' }, + { type: 'uint256', name: 'minGasPrice' }, + { type: 'uint256', name: 'maxGasPrice' }, + { type: 'uint256', name: 'expirationTimeSeconds' }, + { type: 'uint256', name: 'salt' }, + { type: 'bytes', name: 'callData' }, + { type: 'uint256', name: 'value' }, + { type: 'address', name: 'feeToken' }, + { type: 'uint256', name: 'feeAmount' }, + ]; + public static readonly TYPE_HASH = getTypeHash(MetaTransaction.STRUCT_NAME, MetaTransaction.STRUCT_ABI); + + public signer: string; + public sender: string; + public minGasPrice: BigNumber; + public maxGasPrice: BigNumber; + public expirationTimeSeconds: BigNumber; + public salt: BigNumber; + public callData: string; + public value: BigNumber; + public feeToken: string; + public feeAmount: BigNumber; + public chainId: number; + public verifyingContract: string; + + public constructor(fields: Partial = {}) { + const _fields = { ...MTX_DEFAULT_VALUES, ...fields }; + this.signer = _fields.signer; + this.sender = _fields.sender; + this.minGasPrice = _fields.minGasPrice; + this.maxGasPrice = _fields.maxGasPrice; + this.expirationTimeSeconds = _fields.expirationTimeSeconds; + this.salt = _fields.salt; + this.callData = _fields.callData; + this.value = _fields.value; + this.feeToken = _fields.feeToken; + this.feeAmount = _fields.feeAmount; + this.chainId = _fields.chainId; + this.verifyingContract = _fields.verifyingContract; + } + + public clone(fields: Partial = {}): MetaTransaction { + return new MetaTransaction({ + signer: this.signer, + sender: this.sender, + minGasPrice: this.minGasPrice, + maxGasPrice: this.maxGasPrice, + expirationTimeSeconds: this.expirationTimeSeconds, + salt: this.salt, + callData: this.callData, + value: this.value, + feeToken: this.feeToken, + feeAmount: this.feeAmount, + chainId: this.chainId, + verifyingContract: this.verifyingContract, + ...fields, + }); + } + + public getStructHash(): string { + return hexUtils.hash( + hexUtils.concat( + hexUtils.leftPad(MetaTransaction.TYPE_HASH), + hexUtils.leftPad(this.signer), + hexUtils.leftPad(this.sender), + hexUtils.leftPad(this.minGasPrice), + hexUtils.leftPad(this.maxGasPrice), + hexUtils.leftPad(this.expirationTimeSeconds), + hexUtils.leftPad(this.salt), + hexUtils.hash(this.callData), + hexUtils.leftPad(this.value), + hexUtils.leftPad(this.feeToken), + hexUtils.leftPad(this.feeAmount), + ), + ); + } + + public getEIP712TypedData(): EIP712TypedData { + return { + types: { + EIP712Domain: EIP712_DOMAIN_PARAMETERS, + [MetaTransaction.STRUCT_NAME]: MetaTransaction.STRUCT_ABI, + }, + domain: createExchangeProxyEIP712Domain(this.chainId, this.verifyingContract) as any, + primaryType: MetaTransaction.STRUCT_NAME, + message: { + signer: this.signer, + sender: this.sender, + minGasPrice: this.minGasPrice.toString(10), + maxGasPrice: this.maxGasPrice.toString(10), + expirationTimeSeconds: this.expirationTimeSeconds.toString(10), + salt: this.salt.toString(10), + callData: this.callData, + value: this.value.toString(10), + feeToken: this.feeToken, + feeAmount: this.feeAmount.toString(10), + }, + }; + } + + public getHash(): string { + return getExchangeProxyEIP712Hash(this.getStructHash(), this.chainId, this.verifyingContract); + } + + public async getSignatureWithProviderAsync( + provider: SupportedProvider, + type: SignatureType = SignatureType.EthSign, + ): Promise { + switch (type) { + case SignatureType.EIP712: + return eip712SignTypedDataWithProviderAsync(this.getEIP712TypedData(), this.signer, provider); + case SignatureType.EthSign: + return ethSignHashWithProviderAsync(this.getHash(), this.signer, provider); + default: + throw new Error(`Cannot sign with signature type: ${type}`); + } + } + + public getSignatureWithKey(key: string, type: SignatureType = SignatureType.EthSign): Signature { + switch (type) { + case SignatureType.EIP712: + return eip712SignTypedDataWithKey(this.getEIP712TypedData(), key); + case SignatureType.EthSign: + return ethSignHashWithKey(this.getHash(), key); + default: + throw new Error(`Cannot sign with signature type: ${type}`); + } + } +} diff --git a/packages/protocol-utils/src/orders.ts b/packages/protocol-utils/src/orders.ts index 6a53506f03..31a5812e09 100644 --- a/packages/protocol-utils/src/orders.ts +++ b/packages/protocol-utils/src/orders.ts @@ -1,8 +1,15 @@ +import { getContractAddressesForChainOrThrow } from '@0x/contract-addresses'; import { SupportedProvider } from '@0x/subproviders'; import { EIP712TypedData } from '@0x/types'; import { BigNumber, hexUtils, NULL_ADDRESS } from '@0x/utils'; -import { createExchangeProxyEIP712Domain, EIP712_DOMAIN_PARAMETERS, getExchangeProxyEIP712Hash } from './eip712_utils'; +import { ZERO } from './constants'; +import { + createExchangeProxyEIP712Domain, + EIP712_DOMAIN_PARAMETERS, + getExchangeProxyEIP712Hash, + getTypeHash, +} from './eip712_utils'; import { eip712SignTypedDataWithKey, eip712SignTypedDataWithProviderAsync, @@ -12,7 +19,6 @@ import { SignatureType, } from './signature_utils'; -const ZERO = new BigNumber(0); const COMMON_ORDER_DEFAULT_VALUES = { makerToken: NULL_ADDRESS, takerToken: NULL_ADDRESS, @@ -24,7 +30,7 @@ const COMMON_ORDER_DEFAULT_VALUES = { expiry: ZERO, salt: ZERO, chainId: 1, - verifyingContract: '0xdef1c0ded9bec7f1a1670819833240f027b25eff', + verifyingContract: getContractAddressesForChainOrThrow(1).exchangeProxy, }; const LIMIT_ORDER_DEFAULT_VALUES = { ...COMMON_ORDER_DEFAULT_VALUES, @@ -117,30 +123,22 @@ export abstract class OrderBase { } export class LimitOrder extends OrderBase { - public static readonly TYPE_HASH = hexUtils.hash( - hexUtils.toHex( - Buffer.from( - [ - 'LimitOrder(', - [ - 'address makerToken', - 'address takerToken', - 'uint128 makerAmount', - 'uint128 takerAmount', - 'uint128 takerTokenFeeAmount', - 'address maker', - 'address taker', - 'address sender', - 'address feeRecipient', - 'bytes32 pool', - 'uint64 expiry', - 'uint256 salt', - ].join(','), - ')', - ].join(''), - ), - ), - ); + public static readonly STRUCT_NAME = 'LimitOrder'; + public static readonly STRUCT_ABI = [ + { type: 'address', name: 'makerToken' }, + { type: 'address', name: 'takerToken' }, + { type: 'uint128', name: 'makerAmount' }, + { type: 'uint128', name: 'takerAmount' }, + { type: 'uint128', name: 'takerTokenFeeAmount' }, + { type: 'address', name: 'maker' }, + { type: 'address', name: 'taker' }, + { type: 'address', name: 'sender' }, + { type: 'address', name: 'feeRecipient' }, + { type: 'bytes32', name: 'pool' }, + { type: 'uint64', name: 'expiry' }, + { type: 'uint256', name: 'salt' }, + ]; + public static readonly TYPE_HASH = getTypeHash(LimitOrder.STRUCT_NAME, LimitOrder.STRUCT_ABI); public takerTokenFeeAmount: BigNumber; public sender: string; @@ -198,23 +196,10 @@ export class LimitOrder extends OrderBase { return { types: { EIP712Domain: EIP712_DOMAIN_PARAMETERS, - LimitOrder: [ - { type: 'address', name: 'makerToken' }, - { type: 'address', name: 'takerToken' }, - { type: 'uint128', name: 'makerAmount' }, - { type: 'uint128', name: 'takerAmount' }, - { type: 'uint128', name: 'takerTokenFeeAmount' }, - { type: 'address', name: 'maker' }, - { type: 'address', name: 'taker' }, - { type: 'address', name: 'sender' }, - { type: 'address', name: 'feeRecipient' }, - { type: 'bytes32', name: 'pool' }, - { type: 'uint64', name: 'expiry' }, - { type: 'uint256', name: 'salt' }, - ], + [LimitOrder.STRUCT_NAME]: LimitOrder.STRUCT_ABI, }, domain: createExchangeProxyEIP712Domain(this.chainId, this.verifyingContract) as any, - primaryType: 'LimitOrder', + primaryType: LimitOrder.STRUCT_NAME, message: { makerToken: this.makerToken, takerToken: this.takerToken, @@ -234,28 +219,20 @@ export class LimitOrder extends OrderBase { } export class RfqOrder extends OrderBase { - public static readonly TYPE_HASH = hexUtils.hash( - hexUtils.toHex( - Buffer.from( - [ - 'RfqOrder(', - [ - 'address makerToken', - 'address takerToken', - 'uint128 makerAmount', - 'uint128 takerAmount', - 'address maker', - 'address taker', - 'address txOrigin', - 'bytes32 pool', - 'uint64 expiry', - 'uint256 salt', - ].join(','), - ')', - ].join(''), - ), - ), - ); + public static readonly STRUCT_NAME = 'RfqOrder'; + public static readonly STRUCT_ABI = [ + { type: 'address', name: 'makerToken' }, + { type: 'address', name: 'takerToken' }, + { type: 'uint128', name: 'makerAmount' }, + { type: 'uint128', name: 'takerAmount' }, + { type: 'address', name: 'maker' }, + { type: 'address', name: 'taker' }, + { type: 'address', name: 'txOrigin' }, + { type: 'bytes32', name: 'pool' }, + { type: 'uint64', name: 'expiry' }, + { type: 'uint256', name: 'salt' }, + ]; + public static readonly TYPE_HASH = getTypeHash(RfqOrder.STRUCT_NAME, RfqOrder.STRUCT_ABI); public txOrigin: string; @@ -305,21 +282,10 @@ export class RfqOrder extends OrderBase { return { types: { EIP712Domain: EIP712_DOMAIN_PARAMETERS, - RfqOrder: [ - { type: 'address', name: 'makerToken' }, - { type: 'address', name: 'takerToken' }, - { type: 'uint128', name: 'makerAmount' }, - { type: 'uint128', name: 'takerAmount' }, - { type: 'address', name: 'maker' }, - { type: 'address', name: 'taker' }, - { type: 'address', name: 'txOrigin' }, - { type: 'bytes32', name: 'pool' }, - { type: 'uint64', name: 'expiry' }, - { type: 'uint256', name: 'salt' }, - ], + [RfqOrder.STRUCT_NAME]: RfqOrder.STRUCT_ABI, }, domain: createExchangeProxyEIP712Domain(this.chainId, this.verifyingContract) as any, - primaryType: 'RfqOrder', + primaryType: RfqOrder.STRUCT_NAME, message: { makerToken: this.makerToken, takerToken: this.takerToken, diff --git a/packages/protocol-utils/test/meta_transactions_test.ts b/packages/protocol-utils/test/meta_transactions_test.ts new file mode 100644 index 0000000000..4413b2a3d5 --- /dev/null +++ b/packages/protocol-utils/test/meta_transactions_test.ts @@ -0,0 +1,84 @@ +import { chaiSetup, web3Factory, Web3Wrapper } from '@0x/dev-utils'; +import { Web3ProviderEngine } from '@0x/subproviders'; +import { BigNumber } from '@0x/utils'; +import { expect } from 'chai'; +import * as ethjs from 'ethereumjs-util'; + +import { MetaTransaction } from '../src/meta_transactions'; +import { SignatureType } from '../src/signature_utils'; + +chaiSetup.configure(); + +describe('mtxs', () => { + let provider: Web3ProviderEngine; + let providerMaker: string; + const key = '0xee094b79aa0315914955f2f09be9abe541dcdc51f0aae5bec5453e9f73a471a6'; + const keyMaker = ethjs.bufferToHex(ethjs.privateToAddress(ethjs.toBuffer(key))); + + before(async () => { + provider = web3Factory.getRpcProvider({ shouldUseInProcessGanache: true }); + [providerMaker] = await new Web3Wrapper(provider).getAvailableAddressesAsync(); + }); + + describe('MetaTransaction', () => { + const mtx = new MetaTransaction({ + signer: '0x349e8d89e8b37214d9ce3949fc5754152c525bc3', + sender: '0x83c62b2e67dea0df2a27be0def7a22bd7102642c', + minGasPrice: new BigNumber(1234), + maxGasPrice: new BigNumber(5678), + expirationTimeSeconds: new BigNumber(9101112), + salt: new BigNumber(2001), + callData: '0x12345678', + value: new BigNumber(1001), + feeToken: '0xcc3c7ea403427154ec908203ba6c418bd699f7ce', + feeAmount: new BigNumber(9101112), + chainId: 8008, + verifyingContract: '0x6701704d2421c64ee9aa93ec7f96ede81c4be77d', + }); + + it('can get the struct hash', () => { + const actual = mtx.getStructHash(); + const expected = '0x164b8bfaed3718d233d4cc87501d0d8fa0a72ed7deeb8e591524133f17867180'; + expect(actual).to.eq(expected); + }); + + it('can get the EIP712 hash', () => { + const actual = mtx.getHash(); + const expected = '0x068f2f98836e489070608461768bfd3331128787d09278d38869c2b56bfc34a4'; + expect(actual).to.eq(expected); + }); + + it('can get an EthSign signature with a provider', async () => { + const actual = await mtx.clone({ signer: providerMaker }).getSignatureWithProviderAsync(provider); + const expected = { + signatureType: SignatureType.EthSign, + r: '0xbb831776a2d6639d4e4d1641f158773ce202881bac74dddb2672d5ff5521ef5c', + s: '0x746a61ccfdfee3afae15f4a3bd67ded2ce555d89d482940a844eeffaede2ee8a', + v: 27, + }; + expect(actual).to.deep.eq(expected); + }); + + it('can get an EthSign signature with a private key', () => { + const actual = mtx.clone({ signer: keyMaker }).getSignatureWithKey(key); + const expected = { + signatureType: SignatureType.EthSign, + r: '0xbf19b5ef62df8c8315727087e9d8562e3b88d32452ac8193e3ed9f5354a220ef', + s: '0x512387e81b2c03e4bc4cf72ee5293c86498c17fde3ae89f18dd0705076a7f472', + v: 28, + }; + expect(actual).to.deep.eq(expected); + }); + + it('can get an EIP712 signature with a private key', () => { + const actual = mtx.clone({ signer: keyMaker }).getSignatureWithKey(key, SignatureType.EIP712); + const expected = { + signatureType: SignatureType.EIP712, + r: '0x050c6b80a3fafa1b816fdfd646f3e90862a21d3fbf3ed675eaf9c89e092ec405', + s: '0x179600bd412820233598628b85b58f1e9f6da4555421f45266ec2ebf94153d1d', + v: 27, + }; + expect(actual).to.deep.eq(expected); + }); + }); +});