import { getOrderHashHex, OrderError } from '@0xproject/order-utils'; import { Order, SignedOrder } from '@0xproject/types'; import { BigNumber } from '@0xproject/utils'; import * as _ from 'lodash'; import { ZeroEx } from '../0x'; import { ExchangeWrapper } from '../contract_wrappers/exchange_wrapper'; import { ExchangeContractErrs, TradeSide, TransferType, ZeroExError } from '../types'; import { constants } from '../utils/constants'; import { utils } from '../utils/utils'; import { ExchangeTransferSimulator } from './exchange_transfer_simulator'; export class OrderValidationUtils { private _exchangeWrapper: ExchangeWrapper; public static validateCancelOrderThrowIfInvalid( order: Order, cancelTakerTokenAmount: BigNumber, unavailableTakerTokenAmount: BigNumber, ): void { if (cancelTakerTokenAmount.eq(0)) { throw new Error(ExchangeContractErrs.OrderCancelAmountZero); } if (order.takerTokenAmount.eq(unavailableTakerTokenAmount)) { throw new Error(ExchangeContractErrs.OrderAlreadyCancelledOrFilled); } const currentUnixTimestampSec = utils.getCurrentUnixTimestampSec(); if (order.expirationUnixTimestampSec.lessThan(currentUnixTimestampSec)) { throw new Error(ExchangeContractErrs.OrderCancelExpired); } } public static async validateFillOrderBalancesAllowancesThrowIfInvalidAsync( exchangeTradeEmulator: ExchangeTransferSimulator, signedOrder: SignedOrder, fillTakerTokenAmount: BigNumber, senderAddress: string, zrxTokenAddress: string, ): Promise { const fillMakerTokenAmount = OrderValidationUtils._getPartialAmount( fillTakerTokenAmount, signedOrder.takerTokenAmount, signedOrder.makerTokenAmount, ); await exchangeTradeEmulator.transferFromAsync( signedOrder.makerTokenAddress, signedOrder.maker, senderAddress, fillMakerTokenAmount, TradeSide.Maker, TransferType.Trade, ); await exchangeTradeEmulator.transferFromAsync( signedOrder.takerTokenAddress, senderAddress, signedOrder.maker, fillTakerTokenAmount, TradeSide.Taker, TransferType.Trade, ); const makerFeeAmount = OrderValidationUtils._getPartialAmount( fillTakerTokenAmount, signedOrder.takerTokenAmount, signedOrder.makerFee, ); await exchangeTradeEmulator.transferFromAsync( zrxTokenAddress, signedOrder.maker, signedOrder.feeRecipient, makerFeeAmount, TradeSide.Maker, TransferType.Fee, ); const takerFeeAmount = OrderValidationUtils._getPartialAmount( fillTakerTokenAmount, signedOrder.takerTokenAmount, signedOrder.takerFee, ); await exchangeTradeEmulator.transferFromAsync( zrxTokenAddress, senderAddress, signedOrder.feeRecipient, takerFeeAmount, TradeSide.Taker, TransferType.Fee, ); } private static _validateRemainingFillAmountNotZeroOrThrow( takerTokenAmount: BigNumber, unavailableTakerTokenAmount: BigNumber, ) { if (takerTokenAmount.eq(unavailableTakerTokenAmount)) { throw new Error(ExchangeContractErrs.OrderRemainingFillAmountZero); } } private static _validateOrderNotExpiredOrThrow(expirationUnixTimestampSec: BigNumber) { const currentUnixTimestampSec = utils.getCurrentUnixTimestampSec(); if (expirationUnixTimestampSec.lessThan(currentUnixTimestampSec)) { throw new Error(ExchangeContractErrs.OrderFillExpired); } } private static _getPartialAmount(numerator: BigNumber, denominator: BigNumber, target: BigNumber): BigNumber { const fillMakerTokenAmount = numerator .mul(target) .div(denominator) .round(0); return fillMakerTokenAmount; } constructor(exchangeWrapper: ExchangeWrapper) { this._exchangeWrapper = exchangeWrapper; } public async validateOrderFillableOrThrowAsync( exchangeTradeEmulator: ExchangeTransferSimulator, signedOrder: SignedOrder, zrxTokenAddress: string, expectedFillTakerTokenAmount?: BigNumber, ): Promise { const orderHash = getOrderHashHex(signedOrder); const unavailableTakerTokenAmount = await this._exchangeWrapper.getUnavailableTakerAmountAsync(orderHash); OrderValidationUtils._validateRemainingFillAmountNotZeroOrThrow( signedOrder.takerTokenAmount, unavailableTakerTokenAmount, ); OrderValidationUtils._validateOrderNotExpiredOrThrow(signedOrder.expirationUnixTimestampSec); let fillTakerTokenAmount = signedOrder.takerTokenAmount.minus(unavailableTakerTokenAmount); if (!_.isUndefined(expectedFillTakerTokenAmount)) { fillTakerTokenAmount = expectedFillTakerTokenAmount; } await OrderValidationUtils.validateFillOrderBalancesAllowancesThrowIfInvalidAsync( exchangeTradeEmulator, signedOrder, fillTakerTokenAmount, signedOrder.taker, zrxTokenAddress, ); } public async validateFillOrderThrowIfInvalidAsync( exchangeTradeEmulator: ExchangeTransferSimulator, signedOrder: SignedOrder, fillTakerTokenAmount: BigNumber, takerAddress: string, zrxTokenAddress: string, ): Promise { if (fillTakerTokenAmount.eq(0)) { throw new Error(ExchangeContractErrs.OrderFillAmountZero); } const orderHash = getOrderHashHex(signedOrder); if (!ZeroEx.isValidSignature(orderHash, signedOrder.ecSignature, signedOrder.maker)) { throw new Error(OrderError.InvalidSignature); } const unavailableTakerTokenAmount = await this._exchangeWrapper.getUnavailableTakerAmountAsync(orderHash); OrderValidationUtils._validateRemainingFillAmountNotZeroOrThrow( signedOrder.takerTokenAmount, unavailableTakerTokenAmount, ); if (signedOrder.taker !== constants.NULL_ADDRESS && signedOrder.taker !== takerAddress) { throw new Error(ExchangeContractErrs.TransactionSenderIsNotFillOrderTaker); } OrderValidationUtils._validateOrderNotExpiredOrThrow(signedOrder.expirationUnixTimestampSec); const remainingTakerTokenAmount = signedOrder.takerTokenAmount.minus(unavailableTakerTokenAmount); const filledTakerTokenAmount = remainingTakerTokenAmount.lessThan(fillTakerTokenAmount) ? remainingTakerTokenAmount : fillTakerTokenAmount; await OrderValidationUtils.validateFillOrderBalancesAllowancesThrowIfInvalidAsync( exchangeTradeEmulator, signedOrder, filledTakerTokenAmount, takerAddress, zrxTokenAddress, ); const wouldRoundingErrorOccur = await this._exchangeWrapper.isRoundingErrorAsync( filledTakerTokenAmount, signedOrder.takerTokenAmount, signedOrder.makerTokenAmount, ); if (wouldRoundingErrorOccur) { throw new Error(ExchangeContractErrs.OrderFillRoundingError); } return filledTakerTokenAmount; } public async validateFillOrKillOrderThrowIfInvalidAsync( exchangeTradeEmulator: ExchangeTransferSimulator, signedOrder: SignedOrder, fillTakerTokenAmount: BigNumber, takerAddress: string, zrxTokenAddress: string, ): Promise { const filledTakerTokenAmount = await this.validateFillOrderThrowIfInvalidAsync( exchangeTradeEmulator, signedOrder, fillTakerTokenAmount, takerAddress, zrxTokenAddress, ); if (filledTakerTokenAmount !== fillTakerTokenAmount) { throw new Error(ExchangeContractErrs.InsufficientRemainingFillAmount); } } }