aboutsummaryrefslogtreecommitdiffstats
path: root/packages/order-utils/src/order_validation_utils.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/order-utils/src/order_validation_utils.ts')
-rw-r--r--packages/order-utils/src/order_validation_utils.ts223
1 files changed, 223 insertions, 0 deletions
diff --git a/packages/order-utils/src/order_validation_utils.ts b/packages/order-utils/src/order_validation_utils.ts
new file mode 100644
index 000000000..4280e4d97
--- /dev/null
+++ b/packages/order-utils/src/order_validation_utils.ts
@@ -0,0 +1,223 @@
+import { ExchangeContractErrs, Order, SignedOrder } from '@0xproject/types';
+import { BigNumber } from '@0xproject/utils';
+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 { isValidECSignature, parseECSignature } 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<void> {
+ 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<void> {
+ 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,
+ signedOrder: SignedOrder,
+ fillTakerTokenAmount: BigNumber,
+ takerAddress: string,
+ zrxTokenAddress: string,
+ ): Promise<BigNumber> {
+ if (fillTakerTokenAmount.eq(0)) {
+ throw new Error(ExchangeContractErrs.OrderFillAmountZero);
+ }
+ const orderHash = orderHashUtils.getOrderHashHex(signedOrder);
+ // TODO: Verify all signature types! To do this, we need access to a Provider...
+ const ecSignature = parseECSignature(signedOrder.signature);
+ if (!isValidECSignature(orderHash, ecSignature, signedOrder.makerAddress)) {
+ 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 = await OrderValidationUtils.isRoundingError(
+ filledTakerTokenAmount,
+ signedOrder.takerAssetAmount,
+ signedOrder.makerAssetAmount,
+ );
+ if (wouldRoundingErrorOccur) {
+ throw new Error(ExchangeContractErrs.OrderFillRoundingError);
+ }
+ return filledTakerTokenAmount;
+ }
+ public async validateFillOrKillOrderThrowIfInvalidAsync(
+ exchangeTradeEmulator: ExchangeTransferSimulator,
+ signedOrder: SignedOrder,
+ fillTakerTokenAmount: BigNumber,
+ takerAddress: string,
+ zrxTokenAddress: string,
+ ): Promise<void> {
+ const filledTakerTokenAmount = await this.validateFillOrderThrowIfInvalidAsync(
+ exchangeTradeEmulator,
+ signedOrder,
+ fillTakerTokenAmount,
+ takerAddress,
+ zrxTokenAddress,
+ );
+ if (filledTakerTokenAmount !== fillTakerTokenAmount) {
+ throw new Error(ExchangeContractErrs.InsufficientRemainingFillAmount);
+ }
+ }
+}