@0x/contracts-zero-ex: Add reentrancy guard to mtx functions

`@0x/contracts-zero-ex`: Add refund mechanism to mtxs
`@0x/contracts-zero-ex`: Pass sender to transfomers.
`@0x/contracts-zero-ex`: Refund protocol fees to `refundReceiver` in FQT.
`@0x/utils`: Add EP flavor of `IllegalReentrancyError`
`@0x/order-utils`: Add `refundReceiver` to FQT transform data.
`@0x/asset-swapper`: Add `refundReceiver` support to EP swap quote consumer.
This commit is contained in:
Lawrence Forman
2020-08-05 13:33:07 -04:00
committed by Lawrence Forman
parent 7b0a1c3630
commit 9cda9f69cd
35 changed files with 747 additions and 129 deletions

View File

@@ -36,6 +36,10 @@ blockchainTests.resets('MetaTransactions feature', env => {
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();
@@ -263,7 +267,7 @@ blockchainTests.resets('MetaTransactions feature', env => {
it('fails if the translated call fails', async () => {
const args = getRandomTransformERC20Args();
const mtx = getRandomMetaTransaction({
value: new BigNumber(666),
value: new BigNumber(TRANSFORM_ERC20_FAILING_VALUE),
callData: transformERC20Feature
.transformERC20(
args.inputToken,
@@ -469,6 +473,72 @@ blockchainTests.resets('MetaTransactions feature', env => {
),
);
});
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()', () => {
@@ -526,6 +596,102 @@ blockchainTests.resets('MetaTransactions feature', env => {
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()', () => {

View File

@@ -37,6 +37,7 @@ blockchainTests.resets('TransformERC20 feature', env => {
const callDataSigner = ethjs.bufferToHex(ethjs.privateToAddress(ethjs.toBuffer(callDataSignerKey)));
let owner: string;
let taker: string;
let sender: string;
let transformerDeployer: string;
let zeroEx: ZeroExContract;
let feature: TransformERC20Contract;
@@ -44,7 +45,7 @@ blockchainTests.resets('TransformERC20 feature', env => {
let allowanceTarget: string;
before(async () => {
[owner, taker, transformerDeployer] = await env.getAccountAddressesAsync();
[owner, taker, sender, transformerDeployer] = await env.getAccountAddressesAsync();
zeroEx = await fullMigrateAsync(
owner,
env.provider,
@@ -59,12 +60,12 @@ blockchainTests.resets('TransformERC20 feature', env => {
},
{ transformerDeployer },
);
feature = new TransformERC20Contract(zeroEx.address, env.provider, env.txDefaults, abis);
feature = new TransformERC20Contract(zeroEx.address, env.provider, { ...env.txDefaults, from: sender }, abis);
wallet = new FlashWalletContract(await feature.getTransformWallet().callAsync(), env.provider, env.txDefaults);
allowanceTarget = await new ITokenSpenderContract(zeroEx.address, env.provider, env.txDefaults)
.getAllowanceTarget()
.callAsync();
await feature.setQuoteSigner(callDataSigner).awaitTransactionSuccessAsync();
await feature.setQuoteSigner(callDataSigner).awaitTransactionSuccessAsync({ from: owner });
});
const { MAX_UINT256, ZERO_AMOUNT } = constants;
@@ -73,7 +74,7 @@ blockchainTests.resets('TransformERC20 feature', env => {
it('createTransformWallet() replaces the current wallet', async () => {
const newWalletAddress = await feature.createTransformWallet().callAsync({ from: owner });
expect(newWalletAddress).to.not.eq(wallet.address);
await feature.createTransformWallet().awaitTransactionSuccessAsync();
await feature.createTransformWallet().awaitTransactionSuccessAsync({ from: owner });
return expect(feature.getTransformWallet().callAsync()).to.eventually.eq(newWalletAddress);
});
@@ -264,8 +265,9 @@ blockchainTests.resets('TransformERC20 feature', env => {
receipt.logs,
[
{
callDataHash: NULL_BYTES32,
sender,
taker,
callDataHash: NULL_BYTES32,
context: wallet.address,
caller: zeroEx.address,
data: transformation.data,
@@ -320,8 +322,9 @@ blockchainTests.resets('TransformERC20 feature', env => {
receipt.logs,
[
{
callDataHash: NULL_BYTES32,
taker,
sender,
callDataHash: NULL_BYTES32,
context: wallet.address,
caller: zeroEx.address,
data: transformation.data,
@@ -379,8 +382,9 @@ blockchainTests.resets('TransformERC20 feature', env => {
receipt.logs,
[
{
callDataHash: NULL_BYTES32,
sender,
taker,
callDataHash: NULL_BYTES32,
context: wallet.address,
caller: zeroEx.address,
data: transformation.data,
@@ -496,8 +500,9 @@ blockchainTests.resets('TransformERC20 feature', env => {
receipt.logs,
[
{
callDataHash: NULL_BYTES32,
sender,
taker,
callDataHash: NULL_BYTES32,
context: wallet.address,
caller: zeroEx.address,
data: transformations[0].data,
@@ -505,8 +510,9 @@ blockchainTests.resets('TransformERC20 feature', env => {
ethBalance: callValue,
},
{
callDataHash: NULL_BYTES32,
sender,
taker,
callDataHash: NULL_BYTES32,
context: wallet.address,
caller: zeroEx.address,
data: transformations[1].data,