aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--packages/asset-buyer/src/utils/buy_quote_calculator.ts147
-rw-r--r--packages/asset-buyer/test/buy_quote_calculator_test.ts130
2 files changed, 246 insertions, 31 deletions
diff --git a/packages/asset-buyer/src/utils/buy_quote_calculator.ts b/packages/asset-buyer/src/utils/buy_quote_calculator.ts
index b706ea143..53f2228e9 100644
--- a/packages/asset-buyer/src/utils/buy_quote_calculator.ts
+++ b/packages/asset-buyer/src/utils/buy_quote_calculator.ts
@@ -1,4 +1,4 @@
-import { marketUtils } from '@0xproject/order-utils';
+import { marketUtils, rateUtils } from '@0xproject/order-utils';
import { BigNumber } from '@0xproject/utils';
import * as _ from 'lodash';
@@ -21,6 +21,7 @@ export const buyQuoteCalculator = {
const feeOrders = feeOrdersAndFillableAmounts.orders;
const remainingFillableFeeAmounts = feeOrdersAndFillableAmounts.remainingFillableMakerAssetAmounts;
const slippageBufferAmount = assetBuyAmount.mul(slippagePercentage).round();
+ // find the orders that cover the desired assetBuyAmount (with slippage)
const {
resultOrders,
remainingFillAmount,
@@ -29,9 +30,11 @@ export const buyQuoteCalculator = {
remainingFillableMakerAssetAmounts,
slippageBufferAmount,
});
+ // if we do not have enough orders to cover the desired assetBuyAmount, throw
if (remainingFillAmount.gt(constants.ZERO_AMOUNT)) {
throw new Error(AssetBuyerError.InsufficientAssetLiquidity);
}
+ // given the orders calculated above, find the fee-orders that cover the desired assetBuyAmount (with slippage)
// TODO(bmillman): optimization
// update this logic to find the minimum amount of feeOrders to cover the worst case as opposed to
// finding order that cover all fees, this will help with estimating ETH and minimizing gas usage
@@ -40,49 +43,131 @@ export const buyQuoteCalculator = {
remainingFeeAmount,
feeOrdersRemainingFillableMakerAssetAmounts,
} = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders(resultOrders, feeOrders, {
- remainingFillableMakerAssetAmounts,
+ remainingFillableMakerAssetAmounts: ordersRemainingFillableMakerAssetAmounts,
remainingFillableFeeAmounts,
});
+ // if we do not have enough feeOrders to cover the fees, throw
if (remainingFeeAmount.gt(constants.ZERO_AMOUNT)) {
throw new Error(AssetBuyerError.InsufficientZrxLiquidity);
}
+ // assetData information for the result
const assetData = orders[0].makerAssetData;
-
- // calculate minRate and maxRate by calculating min and max eth usage and then dividing into
- // assetBuyAmount to get assetData / WETH, needs to take into account feePercentage as well
- // minEthAmount = (sum(takerAssetAmount[i]) until sum(makerAssetAmount[i]) >= assetBuyAmount ) * (1 + feePercentage)
- // maxEthAmount = (sum(takerAssetAmount[i]) until i == orders.length) * (1 + feePercentage)
- const allOrders = _.concat(resultOrders, resultFeeOrders);
- const allRemainingAmounts = _.concat(
- ordersRemainingFillableMakerAssetAmounts,
- feeOrdersRemainingFillableMakerAssetAmounts,
+ // compile the resulting trimmed set of orders for makerAsset and feeOrders that are needed for assetBuyAmount
+ const trimmedOrdersAndFillableAmounts: OrdersAndFillableAmounts = {
+ orders: resultOrders,
+ remainingFillableMakerAssetAmounts: ordersRemainingFillableMakerAssetAmounts,
+ };
+ const trimmedFeeOrdersAndFillableAmounts: OrdersAndFillableAmounts = {
+ orders: resultFeeOrders,
+ remainingFillableMakerAssetAmounts: feeOrdersRemainingFillableMakerAssetAmounts,
+ };
+ const minRate = calculateRate(
+ trimmedOrdersAndFillableAmounts,
+ trimmedFeeOrdersAndFillableAmounts,
+ assetBuyAmount,
+ feePercentage,
+ );
+ // in order to calculate the maxRate, reverse the ordersAndFillableAmounts such that they are sorted from worst rate to best rate
+ const maxRate = calculateRate(
+ reverseOrdersAndFillableAmounts(trimmedOrdersAndFillableAmounts),
+ reverseOrdersAndFillableAmounts(trimmedFeeOrdersAndFillableAmounts),
+ assetBuyAmount,
+ feePercentage,
);
- let minEthAmount = constants.ZERO_AMOUNT;
- let maxEthAmount = constants.ZERO_AMOUNT;
- let cumulativeMakerAmount = constants.ZERO_AMOUNT;
- _.forEach(allOrders, (order, index) => {
- const remainingFillableMakerAssetAmount = allRemainingAmounts[index];
- const claimableTakerAssetAmount = orderUtils.calculateRemainingTakerAssetAmount(
- order,
- remainingFillableMakerAssetAmount,
- );
- // taker asset is always assumed to be WETH
- maxEthAmount = maxEthAmount.plus(claimableTakerAssetAmount);
- if (cumulativeMakerAmount.lessThan(assetBuyAmount)) {
- minEthAmount = minEthAmount.plus(claimableTakerAssetAmount);
- }
- cumulativeMakerAmount = cumulativeMakerAmount.plus(remainingFillableMakerAssetAmount);
- });
- const feeAdjustedMinRate = minEthAmount.mul(feePercentage + 1).div(assetBuyAmount);
- const feeAdjustedMaxRate = minEthAmount.mul(feePercentage + 1).div(assetBuyAmount);
return {
assetData,
orders: resultOrders,
feeOrders: resultFeeOrders,
- minRate: feeAdjustedMinRate,
- maxRate: feeAdjustedMaxRate,
+ minRate,
+ maxRate,
assetBuyAmount,
feePercentage,
};
},
};
+
+function calculateRate(
+ ordersAndFillableAmounts: OrdersAndFillableAmounts,
+ feeOrdersAndFillableAmounts: OrdersAndFillableAmounts,
+ assetBuyAmount: BigNumber,
+ feePercentage: number,
+): BigNumber {
+ // find the total eth and zrx needed to buy assetAmount from the resultOrders from left to right (best rate to worst rate)
+ const [minEthAmountToBuyAsset, minZrxAmountToBuyAsset] = findEthAndZrxAmountNeededToBuyAsset(
+ ordersAndFillableAmounts,
+ assetBuyAmount,
+ );
+ // find the total eth needed to buy fees
+ const minEthAmountToBuyFees = findEthAmountNeededToBuyFees(feeOrdersAndFillableAmounts, minZrxAmountToBuyAsset);
+ const finalMinEthAmount = minEthAmountToBuyAsset.plus(minEthAmountToBuyFees).mul(feePercentage + 1);
+ // divide into the assetBuyAmount in order to find rate of makerAsset / WETH
+ const result = assetBuyAmount.div(finalMinEthAmount);
+ return result;
+}
+
+// given an OrdersAndFillableAmounts, reverse the orders and remainingFillableMakerAssetAmounts properties
+function reverseOrdersAndFillableAmounts(ordersAndFillableAmounts: OrdersAndFillableAmounts): OrdersAndFillableAmounts {
+ const ordersCopy = _.clone(ordersAndFillableAmounts.orders);
+ const remainingFillableMakerAssetAmountsCopy = _.clone(ordersAndFillableAmounts.remainingFillableMakerAssetAmounts);
+ return {
+ orders: ordersCopy.reverse(),
+ remainingFillableMakerAssetAmounts: remainingFillableMakerAssetAmountsCopy.reverse(),
+ };
+}
+
+function findEthAmountNeededToBuyFees(
+ feeOrdersAndFillableAmounts: OrdersAndFillableAmounts,
+ feeAmount: BigNumber,
+): BigNumber {
+ const { orders, remainingFillableMakerAssetAmounts } = feeOrdersAndFillableAmounts;
+ const result = _.reduce(
+ orders,
+ (acc, order, index) => {
+ const remainingFillableMakerAssetAmount = remainingFillableMakerAssetAmounts[index];
+ const amountToFill = BigNumber.min(acc.remainingFeeAmount, remainingFillableMakerAssetAmount);
+ const feeAdjustedRate = rateUtils.getFeeAdjustedRateOfFeeOrder(order);
+ const ethAmountForThisOrder = feeAdjustedRate.mul(amountToFill);
+ return {
+ ethAmount: acc.ethAmount.plus(ethAmountForThisOrder),
+ remainingFeeAmount: BigNumber.max(constants.ZERO_AMOUNT, acc.remainingFeeAmount.minus(amountToFill)),
+ };
+ },
+ {
+ ethAmount: constants.ZERO_AMOUNT,
+ remainingFeeAmount: feeAmount,
+ },
+ );
+ return result.ethAmount;
+}
+
+function findEthAndZrxAmountNeededToBuyAsset(
+ ordersAndFillableAmounts: OrdersAndFillableAmounts,
+ assetBuyAmount: BigNumber,
+): [BigNumber, BigNumber] {
+ const { orders, remainingFillableMakerAssetAmounts } = ordersAndFillableAmounts;
+ const result = _.reduce(
+ orders,
+ (acc, order, index) => {
+ const remainingFillableMakerAssetAmount = remainingFillableMakerAssetAmounts[index];
+ const amountToFill = BigNumber.min(acc.remainingAssetBuyAmount, remainingFillableMakerAssetAmount);
+ const ethAmountForThisOrder = amountToFill
+ .mul(order.takerAssetAmount)
+ .dividedToIntegerBy(order.makerAssetAmount);
+ const zrxAmountForThisOrder = amountToFill.mul(order.takerFee).dividedToIntegerBy(order.makerAssetAmount);
+ return {
+ ethAmount: acc.ethAmount.plus(ethAmountForThisOrder),
+ zrxAmount: acc.ethAmount.plus(zrxAmountForThisOrder),
+ remainingAssetBuyAmount: BigNumber.max(
+ constants.ZERO_AMOUNT,
+ acc.remainingAssetBuyAmount.minus(amountToFill),
+ ),
+ };
+ },
+ {
+ ethAmount: constants.ZERO_AMOUNT,
+ zrxAmount: constants.ZERO_AMOUNT,
+ remainingAssetBuyAmount: assetBuyAmount,
+ },
+ );
+ return [result.ethAmount, result.zrxAmount];
+}
diff --git a/packages/asset-buyer/test/buy_quote_calculator_test.ts b/packages/asset-buyer/test/buy_quote_calculator_test.ts
new file mode 100644
index 000000000..2cad1ab05
--- /dev/null
+++ b/packages/asset-buyer/test/buy_quote_calculator_test.ts
@@ -0,0 +1,130 @@
+import { orderFactory } from '@0xproject/order-utils/lib/src/order_factory';
+import { BigNumber } from '@0xproject/utils';
+import * as chai from 'chai';
+import * as _ from 'lodash';
+import 'mocha';
+
+import { AssetBuyerError, OrdersAndFillableAmounts } from '../src/types';
+import { buyQuoteCalculator } from '../src/utils/buy_quote_calculator';
+
+import { chaiSetup } from './utils/chai_setup';
+
+chaiSetup.configure();
+const expect = chai.expect;
+
+const NULL_ADDRESS = '0x0000000000000000000000000000000000000000';
+const NULL_BYTES = '0x';
+
+// tslint:disable:custom-no-magic-numbers
+describe('buyQuoteCalculator', () => {
+ describe('#calculate', () => {
+ let ordersAndFillableAmounts: OrdersAndFillableAmounts;
+ let feeOrdersAndFillableAmounts: OrdersAndFillableAmounts;
+ beforeEach(() => {
+ // generate two orders for our desired maker asset
+ // the first order has a rate of 4 makerAsset / WETH with a takerFee of 200 ZRX and has only 200 / 400 makerAsset units left to fill (half fillable)
+ // the second order has a rate of 2 makerAsset / WETH with a takerFee of 100 ZRX and has 200 / 200 makerAsset units left to fill (completely fillable)
+ // generate one order for fees
+ // the fee order has a rate of 1 ZRX / WETH with no taker fee and has 100 ZRX left to fill (completely fillable)
+ const firstOrder = orderFactory.createOrder(
+ NULL_ADDRESS,
+ new BigNumber(400),
+ NULL_BYTES,
+ new BigNumber(100),
+ NULL_BYTES,
+ NULL_ADDRESS,
+ {
+ takerFee: new BigNumber(200),
+ },
+ );
+ const firstRemainingFillAmount = new BigNumber(200);
+ const secondOrder = orderFactory.createOrder(
+ NULL_ADDRESS,
+ new BigNumber(200),
+ NULL_BYTES,
+ new BigNumber(100),
+ NULL_BYTES,
+ NULL_ADDRESS,
+ {
+ takerFee: new BigNumber(100),
+ },
+ );
+ const secondRemainingFillAmount = secondOrder.makerAssetAmount;
+ const signedOrders = _.map([firstOrder, secondOrder], order => {
+ return {
+ ...order,
+ signature: NULL_BYTES,
+ };
+ });
+ ordersAndFillableAmounts = {
+ orders: signedOrders,
+ remainingFillableMakerAssetAmounts: [firstRemainingFillAmount, secondRemainingFillAmount],
+ };
+ const feeOrder = orderFactory.createOrder(
+ NULL_ADDRESS,
+ new BigNumber(100),
+ NULL_BYTES,
+ new BigNumber(100),
+ NULL_BYTES,
+ NULL_ADDRESS,
+ );
+ const signedFeeOrder = {
+ ...feeOrder,
+ signature: NULL_BYTES,
+ };
+ feeOrdersAndFillableAmounts = {
+ orders: [signedFeeOrder],
+ remainingFillableMakerAssetAmounts: [signedFeeOrder.makerAssetAmount],
+ };
+ });
+ it('should throw if not enough maker asset liquidity', () => {
+ // we have 400 makerAsset units available to fill but attempt to calculate a quote for 500 makerAsset units
+ expect(() =>
+ buyQuoteCalculator.calculate(
+ ordersAndFillableAmounts,
+ feeOrdersAndFillableAmounts,
+ new BigNumber(500),
+ 0,
+ 0,
+ ),
+ ).to.throw(AssetBuyerError.InsufficientAssetLiquidity);
+ });
+ it('should throw if not enough ZRX liquidity', () => {
+ // we request 300 makerAsset units but the ZRX order is only enough to fill the first order, which only has 200 makerAssetUnits available
+ expect(() =>
+ buyQuoteCalculator.calculate(
+ ordersAndFillableAmounts,
+ feeOrdersAndFillableAmounts,
+ new BigNumber(300),
+ 0,
+ 0,
+ ),
+ ).to.throw(AssetBuyerError.InsufficientZrxLiquidity);
+ });
+ it('calculates a correct buyQuote', () => {
+ // we request 200 makerAsset units which can be filled using the first order
+ // the first order requires a fee of 100 ZRX from the taker which can be filled by the feeOrder
+ const assetBuyAmount = new BigNumber(200);
+ const feePercentage = 0.02;
+ const slippagePercentage = 0;
+ const buyQuote = buyQuoteCalculator.calculate(
+ ordersAndFillableAmounts,
+ feeOrdersAndFillableAmounts,
+ assetBuyAmount,
+ feePercentage,
+ slippagePercentage,
+ );
+ // test if orders are correct
+ expect(buyQuote.orders).to.deep.equal([ordersAndFillableAmounts.orders[0]]);
+ expect(buyQuote.feeOrders).to.deep.equal([feeOrdersAndFillableAmounts.orders[0]]);
+ // test if rates are correct
+ const expectedMinEthToFill = new BigNumber(150);
+ const expectedMinRate = assetBuyAmount.div(expectedMinEthToFill.mul(feePercentage + 1));
+ expect(buyQuote.minRate).to.bignumber.equal(expectedMinRate);
+ // because we have no slippage protection, minRate is equal to maxRate
+ expect(buyQuote.maxRate).to.bignumber.equal(expectedMinRate);
+ // test if feePercentage gets passed through
+ expect(buyQuote.feePercentage).to.equal(feePercentage);
+ });
+ });
+});