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 { 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 { ITokenSpenderFeatureContract, TestMetaTransactionsTransformERC20FeatureContract, TestMetaTransactionsTransformERC20FeatureEvents, TestMintableERC20TokenContract, } from '../wrappers'; const { NULL_ADDRESS, NULL_BYTES, ZERO_AMOUNT } = constants; blockchainTests.resets('MetaTransactions feature', env => { let owner: string; let sender: string; let signers: string[]; let zeroEx: IZeroExContract; let feature: MetaTransactionsFeatureContract; let feeToken: TestMintableERC20TokenContract; let transformERC20Feature: TestMetaTransactionsTransformERC20FeatureContract; let allowanceTarget: string; 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, sender, ...signers] = await env.getAccountAddressesAsync(); transformERC20Feature = await TestMetaTransactionsTransformERC20FeatureContract.deployFrom0xArtifactAsync( artifacts.TestMetaTransactionsTransformERC20Feature, env.provider, env.txDefaults, {}, ); zeroEx = await fullMigrateAsync(owner, env.provider, env.txDefaults, { transformERC20: transformERC20Feature.address, }); feature = new MetaTransactionsFeatureContract(zeroEx.address, env.provider, { ...env.txDefaults, from: sender }, abis); feeToken = await TestMintableERC20TokenContract.deployFrom0xArtifactAsync( artifacts.TestMintableERC20Token, env.provider, env.txDefaults, {}, ); allowanceTarget = await new ITokenSpenderFeatureContract(zeroEx.address, env.provider, env.txDefaults) .getAllowanceTarget() .callAsync(); // Fund signers with fee tokens. await Promise.all( signers.map(async signer => { await feeToken.mint(signer, MAX_FEE_AMOUNT).awaitTransactionSuccessAsync(); await feeToken.approve(allowanceTarget, MAX_FEE_AMOUNT).awaitTransactionSuccessAsync({ from: signer }); }), ); }); function getRandomMetaTransaction( fields: Partial = {}, ): 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 { return 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 { return { inputToken: randomAddress(), outputToken: randomAddress(), inputTokenAmount: getRandomInteger(1, '1e18'), minOutputTokenAmount: getRandomInteger(1, '1e18'), transformations: [{ deploymentNonce: new BigNumber(123), data: hexUtils.random() }], ...fields, }; } const RAW_SUCCESS_RESULT = hexUtils.leftPad(1337); describe('executeMetaTransaction()', () => { 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_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_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_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_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.MetaTransactions.MetaTransactionInvalidSignatureError( mtxHash, signature, new ZeroExRevertErrors.SignatureValidator.SignatureValidationError( ZeroExRevertErrors.SignatureValidator.SignatureValidationErrorCodes.WrongSigner, mtxHash, signers[0], signature, ).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(), ), ); }); 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_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); }); }); });