* move orderParsingUtils from order-utils to connect * Remove many functions from signatureUtils Removed from the exported object, that is. All of them are used in other existing code, so they were all moved to be as local to their usage as possible. * remove orderHashUtils.isValidOrderHash() * Move all *RevertErrors from order-utils... ...into their respective @0x/contracts- packages. * Refactor @0x/order-utils' orderHashUtils away - Move existing routines into @0x/contracts-test-utils - Migrate non-contract-test callers to a newly-exposed getOrderHash() method in DevUtils. * Move all *RevertErrors from @0x/utils... ...into their respective @0x/contracts- packages. * rm transactionHashUtils.isValidTransactionHash() * DevUtils.sol: Fail yarn test if too big to deploy * Refactor @0x/order-utils transactionHashUtils away - Move existing routines into @0x/contracts-test-utils - Migrate non-contract-test callers to a newly-exposed getTransactionHash() method in DevUtils. * Consolidate `Removed export...` CHANGELOG entries * Rm EthBalanceChecker from devutils wrapper exports * Stop importing from '.' or '.../src' * fix builds * fix prettier; dangling promise * increase max bundle size
713 lines
38 KiB
TypeScript
713 lines
38 KiB
TypeScript
import {
|
|
blockchainTests,
|
|
constants,
|
|
describe,
|
|
expect,
|
|
hexRandom,
|
|
transactionHashUtils,
|
|
} from '@0x/contracts-test-utils';
|
|
import { EIP712DomainWithDefaultSchema, ZeroExTransaction } from '@0x/types';
|
|
import { BigNumber, StringRevertError } from '@0x/utils';
|
|
import { LogWithDecodedArgs } from 'ethereum-types';
|
|
import * as _ from 'lodash';
|
|
|
|
import ExchangeRevertErrors = require('../src/revert_errors');
|
|
|
|
import { artifacts } from './artifacts';
|
|
import { TestTransactionsContract, TestTransactionsTransactionExecutionEventArgs } from './wrappers';
|
|
|
|
blockchainTests.resets('Transaction Unit Tests', ({ provider, web3Wrapper, txDefaults }) => {
|
|
let transactionsContract: TestTransactionsContract;
|
|
let accounts: string[];
|
|
let domain: EIP712DomainWithDefaultSchema;
|
|
|
|
const randomSignature = () => hexRandom(66);
|
|
|
|
const EMPTY_ZERO_EX_TRANSACTION = {
|
|
salt: constants.ZERO_AMOUNT,
|
|
expirationTimeSeconds: constants.ZERO_AMOUNT,
|
|
gasPrice: constants.ZERO_AMOUNT,
|
|
signerAddress: constants.NULL_ADDRESS,
|
|
data: constants.NULL_BYTES,
|
|
domain: {
|
|
verifyingContract: constants.NULL_ADDRESS,
|
|
chainId: 0,
|
|
},
|
|
};
|
|
|
|
const DEADBEEF_RETURN_DATA = '0xdeadbeef';
|
|
const INVALID_SIGNATURE = '0x0000';
|
|
|
|
before(async () => {
|
|
// A list of available addresses to use during testing.
|
|
accounts = await web3Wrapper.getAvailableAddressesAsync();
|
|
|
|
// Deploy the transaction test contract.
|
|
transactionsContract = await TestTransactionsContract.deployFrom0xArtifactAsync(
|
|
artifacts.TestTransactions,
|
|
provider,
|
|
txDefaults,
|
|
{},
|
|
);
|
|
|
|
// Set the default domain.
|
|
domain = {
|
|
verifyingContract: transactionsContract.address,
|
|
chainId: 1337,
|
|
};
|
|
});
|
|
|
|
/**
|
|
* Generates calldata for a call to `executable()` in the `TestTransactions` contract.
|
|
*/
|
|
function getExecutableCallData(shouldSucceed: boolean, callData: string, returnData: string): string {
|
|
return (transactionsContract as any)
|
|
.executable(shouldSucceed, callData, returnData)
|
|
.getABIEncodedTransactionData();
|
|
}
|
|
|
|
interface GenerateZeroExTransactionParams {
|
|
salt?: BigNumber;
|
|
expirationTimeSeconds?: BigNumber;
|
|
gasPrice?: BigNumber;
|
|
signerAddress?: string;
|
|
data?: string;
|
|
domain?: EIP712DomainWithDefaultSchema;
|
|
shouldSucceed?: boolean;
|
|
callData?: string;
|
|
returnData?: string;
|
|
}
|
|
|
|
async function generateZeroExTransactionAsync(
|
|
opts: GenerateZeroExTransactionParams = {},
|
|
): Promise<ZeroExTransaction> {
|
|
const shouldSucceed = opts.shouldSucceed === undefined ? true : opts.shouldSucceed;
|
|
const callData = opts.callData === undefined ? constants.NULL_BYTES : opts.callData;
|
|
const returnData = opts.returnData === undefined ? constants.NULL_BYTES : opts.returnData;
|
|
const data = opts.data === undefined ? getExecutableCallData(shouldSucceed, callData, returnData) : opts.data;
|
|
const gasPrice = opts.gasPrice === undefined ? new BigNumber(constants.DEFAULT_GAS_PRICE) : opts.gasPrice;
|
|
const _domain = opts.domain === undefined ? domain : opts.domain;
|
|
const expirationTimeSeconds =
|
|
opts.expirationTimeSeconds === undefined ? constants.MAX_UINT256 : opts.expirationTimeSeconds;
|
|
const transaction = {
|
|
...EMPTY_ZERO_EX_TRANSACTION,
|
|
...opts,
|
|
data,
|
|
expirationTimeSeconds,
|
|
domain: _domain,
|
|
gasPrice,
|
|
};
|
|
return transaction;
|
|
}
|
|
|
|
describe('batchExecuteTransaction', () => {
|
|
it('should revert if the only call to executeTransaction fails', async () => {
|
|
// Create an expired transaction that will fail when used to call `batchExecuteTransactions()`.
|
|
const transaction = await generateZeroExTransactionAsync({ shouldSucceed: false });
|
|
const transactionHash = transactionHashUtils.getTransactionHashHex(transaction);
|
|
|
|
// Create the StringRevertError that reflects the returndata that will be returned by the failed transaction.
|
|
const executableError = new StringRevertError('EXECUTABLE_FAILED');
|
|
const expectedError = new ExchangeRevertErrors.TransactionExecutionError(
|
|
transactionHash,
|
|
executableError.encode(),
|
|
);
|
|
|
|
// Call the `batchExecuteTransactions()` function and ensure that it reverts with the expected revert error.
|
|
const tx = transactionsContract
|
|
.batchExecuteTransactions([transaction], [randomSignature()])
|
|
.awaitTransactionSuccessAsync();
|
|
return expect(tx).to.revertWith(expectedError);
|
|
});
|
|
|
|
it('should revert if the second call to executeTransaction fails', async () => {
|
|
// Create a transaction that will succeed when used to call `batchExecuteTransactions()`.
|
|
const transaction1 = await generateZeroExTransactionAsync();
|
|
|
|
// Create a transaction that will fail when used to call `batchExecuteTransactions()` because the call to executable will fail.
|
|
const transaction2 = await generateZeroExTransactionAsync({ shouldSucceed: false });
|
|
const transactionHash = transactionHashUtils.getTransactionHashHex(transaction2);
|
|
|
|
// Create the StringRevertError that reflects the returndata that will be returned by the failed transaction.
|
|
const executableError = new StringRevertError('EXECUTABLE_FAILED');
|
|
const expectedError = new ExchangeRevertErrors.TransactionExecutionError(
|
|
transactionHash,
|
|
executableError.encode(),
|
|
);
|
|
|
|
// Call the `batchExecuteTransactions()` function and ensure that it reverts with the expected revert error.
|
|
const tx = transactionsContract
|
|
.batchExecuteTransactions([transaction1, transaction2], [randomSignature(), randomSignature()])
|
|
.awaitTransactionSuccessAsync();
|
|
return expect(tx).to.revertWith(expectedError);
|
|
});
|
|
|
|
it('should revert if the first call to executeTransaction fails', async () => {
|
|
// Create a transaction that will fail when used to call `batchExecuteTransactions()` because the call to executable will fail.
|
|
const transaction1 = await generateZeroExTransactionAsync({ shouldSucceed: false });
|
|
|
|
// Create a transaction that will succeed when used to call `batchExecuteTransactions()`.
|
|
const transaction2 = await generateZeroExTransactionAsync();
|
|
const transactionHash = transactionHashUtils.getTransactionHashHex(transaction1);
|
|
|
|
// Create the StringRevertError that reflects the returndata that will be returned by the failed transaction.
|
|
const executableError = new StringRevertError('EXECUTABLE_FAILED');
|
|
const expectedError = new ExchangeRevertErrors.TransactionExecutionError(
|
|
transactionHash,
|
|
executableError.encode(),
|
|
);
|
|
|
|
// Call the `batchExecuteTransactions()` function and ensure that it reverts with the expected revert error.
|
|
const tx = transactionsContract
|
|
.batchExecuteTransactions([transaction1, transaction2], [randomSignature(), randomSignature()])
|
|
.awaitTransactionSuccessAsync();
|
|
return expect(tx).to.revertWith(expectedError);
|
|
});
|
|
|
|
it('should revert if the same transaction is executed twice in a batch', async () => {
|
|
// Create a transaction that will succeed when used to call `batchExecuteTransactions()`.
|
|
const transaction1 = await generateZeroExTransactionAsync({ signerAddress: accounts[1] });
|
|
|
|
// Duplicate the first transaction. This should cause the call to `batchExecuteTransactions()` to fail
|
|
// because this transaction will have the same order hash as transaction1.
|
|
const transaction2 = transaction1;
|
|
const transactionHash2 = transactionHashUtils.getTransactionHashHex(transaction2);
|
|
|
|
// Call the `batchExecuteTransactions()` function and ensure that it reverts with the expected revert error.
|
|
const expectedError = new ExchangeRevertErrors.TransactionError(
|
|
ExchangeRevertErrors.TransactionErrorCode.AlreadyExecuted,
|
|
transactionHash2,
|
|
);
|
|
const tx = transactionsContract
|
|
.batchExecuteTransactions([transaction1, transaction2], [randomSignature(), randomSignature()])
|
|
.awaitTransactionSuccessAsync({
|
|
from: accounts[0],
|
|
});
|
|
return expect(tx).to.revertWith(expectedError);
|
|
});
|
|
|
|
it('should succeed if the only call to executeTransaction succeeds', async () => {
|
|
// Create a transaction that will succeed when used to call `batchExecuteTransactions()`.
|
|
const transaction = await generateZeroExTransactionAsync({
|
|
signerAddress: accounts[1],
|
|
returnData: DEADBEEF_RETURN_DATA,
|
|
});
|
|
const transactionHash = transactionHashUtils.getTransactionHashHex(transaction);
|
|
const validSignature = randomSignature();
|
|
|
|
const contractFn = transactionsContract.batchExecuteTransactions([transaction], [validSignature]);
|
|
const result = await contractFn.callAsync({ from: accounts[0] });
|
|
const receipt = await contractFn.awaitTransactionSuccessAsync({ from: accounts[0] });
|
|
|
|
expect(result.length).to.be.eq(1);
|
|
const returnData = transactionsContract.getABIDecodedReturnData<void>('executeTransaction', result[0]);
|
|
expect(returnData).to.equal(DEADBEEF_RETURN_DATA);
|
|
|
|
// Ensure that the correct number of events were logged.
|
|
const logs = receipt.logs as Array<LogWithDecodedArgs<TestTransactionsTransactionExecutionEventArgs>>;
|
|
expect(logs.length).to.be.eq(2);
|
|
|
|
// Ensure that the correct events were logged.
|
|
expect(logs[0].event).to.be.eq('ExecutableCalled');
|
|
expect(logs[0].args.data).to.be.eq(constants.NULL_BYTES);
|
|
expect(logs[0].args.contextAddress).to.be.eq(accounts[1]);
|
|
expect(logs[0].args.returnData).to.be.eq(DEADBEEF_RETURN_DATA);
|
|
expect(logs[1].event).to.be.eq('TransactionExecution');
|
|
expect(logs[1].args.transactionHash).to.eq(transactionHash);
|
|
});
|
|
|
|
it('should succeed if the both calls to executeTransaction succeed', async () => {
|
|
// Create two transactions that will succeed when used to call `batchExecuteTransactions()`.
|
|
const transaction1 = await generateZeroExTransactionAsync({
|
|
signerAddress: accounts[0],
|
|
returnData: DEADBEEF_RETURN_DATA,
|
|
});
|
|
const returnData2 = '0xbeefdead';
|
|
const transaction2 = await generateZeroExTransactionAsync({
|
|
signerAddress: accounts[1],
|
|
returnData: returnData2,
|
|
});
|
|
const transactionHash1 = transactionHashUtils.getTransactionHashHex(transaction1);
|
|
const transactionHash2 = transactionHashUtils.getTransactionHashHex(transaction2);
|
|
|
|
const contractFn = transactionsContract.batchExecuteTransactions(
|
|
[transaction1, transaction2],
|
|
[randomSignature(), randomSignature()],
|
|
);
|
|
const result = await contractFn.callAsync({ from: accounts[0] });
|
|
|
|
const receipt = await contractFn.awaitTransactionSuccessAsync({ from: accounts[0] });
|
|
expect(result.length).to.be.eq(2);
|
|
expect(transactionsContract.getABIDecodedReturnData('executeTransaction', result[0])).to.equal(
|
|
DEADBEEF_RETURN_DATA,
|
|
);
|
|
expect(transactionsContract.getABIDecodedReturnData('executeTransaction', result[1])).to.equal(returnData2);
|
|
|
|
// Verify that the correct number of events were logged.
|
|
const logs = receipt.logs as Array<LogWithDecodedArgs<TestTransactionsTransactionExecutionEventArgs>>;
|
|
expect(logs.length).to.be.eq(4);
|
|
|
|
// Ensure that the correct events were logged.
|
|
expect(logs[0].event).to.be.eq('ExecutableCalled');
|
|
expect(logs[0].args.data).to.be.eq(constants.NULL_BYTES);
|
|
expect(logs[0].args.returnData).to.be.eq(DEADBEEF_RETURN_DATA);
|
|
expect(logs[0].args.contextAddress).to.be.eq(constants.NULL_ADDRESS);
|
|
expect(logs[1].event).to.be.eq('TransactionExecution');
|
|
expect(logs[1].args.transactionHash).to.eq(transactionHash1);
|
|
expect(logs[2].event).to.be.eq('ExecutableCalled');
|
|
expect(logs[2].args.data).to.be.eq(constants.NULL_BYTES);
|
|
expect(logs[2].args.returnData).to.be.eq('0xbeefdead');
|
|
expect(logs[2].args.contextAddress).to.be.eq(accounts[1]);
|
|
expect(logs[3].event).to.be.eq('TransactionExecution');
|
|
expect(logs[3].args.transactionHash).to.eq(transactionHash2);
|
|
});
|
|
it('should not allow recursion if currentContextAddress is already set', async () => {
|
|
const innerTransaction1 = await generateZeroExTransactionAsync({ signerAddress: accounts[0] });
|
|
const innerTransaction2 = await generateZeroExTransactionAsync({ signerAddress: accounts[1] });
|
|
const innerBatchExecuteTransaction = await generateZeroExTransactionAsync({
|
|
signerAddress: accounts[2],
|
|
callData: transactionsContract
|
|
.batchExecuteTransactions(
|
|
[innerTransaction1, innerTransaction2],
|
|
[randomSignature(), randomSignature()],
|
|
)
|
|
.getABIEncodedTransactionData(),
|
|
});
|
|
const outerExecuteTransaction = await generateZeroExTransactionAsync({
|
|
signerAddress: accounts[1],
|
|
callData: transactionsContract
|
|
.executeTransaction(innerBatchExecuteTransaction, randomSignature())
|
|
.getABIEncodedTransactionData(),
|
|
});
|
|
const innerBatchExecuteTransactionHash = transactionHashUtils.getTransactionHashHex(
|
|
innerBatchExecuteTransaction,
|
|
);
|
|
const innerExpectedError = new ExchangeRevertErrors.TransactionInvalidContextError(
|
|
innerBatchExecuteTransactionHash,
|
|
accounts[1],
|
|
);
|
|
const outerExecuteTransactionHash = transactionHashUtils.getTransactionHashHex(outerExecuteTransaction);
|
|
const outerExpectedError = new ExchangeRevertErrors.TransactionExecutionError(
|
|
outerExecuteTransactionHash,
|
|
innerExpectedError.encode(),
|
|
);
|
|
const tx = transactionsContract
|
|
.batchExecuteTransactions([outerExecuteTransaction], [randomSignature()])
|
|
.awaitTransactionSuccessAsync({ from: accounts[2] });
|
|
return expect(tx).to.revertWith(outerExpectedError);
|
|
});
|
|
it('should allow recursion as long as currentContextAddress is not set', async () => {
|
|
const innerTransaction1 = await generateZeroExTransactionAsync({ signerAddress: accounts[0] });
|
|
const innerTransaction2 = await generateZeroExTransactionAsync({ signerAddress: accounts[1] });
|
|
// From this point on, all transactions and calls will have the same sender, which does not change currentContextAddress when called
|
|
const innerBatchExecuteTransaction = await generateZeroExTransactionAsync({
|
|
signerAddress: accounts[2],
|
|
callData: transactionsContract
|
|
.batchExecuteTransactions(
|
|
[innerTransaction1, innerTransaction2],
|
|
[randomSignature(), randomSignature()],
|
|
)
|
|
.getABIEncodedTransactionData(),
|
|
});
|
|
const outerExecuteTransaction = await generateZeroExTransactionAsync({
|
|
signerAddress: accounts[2],
|
|
callData: transactionsContract
|
|
.executeTransaction(innerBatchExecuteTransaction, randomSignature())
|
|
.getABIEncodedTransactionData(),
|
|
});
|
|
return expect(
|
|
transactionsContract
|
|
.batchExecuteTransactions([outerExecuteTransaction], [randomSignature()])
|
|
.awaitTransactionSuccessAsync({ from: accounts[2] }),
|
|
).to.be.fulfilled('');
|
|
});
|
|
});
|
|
|
|
describe('executeTransaction', () => {
|
|
function getExecuteTransactionCallData(transaction: ZeroExTransaction, signature: string): string {
|
|
return (transactionsContract as any)
|
|
.executeTransaction(transaction, signature)
|
|
.getABIEncodedTransactionData();
|
|
}
|
|
it('should revert if the current time is past the expiration time', async () => {
|
|
const transaction = await generateZeroExTransactionAsync({
|
|
expirationTimeSeconds: constants.ZERO_AMOUNT,
|
|
});
|
|
const transactionHash = transactionHashUtils.getTransactionHashHex(transaction);
|
|
const expectedError = new ExchangeRevertErrors.TransactionError(
|
|
ExchangeRevertErrors.TransactionErrorCode.Expired,
|
|
transactionHash,
|
|
);
|
|
const tx = transactionsContract
|
|
.executeTransaction(transaction, randomSignature())
|
|
.awaitTransactionSuccessAsync();
|
|
return expect(tx).to.revertWith(expectedError);
|
|
});
|
|
it('should revert if the transaction is submitted with a gasPrice that does not equal the required gasPrice', async () => {
|
|
const transaction = await generateZeroExTransactionAsync();
|
|
const transactionHash = transactionHashUtils.getTransactionHashHex(transaction);
|
|
const actualGasPrice = transaction.gasPrice.plus(1);
|
|
const expectedError = new ExchangeRevertErrors.TransactionGasPriceError(
|
|
transactionHash,
|
|
actualGasPrice,
|
|
transaction.gasPrice,
|
|
);
|
|
const tx = transactionsContract
|
|
.executeTransaction(transaction, randomSignature())
|
|
.awaitTransactionSuccessAsync({
|
|
gasPrice: actualGasPrice,
|
|
});
|
|
return expect(tx).to.revertWith(expectedError);
|
|
});
|
|
it('should revert if reentrancy occurs in the middle of an executeTransaction call and msg.sender != signer for both calls', async () => {
|
|
const validSignature = randomSignature();
|
|
const innerTransaction = await generateZeroExTransactionAsync({ signerAddress: accounts[0] });
|
|
const innerTransactionHash = transactionHashUtils.getTransactionHashHex(innerTransaction);
|
|
const outerTransaction = await generateZeroExTransactionAsync({
|
|
signerAddress: accounts[0],
|
|
callData: getExecuteTransactionCallData(innerTransaction, validSignature),
|
|
returnData: DEADBEEF_RETURN_DATA,
|
|
});
|
|
const outerTransactionHash = transactionHashUtils.getTransactionHashHex(outerTransaction);
|
|
const errorData = new ExchangeRevertErrors.TransactionInvalidContextError(
|
|
innerTransactionHash,
|
|
accounts[0],
|
|
).encode();
|
|
const expectedError = new ExchangeRevertErrors.TransactionExecutionError(outerTransactionHash, errorData);
|
|
const tx = transactionsContract
|
|
.executeTransaction(outerTransaction, validSignature)
|
|
.awaitTransactionSuccessAsync({
|
|
from: accounts[1], // Different then the signing addresses
|
|
});
|
|
return expect(tx).to.revertWith(expectedError);
|
|
});
|
|
it('should revert if reentrancy occurs in the middle of an executeTransaction call and msg.sender != signer and then msg.sender == signer', async () => {
|
|
const validSignature = randomSignature();
|
|
const innerTransaction = await generateZeroExTransactionAsync({ signerAddress: accounts[1] });
|
|
const innerTransactionHash = transactionHashUtils.getTransactionHashHex(innerTransaction);
|
|
const outerTransaction = await generateZeroExTransactionAsync({
|
|
signerAddress: accounts[0],
|
|
callData: getExecuteTransactionCallData(innerTransaction, validSignature),
|
|
returnData: DEADBEEF_RETURN_DATA,
|
|
});
|
|
const outerTransactionHash = transactionHashUtils.getTransactionHashHex(outerTransaction);
|
|
const errorData = new ExchangeRevertErrors.TransactionInvalidContextError(
|
|
innerTransactionHash,
|
|
accounts[0],
|
|
).encode();
|
|
const expectedError = new ExchangeRevertErrors.TransactionExecutionError(outerTransactionHash, errorData);
|
|
const tx = transactionsContract
|
|
.executeTransaction(outerTransaction, validSignature)
|
|
.awaitTransactionSuccessAsync({
|
|
from: accounts[1], // Different then the signing addresses
|
|
});
|
|
return expect(tx).to.revertWith(expectedError);
|
|
});
|
|
it('should allow reentrancy in the middle of an executeTransaction call if msg.sender == signer for both calls', async () => {
|
|
const validSignature = randomSignature();
|
|
const innerTransaction = await generateZeroExTransactionAsync({ signerAddress: accounts[0] });
|
|
const outerTransaction = await generateZeroExTransactionAsync({
|
|
signerAddress: accounts[0],
|
|
callData: getExecuteTransactionCallData(innerTransaction, validSignature),
|
|
returnData: DEADBEEF_RETURN_DATA,
|
|
});
|
|
return expect(
|
|
transactionsContract.executeTransaction(outerTransaction, validSignature).awaitTransactionSuccessAsync({
|
|
from: accounts[0],
|
|
}),
|
|
).to.be.fulfilled('');
|
|
});
|
|
it('should allow reentrancy in the middle of an executeTransaction call if msg.sender == signer and then msg.sender != signer', async () => {
|
|
const validSignature = randomSignature();
|
|
const innerTransaction = await generateZeroExTransactionAsync({ signerAddress: accounts[1] });
|
|
const outerTransaction = await generateZeroExTransactionAsync({
|
|
signerAddress: accounts[0],
|
|
callData: getExecuteTransactionCallData(innerTransaction, validSignature),
|
|
returnData: DEADBEEF_RETURN_DATA,
|
|
});
|
|
return expect(
|
|
transactionsContract.executeTransaction(outerTransaction, validSignature).awaitTransactionSuccessAsync({
|
|
from: accounts[0],
|
|
}),
|
|
).to.be.fulfilled('');
|
|
});
|
|
it('should revert if the transaction has been executed previously', async () => {
|
|
const validSignature = randomSignature();
|
|
const transaction = await generateZeroExTransactionAsync();
|
|
const transactionHash = transactionHashUtils.getTransactionHashHex(transaction);
|
|
// Use the transaction in execute transaction.
|
|
await expect(
|
|
transactionsContract.executeTransaction(transaction, validSignature).awaitTransactionSuccessAsync(),
|
|
).to.be.fulfilled('');
|
|
// Use the same transaction to make another call
|
|
const expectedError = new ExchangeRevertErrors.TransactionError(
|
|
ExchangeRevertErrors.TransactionErrorCode.AlreadyExecuted,
|
|
transactionHash,
|
|
);
|
|
const tx = transactionsContract
|
|
.executeTransaction(transaction, validSignature)
|
|
.awaitTransactionSuccessAsync();
|
|
return expect(tx).to.revertWith(expectedError);
|
|
});
|
|
it('should revert if the signer != msg.sender and the signature is not valid', async () => {
|
|
const transaction = await generateZeroExTransactionAsync({ signerAddress: accounts[1] });
|
|
const transactionHash = transactionHashUtils.getTransactionHashHex(transaction);
|
|
const expectedError = new ExchangeRevertErrors.SignatureError(
|
|
ExchangeRevertErrors.SignatureErrorCode.BadTransactionSignature,
|
|
transactionHash,
|
|
accounts[1],
|
|
INVALID_SIGNATURE,
|
|
);
|
|
const tx = transactionsContract
|
|
.executeTransaction(transaction, INVALID_SIGNATURE)
|
|
.awaitTransactionSuccessAsync({
|
|
from: accounts[0],
|
|
});
|
|
return expect(tx).to.revertWith(expectedError);
|
|
});
|
|
it('should revert if the signer == msg.sender but the delegatecall fails', async () => {
|
|
// This calldata is encoded to fail when it hits the executable function.
|
|
const transaction = await generateZeroExTransactionAsync({
|
|
signerAddress: accounts[1],
|
|
shouldSucceed: false,
|
|
});
|
|
const transactionHash = transactionHashUtils.getTransactionHashHex(transaction);
|
|
const executableError = new StringRevertError('EXECUTABLE_FAILED');
|
|
const expectedError = new ExchangeRevertErrors.TransactionExecutionError(
|
|
transactionHash,
|
|
executableError.encode(),
|
|
);
|
|
const tx = transactionsContract
|
|
.executeTransaction(transaction, randomSignature())
|
|
.awaitTransactionSuccessAsync({
|
|
from: accounts[1],
|
|
});
|
|
return expect(tx).to.revertWith(expectedError);
|
|
});
|
|
it('should revert if the signer != msg.sender and the signature is valid but the delegatecall fails', async () => {
|
|
// This calldata is encoded to fail when it hits the executable function.
|
|
const transaction = await generateZeroExTransactionAsync({
|
|
signerAddress: accounts[1],
|
|
shouldSucceed: false,
|
|
});
|
|
const validSignature = randomSignature(); // Valid because length != 2
|
|
const transactionHash = transactionHashUtils.getTransactionHashHex(transaction);
|
|
const executableError = new StringRevertError('EXECUTABLE_FAILED');
|
|
const expectedError = new ExchangeRevertErrors.TransactionExecutionError(
|
|
transactionHash,
|
|
executableError.encode(),
|
|
);
|
|
const tx = transactionsContract
|
|
.executeTransaction(transaction, validSignature)
|
|
.awaitTransactionSuccessAsync({
|
|
from: accounts[0],
|
|
});
|
|
return expect(tx).to.revertWith(expectedError);
|
|
});
|
|
it('should succeed with the correct return hash and event emitted when msg.sender != signer', async () => {
|
|
// This calldata is encoded to succeed when it hits the executable function.
|
|
const validSignature = randomSignature(); // Valid because length != 2
|
|
const transaction = await generateZeroExTransactionAsync({
|
|
signerAddress: accounts[1],
|
|
returnData: DEADBEEF_RETURN_DATA,
|
|
});
|
|
const transactionHash = transactionHashUtils.getTransactionHashHex(transaction);
|
|
|
|
const contractFn = transactionsContract.executeTransaction(transaction, validSignature);
|
|
const result = await contractFn.callAsync({ from: accounts[0] });
|
|
|
|
const receipt = await contractFn.awaitTransactionSuccessAsync({ from: accounts[0] });
|
|
expect(transactionsContract.getABIDecodedReturnData('executeTransaction', result)).to.equal(
|
|
DEADBEEF_RETURN_DATA,
|
|
);
|
|
|
|
// Ensure that the correct number of events were logged.
|
|
const logs = receipt.logs as Array<LogWithDecodedArgs<TestTransactionsTransactionExecutionEventArgs>>;
|
|
expect(logs.length).to.be.eq(2);
|
|
// Ensure that the correct events were logged.
|
|
expect(logs[0].event).to.be.eq('ExecutableCalled');
|
|
expect(logs[0].args.data).to.be.eq(constants.NULL_BYTES);
|
|
expect(logs[0].args.returnData).to.be.eq(DEADBEEF_RETURN_DATA);
|
|
expect(logs[0].args.contextAddress).to.be.eq(accounts[1]);
|
|
expect(logs[1].event).to.be.eq('TransactionExecution');
|
|
expect(logs[1].args.transactionHash).to.eq(transactionHash);
|
|
});
|
|
it('should succeed with the correct return hash and event emitted when msg.sender == signer', async () => {
|
|
// This calldata is encoded to succeed when it hits the executable function.
|
|
const validSignature = randomSignature(); // Valid because length != 2
|
|
const transaction = await generateZeroExTransactionAsync({
|
|
signerAddress: accounts[0],
|
|
returnData: DEADBEEF_RETURN_DATA,
|
|
});
|
|
const transactionHash = transactionHashUtils.getTransactionHashHex(transaction);
|
|
|
|
const contractFn = transactionsContract.executeTransaction(transaction, validSignature);
|
|
const result = await contractFn.callAsync({ from: accounts[0] });
|
|
|
|
const receipt = await contractFn.awaitTransactionSuccessAsync({ from: accounts[0] });
|
|
expect(transactionsContract.getABIDecodedReturnData('executeTransaction', result)).to.equal(
|
|
DEADBEEF_RETURN_DATA,
|
|
);
|
|
|
|
// Ensure that the correct number of events were logged.
|
|
const logs = receipt.logs as Array<LogWithDecodedArgs<TestTransactionsTransactionExecutionEventArgs>>;
|
|
expect(logs.length).to.be.eq(2);
|
|
// Ensure that the correct events were logged.
|
|
expect(logs[0].event).to.be.eq('ExecutableCalled');
|
|
expect(logs[0].args.data).to.be.eq(constants.NULL_BYTES);
|
|
expect(logs[0].args.returnData).to.be.eq(DEADBEEF_RETURN_DATA);
|
|
expect(logs[0].args.contextAddress).to.be.eq(constants.NULL_ADDRESS);
|
|
expect(logs[1].event).to.be.eq('TransactionExecution');
|
|
expect(logs[1].args.transactionHash).to.eq(transactionHash);
|
|
});
|
|
});
|
|
|
|
blockchainTests.resets('assertExecutableTransaction', () => {
|
|
it('should revert if the transaction is expired', async () => {
|
|
const transaction = await generateZeroExTransactionAsync({
|
|
expirationTimeSeconds: constants.ZERO_AMOUNT,
|
|
});
|
|
const transactionHash = transactionHashUtils.getTransactionHashHex(transaction);
|
|
const expectedError = new ExchangeRevertErrors.TransactionError(
|
|
ExchangeRevertErrors.TransactionErrorCode.Expired,
|
|
transactionHash,
|
|
);
|
|
expect(
|
|
transactionsContract.assertExecutableTransaction(transaction, randomSignature()).callAsync(),
|
|
).to.revertWith(expectedError);
|
|
});
|
|
it('should revert if the gasPrice is less than required', async () => {
|
|
const transaction = await generateZeroExTransactionAsync();
|
|
const transactionHash = transactionHashUtils.getTransactionHashHex(transaction);
|
|
const actualGasPrice = transaction.gasPrice.minus(1);
|
|
const expectedError = new ExchangeRevertErrors.TransactionGasPriceError(
|
|
transactionHash,
|
|
actualGasPrice,
|
|
transaction.gasPrice,
|
|
);
|
|
expect(
|
|
transactionsContract.assertExecutableTransaction(transaction, randomSignature()).callAsync({
|
|
gasPrice: actualGasPrice,
|
|
}),
|
|
).to.revertWith(expectedError);
|
|
});
|
|
it('should revert if the gasPrice is greater than required', async () => {
|
|
const transaction = await generateZeroExTransactionAsync();
|
|
const transactionHash = transactionHashUtils.getTransactionHashHex(transaction);
|
|
const actualGasPrice = transaction.gasPrice.plus(1);
|
|
const expectedError = new ExchangeRevertErrors.TransactionGasPriceError(
|
|
transactionHash,
|
|
actualGasPrice,
|
|
transaction.gasPrice,
|
|
);
|
|
expect(
|
|
transactionsContract.assertExecutableTransaction(transaction, randomSignature()).callAsync({
|
|
gasPrice: actualGasPrice,
|
|
}),
|
|
).to.revertWith(expectedError);
|
|
});
|
|
it('should revert if currentContextAddress is non-zero', async () => {
|
|
await transactionsContract.setCurrentContextAddress(accounts[0]).awaitTransactionSuccessAsync();
|
|
const transaction = await generateZeroExTransactionAsync();
|
|
const transactionHash = transactionHashUtils.getTransactionHashHex(transaction);
|
|
const expectedError = new ExchangeRevertErrors.TransactionInvalidContextError(transactionHash, accounts[0]);
|
|
expect(
|
|
transactionsContract.assertExecutableTransaction(transaction, randomSignature()).callAsync(),
|
|
).to.revertWith(expectedError);
|
|
});
|
|
it('should revert if the transaction has already been executed', async () => {
|
|
const transaction = await generateZeroExTransactionAsync();
|
|
const transactionHash = transactionHashUtils.getTransactionHashHex(transaction);
|
|
await transactionsContract.setTransactionExecuted(transactionHash).awaitTransactionSuccessAsync();
|
|
const expectedError = new ExchangeRevertErrors.TransactionError(
|
|
ExchangeRevertErrors.TransactionErrorCode.AlreadyExecuted,
|
|
transactionHash,
|
|
);
|
|
expect(
|
|
transactionsContract.assertExecutableTransaction(transaction, randomSignature()).callAsync(),
|
|
).to.revertWith(expectedError);
|
|
});
|
|
it('should revert if signer != msg.sender and the signature is invalid', async () => {
|
|
const transaction = await generateZeroExTransactionAsync({ signerAddress: accounts[0] });
|
|
const transactionHash = transactionHashUtils.getTransactionHashHex(transaction);
|
|
const expectedError = new ExchangeRevertErrors.SignatureError(
|
|
ExchangeRevertErrors.SignatureErrorCode.BadTransactionSignature,
|
|
transactionHash,
|
|
accounts[0],
|
|
INVALID_SIGNATURE,
|
|
);
|
|
expect(
|
|
transactionsContract.assertExecutableTransaction(transaction, INVALID_SIGNATURE).callAsync({
|
|
from: accounts[1],
|
|
}),
|
|
).to.revertWith(expectedError);
|
|
});
|
|
it('should be successful if signer == msg.sender and the signature is invalid', async () => {
|
|
const transaction = await generateZeroExTransactionAsync({ signerAddress: accounts[0] });
|
|
return expect(
|
|
transactionsContract.assertExecutableTransaction(transaction, INVALID_SIGNATURE).callAsync({
|
|
from: accounts[0],
|
|
}),
|
|
).to.be.fulfilled('');
|
|
});
|
|
it('should be successful if signer == msg.sender and the signature is valid', async () => {
|
|
const transaction = await generateZeroExTransactionAsync({ signerAddress: accounts[0] });
|
|
return expect(
|
|
transactionsContract.assertExecutableTransaction(transaction, randomSignature()).callAsync({
|
|
from: accounts[0],
|
|
}),
|
|
).to.be.fulfilled('');
|
|
});
|
|
it('should be successful if not expired, the gasPrice is correct, the tx has not been executed, currentContextAddress has not been set, signer != msg.sender, and the signature is valid', async () => {
|
|
const transaction = await generateZeroExTransactionAsync({ signerAddress: accounts[0] });
|
|
return expect(
|
|
transactionsContract.assertExecutableTransaction(transaction, randomSignature()).callAsync({
|
|
from: accounts[1],
|
|
}),
|
|
).to.be.fulfilled('');
|
|
});
|
|
});
|
|
|
|
describe('setCurrentContextAddressIfRequired', () => {
|
|
it('should set the currentContextAddress if signer not equal to sender', async () => {
|
|
const randomAddress = hexRandom(20);
|
|
await transactionsContract
|
|
.setCurrentContextAddressIfRequired(randomAddress, randomAddress)
|
|
.awaitTransactionSuccessAsync();
|
|
const currentContextAddress = await transactionsContract.currentContextAddress().callAsync();
|
|
expect(currentContextAddress).to.eq(randomAddress);
|
|
});
|
|
it('should not set the currentContextAddress if signer equal to sender', async () => {
|
|
const randomAddress = hexRandom(20);
|
|
await transactionsContract
|
|
.setCurrentContextAddressIfRequired(accounts[0], randomAddress)
|
|
.awaitTransactionSuccessAsync({
|
|
from: accounts[0],
|
|
});
|
|
const currentContextAddress = await transactionsContract.currentContextAddress().callAsync();
|
|
expect(currentContextAddress).to.eq(constants.NULL_ADDRESS);
|
|
});
|
|
});
|
|
|
|
describe('getCurrentContext', () => {
|
|
it('should return the sender address when there is not a saved context address', async () => {
|
|
const currentContextAddress = await transactionsContract.getCurrentContextAddress().callAsync({
|
|
from: accounts[0],
|
|
});
|
|
expect(currentContextAddress).to.be.eq(accounts[0]);
|
|
});
|
|
|
|
it('should return the sender address when there is a saved context address', async () => {
|
|
// Set the current context address to the taker address
|
|
await transactionsContract.setCurrentContextAddress(accounts[1]).awaitTransactionSuccessAsync();
|
|
|
|
// Ensure that the queried current context address is the same as the address that was set.
|
|
const currentContextAddress = await transactionsContract.getCurrentContextAddress().callAsync({
|
|
from: accounts[0],
|
|
});
|
|
expect(currentContextAddress).to.be.eq(accounts[1]);
|
|
});
|
|
});
|
|
});
|
|
// tslint:disable-line:max-file-line-count
|