Merge pull request #185 from 0xProject/fix/batch-validation

Fix batch validation
This commit is contained in:
Leonid
2017-10-10 14:15:57 +03:00
committed by GitHub
8 changed files with 382 additions and 352 deletions

View File

@@ -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_
------------------------

View File

@@ -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:

View File

@@ -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',
}

View File

@@ -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`,
);
},

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
});
});
});

View File

@@ -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();
});
});
});