import { blockchainTests, constants, describe, expect, hexRandom, TransactionHelper } from '@0x/contracts-test-utils'; import { ExchangeRevertErrors, transactionHashUtils } from '@0x/order-utils'; import { EIP712DomainWithDefaultSchema, ZeroExTransaction } from '@0x/types'; import { BigNumber, StringRevertError } from '@0x/utils'; import { LogWithDecodedArgs } from 'ethereum-types'; import * as _ from 'lodash'; import { artifacts, TestTransactionsContract, TestTransactionsTransactionExecutionEventArgs } from '../src'; 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'; const transactionHelper = new TransactionHelper(web3Wrapper, artifacts); 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.getABIEncodedTransactionData( shouldSucceed, callData, returnData, ); } 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 { 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.awaitTransactionSuccessAsync( [transaction], [randomSignature()], ); 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.awaitTransactionSuccessAsync( [transaction1, transaction2], [randomSignature(), randomSignature()], ); 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.awaitTransactionSuccessAsync( [transaction1, transaction2], [randomSignature(), randomSignature()], ); 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.awaitTransactionSuccessAsync( [transaction1, transaction2], [randomSignature(), randomSignature()], { 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 [result, receipt] = await transactionHelper.getResultAndReceiptAsync( transactionsContract.batchExecuteTransactions, [transaction], [validSignature], { from: accounts[0] }, ); expect(result.length).to.be.eq(1); const returnData = transactionsContract.executeTransaction.getABIDecodedReturnData(result[0]); expect(returnData).to.equal(DEADBEEF_RETURN_DATA); // Ensure that the correct number of events were logged. const logs = receipt.logs as Array>; 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 [result, receipt] = await transactionHelper.getResultAndReceiptAsync( transactionsContract.batchExecuteTransactions, [transaction1, transaction2], [randomSignature(), randomSignature()], { from: accounts[0] }, ); expect(result.length).to.be.eq(2); expect(transactionsContract.executeTransaction.getABIDecodedReturnData(result[0])).to.equal( DEADBEEF_RETURN_DATA, ); expect(transactionsContract.executeTransaction.getABIDecodedReturnData(result[1])).to.equal(returnData2); // Verify that the correct number of events were logged. const logs = receipt.logs as Array>; 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.getABIEncodedTransactionData( [innerTransaction1, innerTransaction2], [randomSignature(), randomSignature()], ), }); const outerExecuteTransaction = await generateZeroExTransactionAsync({ signerAddress: accounts[1], callData: transactionsContract.executeTransaction.getABIEncodedTransactionData( innerBatchExecuteTransaction, randomSignature(), ), }); 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.awaitTransactionSuccessAsync( [outerExecuteTransaction], [randomSignature()], { 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.getABIEncodedTransactionData( [innerTransaction1, innerTransaction2], [randomSignature(), randomSignature()], ), }); const outerExecuteTransaction = await generateZeroExTransactionAsync({ signerAddress: accounts[2], callData: transactionsContract.executeTransaction.getABIEncodedTransactionData( innerBatchExecuteTransaction, randomSignature(), ), }); return expect( transactionsContract.batchExecuteTransactions.awaitTransactionSuccessAsync( [outerExecuteTransaction], [randomSignature()], { from: accounts[2] }, ), ).to.be.fulfilled(''); }); }); describe('executeTransaction', () => { function getExecuteTransactionCallData(transaction: ZeroExTransaction, signature: string): string { return (transactionsContract as any).executeTransaction.getABIEncodedTransactionData( transaction, signature, ); } 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.awaitTransactionSuccessAsync( transaction, randomSignature(), ); 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.awaitTransactionSuccessAsync( transaction, randomSignature(), { 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.awaitTransactionSuccessAsync( outerTransaction, validSignature, { 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.awaitTransactionSuccessAsync( outerTransaction, validSignature, { 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.awaitTransactionSuccessAsync(outerTransaction, validSignature, { 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.awaitTransactionSuccessAsync(outerTransaction, validSignature, { 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.awaitTransactionSuccessAsync(transaction, validSignature), ).to.be.fulfilled(''); // Use the same transaction to make another call const expectedError = new ExchangeRevertErrors.TransactionError( ExchangeRevertErrors.TransactionErrorCode.AlreadyExecuted, transactionHash, ); const tx = transactionsContract.executeTransaction.awaitTransactionSuccessAsync( transaction, validSignature, ); 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.awaitTransactionSuccessAsync( transaction, INVALID_SIGNATURE, { 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.awaitTransactionSuccessAsync( transaction, randomSignature(), { 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.awaitTransactionSuccessAsync( transaction, validSignature, { 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 [result, receipt] = await transactionHelper.getResultAndReceiptAsync( transactionsContract.executeTransaction, transaction, validSignature, { from: accounts[0] }, ); expect(transactionsContract.executeTransaction.getABIDecodedReturnData(result)).to.equal( DEADBEEF_RETURN_DATA, ); // Ensure that the correct number of events were logged. const logs = receipt.logs as Array>; 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 [result, receipt] = await transactionHelper.getResultAndReceiptAsync( transactionsContract.executeTransaction, transaction, validSignature, { from: accounts[0] }, ); expect(transactionsContract.executeTransaction.getABIDecodedReturnData(result)).to.equal( DEADBEEF_RETURN_DATA, ); // Ensure that the correct number of events were logged. const logs = receipt.logs as Array>; 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.callAsync(transaction, randomSignature()), ).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.callAsync(transaction, randomSignature(), { 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.callAsync(transaction, randomSignature(), { gasPrice: actualGasPrice, }), ).to.revertWith(expectedError); }); it('should revert if currentContextAddress is non-zero', async () => { await transactionsContract.setCurrentContextAddress.awaitTransactionSuccessAsync(accounts[0]); const transaction = await generateZeroExTransactionAsync(); const transactionHash = transactionHashUtils.getTransactionHashHex(transaction); const expectedError = new ExchangeRevertErrors.TransactionInvalidContextError(transactionHash, accounts[0]); expect( transactionsContract.assertExecutableTransaction.callAsync(transaction, randomSignature()), ).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.awaitTransactionSuccessAsync(transactionHash); const expectedError = new ExchangeRevertErrors.TransactionError( ExchangeRevertErrors.TransactionErrorCode.AlreadyExecuted, transactionHash, ); expect( transactionsContract.assertExecutableTransaction.callAsync(transaction, randomSignature()), ).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.callAsync(transaction, INVALID_SIGNATURE, { 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.callAsync(transaction, INVALID_SIGNATURE, { 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.callAsync(transaction, randomSignature(), { 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.callAsync(transaction, randomSignature(), { 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.awaitTransactionSuccessAsync( randomAddress, randomAddress, ); 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.awaitTransactionSuccessAsync( accounts[0], randomAddress, { 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.awaitTransactionSuccessAsync(accounts[1]); // 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