import { ExchangeContractErrs, Order, SignedOrder } from '@0xproject/types'; import { BigNumber } from '@0xproject/utils'; import { Provider } from 'ethereum-types'; import * as _ from 'lodash'; import { OrderError, TradeSide, TransferType } from './types'; import { constants } from './constants'; import { ExchangeTransferSimulator } from './exchange_transfer_simulator'; import { ExchangeContract } from './generated_contract_wrappers/exchange'; import { orderHashUtils } from './order_hash'; import { isValidSignatureAsync } from './signature_utils'; import { utils } from './utils'; export class OrderValidationUtils { private _exchangeContract: ExchangeContract; // TODO: Write some tests for the function // const numerator = new BigNumber(20); // const denominator = new BigNumber(999); // const target = new BigNumber(50); // rounding error = ((20*50/999) - floor(20*50/999)) / (20*50/999) = 0.1% public static isRoundingError(numerator: BigNumber, denominator: BigNumber, target: BigNumber): boolean { // Solidity's mulmod() in JS // Source: https://solidity.readthedocs.io/en/latest/units-and-global-variables.html#mathematical-and-cryptographic-functions if (denominator.eq(0)) { throw new Error('denominator cannot be 0'); } const remainder = target.mul(numerator).mod(denominator); if (remainder.eq(0)) { return false; // no rounding error } // tslint:disable-next-line:custom-no-magic-numbers const errPercentageTimes1000000 = remainder.mul(1000000).div(numerator.mul(target)); // tslint:disable-next-line:custom-no-magic-numbers const isError = errPercentageTimes1000000.gt(1000); return isError; } public static validateCancelOrderThrowIfInvalid( order: Order, cancelTakerTokenAmount: BigNumber, filledTakerTokenAmount: BigNumber, ): void { if (cancelTakerTokenAmount.eq(0)) { throw new Error(ExchangeContractErrs.OrderCancelAmountZero); } if (order.takerAssetAmount.eq(filledTakerTokenAmount)) { throw new Error(ExchangeContractErrs.OrderAlreadyCancelledOrFilled); } const currentUnixTimestampSec = utils.getCurrentUnixTimestampSec(); if (order.expirationTimeSeconds.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.takerAssetAmount, signedOrder.makerAssetAmount, ); await exchangeTradeEmulator.transferFromAsync( signedOrder.makerAssetData, signedOrder.makerAddress, senderAddress, fillMakerTokenAmount, TradeSide.Maker, TransferType.Trade, ); await exchangeTradeEmulator.transferFromAsync( signedOrder.takerAssetData, senderAddress, signedOrder.makerAddress, fillTakerTokenAmount, TradeSide.Taker, TransferType.Trade, ); const makerFeeAmount = OrderValidationUtils._getPartialAmount( fillTakerTokenAmount, signedOrder.takerAssetAmount, signedOrder.makerFee, ); await exchangeTradeEmulator.transferFromAsync( zrxTokenAddress, signedOrder.makerAddress, signedOrder.feeRecipientAddress, makerFeeAmount, TradeSide.Maker, TransferType.Fee, ); const takerFeeAmount = OrderValidationUtils._getPartialAmount( fillTakerTokenAmount, signedOrder.takerAssetAmount, signedOrder.takerFee, ); await exchangeTradeEmulator.transferFromAsync( zrxTokenAddress, senderAddress, signedOrder.feeRecipientAddress, takerFeeAmount, TradeSide.Taker, TransferType.Fee, ); } private static _validateRemainingFillAmountNotZeroOrThrow( takerAssetAmount: BigNumber, filledTakerTokenAmount: BigNumber, ): void { if (takerAssetAmount.eq(filledTakerTokenAmount)) { throw new Error(ExchangeContractErrs.OrderRemainingFillAmountZero); } } private static _validateOrderNotExpiredOrThrow(expirationTimeSeconds: BigNumber): void { const currentUnixTimestampSec = utils.getCurrentUnixTimestampSec(); if (expirationTimeSeconds.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(exchangeContract: ExchangeContract) { this._exchangeContract = exchangeContract; } public async validateOrderFillableOrThrowAsync( exchangeTradeEmulator: ExchangeTransferSimulator, signedOrder: SignedOrder, zrxTokenAddress: string, expectedFillTakerTokenAmount?: BigNumber, ): Promise { const orderHash = orderHashUtils.getOrderHashHex(signedOrder); const filledTakerTokenAmount = await this._exchangeContract.filled.callAsync(orderHash); OrderValidationUtils._validateRemainingFillAmountNotZeroOrThrow( signedOrder.takerAssetAmount, filledTakerTokenAmount, ); OrderValidationUtils._validateOrderNotExpiredOrThrow(signedOrder.expirationTimeSeconds); let fillTakerTokenAmount = signedOrder.takerAssetAmount.minus(filledTakerTokenAmount); if (!_.isUndefined(expectedFillTakerTokenAmount)) { fillTakerTokenAmount = expectedFillTakerTokenAmount; } await OrderValidationUtils.validateFillOrderBalancesAllowancesThrowIfInvalidAsync( exchangeTradeEmulator, signedOrder, fillTakerTokenAmount, signedOrder.takerAddress, zrxTokenAddress, ); } public async validateFillOrderThrowIfInvalidAsync( exchangeTradeEmulator: ExchangeTransferSimulator, provider: Provider, signedOrder: SignedOrder, fillTakerTokenAmount: BigNumber, takerAddress: string, zrxTokenAddress: string, ): Promise { if (fillTakerTokenAmount.eq(0)) { throw new Error(ExchangeContractErrs.OrderFillAmountZero); } const orderHash = orderHashUtils.getOrderHashHex(signedOrder); const isValid = await isValidSignatureAsync( provider, orderHash, signedOrder.signature, signedOrder.makerAddress, ); if (!isValid) { throw new Error(OrderError.InvalidSignature); } const filledTakerTokenAmount = await this._exchangeContract.filled.callAsync(orderHash); OrderValidationUtils._validateRemainingFillAmountNotZeroOrThrow( signedOrder.takerAssetAmount, filledTakerTokenAmount, ); if (signedOrder.takerAddress !== constants.NULL_ADDRESS && signedOrder.takerAddress !== takerAddress) { throw new Error(ExchangeContractErrs.TransactionSenderIsNotFillOrderTaker); } OrderValidationUtils._validateOrderNotExpiredOrThrow(signedOrder.expirationTimeSeconds); const remainingTakerTokenAmount = signedOrder.takerAssetAmount.minus(filledTakerTokenAmount); const desiredFillTakerTokenAmount = remainingTakerTokenAmount.lessThan(fillTakerTokenAmount) ? remainingTakerTokenAmount : fillTakerTokenAmount; await OrderValidationUtils.validateFillOrderBalancesAllowancesThrowIfInvalidAsync( exchangeTradeEmulator, signedOrder, desiredFillTakerTokenAmount, takerAddress, zrxTokenAddress, ); const wouldRoundingErrorOccur = OrderValidationUtils.isRoundingError( filledTakerTokenAmount, signedOrder.takerAssetAmount, signedOrder.makerAssetAmount, ); if (wouldRoundingErrorOccur) { throw new Error(ExchangeContractErrs.OrderFillRoundingError); } return filledTakerTokenAmount; } public async validateFillOrKillOrderThrowIfInvalidAsync( exchangeTradeEmulator: ExchangeTransferSimulator, provider: Provider, signedOrder: SignedOrder, fillTakerTokenAmount: BigNumber, takerAddress: string, zrxTokenAddress: string, ): Promise { const filledTakerTokenAmount = await this.validateFillOrderThrowIfInvalidAsync( exchangeTradeEmulator, provider, signedOrder, fillTakerTokenAmount, takerAddress, zrxTokenAddress, ); if (filledTakerTokenAmount !== fillTakerTokenAmount) { throw new Error(ExchangeContractErrs.InsufficientRemainingFillAmount); } } }