Add mtx tooling to @0x/protocol-utils (#90)

* `@0x/contracts-zero-ex`: Use the `MetaTransaction` class from `@0x/protocol-utils` in tests.

* `@0x/protocol-utils`: Add the `MetaTransaction` class for EP mtxs + refactors

* update changelogs

* `@0x/protocol-utils`: Add mtx tests

* `@0x/protocol-utils`: Rename `mtx.ts` to `meta_transactions.ts` and misc review feedback

Co-authored-by: Lawrence Forman <me@merklejerk.com>
This commit is contained in:
Lawrence Forman
2020-12-16 14:22:00 -05:00
committed by GitHub
parent 602605ab4b
commit 682c07cb73
10 changed files with 388 additions and 156 deletions

View File

@@ -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",

View File

@@ -1 +1,4 @@
import { BigNumber } from '@0x/utils';
export const ETH_TOKEN_ADDRESS = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee';
export const ZERO = new BigNumber(0);

View File

@@ -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(''))),
);
}

View File

@@ -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';

View File

@@ -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<MetaTransactionFields> = {}) {
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<MetaTransactionFields> = {}): 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<Signature> {
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}`);
}
}
}

View File

@@ -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,

View File

@@ -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);
});
});
});