* `@0x/contracts-erc20-bridge-sampler`: Add gas limits to external quote calls. `@0x/contract-addresses`: Point `erc20BridgeSampler` to new version. * `@0x/asset-swapper`: Ignore zero sample results from the sampler contract. `@0x/asset-swapper`: Allow skipping Uniswap when dealing with low precision amounts with `minUniswapDecimals` option. `@0x/asset-swapper`: Increase default `runLimit` from `1024` to `4096`. `@0x/asset-swapper`: Increase default `numSamples` from `8` to `10` `@0x/asset-swapper`: Fix ordering of optimized orders. `@0x/asset-swapper`: Fix best and worst quotes being reversed sometimes. `@0x/asset-swapper`: Fix rounding of quoted asset amounts. * `@0x/contracts-utils`: Add kovan addresses to `DeploymentConstants`. `@0x/contract-addresses`: Add kovan `ERC20BridgeSampler` address. * `@0x/asset-swapper`: Change default `minUniswapDecimals` option from 8 to 7. * `@0x/contracts-erc20-bridge-sampler`: Fix changelog. * `@0x/asset-swapper`: Revert uniswap decimals fix. * `@0x/asset-swapper`: Undo bridge slippage when computing best case quote. * `@0x/asset-swapper`: Take asset data from input orders instead of output orders in quote result calculation. * `@0x/asset-swapper`: Move `SAMPLER_CONTRACT_GAS_LIMIT` constant to `market_operation_utils/constants`. * Compare equivalent asset data * Fix redundant zero check * Update CHANGELOG * Set fee amount in fillable amounts test Co-authored-by: Jacob Evans <dekz@dekz.net>
745 lines
37 KiB
TypeScript
745 lines
37 KiB
TypeScript
import { getContractAddressesForChainOrThrow } from '@0x/contract-addresses';
|
|
import {
|
|
assertRoughlyEquals,
|
|
constants,
|
|
expect,
|
|
getRandomFloat,
|
|
getRandomInteger,
|
|
Numberish,
|
|
randomAddress,
|
|
} from '@0x/contracts-test-utils';
|
|
|
|
import { assetDataUtils, generatePseudoRandomSalt } from '@0x/order-utils';
|
|
import { Order, SignedOrder } from '@0x/types';
|
|
import { BigNumber, hexUtils } from '@0x/utils';
|
|
import * as _ from 'lodash';
|
|
|
|
import { MarketOperationUtils } from '../src/utils/market_operation_utils/';
|
|
import { constants as marketOperationUtilConstants } from '../src/utils/market_operation_utils/constants';
|
|
import { DexOrderSampler } from '../src/utils/market_operation_utils/sampler';
|
|
import { ERC20BridgeSource } from '../src/utils/market_operation_utils/types';
|
|
|
|
import { MockSamplerContract, QueryAndSampleResult } from './utils/mock_sampler_contract';
|
|
|
|
const { SOURCE_TO_ADDRESS, BUY_SOURCES, SELL_SOURCES } = marketOperationUtilConstants;
|
|
|
|
// Because the bridges and the DEX sources are only deployed on mainnet, tests will resort to using mainnet addresses
|
|
const CHAIN_ID = 1;
|
|
// tslint:disable: custom-no-magic-numbers
|
|
describe('MarketOperationUtils tests', () => {
|
|
const contractAddresses = getContractAddressesForChainOrThrow(CHAIN_ID);
|
|
const ETH2DAI_BRIDGE_ADDRESS = contractAddresses.eth2DaiBridge;
|
|
const KYBER_BRIDGE_ADDRESS = contractAddresses.kyberBridge;
|
|
const UNISWAP_BRIDGE_ADDRESS = contractAddresses.uniswapBridge;
|
|
|
|
const MAKER_TOKEN = randomAddress();
|
|
const TAKER_TOKEN = randomAddress();
|
|
const MAKER_ASSET_DATA = assetDataUtils.encodeERC20AssetData(MAKER_TOKEN);
|
|
const TAKER_ASSET_DATA = assetDataUtils.encodeERC20AssetData(TAKER_TOKEN);
|
|
|
|
interface RatesBySource {
|
|
[source: string]: Numberish[];
|
|
}
|
|
|
|
function createOrder(overrides?: Partial<SignedOrder>): SignedOrder {
|
|
return {
|
|
chainId: CHAIN_ID,
|
|
exchangeAddress: contractAddresses.exchange,
|
|
makerAddress: constants.NULL_ADDRESS,
|
|
takerAddress: constants.NULL_ADDRESS,
|
|
senderAddress: constants.NULL_ADDRESS,
|
|
feeRecipientAddress: randomAddress(),
|
|
salt: generatePseudoRandomSalt(),
|
|
expirationTimeSeconds: getRandomInteger(0, 2 ** 64),
|
|
makerAssetData: MAKER_ASSET_DATA,
|
|
takerAssetData: TAKER_ASSET_DATA,
|
|
makerFeeAssetData: constants.NULL_BYTES,
|
|
takerFeeAssetData: constants.NULL_BYTES,
|
|
makerAssetAmount: getRandomInteger(1, 1e18),
|
|
takerAssetAmount: getRandomInteger(1, 1e18),
|
|
makerFee: constants.ZERO_AMOUNT,
|
|
takerFee: constants.ZERO_AMOUNT,
|
|
signature: hexUtils.random(),
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function getSourceFromAssetData(assetData: string): ERC20BridgeSource {
|
|
if (assetData.length === 74) {
|
|
return ERC20BridgeSource.Native;
|
|
}
|
|
const bridgeAddress = hexUtils.slice(assetData, 48, 68).toLowerCase();
|
|
switch (bridgeAddress) {
|
|
case KYBER_BRIDGE_ADDRESS.toLowerCase():
|
|
return ERC20BridgeSource.Kyber;
|
|
case ETH2DAI_BRIDGE_ADDRESS.toLowerCase():
|
|
return ERC20BridgeSource.Eth2Dai;
|
|
case UNISWAP_BRIDGE_ADDRESS.toLowerCase():
|
|
return ERC20BridgeSource.Uniswap;
|
|
default:
|
|
break;
|
|
}
|
|
throw new Error(`Unknown bridge address: ${bridgeAddress}`);
|
|
}
|
|
|
|
function getSourceFromAddress(sourceAddress: string): ERC20BridgeSource {
|
|
for (const k of Object.keys(SOURCE_TO_ADDRESS)) {
|
|
if (SOURCE_TO_ADDRESS[k].toLowerCase() === sourceAddress.toLowerCase()) {
|
|
return k as ERC20BridgeSource;
|
|
}
|
|
}
|
|
throw new Error(`Unknown source address: ${sourceAddress}`);
|
|
}
|
|
|
|
function assertSamePrefix(actual: string, expected: string): void {
|
|
expect(actual.substr(0, expected.length)).to.eq(expected);
|
|
}
|
|
|
|
function createOrdersFromSellRates(takerAssetAmount: BigNumber, rates: Numberish[]): SignedOrder[] {
|
|
const singleTakerAssetAmount = takerAssetAmount.div(rates.length).integerValue(BigNumber.ROUND_UP);
|
|
return rates.map(r =>
|
|
createOrder({
|
|
makerAssetAmount: singleTakerAssetAmount.times(r).integerValue(),
|
|
takerAssetAmount: singleTakerAssetAmount,
|
|
}),
|
|
);
|
|
}
|
|
|
|
function createOrdersFromBuyRates(makerAssetAmount: BigNumber, rates: Numberish[]): SignedOrder[] {
|
|
const singleMakerAssetAmount = makerAssetAmount.div(rates.length).integerValue(BigNumber.ROUND_UP);
|
|
return (rates as any).map((r: Numberish) =>
|
|
createOrder({
|
|
makerAssetAmount: singleMakerAssetAmount,
|
|
takerAssetAmount: singleMakerAssetAmount.div(r).integerValue(),
|
|
}),
|
|
);
|
|
}
|
|
|
|
function createSamplerFromSellRates(rates: RatesBySource): MockSamplerContract {
|
|
return new MockSamplerContract({
|
|
queryOrdersAndSampleSells: (orders, signatures, sources, fillAmounts) => {
|
|
const fillableTakerAssetAmounts = orders.map(o => o.takerAssetAmount);
|
|
const samplesBySourceIndex = sources.map(s =>
|
|
fillAmounts.map((fillAmount, idx) =>
|
|
fillAmount.times(rates[getSourceFromAddress(s)][idx]).integerValue(BigNumber.ROUND_UP),
|
|
),
|
|
);
|
|
return [fillableTakerAssetAmounts, samplesBySourceIndex];
|
|
},
|
|
});
|
|
}
|
|
|
|
function createSamplerFromBuyRates(rates: RatesBySource): MockSamplerContract {
|
|
return new MockSamplerContract({
|
|
queryOrdersAndSampleBuys: (orders, signatures, sources, fillAmounts) => {
|
|
const fillableMakerAssetAmounts = orders.map(o => o.makerAssetAmount);
|
|
const samplesBySourceIndex = sources.map(s =>
|
|
fillAmounts.map((fillAmount, idx) =>
|
|
fillAmount.div(rates[getSourceFromAddress(s)][idx]).integerValue(BigNumber.ROUND_UP),
|
|
),
|
|
);
|
|
return [fillableMakerAssetAmounts, samplesBySourceIndex];
|
|
},
|
|
});
|
|
}
|
|
|
|
const DUMMY_QUERY_AND_SAMPLE_HANDLER_SELL = (
|
|
orders: Order[],
|
|
signatures: string[],
|
|
sources: string[],
|
|
fillAmounts: BigNumber[],
|
|
): QueryAndSampleResult => [
|
|
orders.map((order: Order) => order.takerAssetAmount),
|
|
sources.map(() => fillAmounts.map(() => getRandomInteger(1, 1e18))),
|
|
];
|
|
|
|
const DUMMY_QUERY_AND_SAMPLE_HANDLER_BUY = (
|
|
orders: Order[],
|
|
signatures: string[],
|
|
sources: string[],
|
|
fillAmounts: BigNumber[],
|
|
): QueryAndSampleResult => [
|
|
orders.map((order: Order) => order.makerAssetAmount),
|
|
sources.map(() => fillAmounts.map(() => getRandomInteger(1, 1e18))),
|
|
];
|
|
|
|
const ORDER_DOMAIN = {
|
|
exchangeAddress: contractAddresses.exchange,
|
|
chainId: CHAIN_ID,
|
|
};
|
|
|
|
describe('DexOrderSampler', () => {
|
|
describe('getSampleAmounts()', () => {
|
|
const FILL_AMOUNT = getRandomInteger(1, 1e18);
|
|
const NUM_SAMPLES = 16;
|
|
|
|
it('generates the correct number of amounts', () => {
|
|
const amounts = DexOrderSampler.getSampleAmounts(FILL_AMOUNT, NUM_SAMPLES);
|
|
expect(amounts).to.be.length(NUM_SAMPLES);
|
|
});
|
|
|
|
it('first amount is nonzero', () => {
|
|
const amounts = DexOrderSampler.getSampleAmounts(FILL_AMOUNT, NUM_SAMPLES);
|
|
expect(amounts[0]).to.not.bignumber.eq(0);
|
|
});
|
|
|
|
it('last amount is the fill amount', () => {
|
|
const amounts = DexOrderSampler.getSampleAmounts(FILL_AMOUNT, NUM_SAMPLES);
|
|
expect(amounts[NUM_SAMPLES - 1]).to.bignumber.eq(FILL_AMOUNT);
|
|
});
|
|
|
|
it('can generate a single amount', () => {
|
|
const amounts = DexOrderSampler.getSampleAmounts(FILL_AMOUNT, 1);
|
|
expect(amounts).to.be.length(1);
|
|
expect(amounts[0]).to.bignumber.eq(FILL_AMOUNT);
|
|
});
|
|
|
|
it('generates ascending amounts', () => {
|
|
const amounts = DexOrderSampler.getSampleAmounts(FILL_AMOUNT, NUM_SAMPLES);
|
|
for (const i of _.times(NUM_SAMPLES).slice(1)) {
|
|
const prev = amounts[i - 1];
|
|
const amount = amounts[i];
|
|
expect(prev).to.bignumber.lt(amount);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('getFillableAmountsAndSampleMarketOperationAsync()', () => {
|
|
const SAMPLE_AMOUNTS = [100, 500, 1000].map(v => new BigNumber(v));
|
|
const ORDERS = _.times(4, () => createOrder());
|
|
|
|
it('makes an eth_call with the correct arguments for a sell', async () => {
|
|
const sampler = new MockSamplerContract({
|
|
queryOrdersAndSampleSells: (orders, signatures, sources, fillAmounts) => {
|
|
expect(orders).to.deep.eq(ORDERS);
|
|
expect(signatures).to.deep.eq(ORDERS.map(o => o.signature));
|
|
expect(sources).to.deep.eq(SELL_SOURCES.map(s => SOURCE_TO_ADDRESS[s]));
|
|
expect(fillAmounts).to.deep.eq(SAMPLE_AMOUNTS);
|
|
return [
|
|
orders.map(() => getRandomInteger(1, 1e18)),
|
|
sources.map(() => fillAmounts.map(() => getRandomInteger(1, 1e18))),
|
|
];
|
|
},
|
|
});
|
|
const dexOrderSampler = new DexOrderSampler(sampler);
|
|
await dexOrderSampler.getFillableAmountsAndSampleMarketSellAsync(ORDERS, SAMPLE_AMOUNTS, SELL_SOURCES);
|
|
});
|
|
|
|
it('makes an eth_call with the correct arguments for a buy', async () => {
|
|
const sampler = new MockSamplerContract({
|
|
queryOrdersAndSampleBuys: (orders, signatures, sources, fillAmounts) => {
|
|
expect(orders).to.deep.eq(ORDERS);
|
|
expect(signatures).to.deep.eq(ORDERS.map(o => o.signature));
|
|
expect(sources).to.deep.eq(BUY_SOURCES.map(s => SOURCE_TO_ADDRESS[s]));
|
|
expect(fillAmounts).to.deep.eq(SAMPLE_AMOUNTS);
|
|
return [
|
|
orders.map(() => getRandomInteger(1, 1e18)),
|
|
sources.map(() => fillAmounts.map(() => getRandomInteger(1, 1e18))),
|
|
];
|
|
},
|
|
});
|
|
const dexOrderSampler = new DexOrderSampler(sampler);
|
|
await dexOrderSampler.getFillableAmountsAndSampleMarketBuyAsync(ORDERS, SAMPLE_AMOUNTS, BUY_SOURCES);
|
|
});
|
|
|
|
it('returns correct fillable amounts', async () => {
|
|
const fillableAmounts = _.times(SAMPLE_AMOUNTS.length, () => getRandomInteger(1, 1e18));
|
|
const sampler = new MockSamplerContract({
|
|
queryOrdersAndSampleSells: (orders, signatures, sources, fillAmounts) => [
|
|
fillableAmounts,
|
|
sources.map(() => fillAmounts.map(() => getRandomInteger(1, 1e18))),
|
|
],
|
|
});
|
|
const dexOrderSampler = new DexOrderSampler(sampler);
|
|
const [actualFillableAmounts] = await dexOrderSampler.getFillableAmountsAndSampleMarketSellAsync(
|
|
ORDERS,
|
|
SAMPLE_AMOUNTS,
|
|
SELL_SOURCES,
|
|
);
|
|
expect(actualFillableAmounts).to.deep.eq(fillableAmounts);
|
|
});
|
|
|
|
it('converts samples to DEX quotes', async () => {
|
|
const quotes = SELL_SOURCES.map(source =>
|
|
SAMPLE_AMOUNTS.map(s => ({
|
|
source,
|
|
input: s,
|
|
output: getRandomInteger(1, 1e18),
|
|
})),
|
|
);
|
|
const sampler = new MockSamplerContract({
|
|
queryOrdersAndSampleSells: (orders, signatures, sources, fillAmounts) => [
|
|
orders.map(() => getRandomInteger(1, 1e18)),
|
|
quotes.map(q => q.map(s => s.output)),
|
|
],
|
|
});
|
|
const dexOrderSampler = new DexOrderSampler(sampler);
|
|
const [, actualQuotes] = await dexOrderSampler.getFillableAmountsAndSampleMarketSellAsync(
|
|
ORDERS,
|
|
SAMPLE_AMOUNTS,
|
|
SELL_SOURCES,
|
|
);
|
|
expect(actualQuotes).to.deep.eq(quotes);
|
|
});
|
|
});
|
|
});
|
|
|
|
function createRandomRates(numSamples: number = 32): RatesBySource {
|
|
const ALL_SOURCES = [
|
|
ERC20BridgeSource.Native,
|
|
ERC20BridgeSource.Eth2Dai,
|
|
ERC20BridgeSource.Kyber,
|
|
ERC20BridgeSource.Uniswap,
|
|
];
|
|
return _.zipObject(
|
|
ALL_SOURCES,
|
|
_.times(ALL_SOURCES.length, () => _.fill(new Array(numSamples), getRandomFloat(1e-3, 2))),
|
|
);
|
|
}
|
|
|
|
describe('MarketOperationUtils', () => {
|
|
describe('getMarketSellOrdersAsync()', () => {
|
|
const FILL_AMOUNT = getRandomInteger(1, 1e18);
|
|
const SOURCE_RATES = createRandomRates();
|
|
const ORDERS = createOrdersFromSellRates(
|
|
FILL_AMOUNT,
|
|
_.times(3, () => SOURCE_RATES[ERC20BridgeSource.Native][0]),
|
|
);
|
|
const DEFAULT_SAMPLER = createSamplerFromSellRates(SOURCE_RATES);
|
|
const DEFAULT_OPTS = { numSamples: 3, runLimit: 0 };
|
|
const defaultMarketOperationUtils = new MarketOperationUtils(
|
|
DEFAULT_SAMPLER,
|
|
contractAddresses,
|
|
ORDER_DOMAIN,
|
|
);
|
|
|
|
it('calls `getFillableAmountsAndSampleMarketSellAsync()`', async () => {
|
|
let wasCalled = false;
|
|
const sampler = new MockSamplerContract({
|
|
queryOrdersAndSampleSells: (...args) => {
|
|
wasCalled = true;
|
|
return DUMMY_QUERY_AND_SAMPLE_HANDLER_SELL(...args);
|
|
},
|
|
});
|
|
const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, ORDER_DOMAIN);
|
|
await marketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, DEFAULT_OPTS);
|
|
expect(wasCalled).to.be.true();
|
|
});
|
|
|
|
it('queries `numSamples` samples', async () => {
|
|
const numSamples = _.random(1, 16);
|
|
let fillAmountsLength = 0;
|
|
const sampler = new MockSamplerContract({
|
|
queryOrdersAndSampleSells: (orders, signatures, sources, fillAmounts) => {
|
|
fillAmountsLength = fillAmounts.length;
|
|
return DUMMY_QUERY_AND_SAMPLE_HANDLER_SELL(orders, signatures, sources, fillAmounts);
|
|
},
|
|
});
|
|
const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, ORDER_DOMAIN);
|
|
await marketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, {
|
|
...DEFAULT_OPTS,
|
|
numSamples,
|
|
});
|
|
expect(fillAmountsLength).eq(numSamples);
|
|
});
|
|
|
|
it('polls all DEXes if `excludedSources` is empty', async () => {
|
|
let sourcesPolled: ERC20BridgeSource[] = [];
|
|
const sampler = new MockSamplerContract({
|
|
queryOrdersAndSampleSells: (orders, signatures, sources, fillAmounts) => {
|
|
sourcesPolled = sources.map(a => getSourceFromAddress(a));
|
|
return DUMMY_QUERY_AND_SAMPLE_HANDLER_SELL(orders, signatures, sources, fillAmounts);
|
|
},
|
|
});
|
|
const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, ORDER_DOMAIN);
|
|
await marketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, {
|
|
...DEFAULT_OPTS,
|
|
excludedSources: [],
|
|
});
|
|
expect(sourcesPolled).to.deep.eq(SELL_SOURCES);
|
|
});
|
|
|
|
it('does not poll DEXes in `excludedSources`', async () => {
|
|
const excludedSources = _.sampleSize(SELL_SOURCES, _.random(1, SELL_SOURCES.length));
|
|
let sourcesPolled: ERC20BridgeSource[] = [];
|
|
const sampler = new MockSamplerContract({
|
|
queryOrdersAndSampleSells: (orders, signatures, sources, fillAmounts) => {
|
|
sourcesPolled = sources.map(a => getSourceFromAddress(a));
|
|
return DUMMY_QUERY_AND_SAMPLE_HANDLER_SELL(orders, signatures, sources, fillAmounts);
|
|
},
|
|
});
|
|
const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, ORDER_DOMAIN);
|
|
await marketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, {
|
|
...DEFAULT_OPTS,
|
|
excludedSources,
|
|
});
|
|
expect(sourcesPolled).to.deep.eq(_.without(SELL_SOURCES, ...excludedSources));
|
|
});
|
|
|
|
it('returns the most cost-effective single source if `runLimit == 0`', async () => {
|
|
const bestRate = BigNumber.max(..._.flatten(Object.values(SOURCE_RATES)));
|
|
const bestSource = _.findKey(SOURCE_RATES, ([r]) => new BigNumber(r).eq(bestRate));
|
|
expect(bestSource).to.exist('');
|
|
const improvedOrders = await defaultMarketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, {
|
|
...DEFAULT_OPTS,
|
|
runLimit: 0,
|
|
});
|
|
const uniqueAssetDatas = _.uniq(improvedOrders.map(o => o.makerAssetData));
|
|
expect(uniqueAssetDatas).to.be.length(1);
|
|
expect(getSourceFromAssetData(uniqueAssetDatas[0])).to.be.eq(bestSource);
|
|
});
|
|
|
|
it('generates bridge orders with correct asset data', async () => {
|
|
const improvedOrders = await defaultMarketOperationUtils.getMarketSellOrdersAsync(
|
|
// Pass in empty orders to prevent native orders from being used.
|
|
ORDERS.map(o => ({ ...o, makerAssetAmount: constants.ZERO_AMOUNT })),
|
|
FILL_AMOUNT,
|
|
DEFAULT_OPTS,
|
|
);
|
|
expect(improvedOrders).to.not.be.length(0);
|
|
for (const order of improvedOrders) {
|
|
expect(getSourceFromAssetData(order.makerAssetData)).to.exist('');
|
|
const makerAssetDataPrefix = hexUtils.slice(
|
|
assetDataUtils.encodeERC20BridgeAssetData(
|
|
MAKER_TOKEN,
|
|
constants.NULL_ADDRESS,
|
|
constants.NULL_BYTES,
|
|
),
|
|
0,
|
|
36,
|
|
);
|
|
assertSamePrefix(order.makerAssetData, makerAssetDataPrefix);
|
|
expect(order.takerAssetData).to.eq(TAKER_ASSET_DATA);
|
|
}
|
|
});
|
|
|
|
it('generates bridge orders with correct taker amount', async () => {
|
|
const improvedOrders = await defaultMarketOperationUtils.getMarketSellOrdersAsync(
|
|
// Pass in empty orders to prevent native orders from being used.
|
|
ORDERS.map(o => ({ ...o, makerAssetAmount: constants.ZERO_AMOUNT })),
|
|
FILL_AMOUNT,
|
|
DEFAULT_OPTS,
|
|
);
|
|
const totalTakerAssetAmount = BigNumber.sum(...improvedOrders.map(o => o.takerAssetAmount));
|
|
expect(totalTakerAssetAmount).to.bignumber.gte(FILL_AMOUNT);
|
|
});
|
|
|
|
it('generates bridge orders with max slippage of `bridgeSlippage`', async () => {
|
|
const bridgeSlippage = _.random(0.1, true);
|
|
const improvedOrders = await defaultMarketOperationUtils.getMarketSellOrdersAsync(
|
|
// Pass in empty orders to prevent native orders from being used.
|
|
ORDERS.map(o => ({ ...o, makerAssetAmount: constants.ZERO_AMOUNT })),
|
|
FILL_AMOUNT,
|
|
{ ...DEFAULT_OPTS, bridgeSlippage },
|
|
);
|
|
expect(improvedOrders).to.not.be.length(0);
|
|
for (const order of improvedOrders) {
|
|
const source = getSourceFromAssetData(order.makerAssetData);
|
|
const expectedMakerAmount = FILL_AMOUNT.times(SOURCE_RATES[source][0]);
|
|
const slippage = 1 - order.makerAssetAmount.div(expectedMakerAmount.plus(1)).toNumber();
|
|
assertRoughlyEquals(slippage, bridgeSlippage, 8);
|
|
}
|
|
});
|
|
|
|
it('ignores native orders below `dustFractionThreshold`', async () => {
|
|
const dustFractionThreshold = 0.01;
|
|
const dustAmount = FILL_AMOUNT.times(dustFractionThreshold).integerValue(BigNumber.ROUND_DOWN);
|
|
const maxRate = BigNumber.max(...ORDERS.map(o => o.makerAssetAmount.div(o.takerAssetAmount)));
|
|
// Pass in an order with the globally best rate but with a dust input amount.
|
|
const dustOrder = createOrder({
|
|
makerAssetAmount: dustAmount.times(maxRate.plus(0.01)),
|
|
takerAssetAmount: dustAmount,
|
|
});
|
|
const improvedOrders = await defaultMarketOperationUtils.getMarketSellOrdersAsync(
|
|
_.shuffle([dustOrder, ...ORDERS]),
|
|
FILL_AMOUNT,
|
|
// Ignore all DEX sources so only native orders are returned.
|
|
{ ...DEFAULT_OPTS, dustFractionThreshold, excludedSources: SELL_SOURCES },
|
|
);
|
|
expect(improvedOrders).to.not.be.length(0);
|
|
for (const order of improvedOrders) {
|
|
expect(order.takerAssetAmount).to.bignumber.gt(dustAmount);
|
|
}
|
|
});
|
|
|
|
it('can mix convex sources', async () => {
|
|
const rates: RatesBySource = {};
|
|
rates[ERC20BridgeSource.Native] = [0.4, 0.3, 0.2, 0.1];
|
|
rates[ERC20BridgeSource.Uniswap] = [0.5, 0.05, 0.05, 0.05];
|
|
rates[ERC20BridgeSource.Eth2Dai] = [0.6, 0.05, 0.05, 0.05];
|
|
rates[ERC20BridgeSource.Kyber] = [0.7, 0.05, 0.05, 0.05];
|
|
const marketOperationUtils = new MarketOperationUtils(
|
|
createSamplerFromSellRates(rates),
|
|
contractAddresses,
|
|
ORDER_DOMAIN,
|
|
);
|
|
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync(
|
|
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
|
|
FILL_AMOUNT,
|
|
{ ...DEFAULT_OPTS, numSamples: 4, runLimit: 512, noConflicts: false },
|
|
);
|
|
expect(improvedOrders).to.be.length(4);
|
|
const orderSources = improvedOrders.map(o => getSourceFromAssetData(o.makerAssetData));
|
|
const expectedSources = [
|
|
ERC20BridgeSource.Kyber,
|
|
ERC20BridgeSource.Eth2Dai,
|
|
ERC20BridgeSource.Uniswap,
|
|
ERC20BridgeSource.Native,
|
|
];
|
|
expect(orderSources).to.deep.eq(expectedSources);
|
|
});
|
|
|
|
it('excludes Kyber when `noConflicts` enabled and Uniswap or Eth2Dai are used first', async () => {
|
|
const rates: RatesBySource = {};
|
|
rates[ERC20BridgeSource.Native] = [0.3, 0.2, 0.1, 0.05];
|
|
rates[ERC20BridgeSource.Uniswap] = [0.5, 0.05, 0.05, 0.05];
|
|
rates[ERC20BridgeSource.Eth2Dai] = [0.6, 0.05, 0.05, 0.05];
|
|
rates[ERC20BridgeSource.Kyber] = [0.4, 0.05, 0.05, 0.05];
|
|
const marketOperationUtils = new MarketOperationUtils(
|
|
createSamplerFromSellRates(rates),
|
|
contractAddresses,
|
|
ORDER_DOMAIN,
|
|
);
|
|
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync(
|
|
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
|
|
FILL_AMOUNT,
|
|
{ ...DEFAULT_OPTS, numSamples: 4, runLimit: 512, noConflicts: true },
|
|
);
|
|
expect(improvedOrders).to.be.length(4);
|
|
const orderSources = improvedOrders.map(o => getSourceFromAssetData(o.makerAssetData));
|
|
const expectedSources = [
|
|
ERC20BridgeSource.Eth2Dai,
|
|
ERC20BridgeSource.Uniswap,
|
|
ERC20BridgeSource.Native,
|
|
ERC20BridgeSource.Native,
|
|
];
|
|
expect(orderSources).to.deep.eq(expectedSources);
|
|
});
|
|
|
|
it('excludes Uniswap and Eth2Dai when `noConflicts` enabled and Kyber is used first', async () => {
|
|
const rates: RatesBySource = {};
|
|
rates[ERC20BridgeSource.Native] = [0.1, 0.05, 0.05, 0.05];
|
|
rates[ERC20BridgeSource.Uniswap] = [0.15, 0.05, 0.05, 0.05];
|
|
rates[ERC20BridgeSource.Eth2Dai] = [0.15, 0.05, 0.05, 0.05];
|
|
rates[ERC20BridgeSource.Kyber] = [0.7, 0.05, 0.05, 0.05];
|
|
const marketOperationUtils = new MarketOperationUtils(
|
|
createSamplerFromSellRates(rates),
|
|
contractAddresses,
|
|
ORDER_DOMAIN,
|
|
);
|
|
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync(
|
|
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
|
|
FILL_AMOUNT,
|
|
{ ...DEFAULT_OPTS, numSamples: 4, runLimit: 512, noConflicts: true },
|
|
);
|
|
expect(improvedOrders).to.be.length(4);
|
|
const orderSources = improvedOrders.map(o => getSourceFromAssetData(o.makerAssetData));
|
|
const expectedSources = [
|
|
ERC20BridgeSource.Kyber,
|
|
ERC20BridgeSource.Native,
|
|
ERC20BridgeSource.Native,
|
|
ERC20BridgeSource.Native,
|
|
];
|
|
expect(orderSources).to.deep.eq(expectedSources);
|
|
});
|
|
});
|
|
|
|
describe('getMarketBuyOrdersAsync()', () => {
|
|
const FILL_AMOUNT = getRandomInteger(1, 1e18);
|
|
const SOURCE_RATES = _.omit(createRandomRates(), [ERC20BridgeSource.Kyber]);
|
|
const ORDERS = createOrdersFromBuyRates(
|
|
FILL_AMOUNT,
|
|
_.times(3, () => SOURCE_RATES[ERC20BridgeSource.Native][0]),
|
|
);
|
|
const DEFAULT_SAMPLER = createSamplerFromBuyRates(SOURCE_RATES);
|
|
const DEFAULT_OPTS = { numSamples: 3, runLimit: 0 };
|
|
const defaultMarketOperationUtils = new MarketOperationUtils(
|
|
DEFAULT_SAMPLER,
|
|
contractAddresses,
|
|
ORDER_DOMAIN,
|
|
);
|
|
|
|
it('calls `getFillableAmountsAndSampleMarketSellAsync()`', async () => {
|
|
let wasCalled = false;
|
|
const sampler = new MockSamplerContract({
|
|
queryOrdersAndSampleBuys: (...args) => {
|
|
wasCalled = true;
|
|
return DUMMY_QUERY_AND_SAMPLE_HANDLER_BUY(...args);
|
|
},
|
|
});
|
|
const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, ORDER_DOMAIN);
|
|
|
|
await marketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, DEFAULT_OPTS);
|
|
expect(wasCalled).to.be.true();
|
|
});
|
|
|
|
it('queries `numSamples` samples', async () => {
|
|
const numSamples = _.random(1, 16);
|
|
let fillAmountsLength = 0;
|
|
const sampler = new MockSamplerContract({
|
|
queryOrdersAndSampleBuys: (orders, signatures, sources, fillAmounts) => {
|
|
fillAmountsLength = fillAmounts.length;
|
|
return DUMMY_QUERY_AND_SAMPLE_HANDLER_BUY(orders, signatures, sources, fillAmounts);
|
|
},
|
|
});
|
|
const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, ORDER_DOMAIN);
|
|
|
|
await marketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, {
|
|
...DEFAULT_OPTS,
|
|
numSamples,
|
|
});
|
|
expect(fillAmountsLength).eq(numSamples);
|
|
});
|
|
|
|
it('polls all DEXes if `excludedSources` is empty', async () => {
|
|
let sourcesPolled: ERC20BridgeSource[] = [];
|
|
const sampler = new MockSamplerContract({
|
|
queryOrdersAndSampleBuys: (orders, signatures, sources, fillAmounts) => {
|
|
sourcesPolled = sources.map(a => getSourceFromAddress(a));
|
|
return DUMMY_QUERY_AND_SAMPLE_HANDLER_BUY(orders, signatures, sources, fillAmounts);
|
|
},
|
|
});
|
|
const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, ORDER_DOMAIN);
|
|
|
|
await marketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, {
|
|
...DEFAULT_OPTS,
|
|
excludedSources: [],
|
|
});
|
|
expect(sourcesPolled).to.deep.eq(BUY_SOURCES);
|
|
});
|
|
|
|
it('does not poll DEXes in `excludedSources`', async () => {
|
|
const excludedSources = _.sampleSize(BUY_SOURCES, _.random(1, BUY_SOURCES.length));
|
|
let sourcesPolled: ERC20BridgeSource[] = [];
|
|
const sampler = new MockSamplerContract({
|
|
queryOrdersAndSampleBuys: (orders, signatures, sources, fillAmounts) => {
|
|
sourcesPolled = sources.map(a => getSourceFromAddress(a));
|
|
return DUMMY_QUERY_AND_SAMPLE_HANDLER_BUY(orders, signatures, sources, fillAmounts);
|
|
},
|
|
});
|
|
const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, ORDER_DOMAIN);
|
|
|
|
await marketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, {
|
|
...DEFAULT_OPTS,
|
|
excludedSources,
|
|
});
|
|
expect(sourcesPolled).to.deep.eq(_.without(BUY_SOURCES, ...excludedSources));
|
|
});
|
|
|
|
it('returns the most cost-effective single source if `runLimit == 0`', async () => {
|
|
const bestRate = BigNumber.max(..._.flatten(Object.values(SOURCE_RATES)));
|
|
const bestSource = _.findKey(SOURCE_RATES, ([r]) => new BigNumber(r).eq(bestRate));
|
|
expect(bestSource).to.exist('');
|
|
const improvedOrders = await defaultMarketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, {
|
|
...DEFAULT_OPTS,
|
|
runLimit: 0,
|
|
});
|
|
const uniqueAssetDatas = _.uniq(improvedOrders.map(o => o.makerAssetData));
|
|
expect(uniqueAssetDatas).to.be.length(1);
|
|
expect(getSourceFromAssetData(uniqueAssetDatas[0])).to.be.eq(bestSource);
|
|
});
|
|
it('generates bridge orders with correct asset data', async () => {
|
|
const improvedOrders = await defaultMarketOperationUtils.getMarketBuyOrdersAsync(
|
|
// Pass in empty orders to prevent native orders from being used.
|
|
ORDERS.map(o => ({ ...o, makerAssetAmount: constants.ZERO_AMOUNT })),
|
|
FILL_AMOUNT,
|
|
DEFAULT_OPTS,
|
|
);
|
|
expect(improvedOrders).to.not.be.length(0);
|
|
for (const order of improvedOrders) {
|
|
expect(getSourceFromAssetData(order.makerAssetData)).to.exist('');
|
|
const makerAssetDataPrefix = hexUtils.slice(
|
|
assetDataUtils.encodeERC20BridgeAssetData(
|
|
MAKER_TOKEN,
|
|
constants.NULL_ADDRESS,
|
|
constants.NULL_BYTES,
|
|
),
|
|
0,
|
|
36,
|
|
);
|
|
assertSamePrefix(order.makerAssetData, makerAssetDataPrefix);
|
|
expect(order.takerAssetData).to.eq(TAKER_ASSET_DATA);
|
|
}
|
|
});
|
|
|
|
it('generates bridge orders with correct taker amount', async () => {
|
|
const improvedOrders = await defaultMarketOperationUtils.getMarketBuyOrdersAsync(
|
|
// Pass in empty orders to prevent native orders from being used.
|
|
ORDERS.map(o => ({ ...o, makerAssetAmount: constants.ZERO_AMOUNT })),
|
|
FILL_AMOUNT,
|
|
DEFAULT_OPTS,
|
|
);
|
|
const totalMakerAssetAmount = BigNumber.sum(...improvedOrders.map(o => o.makerAssetAmount));
|
|
expect(totalMakerAssetAmount).to.bignumber.gte(FILL_AMOUNT);
|
|
});
|
|
|
|
it('generates bridge orders with max slippage of `bridgeSlippage`', async () => {
|
|
const bridgeSlippage = _.random(0.1, true);
|
|
const improvedOrders = await defaultMarketOperationUtils.getMarketBuyOrdersAsync(
|
|
// Pass in empty orders to prevent native orders from being used.
|
|
ORDERS.map(o => ({ ...o, makerAssetAmount: constants.ZERO_AMOUNT })),
|
|
FILL_AMOUNT,
|
|
{ ...DEFAULT_OPTS, bridgeSlippage },
|
|
);
|
|
expect(improvedOrders).to.not.be.length(0);
|
|
for (const order of improvedOrders) {
|
|
const source = getSourceFromAssetData(order.makerAssetData);
|
|
const expectedTakerAmount = FILL_AMOUNT.div(SOURCE_RATES[source][0]).integerValue(
|
|
BigNumber.ROUND_UP,
|
|
);
|
|
const slippage = order.takerAssetAmount.div(expectedTakerAmount.plus(1)).toNumber() - 1;
|
|
assertRoughlyEquals(slippage, bridgeSlippage, 8);
|
|
}
|
|
});
|
|
|
|
it('Ignores native orders below `dustFractionThreshold`', async () => {
|
|
const dustFractionThreshold = 0.01;
|
|
const dustAmount = FILL_AMOUNT.times(dustFractionThreshold).integerValue(BigNumber.ROUND_DOWN);
|
|
const maxRate = BigNumber.max(...ORDERS.map(o => o.makerAssetAmount.div(o.takerAssetAmount)));
|
|
// Pass in an order with the globally best rate but with a dust input amount.
|
|
const dustOrder = createOrder({
|
|
makerAssetAmount: dustAmount,
|
|
takerAssetAmount: dustAmount.div(maxRate.plus(0.01)).integerValue(BigNumber.ROUND_DOWN),
|
|
});
|
|
const improvedOrders = await defaultMarketOperationUtils.getMarketBuyOrdersAsync(
|
|
_.shuffle([dustOrder, ...ORDERS]),
|
|
FILL_AMOUNT,
|
|
// Ignore all DEX sources so only native orders are returned.
|
|
{ ...DEFAULT_OPTS, dustFractionThreshold, excludedSources: BUY_SOURCES },
|
|
);
|
|
expect(improvedOrders).to.not.be.length(0);
|
|
for (const order of improvedOrders) {
|
|
expect(order.makerAssetAmount).to.bignumber.gt(dustAmount);
|
|
}
|
|
});
|
|
|
|
it('can mix convex sources', async () => {
|
|
const rates: RatesBySource = {};
|
|
rates[ERC20BridgeSource.Native] = [0.4, 0.3, 0.2, 0.1];
|
|
rates[ERC20BridgeSource.Uniswap] = [0.5, 0.05, 0.05, 0.05];
|
|
rates[ERC20BridgeSource.Eth2Dai] = [0.6, 0.05, 0.05, 0.05];
|
|
const marketOperationUtils = new MarketOperationUtils(
|
|
createSamplerFromBuyRates(rates),
|
|
contractAddresses,
|
|
ORDER_DOMAIN,
|
|
);
|
|
const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync(
|
|
createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
|
|
FILL_AMOUNT,
|
|
{ ...DEFAULT_OPTS, numSamples: 4, runLimit: 512 },
|
|
);
|
|
expect(improvedOrders).to.be.length(4);
|
|
const orderSources = improvedOrders.map(o => getSourceFromAssetData(o.makerAssetData));
|
|
const expectedSources = [
|
|
ERC20BridgeSource.Eth2Dai,
|
|
ERC20BridgeSource.Uniswap,
|
|
ERC20BridgeSource.Native,
|
|
ERC20BridgeSource.Native,
|
|
];
|
|
expect(orderSources).to.deep.eq(expectedSources);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
// tslint:disable-next-line: max-file-line-count
|