915 lines
		
	
	
		
			39 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			915 lines
		
	
	
		
			39 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import {
 | |
|     blockchainTests,
 | |
|     constants,
 | |
|     expect,
 | |
|     getRandomInteger,
 | |
|     randomAddress,
 | |
|     verifyEventsFromLogs,
 | |
| } from '@0x/contracts-test-utils';
 | |
| import { getExchangeProxyMetaTransactionHash, signatureUtils } from '@0x/order-utils';
 | |
| import { ExchangeProxyMetaTransaction } from '@0x/types';
 | |
| import { BigNumber, hexUtils, StringRevertError, ZeroExRevertErrors } from '@0x/utils';
 | |
| import * as _ from 'lodash';
 | |
| 
 | |
| import { Signature } from '../../src/signature_utils';
 | |
| import { generateCallDataSignature, signCallData } from '../../src/signed_call_data';
 | |
| import { IZeroExContract, MetaTransactionsFeatureContract } from '../../src/wrappers';
 | |
| import { artifacts } from '../artifacts';
 | |
| import { abis } from '../utils/abis';
 | |
| import { fullMigrateAsync } from '../utils/migration';
 | |
| import { getRandomLimitOrder, getRandomRfqOrder } from '../utils/orders';
 | |
| import {
 | |
|     TestMetaTransactionsNativeOrdersFeatureContract,
 | |
|     TestMetaTransactionsNativeOrdersFeatureEvents,
 | |
|     TestMetaTransactionsTransformERC20FeatureContract,
 | |
|     TestMetaTransactionsTransformERC20FeatureEvents,
 | |
|     TestMintableERC20TokenContract,
 | |
| } from '../wrappers';
 | |
| 
 | |
| const { NULL_ADDRESS, NULL_BYTES, ZERO_AMOUNT } = constants;
 | |
| 
 | |
| blockchainTests.resets('MetaTransactions feature', env => {
 | |
|     let owner: string;
 | |
|     let maker: string;
 | |
|     let sender: string;
 | |
|     let signers: string[];
 | |
|     let zeroEx: IZeroExContract;
 | |
|     let feature: MetaTransactionsFeatureContract;
 | |
|     let feeToken: TestMintableERC20TokenContract;
 | |
|     let transformERC20Feature: TestMetaTransactionsTransformERC20FeatureContract;
 | |
|     let nativeOrdersFeature: TestMetaTransactionsNativeOrdersFeatureContract;
 | |
| 
 | |
|     const MAX_FEE_AMOUNT = new BigNumber('1e18');
 | |
|     const TRANSFORM_ERC20_FAILING_VALUE = new BigNumber(666);
 | |
|     const TRANSFORM_ERC20_REENTER_VALUE = new BigNumber(777);
 | |
|     const TRANSFORM_ERC20_BATCH_REENTER_VALUE = new BigNumber(888);
 | |
|     const REENTRANCY_FLAG_MTX = 0x1;
 | |
| 
 | |
|     before(async () => {
 | |
|         [owner, maker, sender, ...signers] = await env.getAccountAddressesAsync();
 | |
|         transformERC20Feature = await TestMetaTransactionsTransformERC20FeatureContract.deployFrom0xArtifactAsync(
 | |
|             artifacts.TestMetaTransactionsTransformERC20Feature,
 | |
|             env.provider,
 | |
|             env.txDefaults,
 | |
|             {},
 | |
|         );
 | |
|         nativeOrdersFeature = await TestMetaTransactionsNativeOrdersFeatureContract.deployFrom0xArtifactAsync(
 | |
|             artifacts.TestMetaTransactionsNativeOrdersFeature,
 | |
|             env.provider,
 | |
|             env.txDefaults,
 | |
|             {},
 | |
|         );
 | |
|         zeroEx = await fullMigrateAsync(owner, env.provider, env.txDefaults, {
 | |
|             transformERC20: transformERC20Feature.address,
 | |
|             nativeOrders: nativeOrdersFeature.address,
 | |
|         });
 | |
|         feature = new MetaTransactionsFeatureContract(
 | |
|             zeroEx.address,
 | |
|             env.provider,
 | |
|             { ...env.txDefaults, from: sender },
 | |
|             abis,
 | |
|         );
 | |
|         feeToken = await TestMintableERC20TokenContract.deployFrom0xArtifactAsync(
 | |
|             artifacts.TestMintableERC20Token,
 | |
|             env.provider,
 | |
|             env.txDefaults,
 | |
|             {},
 | |
|         );
 | |
|         // Fund signers with fee tokens.
 | |
|         await Promise.all(
 | |
|             signers.map(async signer => {
 | |
|                 await feeToken.mint(signer, MAX_FEE_AMOUNT).awaitTransactionSuccessAsync();
 | |
|                 await feeToken.approve(zeroEx.address, MAX_FEE_AMOUNT).awaitTransactionSuccessAsync({ from: signer });
 | |
|             }),
 | |
|         );
 | |
|     });
 | |
| 
 | |
|     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> = {},
 | |
|     ): ExchangeProxyMetaTransaction {
 | |
|         return {
 | |
|             signer: _.sampleSize(signers)[0],
 | |
|             sender,
 | |
|             minGasPrice: getRandomInteger('2', '1e9'),
 | |
|             maxGasPrice: getRandomInteger('1e9', '100e9'),
 | |
|             expirationTimeSeconds: new BigNumber(Math.floor(_.now() / 1000) + 360),
 | |
|             salt: new BigNumber(hexUtils.random()),
 | |
|             callData: hexUtils.random(4),
 | |
|             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,
 | |
|             },
 | |
|             ...fields,
 | |
|         };
 | |
|     }
 | |
| 
 | |
|     async function signMetaTransactionAsync(mtx: ExchangeProxyMetaTransaction, signer?: string): Promise<Signature> {
 | |
|         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 actual = await feature.getMetaTransactionHash(mtx).callAsync();
 | |
|             expect(actual).to.eq(expected);
 | |
|         });
 | |
|     });
 | |
| 
 | |
|     interface TransformERC20Args {
 | |
|         inputToken: string;
 | |
|         outputToken: string;
 | |
|         inputTokenAmount: BigNumber;
 | |
|         minOutputTokenAmount: BigNumber;
 | |
|         transformations: Array<{ deploymentNonce: BigNumber; data: string }>;
 | |
|     }
 | |
| 
 | |
|     function getRandomTransformERC20Args(fields: Partial<TransformERC20Args> = {}): TransformERC20Args {
 | |
|         return {
 | |
|             inputToken: randomAddress(),
 | |
|             outputToken: randomAddress(),
 | |
|             inputTokenAmount: getRandomInteger(1, '1e18'),
 | |
|             minOutputTokenAmount: getRandomInteger(1, '1e18'),
 | |
|             transformations: [{ deploymentNonce: new BigNumber(123), data: hexUtils.random() }],
 | |
|             ...fields,
 | |
|         };
 | |
|     }
 | |
| 
 | |
|     const RAW_TRANSFORM_SUCCESS_RESULT = hexUtils.leftPad(1337);
 | |
|     const RAW_ORDER_SUCCESS_RESULT = hexUtils.leftPad(1337, 64);
 | |
| 
 | |
|     describe('executeMetaTransaction()', () => {
 | |
|         it('can call NativeOrders.fillLimitOrder()', async () => {
 | |
|             const order = getRandomLimitOrder({ maker });
 | |
|             const fillAmount = new BigNumber(23456);
 | |
|             const sig = await order.getSignatureWithProviderAsync(env.provider);
 | |
|             const mtx = getRandomMetaTransaction({
 | |
|                 callData: nativeOrdersFeature.fillLimitOrder(order, sig, fillAmount).getABIEncodedTransactionData(),
 | |
|             });
 | |
|             const signature = await signMetaTransactionAsync(mtx);
 | |
|             const callOpts = {
 | |
|                 gasPrice: mtx.minGasPrice,
 | |
|                 value: mtx.value,
 | |
|             };
 | |
|             const rawResult = await feature.executeMetaTransaction(mtx, signature).callAsync(callOpts);
 | |
|             expect(rawResult).to.eq(RAW_ORDER_SUCCESS_RESULT);
 | |
|             const receipt = await feature.executeMetaTransaction(mtx, signature).awaitTransactionSuccessAsync(callOpts);
 | |
| 
 | |
|             verifyEventsFromLogs(
 | |
|                 receipt.logs,
 | |
|                 [
 | |
|                     {
 | |
|                         order: _.omit(order, ['verifyingContract', 'chainId']),
 | |
|                         sender: mtx.sender,
 | |
|                         taker: mtx.signer,
 | |
|                         takerTokenFillAmount: fillAmount,
 | |
|                         signatureType: sig.signatureType,
 | |
|                         v: sig.v,
 | |
|                         r: sig.r,
 | |
|                         s: sig.s,
 | |
|                     },
 | |
|                 ],
 | |
|                 TestMetaTransactionsNativeOrdersFeatureEvents.FillLimitOrderCalled,
 | |
|             );
 | |
|         });
 | |
| 
 | |
|         it('can call NativeOrders.fillRfqOrder()', async () => {
 | |
|             const order = getRandomRfqOrder({ maker });
 | |
|             const sig = await order.getSignatureWithProviderAsync(env.provider);
 | |
|             const fillAmount = new BigNumber(23456);
 | |
|             const mtx = getRandomMetaTransaction({
 | |
|                 callData: nativeOrdersFeature.fillRfqOrder(order, sig, fillAmount).getABIEncodedTransactionData(),
 | |
|             });
 | |
|             const signature = await signMetaTransactionAsync(mtx);
 | |
|             const callOpts = {
 | |
|                 gasPrice: mtx.minGasPrice,
 | |
|                 value: mtx.value,
 | |
|             };
 | |
|             const rawResult = await feature.executeMetaTransaction(mtx, signature).callAsync(callOpts);
 | |
|             expect(rawResult).to.eq(RAW_ORDER_SUCCESS_RESULT);
 | |
|             const receipt = await feature.executeMetaTransaction(mtx, signature).awaitTransactionSuccessAsync(callOpts);
 | |
| 
 | |
|             verifyEventsFromLogs(
 | |
|                 receipt.logs,
 | |
|                 [
 | |
|                     {
 | |
|                         order: _.omit(order, ['verifyingContract', 'chainId']),
 | |
|                         taker: mtx.signer,
 | |
|                         takerTokenFillAmount: fillAmount,
 | |
|                         signatureType: sig.signatureType,
 | |
|                         v: sig.v,
 | |
|                         r: sig.r,
 | |
|                         s: sig.s,
 | |
|                     },
 | |
|                 ],
 | |
|                 TestMetaTransactionsNativeOrdersFeatureEvents.FillRfqOrderCalled,
 | |
|             );
 | |
|         });
 | |
| 
 | |
|         it('can call `TransformERC20.transformERC20()`', async () => {
 | |
|             const args = getRandomTransformERC20Args();
 | |
|             const mtx = getRandomMetaTransaction({
 | |
|                 callData: transformERC20Feature
 | |
|                     .transformERC20(
 | |
|                         args.inputToken,
 | |
|                         args.outputToken,
 | |
|                         args.inputTokenAmount,
 | |
|                         args.minOutputTokenAmount,
 | |
|                         args.transformations,
 | |
|                     )
 | |
|                     .getABIEncodedTransactionData(),
 | |
|             });
 | |
|             const signature = await signMetaTransactionAsync(mtx);
 | |
|             const callOpts = {
 | |
|                 gasPrice: mtx.minGasPrice,
 | |
|                 value: mtx.value,
 | |
|             };
 | |
|             const rawResult = await feature.executeMetaTransaction(mtx, signature).callAsync(callOpts);
 | |
|             expect(rawResult).to.eq(RAW_TRANSFORM_SUCCESS_RESULT);
 | |
|             const receipt = await feature.executeMetaTransaction(mtx, signature).awaitTransactionSuccessAsync(callOpts);
 | |
|             verifyEventsFromLogs(
 | |
|                 receipt.logs,
 | |
|                 [
 | |
|                     {
 | |
|                         inputToken: args.inputToken,
 | |
|                         outputToken: args.outputToken,
 | |
|                         inputTokenAmount: args.inputTokenAmount,
 | |
|                         minOutputTokenAmount: args.minOutputTokenAmount,
 | |
|                         transformations: args.transformations,
 | |
|                         sender: zeroEx.address,
 | |
|                         value: mtx.value,
 | |
|                         callDataHash: hexUtils.hash(mtx.callData),
 | |
|                         taker: mtx.signer,
 | |
|                         callDataSignature: NULL_BYTES,
 | |
|                     },
 | |
|                 ],
 | |
|                 TestMetaTransactionsTransformERC20FeatureEvents.TransformERC20Called,
 | |
|             );
 | |
|         });
 | |
| 
 | |
|         it('can call `TransformERC20.transformERC20()` with signed calldata', async () => {
 | |
|             const args = getRandomTransformERC20Args();
 | |
|             const callData = transformERC20Feature
 | |
|                 .transformERC20(
 | |
|                     args.inputToken,
 | |
|                     args.outputToken,
 | |
|                     args.inputTokenAmount,
 | |
|                     args.minOutputTokenAmount,
 | |
|                     args.transformations,
 | |
|                 )
 | |
|                 .getABIEncodedTransactionData();
 | |
|             const callDataSignerKey = hexUtils.random();
 | |
|             const callDataSignature = generateCallDataSignature(callData, callDataSignerKey);
 | |
|             const signedCallData = signCallData(callData, callDataSignerKey);
 | |
|             const mtx = getRandomMetaTransaction({ callData: signedCallData });
 | |
|             const signature = await signMetaTransactionAsync(mtx);
 | |
|             const callOpts = {
 | |
|                 gasPrice: mtx.minGasPrice,
 | |
|                 value: mtx.value,
 | |
|             };
 | |
|             const rawResult = await feature.executeMetaTransaction(mtx, signature).callAsync(callOpts);
 | |
|             expect(rawResult).to.eq(RAW_TRANSFORM_SUCCESS_RESULT);
 | |
|             const receipt = await feature.executeMetaTransaction(mtx, signature).awaitTransactionSuccessAsync(callOpts);
 | |
|             verifyEventsFromLogs(
 | |
|                 receipt.logs,
 | |
|                 [
 | |
|                     {
 | |
|                         inputToken: args.inputToken,
 | |
|                         outputToken: args.outputToken,
 | |
|                         inputTokenAmount: args.inputTokenAmount,
 | |
|                         minOutputTokenAmount: args.minOutputTokenAmount,
 | |
|                         transformations: args.transformations,
 | |
|                         sender: zeroEx.address,
 | |
|                         value: mtx.value,
 | |
|                         callDataHash: hexUtils.hash(callData),
 | |
|                         taker: mtx.signer,
 | |
|                         callDataSignature,
 | |
|                     },
 | |
|                 ],
 | |
|                 TestMetaTransactionsTransformERC20FeatureEvents.TransformERC20Called,
 | |
|             );
 | |
|         });
 | |
| 
 | |
|         it('can call with any sender if `sender == 0`', async () => {
 | |
|             const args = getRandomTransformERC20Args();
 | |
|             const mtx = getRandomMetaTransaction({
 | |
|                 sender: NULL_ADDRESS,
 | |
|                 callData: transformERC20Feature
 | |
|                     .transformERC20(
 | |
|                         args.inputToken,
 | |
|                         args.outputToken,
 | |
|                         args.inputTokenAmount,
 | |
|                         args.minOutputTokenAmount,
 | |
|                         args.transformations,
 | |
|                     )
 | |
|                     .getABIEncodedTransactionData(),
 | |
|             });
 | |
|             const signature = await signMetaTransactionAsync(mtx);
 | |
|             const callOpts = {
 | |
|                 gasPrice: mtx.minGasPrice,
 | |
|                 value: mtx.value,
 | |
|                 from: randomAddress(),
 | |
|             };
 | |
|             const rawResult = await feature.executeMetaTransaction(mtx, signature).callAsync(callOpts);
 | |
|             expect(rawResult).to.eq(RAW_TRANSFORM_SUCCESS_RESULT);
 | |
|         });
 | |
| 
 | |
|         it('works without fee', async () => {
 | |
|             const args = getRandomTransformERC20Args();
 | |
|             const mtx = getRandomMetaTransaction({
 | |
|                 feeAmount: ZERO_AMOUNT,
 | |
|                 feeToken: randomAddress(),
 | |
|                 callData: transformERC20Feature
 | |
|                     .transformERC20(
 | |
|                         args.inputToken,
 | |
|                         args.outputToken,
 | |
|                         args.inputTokenAmount,
 | |
|                         args.minOutputTokenAmount,
 | |
|                         args.transformations,
 | |
|                     )
 | |
|                     .getABIEncodedTransactionData(),
 | |
|             });
 | |
|             const signature = await signMetaTransactionAsync(mtx);
 | |
|             const callOpts = {
 | |
|                 gasPrice: mtx.minGasPrice,
 | |
|                 value: mtx.value,
 | |
|             };
 | |
|             const rawResult = await feature.executeMetaTransaction(mtx, signature).callAsync(callOpts);
 | |
|             expect(rawResult).to.eq(RAW_TRANSFORM_SUCCESS_RESULT);
 | |
|         });
 | |
| 
 | |
|         it('fails if the translated call fails', async () => {
 | |
|             const args = getRandomTransformERC20Args();
 | |
|             const mtx = getRandomMetaTransaction({
 | |
|                 value: new BigNumber(TRANSFORM_ERC20_FAILING_VALUE),
 | |
|                 callData: transformERC20Feature
 | |
|                     .transformERC20(
 | |
|                         args.inputToken,
 | |
|                         args.outputToken,
 | |
|                         args.inputTokenAmount,
 | |
|                         args.minOutputTokenAmount,
 | |
|                         args.transformations,
 | |
|                     )
 | |
|                     .getABIEncodedTransactionData(),
 | |
|             });
 | |
|             const mtxHash = getExchangeProxyMetaTransactionHash(mtx);
 | |
|             const signature = await signMetaTransactionAsync(mtx);
 | |
|             const callOpts = {
 | |
|                 gasPrice: mtx.minGasPrice,
 | |
|                 value: mtx.value,
 | |
|             };
 | |
|             const tx = feature.executeMetaTransaction(mtx, signature).callAsync(callOpts);
 | |
|             const actualCallData = transformERC20Feature
 | |
|                 ._transformERC20({
 | |
|                     taker: mtx.signer,
 | |
|                     inputToken: args.inputToken,
 | |
|                     outputToken: args.outputToken,
 | |
|                     inputTokenAmount: args.inputTokenAmount,
 | |
|                     minOutputTokenAmount: args.minOutputTokenAmount,
 | |
|                     transformations: args.transformations,
 | |
|                     callDataHash: hexUtils.hash(mtx.callData),
 | |
|                     callDataSignature: NULL_BYTES,
 | |
|                 })
 | |
|                 .getABIEncodedTransactionData();
 | |
|             return expect(tx).to.revertWith(
 | |
|                 new ZeroExRevertErrors.MetaTransactions.MetaTransactionCallFailedError(
 | |
|                     mtxHash,
 | |
|                     actualCallData,
 | |
|                     new StringRevertError('FAIL').encode(),
 | |
|                 ),
 | |
|             );
 | |
|         });
 | |
| 
 | |
|         it('fails with unsupported function', async () => {
 | |
|             const mtx = getRandomMetaTransaction({
 | |
|                 callData: transformERC20Feature.createTransformWallet().getABIEncodedTransactionData(),
 | |
|             });
 | |
|             const mtxHash = getExchangeProxyMetaTransactionHash(mtx);
 | |
|             const signature = await signMetaTransactionAsync(mtx);
 | |
|             const callOpts = {
 | |
|                 gasPrice: mtx.minGasPrice,
 | |
|                 value: mtx.value,
 | |
|             };
 | |
|             const tx = feature.executeMetaTransaction(mtx, signature).awaitTransactionSuccessAsync(callOpts);
 | |
|             return expect(tx).to.revertWith(
 | |
|                 new ZeroExRevertErrors.MetaTransactions.MetaTransactionUnsupportedFunctionError(
 | |
|                     mtxHash,
 | |
|                     hexUtils.slice(mtx.callData, 0, 4),
 | |
|                 ),
 | |
|             );
 | |
|         });
 | |
| 
 | |
|         it('cannot execute the same mtx twice', async () => {
 | |
|             const args = getRandomTransformERC20Args();
 | |
|             const mtx = getRandomMetaTransaction({
 | |
|                 callData: transformERC20Feature
 | |
|                     .transformERC20(
 | |
|                         args.inputToken,
 | |
|                         args.outputToken,
 | |
|                         args.inputTokenAmount,
 | |
|                         args.minOutputTokenAmount,
 | |
|                         args.transformations,
 | |
|                     )
 | |
|                     .getABIEncodedTransactionData(),
 | |
|             });
 | |
|             const mtxHash = getExchangeProxyMetaTransactionHash(mtx);
 | |
|             const signature = await signMetaTransactionAsync(mtx);
 | |
|             const callOpts = {
 | |
|                 gasPrice: mtx.minGasPrice,
 | |
|                 value: mtx.value,
 | |
|             };
 | |
|             const receipt = await feature.executeMetaTransaction(mtx, signature).awaitTransactionSuccessAsync(callOpts);
 | |
|             const tx = feature.executeMetaTransaction(mtx, signature).awaitTransactionSuccessAsync(callOpts);
 | |
|             return expect(tx).to.revertWith(
 | |
|                 new ZeroExRevertErrors.MetaTransactions.MetaTransactionAlreadyExecutedError(
 | |
|                     mtxHash,
 | |
|                     receipt.blockNumber,
 | |
|                 ),
 | |
|             );
 | |
|         });
 | |
| 
 | |
|         it('fails if not enough ETH provided', async () => {
 | |
|             const mtx = getRandomMetaTransaction();
 | |
|             const mtxHash = getExchangeProxyMetaTransactionHash(mtx);
 | |
|             const signature = await signMetaTransactionAsync(mtx);
 | |
|             const callOpts = {
 | |
|                 gasPrice: mtx.minGasPrice,
 | |
|                 value: mtx.value.minus(1),
 | |
|             };
 | |
|             const tx = feature.executeMetaTransaction(mtx, signature).awaitTransactionSuccessAsync(callOpts);
 | |
|             return expect(tx).to.revertWith(
 | |
|                 new ZeroExRevertErrors.MetaTransactions.MetaTransactionInsufficientEthError(
 | |
|                     mtxHash,
 | |
|                     callOpts.value,
 | |
|                     mtx.value,
 | |
|                 ),
 | |
|             );
 | |
|         });
 | |
| 
 | |
|         it('fails if gas price too low', async () => {
 | |
|             const mtx = getRandomMetaTransaction();
 | |
|             const mtxHash = getExchangeProxyMetaTransactionHash(mtx);
 | |
|             const signature = await signMetaTransactionAsync(mtx);
 | |
|             const callOpts = {
 | |
|                 gasPrice: mtx.minGasPrice.minus(1),
 | |
|                 value: mtx.value,
 | |
|             };
 | |
|             const tx = feature.executeMetaTransaction(mtx, signature).awaitTransactionSuccessAsync(callOpts);
 | |
|             return expect(tx).to.revertWith(
 | |
|                 new ZeroExRevertErrors.MetaTransactions.MetaTransactionGasPriceError(
 | |
|                     mtxHash,
 | |
|                     callOpts.gasPrice,
 | |
|                     mtx.minGasPrice,
 | |
|                     mtx.maxGasPrice,
 | |
|                 ),
 | |
|             );
 | |
|         });
 | |
| 
 | |
|         it('fails if gas price too high', async () => {
 | |
|             const mtx = getRandomMetaTransaction();
 | |
|             const mtxHash = getExchangeProxyMetaTransactionHash(mtx);
 | |
|             const signature = await signMetaTransactionAsync(mtx);
 | |
|             const callOpts = {
 | |
|                 gasPrice: mtx.maxGasPrice.plus(1),
 | |
|                 value: mtx.value,
 | |
|             };
 | |
|             const tx = feature.executeMetaTransaction(mtx, signature).awaitTransactionSuccessAsync(callOpts);
 | |
|             return expect(tx).to.revertWith(
 | |
|                 new ZeroExRevertErrors.MetaTransactions.MetaTransactionGasPriceError(
 | |
|                     mtxHash,
 | |
|                     callOpts.gasPrice,
 | |
|                     mtx.minGasPrice,
 | |
|                     mtx.maxGasPrice,
 | |
|                 ),
 | |
|             );
 | |
|         });
 | |
| 
 | |
|         it('fails if expired', async () => {
 | |
|             const mtx = getRandomMetaTransaction({
 | |
|                 expirationTimeSeconds: new BigNumber(Math.floor(_.now() / 1000 - 60)),
 | |
|             });
 | |
|             const mtxHash = getExchangeProxyMetaTransactionHash(mtx);
 | |
|             const signature = await signMetaTransactionAsync(mtx);
 | |
|             const callOpts = {
 | |
|                 gasPrice: mtx.maxGasPrice,
 | |
|                 value: mtx.value,
 | |
|             };
 | |
|             const tx = feature.executeMetaTransaction(mtx, signature).awaitTransactionSuccessAsync(callOpts);
 | |
|             return expect(tx).to.revertWith(
 | |
|                 new ZeroExRevertErrors.MetaTransactions.MetaTransactionExpiredError(
 | |
|                     mtxHash,
 | |
|                     undefined,
 | |
|                     mtx.expirationTimeSeconds,
 | |
|                 ),
 | |
|             );
 | |
|         });
 | |
| 
 | |
|         it('fails if wrong sender', async () => {
 | |
|             const requiredSender = randomAddress();
 | |
|             const mtx = getRandomMetaTransaction({
 | |
|                 sender: requiredSender,
 | |
|             });
 | |
|             const mtxHash = getExchangeProxyMetaTransactionHash(mtx);
 | |
|             const signature = await signMetaTransactionAsync(mtx);
 | |
|             const callOpts = {
 | |
|                 gasPrice: mtx.maxGasPrice,
 | |
|                 value: mtx.value,
 | |
|             };
 | |
|             const tx = feature.executeMetaTransaction(mtx, signature).awaitTransactionSuccessAsync(callOpts);
 | |
|             return expect(tx).to.revertWith(
 | |
|                 new ZeroExRevertErrors.MetaTransactions.MetaTransactionWrongSenderError(
 | |
|                     mtxHash,
 | |
|                     sender,
 | |
|                     requiredSender,
 | |
|                 ),
 | |
|             );
 | |
|         });
 | |
| 
 | |
|         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 callOpts = {
 | |
|                 gasPrice: mtx.maxGasPrice,
 | |
|                 value: mtx.value,
 | |
|             };
 | |
|             const tx = feature.executeMetaTransaction(mtx, signature).awaitTransactionSuccessAsync(callOpts);
 | |
|             return expect(tx).to.revertWith(
 | |
|                 new ZeroExRevertErrors.SignatureValidator.SignatureValidationError(
 | |
|                     ZeroExRevertErrors.SignatureValidator.SignatureValidationErrorCodes.WrongSigner,
 | |
|                     mtxHash,
 | |
|                     signers[0],
 | |
|                     '0x',
 | |
|                 ),
 | |
|             );
 | |
|         });
 | |
| 
 | |
|         it('cannot reenter `executeMetaTransaction()`', async () => {
 | |
|             const args = getRandomTransformERC20Args();
 | |
|             const mtx = getRandomMetaTransaction({
 | |
|                 callData: transformERC20Feature
 | |
|                     .transformERC20(
 | |
|                         args.inputToken,
 | |
|                         args.outputToken,
 | |
|                         args.inputTokenAmount,
 | |
|                         args.minOutputTokenAmount,
 | |
|                         args.transformations,
 | |
|                     )
 | |
|                     .getABIEncodedTransactionData(),
 | |
|                 value: TRANSFORM_ERC20_REENTER_VALUE,
 | |
|             });
 | |
|             const mtxHash = getExchangeProxyMetaTransactionHash(mtx);
 | |
|             const signature = await signMetaTransactionAsync(mtx);
 | |
|             const callOpts = {
 | |
|                 gasPrice: mtx.maxGasPrice,
 | |
|                 value: mtx.value,
 | |
|             };
 | |
|             const tx = feature.executeMetaTransaction(mtx, signature).awaitTransactionSuccessAsync(callOpts);
 | |
|             return expect(tx).to.revertWith(
 | |
|                 new ZeroExRevertErrors.MetaTransactions.MetaTransactionCallFailedError(
 | |
|                     mtxHash,
 | |
|                     undefined,
 | |
|                     new ZeroExRevertErrors.Common.IllegalReentrancyError(
 | |
|                         feature.getSelector('executeMetaTransaction'),
 | |
|                         REENTRANCY_FLAG_MTX,
 | |
|                     ).encode(),
 | |
|                 ),
 | |
|             );
 | |
|         });
 | |
| 
 | |
|         it('cannot reenter `batchExecuteMetaTransactions()`', async () => {
 | |
|             const args = getRandomTransformERC20Args();
 | |
|             const mtx = getRandomMetaTransaction({
 | |
|                 callData: transformERC20Feature
 | |
|                     .transformERC20(
 | |
|                         args.inputToken,
 | |
|                         args.outputToken,
 | |
|                         args.inputTokenAmount,
 | |
|                         args.minOutputTokenAmount,
 | |
|                         args.transformations,
 | |
|                     )
 | |
|                     .getABIEncodedTransactionData(),
 | |
|                 value: TRANSFORM_ERC20_BATCH_REENTER_VALUE,
 | |
|             });
 | |
|             const mtxHash = getExchangeProxyMetaTransactionHash(mtx);
 | |
|             const signature = await signMetaTransactionAsync(mtx);
 | |
|             const callOpts = {
 | |
|                 gasPrice: mtx.maxGasPrice,
 | |
|                 value: mtx.value,
 | |
|             };
 | |
|             const tx = feature.executeMetaTransaction(mtx, signature).awaitTransactionSuccessAsync(callOpts);
 | |
|             return expect(tx).to.revertWith(
 | |
|                 new ZeroExRevertErrors.MetaTransactions.MetaTransactionCallFailedError(
 | |
|                     mtxHash,
 | |
|                     undefined,
 | |
|                     new ZeroExRevertErrors.Common.IllegalReentrancyError(
 | |
|                         feature.getSelector('batchExecuteMetaTransactions'),
 | |
|                         REENTRANCY_FLAG_MTX,
 | |
|                     ).encode(),
 | |
|                 ),
 | |
|             );
 | |
|         });
 | |
| 
 | |
|         it('cannot reenter `executeMetaTransaction()`', async () => {
 | |
|             const args = getRandomTransformERC20Args();
 | |
|             const mtx = getRandomMetaTransaction({
 | |
|                 callData: transformERC20Feature
 | |
|                     .transformERC20(
 | |
|                         args.inputToken,
 | |
|                         args.outputToken,
 | |
|                         args.inputTokenAmount,
 | |
|                         args.minOutputTokenAmount,
 | |
|                         args.transformations,
 | |
|                     )
 | |
|                     .getABIEncodedTransactionData(),
 | |
|                 value: TRANSFORM_ERC20_REENTER_VALUE,
 | |
|             });
 | |
|             const mtxHash = getExchangeProxyMetaTransactionHash(mtx);
 | |
|             const signature = await signMetaTransactionAsync(mtx);
 | |
|             const callOpts = {
 | |
|                 gasPrice: mtx.maxGasPrice,
 | |
|                 value: mtx.value,
 | |
|             };
 | |
|             const tx = feature.executeMetaTransaction(mtx, signature).awaitTransactionSuccessAsync(callOpts);
 | |
|             return expect(tx).to.revertWith(
 | |
|                 new ZeroExRevertErrors.MetaTransactions.MetaTransactionCallFailedError(
 | |
|                     mtxHash,
 | |
|                     undefined,
 | |
|                     new ZeroExRevertErrors.Common.IllegalReentrancyError(
 | |
|                         feature.getSelector('executeMetaTransaction'),
 | |
|                         REENTRANCY_FLAG_MTX,
 | |
|                     ).encode(),
 | |
|                 ),
 | |
|             );
 | |
|         });
 | |
| 
 | |
|         it('cannot reenter `batchExecuteMetaTransactions()`', async () => {
 | |
|             const args = getRandomTransformERC20Args();
 | |
|             const mtx = getRandomMetaTransaction({
 | |
|                 callData: transformERC20Feature
 | |
|                     .transformERC20(
 | |
|                         args.inputToken,
 | |
|                         args.outputToken,
 | |
|                         args.inputTokenAmount,
 | |
|                         args.minOutputTokenAmount,
 | |
|                         args.transformations,
 | |
|                     )
 | |
|                     .getABIEncodedTransactionData(),
 | |
|                 value: TRANSFORM_ERC20_BATCH_REENTER_VALUE,
 | |
|             });
 | |
|             const mtxHash = getExchangeProxyMetaTransactionHash(mtx);
 | |
|             const signature = await signMetaTransactionAsync(mtx);
 | |
|             const callOpts = {
 | |
|                 gasPrice: mtx.maxGasPrice,
 | |
|                 value: mtx.value,
 | |
|             };
 | |
|             const tx = feature.executeMetaTransaction(mtx, signature).awaitTransactionSuccessAsync(callOpts);
 | |
|             return expect(tx).to.revertWith(
 | |
|                 new ZeroExRevertErrors.MetaTransactions.MetaTransactionCallFailedError(
 | |
|                     mtxHash,
 | |
|                     undefined,
 | |
|                     new ZeroExRevertErrors.Common.IllegalReentrancyError(
 | |
|                         feature.getSelector('batchExecuteMetaTransactions'),
 | |
|                         REENTRANCY_FLAG_MTX,
 | |
|                     ).encode(),
 | |
|                 ),
 | |
|             );
 | |
|         });
 | |
|     });
 | |
| 
 | |
|     describe('batchExecuteMetaTransactions()', () => {
 | |
|         it('can execute multiple transactions', async () => {
 | |
|             const mtxs = _.times(2, i => {
 | |
|                 const args = getRandomTransformERC20Args();
 | |
|                 return getRandomMetaTransaction({
 | |
|                     signer: signers[i],
 | |
|                     callData: transformERC20Feature
 | |
|                         .transformERC20(
 | |
|                             args.inputToken,
 | |
|                             args.outputToken,
 | |
|                             args.inputTokenAmount,
 | |
|                             args.minOutputTokenAmount,
 | |
|                             args.transformations,
 | |
|                         )
 | |
|                         .getABIEncodedTransactionData(),
 | |
|                 });
 | |
|             });
 | |
|             const signatures = await Promise.all(mtxs.map(async mtx => signMetaTransactionAsync(mtx)));
 | |
|             const callOpts = {
 | |
|                 gasPrice: BigNumber.max(...mtxs.map(mtx => mtx.minGasPrice)),
 | |
|                 value: BigNumber.sum(...mtxs.map(mtx => mtx.value)),
 | |
|             };
 | |
|             const rawResults = await feature.batchExecuteMetaTransactions(mtxs, signatures).callAsync(callOpts);
 | |
|             expect(rawResults).to.eql(mtxs.map(() => RAW_TRANSFORM_SUCCESS_RESULT));
 | |
|         });
 | |
| 
 | |
|         it('cannot execute the same transaction twice', async () => {
 | |
|             const mtx = (() => {
 | |
|                 const args = getRandomTransformERC20Args();
 | |
|                 return getRandomMetaTransaction({
 | |
|                     signer: _.sampleSize(signers, 1)[0],
 | |
|                     callData: transformERC20Feature
 | |
|                         .transformERC20(
 | |
|                             args.inputToken,
 | |
|                             args.outputToken,
 | |
|                             args.inputTokenAmount,
 | |
|                             args.minOutputTokenAmount,
 | |
|                             args.transformations,
 | |
|                         )
 | |
|                         .getABIEncodedTransactionData(),
 | |
|                 });
 | |
|             })();
 | |
|             const mtxHash = getExchangeProxyMetaTransactionHash(mtx);
 | |
|             const mtxs = _.times(2, () => mtx);
 | |
|             const signatures = await Promise.all(mtxs.map(async m => signMetaTransactionAsync(m)));
 | |
|             const callOpts = {
 | |
|                 gasPrice: BigNumber.max(...mtxs.map(m => m.minGasPrice)),
 | |
|                 value: BigNumber.sum(...mtxs.map(m => m.value)),
 | |
|             };
 | |
|             const block = await env.web3Wrapper.getBlockNumberAsync();
 | |
|             const tx = feature.batchExecuteMetaTransactions(mtxs, signatures).callAsync(callOpts);
 | |
|             return expect(tx).to.revertWith(
 | |
|                 new ZeroExRevertErrors.MetaTransactions.MetaTransactionAlreadyExecutedError(mtxHash, block),
 | |
|             );
 | |
|         });
 | |
| 
 | |
|         it('fails if a meta-transaction fails', async () => {
 | |
|             const args = getRandomTransformERC20Args();
 | |
|             const mtx = getRandomMetaTransaction({
 | |
|                 value: new BigNumber(TRANSFORM_ERC20_FAILING_VALUE),
 | |
|                 callData: transformERC20Feature
 | |
|                     .transformERC20(
 | |
|                         args.inputToken,
 | |
|                         args.outputToken,
 | |
|                         args.inputTokenAmount,
 | |
|                         args.minOutputTokenAmount,
 | |
|                         args.transformations,
 | |
|                     )
 | |
|                     .getABIEncodedTransactionData(),
 | |
|             });
 | |
|             const mtxHash = getExchangeProxyMetaTransactionHash(mtx);
 | |
|             const signature = await signMetaTransactionAsync(mtx);
 | |
|             const callOpts = {
 | |
|                 gasPrice: mtx.minGasPrice,
 | |
|                 value: mtx.value,
 | |
|             };
 | |
|             const tx = feature.batchExecuteMetaTransactions([mtx], [signature]).callAsync(callOpts);
 | |
|             return expect(tx).to.revertWith(
 | |
|                 new ZeroExRevertErrors.MetaTransactions.MetaTransactionCallFailedError(
 | |
|                     mtxHash,
 | |
|                     undefined,
 | |
|                     new StringRevertError('FAIL').encode(),
 | |
|                 ),
 | |
|             );
 | |
|         });
 | |
| 
 | |
|         it('cannot reenter `executeMetaTransaction()`', async () => {
 | |
|             const args = getRandomTransformERC20Args();
 | |
|             const mtx = getRandomMetaTransaction({
 | |
|                 callData: transformERC20Feature
 | |
|                     .transformERC20(
 | |
|                         args.inputToken,
 | |
|                         args.outputToken,
 | |
|                         args.inputTokenAmount,
 | |
|                         args.minOutputTokenAmount,
 | |
|                         args.transformations,
 | |
|                     )
 | |
|                     .getABIEncodedTransactionData(),
 | |
|                 value: TRANSFORM_ERC20_REENTER_VALUE,
 | |
|             });
 | |
|             const mtxHash = getExchangeProxyMetaTransactionHash(mtx);
 | |
|             const signature = await signMetaTransactionAsync(mtx);
 | |
|             const callOpts = {
 | |
|                 gasPrice: mtx.maxGasPrice,
 | |
|                 value: mtx.value,
 | |
|             };
 | |
|             const tx = feature.batchExecuteMetaTransactions([mtx], [signature]).awaitTransactionSuccessAsync(callOpts);
 | |
|             return expect(tx).to.revertWith(
 | |
|                 new ZeroExRevertErrors.MetaTransactions.MetaTransactionCallFailedError(
 | |
|                     mtxHash,
 | |
|                     undefined,
 | |
|                     new ZeroExRevertErrors.Common.IllegalReentrancyError(
 | |
|                         feature.getSelector('executeMetaTransaction'),
 | |
|                         REENTRANCY_FLAG_MTX,
 | |
|                     ).encode(),
 | |
|                 ),
 | |
|             );
 | |
|         });
 | |
| 
 | |
|         it('cannot reenter `batchExecuteMetaTransactions()`', async () => {
 | |
|             const args = getRandomTransformERC20Args();
 | |
|             const mtx = getRandomMetaTransaction({
 | |
|                 callData: transformERC20Feature
 | |
|                     .transformERC20(
 | |
|                         args.inputToken,
 | |
|                         args.outputToken,
 | |
|                         args.inputTokenAmount,
 | |
|                         args.minOutputTokenAmount,
 | |
|                         args.transformations,
 | |
|                     )
 | |
|                     .getABIEncodedTransactionData(),
 | |
|                 value: TRANSFORM_ERC20_BATCH_REENTER_VALUE,
 | |
|             });
 | |
|             const mtxHash = getExchangeProxyMetaTransactionHash(mtx);
 | |
|             const signature = await signMetaTransactionAsync(mtx);
 | |
|             const callOpts = {
 | |
|                 gasPrice: mtx.maxGasPrice,
 | |
|                 value: mtx.value,
 | |
|             };
 | |
|             const tx = feature.batchExecuteMetaTransactions([mtx], [signature]).awaitTransactionSuccessAsync(callOpts);
 | |
|             return expect(tx).to.revertWith(
 | |
|                 new ZeroExRevertErrors.MetaTransactions.MetaTransactionCallFailedError(
 | |
|                     mtxHash,
 | |
|                     undefined,
 | |
|                     new ZeroExRevertErrors.Common.IllegalReentrancyError(
 | |
|                         feature.getSelector('batchExecuteMetaTransactions'),
 | |
|                         REENTRANCY_FLAG_MTX,
 | |
|                     ).encode(),
 | |
|                 ),
 | |
|             );
 | |
|         });
 | |
|     });
 | |
| 
 | |
|     describe('getMetaTransactionExecutedBlock()', () => {
 | |
|         it('returns zero for an unexecuted mtx', async () => {
 | |
|             const mtx = getRandomMetaTransaction();
 | |
|             const block = await feature.getMetaTransactionExecutedBlock(mtx).callAsync();
 | |
|             expect(block).to.bignumber.eq(0);
 | |
|         });
 | |
| 
 | |
|         it('returns the block it was executed in', async () => {
 | |
|             const args = getRandomTransformERC20Args();
 | |
|             const mtx = getRandomMetaTransaction({
 | |
|                 callData: transformERC20Feature
 | |
|                     .transformERC20(
 | |
|                         args.inputToken,
 | |
|                         args.outputToken,
 | |
|                         args.inputTokenAmount,
 | |
|                         args.minOutputTokenAmount,
 | |
|                         args.transformations,
 | |
|                     )
 | |
|                     .getABIEncodedTransactionData(),
 | |
|             });
 | |
|             const signature = await signMetaTransactionAsync(mtx);
 | |
|             const callOpts = {
 | |
|                 gasPrice: mtx.minGasPrice,
 | |
|                 value: mtx.value,
 | |
|             };
 | |
|             const receipt = await feature.executeMetaTransaction(mtx, signature).awaitTransactionSuccessAsync(callOpts);
 | |
|             const block = await feature.getMetaTransactionExecutedBlock(mtx).callAsync();
 | |
|             expect(block).to.bignumber.eq(receipt.blockNumber);
 | |
|         });
 | |
|     });
 | |
| 
 | |
|     describe('getMetaTransactionHashExecutedBlock()', () => {
 | |
|         it('returns zero for an unexecuted mtx', async () => {
 | |
|             const mtx = getRandomMetaTransaction();
 | |
|             const mtxHash = getExchangeProxyMetaTransactionHash(mtx);
 | |
|             const block = await feature.getMetaTransactionHashExecutedBlock(mtxHash).callAsync();
 | |
|             expect(block).to.bignumber.eq(0);
 | |
|         });
 | |
| 
 | |
|         it('returns the block it was executed in', async () => {
 | |
|             const args = getRandomTransformERC20Args();
 | |
|             const mtx = getRandomMetaTransaction({
 | |
|                 callData: transformERC20Feature
 | |
|                     .transformERC20(
 | |
|                         args.inputToken,
 | |
|                         args.outputToken,
 | |
|                         args.inputTokenAmount,
 | |
|                         args.minOutputTokenAmount,
 | |
|                         args.transformations,
 | |
|                     )
 | |
|                     .getABIEncodedTransactionData(),
 | |
|             });
 | |
|             const signature = await signMetaTransactionAsync(mtx);
 | |
|             const callOpts = {
 | |
|                 gasPrice: mtx.minGasPrice,
 | |
|                 value: mtx.value,
 | |
|             };
 | |
|             const receipt = await feature.executeMetaTransaction(mtx, signature).awaitTransactionSuccessAsync(callOpts);
 | |
|             const mtxHash = getExchangeProxyMetaTransactionHash(mtx);
 | |
|             const block = await feature.getMetaTransactionHashExecutedBlock(mtxHash).callAsync();
 | |
|             expect(block).to.bignumber.eq(receipt.blockNumber);
 | |
|         });
 | |
|     });
 | |
| });
 |