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