diff options
-rw-r--r-- | packages/contracts/test/exchange/fill_order.ts | 11 | ||||
-rw-r--r-- | packages/contracts/test/utils/fill_order_combinatorial_utils.ts | 7 | ||||
-rw-r--r-- | packages/contracts/test/utils/order_factory_from_scenario.ts | 23 | ||||
-rw-r--r-- | packages/contracts/test/utils/types.ts | 3 | ||||
-rw-r--r-- | packages/order-utils/CHANGELOG.json | 22 | ||||
-rw-r--r-- | packages/order-utils/src/market_utils.ts | 129 | ||||
-rw-r--r-- | packages/order-utils/src/order_state_utils.ts | 95 | ||||
-rw-r--r-- | packages/order-utils/src/types.ts | 28 | ||||
-rw-r--r-- | packages/order-utils/test/market_utils_test.ts | 50 | ||||
-rw-r--r-- | packages/order-utils/test/order_state_utils_test.ts | 124 | ||||
-rw-r--r-- | packages/order-watcher/test/order_watcher_test.ts | 20 | ||||
-rw-r--r-- | packages/types/src/index.ts | 3 | ||||
-rw-r--r-- | packages/website/ts/utils/utils.ts | 7 |
13 files changed, 363 insertions, 159 deletions
diff --git a/packages/contracts/test/exchange/fill_order.ts b/packages/contracts/test/exchange/fill_order.ts index 1494fe093..e79e2239e 100644 --- a/packages/contracts/test/exchange/fill_order.ts +++ b/packages/contracts/test/exchange/fill_order.ts @@ -104,6 +104,17 @@ describe('FillOrder Tests', () => { }; await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario); }); + it('should transfer the correct amounts when makerAssetAmount < takerAssetAmount with zero decimals', async () => { + const fillScenario = { + ...defaultFillScenario, + orderScenario: { + ...defaultFillScenario.orderScenario, + makerAssetAmountScenario: OrderAssetAmountScenario.Small, + makerAssetDataScenario: AssetDataScenario.ERC20ZeroDecimals, + }, + }; + await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario); + }); it('should transfer the correct amounts when taker is specified and order is claimed by taker', async () => { const fillScenario = { ...defaultFillScenario, diff --git a/packages/contracts/test/utils/fill_order_combinatorial_utils.ts b/packages/contracts/test/utils/fill_order_combinatorial_utils.ts index 284c4a2db..f18ad0dd3 100644 --- a/packages/contracts/test/utils/fill_order_combinatorial_utils.ts +++ b/packages/contracts/test/utils/fill_order_combinatorial_utils.ts @@ -81,6 +81,12 @@ export async function fillOrderCombinatorialUtilsFactoryAsync( erc20FiveDecimalTokenCount, fiveDecimals, ); + const zeroDecimals = new BigNumber(0); + const erc20ZeroDecimalTokenCount = 2; + const [erc20ZeroDecimalTokenA, erc20ZeroDecimalTokenB] = await erc20Wrapper.deployDummyTokensAsync( + erc20ZeroDecimalTokenCount, + zeroDecimals, + ); const erc20Proxy = await erc20Wrapper.deployProxyAsync(); await erc20Wrapper.setBalancesAndAllowancesAsync(); @@ -119,6 +125,7 @@ export async function fillOrderCombinatorialUtilsFactoryAsync( zrxToken.address, [erc20EighteenDecimalTokenA.address, erc20EighteenDecimalTokenB.address], [erc20FiveDecimalTokenA.address, erc20FiveDecimalTokenB.address], + [erc20ZeroDecimalTokenA.address, erc20ZeroDecimalTokenB.address], erc721Token, erc721Balances, exchangeContract.address, diff --git a/packages/contracts/test/utils/order_factory_from_scenario.ts b/packages/contracts/test/utils/order_factory_from_scenario.ts index a908140b9..8e04db588 100644 --- a/packages/contracts/test/utils/order_factory_from_scenario.ts +++ b/packages/contracts/test/utils/order_factory_from_scenario.ts @@ -21,6 +21,8 @@ const POINT_ONE_UNITS_EIGHTEEN_DECIMALS = new BigNumber(100_000_000_000_000_000) const POINT_ZERO_FIVE_UNITS_EIGHTEEN_DECIMALS = new BigNumber(50_000_000_000_000_000); const TEN_UNITS_FIVE_DECIMALS = new BigNumber(1_000_000); const FIVE_UNITS_FIVE_DECIMALS = new BigNumber(500_000); +const TEN_UNITS_ZERO_DECIMALS = new BigNumber(10); +const ONE_THOUSAND_UNITS_ZERO_DECIMALS = new BigNumber(1000); const ONE_NFT_UNIT = new BigNumber(1); export class OrderFactoryFromScenario { @@ -28,6 +30,7 @@ export class OrderFactoryFromScenario { private readonly _zrxAddress: string; private readonly _nonZrxERC20EighteenDecimalTokenAddresses: string[]; private readonly _erc20FiveDecimalTokenAddresses: string[]; + private readonly _erc20ZeroDecimalTokenAddresses: string[]; private readonly _erc721Token: DummyERC721TokenContract; private readonly _erc721Balances: ERC721TokenIdsByOwner; private readonly _exchangeAddress: string; @@ -36,6 +39,7 @@ export class OrderFactoryFromScenario { zrxAddress: string, nonZrxERC20EighteenDecimalTokenAddresses: string[], erc20FiveDecimalTokenAddresses: string[], + erc20ZeroDecimalTokenAddresses: string[], erc721Token: DummyERC721TokenContract, erc721Balances: ERC721TokenIdsByOwner, exchangeAddress: string, @@ -44,6 +48,7 @@ export class OrderFactoryFromScenario { this._zrxAddress = zrxAddress; this._nonZrxERC20EighteenDecimalTokenAddresses = nonZrxERC20EighteenDecimalTokenAddresses; this._erc20FiveDecimalTokenAddresses = erc20FiveDecimalTokenAddresses; + this._erc20ZeroDecimalTokenAddresses = erc20ZeroDecimalTokenAddresses; this._erc721Token = erc721Token; this._erc721Balances = erc721Balances; this._exchangeAddress = exchangeAddress; @@ -89,6 +94,9 @@ export class OrderFactoryFromScenario { erc721MakerAssetIds[0], ); break; + case AssetDataScenario.ERC20ZeroDecimals: + makerAssetData = assetDataUtils.encodeERC20AssetData(this._erc20ZeroDecimalTokenAddresses[0]); + break; default: throw errorUtils.spawnSwitchErr('AssetDataScenario', orderScenario.makerAssetDataScenario); } @@ -109,6 +117,9 @@ export class OrderFactoryFromScenario { erc721TakerAssetIds[0], ); break; + case AssetDataScenario.ERC20ZeroDecimals: + takerAssetData = assetDataUtils.encodeERC20AssetData(this._erc20ZeroDecimalTokenAddresses[1]); + break; default: throw errorUtils.spawnSwitchErr('AssetDataScenario', orderScenario.takerAssetDataScenario); } @@ -126,6 +137,9 @@ export class OrderFactoryFromScenario { case AssetDataScenario.ERC721: makerAssetAmount = ONE_NFT_UNIT; break; + case AssetDataScenario.ERC20ZeroDecimals: + makerAssetAmount = ONE_THOUSAND_UNITS_ZERO_DECIMALS; + break; default: throw errorUtils.spawnSwitchErr('AssetDataScenario', orderScenario.makerAssetDataScenario); } @@ -142,6 +156,9 @@ export class OrderFactoryFromScenario { case AssetDataScenario.ERC721: makerAssetAmount = ONE_NFT_UNIT; break; + case AssetDataScenario.ERC20ZeroDecimals: + makerAssetAmount = TEN_UNITS_ZERO_DECIMALS; + break; default: throw errorUtils.spawnSwitchErr('AssetDataScenario', orderScenario.makerAssetDataScenario); } @@ -166,6 +183,9 @@ export class OrderFactoryFromScenario { case AssetDataScenario.ERC721: takerAssetAmount = ONE_NFT_UNIT; break; + case AssetDataScenario.ERC20ZeroDecimals: + takerAssetAmount = ONE_THOUSAND_UNITS_ZERO_DECIMALS; + break; default: throw errorUtils.spawnSwitchErr('AssetDataScenario', orderScenario.takerAssetDataScenario); } @@ -182,6 +202,9 @@ export class OrderFactoryFromScenario { case AssetDataScenario.ERC721: takerAssetAmount = ONE_NFT_UNIT; break; + case AssetDataScenario.ERC20ZeroDecimals: + takerAssetAmount = TEN_UNITS_ZERO_DECIMALS; + break; default: throw errorUtils.spawnSwitchErr('AssetDataScenario', orderScenario.takerAssetDataScenario); } diff --git a/packages/contracts/test/utils/types.ts b/packages/contracts/test/utils/types.ts index 67313b647..481ee87d6 100644 --- a/packages/contracts/test/utils/types.ts +++ b/packages/contracts/test/utils/types.ts @@ -177,10 +177,11 @@ export enum ExpirationTimeSecondsScenario { } export enum AssetDataScenario { - ERC721 = 'ERC721', + ERC20ZeroDecimals = 'ERC20_ZERO_DECIMALS', ZRXFeeToken = 'ZRX_FEE_TOKEN', ERC20FiveDecimals = 'ERC20_FIVE_DECIMALS', ERC20NonZRXEighteenDecimals = 'ERC20_NON_ZRX_EIGHTEEN_DECIMALS', + ERC721 = 'ERC721', } export enum TakerAssetFillAmountScenario { diff --git a/packages/order-utils/CHANGELOG.json b/packages/order-utils/CHANGELOG.json index a2dcf13c4..86f0da65a 100644 --- a/packages/order-utils/CHANGELOG.json +++ b/packages/order-utils/CHANGELOG.json @@ -1,5 +1,23 @@ [ { + "version": "1.0.1-rc.4", + "changes": [ + { + "note": "Remove rounding error being thrown when maker amount is very small", + "pr": 959 + }, + { + "note": "Added rateUtils and sortingUtils", + "pr": 953 + }, + { + "note": + "Update marketUtils api such that all optional parameters are bundled into one optional param and more defaults are provided", + "pr": 954 + } + ] + }, + { "version": "1.0.1-rc.3", "changes": [ { @@ -18,10 +36,6 @@ }, { "note": "Dependencies updated" - }, - { - "note": "Added rateUtils and sortingUtils", - "pr": 953 } ], "timestamp": 1534210131 diff --git a/packages/order-utils/src/market_utils.ts b/packages/order-utils/src/market_utils.ts index 681059ddf..a0a827546 100644 --- a/packages/order-utils/src/market_utils.ts +++ b/packages/order-utils/src/market_utils.ts @@ -1,46 +1,51 @@ import { schemas } from '@0xproject/json-schemas'; -import { SignedOrder } from '@0xproject/types'; +import { Order } from '@0xproject/types'; import { BigNumber } from '@0xproject/utils'; import * as _ from 'lodash'; import { assert } from './assert'; import { constants } from './constants'; +import { FindFeeOrdersThatCoverFeesForTargetOrdersOpts, FindOrdersThatCoverMakerAssetFillAmountOpts } from './types'; export const marketUtils = { /** - * Takes an array of orders and returns a subset of those orders that has enough makerAssetAmount (taking into account on-chain balances, - * allowances, and partial fills) in order to fill the input makerAssetFillAmount plus slippageBufferAmount. Iterates from first order to last. + * Takes an array of orders and returns a subset of those orders that has enough makerAssetAmount + * in order to fill the input makerAssetFillAmount plus slippageBufferAmount. Iterates from first order to last order. * Sort the input by ascending rate in order to get the subset of orders that will cost the least ETH. - * @param signedOrders An array of objects that conform to the SignedOrder interface. All orders should specify the same makerAsset. - * All orders should specify WETH as the takerAsset. - * @param remainingFillableMakerAssetAmounts An array of BigNumbers corresponding to the signedOrders parameter. - * You can use OrderStateUtils @0xproject/order-utils to perform blockchain lookups - * for these values. - * @param makerAssetFillAmount The amount of makerAsset desired to be filled. - * @param slippageBufferAmount An additional amount of makerAsset to be covered by the result in case of trade collisions or partial fills. + * @param orders An array of objects that extend the Order interface. All orders should specify the same makerAsset. + * All orders should specify WETH as the takerAsset. + * @param makerAssetFillAmount The amount of makerAsset desired to be filled. + * @param opts Optional arguments this function accepts. * @return Resulting orders and remaining fill amount that could not be covered by the input. */ - findOrdersThatCoverMakerAssetFillAmount( - signedOrders: SignedOrder[], - remainingFillableMakerAssetAmounts: BigNumber[], + findOrdersThatCoverMakerAssetFillAmount<T extends Order>( + orders: T[], makerAssetFillAmount: BigNumber, - slippageBufferAmount: BigNumber = constants.ZERO_AMOUNT, - ): { resultOrders: SignedOrder[]; remainingFillAmount: BigNumber } { - assert.doesConformToSchema('signedOrders', signedOrders, schemas.signedOrdersSchema); + opts?: FindOrdersThatCoverMakerAssetFillAmountOpts, + ): { resultOrders: T[]; remainingFillAmount: BigNumber } { + assert.doesConformToSchema('orders', orders, schemas.ordersSchema); + assert.isValidBaseUnitAmount('makerAssetFillAmount', makerAssetFillAmount); + // try to get remainingFillableMakerAssetAmounts from opts, if it's not there, use makerAssetAmount values from orders + const remainingFillableMakerAssetAmounts = _.get( + opts, + 'remainingFillableMakerAssetAmounts', + _.map(orders, order => order.makerAssetAmount), + ) as BigNumber[]; _.forEach(remainingFillableMakerAssetAmounts, (amount, index) => assert.isValidBaseUnitAmount(`remainingFillableMakerAssetAmount[${index}]`, amount), ); - assert.isValidBaseUnitAmount('makerAssetFillAmount', makerAssetFillAmount); - assert.isValidBaseUnitAmount('slippageBufferAmount', slippageBufferAmount); assert.assert( - signedOrders.length === remainingFillableMakerAssetAmounts.length, - 'Expected signedOrders.length to equal remainingFillableMakerAssetAmounts.length', + orders.length === remainingFillableMakerAssetAmounts.length, + 'Expected orders.length to equal opts.remainingFillableMakerAssetAmounts.length', ); + // try to get slippageBufferAmount from opts, if it's not there, default to 0 + const slippageBufferAmount = _.get(opts, 'slippageBufferAmount', constants.ZERO_AMOUNT) as BigNumber; + assert.isValidBaseUnitAmount('opts.slippageBufferAmount', slippageBufferAmount); // calculate total amount of makerAsset needed to be filled const totalFillAmount = makerAssetFillAmount.plus(slippageBufferAmount); - // iterate through the signedOrders input from left to right until we have enough makerAsset to fill totalFillAmount + // iterate through the orders input from left to right until we have enough makerAsset to fill totalFillAmount const result = _.reduce( - signedOrders, + orders, ({ resultOrders, remainingFillAmount }, order, index) => { if (remainingFillAmount.lessThanOrEqualTo(constants.ZERO_AMOUNT)) { return { resultOrders, remainingFillAmount: constants.ZERO_AMOUNT }; @@ -59,55 +64,61 @@ export const marketUtils = { }; } }, - { resultOrders: [] as SignedOrder[], remainingFillAmount: totalFillAmount }, + { resultOrders: [] as T[], remainingFillAmount: totalFillAmount }, ); return result; }, /** - * Takes an array of orders and an array of feeOrders. Returns a subset of the feeOrders that has enough ZRX (taking into account - * on-chain balances, allowances, and partial fills) in order to fill the takerFees required by signedOrders plus a - * slippageBufferAmount. Iterates from first feeOrder to last. Sort the feeOrders by ascending rate in order to get the subset of + * Takes an array of orders and an array of feeOrders. Returns a subset of the feeOrders that has enough ZRX + * in order to fill the takerFees required by orders plus a slippageBufferAmount. + * Iterates from first feeOrder to last. Sort the feeOrders by ascending rate in order to get the subset of * feeOrders that will cost the least ETH. - * @param signedOrders An array of objects that conform to the SignedOrder interface. All orders should specify ZRX as - * the makerAsset and WETH as the takerAsset. - * @param remainingFillableMakerAssetAmounts An array of BigNumbers corresponding to the signedOrders parameter. - * You can use OrderStateUtils @0xproject/order-utils to perform blockchain lookups - * for these values. - * @param signedFeeOrders An array of objects that conform to the SignedOrder interface. All orders should specify ZRX as - * the makerAsset and WETH as the takerAsset. - * @param remainingFillableFeeAmounts An array of BigNumbers corresponding to the signedFeeOrders parameter. - * You can use OrderStateUtils @0xproject/order-utils to perform blockchain lookups - * for these values. - * @param slippageBufferAmount An additional amount of fee to be covered by the result in case of trade collisions or partial fills. + * @param orders An array of objects that extend the Order interface. All orders should specify ZRX as + * the makerAsset and WETH as the takerAsset. + * @param feeOrders An array of objects that extend the Order interface. All orders should specify ZRX as + * the makerAsset and WETH as the takerAsset. + * @param opts Optional arguments this function accepts. * @return Resulting orders and remaining fee amount that could not be covered by the input. */ - findFeeOrdersThatCoverFeesForTargetOrders( - signedOrders: SignedOrder[], - remainingFillableMakerAssetAmounts: BigNumber[], - signedFeeOrders: SignedOrder[], - remainingFillableFeeAmounts: BigNumber[], - slippageBufferAmount: BigNumber = constants.ZERO_AMOUNT, - ): { resultOrders: SignedOrder[]; remainingFeeAmount: BigNumber } { - assert.doesConformToSchema('signedOrders', signedOrders, schemas.signedOrdersSchema); + findFeeOrdersThatCoverFeesForTargetOrders<T extends Order>( + orders: T[], + feeOrders: T[], + opts?: FindFeeOrdersThatCoverFeesForTargetOrdersOpts, + ): { resultOrders: T[]; remainingFeeAmount: BigNumber } { + assert.doesConformToSchema('orders', orders, schemas.ordersSchema); + assert.doesConformToSchema('feeOrders', feeOrders, schemas.ordersSchema); + // try to get remainingFillableMakerAssetAmounts from opts, if it's not there, use makerAssetAmount values from orders + const remainingFillableMakerAssetAmounts = _.get( + opts, + 'remainingFillableMakerAssetAmounts', + _.map(orders, order => order.makerAssetAmount), + ) as BigNumber[]; _.forEach(remainingFillableMakerAssetAmounts, (amount, index) => assert.isValidBaseUnitAmount(`remainingFillableMakerAssetAmount[${index}]`, amount), ); - assert.doesConformToSchema('signedFeeOrders', signedFeeOrders, schemas.signedOrdersSchema); + assert.assert( + orders.length === remainingFillableMakerAssetAmounts.length, + 'Expected orders.length to equal opts.remainingFillableMakerAssetAmounts.length', + ); + // try to get remainingFillableFeeAmounts from opts, if it's not there, use makerAssetAmount values from feeOrders + const remainingFillableFeeAmounts = _.get( + opts, + 'remainingFillableFeeAmounts', + _.map(feeOrders, order => order.makerAssetAmount), + ) as BigNumber[]; _.forEach(remainingFillableFeeAmounts, (amount, index) => assert.isValidBaseUnitAmount(`remainingFillableFeeAmounts[${index}]`, amount), ); - assert.isValidBaseUnitAmount('slippageBufferAmount', slippageBufferAmount); assert.assert( - signedOrders.length === remainingFillableMakerAssetAmounts.length, - 'Expected signedOrders.length to equal remainingFillableMakerAssetAmounts.length', + feeOrders.length === remainingFillableFeeAmounts.length, + 'Expected feeOrders.length to equal opts.remainingFillableFeeAmounts.length', ); - assert.assert( - signedOrders.length === remainingFillableMakerAssetAmounts.length, - 'Expected signedFeeOrders.length to equal remainingFillableFeeAmounts.length', - ); - // calculate total amount of ZRX needed to fill signedOrders + // try to get slippageBufferAmount from opts, if it's not there, default to 0 + const slippageBufferAmount = _.get(opts, 'slippageBufferAmount', constants.ZERO_AMOUNT) as BigNumber; + assert.isValidBaseUnitAmount('opts.slippageBufferAmount', slippageBufferAmount); + // calculate total amount of ZRX needed to fill orders const totalFeeAmount = _.reduce( - signedOrders, + orders, (accFees, order, index) => { const makerAssetAmountAvailable = remainingFillableMakerAssetAmounts[index]; const feeToFillMakerAssetAmountAvailable = makerAssetAmountAvailable @@ -118,10 +129,12 @@ export const marketUtils = { constants.ZERO_AMOUNT, ); const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount( - signedFeeOrders, - remainingFillableFeeAmounts, + feeOrders, totalFeeAmount, - slippageBufferAmount, + { + remainingFillableMakerAssetAmounts: remainingFillableFeeAmounts, + slippageBufferAmount, + }, ); return { resultOrders, diff --git a/packages/order-utils/src/order_state_utils.ts b/packages/order-utils/src/order_state_utils.ts index 189bf4180..18fc18bf6 100644 --- a/packages/order-utils/src/order_state_utils.ts +++ b/packages/order-utils/src/order_state_utils.ts @@ -11,6 +11,7 @@ import { BigNumber } from '@0xproject/utils'; import { AbstractBalanceAndProxyAllowanceFetcher } from './abstract/abstract_balance_and_proxy_allowance_fetcher'; import { AbstractOrderFilledCancelledFetcher } from './abstract/abstract_order_filled_cancelled_fetcher'; import { orderHashUtils } from './order_hash'; +import { OrderValidationUtils } from './order_validation_utils'; import { RemainingFillableCalculator } from './remaining_fillable_calculator'; import { utils } from './utils'; @@ -22,9 +23,16 @@ interface SidedOrderRelevantState { traderFeeProxyAllowance: BigNumber; filledTakerAssetAmount: BigNumber; remainingFillableAssetAmount: BigNumber; + isOrderCancelled: boolean; } - -const ACCEPTABLE_RELATIVE_ROUNDING_ERROR = 0.0001; +interface OrderValidResult { + isValid: true; +} +interface OrderInvalidResult { + isValid: false; + error: ExchangeContractErrs; +} +type OrderValidationResult = OrderValidResult | OrderInvalidResult; export class OrderStateUtils { private readonly _balanceAndProxyAllowanceFetcher: AbstractBalanceAndProxyAllowanceFetcher; @@ -32,64 +40,56 @@ export class OrderStateUtils { private static _validateIfOrderIsValid( signedOrder: SignedOrder, sidedOrderRelevantState: SidedOrderRelevantState, - ): void { + ): OrderValidationResult { const isMakerSide = sidedOrderRelevantState.isMakerSide; + if (sidedOrderRelevantState.isOrderCancelled) { + return { isValid: false, error: ExchangeContractErrs.OrderCancelled }; + } const availableTakerAssetAmount = signedOrder.takerAssetAmount.minus( sidedOrderRelevantState.filledTakerAssetAmount, ); if (availableTakerAssetAmount.eq(0)) { - throw new Error(ExchangeContractErrs.OrderRemainingFillAmountZero); + return { isValid: false, error: ExchangeContractErrs.OrderRemainingFillAmountZero }; } if (sidedOrderRelevantState.traderBalance.eq(0)) { - throw new Error( - isMakerSide - ? ExchangeContractErrs.InsufficientMakerBalance - : ExchangeContractErrs.InsufficientTakerBalance, - ); + const error = isMakerSide + ? ExchangeContractErrs.InsufficientMakerBalance + : ExchangeContractErrs.InsufficientTakerBalance; + return { isValid: false, error }; } if (sidedOrderRelevantState.traderProxyAllowance.eq(0)) { - throw new Error( - isMakerSide - ? ExchangeContractErrs.InsufficientMakerAllowance - : ExchangeContractErrs.InsufficientTakerAllowance, - ); + const error = isMakerSide + ? ExchangeContractErrs.InsufficientMakerAllowance + : ExchangeContractErrs.InsufficientTakerAllowance; + return { isValid: false, error }; } if (!signedOrder.makerFee.eq(0)) { if (sidedOrderRelevantState.traderFeeBalance.eq(0)) { - throw new Error( - isMakerSide - ? ExchangeContractErrs.InsufficientMakerFeeBalance - : ExchangeContractErrs.InsufficientTakerFeeBalance, - ); + const error = isMakerSide + ? ExchangeContractErrs.InsufficientMakerFeeBalance + : ExchangeContractErrs.InsufficientTakerFeeBalance; + return { isValid: false, error }; } if (sidedOrderRelevantState.traderFeeProxyAllowance.eq(0)) { - throw new Error( - isMakerSide - ? ExchangeContractErrs.InsufficientMakerFeeAllowance - : ExchangeContractErrs.InsufficientTakerFeeAllowance, - ); + const error = isMakerSide + ? ExchangeContractErrs.InsufficientMakerFeeAllowance + : ExchangeContractErrs.InsufficientTakerFeeAllowance; + return { isValid: false, error }; } } - - let minFillableTakerAssetAmountWithinNoRoundingErrorRange; - if (isMakerSide) { - minFillableTakerAssetAmountWithinNoRoundingErrorRange = signedOrder.takerAssetAmount - .dividedBy(ACCEPTABLE_RELATIVE_ROUNDING_ERROR) - .dividedBy(signedOrder.makerAssetAmount); - } else { - minFillableTakerAssetAmountWithinNoRoundingErrorRange = signedOrder.makerAssetAmount - .dividedBy(ACCEPTABLE_RELATIVE_ROUNDING_ERROR) - .dividedBy(signedOrder.takerAssetAmount); - } - - if ( - sidedOrderRelevantState.remainingFillableAssetAmount.lessThan( - minFillableTakerAssetAmountWithinNoRoundingErrorRange, - ) - ) { - throw new Error(ExchangeContractErrs.OrderFillRoundingError); + const remainingTakerAssetAmount = signedOrder.takerAssetAmount.minus( + sidedOrderRelevantState.filledTakerAssetAmount, + ); + const isRoundingError = OrderValidationUtils.isRoundingError( + remainingTakerAssetAmount, + signedOrder.takerAssetAmount, + signedOrder.makerAssetAmount, + ); + if (isRoundingError) { + return { isValid: false, error: ExchangeContractErrs.OrderFillRoundingError }; } + return { isValid: true }; } constructor( balanceAndProxyAllowanceFetcher: AbstractBalanceAndProxyAllowanceFetcher, @@ -101,6 +101,7 @@ export class OrderStateUtils { public async getOpenOrderStateAsync(signedOrder: SignedOrder): Promise<OrderState> { const orderRelevantState = await this.getOpenOrderRelevantStateAsync(signedOrder); const orderHash = orderHashUtils.getOrderHashHex(signedOrder); + const isOrderCancelled = await this._orderFilledCancelledFetcher.isOrderCancelledAsync(orderHash); const sidedOrderRelevantState = { isMakerSide: true, traderBalance: orderRelevantState.makerBalance, @@ -109,20 +110,21 @@ export class OrderStateUtils { traderFeeProxyAllowance: orderRelevantState.makerFeeProxyAllowance, filledTakerAssetAmount: orderRelevantState.filledTakerAssetAmount, remainingFillableAssetAmount: orderRelevantState.remainingFillableMakerAssetAmount, + isOrderCancelled, }; - try { - OrderStateUtils._validateIfOrderIsValid(signedOrder, sidedOrderRelevantState); + const orderValidationResult = OrderStateUtils._validateIfOrderIsValid(signedOrder, sidedOrderRelevantState); + if (orderValidationResult.isValid) { const orderState: OrderStateValid = { isValid: true, orderHash, orderRelevantState, }; return orderState; - } catch (err) { + } else { const orderState: OrderStateInvalid = { isValid: false, orderHash, - error: err.message, + error: orderValidationResult.error, }; return orderState; } @@ -278,6 +280,7 @@ export class OrderStateUtils { traderFeeProxyAllowance, filledTakerAssetAmount, remainingFillableAssetAmount, + isOrderCancelled, }; return sidedOrderRelevantState; } diff --git a/packages/order-utils/src/types.ts b/packages/order-utils/src/types.ts index 1fbd8cf7b..2e9c79d80 100644 --- a/packages/order-utils/src/types.ts +++ b/packages/order-utils/src/types.ts @@ -41,3 +41,31 @@ export interface CreateOrderOpts { salt?: BigNumber; expirationTimeSeconds?: BigNumber; } + +/** + * remainingFillableMakerAssetAmount: An array of BigNumbers corresponding to the `orders` parameter. + * You can use `OrderStateUtils` `@0xproject/order-utils` to perform blockchain lookups for these values. + * Defaults to `makerAssetAmount` values from the orders param. + * slippageBufferAmount: An additional amount of makerAsset to be covered by the result in case of trade collisions or partial fills. + * Defaults to 0 + */ +export interface FindOrdersThatCoverMakerAssetFillAmountOpts { + remainingFillableMakerAssetAmounts?: BigNumber[]; + slippageBufferAmount?: BigNumber; +} + +/** + * remainingFillableMakerAssetAmount: An array of BigNumbers corresponding to the `orders` parameter. + * You can use `OrderStateUtils` `@0xproject/order-utils` to perform blockchain lookups for these values. + * Defaults to `makerAssetAmount` values from the orders param. + * remainingFillableFeeAmounts: An array of BigNumbers corresponding to the feeOrders parameter. + * You can use OrderStateUtils @0xproject/order-utils to perform blockchain lookups for these values. + * Defaults to `makerAssetAmount` values from the feeOrders param. + * slippageBufferAmount: An additional amount of fee to be covered by the result in case of trade collisions or partial fills. + * Defaults to 0 + */ +export interface FindFeeOrdersThatCoverFeesForTargetOrdersOpts { + remainingFillableMakerAssetAmounts?: BigNumber[]; + remainingFillableFeeAmounts?: BigNumber[]; + slippageBufferAmount?: BigNumber; +} diff --git a/packages/order-utils/test/market_utils_test.ts b/packages/order-utils/test/market_utils_test.ts index 21c0a4802..109420a02 100644 --- a/packages/order-utils/test/market_utils_test.ts +++ b/packages/order-utils/test/market_utils_test.ts @@ -18,7 +18,6 @@ describe('marketUtils', () => { const fillAmount = new BigNumber(10); const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount( [], - [], fillAmount, ); expect(resultOrders).to.be.empty; @@ -34,8 +33,6 @@ describe('marketUtils', () => { }, 3, ); - // generate remainingFillableMakerAssetAmounts that equal the makerAssetAmount - const remainingFillableMakerAssetAmounts = [makerAssetAmount, makerAssetAmount, makerAssetAmount]; it('returns input orders and zero remainingFillAmount when input exactly matches requested fill amount', async () => { // try to fill 20 units of makerAsset // include 10 units of slippageBufferAmount @@ -43,9 +40,10 @@ describe('marketUtils', () => { const slippageBufferAmount = new BigNumber(10); const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount( inputOrders, - remainingFillableMakerAssetAmounts, fillAmount, - slippageBufferAmount, + { + slippageBufferAmount, + }, ); expect(resultOrders).to.be.deep.equal(inputOrders); expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT); @@ -57,9 +55,10 @@ describe('marketUtils', () => { const slippageBufferAmount = new BigNumber(10); const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount( inputOrders, - remainingFillableMakerAssetAmounts, fillAmount, - slippageBufferAmount, + { + slippageBufferAmount, + }, ); expect(resultOrders).to.be.deep.equal(inputOrders); expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT); @@ -71,9 +70,10 @@ describe('marketUtils', () => { const slippageBufferAmount = new BigNumber(5); const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount( inputOrders, - remainingFillableMakerAssetAmounts, fillAmount, - slippageBufferAmount, + { + slippageBufferAmount, + }, ); expect(resultOrders).to.be.deep.equal(inputOrders); expect(remainingFillAmount).to.be.bignumber.equal(new BigNumber(5)); @@ -83,7 +83,6 @@ describe('marketUtils', () => { const fillAmount = new BigNumber(10); const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount( inputOrders, - remainingFillableMakerAssetAmounts, fillAmount, ); expect(resultOrders).to.be.deep.equal([inputOrders[0]]); @@ -94,7 +93,6 @@ describe('marketUtils', () => { const fillAmount = new BigNumber(15); const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount( inputOrders, - remainingFillableMakerAssetAmounts, fillAmount, ); expect(resultOrders).to.be.deep.equal([inputOrders[0], inputOrders[1]]); @@ -120,8 +118,10 @@ describe('marketUtils', () => { const fillAmount = new BigNumber(30); const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount( inputOrders, - remainingFillableMakerAssetAmounts, fillAmount, + { + remainingFillableMakerAssetAmounts, + }, ); expect(resultOrders).to.be.deep.equal([inputOrders[1], inputOrders[2]]); expect(remainingFillAmount).to.be.bignumber.equal(new BigNumber(15)); @@ -137,15 +137,11 @@ describe('marketUtils', () => { }, 3, ); - // generate remainingFillableFeeAmounts that equal the zrxAmount - const remainingFillableFeeAmounts = [zrxAmount, zrxAmount, zrxAmount]; describe('no target orders', () => { it('returns empty and zero remainingFeeAmount', async () => { const { resultOrders, remainingFeeAmount } = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders( [], - [], inputFeeOrders, - remainingFillableFeeAmounts, ); expect(resultOrders).to.be.empty; expect(remainingFeeAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT); @@ -168,9 +164,10 @@ describe('marketUtils', () => { it('returns empty and non-zero remainingFeeAmount', async () => { const { resultOrders, remainingFeeAmount } = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders( inputOrders, - remainingFillableMakerAssetAmounts, - [], [], + { + remainingFillableMakerAssetAmounts, + }, ); expect(resultOrders).to.be.empty; expect(remainingFeeAmount).to.be.bignumber.equal(new BigNumber(30)); @@ -185,14 +182,10 @@ describe('marketUtils', () => { }, 3, ); - // generate remainingFillableMakerAssetAmounts that equal the makerAssetAmount - const remainingFillableMakerAssetAmounts = [makerAssetAmount, makerAssetAmount, makerAssetAmount]; it('returns empty and zero remainingFeeAmount', async () => { const { resultOrders, remainingFeeAmount } = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders( inputOrders, - remainingFillableMakerAssetAmounts, inputFeeOrders, - remainingFillableFeeAmounts, ); expect(resultOrders).to.be.empty; expect(remainingFeeAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT); @@ -210,14 +203,10 @@ describe('marketUtils', () => { }, 3, ); - // generate remainingFillableMakerAssetAmounts that equal the makerAssetAmount - const remainingFillableMakerAssetAmounts = [makerAssetAmount, makerAssetAmount, makerAssetAmount]; it('returns input fee orders and zero remainingFeeAmount', async () => { const { resultOrders, remainingFeeAmount } = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders( inputOrders, - remainingFillableMakerAssetAmounts, inputFeeOrders, - remainingFillableFeeAmounts, ); expect(resultOrders).to.be.deep.equal(inputFeeOrders); expect(remainingFeeAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT); @@ -243,9 +232,10 @@ describe('marketUtils', () => { it('returns first two input fee orders and zero remainingFeeAmount', async () => { const { resultOrders, remainingFeeAmount } = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders( inputOrders, - remainingFillableMakerAssetAmounts, inputFeeOrders, - remainingFillableFeeAmounts, + { + remainingFillableMakerAssetAmounts, + }, ); expect(resultOrders).to.be.deep.equal([inputFeeOrders[0], inputFeeOrders[1]]); expect(remainingFeeAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT); @@ -263,14 +253,10 @@ describe('marketUtils', () => { }, 3, ); - // generate remainingFillableMakerAssetAmounts that equal the makerAssetAmount - const remainingFillableMakerAssetAmounts = [makerAssetAmount, makerAssetAmount, makerAssetAmount]; it('returns input fee orders and non-zero remainingFeeAmount', async () => { const { resultOrders, remainingFeeAmount } = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders( inputOrders, - remainingFillableMakerAssetAmounts, inputFeeOrders, - remainingFillableFeeAmounts, ); expect(resultOrders).to.be.deep.equal(inputFeeOrders); expect(remainingFeeAmount).to.be.bignumber.equal(new BigNumber(30)); diff --git a/packages/order-utils/test/order_state_utils_test.ts b/packages/order-utils/test/order_state_utils_test.ts new file mode 100644 index 000000000..91ef23b69 --- /dev/null +++ b/packages/order-utils/test/order_state_utils_test.ts @@ -0,0 +1,124 @@ +import { BigNumber } from '@0xproject/utils'; +import * as chai from 'chai'; +import 'mocha'; + +import { AbstractBalanceAndProxyAllowanceFetcher } from '../src/abstract/abstract_balance_and_proxy_allowance_fetcher'; +import { AbstractOrderFilledCancelledFetcher } from '../src/abstract/abstract_order_filled_cancelled_fetcher'; +import { OrderStateUtils } from '../src/order_state_utils'; + +import { chaiSetup } from './utils/chai_setup'; +import { testOrderFactory } from './utils/test_order_factory'; + +chaiSetup.configure(); +const expect = chai.expect; + +describe('OrderStateUtils', () => { + describe('#getOpenOrderStateAsync', () => { + const buildMockBalanceFetcher = (takerBalance: BigNumber): AbstractBalanceAndProxyAllowanceFetcher => { + const balanceFetcher = { + async getBalanceAsync(_assetData: string, _userAddress: string): Promise<BigNumber> { + return takerBalance; + }, + async getProxyAllowanceAsync(_assetData: string, _userAddress: string): Promise<BigNumber> { + return takerBalance; + }, + }; + return balanceFetcher; + }; + const buildMockOrderFilledFetcher = ( + filledAmount: BigNumber = new BigNumber(0), + cancelled: boolean = false, + ): AbstractOrderFilledCancelledFetcher => { + const orderFetcher = { + async getFilledTakerAmountAsync(_orderHash: string): Promise<BigNumber> { + return filledAmount; + }, + async isOrderCancelledAsync(_orderHash: string): Promise<boolean> { + return cancelled; + }, + getZRXAssetData(): string { + return ''; + }, + }; + return orderFetcher; + }; + it('should have valid order state if order can be fully filled with small maker amount', async () => { + const makerAssetAmount = new BigNumber(10); + const takerAssetAmount = new BigNumber(10000000000000000); + const takerBalance = takerAssetAmount; + const orderFilledAmount = new BigNumber(0); + const mockBalanceFetcher = buildMockBalanceFetcher(takerBalance); + const mockOrderFilledFetcher = buildMockOrderFilledFetcher(orderFilledAmount); + const [signedOrder] = testOrderFactory.generateTestSignedOrders( + { + makerAssetAmount, + takerAssetAmount, + }, + 1, + ); + + const orderStateUtils = new OrderStateUtils(mockBalanceFetcher, mockOrderFilledFetcher); + const orderState = await orderStateUtils.getOpenOrderStateAsync(signedOrder); + expect(orderState.isValid).to.eq(true); + }); + it('should be invalid when an order is partially filled where only a rounding error remains', async () => { + const makerAssetAmount = new BigNumber(1001); + const takerAssetAmount = new BigNumber(3); + const takerBalance = takerAssetAmount; + const orderFilledAmount = new BigNumber(2); + const mockBalanceFetcher = buildMockBalanceFetcher(takerBalance); + const mockOrderFilledFetcher = buildMockOrderFilledFetcher(orderFilledAmount); + const [signedOrder] = testOrderFactory.generateTestSignedOrders( + { + makerAssetAmount, + takerAssetAmount, + }, + 1, + ); + + const orderStateUtils = new OrderStateUtils(mockBalanceFetcher, mockOrderFilledFetcher); + const orderState = await orderStateUtils.getOpenOrderStateAsync(signedOrder); + expect(orderState.isValid).to.eq(false); + }); + it('should be invalid when an order is cancelled', async () => { + const makerAssetAmount = new BigNumber(1000); + const takerAssetAmount = new BigNumber(2); + const takerBalance = takerAssetAmount; + const orderFilledAmount = new BigNumber(0); + const isCancelled = true; + const mockBalanceFetcher = buildMockBalanceFetcher(takerBalance); + const mockOrderFilledFetcher = buildMockOrderFilledFetcher(orderFilledAmount, isCancelled); + const [signedOrder] = testOrderFactory.generateTestSignedOrders( + { + makerAssetAmount, + takerAssetAmount, + }, + 1, + ); + + const orderStateUtils = new OrderStateUtils(mockBalanceFetcher, mockOrderFilledFetcher); + const orderState = await orderStateUtils.getOpenOrderStateAsync(signedOrder); + expect(orderState.isValid).to.eq(false); + }); + it('should be invalid when an order is fully filled', async () => { + const makerAssetAmount = new BigNumber(1000); + const takerAssetAmount = new BigNumber(2); + const takerBalance = takerAssetAmount; + const orderFilledAmount = takerAssetAmount; + const isCancelled = false; + const mockBalanceFetcher = buildMockBalanceFetcher(takerBalance); + const mockOrderFilledFetcher = buildMockOrderFilledFetcher(orderFilledAmount, isCancelled); + const [signedOrder] = testOrderFactory.generateTestSignedOrders( + { + makerAssetAmount, + takerAssetAmount, + }, + 1, + ); + + const orderStateUtils = new OrderStateUtils(mockBalanceFetcher, mockOrderFilledFetcher); + const orderState = await orderStateUtils.getOpenOrderStateAsync(signedOrder); + expect(orderState.isValid).to.eq(false); + }); + }); +}); diff --git a/packages/order-watcher/test/order_watcher_test.ts b/packages/order-watcher/test/order_watcher_test.ts index 00962bed0..38bfde7ef 100644 --- a/packages/order-watcher/test/order_watcher_test.ts +++ b/packages/order-watcher/test/order_watcher_test.ts @@ -501,25 +501,27 @@ describe('OrderWatcher', () => { expect(orderState.isValid).to.be.false(); const invalidOrderState = orderState as OrderStateInvalid; expect(invalidOrderState.orderHash).to.be.equal(orderHash); - expect(invalidOrderState.error).to.be.equal(ExchangeContractErrs.OrderFillRoundingError); + expect(invalidOrderState.error).to.be.equal(ExchangeContractErrs.OrderCancelled); }); orderWatcher.subscribe(callback); await contractWrappers.exchange.cancelOrderAsync(signedOrder); })().catch(done); }); - it('should emit orderStateInvalid when within rounding error range', (done: DoneCallback) => { + it('should emit orderStateInvalid when within rounding error range after a partial fill', (done: DoneCallback) => { (async () => { - const remainingFillableAmountInBaseUnits = new BigNumber(100); - signedOrder = await fillScenarios.createFillableSignedOrderAsync( + const fillAmountInBaseUnits = new BigNumber(2); + const makerAssetAmount = new BigNumber(1001); + const takerAssetAmount = new BigNumber(3); + signedOrder = await fillScenarios.createAsymmetricFillableSignedOrderAsync( makerAssetData, takerAssetData, makerAddress, takerAddress, - fillableAmount, + makerAssetAmount, + takerAssetAmount, ); const orderHash = orderHashUtils.getOrderHashHex(signedOrder); await orderWatcher.addOrderAsync(signedOrder); - const callback = callbackErrorReporter.reportNodeCallbackErrors(done)((orderState: OrderState) => { expect(orderState.isValid).to.be.false(); const invalidOrderState = orderState as OrderStateInvalid; @@ -527,11 +529,7 @@ describe('OrderWatcher', () => { expect(invalidOrderState.error).to.be.equal(ExchangeContractErrs.OrderFillRoundingError); }); orderWatcher.subscribe(callback); - await contractWrappers.exchange.fillOrderAsync( - signedOrder, - fillableAmount.minus(remainingFillableAmountInBaseUnits), - takerAddress, - ); + await contractWrappers.exchange.fillOrderAsync(signedOrder, fillAmountInBaseUnits, takerAddress); })().catch(done); }); describe('erc721', () => { diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index fa634420d..04480d093 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -62,8 +62,7 @@ export interface ValidatorSignature { export enum ExchangeContractErrs { OrderFillExpired = 'ORDER_FILL_EXPIRED', OrderCancelExpired = 'ORDER_CANCEL_EXPIRED', - OrderCancelAmountZero = 'ORDER_CANCEL_AMOUNT_ZERO', - OrderAlreadyCancelledOrFilled = 'ORDER_ALREADY_CANCELLED_OR_FILLED', + OrderCancelled = 'ORDER_CANCELLED', OrderFillAmountZero = 'ORDER_FILL_AMOUNT_ZERO', OrderRemainingFillAmountZero = 'ORDER_REMAINING_FILL_AMOUNT_ZERO', OrderFillRoundingError = 'ORDER_FILL_ROUNDING_ERROR', diff --git a/packages/website/ts/utils/utils.ts b/packages/website/ts/utils/utils.ts index 39bbd404c..32b07473c 100644 --- a/packages/website/ts/utils/utils.ts +++ b/packages/website/ts/utils/utils.ts @@ -247,12 +247,9 @@ export const utils = { } = { [ExchangeContractErrs.OrderFillExpired]: 'This order has expired', [ExchangeContractErrs.OrderCancelExpired]: 'This order has expired', - [ExchangeContractErrs.OrderCancelAmountZero]: "Order cancel amount can't be 0", - [ExchangeContractErrs.OrderAlreadyCancelledOrFilled]: - 'This order has already been completely filled or cancelled', + [ExchangeContractErrs.OrderCancelled]: 'This order has been cancelled', [ExchangeContractErrs.OrderFillAmountZero]: "Order fill amount can't be 0", - [ExchangeContractErrs.OrderRemainingFillAmountZero]: - 'This order has already been completely filled or cancelled', + [ExchangeContractErrs.OrderRemainingFillAmountZero]: 'This order has already been completely filled', [ExchangeContractErrs.OrderFillRoundingError]: 'Rounding error will occur when filling this order. Please try filling a different amount.', [ExchangeContractErrs.InsufficientTakerBalance]: |