Merge pull request #185 from 0xProject/fix/batch-validation
Fix batch validation
This commit is contained in:
@@ -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_
|
||||
------------------------
|
||||
|
@@ -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:
|
||||
|
10
src/types.ts
10
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',
|
||||
}
|
||||
|
@@ -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`,
|
||||
);
|
||||
},
|
||||
|
131
src/utils/exchange_transfer_simulator.ts
Normal file
131
src/utils/exchange_transfer_simulator.ts
Normal 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);
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
91
test/exchange_transfer_simulator_test.ts
Normal file
91
test/exchange_transfer_simulator_test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Reference in New Issue
Block a user