Merge pull request #2084 from 0xProject/feat/3.0/transactionGasPrice

Add `gasPrice` to 0x transactions
This commit is contained in:
Amir Bandeali
2019-08-24 10:13:07 -07:00
committed by GitHub
28 changed files with 1097 additions and 77 deletions

View File

@@ -91,7 +91,7 @@ jobs:
keys:
- repo-{{ .Environment.CIRCLE_SHA1 }}
- run: yarn wsrun test:circleci @0x/contracts-multisig @0x/contracts-utils @0x/contracts-exchange-libs @0x/contracts-erc20 @0x/contracts-erc721 @0x/contracts-erc1155 @0x/contracts-extensions @0x/contracts-asset-proxy @0x/contracts-exchange @0x/contracts-exchange-forwarder @0x/contracts-coordinator @0x/contracts-dev-utils @0x/contracts-staking
test-contracts-ganache-3.0:
test-exchange-ganache-3.0:
resource_class: medium+
docker:
- image: nikolaik/python-nodejs:python3.7-nodejs8
@@ -100,7 +100,17 @@ jobs:
- restore_cache:
keys:
- repo-{{ .Environment.CIRCLE_SHA1 }}
- run: yarn wsrun test:circleci @0x/contracts-multisig @0x/contracts-utils @0x/contracts-exchange-libs @0x/contracts-erc20 @0x/contracts-erc721 @0x/contracts-erc1155 @0x/contracts-asset-proxy @0x/contracts-exchange @0x/contracts-exchange-forwarder @0x/contracts-dev-utils @0x/contracts-staking
- run: yarn wsrun test:circleci @0x/contracts-exchange
test-contracts-rest-ganache-3.0:
resource_class: medium+
docker:
- image: nikolaik/python-nodejs:python3.7-nodejs8
working_directory: ~/repo
steps:
- restore_cache:
keys:
- repo-{{ .Environment.CIRCLE_SHA1 }}
- run: yarn wsrun test:circleci @0x/contracts-multisig @0x/contracts-utils @0x/contracts-exchange-libs @0x/contracts-erc20 @0x/contracts-erc721 @0x/contracts-erc1155 @0x/contracts-asset-proxy @0x/contracts-exchange-forwarder @0x/contracts-dev-utils @0x/contracts-staking
# TODO(dorothy-zbornak): Re-enable after updating this package for 3.0.
# - run: yarn wsrun test:circleci @0x/contracts-extensions
# TODO(abandeali): Re-enable after this package is complete.
@@ -563,7 +573,10 @@ workflows:
# - build-website:
# requires:
# - build
- test-contracts-ganache-3.0:
- test-exchange-ganache-3.0:
requires:
- build-3.0
- test-contracts-rest-ganache-3.0:
requires:
- build-3.0
# Disabled until geth docker image is fixed.
@@ -590,7 +603,8 @@ workflows:
# - build-3.0
- submit-coverage-3.0:
requires:
- test-contracts-ganache-3.0
- test-contracts-rest-ganache-3.0
- test-exchange-ganache-3.0
- test-rest-3.0
- static-tests-3.0
# Disabled for 3.0

View File

@@ -20,7 +20,7 @@ const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
describe('EtherToken', () => {
let account: string;
const gasPrice = Web3Wrapper.toBaseUnitAmount(new BigNumber(20), 9);
const gasPrice = new BigNumber(constants.DEFAULT_GAS_PRICE);
let etherToken: WETH9Contract;
before(async () => {

View File

@@ -20,7 +20,7 @@ import { artifacts, ForwarderContract, ForwarderTestFactory, ForwarderWrapper }
const DECIMALS_DEFAULT = 18;
blockchainTests.only(ContractName.Forwarder, env => {
blockchainTests(ContractName.Forwarder, env => {
let chainId: number;
let makerAddress: string;
let owner: string;
@@ -45,7 +45,7 @@ blockchainTests.only(ContractName.Forwarder, env => {
let tx: TransactionReceiptWithDecodedLogs;
let erc721MakerAssetIds: BigNumber[];
let gasPrice: BigNumber;
const gasPrice = new BigNumber(constants.DEFAULT_GAS_PRICE);
before(async () => {
await env.blockchainLifecycle.startAsync();
@@ -61,10 +61,6 @@ blockchainTests.only(ContractName.Forwarder, env => {
forwarderFeeRecipientAddress,
] = accounts);
const txHash = await env.web3Wrapper.sendTransactionAsync({ from: accounts[0], to: accounts[0], value: 0 });
const transaction = await env.web3Wrapper.getTransactionByHashAsync(txHash);
gasPrice = new BigNumber(transaction.gasPrice);
const erc721Wrapper = new ERC721Wrapper(env.provider, usedAddresses, owner);
erc20Wrapper = new ERC20Wrapper(env.provider, usedAddresses, owner);

View File

@@ -53,7 +53,6 @@ library LibExchangeRichErrors {
}
enum TransactionErrorCodes {
NO_REENTRANCY,
ALREADY_EXECUTED,
EXPIRED
}
@@ -131,6 +130,14 @@ library LibExchangeRichErrors {
// bytes4(keccak256("TransactionExecutionError(bytes32,bytes)"))
bytes4 internal constant TRANSACTION_EXECUTION_ERROR_SELECTOR =
0x20d11f61;
// bytes4(keccak256("TransactionGasPriceError(bytes32,uint256,uint256)"))
bytes4 internal constant TRANSACTION_GAS_PRICE_ERROR_SELECTOR =
0xa26dac09;
// bytes4(keccak256("TransactionInvalidContextError(bytes32,address)"))
bytes4 internal constant TRANSACTION_INVALID_CONTEXT_ERROR_SELECTOR =
0xdec4aedf;
// bytes4(keccak256("IncompleteFillError(uint8,uint256,uint256)"))
bytes4 internal constant INCOMPLETE_FILL_ERROR_SELECTOR =
@@ -293,6 +300,14 @@ library LibExchangeRichErrors {
return BATCH_MATCH_ORDERS_ERROR_SELECTOR;
}
function TransactionGasPriceErrorSelector()
internal
pure
returns (bytes4)
{
return TRANSACTION_GAS_PRICE_ERROR_SELECTOR;
}
function BatchMatchOrdersError(
BatchMatchOrdersErrorCodes errorCode
)
@@ -581,6 +596,38 @@ library LibExchangeRichErrors {
);
}
function TransactionGasPriceError(
bytes32 transactionHash,
uint256 actualGasPrice,
uint256 requiredGasPrice
)
internal
pure
returns (bytes memory)
{
return abi.encodeWithSelector(
TRANSACTION_GAS_PRICE_ERROR_SELECTOR,
transactionHash,
actualGasPrice,
requiredGasPrice
);
}
function TransactionInvalidContextError(
bytes32 transactionHash,
address currentContextAddress
)
internal
pure
returns (bytes memory)
{
return abi.encodeWithSelector(
TRANSACTION_INVALID_CONTEXT_ERROR_SELECTOR,
transactionHash,
currentContextAddress
);
}
function IncompleteFillError(
IncompleteFillErrorCode errorCode,
uint256 expectedAssetFillAmount,

View File

@@ -30,16 +30,18 @@ library LibZeroExTransaction {
// keccak256(abi.encodePacked(
// "ZeroExTransaction(",
// "uint256 salt,",
// "uint256 expirationTimeSeconds,"
// "uint256 expirationTimeSeconds,",
// "uint256 gasPrice,",
// "address signerAddress,",
// "bytes data",
// ")"
// ));
bytes32 constant internal _EIP712_ZEROEX_TRANSACTION_SCHEMA_HASH = 0x6b4c70d217b44d0ff0d3bf7aeb18eb8604c5cd06f615a4b497aeefa4f01d2775;
bytes32 constant internal _EIP712_ZEROEX_TRANSACTION_SCHEMA_HASH = 0xec69816980a3a3ca4554410e60253953e9ff375ba4536a98adfa15cc71541508;
struct ZeroExTransaction {
uint256 salt; // Arbitrary number to ensure uniqueness of transaction hash.
uint256 expirationTimeSeconds; // Timestamp in seconds at which transaction expires.
uint256 gasPrice; // gasPrice at which transaction is required to be executed with.
address signerAddress; // Address of transaction signer.
bytes data; // AbiV2 encoded calldata.
}
@@ -72,15 +74,17 @@ library LibZeroExTransaction {
bytes memory data = transaction.data;
uint256 salt = transaction.salt;
uint256 expirationTimeSeconds = transaction.expirationTimeSeconds;
uint256 gasPrice = transaction.gasPrice;
address signerAddress = transaction.signerAddress;
// Assembly for more efficiently computing:
// keccak256(abi.encodePacked(
// EIP712_ZEROEX_TRANSACTION_SCHEMA_HASH,
// transaction.salt,
// transaction.expirationTimeSeconds,
// uint256(transaction.signerAddress),
// keccak256(transaction.data)
// result = keccak256(abi.encodePacked(
// schemaHash,
// salt,
// expirationTimeSeconds,
// gasPrice,
// uint256(signerAddress),
// keccak256(data)
// ));
assembly {
@@ -90,14 +94,15 @@ library LibZeroExTransaction {
// Load free memory pointer
let memPtr := mload(64)
mstore(memPtr, schemaHash) // hash of schema
mstore(add(memPtr, 32), salt) // salt
mstore(add(memPtr, 64), expirationTimeSeconds) // expirationTimeSeconds
mstore(add(memPtr, 96), and(signerAddress, 0xffffffffffffffffffffffffffffffffffffffff)) // signerAddress
mstore(add(memPtr, 128), dataHash) // hash of data
mstore(memPtr, schemaHash) // hash of schema
mstore(add(memPtr, 32), salt) // salt
mstore(add(memPtr, 64), expirationTimeSeconds) // expirationTimeSeconds
mstore(add(memPtr, 96), gasPrice) // gasPrice
mstore(add(memPtr, 128), and(signerAddress, 0xffffffffffffffffffffffffffffffffffffffff)) // signerAddress
mstore(add(memPtr, 160), dataHash) // hash of data
// Compute hash
result := keccak256(memPtr, 160)
result := keccak256(memPtr, 192)
}
return result;
}

View File

@@ -18,6 +18,7 @@ blockchainTests('LibZeroExTransaction', env => {
const EMPTY_TRANSACTION: ZeroExTransaction = {
salt: constants.ZERO_AMOUNT,
expirationTimeSeconds: constants.ZERO_AMOUNT,
gasPrice: constants.ZERO_AMOUNT,
signerAddress: constants.NULL_ADDRESS,
data: constants.NULL_BYTES,
domain: {
@@ -66,6 +67,7 @@ blockchainTests('LibZeroExTransaction', env => {
await testGetTypedDataHashAsync({
salt: randomUint256(),
expirationTimeSeconds: randomUint256(),
gasPrice: randomUint256(),
signerAddress: randomAddress(),
data: randomAssetData(),
domain: {
@@ -121,6 +123,7 @@ blockchainTests('LibZeroExTransaction', env => {
await testGetStructHashAsync({
salt: randomUint256(),
expirationTimeSeconds: randomUint256(),
gasPrice: randomUint256(),
signerAddress: randomAddress(),
data: randomAssetData(),
// The domain is not used in this test, so it's okay if it is left empty.

View File

@@ -61,6 +61,7 @@ contract ExchangeWrapper {
LibZeroExTransaction.ZeroExTransaction memory transaction = LibZeroExTransaction.ZeroExTransaction({
salt: salt,
expirationTimeSeconds: transactionExpirationTimeSeconds,
gasPrice: tx.gasprice,
data: data,
signerAddress: makerAddress
});
@@ -99,6 +100,7 @@ contract ExchangeWrapper {
LibZeroExTransaction.ZeroExTransaction memory transaction = LibZeroExTransaction.ZeroExTransaction({
salt: salt,
expirationTimeSeconds: transactionExpirationTimeSeconds,
gasPrice: tx.gasprice,
data: data,
signerAddress: takerAddress
});

View File

@@ -134,6 +134,7 @@ contract Whitelist is
LibZeroExTransaction.ZeroExTransaction memory transaction = LibZeroExTransaction.ZeroExTransaction({
salt: salt,
data: data,
gasPrice: tx.gasprice,
expirationTimeSeconds: uint256(-1),
signerAddress: takerAddress
});

View File

@@ -361,11 +361,14 @@ contract MixinExchangeCore is
orderInfo.orderHash,
makerAddress,
signature
)) {
)
) {
if (!_isValidOrderWithHashSignature(
order,
orderInfo.orderHash,
signature)) {
signature
)
) {
LibRichErrors.rrevert(LibExchangeRichErrors.SignatureError(
LibExchangeRichErrors.SignatureErrorCodes.BAD_SIGNATURE,
orderInfo.orderHash,

View File

@@ -42,7 +42,7 @@ contract MixinTransactions is
address public currentContextAddress;
/// @dev Executes an Exchange method call in the context of signer.
/// @param transaction 0x transaction containing salt, signerAddress, and data.
/// @param transaction 0x transaction structure.
/// @param signature Proof that transaction has been signed by signer.
/// @return ABI encoded return data of the underlying Exchange function call.
function executeTransaction(
@@ -56,7 +56,7 @@ contract MixinTransactions is
}
/// @dev Executes a batch of Exchange method calls in the context of signer(s).
/// @param transactions Array of 0x transactions containing salt, signerAddress, and data.
/// @param transactions Array of 0x transaction structures.
/// @param signatures Array of proofs that transactions have been signed by signer(s).
/// @return Array containing ABI encoded return data for each of the underlying Exchange function calls.
function batchExecuteTransactions(
@@ -75,7 +75,7 @@ contract MixinTransactions is
}
/// @dev Executes an Exchange method call in the context of signer.
/// @param transaction 0x transaction containing salt, signerAddress, and data.
/// @param transaction 0x transaction structure.
/// @param signature Proof that transaction has been signed by signer.
/// @return ABI encoded return data of the underlying Exchange function call.
function _executeTransaction(
@@ -87,46 +87,14 @@ contract MixinTransactions is
{
bytes32 transactionHash = transaction.getTypedDataHash(EIP712_EXCHANGE_DOMAIN_HASH);
// Check transaction is not expired
// solhint-disable-next-line not-rely-on-time
if (block.timestamp >= transaction.expirationTimeSeconds) {
LibRichErrors.rrevert(LibExchangeRichErrors.TransactionError(
LibExchangeRichErrors.TransactionErrorCodes.EXPIRED,
transactionHash
));
}
_assertExecutableTransaction(
transaction,
signature,
transactionHash
);
// Prevent reentrancy
if (currentContextAddress != address(0)) {
LibRichErrors.rrevert(LibExchangeRichErrors.TransactionError(
LibExchangeRichErrors.TransactionErrorCodes.NO_REENTRANCY,
transactionHash
));
}
// Validate transaction has not been executed
if (transactionsExecuted[transactionHash]) {
LibRichErrors.rrevert(LibExchangeRichErrors.TransactionError(
LibExchangeRichErrors.TransactionErrorCodes.ALREADY_EXECUTED,
transactionHash
));
}
// Transaction always valid if signer is sender of transaction
address signerAddress = transaction.signerAddress;
if (signerAddress != msg.sender) {
// Validate signature
if (!_isValidTransactionWithHashSignature(
transaction,
transactionHash,
signature)) {
LibRichErrors.rrevert(LibExchangeRichErrors.TransactionSignatureError(
transactionHash,
signerAddress,
signature
));
}
// Set the current transaction signer
currentContextAddress = signerAddress;
}
@@ -151,6 +119,71 @@ contract MixinTransactions is
return returnData;
}
/// @dev Validates context for executeTransaction. Succeeds or throws.
/// @param transaction 0x transaction structure.
/// @param signature Proof that transaction has been signed by signer.
/// @param transactionHash EIP712 typed data hash of 0x transaction.
function _assertExecutableTransaction(
LibZeroExTransaction.ZeroExTransaction memory transaction,
bytes memory signature,
bytes32 transactionHash
)
internal
view
{
// Check transaction is not expired
// solhint-disable-next-line not-rely-on-time
if (block.timestamp >= transaction.expirationTimeSeconds) {
LibRichErrors.rrevert(LibExchangeRichErrors.TransactionError(
LibExchangeRichErrors.TransactionErrorCodes.EXPIRED,
transactionHash
));
}
// Validate that transaction is executed with the correct gasPrice
uint256 requiredGasPrice = transaction.gasPrice;
if (tx.gasprice != requiredGasPrice) {
LibRichErrors.rrevert(LibExchangeRichErrors.TransactionGasPriceError(
transactionHash,
tx.gasprice,
requiredGasPrice
));
}
// Prevent `executeTransaction` from being called when context is already set
address currentContextAddress_ = currentContextAddress;
if (currentContextAddress_ != address(0)) {
LibRichErrors.rrevert(LibExchangeRichErrors.TransactionInvalidContextError(
transactionHash,
currentContextAddress_
));
}
// Validate transaction has not been executed
if (transactionsExecuted[transactionHash]) {
LibRichErrors.rrevert(LibExchangeRichErrors.TransactionError(
LibExchangeRichErrors.TransactionErrorCodes.ALREADY_EXECUTED,
transactionHash
));
}
// Validate signature
// Transaction always valid if signer is sender of transaction
address signerAddress = transaction.signerAddress;
if (signerAddress != msg.sender && !_isValidTransactionWithHashSignature(
transaction,
transactionHash,
signature
)
) {
LibRichErrors.rrevert(LibExchangeRichErrors.TransactionSignatureError(
transactionHash,
signerAddress,
signature
));
}
}
/// @dev The current function will be called in the context of this address (either 0x transaction signer or `msg.sender`).
/// If calling a fill function, this address will represent the taker.
/// If calling a cancel function, this address will represent the maker.

View File

@@ -57,4 +57,4 @@ contract MixinTransferSimulator is
}
revert("TRANSFERS_SUCCESSFUL");
}
}
}

View File

@@ -0,0 +1,117 @@
/*
Copyright 2019 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.5.9;
pragma experimental ABIEncoderV2;
import "@0x/contracts-exchange-libs/contracts/src/LibZeroExTransaction.sol";
import "../src/Exchange.sol";
contract TestTransactions is
Exchange
{
event ExecutableCalled(
bytes data,
bytes returnData,
address contextAddress
);
constructor ()
public
Exchange(1337)
{} // solhint-disable-line no-empty-blocks
function setCurrentContextAddress(address context)
external
{
currentContextAddress = context;
}
function setTransactionExecuted(bytes32 hash)
external
{
transactionsExecuted[hash] = true;
}
function getCurrentContextAddress()
external
view
returns (address)
{
return _getCurrentContextAddress();
}
function assertExecutableTransaction(
LibZeroExTransaction.ZeroExTransaction memory transaction,
bytes memory signature
)
public
view
{
return _assertExecutableTransaction(
transaction,
signature,
transaction.getTypedDataHash(EIP712_EXCHANGE_DOMAIN_HASH)
);
}
// This function will execute arbitrary calldata via a delegatecall. This is highly unsafe to use in production, and this
// is only meant to be used during testing.
function executable(
bool shouldSucceed,
bytes memory data,
bytes memory returnData
)
public
returns (bytes memory)
{
emit ExecutableCalled(
data,
returnData,
currentContextAddress
);
require(shouldSucceed, "EXECUTABLE_FAILED");
if (data.length != 0) {
(bool didSucceed, bytes memory callResultData) = address(this).delegatecall(data); // This is a delegatecall to preserve the `msg.sender` field
if (!didSucceed) {
assembly { revert(add(callResultData, 0x20), mload(callResultData)) }
}
}
return returnData;
}
function _isValidTransactionWithHashSignature(
LibZeroExTransaction.ZeroExTransaction memory,
bytes32,
bytes memory signature
)
internal
view
returns (bool)
{
if (
signature.length == 2 &&
signature[0] == 0x0 &&
signature[1] == 0x0
) {
return false;
}
return true;
}
}

View File

@@ -35,7 +35,7 @@
"compile:truffle": "truffle compile"
},
"config": {
"abis": "./generated-artifacts/@(Exchange|ExchangeWrapper|IAssetProxy|IAssetProxyDispatcher|IEIP1271Wallet|IExchange|IExchangeCore|IMatchOrders|ISignatureValidator|ITransactions|ITransferSimulator|IWallet|IWrapperFunctions|IsolatedExchange|LibExchangeRichErrorDecoder|MixinAssetProxyDispatcher|MixinExchangeCore|MixinMatchOrders|MixinSignatureValidator|MixinTransactions|MixinTransferSimulator|MixinWrapperFunctions|ReentrancyTester|TestAssetProxyDispatcher|TestExchangeInternals|TestLibExchangeRichErrorDecoder|TestSignatureValidator|TestValidatorWallet|TestWrapperFunctions|Whitelist).json",
"abis": "./generated-artifacts/@(Exchange|ExchangeWrapper|IAssetProxy|IAssetProxyDispatcher|IEIP1271Wallet|IExchange|IExchangeCore|IMatchOrders|ISignatureValidator|ITransactions|ITransferSimulator|IWallet|IWrapperFunctions|IsolatedExchange|LibExchangeRichErrorDecoder|MixinAssetProxyDispatcher|MixinExchangeCore|MixinMatchOrders|MixinSignatureValidator|MixinTransactions|MixinTransferSimulator|MixinWrapperFunctions|ReentrancyTester|TestAssetProxyDispatcher|TestExchangeInternals|TestLibExchangeRichErrorDecoder|TestSignatureValidator|TestTransactions|TestValidatorWallet|TestWrapperFunctions|Whitelist).json",
"abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually."
},
"repository": {

View File

@@ -14,11 +14,11 @@ import * as IExchange from '../generated-artifacts/IExchange.json';
import * as IExchangeCore from '../generated-artifacts/IExchangeCore.json';
import * as IMatchOrders from '../generated-artifacts/IMatchOrders.json';
import * as ISignatureValidator from '../generated-artifacts/ISignatureValidator.json';
import * as IsolatedExchange from '../generated-artifacts/IsolatedExchange.json';
import * as ITransactions from '../generated-artifacts/ITransactions.json';
import * as ITransferSimulator from '../generated-artifacts/ITransferSimulator.json';
import * as IWallet from '../generated-artifacts/IWallet.json';
import * as IWrapperFunctions from '../generated-artifacts/IWrapperFunctions.json';
import * as IsolatedExchange from '../generated-artifacts/IsolatedExchange.json';
import * as LibExchangeRichErrorDecoder from '../generated-artifacts/LibExchangeRichErrorDecoder.json';
import * as MixinAssetProxyDispatcher from '../generated-artifacts/MixinAssetProxyDispatcher.json';
import * as MixinExchangeCore from '../generated-artifacts/MixinExchangeCore.json';
@@ -32,6 +32,7 @@ import * as TestAssetProxyDispatcher from '../generated-artifacts/TestAssetProxy
import * as TestExchangeInternals from '../generated-artifacts/TestExchangeInternals.json';
import * as TestLibExchangeRichErrorDecoder from '../generated-artifacts/TestLibExchangeRichErrorDecoder.json';
import * as TestSignatureValidator from '../generated-artifacts/TestSignatureValidator.json';
import * as TestTransactions from '../generated-artifacts/TestTransactions.json';
import * as TestValidatorWallet from '../generated-artifacts/TestValidatorWallet.json';
import * as TestWrapperFunctions from '../generated-artifacts/TestWrapperFunctions.json';
import * as Whitelist from '../generated-artifacts/Whitelist.json';
@@ -64,6 +65,7 @@ export const artifacts = {
TestExchangeInternals: TestExchangeInternals as ContractArtifact,
TestLibExchangeRichErrorDecoder: TestLibExchangeRichErrorDecoder as ContractArtifact,
TestSignatureValidator: TestSignatureValidator as ContractArtifact,
TestTransactions: TestTransactions as ContractArtifact,
TestValidatorWallet: TestValidatorWallet as ContractArtifact,
TestWrapperFunctions: TestWrapperFunctions as ContractArtifact,
};

View File

@@ -30,6 +30,7 @@ export * from '../generated-wrappers/test_asset_proxy_dispatcher';
export * from '../generated-wrappers/test_exchange_internals';
export * from '../generated-wrappers/test_lib_exchange_rich_error_decoder';
export * from '../generated-wrappers/test_signature_validator';
export * from '../generated-wrappers/test_transactions';
export * from '../generated-wrappers/test_validator_wallet';
export * from '../generated-wrappers/test_wrapper_functions';
export * from '../generated-wrappers/whitelist';

View File

@@ -177,6 +177,48 @@ blockchainTests.resets('Exchange transactions', env => {
const tx = exchangeWrapper.executeTransactionAsync(transaction, senderAddress);
return expect(tx).to.revertWith(expectedError);
});
it('should revert if the actual gasPrice is greater than expected', async () => {
const order = await orderFactory.newSignedOrderAsync();
const orders = [order];
const data = exchangeDataEncoder.encodeOrdersToExchangeData(ExchangeFunctionName.FillOrder, orders);
const transaction = await takerTransactionFactory.newSignedTransactionAsync({
data,
});
const transactionHashHex = transactionHashUtils.getTransactionHashHex(transaction);
const actualGasPrice = transaction.gasPrice.plus(1);
const expectedError = new ExchangeRevertErrors.TransactionGasPriceError(
transactionHashHex,
actualGasPrice,
transaction.gasPrice,
);
const tx = exchangeInstance.executeTransaction.sendTransactionAsync(
transaction,
transaction.signature,
{ gasPrice: actualGasPrice, from: senderAddress },
);
return expect(tx).to.revertWith(expectedError);
});
it('should revert if the actual gasPrice is less than expected', async () => {
const order = await orderFactory.newSignedOrderAsync();
const orders = [order];
const data = exchangeDataEncoder.encodeOrdersToExchangeData(ExchangeFunctionName.FillOrder, orders);
const transaction = await takerTransactionFactory.newSignedTransactionAsync({
data,
});
const transactionHashHex = transactionHashUtils.getTransactionHashHex(transaction);
const actualGasPrice = transaction.gasPrice.minus(1);
const expectedError = new ExchangeRevertErrors.TransactionGasPriceError(
transactionHashHex,
actualGasPrice,
transaction.gasPrice,
);
const tx = exchangeInstance.executeTransaction.sendTransactionAsync(
transaction,
transaction.signature,
{ gasPrice: actualGasPrice, from: senderAddress },
);
return expect(tx).to.revertWith(expectedError);
});
});
describe('fill methods', () => {
for (const fnName of [
@@ -307,9 +349,9 @@ blockchainTests.resets('Exchange transactions', env => {
const recursiveTransactionHashHex = transactionHashUtils.getTransactionHashHex(
recursiveTransaction,
);
const noReentrancyError = new ExchangeRevertErrors.TransactionError(
ExchangeRevertErrors.TransactionErrorCode.NoReentrancy,
const noReentrancyError = new ExchangeRevertErrors.TransactionInvalidContextError(
transactionHashHex,
transaction.signerAddress,
).encode();
const expectedError = new ExchangeRevertErrors.TransactionExecutionError(
recursiveTransactionHashHex,

View File

@@ -0,0 +1,715 @@
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: {
verifyingContractAddress: 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 = {
verifyingContractAddress: 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<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.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<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 [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<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.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.TransactionSignatureError(
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<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 [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<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.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.TransactionSignatureError(
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('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

View File

@@ -30,6 +30,7 @@
"generated-artifacts/TestExchangeInternals.json",
"generated-artifacts/TestLibExchangeRichErrorDecoder.json",
"generated-artifacts/TestSignatureValidator.json",
"generated-artifacts/TestTransactions.json",
"generated-artifacts/TestValidatorWallet.json",
"generated-artifacts/TestWrapperFunctions.json",
"generated-artifacts/Whitelist.json"

View File

@@ -68,4 +68,5 @@ export const constants = {
ONE_ETHER: new BigNumber(1e18),
EIP712_DOMAIN_NAME: '0x Protocol',
EIP712_DOMAIN_VERSION: '3.0.0',
DEFAULT_GAS_PRICE: 1,
};

View File

@@ -4,6 +4,7 @@ import { BigNumber } from '@0x/utils';
import * as ethUtil from 'ethereumjs-util';
import { getLatestBlockTimestampAsync } from './block_timestamp';
import { constants } from './constants';
import { signingUtils } from './signing_utils';
export class TransactionFactory {
@@ -34,6 +35,7 @@ export class TransactionFactory {
signerAddress,
data: customTransactionParams.data,
expirationTimeSeconds: new BigNumber(currentBlockTimestamp).plus(tenMinutesInSeconds),
gasPrice: new BigNumber(constants.DEFAULT_GAS_PRICE),
domain: {
verifyingContractAddress: this._exchangeAddress,
chainId: this._chainId,

View File

@@ -4,6 +4,7 @@ import { logUtils } from '@0x/utils';
import { Web3Wrapper } from '@0x/web3-wrapper';
import * as _ from 'lodash';
import { constants } from './constants';
import { coverage } from './coverage';
import { profiler } from './profiler';
import { revertTrace } from './revert_trace';
@@ -31,6 +32,7 @@ switch (process.env.TEST_PROVIDER) {
const ganacheTxDefaults = {
from: devConstants.TESTRPC_FIRST_ADDRESS,
gas: devConstants.GAS_LIMIT,
gasPrice: constants.DEFAULT_GAS_PRICE,
};
const gethTxDefaults = {
from: devConstants.TESTRPC_FIRST_ADDRESS,

View File

@@ -5,8 +5,9 @@
"signerAddress": { "$ref": "/addressSchema" },
"salt": { "$ref": "/wholeNumberSchema" },
"expirationTimeSeconds": { "$ref": "/wholeNumberSchema" },
"gasPrice": { "$ref": "/wholeNumberSchema" },
"domain": { "$ref": "/eip712DomainSchema" }
},
"required": ["data", "salt", "expirationTimeSeconds", "signerAddress", "domain"],
"required": ["data", "salt", "expirationTimeSeconds", "gasPrice", "signerAddress", "domain"],
"type": "object"
}

View File

@@ -134,6 +134,7 @@ export const constants = {
parameters: [
{ name: 'salt', type: 'uint256' },
{ name: 'expirationTimeSeconds', type: 'uint256' },
{ name: 'gasPrice', type: 'uint256' },
{ name: 'signerAddress', type: 'address' },
{ name: 'data', type: 'bytes' },
],

View File

@@ -33,7 +33,6 @@ export enum AssetProxyDispatchErrorCode {
}
export enum TransactionErrorCode {
NoReentrancy,
AlreadyExecuted,
Expired,
}
@@ -136,7 +135,7 @@ export class FillError extends RevertError {
}
export class OrderEpochError extends RevertError {
constructor(maker?: string, sender?: string, currentEpoch?: BigNumber | number | string) {
constructor(maker?: string, sender?: string, currentEpoch?: BigNumber) {
super('OrderEpochError', 'OrderEpochError(address maker, address sender, uint256 currentEpoch)', {
maker,
sender,
@@ -213,6 +212,33 @@ export class TransactionExecutionError extends RevertError {
}
}
export class TransactionGasPriceError extends RevertError {
constructor(transactionHash?: string, actualGasPrice?: BigNumber, requiredGasPrice?: BigNumber) {
super(
'TransactionGasPriceError',
'TransactionGasPriceError(bytes32 transactionHash, uint256 actualGasPrice, uint256 requiredGasPrice)',
{
transactionHash,
actualGasPrice,
requiredGasPrice,
},
);
}
}
export class TransactionInvalidContextError extends RevertError {
constructor(transactionHash?: string, currentContextAddress?: string) {
super(
'TransactionInvalidContextError',
'TransactionInvalidContextError(bytes32 transactionHash, address currentContextAddress)',
{
transactionHash,
currentContextAddress,
},
);
}
}
export class IncompleteFillError extends RevertError {
constructor(
error?: IncompleteFillErrorCode,

View File

@@ -59,6 +59,7 @@ describe('EIP712 Utils', () => {
const typedData = eip712Utils.createZeroExTransactionTypedData({
salt: new BigNumber(0),
expirationTimeSeconds: new BigNumber(0),
gasPrice: new BigNumber(0),
data: constants.NULL_BYTES,
signerAddress: constants.NULL_ADDRESS,
domain: {

View File

@@ -55,6 +55,7 @@ describe('Signature utils', () => {
signerAddress: makerAddress,
data: '0x6927e990021d23b1eb7b8789f6a6feaf98fe104bb0cf8259421b79f9a34222b0',
expirationTimeSeconds: new BigNumber(0),
gasPrice: new BigNumber(0),
};
});
describe('#isValidSignatureAsync', () => {

View File

@@ -14,13 +14,14 @@ const expect = chai.expect;
describe('0x transaction hashing', () => {
describe('#getTransactionHashHex', () => {
const expectedTransactionHash = '0x9779e4ca195f8c9c6f137f495599e9a1944806310b64748479bfa6c6b1ae7eb4';
const expectedTransactionHash = '0x420b19f08d5b09c012f381f4bf80a97740b8629f2bac7f42dd7f6aefbb24f3c0';
const fakeVerifyingContractAddress = '0x5e72914535f202659083db3a02c984188fa26e9f';
const fakeChainId = 1337;
const transaction: ZeroExTransaction = {
signerAddress: constants.NULL_ADDRESS,
salt: new BigNumber(0),
expirationTimeSeconds: new BigNumber(0),
gasPrice: new BigNumber(0),
data: constants.NULL_BYTES,
domain: {
verifyingContractAddress: fakeVerifyingContractAddress,
@@ -40,6 +41,7 @@ describe('0x transaction hashing', () => {
...transaction,
salt: '0',
expirationTimeSeconds: '0',
gasPrice: '0',
} as any);
expect(transactionHash).to.be.equal(expectedTransactionHash);
});

View File

@@ -56,6 +56,7 @@ export enum MarketOperation {
export interface ZeroExTransaction {
salt: BigNumber;
expirationTimeSeconds: BigNumber;
gasPrice: BigNumber;
signerAddress: string;
data: string;
domain: EIP712DomainWithDefaultSchema;