UniswapFeature (#2703)

* Minimal Uniswap accessor.

* Add comments

* Safe math

* mainnet gas benchmark

* Assembler Uniswap

* Selectors and addresses

* Fix bugs in ABI encoders

* Typo

* AsmUniswap test

* Fix wantAmount computation

* Golfing

* Bypass AllowanceTarget

* Generalized asm uniswapper

* Implement ordering

* Fix pair computation

* #6 Golfing Iron

* Remove 'to' argument (saves 377 gas)

* New contract api

* `@0x/contracts-zero-ex`: Add `UniswapFeature`

* `@0x/contract-artifacts`: Regenerate artifacts

* `@0x/contract-wrappers`: Regenerate wrappers

* `@0x/asset-swapper`: Add Uniswap VIP support.
`@0x/asset-swapper`: Add `includeSources` support.

* `@0x/contracts-zero-ex`: Fix misleading comments in `UniswapFeature`.
`@0x/asset-swapper`: Fix linter errors.

* `@0x/asset-swapper`: Fix source filter bugs.

* `@0x/contracts-zero-ex`: `UniswapFeature`: Reduce calldata size for AllowanceTarget call
`@0x/asset-swapper`: Fix failing test.

* `@0x/contracts-zero-ex`: Fix ETH buy tokens not being normalized to WETH.

* `@0x/asset-swapper`: Fix multi-hop weirdness with source filters.

* `@0x/asset-swapper`: Fix failing test.

* `@0x/asset-swapper`: Really fix that broken AS test.

* `@0x/asset-swapper`: use filter objects instead of source array for valid buy and sell sources/

* `@0x/asset-swapper`: Move some source filtering logic into the sampler operations.

* `@0x/contracts-zero-ex`: Address PR feedback

* `@0x/contracts-zero-ex`: Fix feature version bug.

* `@0x/asset-swapper`: Did I actually fix AS tests this time? Who knows.

Co-authored-by: Remco Bloemen <remco@0x.org>
Co-authored-by: Michael Zhu <mchl.zhu.96@gmail.com>
Co-authored-by: Lawrence Forman <me@merklejerk.com>
This commit is contained in:
Lawrence Forman
2020-09-23 02:27:48 -04:00
committed by GitHub
parent 32d11d1ba5
commit f84b375cde
27 changed files with 1044 additions and 179 deletions

View File

@@ -18,10 +18,22 @@ import * as TypeMoq from 'typemoq';
import { MarketOperation, QuoteRequestor, RfqtRequestOpts, SignedOrderWithFillableAmounts } from '../src';
import { getRfqtIndicativeQuotesAsync, MarketOperationUtils } from '../src/utils/market_operation_utils/';
import { BalancerPoolsCache } from '../src/utils/market_operation_utils/balancer_utils';
import { BUY_SOURCES, POSITIVE_INF, SELL_SOURCES, ZERO_AMOUNT } from '../src/utils/market_operation_utils/constants';
import {
BUY_SOURCE_FILTER,
POSITIVE_INF,
SELL_SOURCE_FILTER,
ZERO_AMOUNT,
} from '../src/utils/market_operation_utils/constants';
import { createFillPaths } from '../src/utils/market_operation_utils/fills';
import { DexOrderSampler } from '../src/utils/market_operation_utils/sampler';
import { DexSample, ERC20BridgeSource, FillData, NativeFillData } from '../src/utils/market_operation_utils/types';
import { BATCH_SOURCE_FILTERS } from '../src/utils/market_operation_utils/sampler_operations';
import {
DexSample,
ERC20BridgeSource,
FillData,
NativeFillData,
OptimizedMarketOrder,
} from '../src/utils/market_operation_utils/types';
const MAKER_TOKEN = randomAddress();
const TAKER_TOKEN = randomAddress();
@@ -36,7 +48,10 @@ const DEFAULT_EXCLUDED = [
ERC20BridgeSource.Bancor,
ERC20BridgeSource.Swerve,
ERC20BridgeSource.SushiSwap,
ERC20BridgeSource.MultiHop,
];
const BUY_SOURCES = BUY_SOURCE_FILTER.sources;
const SELL_SOURCES = SELL_SOURCE_FILTER.sources;
// tslint:disable: custom-no-magic-numbers promise-function-async
describe('MarketOperationUtils tests', () => {
@@ -167,7 +182,7 @@ describe('MarketOperationUtils tests', () => {
fillAmounts: BigNumber[],
_wethAddress: string,
) => {
return sources.map(s => createSamplesFromRates(s, fillAmounts, rates[s]));
return BATCH_SOURCE_FILTERS.getAllowed(sources).map(s => createSamplesFromRates(s, fillAmounts, rates[s]));
};
}
@@ -209,7 +224,9 @@ describe('MarketOperationUtils tests', () => {
fillAmounts: BigNumber[],
_wethAddress: string,
) => {
return sources.map(s => createSamplesFromRates(s, fillAmounts, rates[s].map(r => new BigNumber(1).div(r))));
return BATCH_SOURCE_FILTERS.getAllowed(sources).map(s =>
createSamplesFromRates(s, fillAmounts, rates[s].map(r => new BigNumber(1).div(r))),
);
};
}
@@ -244,6 +261,27 @@ describe('MarketOperationUtils tests', () => {
return rates;
}
function getSortedOrderSources(side: MarketOperation, orders: OptimizedMarketOrder[]): ERC20BridgeSource[][] {
return (
orders
// Sort orders by descending rate.
.sort((a, b) =>
b.makerAssetAmount.div(b.takerAssetAmount).comparedTo(a.makerAssetAmount.div(a.takerAssetAmount)),
)
// Then sort fills by descending rate.
.map(o => {
return o.fills
.slice()
.sort((a, b) =>
side === MarketOperation.Sell
? b.output.div(b.input).comparedTo(a.output.div(a.input))
: b.input.div(b.output).comparedTo(a.input.div(a.output)),
)
.map(f => f.source);
})
);
}
const NUM_SAMPLES = 3;
interface RatesBySource {
@@ -265,6 +303,7 @@ describe('MarketOperationUtils tests', () => {
[ERC20BridgeSource.Mooniswap]: _.times(NUM_SAMPLES, () => 0),
[ERC20BridgeSource.Swerve]: _.times(NUM_SAMPLES, () => 0),
[ERC20BridgeSource.SushiSwap]: _.times(NUM_SAMPLES, () => 0),
[ERC20BridgeSource.MultiHop]: _.times(NUM_SAMPLES, () => 0),
};
const DEFAULT_RATES: RatesBySource = {
@@ -307,6 +346,9 @@ describe('MarketOperationUtils tests', () => {
},
[ERC20BridgeSource.LiquidityProvider]: { poolAddress: randomAddress() },
[ERC20BridgeSource.SushiSwap]: { tokenAddressPath: [] },
[ERC20BridgeSource.Mooniswap]: { poolAddress: randomAddress() },
[ERC20BridgeSource.Native]: { order: createOrder() },
[ERC20BridgeSource.MultiHop]: {},
};
const DEFAULT_OPS = {
@@ -356,7 +398,7 @@ describe('MarketOperationUtils tests', () => {
const MOCK_SAMPLER = ({
async executeAsync(...ops: any[]): Promise<any[]> {
return ops;
return MOCK_SAMPLER.executeBatchAsync(ops);
},
async executeBatchAsync(ops: any[]): Promise<any[]> {
return ops;
@@ -377,33 +419,7 @@ describe('MarketOperationUtils tests', () => {
intentOnFilling: false,
};
it('returns an empty array if native liquidity is excluded from the salad', async () => {
const requestor = TypeMoq.Mock.ofType(QuoteRequestor, TypeMoq.MockBehavior.Strict);
const result = await getRfqtIndicativeQuotesAsync(
MAKER_ASSET_DATA,
TAKER_ASSET_DATA,
MarketOperation.Sell,
new BigNumber('100e18'),
{
rfqt: { quoteRequestor: requestor.object, ...partialRfqt },
excludedSources: [ERC20BridgeSource.Native],
},
);
expect(result.length).to.eql(0);
requestor.verify(
r =>
r.requestRfqtIndicativeQuotesAsync(
TypeMoq.It.isAny(),
TypeMoq.It.isAny(),
TypeMoq.It.isAny(),
TypeMoq.It.isAny(),
TypeMoq.It.isAny(),
),
TypeMoq.Times.never(),
);
});
it('calls RFQT if Native source is not excluded', async () => {
it('calls RFQT', async () => {
const requestor = TypeMoq.Mock.ofType(QuoteRequestor, TypeMoq.MockBehavior.Loose);
requestor
.setup(r =>
@@ -424,7 +440,6 @@ describe('MarketOperationUtils tests', () => {
new BigNumber('100e18'),
{
rfqt: { quoteRequestor: requestor.object, ...partialRfqt },
excludedSources: [],
},
);
requestor.verifyAll();
@@ -481,6 +496,10 @@ describe('MarketOperationUtils tests', () => {
sourcesPolled = sourcesPolled.concat(sources.slice());
return DEFAULT_OPS.getSellQuotes(sources, makerToken, takerToken, amounts, wethAddress);
},
getTwoHopSellQuotes: (...args: any[]) => {
sourcesPolled.push(ERC20BridgeSource.MultiHop);
return DEFAULT_OPS.getTwoHopSellQuotes(...args);
},
getBalancerSellQuotesOffChainAsync: (
makerToken: string,
takerToken: string,
@@ -494,7 +513,7 @@ describe('MarketOperationUtils tests', () => {
...DEFAULT_OPTS,
excludedSources: [],
});
expect(sourcesPolled.sort()).to.deep.equals(SELL_SOURCES.slice().sort());
expect(_.uniq(sourcesPolled).sort()).to.deep.equals(SELL_SOURCES.slice().sort());
});
it('polls the liquidity provider when the registry is provided in the arguments', async () => {
@@ -504,6 +523,13 @@ describe('MarketOperationUtils tests', () => {
);
replaceSamplerOps({
getSellQuotes: fn,
getTwoHopSellQuotes: (sources: ERC20BridgeSource[], ..._args: any[]) => {
if (sources.length !== 0) {
args.sources.push(ERC20BridgeSource.MultiHop);
args.sources.push(...sources);
}
return DEFAULT_OPS.getTwoHopSellQuotes(..._args);
},
getBalancerSellQuotesOffChainAsync: (
makerToken: string,
takerToken: string,
@@ -524,20 +550,27 @@ describe('MarketOperationUtils tests', () => {
...DEFAULT_OPTS,
excludedSources: [],
});
expect(args.sources.sort()).to.deep.equals(
expect(_.uniq(args.sources).sort()).to.deep.equals(
SELL_SOURCES.concat([ERC20BridgeSource.LiquidityProvider]).sort(),
);
expect(args.liquidityProviderAddress).to.eql(registryAddress);
});
it('does not poll DEXes in `excludedSources`', async () => {
const excludedSources = _.sampleSize(SELL_SOURCES, _.random(1, SELL_SOURCES.length));
const excludedSources = [ERC20BridgeSource.Uniswap, ERC20BridgeSource.Eth2Dai];
let sourcesPolled: ERC20BridgeSource[] = [];
replaceSamplerOps({
getSellQuotes: (sources, makerToken, takerToken, amounts, wethAddress) => {
sourcesPolled = sourcesPolled.concat(sources.slice());
return DEFAULT_OPS.getSellQuotes(sources, makerToken, takerToken, amounts, wethAddress);
},
getTwoHopSellQuotes: (sources: ERC20BridgeSource[], ...args: any[]) => {
if (sources.length !== 0) {
sourcesPolled.push(ERC20BridgeSource.MultiHop);
sourcesPolled.push(...sources);
}
return DEFAULT_OPS.getTwoHopSellQuotes(...args);
},
getBalancerSellQuotesOffChainAsync: (
makerToken: string,
takerToken: string,
@@ -551,7 +584,39 @@ describe('MarketOperationUtils tests', () => {
...DEFAULT_OPTS,
excludedSources,
});
expect(sourcesPolled.sort()).to.deep.equals(_.without(SELL_SOURCES, ...excludedSources).sort());
expect(_.uniq(sourcesPolled).sort()).to.deep.equals(_.without(SELL_SOURCES, ...excludedSources).sort());
});
it('only polls DEXes in `includedSources`', async () => {
const includedSources = [ERC20BridgeSource.Uniswap, ERC20BridgeSource.Eth2Dai];
let sourcesPolled: ERC20BridgeSource[] = [];
replaceSamplerOps({
getSellQuotes: (sources, makerToken, takerToken, amounts, wethAddress) => {
sourcesPolled = sourcesPolled.concat(sources.slice());
return DEFAULT_OPS.getSellQuotes(sources, makerToken, takerToken, amounts, wethAddress);
},
getTwoHopSellQuotes: (sources: ERC20BridgeSource[], ...args: any[]) => {
if (sources.length !== 0) {
sourcesPolled.push(ERC20BridgeSource.MultiHop);
sourcesPolled.push(...sources);
}
return DEFAULT_OPS.getTwoHopSellQuotes(sources, ...args);
},
getBalancerSellQuotesOffChainAsync: (
makerToken: string,
takerToken: string,
takerFillAmounts: BigNumber[],
) => {
sourcesPolled = sourcesPolled.concat(ERC20BridgeSource.Balancer);
return DEFAULT_OPS.getBalancerSellQuotesOffChainAsync(makerToken, takerToken, takerFillAmounts);
},
});
await marketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, {
...DEFAULT_OPTS,
excludedSources: [],
includedSources,
});
expect(_.uniq(sourcesPolled).sort()).to.deep.equals(includedSources.sort());
});
it('generates bridge orders with correct asset data', async () => {
@@ -858,7 +923,7 @@ describe('MarketOperationUtils tests', () => {
);
const improvedOrders = improvedOrdersResponse.optimizedOrders;
expect(improvedOrders).to.be.length(3);
const orderFillSources = improvedOrders.map(o => o.fills.map(f => f.source));
const orderFillSources = getSortedOrderSources(MarketOperation.Sell, improvedOrders);
expect(orderFillSources).to.deep.eq([
[ERC20BridgeSource.Uniswap],
[ERC20BridgeSource.Native],
@@ -910,6 +975,13 @@ describe('MarketOperationUtils tests', () => {
sourcesPolled = sourcesPolled.concat(sources.slice());
return DEFAULT_OPS.getBuyQuotes(sources, makerToken, takerToken, amounts, wethAddress);
},
getTwoHopBuyQuotes: (sources: ERC20BridgeSource[], ..._args: any[]) => {
if (sources.length !== 0) {
sourcesPolled.push(ERC20BridgeSource.MultiHop);
sourcesPolled.push(...sources);
}
return DEFAULT_OPS.getTwoHopBuyQuotes(..._args);
},
getBalancerBuyQuotesOffChainAsync: (
makerToken: string,
takerToken: string,
@@ -923,7 +995,7 @@ describe('MarketOperationUtils tests', () => {
...DEFAULT_OPTS,
excludedSources: [],
});
expect(sourcesPolled.sort()).to.deep.equals(BUY_SOURCES.sort());
expect(_.uniq(sourcesPolled).sort()).to.deep.equals(BUY_SOURCES.sort());
});
it('polls the liquidity provider when the registry is provided in the arguments', async () => {
@@ -933,6 +1005,13 @@ describe('MarketOperationUtils tests', () => {
);
replaceSamplerOps({
getBuyQuotes: fn,
getTwoHopBuyQuotes: (sources: ERC20BridgeSource[], ..._args: any[]) => {
if (sources.length !== 0) {
args.sources.push(ERC20BridgeSource.MultiHop);
args.sources.push(...sources);
}
return DEFAULT_OPS.getTwoHopBuyQuotes(..._args);
},
getBalancerBuyQuotesOffChainAsync: (
makerToken: string,
takerToken: string,
@@ -953,20 +1032,27 @@ describe('MarketOperationUtils tests', () => {
...DEFAULT_OPTS,
excludedSources: [],
});
expect(args.sources.sort()).to.deep.eq(
expect(_.uniq(args.sources).sort()).to.deep.eq(
BUY_SOURCES.concat([ERC20BridgeSource.LiquidityProvider]).sort(),
);
expect(args.liquidityProviderAddress).to.eql(registryAddress);
});
it('does not poll DEXes in `excludedSources`', async () => {
const excludedSources = _.sampleSize(SELL_SOURCES, _.random(1, SELL_SOURCES.length));
const excludedSources = [ERC20BridgeSource.Uniswap, ERC20BridgeSource.Eth2Dai];
let sourcesPolled: ERC20BridgeSource[] = [];
replaceSamplerOps({
getBuyQuotes: (sources, makerToken, takerToken, amounts, wethAddress) => {
sourcesPolled = sourcesPolled.concat(sources.slice());
return DEFAULT_OPS.getBuyQuotes(sources, makerToken, takerToken, amounts, wethAddress);
},
getTwoHopBuyQuotes: (sources: ERC20BridgeSource[], ..._args: any[]) => {
if (sources.length !== 0) {
sourcesPolled.push(ERC20BridgeSource.MultiHop);
sourcesPolled.push(...sources);
}
return DEFAULT_OPS.getTwoHopBuyQuotes(..._args);
},
getBalancerBuyQuotesOffChainAsync: (
makerToken: string,
takerToken: string,
@@ -980,7 +1066,39 @@ describe('MarketOperationUtils tests', () => {
...DEFAULT_OPTS,
excludedSources,
});
expect(sourcesPolled.sort()).to.deep.eq(_.without(BUY_SOURCES, ...excludedSources).sort());
expect(_.uniq(sourcesPolled).sort()).to.deep.eq(_.without(BUY_SOURCES, ...excludedSources).sort());
});
it('only polls DEXes in `includedSources`', async () => {
const includedSources = [ERC20BridgeSource.Uniswap, ERC20BridgeSource.Eth2Dai];
let sourcesPolled: ERC20BridgeSource[] = [];
replaceSamplerOps({
getBuyQuotes: (sources, makerToken, takerToken, amounts, wethAddress) => {
sourcesPolled = sourcesPolled.concat(sources.slice());
return DEFAULT_OPS.getBuyQuotes(sources, makerToken, takerToken, amounts, wethAddress);
},
getTwoHopBuyQuotes: (sources: ERC20BridgeSource[], ..._args: any[]) => {
if (sources.length !== 0) {
sourcesPolled.push(ERC20BridgeSource.MultiHop);
sourcesPolled.push(...sources);
}
return DEFAULT_OPS.getTwoHopBuyQuotes(..._args);
},
getBalancerBuyQuotesOffChainAsync: (
makerToken: string,
takerToken: string,
makerFillAmounts: BigNumber[],
) => {
sourcesPolled = sourcesPolled.concat(ERC20BridgeSource.Balancer);
return DEFAULT_OPS.getBalancerBuyQuotesOffChainAsync(makerToken, takerToken, makerFillAmounts);
},
});
await marketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, {
...DEFAULT_OPTS,
excludedSources: [],
includedSources,
});
expect(_.uniq(sourcesPolled).sort()).to.deep.eq(includedSources.sort());
});
it('generates bridge orders with correct asset data', async () => {
@@ -1198,7 +1316,7 @@ describe('MarketOperationUtils tests', () => {
);
const improvedOrders = improvedOrdersResponse.optimizedOrders;
expect(improvedOrders).to.be.length(2);
const orderFillSources = improvedOrders.map(o => o.fills.map(f => f.source));
const orderFillSources = getSortedOrderSources(MarketOperation.Sell, improvedOrders);
expect(orderFillSources).to.deep.eq([
[ERC20BridgeSource.Native],
[ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Uniswap],