827 lines
36 KiB
TypeScript
827 lines
36 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 { 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> = {},
|
|
): 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<string> {
|
|
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> = {}): 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').toString(),
|
|
),
|
|
);
|
|
});
|
|
|
|
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,
|
|
).toString(),
|
|
),
|
|
);
|
|
});
|
|
|
|
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);
|
|
});
|
|
});
|
|
});
|