aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLeonid <logvinov.leon@gmail.com>2017-10-10 19:15:57 +0800
committerGitHub <noreply@github.com>2017-10-10 19:15:57 +0800
commit056e0f26abb36774a3f6cbdb4429eaeff735a270 (patch)
tree77a413fe5b26e1993e890a19c15830b2e8103615
parentf38d2f80a6e3af4ff7e2454d42abdf2a389e4303 (diff)
parentbda979a6c7e825446adce2d6d3b4800b5012a08d (diff)
downloaddexon-sol-tools-056e0f26abb36774a3f6cbdb4429eaeff735a270.tar
dexon-sol-tools-056e0f26abb36774a3f6cbdb4429eaeff735a270.tar.gz
dexon-sol-tools-056e0f26abb36774a3f6cbdb4429eaeff735a270.tar.bz2
dexon-sol-tools-056e0f26abb36774a3f6cbdb4429eaeff735a270.tar.lz
dexon-sol-tools-056e0f26abb36774a3f6cbdb4429eaeff735a270.tar.xz
dexon-sol-tools-056e0f26abb36774a3f6cbdb4429eaeff735a270.tar.zst
dexon-sol-tools-056e0f26abb36774a3f6cbdb4429eaeff735a270.zip
Merge pull request #185 from 0xProject/fix/batch-validation
Fix batch validation
-rw-r--r--CHANGELOG.md1
-rw-r--r--src/contract_wrappers/exchange_wrapper.ts67
-rw-r--r--src/types.ts10
-rw-r--r--src/utils/assert.ts2
-rw-r--r--src/utils/exchange_transfer_simulator.ts131
-rw-r--r--src/utils/order_validation_utils.ts153
-rw-r--r--test/exchange_transfer_simulator_test.ts91
-rw-r--r--test/order_validation_test.ts279
8 files changed, 382 insertions, 352 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2c160ee47..2aae081bf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,7 @@ v0.21.0 - _TBD, 2017_
* Added `zeroEx.token.unsubscribe` and `zeroEx.exchange.unsubscribe`
* Renamed `zeroEx.exchange.stopWatchingAllEventsAsync` to `zeroEx.exhange.unsubscribeAll`
* Renamed `zeroEx.token.stopWatchingAllEventsAsync` to `zeroEx.token.unsubscribeAll`
+ * Fixed the batch fills validation by emulating all balance & proxy allowance changes (#185)
v0.20.0 - _October 5, 2017_
------------------------
diff --git a/src/contract_wrappers/exchange_wrapper.ts b/src/contract_wrappers/exchange_wrapper.ts
index 5f02903ce..9e4936de6 100644
--- a/src/contract_wrappers/exchange_wrapper.ts
+++ b/src/contract_wrappers/exchange_wrapper.ts
@@ -36,6 +36,7 @@ import {ContractWrapper} from './contract_wrapper';
import {TokenWrapper} from './token_wrapper';
import {decorators} from '../utils/decorators';
import {AbiDecoder} from '../utils/abi_decoder';
+import {ExchangeTransferSimulator} from '../utils/exchange_transfer_simulator';
import {artifacts} from '../artifacts';
const SHOULD_VALIDATE_BY_DEFAULT = true;
@@ -173,7 +174,10 @@ export class ExchangeWrapper extends ContractWrapper {
SHOULD_VALIDATE_BY_DEFAULT :
orderTransactionOpts.shouldValidate;
if (shouldValidate) {
- await this.validateFillOrderThrowIfInvalidAsync(signedOrder, fillTakerTokenAmount, takerAddress);
+ const zrxTokenAddress = await this.getZRXTokenAddressAsync();
+ const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper);
+ await this._orderValidationUtils.validateFillOrderThrowIfInvalidAsync(
+ exchangeTradeEmulator, signedOrder, fillTakerTokenAmount, takerAddress, zrxTokenAddress);
}
const [orderAddresses, orderValues] = ExchangeWrapper._getOrderAddressesAndValues(signedOrder);
@@ -242,8 +246,12 @@ export class ExchangeWrapper extends ContractWrapper {
SHOULD_VALIDATE_BY_DEFAULT :
orderTransactionOpts.shouldValidate;
if (shouldValidate) {
- await Promise.all(signedOrders.map(signedOrder => this.validateFillOrderThrowIfInvalidAsync(
- signedOrder, fillTakerTokenAmount, takerAddress)));
+ const zrxTokenAddress = await this.getZRXTokenAddressAsync();
+ const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper);
+ for (const signedOrder of signedOrders) {
+ await this._orderValidationUtils.validateFillOrderThrowIfInvalidAsync(
+ exchangeTradeEmulator, signedOrder, fillTakerTokenAmount, takerAddress, zrxTokenAddress);
+ }
}
if (_.isEmpty(signedOrders)) {
@@ -328,9 +336,14 @@ export class ExchangeWrapper extends ContractWrapper {
SHOULD_VALIDATE_BY_DEFAULT :
orderTransactionOpts.shouldValidate;
if (shouldValidate) {
- await Promise.all(orderFillRequests.map(orderFillRequest => this.validateFillOrderThrowIfInvalidAsync(
- orderFillRequest.signedOrder, orderFillRequest.takerTokenFillAmount, takerAddress)),
- );
+ const zrxTokenAddress = await this.getZRXTokenAddressAsync();
+ const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper);
+ for (const orderFillRequest of orderFillRequests) {
+ await this._orderValidationUtils.validateFillOrderThrowIfInvalidAsync(
+ exchangeTradeEmulator, orderFillRequest.signedOrder, orderFillRequest.takerTokenFillAmount,
+ takerAddress, zrxTokenAddress,
+ );
+ }
}
if (_.isEmpty(orderFillRequests)) {
throw new Error(ExchangeContractErrs.BatchOrdersMustHaveAtLeastOneItem);
@@ -403,7 +416,10 @@ export class ExchangeWrapper extends ContractWrapper {
SHOULD_VALIDATE_BY_DEFAULT :
orderTransactionOpts.shouldValidate;
if (shouldValidate) {
- await this.validateFillOrKillOrderThrowIfInvalidAsync(signedOrder, fillTakerTokenAmount, takerAddress);
+ const zrxTokenAddress = await this.getZRXTokenAddressAsync();
+ const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper);
+ await this._orderValidationUtils.validateFillOrKillOrderThrowIfInvalidAsync(
+ exchangeTradeEmulator, signedOrder, fillTakerTokenAmount, takerAddress, zrxTokenAddress);
}
const [orderAddresses, orderValues] = ExchangeWrapper._getOrderAddressesAndValues(signedOrder);
@@ -464,9 +480,14 @@ export class ExchangeWrapper extends ContractWrapper {
SHOULD_VALIDATE_BY_DEFAULT :
orderTransactionOpts.shouldValidate;
if (shouldValidate) {
- await Promise.all(orderFillOrKillRequests.map(request => this.validateFillOrKillOrderThrowIfInvalidAsync(
- request.signedOrder, request.fillTakerAmount, takerAddress)),
- );
+ const zrxTokenAddress = await this.getZRXTokenAddressAsync();
+ const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper);
+ for (const orderFillOrKillRequest of orderFillOrKillRequests) {
+ await this._orderValidationUtils.validateFillOrKillOrderThrowIfInvalidAsync(
+ exchangeTradeEmulator, orderFillOrKillRequest.signedOrder, orderFillOrKillRequest.fillTakerAmount,
+ takerAddress, zrxTokenAddress,
+ );
+ }
}
const orderAddressesValuesAndTakerTokenFillAmounts = _.map(orderFillOrKillRequests, request => {
@@ -530,7 +551,10 @@ export class ExchangeWrapper extends ContractWrapper {
SHOULD_VALIDATE_BY_DEFAULT :
orderTransactionOpts.shouldValidate;
if (shouldValidate) {
- await this.validateCancelOrderThrowIfInvalidAsync(order, cancelTakerTokenAmount);
+ const orderHash = utils.getOrderHashHex(order);
+ const unavailableTakerTokenAmount = await this.getUnavailableTakerAmountAsync(orderHash);
+ await this._orderValidationUtils.validateCancelOrderThrowIfInvalidAsync(
+ order, cancelTakerTokenAmount, unavailableTakerTokenAmount);
}
const [orderAddresses, orderValues] = ExchangeWrapper._getOrderAddressesAndValues(order);
@@ -580,9 +604,15 @@ export class ExchangeWrapper extends ContractWrapper {
SHOULD_VALIDATE_BY_DEFAULT :
orderTransactionOpts.shouldValidate;
if (shouldValidate) {
- await Promise.all(orderCancellationRequests.map(cancellationRequest =>
- this.validateCancelOrderThrowIfInvalidAsync(
- cancellationRequest.order, cancellationRequest.takerTokenCancelAmount)));
+ for (const orderCancellationRequest of orderCancellationRequests) {
+ const orderHash = utils.getOrderHashHex(orderCancellationRequest.order);
+ const unavailableTakerTokenAmount = await this.getUnavailableTakerAmountAsync(orderHash);
+ await this._orderValidationUtils.validateCancelOrderThrowIfInvalidAsync(
+ orderCancellationRequest.order, orderCancellationRequest.takerTokenCancelAmount,
+ unavailableTakerTokenAmount,
+ );
+ }
+
}
if (_.isEmpty(orderCancellationRequests)) {
throw new Error(ExchangeContractErrs.BatchOrdersMustHaveAtLeastOneItem);
@@ -688,8 +718,9 @@ export class ExchangeWrapper extends ContractWrapper {
assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema);
const zrxTokenAddress = await this.getZRXTokenAddressAsync();
const expectedFillTakerTokenAmount = !_.isUndefined(opts) ? opts.expectedFillTakerTokenAmount : undefined;
+ const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper);
await this._orderValidationUtils.validateOrderFillableOrThrowAsync(
- signedOrder, zrxTokenAddress, expectedFillTakerTokenAmount,
+ exchangeTradeEmulator, signedOrder, zrxTokenAddress, expectedFillTakerTokenAmount,
);
}
/**
@@ -707,8 +738,9 @@ export class ExchangeWrapper extends ContractWrapper {
assert.isBigNumber('fillTakerTokenAmount', fillTakerTokenAmount);
await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper);
const zrxTokenAddress = await this.getZRXTokenAddressAsync();
+ const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper);
await this._orderValidationUtils.validateFillOrderThrowIfInvalidAsync(
- signedOrder, fillTakerTokenAmount, takerAddress, zrxTokenAddress);
+ exchangeTradeEmulator, signedOrder, fillTakerTokenAmount, takerAddress, zrxTokenAddress);
}
/**
* Checks if cancelling a given order will succeed and throws an informative error if it won't.
@@ -740,8 +772,9 @@ export class ExchangeWrapper extends ContractWrapper {
assert.isBigNumber('fillTakerTokenAmount', fillTakerTokenAmount);
await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper);
const zrxTokenAddress = await this.getZRXTokenAddressAsync();
+ const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper);
await this._orderValidationUtils.validateFillOrKillOrderThrowIfInvalidAsync(
- signedOrder, fillTakerTokenAmount, takerAddress, zrxTokenAddress);
+ exchangeTradeEmulator, signedOrder, fillTakerTokenAmount, takerAddress, zrxTokenAddress);
}
/**
* Checks if rounding error will be > 0.1% when computing makerTokenAmount by doing:
diff --git a/src/types.ts b/src/types.ts
index 44094f442..8c0bc1cf9 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -466,3 +466,13 @@ export interface OrderTransactionOpts {
}
export type FilterObject = Web3.FilterObject;
+
+export enum TradeSide {
+ Maker = 'maker',
+ Taker = 'taker',
+}
+
+export enum TransferType {
+ Trade = 'trade',
+ Fee = 'fee',
+}
diff --git a/src/utils/assert.ts b/src/utils/assert.ts
index 0b7a11939..099f4490f 100644
--- a/src/utils/assert.ts
+++ b/src/utils/assert.ts
@@ -28,7 +28,7 @@ export const assert = {
const web3 = new Web3();
this.assert(web3.isAddress(value), this.typeAssertionMessage(variableName, 'ETHAddressHex', value));
this.assert(
- web3.isAddress(value) && !web3.isChecksumAddress(value),
+ web3.isAddress(value) && value.toLowerCase() === value,
`Checksummed addresses are not supported. Convert ${variableName} to lower case before passing`,
);
},
diff --git a/src/utils/exchange_transfer_simulator.ts b/src/utils/exchange_transfer_simulator.ts
new file mode 100644
index 000000000..db12abd29
--- /dev/null
+++ b/src/utils/exchange_transfer_simulator.ts
@@ -0,0 +1,131 @@
+import * as _ from 'lodash';
+import {ExchangeContractErrs, TradeSide, TransferType} from '../types';
+import {TokenWrapper} from '../contract_wrappers/token_wrapper';
+
+enum FailureReason {
+ Balance = 'balance',
+ ProxyAllowance = 'proxyAllowance',
+}
+
+const ERR_MSG_MAPPING = {
+ [FailureReason.Balance]: {
+ [TradeSide.Maker]: {
+ [TransferType.Trade]: ExchangeContractErrs.InsufficientMakerBalance,
+ [TransferType.Fee]: ExchangeContractErrs.InsufficientMakerFeeBalance,
+ },
+ [TradeSide.Taker]: {
+ [TransferType.Trade]: ExchangeContractErrs.InsufficientTakerBalance,
+ [TransferType.Fee]: ExchangeContractErrs.InsufficientTakerFeeBalance,
+ },
+ },
+ [FailureReason.ProxyAllowance]: {
+ [TradeSide.Maker]: {
+ [TransferType.Trade]: ExchangeContractErrs.InsufficientMakerAllowance,
+ [TransferType.Fee]: ExchangeContractErrs.InsufficientMakerFeeAllowance,
+ },
+ [TradeSide.Taker]: {
+ [TransferType.Trade]: ExchangeContractErrs.InsufficientTakerAllowance,
+ [TransferType.Fee]: ExchangeContractErrs.InsufficientTakerFeeAllowance,
+ },
+ },
+};
+
+/**
+ * Copy on read store for balances/proxyAllowances of tokens/accounts touched in trades
+ */
+export class BalanceAndProxyAllowanceLazyStore {
+ protected _token: TokenWrapper;
+ private _balance: {
+ [tokenAddress: string]: {
+ [userAddress: string]: BigNumber.BigNumber,
+ },
+ };
+ private _proxyAllowance: {
+ [tokenAddress: string]: {
+ [userAddress: string]: BigNumber.BigNumber,
+ },
+ };
+ constructor(token: TokenWrapper) {
+ this._token = token;
+ this._balance = {};
+ this._proxyAllowance = {};
+ }
+ protected async getBalanceAsync(tokenAddress: string, userAddress: string): Promise<BigNumber.BigNumber> {
+ if (_.isUndefined(this._balance[tokenAddress]) || _.isUndefined(this._balance[tokenAddress][userAddress])) {
+ const balance = await this._token.getBalanceAsync(tokenAddress, userAddress);
+ this.setBalance(tokenAddress, userAddress, balance);
+ }
+ const cachedBalance = this._balance[tokenAddress][userAddress];
+ return cachedBalance;
+ }
+ protected setBalance(tokenAddress: string, userAddress: string, balance: BigNumber.BigNumber): void {
+ if (_.isUndefined(this._balance[tokenAddress])) {
+ this._balance[tokenAddress] = {};
+ }
+ this._balance[tokenAddress][userAddress] = balance;
+ }
+ protected async getProxyAllowanceAsync(tokenAddress: string, userAddress: string): Promise<BigNumber.BigNumber> {
+ if (_.isUndefined(this._proxyAllowance[tokenAddress]) ||
+ _.isUndefined(this._proxyAllowance[tokenAddress][userAddress])) {
+ const proxyAllowance = await this._token.getProxyAllowanceAsync(tokenAddress, userAddress);
+ this.setProxyAllowance(tokenAddress, userAddress, proxyAllowance);
+ }
+ const cachedProxyAllowance = this._proxyAllowance[tokenAddress][userAddress];
+ return cachedProxyAllowance;
+ }
+ protected setProxyAllowance(tokenAddress: string, userAddress: string, proxyAllowance: BigNumber.BigNumber): void {
+ if (_.isUndefined(this._proxyAllowance[tokenAddress])) {
+ this._proxyAllowance[tokenAddress] = {};
+ }
+ this._proxyAllowance[tokenAddress][userAddress] = proxyAllowance;
+ }
+}
+
+export class ExchangeTransferSimulator extends BalanceAndProxyAllowanceLazyStore {
+ /**
+ * Simulates transferFrom call performed by a proxy
+ * @param tokenAddress Address of the token to be transferred
+ * @param from Owner of the transferred tokens
+ * @param to Recipient of the transferred tokens
+ * @param amountInBaseUnits The amount of tokens being transferred
+ * @param tradeSide Is Maker/Taker transferring
+ * @param transferType Is it a fee payment or a value transfer
+ */
+ public async transferFromAsync(tokenAddress: string, from: string, to: string,
+ amountInBaseUnits: BigNumber.BigNumber, tradeSide: TradeSide,
+ transferType: TransferType): Promise<void> {
+ const balance = await this.getBalanceAsync(tokenAddress, from);
+ const proxyAllowance = await this.getProxyAllowanceAsync(tokenAddress, from);
+ if (proxyAllowance.lessThan(amountInBaseUnits)) {
+ this.throwValidationError(FailureReason.ProxyAllowance, tradeSide, transferType);
+ }
+ if (balance.lessThan(amountInBaseUnits)) {
+ this.throwValidationError(FailureReason.Balance, tradeSide, transferType);
+ }
+ await this.decreaseProxyAllowanceAsync(tokenAddress, from, amountInBaseUnits);
+ await this.decreaseBalanceAsync(tokenAddress, from, amountInBaseUnits);
+ await this.increaseBalanceAsync(tokenAddress, to, amountInBaseUnits);
+ }
+ private async decreaseProxyAllowanceAsync(tokenAddress: string, userAddress: string,
+ amountInBaseUnits: BigNumber.BigNumber): Promise<void> {
+ const proxyAllowance = await this.getProxyAllowanceAsync(tokenAddress, userAddress);
+ if (!proxyAllowance.eq(this._token.UNLIMITED_ALLOWANCE_IN_BASE_UNITS)) {
+ this.setProxyAllowance(tokenAddress, userAddress, proxyAllowance.minus(amountInBaseUnits));
+ }
+ }
+ private async increaseBalanceAsync(tokenAddress: string, userAddress: string,
+ amountInBaseUnits: BigNumber.BigNumber): Promise<void> {
+ const balance = await this.getBalanceAsync(tokenAddress, userAddress);
+ this.setBalance(tokenAddress, userAddress, balance.plus(amountInBaseUnits));
+ }
+ private async decreaseBalanceAsync(tokenAddress: string, userAddress: string,
+ amountInBaseUnits: BigNumber.BigNumber): Promise<void> {
+ const balance = await this.getBalanceAsync(tokenAddress, userAddress);
+ this.setBalance(tokenAddress, userAddress, balance.minus(amountInBaseUnits));
+ }
+ private throwValidationError(failureReason: FailureReason, tradeSide: TradeSide,
+ transferType: TransferType): Promise<never> {
+ const errMsg = ERR_MSG_MAPPING[failureReason][tradeSide][transferType];
+ throw new Error(errMsg);
+ }
+}
diff --git a/src/utils/order_validation_utils.ts b/src/utils/order_validation_utils.ts
index 0450d26a6..5d14602db 100644
--- a/src/utils/order_validation_utils.ts
+++ b/src/utils/order_validation_utils.ts
@@ -1,10 +1,11 @@
import * as _ from 'lodash';
-import {ExchangeContractErrs, SignedOrder, Order, ZeroExError} from '../types';
+import {ExchangeContractErrs, SignedOrder, Order, ZeroExError, TradeSide, TransferType} from '../types';
import {ZeroEx} from '../0x';
import {TokenWrapper} from '../contract_wrappers/token_wrapper';
import {ExchangeWrapper} from '../contract_wrappers/exchange_wrapper';
import {utils} from '../utils/utils';
import {constants} from '../utils/constants';
+import {ExchangeTransferSimulator} from './exchange_transfer_simulator';
export class OrderValidationUtils {
private tokenWrapper: TokenWrapper;
@@ -14,8 +15,8 @@ export class OrderValidationUtils {
this.exchangeWrapper = exchangeWrapper;
}
public async validateOrderFillableOrThrowAsync(
- signedOrder: SignedOrder, zrxTokenAddress: string, expectedFillTakerTokenAmount?: BigNumber.BigNumber,
- ): Promise<void> {
+ exchangeTradeEmulator: ExchangeTransferSimulator, signedOrder: SignedOrder, zrxTokenAddress: string,
+ expectedFillTakerTokenAmount?: BigNumber.BigNumber): Promise<void> {
const orderHash = utils.getOrderHashHex(signedOrder);
const unavailableTakerTokenAmount = await this.exchangeWrapper.getUnavailableTakerAmountAsync(orderHash);
this.validateRemainingFillAmountNotZeroOrThrow(
@@ -26,14 +27,20 @@ export class OrderValidationUtils {
if (!_.isUndefined(expectedFillTakerTokenAmount)) {
fillTakerTokenAmount = expectedFillTakerTokenAmount;
}
- await this.validateFillOrderMakerBalancesAllowancesThrowIfInvalidAsync(
- signedOrder, fillTakerTokenAmount, zrxTokenAddress,
+ const fillMakerTokenAmount = this.getFillMakerTokenAmount(signedOrder, fillTakerTokenAmount);
+ await exchangeTradeEmulator.transferFromAsync(
+ signedOrder.makerTokenAddress, signedOrder.maker, signedOrder.taker, fillMakerTokenAmount,
+ TradeSide.Maker, TransferType.Trade,
+ );
+ await exchangeTradeEmulator.transferFromAsync(
+ zrxTokenAddress, signedOrder.maker, signedOrder.feeRecipient, signedOrder.makerFee,
+ TradeSide.Maker, TransferType.Fee,
);
}
- public async validateFillOrderThrowIfInvalidAsync(signedOrder: SignedOrder,
- fillTakerTokenAmount: BigNumber.BigNumber,
- takerAddress: string,
- zrxTokenAddress: string): Promise<void> {
+ public async validateFillOrderThrowIfInvalidAsync(
+ exchangeTradeEmulator: ExchangeTransferSimulator, signedOrder: SignedOrder,
+ fillTakerTokenAmount: BigNumber.BigNumber, takerAddress: string,
+ zrxTokenAddress: string): Promise<BigNumber.BigNumber> {
if (fillTakerTokenAmount.eq(0)) {
throw new Error(ExchangeContractErrs.OrderFillAmountZero);
}
@@ -49,29 +56,29 @@ export class OrderValidationUtils {
throw new Error(ExchangeContractErrs.TransactionSenderIsNotFillOrderTaker);
}
this.validateOrderNotExpiredOrThrow(signedOrder.expirationUnixTimestampSec);
+ const remainingTakerTokenAmount = signedOrder.takerTokenAmount.minus(unavailableTakerTokenAmount);
+ const filledTakerTokenAmount = remainingTakerTokenAmount.lessThan(fillTakerTokenAmount) ?
+ remainingTakerTokenAmount :
+ fillTakerTokenAmount;
await this.validateFillOrderBalancesAllowancesThrowIfInvalidAsync(
- signedOrder, fillTakerTokenAmount, takerAddress, zrxTokenAddress,
+ exchangeTradeEmulator, signedOrder, filledTakerTokenAmount, takerAddress, zrxTokenAddress,
);
const wouldRoundingErrorOccur = await this.exchangeWrapper.isRoundingErrorAsync(
- fillTakerTokenAmount, signedOrder.takerTokenAmount, signedOrder.makerTokenAmount,
+ filledTakerTokenAmount, signedOrder.takerTokenAmount, signedOrder.makerTokenAmount,
);
if (wouldRoundingErrorOccur) {
throw new Error(ExchangeContractErrs.OrderFillRoundingError);
}
+ return filledTakerTokenAmount;
}
- public async validateFillOrKillOrderThrowIfInvalidAsync(signedOrder: SignedOrder,
- fillTakerTokenAmount: BigNumber.BigNumber,
- takerAddress: string,
- zrxTokenAddress: string): Promise<void> {
- await this.validateFillOrderThrowIfInvalidAsync(
- signedOrder, fillTakerTokenAmount, takerAddress, zrxTokenAddress,
+ public async validateFillOrKillOrderThrowIfInvalidAsync(
+ exchangeTradeEmulator: ExchangeTransferSimulator, signedOrder: SignedOrder,
+ fillTakerTokenAmount: BigNumber.BigNumber, takerAddress: string, zrxTokenAddress: string): Promise<void> {
+ const filledTakerTokenAmount = await this.validateFillOrderThrowIfInvalidAsync(
+ exchangeTradeEmulator, signedOrder, fillTakerTokenAmount, takerAddress, zrxTokenAddress,
);
- // Check that fillValue available >= fillTakerAmount
- const orderHashHex = utils.getOrderHashHex(signedOrder);
- const unavailableTakerAmount = await this.exchangeWrapper.getUnavailableTakerAmountAsync(orderHashHex);
- const remainingTakerAmount = signedOrder.takerTokenAmount.minus(unavailableTakerAmount);
- if (remainingTakerAmount < fillTakerTokenAmount) {
+ if (filledTakerTokenAmount !== fillTakerTokenAmount) {
throw new Error(ExchangeContractErrs.InsufficientRemainingFillAmount);
}
}
@@ -91,87 +98,25 @@ export class OrderValidationUtils {
}
}
public async validateFillOrderBalancesAllowancesThrowIfInvalidAsync(
- signedOrder: SignedOrder, fillTakerAmount: BigNumber.BigNumber, senderAddress: string, zrxTokenAddress: string,
- ): Promise<void> {
- await this.validateFillOrderMakerBalancesAllowancesThrowIfInvalidAsync(
- signedOrder, fillTakerAmount, zrxTokenAddress,
+ exchangeTradeEmulator: ExchangeTransferSimulator, signedOrder: SignedOrder,
+ fillTakerTokenAmount: BigNumber.BigNumber, senderAddress: string, zrxTokenAddress: string): Promise<void> {
+ const fillMakerTokenAmount = this.getFillMakerTokenAmount(signedOrder, fillTakerTokenAmount);
+ await exchangeTradeEmulator.transferFromAsync(
+ signedOrder.makerTokenAddress, signedOrder.maker, signedOrder.taker, fillMakerTokenAmount,
+ TradeSide.Maker, TransferType.Trade,
);
- await this.validateFillOrderTakerBalancesAllowancesThrowIfInvalidAsync(
- signedOrder, fillTakerAmount, senderAddress, zrxTokenAddress,
+ await exchangeTradeEmulator.transferFromAsync(
+ signedOrder.takerTokenAddress, signedOrder.taker, signedOrder.maker, fillTakerTokenAmount,
+ TradeSide.Taker, TransferType.Trade,
+ );
+ await exchangeTradeEmulator.transferFromAsync(
+ zrxTokenAddress, signedOrder.maker, signedOrder.feeRecipient, signedOrder.makerFee, TradeSide.Maker,
+ TransferType.Fee,
+ );
+ await exchangeTradeEmulator.transferFromAsync(
+ zrxTokenAddress, signedOrder.taker, signedOrder.feeRecipient, signedOrder.takerFee, TradeSide.Taker,
+ TransferType.Fee,
);
- }
- private async validateFillOrderMakerBalancesAllowancesThrowIfInvalidAsync(
- signedOrder: SignedOrder, fillTakerAmount: BigNumber.BigNumber, zrxTokenAddress: string,
- ): Promise<void> {
- const makerBalance = await this.tokenWrapper.getBalanceAsync(signedOrder.makerTokenAddress, signedOrder.maker);
- const makerAllowance = await this.tokenWrapper.getProxyAllowanceAsync(
- signedOrder.makerTokenAddress, signedOrder.maker);
-
- const isMakerTokenZRX = signedOrder.makerTokenAddress === zrxTokenAddress;
- // exchangeRate is the price of one maker token denominated in taker tokens
- const exchangeRate = signedOrder.takerTokenAmount.div(signedOrder.makerTokenAmount);
- const fillMakerAmount = fillTakerAmount.div(exchangeRate);
-
- const requiredMakerAmount = isMakerTokenZRX ? fillMakerAmount.plus(signedOrder.makerFee) : fillMakerAmount;
- if (requiredMakerAmount.greaterThan(makerBalance)) {
- throw new Error(ExchangeContractErrs.InsufficientMakerBalance);
- }
- if (requiredMakerAmount.greaterThan(makerAllowance)) {
- throw new Error(ExchangeContractErrs.InsufficientMakerAllowance);
- }
-
- if (!isMakerTokenZRX) {
- let makerZRXBalance = await this.tokenWrapper.getBalanceAsync(zrxTokenAddress, signedOrder.maker);
- const isTakerTokenZRX = signedOrder.takerTokenAddress === zrxTokenAddress;
- if (isTakerTokenZRX) {
- makerZRXBalance = makerZRXBalance.plus(fillTakerAmount);
- }
- const makerZRXAllowance = await this.tokenWrapper.getProxyAllowanceAsync(
- zrxTokenAddress, signedOrder.maker);
-
- if (signedOrder.makerFee.greaterThan(makerZRXBalance)) {
- throw new Error(ExchangeContractErrs.InsufficientMakerFeeBalance);
- }
- if (signedOrder.makerFee.greaterThan(makerZRXAllowance)) {
- throw new Error(ExchangeContractErrs.InsufficientMakerFeeAllowance);
- }
- }
- }
- private async validateFillOrderTakerBalancesAllowancesThrowIfInvalidAsync(
- signedOrder: SignedOrder, fillTakerAmount: BigNumber.BigNumber, senderAddress: string, zrxTokenAddress: string,
- ): Promise<void> {
- const takerBalance = await this.tokenWrapper.getBalanceAsync(signedOrder.takerTokenAddress, senderAddress);
- const takerAllowance = await this.tokenWrapper.getProxyAllowanceAsync(
- signedOrder.takerTokenAddress, senderAddress);
-
- const isTakerTokenZRX = signedOrder.takerTokenAddress === zrxTokenAddress;
- // exchangeRate is the price of one maker token denominated in taker tokens
- const exchangeRate = signedOrder.takerTokenAmount.div(signedOrder.makerTokenAmount);
- const fillMakerAmount = fillTakerAmount.div(exchangeRate);
-
- const requiredTakerAmount = isTakerTokenZRX ? fillTakerAmount.plus(signedOrder.takerFee) : fillTakerAmount;
- if (requiredTakerAmount.greaterThan(takerBalance)) {
- throw new Error(ExchangeContractErrs.InsufficientTakerBalance);
- }
- if (requiredTakerAmount.greaterThan(takerAllowance)) {
- throw new Error(ExchangeContractErrs.InsufficientTakerAllowance);
- }
-
- if (!isTakerTokenZRX) {
- const isMakerTokenZRX = signedOrder.makerTokenAddress === zrxTokenAddress;
- let takerZRXBalance = await this.tokenWrapper.getBalanceAsync(zrxTokenAddress, senderAddress);
- if (isMakerTokenZRX) {
- takerZRXBalance = takerZRXBalance.plus(fillMakerAmount);
- }
- const takerZRXAllowance = await this.tokenWrapper.getProxyAllowanceAsync(zrxTokenAddress, senderAddress);
-
- if (signedOrder.takerFee.greaterThan(takerZRXBalance)) {
- throw new Error(ExchangeContractErrs.InsufficientTakerFeeBalance);
- }
- if (signedOrder.takerFee.greaterThan(takerZRXAllowance)) {
- throw new Error(ExchangeContractErrs.InsufficientTakerFeeAllowance);
- }
- }
}
private validateRemainingFillAmountNotZeroOrThrow(
takerTokenAmount: BigNumber.BigNumber, unavailableTakerTokenAmount: BigNumber.BigNumber,
@@ -186,4 +131,10 @@ export class OrderValidationUtils {
throw new Error(ExchangeContractErrs.OrderFillExpired);
}
}
+ private getFillMakerTokenAmount(signedOrder: Order,
+ fillTakerTokenAmount: BigNumber.BigNumber): BigNumber.BigNumber {
+ const exchangeRate = signedOrder.takerTokenAmount.div(signedOrder.makerTokenAmount);
+ const fillMakerTokenAmount = fillTakerTokenAmount.div(exchangeRate);
+ return fillMakerTokenAmount;
+ }
}
diff --git a/test/exchange_transfer_simulator_test.ts b/test/exchange_transfer_simulator_test.ts
new file mode 100644
index 000000000..6ad2c007c
--- /dev/null
+++ b/test/exchange_transfer_simulator_test.ts
@@ -0,0 +1,91 @@
+import * as chai from 'chai';
+import * as BigNumber from 'bignumber.js';
+import * as Web3 from 'web3';
+import {chaiSetup} from './utils/chai_setup';
+import {web3Factory} from './utils/web3_factory';
+import {ZeroEx, SignedOrder, ExchangeContractErrs, Token} from '../src';
+import {TradeSide, TransferType} from '../src/types';
+import {BlockchainLifecycle} from './utils/blockchain_lifecycle';
+import {FillScenarios} from './utils/fill_scenarios';
+import {ExchangeTransferSimulator} from '../src/utils/exchange_transfer_simulator';
+
+chaiSetup.configure();
+const expect = chai.expect;
+const blockchainLifecycle = new BlockchainLifecycle();
+
+describe('ExchangeTransferSimulator', () => {
+ const web3 = web3Factory.create();
+ const zeroEx = new ZeroEx(web3.currentProvider);
+ const transferAmount = new BigNumber(5);
+ let userAddresses: string[];
+ let tokens: Token[];
+ let coinbase: string;
+ let sender: string;
+ let recipient: string;
+ let exampleTokenAddress: string;
+ let exchangeTransferSimulator: ExchangeTransferSimulator;
+ let txHash: string;
+ before(async () => {
+ userAddresses = await zeroEx.getAvailableAddressesAsync();
+ [coinbase, sender, recipient] = userAddresses;
+ tokens = await zeroEx.tokenRegistry.getTokensAsync();
+ exampleTokenAddress = tokens[0].address;
+ });
+ beforeEach(async () => {
+ await blockchainLifecycle.startAsync();
+ });
+ afterEach(async () => {
+ await blockchainLifecycle.revertAsync();
+ });
+ describe('#transferFromAsync', () => {
+ beforeEach(() => {
+ exchangeTransferSimulator = new ExchangeTransferSimulator(zeroEx.token);
+ });
+ it('throws if the user doesn\'t have enough allowance', async () => {
+ return expect(exchangeTransferSimulator.transferFromAsync(
+ exampleTokenAddress, sender, recipient, transferAmount, TradeSide.Taker, TransferType.Trade,
+ )).to.be.rejectedWith(ExchangeContractErrs.InsufficientTakerAllowance);
+ });
+ it('throws if the user doesn\'t have enough balance', async () => {
+ txHash = await zeroEx.token.setProxyAllowanceAsync(exampleTokenAddress, sender, transferAmount);
+ await zeroEx.awaitTransactionMinedAsync(txHash);
+ return expect(exchangeTransferSimulator.transferFromAsync(
+ exampleTokenAddress, sender, recipient, transferAmount, TradeSide.Maker, TransferType.Trade,
+ )).to.be.rejectedWith(ExchangeContractErrs.InsufficientMakerBalance);
+ });
+ it('updates balances and proxyAllowance after transfer', async () => {
+ txHash = await zeroEx.token.transferAsync(exampleTokenAddress, coinbase, sender, transferAmount);
+ await zeroEx.awaitTransactionMinedAsync(txHash);
+ txHash = await zeroEx.token.setProxyAllowanceAsync(exampleTokenAddress, sender, transferAmount);
+ await zeroEx.awaitTransactionMinedAsync(txHash);
+ await exchangeTransferSimulator.transferFromAsync(
+ exampleTokenAddress, sender, recipient, transferAmount, TradeSide.Taker, TransferType.Trade,
+ );
+ const senderBalance = await (exchangeTransferSimulator as any).getBalanceAsync(exampleTokenAddress, sender);
+ const recipientBalance = await (exchangeTransferSimulator as any).getBalanceAsync(
+ exampleTokenAddress, recipient);
+ const senderProxyAllowance = await (exchangeTransferSimulator as any).getProxyAllowanceAsync(
+ exampleTokenAddress, sender);
+ expect(senderBalance).to.be.bignumber.equal(0);
+ expect(recipientBalance).to.be.bignumber.equal(transferAmount);
+ expect(senderProxyAllowance).to.be.bignumber.equal(0);
+ });
+ it('doesn\'t update proxyAllowance after transfer if unlimited', async () => {
+ txHash = await zeroEx.token.transferAsync(exampleTokenAddress, coinbase, sender, transferAmount);
+ await zeroEx.awaitTransactionMinedAsync(txHash);
+ txHash = await zeroEx.token.setUnlimitedProxyAllowanceAsync(exampleTokenAddress, sender);
+ await zeroEx.awaitTransactionMinedAsync(txHash);
+ await exchangeTransferSimulator.transferFromAsync(
+ exampleTokenAddress, sender, recipient, transferAmount, TradeSide.Taker, TransferType.Trade,
+ );
+ const senderBalance = await (exchangeTransferSimulator as any).getBalanceAsync(exampleTokenAddress, sender);
+ const recipientBalance = await (exchangeTransferSimulator as any).getBalanceAsync(
+ exampleTokenAddress, recipient);
+ const senderProxyAllowance = await (exchangeTransferSimulator as any).getProxyAllowanceAsync(
+ exampleTokenAddress, sender);
+ expect(senderBalance).to.be.bignumber.equal(0);
+ expect(recipientBalance).to.be.bignumber.equal(transferAmount);
+ expect(senderProxyAllowance).to.be.bignumber.equal(zeroEx.token.UNLIMITED_ALLOWANCE_IN_BASE_UNITS);
+ });
+ });
+});
diff --git a/test/order_validation_test.ts b/test/order_validation_test.ts
index c1a0a6c8c..6f9388a69 100644
--- a/test/order_validation_test.ts
+++ b/test/order_validation_test.ts
@@ -2,13 +2,16 @@ import * as chai from 'chai';
import * as Web3 from 'web3';
import * as BigNumber from 'bignumber.js';
import promisify = require('es6-promisify');
+import * as Sinon from 'sinon';
import {chaiSetup} from './utils/chai_setup';
import {web3Factory} from './utils/web3_factory';
import {ZeroEx, SignedOrder, Token, ExchangeContractErrs, ZeroExError} from '../src';
+import {TradeSide, TransferType} from '../src/types';
import {TokenUtils} from './utils/token_utils';
import {BlockchainLifecycle} from './utils/blockchain_lifecycle';
import {FillScenarios} from './utils/fill_scenarios';
import {OrderValidationUtils} from '../src/utils/order_validation_utils';
+import {ExchangeTransferSimulator} from '../src/utils/exchange_transfer_simulator';
chaiSetup.configure();
const expect = chai.expect;
@@ -207,242 +210,52 @@ describe('OrderValidation', () => {
.to.be.rejectedWith(ExchangeContractErrs.OrderAlreadyCancelledOrFilled);
});
});
- describe('#validateFillOrderMakerBalancesAllowancesThrowIfInvalidAsync', () => {
- describe('should throw when not enough balance or allowance to fulfill the order', () => {
- const balanceToSubtractFromMaker = new BigNumber(3);
- const balanceToSubtractFromTaker = new BigNumber(3);
- const lackingAllowance = new BigNumber(3);
- let signedOrder: SignedOrder;
- beforeEach('create fillable signed order', async () => {
- signedOrder = await fillScenarios.createFillableSignedOrderAsync(
- makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
- );
- });
- it('should throw when maker balance is less than maker fill amount', async () => {
- await zeroEx.token.transferAsync(
- makerTokenAddress, makerAddress, coinbase, balanceToSubtractFromMaker,
- );
- return expect((orderValidationUtils as any).validateFillOrderMakerBalancesAllowancesThrowIfInvalidAsync(
- signedOrder, fillTakerAmount, takerAddress, zrxTokenAddress,
- )).to.be.rejectedWith(ExchangeContractErrs.InsufficientMakerBalance);
- });
- it('should throw when maker allowance is less than maker fill amount', async () => {
- const newAllowanceWhichIsLessThanFillAmount = fillTakerAmount.minus(lackingAllowance);
- await zeroEx.token.setProxyAllowanceAsync(makerTokenAddress, makerAddress,
- newAllowanceWhichIsLessThanFillAmount);
- return expect((orderValidationUtils as any).validateFillOrderMakerBalancesAllowancesThrowIfInvalidAsync(
- signedOrder, fillTakerAmount, takerAddress, zrxTokenAddress,
- )).to.be.rejectedWith(ExchangeContractErrs.InsufficientMakerAllowance);
- });
+ describe('#validateFillOrderBalancesAllowancesThrowIfInvalidAsync', () => {
+ let exchangeTransferSimulator: ExchangeTransferSimulator;
+ let transferFromAsync: any;
+ const bigNumberMatch = (expected: BigNumber.BigNumber) => {
+ return Sinon.match((value: BigNumber.BigNumber) => value.eq(expected));
+ };
+ beforeEach('create exchangeTransferSimulator', async () => {
+ exchangeTransferSimulator = new ExchangeTransferSimulator(zeroEx.token);
+ transferFromAsync = Sinon.spy();
+ exchangeTransferSimulator.transferFromAsync = transferFromAsync;
});
- describe('should throw when not enough balance or allowance to pay fees', () => {
+ it('should call exchangeTransferSimulator.transferFrom in a correct order', async () => {
const makerFee = new BigNumber(2);
const takerFee = new BigNumber(2);
- let signedOrder: SignedOrder;
- beforeEach('setup', async () => {
- signedOrder = await fillScenarios.createFillableSignedOrderWithFeesAsync(
- makerTokenAddress, takerTokenAddress, makerFee, takerFee,
- makerAddress, takerAddress, fillableAmount, feeRecipient,
- );
- });
- it('should throw when maker doesn\'t have enough balance to pay fees', async () => {
- const balanceToSubtractFromMaker = new BigNumber(1);
- await zeroEx.token.transferAsync(
- zrxTokenAddress, makerAddress, coinbase, balanceToSubtractFromMaker,
- );
- return expect((orderValidationUtils as any).validateFillOrderMakerBalancesAllowancesThrowIfInvalidAsync(
- signedOrder, fillTakerAmount, zrxTokenAddress,
- )).to.be.rejectedWith(ExchangeContractErrs.InsufficientMakerFeeBalance);
- });
- it('should throw when maker doesn\'t have enough allowance to pay fees', async () => {
- const newAllowanceWhichIsLessThanFees = makerFee.minus(1);
- await zeroEx.token.setProxyAllowanceAsync(zrxTokenAddress, makerAddress,
- newAllowanceWhichIsLessThanFees);
- return expect((orderValidationUtils as any).validateFillOrderMakerBalancesAllowancesThrowIfInvalidAsync(
- signedOrder, fillTakerAmount, zrxTokenAddress,
- )).to.be.rejectedWith(ExchangeContractErrs.InsufficientMakerFeeAllowance);
- });
- });
- describe('should throw on insufficient balance or allowance when makerToken is ZRX',
- () => {
- const makerFee = new BigNumber(2);
- const takerFee = new BigNumber(2);
- let signedOrder: SignedOrder;
- beforeEach(async () => {
- signedOrder = await fillScenarios.createFillableSignedOrderWithFeesAsync(
- zrxTokenAddress, takerTokenAddress, makerFee, takerFee,
- makerAddress, takerAddress, fillableAmount, feeRecipient,
- );
- });
- it('should throw on insufficient balance when makerToken is ZRX', async () => {
- const balanceToSubtractFromMaker = new BigNumber(1);
- await zeroEx.token.transferAsync(
- zrxTokenAddress, makerAddress, coinbase, balanceToSubtractFromMaker,
- );
- return expect((orderValidationUtils as any).validateFillOrderMakerBalancesAllowancesThrowIfInvalidAsync(
- signedOrder, fillTakerAmount, zrxTokenAddress,
- )).to.be.rejectedWith(ExchangeContractErrs.InsufficientMakerBalance);
- });
- it('should throw on insufficient allowance when makerToken is ZRX', async () => {
- const oldAllowance = await zeroEx.token.getProxyAllowanceAsync(zrxTokenAddress, makerAddress);
- const newAllowanceWhichIsInsufficient = oldAllowance.minus(1);
- await zeroEx.token.setProxyAllowanceAsync(
- zrxTokenAddress, makerAddress, newAllowanceWhichIsInsufficient);
- return expect((orderValidationUtils as any).validateFillOrderMakerBalancesAllowancesThrowIfInvalidAsync(
- signedOrder, fillTakerAmount, zrxTokenAddress,
- )).to.be.rejectedWith(ExchangeContractErrs.InsufficientMakerAllowance);
- });
- });
- describe('should correctly validate fees amounts if taker token is ZRX',
- () => {
- let signedOrder: SignedOrder;
- let txHash: string;
- it('should not throw if maker will have enough ZRX to pay fees after the transfer', async () => {
- const makerFee = new BigNumber(2);
- const takerFee = new BigNumber(2);
- signedOrder = await fillScenarios.createFillableSignedOrderWithFeesAsync(
- makerTokenAddress, zrxTokenAddress, makerFee, takerFee,
- makerAddress, takerAddress, fillableAmount, feeRecipient,
- );
- txHash = await zeroEx.token.transferAsync(zrxTokenAddress, makerAddress, coinbase, makerFee);
- await zeroEx.awaitTransactionMinedAsync(txHash);
- await (orderValidationUtils as any).validateFillOrderMakerBalancesAllowancesThrowIfInvalidAsync(
- signedOrder, fillTakerAmount, zrxTokenAddress,
- );
- });
- it('should throw if maker will not have enough ZRX to pay fees even after the transfer', async () => {
- const makerFee = fillableAmount.plus(1);
- const takerFee = fillableAmount.plus(1);
- signedOrder = await fillScenarios.createFillableSignedOrderWithFeesAsync(
- makerTokenAddress, zrxTokenAddress, makerFee, takerFee,
- makerAddress, takerAddress, fillableAmount, feeRecipient,
- );
- txHash = await zeroEx.token.transferAsync(zrxTokenAddress, makerAddress, coinbase, makerFee);
- await zeroEx.awaitTransactionMinedAsync(txHash);
- return expect(
- (orderValidationUtils as any).validateFillOrderMakerBalancesAllowancesThrowIfInvalidAsync(
- signedOrder, fillTakerAmount, zrxTokenAddress,
- )).to.be.rejectedWith(ExchangeContractErrs.InsufficientMakerFeeBalance);
- });
- });
- });
- describe('#validateFillOrderTakerBalancesAllowancesThrowIfInvalidAsync', () => {
- describe('should throw when not enough balance or allowance to fulfill the order', () => {
- const balanceToSubtractFromMaker = new BigNumber(3);
- const balanceToSubtractFromTaker = new BigNumber(3);
- const lackingAllowance = new BigNumber(3);
- let signedOrder: SignedOrder;
- beforeEach('create fillable signed order', async () => {
- signedOrder = await fillScenarios.createFillableSignedOrderAsync(
- makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
- );
- });
- it('should throw when taker balance is less than fill amount', async () => {
- await zeroEx.token.transferAsync(
- takerTokenAddress, takerAddress, coinbase, balanceToSubtractFromTaker,
- );
- return expect((orderValidationUtils as any).validateFillOrderTakerBalancesAllowancesThrowIfInvalidAsync(
- signedOrder, fillTakerAmount, takerAddress, zrxTokenAddress,
- )).to.be.rejectedWith(ExchangeContractErrs.InsufficientTakerBalance);
- });
- it('should throw when taker allowance is less than fill amount', async () => {
- const newAllowanceWhichIsLessThanFillAmount = fillTakerAmount.minus(lackingAllowance);
- await zeroEx.token.setProxyAllowanceAsync(takerTokenAddress, takerAddress,
- newAllowanceWhichIsLessThanFillAmount);
- return expect((orderValidationUtils as any).validateFillOrderTakerBalancesAllowancesThrowIfInvalidAsync(
- signedOrder, fillTakerAmount, takerAddress, zrxTokenAddress,
- )).to.be.rejectedWith(ExchangeContractErrs.InsufficientTakerAllowance);
- });
- });
- describe('should throw when not enough balance or allowance to pay fees', () => {
- const makerFee = new BigNumber(2);
- const takerFee = new BigNumber(2);
- let signedOrder: SignedOrder;
- beforeEach('setup', async () => {
- signedOrder = await fillScenarios.createFillableSignedOrderWithFeesAsync(
- makerTokenAddress, takerTokenAddress, makerFee, takerFee,
- makerAddress, takerAddress, fillableAmount, feeRecipient,
- );
- });
- it('should throw when taker doesn\'t have enough balance to pay fees', async () => {
- const balanceToSubtractFromTaker = new BigNumber(1);
- await zeroEx.token.transferAsync(
- zrxTokenAddress, takerAddress, coinbase, balanceToSubtractFromTaker,
- );
- return expect((orderValidationUtils as any).validateFillOrderTakerBalancesAllowancesThrowIfInvalidAsync(
- signedOrder, fillTakerAmount, takerAddress, zrxTokenAddress,
- )).to.be.rejectedWith(ExchangeContractErrs.InsufficientTakerFeeBalance);
- });
- it('should throw when taker doesn\'t have enough allowance to pay fees', async () => {
- const newAllowanceWhichIsLessThanFees = makerFee.minus(1);
- await zeroEx.token.setProxyAllowanceAsync(zrxTokenAddress, takerAddress,
- newAllowanceWhichIsLessThanFees);
- return expect((orderValidationUtils as any).validateFillOrderTakerBalancesAllowancesThrowIfInvalidAsync(
- signedOrder, fillTakerAmount, takerAddress, zrxTokenAddress,
- )).to.be.rejectedWith(ExchangeContractErrs.InsufficientTakerFeeAllowance);
- });
- });
- describe('should throw on insufficient balance or allowance when takerToken is ZRX',
- () => {
- const makerFee = new BigNumber(2);
- const takerFee = new BigNumber(2);
- let signedOrder: SignedOrder;
- beforeEach(async () => {
- signedOrder = await fillScenarios.createFillableSignedOrderWithFeesAsync(
- makerTokenAddress, zrxTokenAddress, makerFee, takerFee,
- makerAddress, takerAddress, fillableAmount, feeRecipient,
- );
- });
- it('should throw on insufficient balance when takerToken is ZRX', async () => {
- const balanceToSubtractFromTaker = new BigNumber(1);
- await zeroEx.token.transferAsync(
- zrxTokenAddress, takerAddress, coinbase, balanceToSubtractFromTaker,
- );
- return expect((orderValidationUtils as any).validateFillOrderTakerBalancesAllowancesThrowIfInvalidAsync(
- signedOrder, fillTakerAmount, takerAddress, zrxTokenAddress,
- )).to.be.rejectedWith(ExchangeContractErrs.InsufficientTakerBalance);
- });
- it('should throw on insufficient allowance when takerToken is ZRX', async () => {
- const oldAllowance = await zeroEx.token.getProxyAllowanceAsync(zrxTokenAddress, takerAddress);
- const newAllowanceWhichIsInsufficient = oldAllowance.minus(1);
- await zeroEx.token.setProxyAllowanceAsync(
- zrxTokenAddress, takerAddress, newAllowanceWhichIsInsufficient);
- return expect((orderValidationUtils as any).validateFillOrderTakerBalancesAllowancesThrowIfInvalidAsync(
- signedOrder, fillTakerAmount, takerAddress, zrxTokenAddress,
- )).to.be.rejectedWith(ExchangeContractErrs.InsufficientTakerAllowance);
- });
- });
- describe('should correctly validate fees amounts if maker token is ZRX',
- () => {
- let signedOrder: SignedOrder;
- let txHash: string;
- it('should not throw if taker will have enough ZRX to pay fees after the transfer', async () => {
- const makerFee = new BigNumber(2);
- const takerFee = new BigNumber(2);
- signedOrder = await fillScenarios.createFillableSignedOrderWithFeesAsync(
- zrxTokenAddress, takerTokenAddress, makerFee, takerFee,
- makerAddress, takerAddress, fillableAmount, feeRecipient,
- );
- txHash = await zeroEx.token.transferAsync(zrxTokenAddress, takerAddress, coinbase, takerFee);
- await zeroEx.awaitTransactionMinedAsync(txHash);
- await (orderValidationUtils as any).validateFillOrderTakerBalancesAllowancesThrowIfInvalidAsync(
- signedOrder, fillTakerAmount, takerAddress, zrxTokenAddress,
- );
- });
- it('should throw if maker will not have enough ZRX to pay fees even after the transfer', async () => {
- const makerFee = fillableAmount.plus(1);
- const takerFee = fillableAmount.plus(1);
- signedOrder = await fillScenarios.createFillableSignedOrderWithFeesAsync(
- zrxTokenAddress, takerTokenAddress, makerFee, takerFee,
- makerAddress, takerAddress, fillableAmount, feeRecipient,
- );
- txHash = await zeroEx.token.transferAsync(zrxTokenAddress, takerAddress, coinbase, takerFee);
- await zeroEx.awaitTransactionMinedAsync(txHash);
- return expect(
- (orderValidationUtils as any).validateFillOrderTakerBalancesAllowancesThrowIfInvalidAsync(
- signedOrder, fillTakerAmount, takerAddress, zrxTokenAddress,
- )).to.be.rejectedWith(ExchangeContractErrs.InsufficientTakerFeeBalance);
- });
+ const signedOrder = await fillScenarios.createFillableSignedOrderWithFeesAsync(
+ makerTokenAddress, takerTokenAddress, makerFee, takerFee,
+ makerAddress, takerAddress, fillableAmount, feeRecipient,
+ );
+ await orderValidationUtils.validateFillOrderBalancesAllowancesThrowIfInvalidAsync(
+ exchangeTransferSimulator, signedOrder, fillableAmount, takerAddress, zrxTokenAddress,
+ );
+ expect(transferFromAsync.callCount).to.be.equal(4);
+ expect(
+ transferFromAsync.getCall(0).calledWith(
+ makerTokenAddress, makerAddress, takerAddress, bigNumberMatch(fillableAmount),
+ TradeSide.Maker, TransferType.Trade,
+ ),
+ ).to.be.true();
+ expect(
+ transferFromAsync.getCall(1).calledWith(
+ takerTokenAddress, takerAddress, makerAddress, bigNumberMatch(fillableAmount),
+ TradeSide.Taker, TransferType.Trade,
+ ),
+ ).to.be.true();
+ expect(
+ transferFromAsync.getCall(2).calledWith(
+ zrxTokenAddress, makerAddress, feeRecipient, bigNumberMatch(makerFee),
+ TradeSide.Maker, TransferType.Fee,
+ ),
+ ).to.be.true();
+ expect(
+ transferFromAsync.getCall(3).calledWith(
+ zrxTokenAddress, takerAddress, feeRecipient, bigNumberMatch(takerFee),
+ TradeSide.Taker, TransferType.Fee,
+ ),
+ ).to.be.true();
});
});
});