From 235e406620d63d05dfa62c7c98c6a7be86d0b76f Mon Sep 17 00:00:00 2001 From: Daniel Pyrathon Date: Thu, 24 Sep 2020 15:27:55 -0700 Subject: [PATCH 01/32] initial decoupling of Quote reporter --- .../src/utils/market_operation_utils/index.ts | 95 +++++++++---------- .../src/utils/market_operation_utils/types.ts | 3 +- 2 files changed, 46 insertions(+), 52 deletions(-) diff --git a/packages/asset-swapper/src/utils/market_operation_utils/index.ts b/packages/asset-swapper/src/utils/market_operation_utils/index.ts index 672045106e..75e91a80e9 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/index.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/index.ts @@ -33,6 +33,7 @@ import { DexSample, ERC20BridgeSource, FeeSchedule, + FillData, GetMarketOrdersOpts, MarketSideLiquidity, OptimizedMarketOrder, @@ -78,6 +79,20 @@ export class MarketOperationUtils { private readonly _buySources: SourceFilters; private readonly _feeSources = new SourceFilters(FEE_QUOTE_SOURCES); + private static _computeQuoteReport(nativeOrders: SignedOrder[], quoteRequestor: QuoteRequestor, marketSideLiquidity: MarketSideLiquidity, optimizerResult: OptimizerResult): void { + const {side, dexQuotes, twoHopQuotes, orderFillableAmounts } = marketSideLiquidity; + const { liquidityDelivered } = optimizerResult; + generateQuoteReport( + side, + _.flatten(dexQuotes), + twoHopQuotes, + nativeOrders, + orderFillableAmounts, + liquidityDelivered, + quoteRequestor, + ); + } + constructor( private readonly _sampler: DexOrderSampler, private readonly contractAddresses: ContractAddresses, @@ -339,19 +354,22 @@ export class MarketOperationUtils { nativeOrders: SignedOrder[], takerAmount: BigNumber, opts?: Partial, + gasPrice?: BigNumber, ): Promise { - const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts }; - const marketSideLiquidity = await this.getMarketSellLiquidityAsync(nativeOrders, takerAmount, _opts); - return this._generateOptimizedOrdersAsync(marketSideLiquidity, { - bridgeSlippage: _opts.bridgeSlippage, - maxFallbackSlippage: _opts.maxFallbackSlippage, - excludedSources: _opts.excludedSources, - feeSchedule: _opts.feeSchedule, - allowFallback: _opts.allowFallback, - shouldBatchBridgeOrders: _opts.shouldBatchBridgeOrders, - quoteRequestor: _opts.rfqt ? _opts.rfqt.quoteRequestor : undefined, - shouldGenerateQuoteReport: _opts.shouldGenerateQuoteReport, + const defaultOpts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts }; + const marketSideLiquidity = await this.getMarketSellLiquidityAsync(nativeOrders, takerAmount, defaultOpts); + const optimizedOrders = await this._generateOptimizedOrdersAsync(marketSideLiquidity, { + bridgeSlippage: defaultOpts.bridgeSlippage, + maxFallbackSlippage: defaultOpts.maxFallbackSlippage, + excludedSources: defaultOpts.excludedSources, + feeSchedule: defaultOpts.feeSchedule, + allowFallback: defaultOpts.allowFallback, + shouldBatchBridgeOrders: defaultOpts.shouldBatchBridgeOrders, }); + if (defaultOpts.shouldGenerateQuoteReport && defaultOpts.rfqt && defaultOpts.rfqt.quoteRequestor) { + MarketOperationUtils._computeQuoteReport(nativeOrders, defaultOpts.rfqt.quoteRequestor, marketSideLiquidity, optimizedResult); + } + return optimizedOrders; } /** @@ -367,18 +385,20 @@ export class MarketOperationUtils { makerAmount: BigNumber, opts?: Partial, ): Promise { - const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts }; - const marketSideLiquidity = await this.getMarketBuyLiquidityAsync(nativeOrders, makerAmount, _opts); - return this._generateOptimizedOrdersAsync(marketSideLiquidity, { - bridgeSlippage: _opts.bridgeSlippage, - maxFallbackSlippage: _opts.maxFallbackSlippage, - excludedSources: _opts.excludedSources, - feeSchedule: _opts.feeSchedule, - allowFallback: _opts.allowFallback, - shouldBatchBridgeOrders: _opts.shouldBatchBridgeOrders, - quoteRequestor: _opts.rfqt ? _opts.rfqt.quoteRequestor : undefined, - shouldGenerateQuoteReport: _opts.shouldGenerateQuoteReport, + const defaultOpts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts }; + const marketSideLiquidity = await this.getMarketBuyLiquidityAsync(nativeOrders, makerAmount, defaultOpts); + const optimizedResult = await this._generateOptimizedOrdersAsync(marketSideLiquidity, { + bridgeSlippage: defaultOpts.bridgeSlippage, + maxFallbackSlippage: defaultOpts.maxFallbackSlippage, + excludedSources: defaultOpts.excludedSources, + feeSchedule: defaultOpts.feeSchedule, + allowFallback: defaultOpts.allowFallback, + shouldBatchBridgeOrders: defaultOpts.shouldBatchBridgeOrders, }); + if (defaultOpts.shouldGenerateQuoteReport && defaultOpts.rfqt && defaultOpts.rfqt.quoteRequestor) { + MarketOperationUtils._computeQuoteReport(nativeOrders, defaultOpts.rfqt.quoteRequestor, marketSideLiquidity, optimizedResult); + } + return optimizedResult; } /** @@ -468,7 +488,6 @@ export class MarketOperationUtils { feeSchedule: _opts.feeSchedule, allowFallback: _opts.allowFallback, shouldBatchBridgeOrders: _opts.shouldBatchBridgeOrders, - shouldGenerateQuoteReport: false, }, ); return optimizedOrders; @@ -491,8 +510,6 @@ export class MarketOperationUtils { feeSchedule?: FeeSchedule; allowFallback?: boolean; shouldBatchBridgeOrders?: boolean; - quoteRequestor?: QuoteRequestor; - shouldGenerateQuoteReport?: boolean; }, ): Promise { const { @@ -506,7 +523,6 @@ export class MarketOperationUtils { dexQuotes, ethToOutputRate, ethToInputRate, - twoHopQuotes, } = marketSideLiquidity; const maxFallbackSlippage = opts.maxFallbackSlippage || 0; @@ -549,18 +565,7 @@ export class MarketOperationUtils { ); if (bestTwoHopQuote && bestTwoHopRate.isGreaterThan(optimalPathRate)) { const twoHopOrders = createOrdersFromTwoHopSample(bestTwoHopQuote, orderOpts); - const twoHopQuoteReport = opts.shouldGenerateQuoteReport - ? generateQuoteReport( - side, - _.flatten(dexQuotes), - twoHopQuotes, - nativeOrders, - orderFillableAmounts, - bestTwoHopQuote, - opts.quoteRequestor, - ) - : undefined; - return { optimizedOrders: twoHopOrders, quoteReport: twoHopQuoteReport, isTwoHop: true }; + return { optimizedOrders: twoHopOrders, liquidityDelivered: bestTwoHopQuote, isTwoHop: true }; } // Generate a fallback path if native orders are in the optimal path. @@ -591,18 +596,8 @@ export class MarketOperationUtils { } } const optimizedOrders = createOrdersFromPath(optimalPath, orderOpts); - const quoteReport = opts.shouldGenerateQuoteReport - ? generateQuoteReport( - side, - _.flatten(dexQuotes), - twoHopQuotes, - nativeOrders, - orderFillableAmounts, - _.flatten(optimizedOrders.map(order => order.fills)), - opts.quoteRequestor, - ) - : undefined; - return { optimizedOrders, quoteReport, isTwoHop: false }; + const liquidityDelivered = _.flatten(optimizedOrders.map(order => order.fills)); + return { optimizedOrders, liquidityDelivered, isTwoHop: false }; } } diff --git a/packages/asset-swapper/src/utils/market_operation_utils/types.ts b/packages/asset-swapper/src/utils/market_operation_utils/types.ts index 2bbee4dc81..08e8d05916 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/types.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/types.ts @@ -4,7 +4,6 @@ import { BigNumber } from '@0x/utils'; import { RfqtRequestOpts, SignedOrderWithFillableAmounts } from '../../types'; import { QuoteRequestor } from '../../utils/quote_requestor'; -import { QuoteReport } from '../quote_report_generator'; /** * Order domain keys: chainId and exchange @@ -322,7 +321,7 @@ export interface SourceQuoteOperation export interface OptimizerResult { optimizedOrders: OptimizedMarketOrder[]; isTwoHop: boolean; - quoteReport?: QuoteReport; + liquidityDelivered: CollapsedFill[] | DexSample; } export type MarketDepthSide = Array>>; From 5333befd89e8bc197b033796fe416c7b1cd679ef Mon Sep 17 00:00:00 2001 From: Daniel Pyrathon Date: Thu, 24 Sep 2020 15:37:18 -0700 Subject: [PATCH 02/32] optionally return a quote report too --- .../src/utils/market_operation_utils/index.ts | 27 ++++++++++--------- .../src/utils/market_operation_utils/types.ts | 5 ++++ 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/packages/asset-swapper/src/utils/market_operation_utils/index.ts b/packages/asset-swapper/src/utils/market_operation_utils/index.ts index 75e91a80e9..4db157e76c 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/index.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/index.ts @@ -7,7 +7,7 @@ import * as _ from 'lodash'; import { MarketOperation } from '../../types'; import { QuoteRequestor } from '../quote_requestor'; -import { generateQuoteReport } from './../quote_report_generator'; +import { generateQuoteReport, QuoteReport } from './../quote_report_generator'; import { BUY_SOURCE_FILTER, DEFAULT_GET_MARKET_ORDERS_OPTS, @@ -40,6 +40,7 @@ import { OptimizerResult, OrderDomain, TokenAdjacencyGraph, + OptimizerResultWithReport, } from './types'; // tslint:disable:boolean-naming @@ -79,10 +80,10 @@ export class MarketOperationUtils { private readonly _buySources: SourceFilters; private readonly _feeSources = new SourceFilters(FEE_QUOTE_SOURCES); - private static _computeQuoteReport(nativeOrders: SignedOrder[], quoteRequestor: QuoteRequestor, marketSideLiquidity: MarketSideLiquidity, optimizerResult: OptimizerResult): void { + private static _computeQuoteReport(nativeOrders: SignedOrder[], quoteRequestor: QuoteRequestor, marketSideLiquidity: MarketSideLiquidity, optimizerResult: OptimizerResult): QuoteReport { const {side, dexQuotes, twoHopQuotes, orderFillableAmounts } = marketSideLiquidity; const { liquidityDelivered } = optimizerResult; - generateQuoteReport( + return generateQuoteReport( side, _.flatten(dexQuotes), twoHopQuotes, @@ -354,11 +355,10 @@ export class MarketOperationUtils { nativeOrders: SignedOrder[], takerAmount: BigNumber, opts?: Partial, - gasPrice?: BigNumber, - ): Promise { + ): Promise { const defaultOpts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts }; const marketSideLiquidity = await this.getMarketSellLiquidityAsync(nativeOrders, takerAmount, defaultOpts); - const optimizedOrders = await this._generateOptimizedOrdersAsync(marketSideLiquidity, { + const optimizerResult = await this._generateOptimizedOrdersAsync(marketSideLiquidity, { bridgeSlippage: defaultOpts.bridgeSlippage, maxFallbackSlippage: defaultOpts.maxFallbackSlippage, excludedSources: defaultOpts.excludedSources, @@ -366,10 +366,12 @@ export class MarketOperationUtils { allowFallback: defaultOpts.allowFallback, shouldBatchBridgeOrders: defaultOpts.shouldBatchBridgeOrders, }); + + let quoteReport: QuoteReport | undefined; if (defaultOpts.shouldGenerateQuoteReport && defaultOpts.rfqt && defaultOpts.rfqt.quoteRequestor) { - MarketOperationUtils._computeQuoteReport(nativeOrders, defaultOpts.rfqt.quoteRequestor, marketSideLiquidity, optimizedResult); + quoteReport = MarketOperationUtils._computeQuoteReport(nativeOrders, defaultOpts.rfqt.quoteRequestor, marketSideLiquidity, optimizerResult); } - return optimizedOrders; + return {...optimizerResult, quoteReport}; } /** @@ -384,10 +386,10 @@ export class MarketOperationUtils { nativeOrders: SignedOrder[], makerAmount: BigNumber, opts?: Partial, - ): Promise { + ): Promise { const defaultOpts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts }; const marketSideLiquidity = await this.getMarketBuyLiquidityAsync(nativeOrders, makerAmount, defaultOpts); - const optimizedResult = await this._generateOptimizedOrdersAsync(marketSideLiquidity, { + const optimizerResult = await this._generateOptimizedOrdersAsync(marketSideLiquidity, { bridgeSlippage: defaultOpts.bridgeSlippage, maxFallbackSlippage: defaultOpts.maxFallbackSlippage, excludedSources: defaultOpts.excludedSources, @@ -395,10 +397,11 @@ export class MarketOperationUtils { allowFallback: defaultOpts.allowFallback, shouldBatchBridgeOrders: defaultOpts.shouldBatchBridgeOrders, }); + let quoteReport: QuoteReport | undefined; if (defaultOpts.shouldGenerateQuoteReport && defaultOpts.rfqt && defaultOpts.rfqt.quoteRequestor) { - MarketOperationUtils._computeQuoteReport(nativeOrders, defaultOpts.rfqt.quoteRequestor, marketSideLiquidity, optimizedResult); + quoteReport = MarketOperationUtils._computeQuoteReport(nativeOrders, defaultOpts.rfqt.quoteRequestor, marketSideLiquidity, optimizerResult); } - return optimizedResult; + return {...optimizerResult, quoteReport}; } /** diff --git a/packages/asset-swapper/src/utils/market_operation_utils/types.ts b/packages/asset-swapper/src/utils/market_operation_utils/types.ts index 08e8d05916..5e7ea158fd 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/types.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/types.ts @@ -4,6 +4,7 @@ import { BigNumber } from '@0x/utils'; import { RfqtRequestOpts, SignedOrderWithFillableAmounts } from '../../types'; import { QuoteRequestor } from '../../utils/quote_requestor'; +import { QuoteReport } from '../quote_report_generator'; /** * Order domain keys: chainId and exchange @@ -324,6 +325,10 @@ export interface OptimizerResult { liquidityDelivered: CollapsedFill[] | DexSample; } +export interface OptimizerResultWithReport extends OptimizerResult { + quoteReport?: QuoteReport; +} + export type MarketDepthSide = Array>>; export interface MarketDepth { From 20edcd1ae5782cc87035525e414b8559dff9c618 Mon Sep 17 00:00:00 2001 From: Daniel Pyrathon Date: Thu, 24 Sep 2020 16:32:56 -0700 Subject: [PATCH 03/32] lint and fix --- .../asset-swapper/src/utils/market_operation_utils/index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/asset-swapper/src/utils/market_operation_utils/index.ts b/packages/asset-swapper/src/utils/market_operation_utils/index.ts index 4db157e76c..3b29fde6be 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/index.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/index.ts @@ -33,14 +33,13 @@ import { DexSample, ERC20BridgeSource, FeeSchedule, - FillData, GetMarketOrdersOpts, MarketSideLiquidity, OptimizedMarketOrder, OptimizerResult, + OptimizerResultWithReport, OrderDomain, TokenAdjacencyGraph, - OptimizerResultWithReport, } from './types'; // tslint:disable:boolean-naming From fbe99b41f8af90d944a9ab62fcec8a5006cf6e0b Mon Sep 17 00:00:00 2001 From: "F. Eugene Aumson" Date: Fri, 25 Sep 2020 19:01:53 -0400 Subject: [PATCH 04/32] asset-swapper: log RFQ maker (un)blacklistings --- packages/asset-swapper/src/constants.ts | 10 +++++++++- packages/asset-swapper/src/types.ts | 3 +-- .../asset-swapper/src/utils/quote_requestor.ts | 16 +++++++--------- .../src/utils/rfq_maker_blacklist.ts | 12 +++++++++++- 4 files changed, 28 insertions(+), 13 deletions(-) diff --git a/packages/asset-swapper/src/constants.ts b/packages/asset-swapper/src/constants.ts index f54b81470d..e68c605930 100644 --- a/packages/asset-swapper/src/constants.ts +++ b/packages/asset-swapper/src/constants.ts @@ -1,9 +1,10 @@ -import { BigNumber } from '@0x/utils'; +import { BigNumber, logUtils } from '@0x/utils'; import { ExchangeProxyContractOpts, ExtensionContractType, ForwarderExtensionContractOpts, + LogFunction, OrderPrunerOpts, OrderPrunerPermittedFeeTypes, RfqtRequestOpts, @@ -89,6 +90,11 @@ const DEFAULT_RFQT_REQUEST_OPTS: Partial = { makerEndpointMaxResponseTimeMs: 1000, }; +export const DEFAULT_INFO_LOGGER: LogFunction = (obj, msg) => + logUtils.log(`${msg ? `${msg}: ` : ''}${JSON.stringify(obj)}`); +export const DEFAULT_WARNING_LOGGER: LogFunction = (obj, msg) => + logUtils.warn(`${msg ? `${msg}: ` : ''}${JSON.stringify(obj)}`); + export const constants = { ETH_GAS_STATION_API_URL, PROTOCOL_FEE_MULTIPLIER, @@ -113,4 +119,6 @@ export const constants = { PROTOCOL_FEE_UTILS_POLLING_INTERVAL_IN_MS, MARKET_UTILS_AMOUNT_BUFFER_PERCENTAGE, BRIDGE_ASSET_DATA_PREFIX: '0xdc1600f3', + DEFAULT_INFO_LOGGER, + DEFAULT_WARNING_LOGGER, }; diff --git a/packages/asset-swapper/src/types.ts b/packages/asset-swapper/src/types.ts index 6ec4357215..3cb701481f 100644 --- a/packages/asset-swapper/src/types.ts +++ b/packages/asset-swapper/src/types.ts @@ -9,7 +9,6 @@ import { TokenAdjacencyGraph, } from './utils/market_operation_utils/types'; import { QuoteReport } from './utils/quote_report_generator'; -import { LogFunction } from './utils/quote_requestor'; /** * expiryBufferMs: The number of seconds to add when calculating whether an order is expired or not. Defaults to 300s (5m). @@ -273,7 +272,7 @@ export interface RfqtMakerAssetOfferings { [endpoint: string]: Array<[string, string]>; } -export { LogFunction } from './utils/quote_requestor'; +export type LogFunction = (obj: object, msg?: string, ...args: any[]) => void; export interface SwapQuoterRfqtOpts { takerApiKeyWhitelist: string[]; diff --git a/packages/asset-swapper/src/utils/quote_requestor.ts b/packages/asset-swapper/src/utils/quote_requestor.ts index cd34d0d07a..529a890b59 100644 --- a/packages/asset-swapper/src/utils/quote_requestor.ts +++ b/packages/asset-swapper/src/utils/quote_requestor.ts @@ -2,13 +2,13 @@ import { schemas, SchemaValidator } from '@0x/json-schemas'; import { assetDataUtils, orderCalculationUtils, SignedOrder } from '@0x/order-utils'; import { RFQTFirmQuote, RFQTIndicativeQuote, TakerRequest } from '@0x/quote-server'; import { ERC20AssetData } from '@0x/types'; -import { BigNumber, logUtils } from '@0x/utils'; +import { BigNumber } from '@0x/utils'; import Axios, { AxiosInstance } from 'axios'; import { Agent as HttpAgent } from 'http'; import { Agent as HttpsAgent } from 'https'; import { constants } from '../constants'; -import { MarketOperation, RfqtMakerAssetOfferings, RfqtRequestOpts } from '../types'; +import { LogFunction, MarketOperation, RfqtMakerAssetOfferings, RfqtRequestOpts } from '../types'; import { ONE_SECOND_MS } from './market_operation_utils/constants'; import { RfqMakerBlacklist } from './rfq_maker_blacklist'; @@ -107,20 +107,18 @@ function convertIfAxiosError(error: any): Error | object /* axios' .d.ts has Axi } } -export type LogFunction = (obj: object, msg?: string, ...args: any[]) => void; - export class QuoteRequestor { private readonly _schemaValidator: SchemaValidator = new SchemaValidator(); private readonly _orderSignatureToMakerUri: { [orderSignature: string]: string } = {}; constructor( private readonly _rfqtAssetOfferings: RfqtMakerAssetOfferings, - private readonly _warningLogger: LogFunction = (obj, msg) => - logUtils.warn(`${msg ? `${msg}: ` : ''}${JSON.stringify(obj)}`), - private readonly _infoLogger: LogFunction = (obj, msg) => - logUtils.log(`${msg ? `${msg}: ` : ''}${JSON.stringify(obj)}`), + private readonly _warningLogger: LogFunction = constants.DEFAULT_WARNING_LOGGER, + private readonly _infoLogger: LogFunction = constants.DEFAULT_INFO_LOGGER, private readonly _expiryBufferMs: number = constants.DEFAULT_SWAP_QUOTER_OPTS.expiryBufferMs, - ) {} + ) { + rfqMakerBlacklist.infoLogger = this._infoLogger; + } public async requestRfqtFirmQuotesAsync( makerAssetData: string, diff --git a/packages/asset-swapper/src/utils/rfq_maker_blacklist.ts b/packages/asset-swapper/src/utils/rfq_maker_blacklist.ts index b261b116e0..80297b8069 100644 --- a/packages/asset-swapper/src/utils/rfq_maker_blacklist.ts +++ b/packages/asset-swapper/src/utils/rfq_maker_blacklist.ts @@ -4,11 +4,16 @@ */ import { constants } from '../constants'; +import { LogFunction } from '../types'; export class RfqMakerBlacklist { private readonly _makerTimeoutStreakLength: { [makerUrl: string]: number } = {}; private readonly _makerBlacklistedUntilDate: { [makerUrl: string]: number } = {}; - constructor(private readonly _blacklistDurationMinutes: number, private readonly _timeoutStreakThreshold: number) {} + constructor( + private readonly _blacklistDurationMinutes: number, + private readonly _timeoutStreakThreshold: number, + public infoLogger: LogFunction = constants.DEFAULT_INFO_LOGGER, + ) {} public logTimeoutOrLackThereof(makerUrl: string, didTimeout: boolean): void { if (!this._makerTimeoutStreakLength.hasOwnProperty(makerUrl)) { this._makerTimeoutStreakLength[makerUrl] = 0; @@ -18,6 +23,10 @@ export class RfqMakerBlacklist { if (this._makerTimeoutStreakLength[makerUrl] === this._timeoutStreakThreshold) { this._makerBlacklistedUntilDate[makerUrl] = Date.now() + this._blacklistDurationMinutes * constants.ONE_MINUTE_MS; + this.infoLogger( + { makerUrl, blacklistedUntil: new Date(this._makerBlacklistedUntilDate[makerUrl]).toISOString() }, + 'maker blacklisted', + ); } } else { this._makerTimeoutStreakLength[makerUrl] = 0; @@ -27,6 +36,7 @@ export class RfqMakerBlacklist { const now = Date.now(); if (now > this._makerBlacklistedUntilDate[makerUrl]) { delete this._makerBlacklistedUntilDate[makerUrl]; + this.infoLogger({ makerUrl }, 'maker unblacklisted'); } return this._makerBlacklistedUntilDate[makerUrl] > now; } From a7d502a5016412c1a7e7647d2616f40c7265fc22 Mon Sep 17 00:00:00 2001 From: Daniel Pyrathon Date: Mon, 28 Sep 2020 12:53:40 -0700 Subject: [PATCH 05/32] create quote report regardless of RFQT --- .../src/utils/market_operation_utils/index.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/asset-swapper/src/utils/market_operation_utils/index.ts b/packages/asset-swapper/src/utils/market_operation_utils/index.ts index 3b29fde6be..c922496400 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/index.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/index.ts @@ -40,6 +40,7 @@ import { OptimizerResultWithReport, OrderDomain, TokenAdjacencyGraph, + GenerateOptimizedOrdersOpts, } from './types'; // tslint:disable:boolean-naming @@ -79,7 +80,12 @@ export class MarketOperationUtils { private readonly _buySources: SourceFilters; private readonly _feeSources = new SourceFilters(FEE_QUOTE_SOURCES); - private static _computeQuoteReport(nativeOrders: SignedOrder[], quoteRequestor: QuoteRequestor, marketSideLiquidity: MarketSideLiquidity, optimizerResult: OptimizerResult): QuoteReport { + private static _computeQuoteReport( + nativeOrders: SignedOrder[], + quoteRequestor: QuoteRequestor | undefined, + marketSideLiquidity: MarketSideLiquidity, + optimizerResult: OptimizerResult, + ): QuoteReport { const {side, dexQuotes, twoHopQuotes, orderFillableAmounts } = marketSideLiquidity; const { liquidityDelivered } = optimizerResult; return generateQuoteReport( @@ -366,9 +372,15 @@ export class MarketOperationUtils { shouldBatchBridgeOrders: defaultOpts.shouldBatchBridgeOrders, }); + // Compute Quote Report and return the results. let quoteReport: QuoteReport | undefined; - if (defaultOpts.shouldGenerateQuoteReport && defaultOpts.rfqt && defaultOpts.rfqt.quoteRequestor) { - quoteReport = MarketOperationUtils._computeQuoteReport(nativeOrders, defaultOpts.rfqt.quoteRequestor, marketSideLiquidity, optimizerResult); + if (defaultOpts.shouldGenerateQuoteReport) { + quoteReport = MarketOperationUtils._computeQuoteReport( + nativeOrders, + defaultOpts.rfqt ? defaultOpts.rfqt.quoteRequestor : undefined, + marketSideLiquidity, + optimizerResult, + ); } return {...optimizerResult, quoteReport}; } From 61272961a8380d44da8a7e33ebdc1422f5ffc5eb Mon Sep 17 00:00:00 2001 From: Daniel Pyrathon Date: Tue, 29 Sep 2020 02:02:17 -0700 Subject: [PATCH 06/32] fixed a broken import, renamed variable --- .../src/utils/market_operation_utils/index.ts | 41 +++++++++---------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/packages/asset-swapper/src/utils/market_operation_utils/index.ts b/packages/asset-swapper/src/utils/market_operation_utils/index.ts index c922496400..59305b535c 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/index.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/index.ts @@ -40,7 +40,6 @@ import { OptimizerResultWithReport, OrderDomain, TokenAdjacencyGraph, - GenerateOptimizedOrdersOpts, } from './types'; // tslint:disable:boolean-naming @@ -361,23 +360,23 @@ export class MarketOperationUtils { takerAmount: BigNumber, opts?: Partial, ): Promise { - const defaultOpts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts }; - const marketSideLiquidity = await this.getMarketSellLiquidityAsync(nativeOrders, takerAmount, defaultOpts); + const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts }; + const marketSideLiquidity = await this.getMarketSellLiquidityAsync(nativeOrders, takerAmount, _opts); const optimizerResult = await this._generateOptimizedOrdersAsync(marketSideLiquidity, { - bridgeSlippage: defaultOpts.bridgeSlippage, - maxFallbackSlippage: defaultOpts.maxFallbackSlippage, - excludedSources: defaultOpts.excludedSources, - feeSchedule: defaultOpts.feeSchedule, - allowFallback: defaultOpts.allowFallback, - shouldBatchBridgeOrders: defaultOpts.shouldBatchBridgeOrders, + bridgeSlippage: _opts.bridgeSlippage, + maxFallbackSlippage: _opts.maxFallbackSlippage, + excludedSources: _opts.excludedSources, + feeSchedule: _opts.feeSchedule, + allowFallback: _opts.allowFallback, + shouldBatchBridgeOrders: _opts.shouldBatchBridgeOrders, }); // Compute Quote Report and return the results. let quoteReport: QuoteReport | undefined; - if (defaultOpts.shouldGenerateQuoteReport) { + if (_opts.shouldGenerateQuoteReport) { quoteReport = MarketOperationUtils._computeQuoteReport( nativeOrders, - defaultOpts.rfqt ? defaultOpts.rfqt.quoteRequestor : undefined, + _opts.rfqt ? _opts.rfqt.quoteRequestor : undefined, marketSideLiquidity, optimizerResult, ); @@ -398,19 +397,19 @@ export class MarketOperationUtils { makerAmount: BigNumber, opts?: Partial, ): Promise { - const defaultOpts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts }; - const marketSideLiquidity = await this.getMarketBuyLiquidityAsync(nativeOrders, makerAmount, defaultOpts); + const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts }; + const marketSideLiquidity = await this.getMarketBuyLiquidityAsync(nativeOrders, makerAmount, _opts); const optimizerResult = await this._generateOptimizedOrdersAsync(marketSideLiquidity, { - bridgeSlippage: defaultOpts.bridgeSlippage, - maxFallbackSlippage: defaultOpts.maxFallbackSlippage, - excludedSources: defaultOpts.excludedSources, - feeSchedule: defaultOpts.feeSchedule, - allowFallback: defaultOpts.allowFallback, - shouldBatchBridgeOrders: defaultOpts.shouldBatchBridgeOrders, + bridgeSlippage: _opts.bridgeSlippage, + maxFallbackSlippage: _opts.maxFallbackSlippage, + excludedSources: _opts.excludedSources, + feeSchedule: _opts.feeSchedule, + allowFallback: _opts.allowFallback, + shouldBatchBridgeOrders: _opts.shouldBatchBridgeOrders, }); let quoteReport: QuoteReport | undefined; - if (defaultOpts.shouldGenerateQuoteReport && defaultOpts.rfqt && defaultOpts.rfqt.quoteRequestor) { - quoteReport = MarketOperationUtils._computeQuoteReport(nativeOrders, defaultOpts.rfqt.quoteRequestor, marketSideLiquidity, optimizerResult); + if (_opts.shouldGenerateQuoteReport && _opts.rfqt && _opts.rfqt.quoteRequestor) { + quoteReport = MarketOperationUtils._computeQuoteReport(nativeOrders, _opts.rfqt.quoteRequestor, marketSideLiquidity, optimizerResult); } return {...optimizerResult, quoteReport}; } From 90ed283b201ac0d20017d2a4a45392e33efebf85 Mon Sep 17 00:00:00 2001 From: Daniel Pyrathon Date: Tue, 29 Sep 2020 02:08:00 -0700 Subject: [PATCH 07/32] performed linting --- packages/asset-swapper/package.json | 2 +- .../src/utils/market_operation_utils/index.ts | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/asset-swapper/package.json b/packages/asset-swapper/package.json index 293c1a5ee0..f4d5d2ad19 100644 --- a/packages/asset-swapper/package.json +++ b/packages/asset-swapper/package.json @@ -17,7 +17,7 @@ "compile": "sol-compiler", "lint": "tslint --format stylish --project . --exclude ./generated-wrappers/**/* --exclude ./test/generated-wrappers/**/* --exclude ./generated-artifacts/**/* --exclude ./test/generated-artifacts/**/* --exclude **/lib/**/* && yarn lint-contracts", "lint-contracts": "#solhint -c .solhint.json contracts/**/**/**/**/*.sol", - "prettier": "prettier '**/*.{ts,tsx,json,md}' --config ../../.prettierrc --ignore-path ../../.prettierignore", + "prettier": "prettier --write '**/*.{ts,tsx,json,md}' --config ../../.prettierrc --ignore-path ../../.prettierignore", "fix": "tslint --fix --format stylish --project . --exclude ./generated-wrappers/**/* --exclude ./generated-artifacts/**/* --exclude ./test/generated-wrappers/**/* --exclude ./test/generated-artifacts/**/* --exclude **/lib/**/* && yarn lint-contracts", "test": "yarn run_mocha", "rebuild_and_test": "run-s clean build test", diff --git a/packages/asset-swapper/src/utils/market_operation_utils/index.ts b/packages/asset-swapper/src/utils/market_operation_utils/index.ts index 59305b535c..d8f673b7b1 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/index.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/index.ts @@ -85,7 +85,7 @@ export class MarketOperationUtils { marketSideLiquidity: MarketSideLiquidity, optimizerResult: OptimizerResult, ): QuoteReport { - const {side, dexQuotes, twoHopQuotes, orderFillableAmounts } = marketSideLiquidity; + const { side, dexQuotes, twoHopQuotes, orderFillableAmounts } = marketSideLiquidity; const { liquidityDelivered } = optimizerResult; return generateQuoteReport( side, @@ -381,7 +381,7 @@ export class MarketOperationUtils { optimizerResult, ); } - return {...optimizerResult, quoteReport}; + return { ...optimizerResult, quoteReport }; } /** @@ -409,9 +409,14 @@ export class MarketOperationUtils { }); let quoteReport: QuoteReport | undefined; if (_opts.shouldGenerateQuoteReport && _opts.rfqt && _opts.rfqt.quoteRequestor) { - quoteReport = MarketOperationUtils._computeQuoteReport(nativeOrders, _opts.rfqt.quoteRequestor, marketSideLiquidity, optimizerResult); + quoteReport = MarketOperationUtils._computeQuoteReport( + nativeOrders, + _opts.rfqt.quoteRequestor, + marketSideLiquidity, + optimizerResult, + ); } - return {...optimizerResult, quoteReport}; + return { ...optimizerResult, quoteReport }; } /** From 733c3046bc7bab433e47a9f688e2d932675c14b3 Mon Sep 17 00:00:00 2001 From: Daniel Pyrathon Date: Wed, 30 Sep 2020 17:02:09 -0700 Subject: [PATCH 08/32] fixed a small bug with buys and quote reporter --- .../asset-swapper/src/utils/market_operation_utils/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/asset-swapper/src/utils/market_operation_utils/index.ts b/packages/asset-swapper/src/utils/market_operation_utils/index.ts index d8f673b7b1..dea88d3537 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/index.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/index.ts @@ -408,10 +408,10 @@ export class MarketOperationUtils { shouldBatchBridgeOrders: _opts.shouldBatchBridgeOrders, }); let quoteReport: QuoteReport | undefined; - if (_opts.shouldGenerateQuoteReport && _opts.rfqt && _opts.rfqt.quoteRequestor) { + if (_opts.shouldGenerateQuoteReport) { quoteReport = MarketOperationUtils._computeQuoteReport( nativeOrders, - _opts.rfqt.quoteRequestor, + _opts.rfqt ? _opts.rfqt.quoteRequestor : undefined, marketSideLiquidity, optimizerResult, ); From 84862f15c1cb6551748102a9a101b0a04dedc8e1 Mon Sep 17 00:00:00 2001 From: "F. Eugene Aumson" Date: Fri, 2 Oct 2020 13:43:17 -0400 Subject: [PATCH 09/32] Store blacklistedUntil as local var, don't ref map Addresses review comment https://github.com/0xProject/0x-monorepo/pull/2714#discussion_r497816501 --- packages/asset-swapper/src/utils/rfq_maker_blacklist.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/asset-swapper/src/utils/rfq_maker_blacklist.ts b/packages/asset-swapper/src/utils/rfq_maker_blacklist.ts index 80297b8069..8cfdf4c2b6 100644 --- a/packages/asset-swapper/src/utils/rfq_maker_blacklist.ts +++ b/packages/asset-swapper/src/utils/rfq_maker_blacklist.ts @@ -21,10 +21,10 @@ export class RfqMakerBlacklist { if (didTimeout) { this._makerTimeoutStreakLength[makerUrl] += 1; if (this._makerTimeoutStreakLength[makerUrl] === this._timeoutStreakThreshold) { - this._makerBlacklistedUntilDate[makerUrl] = - Date.now() + this._blacklistDurationMinutes * constants.ONE_MINUTE_MS; + const blacklistEnd = Date.now() + this._blacklistDurationMinutes * constants.ONE_MINUTE_MS; + this._makerBlacklistedUntilDate[makerUrl] = blacklistEnd; this.infoLogger( - { makerUrl, blacklistedUntil: new Date(this._makerBlacklistedUntilDate[makerUrl]).toISOString() }, + { makerUrl, blacklistedUntil: new Date(blacklistEnd).toISOString() }, 'maker blacklisted', ); } From 32923ec7e14913db631d77252ca1b91eb79dc5a7 Mon Sep 17 00:00:00 2001 From: "F. Eugene Aumson" Date: Fri, 2 Oct 2020 14:18:31 -0400 Subject: [PATCH 10/32] Blacklist for timeout === max, not just > --- packages/asset-swapper/src/utils/quote_requestor.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/asset-swapper/src/utils/quote_requestor.ts b/packages/asset-swapper/src/utils/quote_requestor.ts index 529a890b59..ea889e5ebd 100644 --- a/packages/asset-swapper/src/utils/quote_requestor.ts +++ b/packages/asset-swapper/src/utils/quote_requestor.ts @@ -393,7 +393,7 @@ export class QuoteRequestor { }, }, }); - rfqMakerBlacklist.logTimeoutOrLackThereof(url, latencyMs > maxResponseTimeMs); + rfqMakerBlacklist.logTimeoutOrLackThereof(url, latencyMs >= maxResponseTimeMs); result.push({ response: response.data, makerUri: url }); } catch (err) { const latencyMs = Date.now() - timeBeforeAwait; @@ -409,7 +409,7 @@ export class QuoteRequestor { }, }, }); - rfqMakerBlacklist.logTimeoutOrLackThereof(url, latencyMs > maxResponseTimeMs); + rfqMakerBlacklist.logTimeoutOrLackThereof(url, latencyMs >= maxResponseTimeMs); this._warningLogger( convertIfAxiosError(err), `Failed to get RFQ-T ${quoteType} quote from market maker endpoint ${url} for API key ${ From 78e3cd39d189202d4106963366c3d7dd3ca857d6 Mon Sep 17 00:00:00 2001 From: Michael Zhu Date: Fri, 28 Aug 2020 10:37:17 -0700 Subject: [PATCH 11/32] `@0x/contracts-zero-ex`: Add LiquidityProviderFeature contracts --- contracts/zero-ex/contracts/src/IZeroEx.sol | 4 +- .../errors/LibLiquidityProviderRichErrors.sol | 63 ++++ .../features/ILiquidityProviderFeature.sol | 66 ++++ .../src/features/LiquidityProviderFeature.sol | 204 +++++++++++++ .../src/features/MetaTransactionsFeature.sol | 4 +- .../storage/LibLiquidityProviderStorage.sol | 45 +++ .../contracts/src/storage/LibStorage.sol | 3 +- contracts/zero-ex/package.json | 2 +- contracts/zero-ex/test/artifacts.ts | 8 + .../test/features/liquidity_provider_test.ts | 48 +++ contracts/zero-ex/test/wrappers.ts | 4 + contracts/zero-ex/tsconfig.json | 4 + .../contract-artifacts/artifacts/IZeroEx.json | 60 ++++ .../src/generated-wrappers/i_zero_ex.ts | 286 ++++++++++++++++++ 14 files changed, 796 insertions(+), 5 deletions(-) create mode 100644 contracts/zero-ex/contracts/src/errors/LibLiquidityProviderRichErrors.sol create mode 100644 contracts/zero-ex/contracts/src/features/ILiquidityProviderFeature.sol create mode 100644 contracts/zero-ex/contracts/src/features/LiquidityProviderFeature.sol create mode 100644 contracts/zero-ex/contracts/src/storage/LibLiquidityProviderStorage.sol create mode 100644 contracts/zero-ex/test/features/liquidity_provider_test.ts diff --git a/contracts/zero-ex/contracts/src/IZeroEx.sol b/contracts/zero-ex/contracts/src/IZeroEx.sol index b31371a02f..8a84a0425a 100644 --- a/contracts/zero-ex/contracts/src/IZeroEx.sol +++ b/contracts/zero-ex/contracts/src/IZeroEx.sol @@ -26,6 +26,7 @@ import "./features/ISignatureValidatorFeature.sol"; import "./features/ITransformERC20Feature.sol"; import "./features/IMetaTransactionsFeature.sol"; import "./features/IUniswapFeature.sol"; +import "./features/ILiquidityProviderFeature.sol"; /// @dev Interface for a fully featured Exchange Proxy. @@ -36,7 +37,8 @@ interface IZeroEx is ISignatureValidatorFeature, ITransformERC20Feature, IMetaTransactionsFeature, - IUniswapFeature + IUniswapFeature, + ILiquidityProviderFeature { // solhint-disable state-visibility diff --git a/contracts/zero-ex/contracts/src/errors/LibLiquidityProviderRichErrors.sol b/contracts/zero-ex/contracts/src/errors/LibLiquidityProviderRichErrors.sol new file mode 100644 index 0000000000..177df23a08 --- /dev/null +++ b/contracts/zero-ex/contracts/src/errors/LibLiquidityProviderRichErrors.sol @@ -0,0 +1,63 @@ +/* + + Copyright 2020 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.6.5; + + +library LibLiquidityProviderRichErrors { + + // solhint-disable func-name-mixedcase + + function LiquidityProviderIncompleteSellError( + address providerAddress, + address makerToken, + address takerToken, + uint256 sellAmount, + uint256 boughtAmount, + uint256 minBuyAmount + ) + internal + pure + returns (bytes memory) + { + return abi.encodeWithSelector( + bytes4(keccak256("LiquidityProviderIncompleteSellError(address,address,address,uint256,uint256,uint256)")), + providerAddress, + makerToken, + takerToken, + sellAmount, + boughtAmount, + minBuyAmount + ); + } + + function NoLiquidityProviderForMarketError( + address xAsset, + address yAsset + ) + internal + pure + returns (bytes memory) + { + return abi.encodeWithSelector( + bytes4(keccak256("NoLiquidityProviderForMarketError(address,address)")), + xAsset, + yAsset + ); + } +} diff --git a/contracts/zero-ex/contracts/src/features/ILiquidityProviderFeature.sol b/contracts/zero-ex/contracts/src/features/ILiquidityProviderFeature.sol new file mode 100644 index 0000000000..02792df8f6 --- /dev/null +++ b/contracts/zero-ex/contracts/src/features/ILiquidityProviderFeature.sol @@ -0,0 +1,66 @@ +/* + + Copyright 2020 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.6.5; +pragma experimental ABIEncoderV2; + + +/// @dev Feature to swap directly with an on-chain liquidity provider. +interface ILiquidityProviderFeature { + event LiquidityProviderForMarketUpdated( + address indexed xAsset, + address indexed yAsset, + address providerAddress + ); + + function sellToLiquidityProvider( + address makerToken, + address takerToken, + address payable recipient, + uint256 sellAmount, + uint256 minBuyAmount + ) + external + payable + returns (uint256 boughtAmount); + + /// @dev Sets address of the liquidity provider for a market given + /// (xAsset, yAsset). + /// @param xAsset First asset managed by the liquidity provider. + /// @param yAsset Second asset managed by the liquidity provider. + /// @param providerAddress Address of the liquidity provider. + function setLiquidityProviderForMarket( + address xAsset, + address yAsset, + address providerAddress + ) + external; + + /// @dev Returns the address of the liquidity provider for a market given + /// (xAsset, yAsset), or reverts if pool does not exist. + /// @param xAsset First asset managed by the liquidity provider. + /// @param yAsset Second asset managed by the liquidity provider. + /// @return providerAddress Address of the liquidity provider. + function getLiquidityProviderForMarket( + address xAsset, + address yAsset + ) + external + view + returns (address providerAddress); +} diff --git a/contracts/zero-ex/contracts/src/features/LiquidityProviderFeature.sol b/contracts/zero-ex/contracts/src/features/LiquidityProviderFeature.sol new file mode 100644 index 0000000000..d152394591 --- /dev/null +++ b/contracts/zero-ex/contracts/src/features/LiquidityProviderFeature.sol @@ -0,0 +1,204 @@ +/* + + Copyright 2020 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.6.5; +pragma experimental ABIEncoderV2; + +import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol"; +import "@0x/contracts-erc20/contracts/src/v06/IEtherTokenV06.sol"; +import "@0x/contracts-erc20/contracts/src/v06/LibERC20TokenV06.sol"; +import "@0x/contracts-utils/contracts/src/v06/errors/LibRichErrorsV06.sol"; +import "@0x/contracts-utils/contracts/src/v06/LibSafeMathV06.sol"; +import "../errors/LibLiquidityProviderRichErrors.sol"; +import "../fixins/FixinCommon.sol"; +import "../storage/LibLiquidityProviderStorage.sol"; +import "./IFeature.sol"; +import "./ILiquidityProviderFeature.sol"; +import "./ITokenSpenderFeature.sol"; + + +interface IERC20Bridge { + /// @dev Transfers `amount` of the ERC20 `tokenAddress` from `from` to `to`. + /// @param tokenAddress The address of the ERC20 token to transfer. + /// @param from Address to transfer asset from. + /// @param to Address to transfer asset to. + /// @param amount Amount of asset to transfer. + /// @param bridgeData Arbitrary asset data needed by the bridge contract. + /// @return success The magic bytes `0xdc1600f3` if successful. + function bridgeTransferFrom( + address tokenAddress, + address from, + address to, + uint256 amount, + bytes calldata bridgeData + ) + external + returns (bytes4 success); +} + +contract LiquidityProviderFeature is + IFeature, + ILiquidityProviderFeature, + FixinCommon +{ + using LibERC20TokenV06 for IERC20TokenV06; + using LibSafeMathV06 for uint256; + using LibRichErrorsV06 for bytes; + + /// @dev Name of this feature. + string public constant override FEATURE_NAME = "LiquidityProviderFeature"; + /// @dev Version of this feature. + uint256 public immutable override FEATURE_VERSION = _encodeVersion(1, 0, 0); + + /// @dev ETH pseudo-token address. + address constant internal ETH_TOKEN_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + /// @dev The WETH contract address. + IEtherTokenV06 public immutable weth; + + /// @dev Store the WETH address in an immutable. + /// @param weth_ The weth token. + constructor(IEtherTokenV06 weth_) + public + FixinCommon() + { + weth = weth_; + } + + function sellToLiquidityProvider( + address makerToken, + address takerToken, + address payable recipient, + uint256 sellAmount, + uint256 minBuyAmount + ) + external + override + payable + returns (uint256 boughtAmount) + { + address providerAddress = getLiquidityProviderForMarket(makerToken, takerToken); + if (recipient == address(0)) { + recipient = msg.sender; + } + + if (takerToken == ETH_TOKEN_ADDRESS) { + // Wrap ETH. + weth.deposit{value: sellAmount}(); + weth.transfer(providerAddress, sellAmount); + } else { + ITokenSpenderFeature(address(this))._spendERC20Tokens( + IERC20TokenV06(takerToken), + msg.sender, + providerAddress, + sellAmount + ); + } + + if (makerToken == ETH_TOKEN_ADDRESS) { + uint256 balanceBefore = weth.balanceOf(address(this)); + IERC20Bridge(providerAddress).bridgeTransferFrom( + address(weth), + address(0), + address(this), + minBuyAmount, + "" + ); + boughtAmount = weth.balanceOf(address(this)).safeSub(balanceBefore); + // Unwrap wETH and send ETH to recipient. + weth.withdraw(boughtAmount); + recipient.transfer(boughtAmount); + } else { + uint256 balanceBefore = IERC20TokenV06(makerToken).balanceOf(recipient); + IERC20Bridge(providerAddress).bridgeTransferFrom( + makerToken, + address(0), + recipient, + minBuyAmount, + "" + ); + boughtAmount = IERC20TokenV06(makerToken).balanceOf(recipient).safeSub(balanceBefore); + } + if (boughtAmount < minBuyAmount) { + LibLiquidityProviderRichErrors.LiquidityProviderIncompleteSellError( + providerAddress, + makerToken, + takerToken, + sellAmount, + boughtAmount, + minBuyAmount + ).rrevert(); + } + } + + /// @dev Sets address of the liquidity provider for a market given + /// (xAsset, yAsset). + /// @param xAsset First asset managed by the liquidity provider. + /// @param yAsset Second asset managed by the liquidity provider. + /// @param providerAddress Address of the liquidity provider. + function setLiquidityProviderForMarket( + address xAsset, + address yAsset, + address providerAddress + ) + external + override + onlyOwner + { + LibLiquidityProviderStorage.getStorage() + .addressBook[xAsset][yAsset] = providerAddress; + LibLiquidityProviderStorage.getStorage() + .addressBook[yAsset][xAsset] = providerAddress; + emit LiquidityProviderForMarketUpdated( + xAsset, + yAsset, + providerAddress + ); + } + + /// @dev Returns the address of the liquidity provider for a market given + /// (xAsset, yAsset), or reverts if pool does not exist. + /// @param xAsset First asset managed by the liquidity provider. + /// @param yAsset Second asset managed by the liquidity provider. + /// @return providerAddress Address of the liquidity provider. + function getLiquidityProviderForMarket( + address xAsset, + address yAsset + ) + public + view + override + returns (address providerAddress) + { + if (xAsset == ETH_TOKEN_ADDRESS) { + providerAddress = LibLiquidityProviderStorage.getStorage() + .addressBook[address(weth)][yAsset]; + } else if (yAsset == ETH_TOKEN_ADDRESS) { + providerAddress = LibLiquidityProviderStorage.getStorage() + .addressBook[xAsset][address(weth)]; + } else { + providerAddress = LibLiquidityProviderStorage.getStorage() + .addressBook[xAsset][yAsset]; + } + if (providerAddress == address(0)) { + LibLiquidityProviderRichErrors.NoLiquidityProviderForMarketError( + xAsset, + yAsset + ).rrevert(); + } + } +} diff --git a/contracts/zero-ex/contracts/src/features/MetaTransactionsFeature.sol b/contracts/zero-ex/contracts/src/features/MetaTransactionsFeature.sol index bd6e40b1c5..6ac4475d8d 100644 --- a/contracts/zero-ex/contracts/src/features/MetaTransactionsFeature.sol +++ b/contracts/zero-ex/contracts/src/features/MetaTransactionsFeature.sol @@ -184,7 +184,7 @@ contract MetaTransactionsFeature is /// @dev Execute a meta-transaction via `sender`. Privileged variant. /// Only callable from within. - /// @param sender Who is executing the meta-transaction.. + /// @param sender Who is executing the meta-transaction. /// @param mtx The meta-transaction. /// @param signature The signature by `mtx.signer`. /// @return returnResult The ABI-encoded result of the underlying call. @@ -454,7 +454,7 @@ contract MetaTransactionsFeature is } /// @dev Make an arbitrary internal, meta-transaction call. - /// Warning: Do not let unadulerated `callData` into this function. + /// Warning: Do not let unadulterated `callData` into this function. function _callSelf(bytes32 hash, bytes memory callData, uint256 value) private returns (bytes memory returnResult) diff --git a/contracts/zero-ex/contracts/src/storage/LibLiquidityProviderStorage.sol b/contracts/zero-ex/contracts/src/storage/LibLiquidityProviderStorage.sol new file mode 100644 index 0000000000..99f0361ef0 --- /dev/null +++ b/contracts/zero-ex/contracts/src/storage/LibLiquidityProviderStorage.sol @@ -0,0 +1,45 @@ +/* + + Copyright 2020 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.6.5; +pragma experimental ABIEncoderV2; + +import "./LibStorage.sol"; + + +/// @dev Storage helpers for `LiquidityProviderFeature`. +library LibLiquidityProviderStorage { + + /// @dev Storage bucket for this feature. + struct Storage { + // Mapping of taker token -> maker token -> liquidity provider address + // Note that addressBook[x][y] == addressBook[y][x] will always hold. + mapping (address => mapping (address => address)) addressBook; + } + + /// @dev Get the storage bucket for this contract. + function getStorage() internal pure returns (Storage storage stor) { + uint256 storageSlot = LibStorage.getStorageSlot( + LibStorage.StorageId.LiquidityProvider + ); + // Dip into assembly to change the slot pointed to by the local + // variable `stor`. + // See https://solidity.readthedocs.io/en/v0.6.8/assembly.html?highlight=slot#access-to-external-variables-functions-and-libraries + assembly { stor_slot := storageSlot } + } +} diff --git a/contracts/zero-ex/contracts/src/storage/LibStorage.sol b/contracts/zero-ex/contracts/src/storage/LibStorage.sol index 809977d4a8..129254f82a 100644 --- a/contracts/zero-ex/contracts/src/storage/LibStorage.sol +++ b/contracts/zero-ex/contracts/src/storage/LibStorage.sol @@ -36,7 +36,8 @@ library LibStorage { TokenSpender, TransformERC20, MetaTransactions, - ReentrancyGuard + ReentrancyGuard, + LiquidityProvider } /// @dev Get the storage slot given a storage ID. We assign unique, well-spaced diff --git a/contracts/zero-ex/package.json b/contracts/zero-ex/package.json index 89d547f032..ee3cac9677 100644 --- a/contracts/zero-ex/package.json +++ b/contracts/zero-ex/package.json @@ -41,7 +41,7 @@ "config": { "publicInterfaceContracts": "IZeroEx,ZeroEx,FullMigration,InitialMigration,IFlashWallet,IAllowanceTarget,IERC20Transformer,IOwnableFeature,ISimpleFunctionRegistryFeature,ITokenSpenderFeature,ITransformERC20Feature,FillQuoteTransformer,PayTakerTransformer,WethTransformer,OwnableFeature,SimpleFunctionRegistryFeature,TransformERC20Feature,TokenSpenderFeature,AffiliateFeeTransformer,SignatureValidatorFeature,MetaTransactionsFeature,LogMetadataTransformer,BridgeAdapter", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.", - "abis": "./test/generated-artifacts/@(AffiliateFeeTransformer|AllowanceTarget|BootstrapFeature|BridgeAdapter|FillQuoteTransformer|FixinCommon|FixinEIP712|FixinReentrancyGuard|FlashWallet|FullMigration|IAllowanceTarget|IBootstrapFeature|IBridgeAdapter|IERC20Bridge|IERC20Transformer|IExchange|IFeature|IFlashWallet|IGasToken|IMetaTransactionsFeature|IOwnableFeature|ISignatureValidatorFeature|ISimpleFunctionRegistryFeature|ITestSimpleFunctionRegistryFeature|ITokenSpenderFeature|ITransformERC20Feature|IUniswapFeature|IZeroEx|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibMetaTransactionsRichErrors|LibMetaTransactionsStorage|LibMigrate|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibReentrancyGuardStorage|LibSignatureRichErrors|LibSignedCallData|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibSpenderRichErrors|LibStorage|LibTokenSpenderStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|LogMetadataTransformer|MetaTransactionsFeature|MixinAdapterAddresses|MixinBalancer|MixinCurve|MixinKyber|MixinMStable|MixinMooniswap|MixinOasis|MixinUniswap|MixinUniswapV2|MixinZeroExBridge|OwnableFeature|PayTakerTransformer|SignatureValidatorFeature|SimpleFunctionRegistryFeature|TestCallTarget|TestDelegateCaller|TestFillQuoteTransformerBridge|TestFillQuoteTransformerExchange|TestFillQuoteTransformerHost|TestFullMigration|TestInitialMigration|TestMetaTransactionsTransformERC20Feature|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestTokenSpender|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerBase|TestTransformerDeployerTransformer|TestTransformerHost|TestWeth|TestWethTransformerHost|TestZeroExFeature|TokenSpenderFeature|TransformERC20Feature|Transformer|TransformerDeployer|UniswapFeature|WethTransformer|ZeroEx).json" + "abis": "./test/generated-artifacts/@(AffiliateFeeTransformer|AllowanceTarget|BootstrapFeature|BridgeAdapter|FillQuoteTransformer|FixinCommon|FixinEIP712|FixinReentrancyGuard|FlashWallet|FullMigration|IAllowanceTarget|IBootstrapFeature|IBridgeAdapter|IERC20Bridge|IERC20Transformer|IExchange|IFeature|IFlashWallet|IGasToken|ILiquidityProviderFeature|IMetaTransactionsFeature|IOwnableFeature|ISignatureValidatorFeature|ISimpleFunctionRegistryFeature|ITestSimpleFunctionRegistryFeature|ITokenSpenderFeature|ITransformERC20Feature|IUniswapFeature|IZeroEx|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibLiquidityProviderRichErrors|LibLiquidityProviderStorage|LibMetaTransactionsRichErrors|LibMetaTransactionsStorage|LibMigrate|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibReentrancyGuardStorage|LibSignatureRichErrors|LibSignedCallData|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibSpenderRichErrors|LibStorage|LibTokenSpenderStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|LiquidityProviderFeature|LogMetadataTransformer|MetaTransactionsFeature|MixinAdapterAddresses|MixinBalancer|MixinCurve|MixinKyber|MixinMStable|MixinMooniswap|MixinOasis|MixinUniswap|MixinUniswapV2|MixinZeroExBridge|OwnableFeature|PayTakerTransformer|SignatureValidatorFeature|SimpleFunctionRegistryFeature|TestCallTarget|TestDelegateCaller|TestFillQuoteTransformerBridge|TestFillQuoteTransformerExchange|TestFillQuoteTransformerHost|TestFullMigration|TestInitialMigration|TestMetaTransactionsTransformERC20Feature|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestTokenSpender|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerBase|TestTransformerDeployerTransformer|TestTransformerHost|TestWeth|TestWethTransformerHost|TestZeroExFeature|TokenSpenderFeature|TransformERC20Feature|Transformer|TransformerDeployer|UniswapFeature|WethTransformer|ZeroEx).json" }, "repository": { "type": "git", diff --git a/contracts/zero-ex/test/artifacts.ts b/contracts/zero-ex/test/artifacts.ts index d97231955e..a2eb14facf 100644 --- a/contracts/zero-ex/test/artifacts.ts +++ b/contracts/zero-ex/test/artifacts.ts @@ -24,6 +24,7 @@ import * as IExchange from '../test/generated-artifacts/IExchange.json'; import * as IFeature from '../test/generated-artifacts/IFeature.json'; import * as IFlashWallet from '../test/generated-artifacts/IFlashWallet.json'; import * as IGasToken from '../test/generated-artifacts/IGasToken.json'; +import * as ILiquidityProviderFeature from '../test/generated-artifacts/ILiquidityProviderFeature.json'; import * as IMetaTransactionsFeature from '../test/generated-artifacts/IMetaTransactionsFeature.json'; import * as InitialMigration from '../test/generated-artifacts/InitialMigration.json'; import * as IOwnableFeature from '../test/generated-artifacts/IOwnableFeature.json'; @@ -37,6 +38,8 @@ import * as IZeroEx from '../test/generated-artifacts/IZeroEx.json'; import * as LibBootstrap from '../test/generated-artifacts/LibBootstrap.json'; import * as LibCommonRichErrors from '../test/generated-artifacts/LibCommonRichErrors.json'; import * as LibERC20Transformer from '../test/generated-artifacts/LibERC20Transformer.json'; +import * as LibLiquidityProviderRichErrors from '../test/generated-artifacts/LibLiquidityProviderRichErrors.json'; +import * as LibLiquidityProviderStorage from '../test/generated-artifacts/LibLiquidityProviderStorage.json'; import * as LibMetaTransactionsRichErrors from '../test/generated-artifacts/LibMetaTransactionsRichErrors.json'; import * as LibMetaTransactionsStorage from '../test/generated-artifacts/LibMetaTransactionsStorage.json'; import * as LibMigrate from '../test/generated-artifacts/LibMigrate.json'; @@ -55,6 +58,7 @@ import * as LibTokenSpenderStorage from '../test/generated-artifacts/LibTokenSpe import * as LibTransformERC20RichErrors from '../test/generated-artifacts/LibTransformERC20RichErrors.json'; import * as LibTransformERC20Storage from '../test/generated-artifacts/LibTransformERC20Storage.json'; import * as LibWalletRichErrors from '../test/generated-artifacts/LibWalletRichErrors.json'; +import * as LiquidityProviderFeature from '../test/generated-artifacts/LiquidityProviderFeature.json'; import * as LogMetadataTransformer from '../test/generated-artifacts/LogMetadataTransformer.json'; import * as MetaTransactionsFeature from '../test/generated-artifacts/MetaTransactionsFeature.json'; import * as MixinAdapterAddresses from '../test/generated-artifacts/MixinAdapterAddresses.json'; @@ -104,6 +108,7 @@ export const artifacts = { IZeroEx: IZeroEx as ContractArtifact, ZeroEx: ZeroEx as ContractArtifact, LibCommonRichErrors: LibCommonRichErrors as ContractArtifact, + LibLiquidityProviderRichErrors: LibLiquidityProviderRichErrors as ContractArtifact, LibMetaTransactionsRichErrors: LibMetaTransactionsRichErrors as ContractArtifact, LibOwnableRichErrors: LibOwnableRichErrors as ContractArtifact, LibProxyRichErrors: LibProxyRichErrors as ContractArtifact, @@ -120,6 +125,7 @@ export const artifacts = { BootstrapFeature: BootstrapFeature as ContractArtifact, IBootstrapFeature: IBootstrapFeature as ContractArtifact, IFeature: IFeature as ContractArtifact, + ILiquidityProviderFeature: ILiquidityProviderFeature as ContractArtifact, IMetaTransactionsFeature: IMetaTransactionsFeature as ContractArtifact, IOwnableFeature: IOwnableFeature as ContractArtifact, ISignatureValidatorFeature: ISignatureValidatorFeature as ContractArtifact, @@ -127,6 +133,7 @@ export const artifacts = { ITokenSpenderFeature: ITokenSpenderFeature as ContractArtifact, ITransformERC20Feature: ITransformERC20Feature as ContractArtifact, IUniswapFeature: IUniswapFeature as ContractArtifact, + LiquidityProviderFeature: LiquidityProviderFeature as ContractArtifact, MetaTransactionsFeature: MetaTransactionsFeature as ContractArtifact, OwnableFeature: OwnableFeature as ContractArtifact, SignatureValidatorFeature: SignatureValidatorFeature as ContractArtifact, @@ -142,6 +149,7 @@ export const artifacts = { InitialMigration: InitialMigration as ContractArtifact, LibBootstrap: LibBootstrap as ContractArtifact, LibMigrate: LibMigrate as ContractArtifact, + LibLiquidityProviderStorage: LibLiquidityProviderStorage as ContractArtifact, LibMetaTransactionsStorage: LibMetaTransactionsStorage as ContractArtifact, LibOwnableStorage: LibOwnableStorage as ContractArtifact, LibProxyStorage: LibProxyStorage as ContractArtifact, diff --git a/contracts/zero-ex/test/features/liquidity_provider_test.ts b/contracts/zero-ex/test/features/liquidity_provider_test.ts new file mode 100644 index 0000000000..4c183456df --- /dev/null +++ b/contracts/zero-ex/test/features/liquidity_provider_test.ts @@ -0,0 +1,48 @@ +import { + blockchainTests, + expect, + getRandomInteger, + randomAddress, + verifyEventsFromLogs, +} from '@0x/contracts-test-utils'; +import { BigNumber, hexUtils, StringRevertError, ZeroExRevertErrors } from '@0x/utils'; + +import { IZeroExContract, TokenSpenderFeatureContract } from '../../src/wrappers'; +import { artifacts } from '../artifacts'; +import { abis } from '../utils/abis'; +import { fullMigrateAsync } from '../utils/migration'; +import { TestTokenSpenderERC20TokenContract, TestTokenSpenderERC20TokenEvents } from '../wrappers'; + +blockchainTests.resets('LiquidityProvider feature', env => { + let zeroEx: IZeroExContract; + let feature: TokenSpenderFeatureContract; + let token: TestTokenSpenderERC20TokenContract; + let allowanceTarget: string; + + before(async () => { + const [owner] = await env.getAccountAddressesAsync(); + zeroEx = await fullMigrateAsync(owner, env.provider, env.txDefaults, { + tokenSpender: (await TokenSpenderFeatureContract.deployFrom0xArtifactAsync( + artifacts.TestTokenSpender, + env.provider, + env.txDefaults, + artifacts, + )).address, + }); + feature = new TokenSpenderFeatureContract(zeroEx.address, env.provider, env.txDefaults, abis); + token = await TestTokenSpenderERC20TokenContract.deployFrom0xArtifactAsync( + artifacts.TestTokenSpenderERC20Token, + env.provider, + env.txDefaults, + artifacts, + ); + allowanceTarget = await feature.getAllowanceTarget().callAsync(); + }); + + describe('Registry', () => { + + }); + describe('Swap', () => { + + }); +}); diff --git a/contracts/zero-ex/test/wrappers.ts b/contracts/zero-ex/test/wrappers.ts index 904e4db89a..1aa12ac0c7 100644 --- a/contracts/zero-ex/test/wrappers.ts +++ b/contracts/zero-ex/test/wrappers.ts @@ -22,6 +22,7 @@ export * from '../test/generated-wrappers/i_exchange'; export * from '../test/generated-wrappers/i_feature'; export * from '../test/generated-wrappers/i_flash_wallet'; export * from '../test/generated-wrappers/i_gas_token'; +export * from '../test/generated-wrappers/i_liquidity_provider_feature'; export * from '../test/generated-wrappers/i_meta_transactions_feature'; export * from '../test/generated-wrappers/i_ownable_feature'; export * from '../test/generated-wrappers/i_signature_validator_feature'; @@ -35,6 +36,8 @@ export * from '../test/generated-wrappers/initial_migration'; export * from '../test/generated-wrappers/lib_bootstrap'; export * from '../test/generated-wrappers/lib_common_rich_errors'; export * from '../test/generated-wrappers/lib_erc20_transformer'; +export * from '../test/generated-wrappers/lib_liquidity_provider_rich_errors'; +export * from '../test/generated-wrappers/lib_liquidity_provider_storage'; export * from '../test/generated-wrappers/lib_meta_transactions_rich_errors'; export * from '../test/generated-wrappers/lib_meta_transactions_storage'; export * from '../test/generated-wrappers/lib_migrate'; @@ -53,6 +56,7 @@ export * from '../test/generated-wrappers/lib_token_spender_storage'; export * from '../test/generated-wrappers/lib_transform_erc20_rich_errors'; export * from '../test/generated-wrappers/lib_transform_erc20_storage'; export * from '../test/generated-wrappers/lib_wallet_rich_errors'; +export * from '../test/generated-wrappers/liquidity_provider_feature'; export * from '../test/generated-wrappers/log_metadata_transformer'; export * from '../test/generated-wrappers/meta_transactions_feature'; export * from '../test/generated-wrappers/mixin_adapter_addresses'; diff --git a/contracts/zero-ex/tsconfig.json b/contracts/zero-ex/tsconfig.json index c74120dd9d..de0a013816 100644 --- a/contracts/zero-ex/tsconfig.json +++ b/contracts/zero-ex/tsconfig.json @@ -45,6 +45,7 @@ "test/generated-artifacts/IFeature.json", "test/generated-artifacts/IFlashWallet.json", "test/generated-artifacts/IGasToken.json", + "test/generated-artifacts/ILiquidityProviderFeature.json", "test/generated-artifacts/IMetaTransactionsFeature.json", "test/generated-artifacts/IOwnableFeature.json", "test/generated-artifacts/ISignatureValidatorFeature.json", @@ -58,6 +59,8 @@ "test/generated-artifacts/LibBootstrap.json", "test/generated-artifacts/LibCommonRichErrors.json", "test/generated-artifacts/LibERC20Transformer.json", + "test/generated-artifacts/LibLiquidityProviderRichErrors.json", + "test/generated-artifacts/LibLiquidityProviderStorage.json", "test/generated-artifacts/LibMetaTransactionsRichErrors.json", "test/generated-artifacts/LibMetaTransactionsStorage.json", "test/generated-artifacts/LibMigrate.json", @@ -76,6 +79,7 @@ "test/generated-artifacts/LibTransformERC20RichErrors.json", "test/generated-artifacts/LibTransformERC20Storage.json", "test/generated-artifacts/LibWalletRichErrors.json", + "test/generated-artifacts/LiquidityProviderFeature.json", "test/generated-artifacts/LogMetadataTransformer.json", "test/generated-artifacts/MetaTransactionsFeature.json", "test/generated-artifacts/MixinAdapterAddresses.json", diff --git a/packages/contract-artifacts/artifacts/IZeroEx.json b/packages/contract-artifacts/artifacts/IZeroEx.json index 4255ce4384..96f2b33437 100644 --- a/packages/contract-artifacts/artifacts/IZeroEx.json +++ b/packages/contract-artifacts/artifacts/IZeroEx.json @@ -3,6 +3,16 @@ "contractName": "IZeroEx", "compilerOutput": { "abi": [ + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "address", "name": "xAsset", "type": "address" }, + { "indexed": true, "internalType": "address", "name": "yAsset", "type": "address" }, + { "indexed": false, "internalType": "address", "name": "providerAddress", "type": "address" } + ], + "name": "LiquidityProviderForMarketUpdated", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -222,6 +232,16 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { "internalType": "address", "name": "xAsset", "type": "address" }, + { "internalType": "address", "name": "yAsset", "type": "address" } + ], + "name": "getLiquidityProviderForMarket", + "outputs": [{ "internalType": "address", "name": "providerAddress", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -366,6 +386,19 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { "internalType": "address", "name": "makerToken", "type": "address" }, + { "internalType": "address", "name": "takerToken", "type": "address" }, + { "internalType": "address payable", "name": "recipient", "type": "address" }, + { "internalType": "uint256", "name": "sellAmount", "type": "uint256" }, + { "internalType": "uint256", "name": "minBuyAmount", "type": "uint256" } + ], + "name": "sellToLiquidityProvider", + "outputs": [{ "internalType": "uint256", "name": "boughtAmount", "type": "uint256" }], + "stateMutability": "payable", + "type": "function" + }, { "inputs": [ { "internalType": "contract IERC20TokenV06[]", "name": "tokens", "type": "address[]" }, @@ -378,6 +411,17 @@ "stateMutability": "payable", "type": "function" }, + { + "inputs": [ + { "internalType": "address", "name": "xAsset", "type": "address" }, + { "internalType": "address", "name": "yAsset", "type": "address" }, + { "internalType": "address", "name": "providerAddress", "type": "address" } + ], + "name": "setLiquidityProviderForMarket", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [{ "internalType": "address", "name": "quoteSigner", "type": "address" }], "name": "setQuoteSigner", @@ -492,6 +536,14 @@ "params": { "selector": "The function selector." }, "returns": { "impl": "The implementation contract address." } }, + "getLiquidityProviderForMarket(address,address)": { + "details": "Returns the address of the liquidity provider for a market given (xAsset, yAsset), or reverts if pool does not exist.", + "params": { + "xAsset": "First asset managed by the liquidity provider.", + "yAsset": "Second asset managed by the liquidity provider." + }, + "returns": { "providerAddress": "Address of the liquidity provider." } + }, "getMetaTransactionExecutedBlock((address,address,uint256,uint256,uint256,uint256,bytes,uint256,address,uint256))": { "details": "Get the block at which a meta-transaction has been executed.", "params": { "mtx": "The meta-transaction." }, @@ -574,6 +626,14 @@ }, "returns": { "buyAmount": "Amount of `tokens[-1]` bought." } }, + "setLiquidityProviderForMarket(address,address,address)": { + "details": "Sets address of the liquidity provider for a market given (xAsset, yAsset).", + "params": { + "providerAddress": "Address of the liquidity provider.", + "xAsset": "First asset managed by the liquidity provider.", + "yAsset": "Second asset managed by the liquidity provider." + } + }, "setQuoteSigner(address)": { "details": "Replace the optional signer for `transformERC20()` calldata. Only callable by the owner.", "params": { "quoteSigner": "The address of the new calldata signer." } diff --git a/packages/contract-wrappers/src/generated-wrappers/i_zero_ex.ts b/packages/contract-wrappers/src/generated-wrappers/i_zero_ex.ts index faf40e2b47..e0da11d70b 100644 --- a/packages/contract-wrappers/src/generated-wrappers/i_zero_ex.ts +++ b/packages/contract-wrappers/src/generated-wrappers/i_zero_ex.ts @@ -36,6 +36,7 @@ import * as ethers from 'ethers'; // tslint:enable:no-unused-variable export type IZeroExEventArgs = + | IZeroExLiquidityProviderForMarketUpdatedEventArgs | IZeroExMetaTransactionExecutedEventArgs | IZeroExMigratedEventArgs | IZeroExOwnershipTransferredEventArgs @@ -45,6 +46,7 @@ export type IZeroExEventArgs = | IZeroExTransformerDeployerUpdatedEventArgs; export enum IZeroExEvents { + LiquidityProviderForMarketUpdated = 'LiquidityProviderForMarketUpdated', MetaTransactionExecuted = 'MetaTransactionExecuted', Migrated = 'Migrated', OwnershipTransferred = 'OwnershipTransferred', @@ -54,6 +56,12 @@ export enum IZeroExEvents { TransformerDeployerUpdated = 'TransformerDeployerUpdated', } +export interface IZeroExLiquidityProviderForMarketUpdatedEventArgs extends DecodedLogArgs { + xAsset: string; + yAsset: string; + providerAddress: string; +} + export interface IZeroExMetaTransactionExecutedEventArgs extends DecodedLogArgs { hash: string; selector: string; @@ -211,6 +219,29 @@ export class IZeroExContract extends BaseContract { */ public static ABI(): ContractAbi { const abi = [ + { + anonymous: false, + inputs: [ + { + name: 'xAsset', + type: 'address', + indexed: true, + }, + { + name: 'yAsset', + type: 'address', + indexed: true, + }, + { + name: 'providerAddress', + type: 'address', + indexed: false, + }, + ], + name: 'LiquidityProviderForMarketUpdated', + outputs: [], + type: 'event', + }, { anonymous: false, inputs: [ @@ -697,6 +728,27 @@ export class IZeroExContract extends BaseContract { stateMutability: 'view', type: 'function', }, + { + inputs: [ + { + name: 'xAsset', + type: 'address', + }, + { + name: 'yAsset', + type: 'address', + }, + ], + name: 'getLiquidityProviderForMarket', + outputs: [ + { + name: 'providerAddress', + type: 'address', + }, + ], + stateMutability: 'view', + type: 'function', + }, { inputs: [ { @@ -1000,6 +1052,39 @@ export class IZeroExContract extends BaseContract { stateMutability: 'nonpayable', type: 'function', }, + { + inputs: [ + { + name: 'makerToken', + type: 'address', + }, + { + name: 'takerToken', + type: 'address', + }, + { + name: 'recipient', + type: 'address', + }, + { + name: 'sellAmount', + type: 'uint256', + }, + { + name: 'minBuyAmount', + type: 'uint256', + }, + ], + name: 'sellToLiquidityProvider', + outputs: [ + { + name: 'boughtAmount', + type: 'uint256', + }, + ], + stateMutability: 'payable', + type: 'function', + }, { inputs: [ { @@ -1029,6 +1114,26 @@ export class IZeroExContract extends BaseContract { stateMutability: 'payable', type: 'function', }, + { + inputs: [ + { + name: 'xAsset', + type: 'address', + }, + { + name: 'yAsset', + type: 'address', + }, + { + name: 'providerAddress', + type: 'address', + }, + ], + name: 'setLiquidityProviderForMarket', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, { inputs: [ { @@ -1743,6 +1848,60 @@ export class IZeroExContract extends BaseContract { }, }; } + /** + * Returns the address of the liquidity provider for a market given + * (xAsset, yAsset), or reverts if pool does not exist. + * @param xAsset First asset managed by the liquidity provider. + * @param yAsset Second asset managed by the liquidity provider. + */ + public getLiquidityProviderForMarket(xAsset: string, yAsset: string): ContractTxFunctionObj { + const self = (this as any) as IZeroExContract; + assert.isString('xAsset', xAsset); + assert.isString('yAsset', yAsset); + const functionSignature = 'getLiquidityProviderForMarket(address,address)'; + + return { + async sendTransactionAsync( + txData?: Partial | undefined, + opts: SendTransactionOpts = { shouldValidate: true }, + ): Promise { + const txDataWithDefaults = await self._applyDefaultsToTxDataAsync( + { data: this.getABIEncodedTransactionData(), ...txData }, + this.estimateGasAsync.bind(this), + ); + if (opts.shouldValidate !== false) { + await this.callAsync(txDataWithDefaults); + } + return self._web3Wrapper.sendTransactionAsync(txDataWithDefaults); + }, + awaitTransactionSuccessAsync( + txData?: Partial, + opts: AwaitTransactionSuccessOpts = { shouldValidate: true }, + ): PromiseWithTransactionHash { + return self._promiseWithTransactionHash(this.sendTransactionAsync(txData, opts), opts); + }, + async estimateGasAsync(txData?: Partial | undefined): Promise { + const txDataWithDefaults = await self._applyDefaultsToTxDataAsync({ + data: this.getABIEncodedTransactionData(), + ...txData, + }); + return self._web3Wrapper.estimateGasAsync(txDataWithDefaults); + }, + async callAsync(callData: Partial = {}, defaultBlock?: BlockParam): Promise { + BaseContract._assertCallParams(callData, defaultBlock); + const rawCallResult = await self._performCallAsync( + { data: this.getABIEncodedTransactionData(), ...callData }, + defaultBlock, + ); + const abiEncoder = self._lookupAbiEncoder(functionSignature); + BaseContract._throwIfUnexpectedEmptyCallResult(rawCallResult, abiEncoder); + return abiEncoder.strictDecodeReturnValue(rawCallResult); + }, + getABIEncodedTransactionData(): string { + return self._strictEncodeArguments(functionSignature, [xAsset.toLowerCase(), yAsset.toLowerCase()]); + }, + }; + } /** * Get the block at which a meta-transaction has been executed. * @param mtx The meta-transaction. @@ -2447,6 +2606,69 @@ export class IZeroExContract extends BaseContract { }, }; } + public sellToLiquidityProvider( + makerToken: string, + takerToken: string, + recipient: string, + sellAmount: BigNumber, + minBuyAmount: BigNumber, + ): ContractTxFunctionObj { + const self = (this as any) as IZeroExContract; + assert.isString('makerToken', makerToken); + assert.isString('takerToken', takerToken); + assert.isString('recipient', recipient); + assert.isBigNumber('sellAmount', sellAmount); + assert.isBigNumber('minBuyAmount', minBuyAmount); + const functionSignature = 'sellToLiquidityProvider(address,address,address,uint256,uint256)'; + + return { + async sendTransactionAsync( + txData?: Partial | undefined, + opts: SendTransactionOpts = { shouldValidate: true }, + ): Promise { + const txDataWithDefaults = await self._applyDefaultsToTxDataAsync( + { data: this.getABIEncodedTransactionData(), ...txData }, + this.estimateGasAsync.bind(this), + ); + if (opts.shouldValidate !== false) { + await this.callAsync(txDataWithDefaults); + } + return self._web3Wrapper.sendTransactionAsync(txDataWithDefaults); + }, + awaitTransactionSuccessAsync( + txData?: Partial, + opts: AwaitTransactionSuccessOpts = { shouldValidate: true }, + ): PromiseWithTransactionHash { + return self._promiseWithTransactionHash(this.sendTransactionAsync(txData, opts), opts); + }, + async estimateGasAsync(txData?: Partial | undefined): Promise { + const txDataWithDefaults = await self._applyDefaultsToTxDataAsync({ + data: this.getABIEncodedTransactionData(), + ...txData, + }); + return self._web3Wrapper.estimateGasAsync(txDataWithDefaults); + }, + async callAsync(callData: Partial = {}, defaultBlock?: BlockParam): Promise { + BaseContract._assertCallParams(callData, defaultBlock); + const rawCallResult = await self._performCallAsync( + { data: this.getABIEncodedTransactionData(), ...callData }, + defaultBlock, + ); + const abiEncoder = self._lookupAbiEncoder(functionSignature); + BaseContract._throwIfUnexpectedEmptyCallResult(rawCallResult, abiEncoder); + return abiEncoder.strictDecodeReturnValue(rawCallResult); + }, + getABIEncodedTransactionData(): string { + return self._strictEncodeArguments(functionSignature, [ + makerToken.toLowerCase(), + takerToken.toLowerCase(), + recipient.toLowerCase(), + sellAmount, + minBuyAmount, + ]); + }, + }; + } /** * Efficiently sell directly to uniswap/sushiswap. * @param tokens Sell path. @@ -2509,6 +2731,70 @@ export class IZeroExContract extends BaseContract { }, }; } + /** + * Sets address of the liquidity provider for a market given + * (xAsset, yAsset). + * @param xAsset First asset managed by the liquidity provider. + * @param yAsset Second asset managed by the liquidity provider. + * @param providerAddress Address of the liquidity provider. + */ + public setLiquidityProviderForMarket( + xAsset: string, + yAsset: string, + providerAddress: string, + ): ContractTxFunctionObj { + const self = (this as any) as IZeroExContract; + assert.isString('xAsset', xAsset); + assert.isString('yAsset', yAsset); + assert.isString('providerAddress', providerAddress); + const functionSignature = 'setLiquidityProviderForMarket(address,address,address)'; + + return { + async sendTransactionAsync( + txData?: Partial | undefined, + opts: SendTransactionOpts = { shouldValidate: true }, + ): Promise { + const txDataWithDefaults = await self._applyDefaultsToTxDataAsync( + { data: this.getABIEncodedTransactionData(), ...txData }, + this.estimateGasAsync.bind(this), + ); + if (opts.shouldValidate !== false) { + await this.callAsync(txDataWithDefaults); + } + return self._web3Wrapper.sendTransactionAsync(txDataWithDefaults); + }, + awaitTransactionSuccessAsync( + txData?: Partial, + opts: AwaitTransactionSuccessOpts = { shouldValidate: true }, + ): PromiseWithTransactionHash { + return self._promiseWithTransactionHash(this.sendTransactionAsync(txData, opts), opts); + }, + async estimateGasAsync(txData?: Partial | undefined): Promise { + const txDataWithDefaults = await self._applyDefaultsToTxDataAsync({ + data: this.getABIEncodedTransactionData(), + ...txData, + }); + return self._web3Wrapper.estimateGasAsync(txDataWithDefaults); + }, + async callAsync(callData: Partial = {}, defaultBlock?: BlockParam): Promise { + BaseContract._assertCallParams(callData, defaultBlock); + const rawCallResult = await self._performCallAsync( + { data: this.getABIEncodedTransactionData(), ...callData }, + defaultBlock, + ); + const abiEncoder = self._lookupAbiEncoder(functionSignature); + BaseContract._throwIfUnexpectedEmptyCallResult(rawCallResult, abiEncoder); + return abiEncoder.strictDecodeReturnValue(rawCallResult); + }, + getABIEncodedTransactionData(): string { + return self._strictEncodeArguments(functionSignature, [ + xAsset.toLowerCase(), + yAsset.toLowerCase(), + providerAddress.toLowerCase(), + ]); + }, + }; + } /** * Replace the optional signer for `transformERC20()` calldata. * Only callable by the owner. From f089f5d87f3be0e09274de415d625592966a27f3 Mon Sep 17 00:00:00 2001 From: Michael Zhu Date: Wed, 9 Sep 2020 12:57:45 -0700 Subject: [PATCH 12/32] Add migrate function to LiquidityProviderFeature --- .../src/features/LiquidityProviderFeature.sol | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/contracts/zero-ex/contracts/src/features/LiquidityProviderFeature.sol b/contracts/zero-ex/contracts/src/features/LiquidityProviderFeature.sol index d152394591..4c7cda1b4b 100644 --- a/contracts/zero-ex/contracts/src/features/LiquidityProviderFeature.sol +++ b/contracts/zero-ex/contracts/src/features/LiquidityProviderFeature.sol @@ -26,6 +26,7 @@ import "@0x/contracts-utils/contracts/src/v06/errors/LibRichErrorsV06.sol"; import "@0x/contracts-utils/contracts/src/v06/LibSafeMathV06.sol"; import "../errors/LibLiquidityProviderRichErrors.sol"; import "../fixins/FixinCommon.sol"; +import "../migrations/LibMigrate.sol"; import "../storage/LibLiquidityProviderStorage.sol"; import "./IFeature.sol"; import "./ILiquidityProviderFeature.sol"; @@ -79,6 +80,19 @@ contract LiquidityProviderFeature is weth = weth_; } + /// @dev Initialize and register this feature. + /// Should be delegatecalled by `Migrate.migrate()`. + /// @return success `LibMigrate.SUCCESS` on success. + function migrate() + external + returns (bytes4 success) + { + _registerFeatureFunction(this.sellToLiquidityProvider.selector); + _registerFeatureFunction(this.setLiquidityProviderForMarket.selector); + _registerFeatureFunction(this.getLiquidityProviderForMarket.selector); + return LibMigrate.MIGRATE_SUCCESS; + } + function sellToLiquidityProvider( address makerToken, address takerToken, From c6d738ed0c7363a1074bab58ece4bdbdc0ac7932 Mon Sep 17 00:00:00 2001 From: Michael Zhu Date: Mon, 7 Sep 2020 20:56:05 -0700 Subject: [PATCH 13/32] Refactor asset-swapper --- contracts/zero-ex/package.json | 2 +- contracts/zero-ex/src/artifacts.ts | 2 + contracts/zero-ex/src/wrappers.ts | 1 + contracts/zero-ex/tsconfig.json | 1 + packages/asset-swapper/src/index.ts | 2 +- .../utils/market_operation_utils/constants.ts | 6 + .../src/utils/market_operation_utils/fills.ts | 338 +++++------------- .../src/utils/market_operation_utils/index.ts | 72 ++-- .../market_operation_utils/multihop_utils.ts | 2 +- .../utils/market_operation_utils/orders.ts | 41 +-- .../src/utils/market_operation_utils/path.ts | 287 +++++++++++++++ .../market_operation_utils/path_optimizer.ts | 139 +++---- .../market_operation_utils/rate_utils.ts | 48 +++ .../src/utils/market_operation_utils/types.ts | 21 +- .../src/utils/quote_report_generator.ts | 6 +- .../src/utils/quote_simulation.ts | 2 +- .../src/utils/swap_quote_calculator.ts | 115 +++--- .../test/market_operation_utils_test.ts | 12 +- 18 files changed, 597 insertions(+), 500 deletions(-) create mode 100644 packages/asset-swapper/src/utils/market_operation_utils/path.ts create mode 100644 packages/asset-swapper/src/utils/market_operation_utils/rate_utils.ts diff --git a/contracts/zero-ex/package.json b/contracts/zero-ex/package.json index ee3cac9677..c4f9bbc6c7 100644 --- a/contracts/zero-ex/package.json +++ b/contracts/zero-ex/package.json @@ -39,7 +39,7 @@ "publish:private": "yarn build && gitpkg publish" }, "config": { - "publicInterfaceContracts": "IZeroEx,ZeroEx,FullMigration,InitialMigration,IFlashWallet,IAllowanceTarget,IERC20Transformer,IOwnableFeature,ISimpleFunctionRegistryFeature,ITokenSpenderFeature,ITransformERC20Feature,FillQuoteTransformer,PayTakerTransformer,WethTransformer,OwnableFeature,SimpleFunctionRegistryFeature,TransformERC20Feature,TokenSpenderFeature,AffiliateFeeTransformer,SignatureValidatorFeature,MetaTransactionsFeature,LogMetadataTransformer,BridgeAdapter", + "publicInterfaceContracts": "IZeroEx,ZeroEx,FullMigration,InitialMigration,IFlashWallet,IAllowanceTarget,IERC20Transformer,IOwnableFeature,ISimpleFunctionRegistryFeature,ITokenSpenderFeature,ITransformERC20Feature,FillQuoteTransformer,PayTakerTransformer,WethTransformer,OwnableFeature,SimpleFunctionRegistryFeature,TransformERC20Feature,TokenSpenderFeature,AffiliateFeeTransformer,SignatureValidatorFeature,MetaTransactionsFeature,LogMetadataTransformer,BridgeAdapter,LiquidityProviderFeature", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.", "abis": "./test/generated-artifacts/@(AffiliateFeeTransformer|AllowanceTarget|BootstrapFeature|BridgeAdapter|FillQuoteTransformer|FixinCommon|FixinEIP712|FixinReentrancyGuard|FlashWallet|FullMigration|IAllowanceTarget|IBootstrapFeature|IBridgeAdapter|IERC20Bridge|IERC20Transformer|IExchange|IFeature|IFlashWallet|IGasToken|ILiquidityProviderFeature|IMetaTransactionsFeature|IOwnableFeature|ISignatureValidatorFeature|ISimpleFunctionRegistryFeature|ITestSimpleFunctionRegistryFeature|ITokenSpenderFeature|ITransformERC20Feature|IUniswapFeature|IZeroEx|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibLiquidityProviderRichErrors|LibLiquidityProviderStorage|LibMetaTransactionsRichErrors|LibMetaTransactionsStorage|LibMigrate|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibReentrancyGuardStorage|LibSignatureRichErrors|LibSignedCallData|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibSpenderRichErrors|LibStorage|LibTokenSpenderStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|LiquidityProviderFeature|LogMetadataTransformer|MetaTransactionsFeature|MixinAdapterAddresses|MixinBalancer|MixinCurve|MixinKyber|MixinMStable|MixinMooniswap|MixinOasis|MixinUniswap|MixinUniswapV2|MixinZeroExBridge|OwnableFeature|PayTakerTransformer|SignatureValidatorFeature|SimpleFunctionRegistryFeature|TestCallTarget|TestDelegateCaller|TestFillQuoteTransformerBridge|TestFillQuoteTransformerExchange|TestFillQuoteTransformerHost|TestFullMigration|TestInitialMigration|TestMetaTransactionsTransformERC20Feature|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestTokenSpender|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerBase|TestTransformerDeployerTransformer|TestTransformerHost|TestWeth|TestWethTransformerHost|TestZeroExFeature|TokenSpenderFeature|TransformERC20Feature|Transformer|TransformerDeployer|UniswapFeature|WethTransformer|ZeroEx).json" }, diff --git a/contracts/zero-ex/src/artifacts.ts b/contracts/zero-ex/src/artifacts.ts index b51add69e6..9e41b7606f 100644 --- a/contracts/zero-ex/src/artifacts.ts +++ b/contracts/zero-ex/src/artifacts.ts @@ -18,6 +18,7 @@ import * as ISimpleFunctionRegistryFeature from '../generated-artifacts/ISimpleF import * as ITokenSpenderFeature from '../generated-artifacts/ITokenSpenderFeature.json'; import * as ITransformERC20Feature from '../generated-artifacts/ITransformERC20Feature.json'; import * as IZeroEx from '../generated-artifacts/IZeroEx.json'; +import * as LiquidityProviderFeature from '../generated-artifacts/LiquidityProviderFeature.json'; import * as LogMetadataTransformer from '../generated-artifacts/LogMetadataTransformer.json'; import * as MetaTransactionsFeature from '../generated-artifacts/MetaTransactionsFeature.json'; import * as OwnableFeature from '../generated-artifacts/OwnableFeature.json'; @@ -52,4 +53,5 @@ export const artifacts = { MetaTransactionsFeature: MetaTransactionsFeature as ContractArtifact, LogMetadataTransformer: LogMetadataTransformer as ContractArtifact, BridgeAdapter: BridgeAdapter as ContractArtifact, + LiquidityProviderFeature: LiquidityProviderFeature as ContractArtifact, }; diff --git a/contracts/zero-ex/src/wrappers.ts b/contracts/zero-ex/src/wrappers.ts index ea09d3416d..0c88b3fa82 100644 --- a/contracts/zero-ex/src/wrappers.ts +++ b/contracts/zero-ex/src/wrappers.ts @@ -16,6 +16,7 @@ export * from '../generated-wrappers/i_token_spender_feature'; export * from '../generated-wrappers/i_transform_erc20_feature'; export * from '../generated-wrappers/i_zero_ex'; export * from '../generated-wrappers/initial_migration'; +export * from '../generated-wrappers/liquidity_provider_feature'; export * from '../generated-wrappers/log_metadata_transformer'; export * from '../generated-wrappers/meta_transactions_feature'; export * from '../generated-wrappers/ownable_feature'; diff --git a/contracts/zero-ex/tsconfig.json b/contracts/zero-ex/tsconfig.json index de0a013816..0f0163243e 100644 --- a/contracts/zero-ex/tsconfig.json +++ b/contracts/zero-ex/tsconfig.json @@ -16,6 +16,7 @@ "generated-artifacts/ITransformERC20Feature.json", "generated-artifacts/IZeroEx.json", "generated-artifacts/InitialMigration.json", + "generated-artifacts/LiquidityProviderFeature.json", "generated-artifacts/LogMetadataTransformer.json", "generated-artifacts/MetaTransactionsFeature.json", "generated-artifacts/OwnableFeature.json", diff --git a/packages/asset-swapper/src/index.ts b/packages/asset-swapper/src/index.ts index f9029e256b..79e86a3f75 100644 --- a/packages/asset-swapper/src/index.ts +++ b/packages/asset-swapper/src/index.ts @@ -119,6 +119,7 @@ export { SwapQuoterRfqtOpts, } from './types'; export { affiliateFeeUtils } from './utils/affiliate_fee_utils'; +export { SOURCE_FLAGS } from './utils/market_operation_utils/constants'; export { Parameters, SamplerContractCall, @@ -136,7 +137,6 @@ export { FeeSchedule, Fill, FillData, - FillFlags, GetMarketOrdersRfqtOpts, KyberFillData, LiquidityProviderFillData, diff --git a/packages/asset-swapper/src/utils/market_operation_utils/constants.ts b/packages/asset-swapper/src/utils/market_operation_utils/constants.ts index 1b65b5538a..8b4c7dd2e5 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/constants.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/constants.ts @@ -58,6 +58,7 @@ export const DEFAULT_GET_MARKET_ORDERS_OPTS: GetMarketOrdersOpts = { sampleDistributionBase: 1.05, feeSchedule: {}, gasSchedule: {}, + exchangeProxyOverhead: () => ZERO_AMOUNT, allowFallback: true, shouldBatchBridgeOrders: true, shouldGenerateQuoteReport: false, @@ -68,6 +69,11 @@ export const DEFAULT_GET_MARKET_ORDERS_OPTS: GetMarketOrdersOpts = { */ export const FEE_QUOTE_SOURCES = [ERC20BridgeSource.Uniswap, ERC20BridgeSource.UniswapV2]; +export const SOURCE_FLAGS: { [source in ERC20BridgeSource]: number } = Object.assign( + {}, + ...Object.values(ERC20BridgeSource).map((source: ERC20BridgeSource, index) => ({ [source]: 1 << index })), +); + /** * Mainnet Curve configuration */ diff --git a/packages/asset-swapper/src/utils/market_operation_utils/fills.ts b/packages/asset-swapper/src/utils/market_operation_utils/fills.ts index 7ae13d60af..24ce21bbec 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/fills.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/fills.ts @@ -3,15 +3,15 @@ import { BigNumber, hexUtils } from '@0x/utils'; import { MarketOperation, SignedOrderWithFillableAmounts } from '../../types'; import { fillableAmountsUtils } from '../../utils/fillable_amounts_utils'; -import { POSITIVE_INF, ZERO_AMOUNT } from './constants'; -import { CollapsedFill, DexSample, ERC20BridgeSource, FeeSchedule, Fill, FillFlags, MultiHopFillData } from './types'; +import { POSITIVE_INF, SOURCE_FLAGS, ZERO_AMOUNT } from './constants'; +import { DexSample, ERC20BridgeSource, FeeSchedule, Fill } from './types'; // tslint:disable: prefer-for-of no-bitwise completed-docs /** - * Create fill paths from orders and dex quotes. + * Create `Fill` objects from orders and dex quotes. */ -export function createFillPaths(opts: { +export function createFills(opts: { side: MarketOperation; orders?: SignedOrderWithFillableAmounts[]; dexQuotes?: DexSample[][]; @@ -28,30 +28,50 @@ export function createFillPaths(opts: { const dexQuotes = opts.dexQuotes || []; const ethToOutputRate = opts.ethToOutputRate || ZERO_AMOUNT; const ethToInputRate = opts.ethToInputRate || ZERO_AMOUNT; - // Create native fill paths. - const nativePath = nativeOrdersToPath(side, orders, opts.targetInput, ethToOutputRate, ethToInputRate, feeSchedule); - // Create DEX fill paths. - const dexPaths = dexQuotesToPaths(side, dexQuotes, ethToOutputRate, feeSchedule); - return filterPaths([...dexPaths, nativePath].map(p => clipPathToInput(p, opts.targetInput)), excludedSources); + // Create native fills. + const nativeFills = nativeOrdersToFills( + side, + orders, + opts.targetInput, + ethToOutputRate, + ethToInputRate, + feeSchedule, + ); + // Create DEX fills. + const dexFills = dexQuotes.map(singleSourceSamples => + dexSamplesToFills(side, singleSourceSamples, ethToOutputRate, ethToInputRate, feeSchedule), + ); + return [...dexFills, nativeFills] + .map(p => clipFillsToInput(p, opts.targetInput)) + .filter(fills => hasLiquidity(fills) && !excludedSources.includes(fills[0].source)); } -function filterPaths(paths: Fill[][], excludedSources: ERC20BridgeSource[]): Fill[][] { - return paths.filter(path => { - if (path.length === 0) { - return false; +function clipFillsToInput(fills: Fill[], targetInput: BigNumber = POSITIVE_INF): Fill[] { + const clipped: Fill[] = []; + let input = ZERO_AMOUNT; + for (const fill of fills) { + if (input.gte(targetInput)) { + break; } - const [input, output] = getPathSize(path); - if (input.eq(0) || output.eq(0)) { - return false; - } - if (excludedSources.includes(path[0].source)) { - return false; - } - return true; - }); + input = input.plus(fill.input); + clipped.push(fill); + } + return clipped; } -function nativeOrdersToPath( +function hasLiquidity(fills: Fill[]): boolean { + if (fills.length === 0) { + return false; + } + const totalInput = BigNumber.sum(...fills.map(fill => fill.input)); + const totalOutput = BigNumber.sum(...fills.map(fill => fill.output)); + if (totalInput.isZero() || totalOutput.isZero()) { + return false; + } + return true; +} + +function nativeOrdersToFills( side: MarketOperation, orders: SignedOrderWithFillableAmounts[], targetInput: BigNumber = POSITIVE_INF, @@ -61,7 +81,7 @@ function nativeOrdersToPath( ): Fill[] { const sourcePathId = hexUtils.random(); // Create a single path from all orders. - let path: Array = []; + let fills: Array = []; for (const order of orders) { const makerAmount = fillableAmountsUtils.getMakerAssetAmountSwappedAfterOrderFees(order); const takerAmount = fillableAmountsUtils.getTakerAssetAmountSwappedAfterOrderFees(order); @@ -87,13 +107,13 @@ function nativeOrdersToPath( if (adjustedRate.lte(0)) { continue; } - path.push({ + fills.push({ sourcePathId, adjustedRate, adjustedOutput, input: clippedInput, output: clippedOutput, - flags: 0, + flags: SOURCE_FLAGS[ERC20BridgeSource.Native], index: 0, // TBD parent: undefined, // TBD source: ERC20BridgeSource.Native, @@ -101,240 +121,56 @@ function nativeOrdersToPath( }); } // Sort by descending adjusted rate. - path = path.sort((a, b) => b.adjustedRate.comparedTo(a.adjustedRate)); + fills = fills.sort((a, b) => b.adjustedRate.comparedTo(a.adjustedRate)); // Re-index fills. - for (let i = 0; i < path.length; ++i) { - path[i].parent = i === 0 ? undefined : path[i - 1]; - path[i].index = i; + for (let i = 0; i < fills.length; ++i) { + fills[i].parent = i === 0 ? undefined : fills[i - 1]; + fills[i].index = i; } - return path; + return fills; } -function dexQuotesToPaths( +function dexSamplesToFills( side: MarketOperation, - dexQuotes: DexSample[][], + samples: DexSample[], ethToOutputRate: BigNumber, + ethToInputRate: BigNumber, fees: FeeSchedule, -): Fill[][] { - const paths: Fill[][] = []; - for (let quote of dexQuotes) { - const sourcePathId = hexUtils.random(); - const path: Fill[] = []; - // Drop any non-zero entries. This can occur if the any fills on Kyber were UniswapReserves - // We need not worry about Kyber fills going to UniswapReserve as the input amount - // we fill is the same as we sampled. I.e we received [0,20,30] output from [1,2,3] input - // and we only fill [2,3] on Kyber (as 1 returns 0 output) - quote = quote.filter(q => !q.output.isZero()); - for (let i = 0; i < quote.length; i++) { - const sample = quote[i]; - const prevSample = i === 0 ? undefined : quote[i - 1]; - const { source, fillData } = sample; - const input = sample.input.minus(prevSample ? prevSample.input : 0); - const output = sample.output.minus(prevSample ? prevSample.output : 0); - const fee = fees[source] === undefined ? 0 : fees[source]!(sample.fillData); - const penalty = - i === 0 // Only the first fill in a DEX path incurs a penalty. - ? ethToOutputRate.times(fee) - : ZERO_AMOUNT; - const adjustedOutput = side === MarketOperation.Sell ? output.minus(penalty) : output.plus(penalty); - - path.push({ - sourcePathId, - input, - output, - adjustedOutput, - source, - fillData, - index: i, - parent: i !== 0 ? path[path.length - 1] : undefined, - flags: sourceToFillFlags(source), - }); +): Fill[] { + const sourcePathId = hexUtils.random(); + const fills: Fill[] = []; + // Drop any non-zero entries. This can occur if the any fills on Kyber were UniswapReserves + // We need not worry about Kyber fills going to UniswapReserve as the input amount + // we fill is the same as we sampled. I.e we received [0,20,30] output from [1,2,3] input + // and we only fill [2,3] on Kyber (as 1 returns 0 output) + samples = samples.filter(q => !q.output.isZero()); + for (let i = 0; i < samples.length; i++) { + const sample = samples[i]; + const prevSample = i === 0 ? undefined : samples[i - 1]; + const { source, fillData } = sample; + const input = sample.input.minus(prevSample ? prevSample.input : 0); + const output = sample.output.minus(prevSample ? prevSample.output : 0); + const fee = fees[source] === undefined ? 0 : fees[source]!(sample.fillData); + let penalty = ZERO_AMOUNT; + if (i === 0) { + // Only the first fill in a DEX path incurs a penalty. + penalty = !ethToOutputRate.isZero() + ? ethToOutputRate.times(fee) + : ethToInputRate.times(fee).times(output.dividedToIntegerBy(input)); } - paths.push(path); - } - return paths; -} + const adjustedOutput = side === MarketOperation.Sell ? output.minus(penalty) : output.plus(penalty); -export function getTwoHopAdjustedRate( - side: MarketOperation, - twoHopQuote: DexSample, - targetInput: BigNumber, - ethToOutputRate: BigNumber, - fees: FeeSchedule = {}, -): BigNumber { - const { output, input, fillData } = twoHopQuote; - if (input.isLessThan(targetInput) || output.isZero()) { - return ZERO_AMOUNT; - } - const penalty = ethToOutputRate.times(fees[ERC20BridgeSource.MultiHop]!(fillData)); - const adjustedOutput = side === MarketOperation.Sell ? output.minus(penalty) : output.plus(penalty); - return side === MarketOperation.Sell ? adjustedOutput.div(input) : input.div(adjustedOutput); -} - -function sourceToFillFlags(source: ERC20BridgeSource): number { - switch (source) { - case ERC20BridgeSource.Uniswap: - return FillFlags.ConflictsWithMultiBridge; - case ERC20BridgeSource.LiquidityProvider: - return FillFlags.ConflictsWithMultiBridge; - case ERC20BridgeSource.MultiBridge: - return FillFlags.MultiBridge; - default: - return 0; - } -} - -export function getPathSize(path: Fill[], targetInput: BigNumber = POSITIVE_INF): [BigNumber, BigNumber] { - let input = ZERO_AMOUNT; - let output = ZERO_AMOUNT; - for (const fill of path) { - if (input.plus(fill.input).gte(targetInput)) { - const di = targetInput.minus(input); - input = input.plus(di); - output = output.plus(fill.output.times(di.div(fill.input))); - break; - } else { - input = input.plus(fill.input); - output = output.plus(fill.output); - } - } - return [input.integerValue(), output.integerValue()]; -} - -export function getPathAdjustedSize(path: Fill[], targetInput: BigNumber = POSITIVE_INF): [BigNumber, BigNumber] { - let input = ZERO_AMOUNT; - let output = ZERO_AMOUNT; - for (const fill of path) { - if (input.plus(fill.input).gte(targetInput)) { - const di = targetInput.minus(input); - if (di.gt(0)) { - input = input.plus(di); - // Penalty does not get interpolated. - const penalty = fill.adjustedOutput.minus(fill.output); - output = output.plus(fill.output.times(di.div(fill.input)).plus(penalty)); - } - break; - } else { - input = input.plus(fill.input); - output = output.plus(fill.adjustedOutput); - } - } - return [input.integerValue(), output.integerValue()]; -} - -export function isValidPath(path: Fill[], skipDuplicateCheck: boolean = false): boolean { - let flags = 0; - for (let i = 0; i < path.length; ++i) { - // Fill must immediately follow its parent. - if (path[i].parent) { - if (i === 0 || path[i - 1] !== path[i].parent) { - return false; - } - } - if (!skipDuplicateCheck) { - // Fill must not be duplicated. - for (let j = 0; j < i; ++j) { - if (path[i] === path[j]) { - return false; - } - } - } - flags |= path[i].flags; - } - return arePathFlagsAllowed(flags); -} - -export function arePathFlagsAllowed(flags: number): boolean { - const multiBridgeConflict = FillFlags.MultiBridge | FillFlags.ConflictsWithMultiBridge; - return (flags & multiBridgeConflict) !== multiBridgeConflict; -} - -export function clipPathToInput(path: Fill[], targetInput: BigNumber = POSITIVE_INF): Fill[] { - const clipped: Fill[] = []; - let input = ZERO_AMOUNT; - for (const fill of path) { - if (input.gte(targetInput)) { - break; - } - input = input.plus(fill.input); - clipped.push(fill); - } - return clipped; -} - -export function collapsePath(path: Fill[]): CollapsedFill[] { - const collapsed: CollapsedFill[] = []; - for (const fill of path) { - const source = fill.source; - if (collapsed.length !== 0 && source !== ERC20BridgeSource.Native) { - const prevFill = collapsed[collapsed.length - 1]; - // If the last fill is from the same source, merge them. - if (prevFill.sourcePathId === fill.sourcePathId) { - prevFill.input = prevFill.input.plus(fill.input); - prevFill.output = prevFill.output.plus(fill.output); - prevFill.fillData = fill.fillData; - prevFill.subFills.push(fill); - continue; - } - } - collapsed.push({ - sourcePathId: fill.sourcePathId, - source: fill.source, - fillData: fill.fillData, - input: fill.input, - output: fill.output, - subFills: [fill], + fills.push({ + sourcePathId, + input, + output, + adjustedOutput, + source, + fillData, + index: i, + parent: i !== 0 ? fills[fills.length - 1] : undefined, + flags: SOURCE_FLAGS[source], }); } - return collapsed; -} - -export function getPathAdjustedCompleteRate(side: MarketOperation, path: Fill[], targetInput: BigNumber): BigNumber { - const [input, output] = getPathAdjustedSize(path, targetInput); - return getCompleteRate(side, input, output, targetInput); -} - -export function getPathAdjustedRate(side: MarketOperation, path: Fill[], targetInput: BigNumber): BigNumber { - const [input, output] = getPathAdjustedSize(path, targetInput); - return getRate(side, input, output); -} - -export function getPathAdjustedSlippage( - side: MarketOperation, - path: Fill[], - inputAmount: BigNumber, - maxRate: BigNumber, -): number { - if (maxRate.eq(0)) { - return 0; - } - const totalRate = getPathAdjustedRate(side, path, inputAmount); - const rateChange = maxRate.minus(totalRate); - return rateChange.div(maxRate).toNumber(); -} - -export function getCompleteRate( - side: MarketOperation, - input: BigNumber, - output: BigNumber, - targetInput: BigNumber, -): BigNumber { - if (input.eq(0) || output.eq(0) || targetInput.eq(0)) { - return ZERO_AMOUNT; - } - // Penalize paths that fall short of the entire input amount by a factor of - // input / targetInput => (i / t) - if (side === MarketOperation.Sell) { - // (o / i) * (i / t) => (o / t) - return output.div(targetInput); - } - // (i / o) * (i / t) - return input.div(output).times(input.div(targetInput)); -} - -export function getRate(side: MarketOperation, input: BigNumber, output: BigNumber): BigNumber { - if (input.eq(0) || output.eq(0)) { - return ZERO_AMOUNT; - } - return side === MarketOperation.Sell ? output.div(input) : input.div(output); + return fills; } diff --git a/packages/asset-swapper/src/utils/market_operation_utils/index.ts b/packages/asset-swapper/src/utils/market_operation_utils/index.ts index dea88d3537..998099b140 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/index.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/index.ts @@ -14,12 +14,12 @@ import { FEE_QUOTE_SOURCES, ONE_ETHER, SELL_SOURCE_FILTER, + SOURCE_FLAGS, ZERO_AMOUNT, } from './constants'; -import { createFillPaths, getPathAdjustedRate, getPathAdjustedSlippage } from './fills'; +import { createFills } from './fills'; import { getBestTwoHopQuote } from './multihop_utils'; import { - createOrdersFromPath, createOrdersFromTwoHopSample, createSignedOrdersFromRfqtIndicativeQuotes, createSignedOrdersWithFillableAmounts, @@ -32,12 +32,12 @@ import { AggregationError, DexSample, ERC20BridgeSource, + ExchangeProxyOverhead, FeeSchedule, GetMarketOrdersOpts, MarketSideLiquidity, OptimizedMarketOrder, OptimizerResult, - OptimizerResultWithReport, OrderDomain, TokenAdjacencyGraph, } from './types'; @@ -359,7 +359,7 @@ export class MarketOperationUtils { nativeOrders: SignedOrder[], takerAmount: BigNumber, opts?: Partial, - ): Promise { + ): Promise { const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts }; const marketSideLiquidity = await this.getMarketSellLiquidityAsync(nativeOrders, takerAmount, _opts); const optimizerResult = await this._generateOptimizedOrdersAsync(marketSideLiquidity, { @@ -396,7 +396,7 @@ export class MarketOperationUtils { nativeOrders: SignedOrder[], makerAmount: BigNumber, opts?: Partial, - ): Promise { + ): Promise { const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts }; const marketSideLiquidity = await this.getMarketBuyLiquidityAsync(nativeOrders, makerAmount, _opts); const optimizerResult = await this._generateOptimizedOrdersAsync(marketSideLiquidity, { @@ -526,6 +526,7 @@ export class MarketOperationUtils { maxFallbackSlippage?: number; excludedSources?: ERC20BridgeSource[]; feeSchedule?: FeeSchedule; + exchangeProxyOverhead?: ExchangeProxyOverhead; allowFallback?: boolean; shouldBatchBridgeOrders?: boolean; }, @@ -554,8 +555,8 @@ export class MarketOperationUtils { shouldBatchBridgeOrders: !!opts.shouldBatchBridgeOrders, }; - // Convert native orders and dex quotes into fill paths. - const paths = createFillPaths({ + // Convert native orders and dex quotes into `Fill` objects. + const fills = createFills({ side, // Augment native orders with their fillable amounts. orders: [ @@ -571,11 +572,16 @@ export class MarketOperationUtils { }); // Find the optimal path. - let optimalPath = (await findOptimalPathAsync(side, paths, inputAmount, opts.runLimit)) || []; - if (optimalPath.length === 0) { + const optimizerOpts = { + ethToOutputRate, + ethToInputRate, + exchangeProxyOverhead: opts.exchangeProxyOverhead || (() => ZERO_AMOUNT), + }; + let optimalPath = await findOptimalPathAsync(side, fills, inputAmount, opts.runLimit, optimizerOpts); + if (optimalPath === undefined) { throw new Error(AggregationError.NoOptimalPath); } - const optimalPathRate = getPathAdjustedRate(side, optimalPath, inputAmount); + const optimalPathRate = optimalPath.adjustedRate(); const { adjustedRate: bestTwoHopRate, quote: bestTwoHopQuote } = getBestTwoHopQuote( marketSideLiquidity, @@ -583,39 +589,35 @@ export class MarketOperationUtils { ); if (bestTwoHopQuote && bestTwoHopRate.isGreaterThan(optimalPathRate)) { const twoHopOrders = createOrdersFromTwoHopSample(bestTwoHopQuote, orderOpts); - return { optimizedOrders: twoHopOrders, liquidityDelivered: bestTwoHopQuote, isTwoHop: true }; + return { + optimizedOrders: twoHopOrders, + liquidityDelivered: bestTwoHopQuote, + sourceFlags: SOURCE_FLAGS[ERC20BridgeSource.MultiHop], + }; } // Generate a fallback path if native orders are in the optimal path. - const nativeSubPath = optimalPath.filter(f => f.source === ERC20BridgeSource.Native); - if (opts.allowFallback && nativeSubPath.length !== 0) { + const nativeFills = optimalPath.fills.filter(f => f.source === ERC20BridgeSource.Native); + if (opts.allowFallback && nativeFills.length !== 0) { // We create a fallback path that is exclusive of Native liquidity // This is the optimal on-chain path for the entire input amount - const nonNativePaths = paths.filter(p => p.length > 0 && p[0].source !== ERC20BridgeSource.Native); - const nonNativeOptimalPath = - (await findOptimalPathAsync(side, nonNativePaths, inputAmount, opts.runLimit)) || []; + const nonNativeFills = fills.filter(p => p.length > 0 && p[0].source !== ERC20BridgeSource.Native); + const nonNativeOptimalPath = await findOptimalPathAsync(side, nonNativeFills, inputAmount, opts.runLimit); // Calculate the slippage of on-chain sources compared to the most optimal path - const fallbackSlippage = getPathAdjustedSlippage(side, nonNativeOptimalPath, inputAmount, optimalPathRate); - if (nativeSubPath.length === optimalPath.length || fallbackSlippage <= maxFallbackSlippage) { - // If the last fill is Native and penultimate is not, then the intention was to partial fill - // In this case we drop it entirely as we can't handle a failure at the end and we don't - // want to fully fill when it gets prepended to the front below - const [last, penultimateIfExists] = optimalPath.slice().reverse(); - const lastNativeFillIfExists = - last.source === ERC20BridgeSource.Native && - penultimateIfExists && - penultimateIfExists.source !== ERC20BridgeSource.Native - ? last - : undefined; - // By prepending native paths to the front they cannot split on-chain sources and incur - // an additional protocol fee. I.e [Uniswap,Native,Kyber] becomes [Native,Uniswap,Kyber] - // In the previous step we dropped any hanging Native partial fills, as to not fully fill - optimalPath = [...nativeSubPath.filter(f => f !== lastNativeFillIfExists), ...nonNativeOptimalPath]; + if ( + nonNativeOptimalPath !== undefined && + (nativeFills.length === optimalPath.fills.length || + nonNativeOptimalPath.adjustedSlippage(optimalPathRate) <= maxFallbackSlippage) + ) { + optimalPath.addFallback(nonNativeOptimalPath); } } - const optimizedOrders = createOrdersFromPath(optimalPath, orderOpts); - const liquidityDelivered = _.flatten(optimizedOrders.map(order => order.fills)); - return { optimizedOrders, liquidityDelivered, isTwoHop: false }; + const collapsedPath = optimalPath.collapse(orderOpts); + return { + optimizedOrders: collapsedPath.orders, + liquidityDelivered: collapsedPath.collapsedFills, + sourceFlags: collapsedPath.sourceFlags, + } as OptimizerResult; } } diff --git a/packages/asset-swapper/src/utils/market_operation_utils/multihop_utils.ts b/packages/asset-swapper/src/utils/market_operation_utils/multihop_utils.ts index 28c5043d62..146f0b7826 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/multihop_utils.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/multihop_utils.ts @@ -2,7 +2,7 @@ import { BigNumber } from '@0x/utils'; import * as _ from 'lodash'; import { ZERO_AMOUNT } from './constants'; -import { getTwoHopAdjustedRate } from './fills'; +import { getTwoHopAdjustedRate } from './rate_utils'; import { DexSample, FeeSchedule, MarketSideLiquidity, MultiHopFillData, TokenAdjacencyGraph } from './types'; /** diff --git a/packages/asset-swapper/src/utils/market_operation_utils/orders.ts b/packages/asset-swapper/src/utils/market_operation_utils/orders.ts index efcb699a7a..67cfbc118e 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/orders.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/orders.ts @@ -16,7 +16,6 @@ import { WALLET_SIGNATURE, ZERO_AMOUNT, } from './constants'; -import { collapsePath } from './fills'; import { getMultiBridgeIntermediateToken } from './multibridge_utils'; import { AggregationError, @@ -26,7 +25,6 @@ import { CurveFillData, DexSample, ERC20BridgeSource, - Fill, KyberFillData, LiquidityProviderFillData, MooniswapFillData, @@ -155,37 +153,6 @@ export interface CreateOrderFromPathOpts { shouldBatchBridgeOrders: boolean; } -// Convert sell fills into orders. -export function createOrdersFromPath(path: Fill[], opts: CreateOrderFromPathOpts): OptimizedMarketOrder[] { - const [makerToken, takerToken] = getMakerTakerTokens(opts); - const collapsedPath = collapsePath(path); - const orders: OptimizedMarketOrder[] = []; - for (let i = 0; i < collapsedPath.length; ) { - if (collapsedPath[i].source === ERC20BridgeSource.Native) { - orders.push(createNativeOrder(collapsedPath[i] as NativeCollapsedFill)); - ++i; - continue; - } - // If there are contiguous bridge orders, we can batch them together. - const contiguousBridgeFills = [collapsedPath[i]]; - for (let j = i + 1; j < collapsedPath.length; ++j) { - if (collapsedPath[j].source === ERC20BridgeSource.Native) { - break; - } - contiguousBridgeFills.push(collapsedPath[j]); - } - // Always use DexForwarderBridge unless configured not to - if (!opts.shouldBatchBridgeOrders) { - orders.push(createBridgeOrder(contiguousBridgeFills[0], makerToken, takerToken, opts)); - i += 1; - } else { - orders.push(createBatchedBridgeOrder(contiguousBridgeFills, opts)); - i += contiguousBridgeFills.length; - } - } - return orders; -} - export function createOrdersFromTwoHopSample( sample: DexSample, opts: CreateOrderFromPathOpts, @@ -248,7 +215,7 @@ function getBridgeAddressFromFill(fill: CollapsedFill, opts: CreateOrderFromPath throw new Error(AggregationError.NoBridgeForSource); } -function createBridgeOrder( +export function createBridgeOrder( fill: CollapsedFill, makerToken: string, takerToken: string, @@ -362,7 +329,7 @@ function createBridgeOrder( }; } -function createBatchedBridgeOrder(fills: CollapsedFill[], opts: CreateOrderFromPathOpts): OptimizedMarketOrder { +export function createBatchedBridgeOrder(fills: CollapsedFill[], opts: CreateOrderFromPathOpts): OptimizedMarketOrder { const [makerToken, takerToken] = getMakerTakerTokens(opts); let totalMakerAssetAmount = ZERO_AMOUNT; let totalTakerAssetAmount = ZERO_AMOUNT; @@ -403,7 +370,7 @@ function createBatchedBridgeOrder(fills: CollapsedFill[], opts: CreateOrderFromP }; } -function getMakerTakerTokens(opts: CreateOrderFromPathOpts): [string, string] { +export function getMakerTakerTokens(opts: CreateOrderFromPathOpts): [string, string] { const makerToken = opts.side === MarketOperation.Sell ? opts.outputToken : opts.inputToken; const takerToken = opts.side === MarketOperation.Sell ? opts.inputToken : opts.outputToken; return [makerToken, takerToken]; @@ -525,7 +492,7 @@ function createCommonBridgeOrderFields(orderDomain: OrderDomain): CommonBridgeOr }; } -function createNativeOrder(fill: NativeCollapsedFill): OptimizedMarketOrder { +export function createNativeOrder(fill: NativeCollapsedFill): OptimizedMarketOrder { return { fills: [fill], ...fill.fillData!.order, // tslint:disable-line:no-non-null-assertion diff --git a/packages/asset-swapper/src/utils/market_operation_utils/path.ts b/packages/asset-swapper/src/utils/market_operation_utils/path.ts new file mode 100644 index 0000000000..89333f6925 --- /dev/null +++ b/packages/asset-swapper/src/utils/market_operation_utils/path.ts @@ -0,0 +1,287 @@ +import { BigNumber } from '@0x/utils'; + +import { MarketOperation } from '../../types'; + +import { POSITIVE_INF, SOURCE_FLAGS, ZERO_AMOUNT } from './constants'; +import { + createBatchedBridgeOrder, + createBridgeOrder, + createNativeOrder, + CreateOrderFromPathOpts, + getMakerTakerTokens, +} from './orders'; +import { getCompleteRate, getRate } from './rate_utils'; +import { + CollapsedFill, + ERC20BridgeSource, + ExchangeProxyOverhead, + Fill, + NativeCollapsedFill, + OptimizedMarketOrder, +} from './types'; + +// tslint:disable: prefer-for-of no-bitwise completed-docs + +export interface PathSize { + input: BigNumber; + output: BigNumber; +} + +export interface PathPenaltyOpts { + ethToOutputRate: BigNumber; + ethToInputRate: BigNumber; + exchangeProxyOverhead: ExchangeProxyOverhead; +} + +export const DEFAULT_PATH_PENALTY_OPTS: PathPenaltyOpts = { + ethToOutputRate: ZERO_AMOUNT, + ethToInputRate: ZERO_AMOUNT, + exchangeProxyOverhead: () => ZERO_AMOUNT, +}; + +export class Path { + public collapsedFills?: ReadonlyArray; + public orders?: OptimizedMarketOrder[]; + public sourceFlags: number = 0; + protected _size: PathSize = { input: ZERO_AMOUNT, output: ZERO_AMOUNT }; + protected _adjustedSize: PathSize = { input: ZERO_AMOUNT, output: ZERO_AMOUNT }; + + protected constructor( + protected readonly side: MarketOperation, + public fills: ReadonlyArray, + protected readonly targetInput: BigNumber, + public readonly pathPenaltyOpts: PathPenaltyOpts, + ) {} + + public static create( + side: MarketOperation, + fills: ReadonlyArray, + targetInput: BigNumber = POSITIVE_INF, + pathPenaltyOpts: PathPenaltyOpts = DEFAULT_PATH_PENALTY_OPTS, + ): Path { + const path = new Path(side, fills, targetInput, pathPenaltyOpts); + fills.forEach(fill => { + path.sourceFlags |= fill.flags; + path._addFillSize(fill); + }); + return path; + } + + public static clone(base: Path): Path { + const clonedPath = new Path(base.side, base.fills.slice(), base.targetInput, base.pathPenaltyOpts); + clonedPath.sourceFlags = base.sourceFlags; + clonedPath._size = { ...base._size }; + clonedPath._adjustedSize = { ...base._adjustedSize }; + clonedPath.collapsedFills = base.collapsedFills === undefined ? undefined : base.collapsedFills.slice(); + clonedPath.orders = base.orders === undefined ? undefined : base.orders.slice(); + return clonedPath; + } + + public append(fill: Fill): this { + (this.fills as Fill[]).push(fill); + this.sourceFlags |= fill.flags; + this._addFillSize(fill); + return this; + } + + public addFallback(fallback: Path): this { + // If the last fill is Native and penultimate is not, then the intention was to partial fill + // In this case we drop it entirely as we can't handle a failure at the end and we don't + // want to fully fill when it gets prepended to the front below + const [last, penultimateIfExists] = this.fills.slice().reverse(); + const lastNativeFillIfExists = + last.source === ERC20BridgeSource.Native && + penultimateIfExists && + penultimateIfExists.source !== ERC20BridgeSource.Native + ? last + : undefined; + // By prepending native paths to the front they cannot split on-chain sources and incur + // an additional protocol fee. I.e [Uniswap,Native,Kyber] becomes [Native,Uniswap,Kyber] + // In the previous step we dropped any hanging Native partial fills, as to not fully fill + const nativeFills = this.fills.filter(f => f.source === ERC20BridgeSource.Native); + this.fills = [...nativeFills.filter(f => f !== lastNativeFillIfExists), ...fallback.fills]; + // Recompute the source flags + this.sourceFlags = this.fills.reduce((flags, fill) => (flags |= fill.flags), 0); + return this; + } + + public collapse(opts: CreateOrderFromPathOpts): CollapsedPath { + const [makerToken, takerToken] = getMakerTakerTokens(opts); + const collapsedFills = this.collapsedFills === undefined ? this._collapseFills() : this.collapsedFills; + this.orders = []; + for (let i = 0; i < collapsedFills.length; ) { + if (collapsedFills[i].source === ERC20BridgeSource.Native) { + this.orders.push(createNativeOrder(collapsedFills[i] as NativeCollapsedFill)); + ++i; + continue; + } + // If there are contiguous bridge orders, we can batch them together. + const contiguousBridgeFills = [collapsedFills[i]]; + for (let j = i + 1; j < collapsedFills.length; ++j) { + if (collapsedFills[j].source === ERC20BridgeSource.Native) { + break; + } + contiguousBridgeFills.push(collapsedFills[j]); + } + // Always use DexForwarderBridge unless configured not to + if (!opts.shouldBatchBridgeOrders) { + this.orders.push(createBridgeOrder(contiguousBridgeFills[0], makerToken, takerToken, opts)); + i += 1; + } else { + this.orders.push(createBatchedBridgeOrder(contiguousBridgeFills, opts)); + i += contiguousBridgeFills.length; + } + } + return this as CollapsedPath; + } + + public size(): PathSize { + return this._size; + } + + public adjustedSize(): PathSize { + const { input, output } = this._adjustedSize; + const { exchangeProxyOverhead, ethToOutputRate, ethToInputRate } = this.pathPenaltyOpts; + const gasOverhead = exchangeProxyOverhead(this.sourceFlags); + const pathPenalty = !ethToOutputRate.isZero() + ? ethToOutputRate.times(gasOverhead) + : ethToInputRate.times(gasOverhead).times(output.dividedToIntegerBy(input)); + return { + input, + output: this.side === MarketOperation.Sell ? output.minus(pathPenalty) : output.plus(pathPenalty), + }; + } + + public adjustedCompleteRate(): BigNumber { + const { input, output } = this.adjustedSize(); + return getCompleteRate(this.side, input, output, this.targetInput); + } + + public adjustedRate(): BigNumber { + const { input, output } = this.adjustedSize(); + return getRate(this.side, input, output); + } + + public adjustedSlippage(maxRate: BigNumber): number { + if (maxRate.eq(0)) { + return 0; + } + const totalRate = this.adjustedRate(); + const rateChange = maxRate.minus(totalRate); + return rateChange.div(maxRate).toNumber(); + } + + public isBetterThan(other: Path): boolean { + if (!this.targetInput.isEqualTo(other.targetInput)) { + throw new Error(`Target input mismatch: ${this.targetInput} !== ${other.targetInput}`); + } + const { targetInput } = this; + const { input } = this._size; + const { input: otherInput } = other._size; + if (input.isLessThan(targetInput) || otherInput.isLessThan(targetInput)) { + return input.isGreaterThan(otherInput); + } else { + return this.adjustedCompleteRate().isGreaterThan(other.adjustedCompleteRate()); + } + // if (otherInput.isLessThan(targetInput)) { + // return input.isGreaterThan(otherInput); + // } else if (input.isGreaterThanOrEqualTo(targetInput)) { + // return this.adjustedCompleteRate().isGreaterThan(other.adjustedCompleteRate()); + // } + // return false; + } + + public isComplete(): boolean { + const { input } = this._size; + return input.gte(this.targetInput); + } + + public isValid(skipDuplicateCheck: boolean = false): boolean { + for (let i = 0; i < this.fills.length; ++i) { + // Fill must immediately follow its parent. + if (this.fills[i].parent) { + if (i === 0 || this.fills[i - 1] !== this.fills[i].parent) { + return false; + } + } + if (!skipDuplicateCheck) { + // Fill must not be duplicated. + for (let j = 0; j < i; ++j) { + if (this.fills[i] === this.fills[j]) { + return false; + } + } + } + } + return doSourcesConflict(this.sourceFlags); + } + + public isValidNextFill(fill: Fill): boolean { + if (this.fills.length === 0) { + return !fill.parent; + } + if (this.fills[this.fills.length - 1] === fill.parent) { + return true; + } + if (fill.parent) { + return false; + } + return doSourcesConflict(this.sourceFlags | fill.flags); + } + + private _collapseFills(): ReadonlyArray { + this.collapsedFills = []; + for (const fill of this.fills) { + const source = fill.source; + if (this.collapsedFills.length !== 0 && source !== ERC20BridgeSource.Native) { + const prevFill = this.collapsedFills[this.collapsedFills.length - 1]; + // If the last fill is from the same source, merge them. + if (prevFill.sourcePathId === fill.sourcePathId) { + prevFill.input = prevFill.input.plus(fill.input); + prevFill.output = prevFill.output.plus(fill.output); + prevFill.fillData = fill.fillData; + prevFill.subFills.push(fill); + continue; + } + } + (this.collapsedFills as CollapsedFill[]).push({ + sourcePathId: fill.sourcePathId, + source: fill.source, + fillData: fill.fillData, + input: fill.input, + output: fill.output, + subFills: [fill], + }); + } + return this.collapsedFills; + } + + private _addFillSize(fill: Fill): void { + if (this._size.input.plus(fill.input).isGreaterThan(this.targetInput)) { + const remainingInput = this.targetInput.minus(this._size.input); + const scaledFillOutput = fill.output.times(remainingInput.div(fill.input)); + this._size.input = this.targetInput; + this._size.output = this._size.output.plus(scaledFillOutput); + // Penalty does not get interpolated. + const penalty = fill.adjustedOutput.minus(fill.output); + this._adjustedSize.input = this.targetInput; + this._adjustedSize.output = this._adjustedSize.output.plus(scaledFillOutput).plus(penalty); + } else { + this._size.input = this._size.input.plus(fill.input); + this._size.output = this._size.output.plus(fill.output); + this._adjustedSize.input = this._adjustedSize.input.plus(fill.input); + this._adjustedSize.output = this._adjustedSize.output.plus(fill.adjustedOutput); + } + } +} + +export interface CollapsedPath extends Path { + readonly collapsedFills: ReadonlyArray; + readonly orders: OptimizedMarketOrder[]; +} + +const MULTIBRIDGE_SOURCES = SOURCE_FLAGS.LiquidityProvider | SOURCE_FLAGS.Uniswap; +export function doSourcesConflict(flags: number): boolean { + const multiBridgeConflict = flags & SOURCE_FLAGS.MultiBridge && flags & MULTIBRIDGE_SOURCES; + return !multiBridgeConflict; +} diff --git a/packages/asset-swapper/src/utils/market_operation_utils/path_optimizer.ts b/packages/asset-swapper/src/utils/market_operation_utils/path_optimizer.ts index f649c59e07..31244d2299 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/path_optimizer.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/path_optimizer.ts @@ -1,17 +1,9 @@ import { BigNumber } from '@0x/utils'; +import * as _ from 'lodash'; import { MarketOperation } from '../../types'; -import { ZERO_AMOUNT } from './constants'; -import { - arePathFlagsAllowed, - getCompleteRate, - getPathAdjustedCompleteRate, - getPathAdjustedRate, - getPathAdjustedSize, - getPathSize, - isValidPath, -} from './fills'; +import { DEFAULT_PATH_PENALTY_OPTS, Path, PathPenaltyOpts } from './path'; import { Fill } from './types'; // tslint:disable: prefer-for-of custom-no-magic-numbers completed-docs no-bitwise @@ -19,134 +11,93 @@ import { Fill } from './types'; const RUN_LIMIT_DECAY_FACTOR = 0.5; /** - * Find the optimal mixture of paths that maximizes (for sells) or minimizes + * Find the optimal mixture of fills that maximizes (for sells) or minimizes * (for buys) output, while meeting the input requirement. */ export async function findOptimalPathAsync( side: MarketOperation, - paths: Fill[][], + fills: Fill[][], targetInput: BigNumber, runLimit: number = 2 ** 8, -): Promise { - // Sort paths by descending adjusted completed rate. - const sortedPaths = paths - .slice(0) - .sort((a, b) => - getPathAdjustedCompleteRate(side, b, targetInput).comparedTo( - getPathAdjustedCompleteRate(side, a, targetInput), - ), - ); - let optimalPath = sortedPaths[0] || []; + opts: PathPenaltyOpts = DEFAULT_PATH_PENALTY_OPTS, +): Promise { + const rates = rateBySourcePathId(side, fills, targetInput); + const paths = fills.map(singleSourceFills => Path.create(side, singleSourceFills, targetInput, opts)); + // Sort fill arrays by descending adjusted completed rate. + const sortedPaths = paths.sort((a, b) => b.adjustedCompleteRate().comparedTo(a.adjustedCompleteRate())); + if (sortedPaths.length === 0) { + return undefined; + } + let optimalPath = sortedPaths[0]; for (const [i, path] of sortedPaths.slice(1).entries()) { - optimalPath = mixPaths(side, optimalPath, path, targetInput, runLimit * RUN_LIMIT_DECAY_FACTOR ** i); + optimalPath = mixPaths(side, optimalPath, path, targetInput, runLimit * RUN_LIMIT_DECAY_FACTOR ** i, rates); // Yield to event loop. await Promise.resolve(); } - return isPathComplete(optimalPath, targetInput) ? optimalPath : undefined; + return optimalPath.isComplete() ? optimalPath : undefined; } function mixPaths( side: MarketOperation, - pathA: Fill[], - pathB: Fill[], + pathA: Path, + pathB: Path, targetInput: BigNumber, maxSteps: number, -): Fill[] { + rateBySourcePathId: { [id: string]: BigNumber }, +): Path { const _maxSteps = Math.max(maxSteps, 32); let steps = 0; // We assume pathA is the better of the two initially. - let bestPath: Fill[] = pathA; - let [bestPathInput, bestPathOutput] = getPathAdjustedSize(pathA, targetInput); - let bestPathRate = getCompleteRate(side, bestPathInput, bestPathOutput, targetInput); - const _isBetterPath = (input: BigNumber, rate: BigNumber) => { - if (bestPathInput.lt(targetInput)) { - return input.gt(bestPathInput); - } else if (input.gte(targetInput)) { - return rate.gt(bestPathRate); - } - return false; - }; - const _walk = (path: Fill[], input: BigNumber, output: BigNumber, flags: number, remainingFills: Fill[]) => { + let bestPath: Path = pathA; + + const _walk = (path: Path, remainingFills: Fill[]) => { steps += 1; - const rate = getCompleteRate(side, input, output, targetInput); - if (_isBetterPath(input, rate)) { + if (path.isBetterThan(bestPath)) { bestPath = path; - bestPathInput = input; - bestPathOutput = output; - bestPathRate = rate; } - const remainingInput = targetInput.minus(input); - if (remainingInput.gt(0)) { + const remainingInput = targetInput.minus(path.size().input); + if (remainingInput.isGreaterThan(0)) { for (let i = 0; i < remainingFills.length && steps < _maxSteps; ++i) { const fill = remainingFills[i]; // Only walk valid paths. - if (!isValidNextPathFill(path, flags, fill)) { + if (!path.isValidNextFill(fill)) { continue; } // Remove this fill from the next list of candidate fills. const nextRemainingFills = remainingFills.slice(); nextRemainingFills.splice(i, 1); // Recurse. - _walk( - [...path, fill], - input.plus(BigNumber.min(remainingInput, fill.input)), - output.plus( - // Clip the output of the next fill to the remaining - // input. - clipFillAdjustedOutput(fill, remainingInput), - ), - flags | fill.flags, - nextRemainingFills, - ); + _walk(Path.clone(path).append(fill), nextRemainingFills); } } }; - const allFills = [...pathA, ...pathB]; - const sources = allFills.filter(f => f.index === 0).map(f => f.sourcePathId); - const rateBySource = Object.assign( - {}, - ...sources.map(s => ({ - [s]: getPathAdjustedRate(side, allFills.filter(f => f.sourcePathId === s), targetInput), - })), - ); + const allFills = [...pathA.fills, ...pathB.fills]; // Sort subpaths by rate and keep fills contiguous to improve our // chances of walking ideal, valid paths first. const sortedFills = allFills.sort((a, b) => { if (a.sourcePathId !== b.sourcePathId) { - return rateBySource[b.sourcePathId].comparedTo(rateBySource[a.sourcePathId]); + return rateBySourcePathId[b.sourcePathId].comparedTo(rateBySourcePathId[a.sourcePathId]); } return a.index - b.index; }); - _walk([], ZERO_AMOUNT, ZERO_AMOUNT, 0, sortedFills); - if (!isValidPath(bestPath)) { + _walk(Path.create(side, [], targetInput, pathA.pathPenaltyOpts), sortedFills); + if (!bestPath.isValid()) { throw new Error('nooope'); } return bestPath; } -function isValidNextPathFill(path: Fill[], pathFlags: number, fill: Fill): boolean { - if (path.length === 0) { - return !fill.parent; - } - if (path[path.length - 1] === fill.parent) { - return true; - } - if (fill.parent) { - return false; - } - return arePathFlagsAllowed(pathFlags | fill.flags); -} - -function isPathComplete(path: Fill[], targetInput: BigNumber): boolean { - const [input] = getPathSize(path); - return input.gte(targetInput); -} - -function clipFillAdjustedOutput(fill: Fill, remainingInput: BigNumber): BigNumber { - if (fill.input.lte(remainingInput)) { - return fill.adjustedOutput; - } - // Penalty does not get interpolated. - const penalty = fill.adjustedOutput.minus(fill.output); - return remainingInput.times(fill.output.div(fill.input)).plus(penalty); +function rateBySourcePathId( + side: MarketOperation, + fills: Fill[][], + targetInput: BigNumber, +): { [id: string]: BigNumber } { + const flattenedFills = _.flatten(fills); + const sourcePathIds = flattenedFills.filter(f => f.index === 0).map(f => f.sourcePathId); + return Object.assign( + {}, + ...sourcePathIds.map(s => ({ + [s]: Path.create(side, flattenedFills.filter(f => f.sourcePathId === s), targetInput).adjustedRate(), + })), + ); } diff --git a/packages/asset-swapper/src/utils/market_operation_utils/rate_utils.ts b/packages/asset-swapper/src/utils/market_operation_utils/rate_utils.ts new file mode 100644 index 0000000000..713972cb33 --- /dev/null +++ b/packages/asset-swapper/src/utils/market_operation_utils/rate_utils.ts @@ -0,0 +1,48 @@ +import { BigNumber } from '@0x/utils'; + +import { MarketOperation } from '../../types'; + +import { ZERO_AMOUNT } from './constants'; +import { DexSample, ERC20BridgeSource, FeeSchedule, MultiHopFillData } from './types'; + +export function getTwoHopAdjustedRate( + side: MarketOperation, + twoHopQuote: DexSample, + targetInput: BigNumber, + ethToOutputRate: BigNumber, + fees: FeeSchedule = {}, +): BigNumber { + const { output, input, fillData } = twoHopQuote; + if (input.isLessThan(targetInput) || output.isZero()) { + return ZERO_AMOUNT; + } + const penalty = ethToOutputRate.times(fees[ERC20BridgeSource.MultiHop]!(fillData)); + const adjustedOutput = side === MarketOperation.Sell ? output.minus(penalty) : output.plus(penalty); + return side === MarketOperation.Sell ? adjustedOutput.div(input) : input.div(adjustedOutput); +} + +export function getCompleteRate( + side: MarketOperation, + input: BigNumber, + output: BigNumber, + targetInput: BigNumber, +): BigNumber { + if (input.eq(0) || output.eq(0) || targetInput.eq(0)) { + return ZERO_AMOUNT; + } + // Penalize paths that fall short of the entire input amount by a factor of + // input / targetInput => (i / t) + if (side === MarketOperation.Sell) { + // (o / i) * (i / t) => (o / t) + return output.div(targetInput); + } + // (i / o) * (i / t) + return input.div(output).times(input.div(targetInput)); +} + +export function getRate(side: MarketOperation, input: BigNumber, output: BigNumber): BigNumber { + if (input.eq(0) || output.eq(0)) { + return ZERO_AMOUNT; + } + return side === MarketOperation.Sell ? output.div(input) : input.div(output); +} diff --git a/packages/asset-swapper/src/utils/market_operation_utils/types.ts b/packages/asset-swapper/src/utils/market_operation_utils/types.ts index 5e7ea158fd..c6af06a62e 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/types.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/types.ts @@ -156,16 +156,6 @@ export interface DexSample extends Source output: BigNumber; } -/** - * Flags for `Fill` objects. - */ -export enum FillFlags { - ConflictsWithKyber = 0x1, - Kyber = 0x2, - ConflictsWithMultiBridge = 0x4, - MultiBridge = 0x8, -} - /** * Represents a node on a fill path. */ @@ -174,8 +164,8 @@ export interface Fill extends SourceInfo< // This is generated when the path is generated and is useful to distinguish // paths that have the same `source` IDs but are distinct (e.g., Curves). sourcePathId: string; - // See `FillFlags`. - flags: FillFlags; + // See `SOURCE_FLAGS`. + flags: number; // Input fill amount (taker asset amount in a sell, maker asset amount in a buy). input: BigNumber; // Output fill amount (maker asset amount in a sell, taker asset amount in a buy). @@ -234,6 +224,7 @@ export interface GetMarketOrdersRfqtOpts extends RfqtRequestOpts { export type FeeEstimate = (fillData?: FillData) => number | BigNumber; export type FeeSchedule = Partial<{ [key in ERC20BridgeSource]: FeeEstimate }>; +export type ExchangeProxyOverhead = (sourceFlags: number) => BigNumber; /** * Options for `getMarketSellOrdersAsync()` and `getMarketBuyOrdersAsync()`. @@ -288,6 +279,7 @@ export interface GetMarketOrdersOpts { * Estimated gas consumed by each liquidity source. */ gasSchedule: FeeSchedule; + exchangeProxyOverhead: ExchangeProxyOverhead; /** * Whether to pad the quote with a redundant fallback quote using different * sources. Defaults to `true`. @@ -321,11 +313,8 @@ export interface SourceQuoteOperation export interface OptimizerResult { optimizedOrders: OptimizedMarketOrder[]; - isTwoHop: boolean; + sourceFlags: number; liquidityDelivered: CollapsedFill[] | DexSample; -} - -export interface OptimizerResultWithReport extends OptimizerResult { quoteReport?: QuoteReport; } diff --git a/packages/asset-swapper/src/utils/quote_report_generator.ts b/packages/asset-swapper/src/utils/quote_report_generator.ts index 81fce3c0f0..31b49dcda8 100644 --- a/packages/asset-swapper/src/utils/quote_report_generator.ts +++ b/packages/asset-swapper/src/utils/quote_report_generator.ts @@ -64,7 +64,7 @@ export function generateQuoteReport( multiHopQuotes: Array>, nativeOrders: SignedOrder[], orderFillableAmounts: BigNumber[], - liquidityDelivered: CollapsedFill[] | DexSample, + liquidityDelivered: ReadonlyArray | DexSample, quoteRequestor?: QuoteRequestor, ): QuoteReport { const dexReportSourcesConsidered = dexQuotes.map(quote => _dexSampleToReportSource(quote, marketOperation)); @@ -101,7 +101,9 @@ export function generateQuoteReport( } }); } else { - sourcesDelivered = [_multiHopSampleToReportSource(liquidityDelivered, marketOperation)]; + sourcesDelivered = [ + _multiHopSampleToReportSource(liquidityDelivered as DexSample, marketOperation), + ]; } return { sourcesConsidered, diff --git a/packages/asset-swapper/src/utils/quote_simulation.ts b/packages/asset-swapper/src/utils/quote_simulation.ts index 5e46046e79..863abb1489 100644 --- a/packages/asset-swapper/src/utils/quote_simulation.ts +++ b/packages/asset-swapper/src/utils/quote_simulation.ts @@ -349,7 +349,7 @@ function fromIntermediateQuoteFillResult(ir: IntermediateQuoteFillResult, quoteI }; } -export function getFlattenedFillsFromOrders(orders: OptimizedMarketOrder[]): CollapsedFill[] { +function getFlattenedFillsFromOrders(orders: OptimizedMarketOrder[]): CollapsedFill[] { const fills: CollapsedFill[] = []; for (const o of orders) { fills.push(...o.fills); diff --git a/packages/asset-swapper/src/utils/swap_quote_calculator.ts b/packages/asset-swapper/src/utils/swap_quote_calculator.ts index 2d16f48a3e..3f4e56eb10 100644 --- a/packages/asset-swapper/src/utils/swap_quote_calculator.ts +++ b/packages/asset-swapper/src/utils/swap_quote_calculator.ts @@ -16,6 +16,7 @@ import { } from '../types'; import { MarketOperationUtils } from './market_operation_utils'; +import { SOURCE_FLAGS } from './market_operation_utils/constants'; import { convertNativeOrderToFullyFillableOptimizedOrders } from './market_operation_utils/orders'; import { ERC20BridgeSource, @@ -130,70 +131,74 @@ export class SwapQuoteCalculator { let optimizedOrders: OptimizedMarketOrder[]; let quoteReport: QuoteReport | undefined; - let isTwoHop = false; + let sourceFlags: number = 0; - { - // Scale fees by gas price. - const _opts: GetMarketOrdersOpts = { - ...opts, - feeSchedule: _.mapValues(opts.feeSchedule, gasCost => (fillData?: FillData) => - gasCost === undefined ? 0 : gasPrice.times(gasCost(fillData)), - ), - }; + // Scale fees by gas price. + const _opts: GetMarketOrdersOpts = { + ...opts, + feeSchedule: _.mapValues(opts.feeSchedule, gasCost => (fillData?: FillData) => + gasCost === undefined ? 0 : gasPrice.times(gasCost(fillData)), + ), + exchangeProxyOverhead: (sourceFlags: number) => gasPrice.times(opts.exchangeProxyOverhead!(sourceFlags)), + }; - const firstOrderMakerAssetData = !!prunedOrders[0] - ? assetDataUtils.decodeAssetDataOrThrow(prunedOrders[0].makerAssetData) - : { assetProxyId: '' }; + const firstOrderMakerAssetData = !!prunedOrders[0] + ? assetDataUtils.decodeAssetDataOrThrow(prunedOrders[0].makerAssetData) + : { assetProxyId: '' }; - if (firstOrderMakerAssetData.assetProxyId === AssetProxyId.ERC721) { - // HACK: to conform ERC721 orders to the output of market operation utils, assumes complete fillable - optimizedOrders = prunedOrders.map(o => convertNativeOrderToFullyFillableOptimizedOrders(o)); + if (firstOrderMakerAssetData.assetProxyId === AssetProxyId.ERC721) { + // HACK: to conform ERC721 orders to the output of market operation utils, assumes complete fillable + optimizedOrders = prunedOrders.map(o => convertNativeOrderToFullyFillableOptimizedOrders(o)); + } else { + if (operation === MarketOperation.Buy) { + const buyResult = await this._marketOperationUtils.getMarketBuyOrdersAsync( + prunedOrders, + assetFillAmount, + _opts, + ); + optimizedOrders = buyResult.optimizedOrders; + quoteReport = buyResult.quoteReport; + sourceFlags = buyResult.sourceFlags; } else { - if (operation === MarketOperation.Buy) { - const buyResult = await this._marketOperationUtils.getMarketBuyOrdersAsync( - prunedOrders, - assetFillAmount, - _opts, - ); - optimizedOrders = buyResult.optimizedOrders; - quoteReport = buyResult.quoteReport; - isTwoHop = buyResult.isTwoHop; - } else { - const sellResult = await this._marketOperationUtils.getMarketSellOrdersAsync( - prunedOrders, - assetFillAmount, - _opts, - ); - optimizedOrders = sellResult.optimizedOrders; - quoteReport = sellResult.quoteReport; - isTwoHop = sellResult.isTwoHop; - } + const sellResult = await this._marketOperationUtils.getMarketSellOrdersAsync( + prunedOrders, + assetFillAmount, + _opts, + ); + optimizedOrders = sellResult.optimizedOrders; + quoteReport = sellResult.quoteReport; + sourceFlags = sellResult.sourceFlags; } } // assetData information for the result const { makerAssetData, takerAssetData } = prunedOrders[0]; - return isTwoHop - ? createTwoHopSwapQuote( - makerAssetData, - takerAssetData, - optimizedOrders, - operation, - assetFillAmount, - gasPrice, - opts.gasSchedule, - quoteReport, - ) - : createSwapQuote( - makerAssetData, - takerAssetData, - optimizedOrders, - operation, - assetFillAmount, - gasPrice, - opts.gasSchedule, - quoteReport, - ); + const swapQuote = + sourceFlags === SOURCE_FLAGS[ERC20BridgeSource.MultiHop] + ? createTwoHopSwapQuote( + makerAssetData, + takerAssetData, + optimizedOrders, + operation, + assetFillAmount, + gasPrice, + opts.gasSchedule, + quoteReport, + ) + : createSwapQuote( + makerAssetData, + takerAssetData, + optimizedOrders, + operation, + assetFillAmount, + gasPrice, + opts.gasSchedule, + quoteReport, + ); + const exchangeProxyOverhead = _opts.exchangeProxyOverhead(sourceFlags).toNumber(); + swapQuote.bestCaseQuoteInfo.gas += exchangeProxyOverhead; + swapQuote.worstCaseQuoteInfo.gas += exchangeProxyOverhead; + return swapQuote; } } diff --git a/packages/asset-swapper/test/market_operation_utils_test.ts b/packages/asset-swapper/test/market_operation_utils_test.ts index 77261f3b6b..1e3323c97a 100644 --- a/packages/asset-swapper/test/market_operation_utils_test.ts +++ b/packages/asset-swapper/test/market_operation_utils_test.ts @@ -24,7 +24,7 @@ import { SELL_SOURCE_FILTER, ZERO_AMOUNT, } from '../src/utils/market_operation_utils/constants'; -import { createFillPaths } from '../src/utils/market_operation_utils/fills'; +import { createFills } from '../src/utils/market_operation_utils/fills'; import { DexOrderSampler } from '../src/utils/market_operation_utils/sampler'; import { BATCH_SOURCE_FILTERS } from '../src/utils/market_operation_utils/sampler_operations'; import { @@ -1299,7 +1299,7 @@ describe('MarketOperationUtils tests', () => { it('batches contiguous bridge sources', async () => { const rates: RatesBySource = { ...ZERO_RATES }; - rates[ERC20BridgeSource.Native] = [0.5, 0.01, 0.01, 0.01]; + rates[ERC20BridgeSource.Native] = [0.3, 0.01, 0.01, 0.01]; rates[ERC20BridgeSource.Eth2Dai] = [0.49, 0.02, 0.01, 0.01]; rates[ERC20BridgeSource.Uniswap] = [0.48, 0.01, 0.01, 0.01]; replaceSamplerOps({ @@ -1318,14 +1318,14 @@ describe('MarketOperationUtils tests', () => { expect(improvedOrders).to.be.length(2); const orderFillSources = getSortedOrderSources(MarketOperation.Sell, improvedOrders); expect(orderFillSources).to.deep.eq([ - [ERC20BridgeSource.Native], [ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Uniswap], + [ERC20BridgeSource.Native], ]); }); }); }); - describe('createFillPaths', () => { + describe('createFills', () => { const takerAssetAmount = new BigNumber(5000000); const ethToOutputRate = new BigNumber(0.5); // tslint:disable-next-line:no-object-literal-type-assertion @@ -1359,7 +1359,7 @@ describe('MarketOperationUtils tests', () => { }; it('penalizes native fill based on target amount when target is smaller', () => { - const path = createFillPaths({ + const path = createFills({ side: MarketOperation.Sell, orders, dexQuotes: [], @@ -1372,7 +1372,7 @@ describe('MarketOperationUtils tests', () => { }); it('penalizes native fill based on available amount when target is larger', () => { - const path = createFillPaths({ + const path = createFills({ side: MarketOperation.Sell, orders, dexQuotes: [], From 1c15ecacb0f37c4b6f4d1063ee109b678dc2300f Mon Sep 17 00:00:00 2001 From: Michael Zhu Date: Wed, 9 Sep 2020 18:39:58 -0700 Subject: [PATCH 14/32] add tests for LiquidityProviderFeature --- .../src/features/LiquidityProviderFeature.sol | 20 +- .../zero-ex/contracts/test/TestBridge.sol | 69 +++++ contracts/zero-ex/package.json | 2 +- contracts/zero-ex/test/artifacts.ts | 2 + .../test/features/liquidity_provider_test.ts | 246 ++++++++++++++++-- contracts/zero-ex/test/wrappers.ts | 1 + contracts/zero-ex/tsconfig.json | 1 + packages/utils/src/index.ts | 1 + .../liquidity_provider_revert_errors.ts | 47 ++++ 9 files changed, 347 insertions(+), 42 deletions(-) create mode 100644 contracts/zero-ex/contracts/test/TestBridge.sol create mode 100644 packages/utils/src/revert_errors/zero-ex/liquidity_provider_revert_errors.ts diff --git a/contracts/zero-ex/contracts/src/features/LiquidityProviderFeature.sol b/contracts/zero-ex/contracts/src/features/LiquidityProviderFeature.sol index 4c7cda1b4b..61bb080866 100644 --- a/contracts/zero-ex/contracts/src/features/LiquidityProviderFeature.sol +++ b/contracts/zero-ex/contracts/src/features/LiquidityProviderFeature.sol @@ -28,30 +28,12 @@ import "../errors/LibLiquidityProviderRichErrors.sol"; import "../fixins/FixinCommon.sol"; import "../migrations/LibMigrate.sol"; import "../storage/LibLiquidityProviderStorage.sol"; +import "../vendor/v3/IERC20Bridge.sol"; import "./IFeature.sol"; import "./ILiquidityProviderFeature.sol"; import "./ITokenSpenderFeature.sol"; -interface IERC20Bridge { - /// @dev Transfers `amount` of the ERC20 `tokenAddress` from `from` to `to`. - /// @param tokenAddress The address of the ERC20 token to transfer. - /// @param from Address to transfer asset from. - /// @param to Address to transfer asset to. - /// @param amount Amount of asset to transfer. - /// @param bridgeData Arbitrary asset data needed by the bridge contract. - /// @return success The magic bytes `0xdc1600f3` if successful. - function bridgeTransferFrom( - address tokenAddress, - address from, - address to, - uint256 amount, - bytes calldata bridgeData - ) - external - returns (bytes4 success); -} - contract LiquidityProviderFeature is IFeature, ILiquidityProviderFeature, diff --git a/contracts/zero-ex/contracts/test/TestBridge.sol b/contracts/zero-ex/contracts/test/TestBridge.sol new file mode 100644 index 0000000000..4f94594649 --- /dev/null +++ b/contracts/zero-ex/contracts/test/TestBridge.sol @@ -0,0 +1,69 @@ +/* + + Copyright 2020 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.6.5; +pragma experimental ABIEncoderV2; + +import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol"; +import "../src/vendor/v3/IERC20Bridge.sol"; + + +contract TestBridge is + IERC20Bridge +{ + IERC20TokenV06 public immutable xAsset; + IERC20TokenV06 public immutable yAsset; + + constructor(IERC20TokenV06 xAsset_, IERC20TokenV06 yAsset_) + public + { + xAsset = xAsset_; + yAsset = yAsset_; + } + + /// @dev Transfers `amount` of the ERC20 `tokenAddress` from `from` to `to`. + /// @param tokenAddress The address of the ERC20 token to transfer. + /// @param from Address to transfer asset from. + /// @param to Address to transfer asset to. + /// @param amount Amount of asset to transfer. + /// @param bridgeData Arbitrary asset data needed by the bridge contract. + /// @return success The magic bytes `0xdc1600f3` if successful. + function bridgeTransferFrom( + address tokenAddress, + address from, + address to, + uint256 amount, + bytes calldata bridgeData + ) + external + override + returns (bytes4 success) + { + IERC20TokenV06 takerToken = tokenAddress == address(xAsset) ? yAsset : xAsset; + uint256 takerTokenBalance = takerToken.balanceOf(address(this)); + emit ERC20BridgeTransfer( + address(takerToken), + tokenAddress, + takerTokenBalance, + amount, + from, + to + ); + return 0xdecaf000; + } +} diff --git a/contracts/zero-ex/package.json b/contracts/zero-ex/package.json index c4f9bbc6c7..55d941e368 100644 --- a/contracts/zero-ex/package.json +++ b/contracts/zero-ex/package.json @@ -41,7 +41,7 @@ "config": { "publicInterfaceContracts": "IZeroEx,ZeroEx,FullMigration,InitialMigration,IFlashWallet,IAllowanceTarget,IERC20Transformer,IOwnableFeature,ISimpleFunctionRegistryFeature,ITokenSpenderFeature,ITransformERC20Feature,FillQuoteTransformer,PayTakerTransformer,WethTransformer,OwnableFeature,SimpleFunctionRegistryFeature,TransformERC20Feature,TokenSpenderFeature,AffiliateFeeTransformer,SignatureValidatorFeature,MetaTransactionsFeature,LogMetadataTransformer,BridgeAdapter,LiquidityProviderFeature", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.", - "abis": "./test/generated-artifacts/@(AffiliateFeeTransformer|AllowanceTarget|BootstrapFeature|BridgeAdapter|FillQuoteTransformer|FixinCommon|FixinEIP712|FixinReentrancyGuard|FlashWallet|FullMigration|IAllowanceTarget|IBootstrapFeature|IBridgeAdapter|IERC20Bridge|IERC20Transformer|IExchange|IFeature|IFlashWallet|IGasToken|ILiquidityProviderFeature|IMetaTransactionsFeature|IOwnableFeature|ISignatureValidatorFeature|ISimpleFunctionRegistryFeature|ITestSimpleFunctionRegistryFeature|ITokenSpenderFeature|ITransformERC20Feature|IUniswapFeature|IZeroEx|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibLiquidityProviderRichErrors|LibLiquidityProviderStorage|LibMetaTransactionsRichErrors|LibMetaTransactionsStorage|LibMigrate|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibReentrancyGuardStorage|LibSignatureRichErrors|LibSignedCallData|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibSpenderRichErrors|LibStorage|LibTokenSpenderStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|LiquidityProviderFeature|LogMetadataTransformer|MetaTransactionsFeature|MixinAdapterAddresses|MixinBalancer|MixinCurve|MixinKyber|MixinMStable|MixinMooniswap|MixinOasis|MixinUniswap|MixinUniswapV2|MixinZeroExBridge|OwnableFeature|PayTakerTransformer|SignatureValidatorFeature|SimpleFunctionRegistryFeature|TestCallTarget|TestDelegateCaller|TestFillQuoteTransformerBridge|TestFillQuoteTransformerExchange|TestFillQuoteTransformerHost|TestFullMigration|TestInitialMigration|TestMetaTransactionsTransformERC20Feature|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestTokenSpender|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerBase|TestTransformerDeployerTransformer|TestTransformerHost|TestWeth|TestWethTransformerHost|TestZeroExFeature|TokenSpenderFeature|TransformERC20Feature|Transformer|TransformerDeployer|UniswapFeature|WethTransformer|ZeroEx).json" + "abis": "./test/generated-artifacts/@(AffiliateFeeTransformer|AllowanceTarget|BootstrapFeature|BridgeAdapter|FillQuoteTransformer|FixinCommon|FixinEIP712|FixinReentrancyGuard|FlashWallet|FullMigration|IAllowanceTarget|IBootstrapFeature|IBridgeAdapter|IERC20Bridge|IERC20Transformer|IExchange|IFeature|IFlashWallet|IGasToken|ILiquidityProviderFeature|IMetaTransactionsFeature|IOwnableFeature|ISignatureValidatorFeature|ISimpleFunctionRegistryFeature|ITestSimpleFunctionRegistryFeature|ITokenSpenderFeature|ITransformERC20Feature|IUniswapFeature|IZeroEx|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibLiquidityProviderRichErrors|LibLiquidityProviderStorage|LibMetaTransactionsRichErrors|LibMetaTransactionsStorage|LibMigrate|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibReentrancyGuardStorage|LibSignatureRichErrors|LibSignedCallData|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibSpenderRichErrors|LibStorage|LibTokenSpenderStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|LiquidityProviderFeature|LogMetadataTransformer|MetaTransactionsFeature|MixinAdapterAddresses|MixinBalancer|MixinCurve|MixinKyber|MixinMStable|MixinMooniswap|MixinOasis|MixinUniswap|MixinUniswapV2|MixinZeroExBridge|OwnableFeature|PayTakerTransformer|SignatureValidatorFeature|SimpleFunctionRegistryFeature|TestBridge|TestCallTarget|TestDelegateCaller|TestFillQuoteTransformerBridge|TestFillQuoteTransformerExchange|TestFillQuoteTransformerHost|TestFullMigration|TestInitialMigration|TestMetaTransactionsTransformERC20Feature|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestTokenSpender|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerBase|TestTransformerDeployerTransformer|TestTransformerHost|TestWeth|TestWethTransformerHost|TestZeroExFeature|TokenSpenderFeature|TransformERC20Feature|Transformer|TransformerDeployer|UniswapFeature|WethTransformer|ZeroEx).json" }, "repository": { "type": "git", diff --git a/contracts/zero-ex/test/artifacts.ts b/contracts/zero-ex/test/artifacts.ts index a2eb14facf..6de120b62e 100644 --- a/contracts/zero-ex/test/artifacts.ts +++ b/contracts/zero-ex/test/artifacts.ts @@ -75,6 +75,7 @@ import * as OwnableFeature from '../test/generated-artifacts/OwnableFeature.json import * as PayTakerTransformer from '../test/generated-artifacts/PayTakerTransformer.json'; import * as SignatureValidatorFeature from '../test/generated-artifacts/SignatureValidatorFeature.json'; import * as SimpleFunctionRegistryFeature from '../test/generated-artifacts/SimpleFunctionRegistryFeature.json'; +import * as TestBridge from '../test/generated-artifacts/TestBridge.json'; import * as TestCallTarget from '../test/generated-artifacts/TestCallTarget.json'; import * as TestDelegateCaller from '../test/generated-artifacts/TestDelegateCaller.json'; import * as TestFillQuoteTransformerBridge from '../test/generated-artifacts/TestFillQuoteTransformerBridge.json'; @@ -182,6 +183,7 @@ export const artifacts = { IExchange: IExchange as ContractArtifact, IGasToken: IGasToken as ContractArtifact, ITestSimpleFunctionRegistryFeature: ITestSimpleFunctionRegistryFeature as ContractArtifact, + TestBridge: TestBridge as ContractArtifact, TestCallTarget: TestCallTarget as ContractArtifact, TestDelegateCaller: TestDelegateCaller as ContractArtifact, TestFillQuoteTransformerBridge: TestFillQuoteTransformerBridge as ContractArtifact, diff --git a/contracts/zero-ex/test/features/liquidity_provider_test.ts b/contracts/zero-ex/test/features/liquidity_provider_test.ts index 4c183456df..12fbfc0e9a 100644 --- a/contracts/zero-ex/test/features/liquidity_provider_test.ts +++ b/contracts/zero-ex/test/features/liquidity_provider_test.ts @@ -1,26 +1,28 @@ -import { - blockchainTests, - expect, - getRandomInteger, - randomAddress, - verifyEventsFromLogs, -} from '@0x/contracts-test-utils'; -import { BigNumber, hexUtils, StringRevertError, ZeroExRevertErrors } from '@0x/utils'; +import { artifacts as erc20Artifacts, DummyERC20TokenContract } from '@0x/contracts-erc20'; +import { blockchainTests, constants, expect, randomAddress, verifyEventsFromLogs } from '@0x/contracts-test-utils'; +import { BigNumber, OwnableRevertErrors, ZeroExRevertErrors } from '@0x/utils'; -import { IZeroExContract, TokenSpenderFeatureContract } from '../../src/wrappers'; +import { + IOwnableFeatureContract, + IZeroExContract, + LiquidityProviderFeatureContract, + TokenSpenderFeatureContract, +} from '../../src/wrappers'; import { artifacts } from '../artifacts'; import { abis } from '../utils/abis'; import { fullMigrateAsync } from '../utils/migration'; -import { TestTokenSpenderERC20TokenContract, TestTokenSpenderERC20TokenEvents } from '../wrappers'; +import { IERC20BridgeEvents, TestBridgeContract, TestWethContract } from '../wrappers'; -blockchainTests.resets('LiquidityProvider feature', env => { +blockchainTests('LiquidityProvider feature', env => { let zeroEx: IZeroExContract; - let feature: TokenSpenderFeatureContract; - let token: TestTokenSpenderERC20TokenContract; - let allowanceTarget: string; + let feature: LiquidityProviderFeatureContract; + let token: DummyERC20TokenContract; + let weth: TestWethContract; + let owner: string; + let taker: string; before(async () => { - const [owner] = await env.getAccountAddressesAsync(); + [owner, taker] = await env.getAccountAddressesAsync(); zeroEx = await fullMigrateAsync(owner, env.provider, env.txDefaults, { tokenSpender: (await TokenSpenderFeatureContract.deployFrom0xArtifactAsync( artifacts.TestTokenSpender, @@ -29,20 +31,220 @@ blockchainTests.resets('LiquidityProvider feature', env => { artifacts, )).address, }); - feature = new TokenSpenderFeatureContract(zeroEx.address, env.provider, env.txDefaults, abis); - token = await TestTokenSpenderERC20TokenContract.deployFrom0xArtifactAsync( - artifacts.TestTokenSpenderERC20Token, + const tokenSpender = new TokenSpenderFeatureContract(zeroEx.address, env.provider, env.txDefaults, abis); + const allowanceTarget = await tokenSpender.getAllowanceTarget().callAsync(); + + token = await DummyERC20TokenContract.deployFrom0xArtifactAsync( + erc20Artifacts.DummyERC20Token, + env.provider, + env.txDefaults, + erc20Artifacts, + constants.DUMMY_TOKEN_NAME, + constants.DUMMY_TOKEN_SYMBOL, + constants.DUMMY_TOKEN_DECIMALS, + constants.DUMMY_TOKEN_TOTAL_SUPPLY, + ); + await token.setBalance(taker, constants.INITIAL_ERC20_BALANCE).awaitTransactionSuccessAsync(); + weth = await TestWethContract.deployFrom0xArtifactAsync( + artifacts.TestWeth, env.provider, env.txDefaults, artifacts, ); - allowanceTarget = await feature.getAllowanceTarget().callAsync(); - }); + await token + .approve(allowanceTarget, constants.INITIAL_ERC20_ALLOWANCE) + .awaitTransactionSuccessAsync({ from: taker }); + feature = new LiquidityProviderFeatureContract(zeroEx.address, env.provider, env.txDefaults, abis); + const featureImpl = await LiquidityProviderFeatureContract.deployFrom0xArtifactAsync( + artifacts.LiquidityProviderFeature, + env.provider, + env.txDefaults, + artifacts, + weth.address, + ); + await new IOwnableFeatureContract(zeroEx.address, env.provider, env.txDefaults, abis) + .migrate(featureImpl.address, featureImpl.migrate().getABIEncodedTransactionData(), owner) + .awaitTransactionSuccessAsync(); + }); describe('Registry', () => { - + it('`getLiquidityProviderForMarket` reverts if address is not set', async () => { + const [xAsset, yAsset] = [randomAddress(), randomAddress()]; + let tx = feature.getLiquidityProviderForMarket(xAsset, yAsset).awaitTransactionSuccessAsync(); + expect(tx).to.revertWith( + new ZeroExRevertErrors.LiquidityProvider.NoLiquidityProviderForMarketError(xAsset, yAsset), + ); + tx = feature.getLiquidityProviderForMarket(yAsset, xAsset).awaitTransactionSuccessAsync(); + return expect(tx).to.revertWith( + new ZeroExRevertErrors.LiquidityProvider.NoLiquidityProviderForMarketError(yAsset, xAsset), + ); + }); + it('can set/get a liquidity provider address for a given market', async () => { + const expectedAddress = randomAddress(); + await feature + .setLiquidityProviderForMarket(token.address, weth.address, expectedAddress) + .awaitTransactionSuccessAsync(); + let actualAddress = await feature.getLiquidityProviderForMarket(token.address, weth.address).callAsync(); + expect(actualAddress).to.equal(expectedAddress); + actualAddress = await feature.getLiquidityProviderForMarket(weth.address, token.address).callAsync(); + expect(actualAddress).to.equal(expectedAddress); + }); + it('can update a liquidity provider address for a given market', async () => { + const expectedAddress = randomAddress(); + await feature + .setLiquidityProviderForMarket(token.address, weth.address, expectedAddress) + .awaitTransactionSuccessAsync(); + let actualAddress = await feature.getLiquidityProviderForMarket(token.address, weth.address).callAsync(); + expect(actualAddress).to.equal(expectedAddress); + actualAddress = await feature.getLiquidityProviderForMarket(weth.address, token.address).callAsync(); + expect(actualAddress).to.equal(expectedAddress); + }); + it('can effectively remove a liquidity provider for a market by setting the address to 0', async () => { + await feature + .setLiquidityProviderForMarket(token.address, weth.address, constants.NULL_ADDRESS) + .awaitTransactionSuccessAsync(); + const tx = feature + .getLiquidityProviderForMarket(token.address, weth.address) + .awaitTransactionSuccessAsync(); + return expect(tx).to.revertWith( + new ZeroExRevertErrors.LiquidityProvider.NoLiquidityProviderForMarketError(token.address, weth.address), + ); + }); + it('reverts if non-owner attempts to set an address', async () => { + const tx = feature + .setLiquidityProviderForMarket(randomAddress(), randomAddress(), randomAddress()) + .awaitTransactionSuccessAsync({ from: taker }); + return expect(tx).to.revertWith(new OwnableRevertErrors.OnlyOwnerError(taker, owner)); + }); }); - describe('Swap', () => { + blockchainTests.resets('Swap', () => { + let liquidityProvider: TestBridgeContract; + const ETH_TOKEN_ADDRESS = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'; + before(async () => { + liquidityProvider = await TestBridgeContract.deployFrom0xArtifactAsync( + artifacts.TestBridge, + env.provider, + env.txDefaults, + artifacts, + token.address, + weth.address, + ); + await feature + .setLiquidityProviderForMarket(token.address, weth.address, liquidityProvider.address) + .awaitTransactionSuccessAsync(); + }); + it('Cannot execute a swap for a market without a liquidity provider set', async () => { + const [xAsset, yAsset] = [randomAddress(), randomAddress()]; + const tx = feature + .sellToLiquidityProvider( + xAsset, + yAsset, + constants.NULL_ADDRESS, + constants.ONE_ETHER, + constants.ZERO_AMOUNT, + ) + .awaitTransactionSuccessAsync({ from: taker }); + return expect(tx).to.revertWith( + new ZeroExRevertErrors.LiquidityProvider.NoLiquidityProviderForMarketError(xAsset, yAsset), + ); + }); + it('Successfully executes an ERC20-ERC20 swap', async () => { + const tx = await feature + .sellToLiquidityProvider( + weth.address, + token.address, + constants.NULL_ADDRESS, + constants.ONE_ETHER, + constants.ZERO_AMOUNT, + ) + .awaitTransactionSuccessAsync({ from: taker }); + verifyEventsFromLogs( + tx.logs, + [ + { + inputToken: token.address, + outputToken: weth.address, + inputTokenAmount: constants.ONE_ETHER, + outputTokenAmount: constants.ZERO_AMOUNT, + from: constants.NULL_ADDRESS, + to: taker, + }, + ], + IERC20BridgeEvents.ERC20BridgeTransfer, + ); + }); + it('Reverts if cannot fulfill the minimum buy amount', async () => { + const minBuyAmount = new BigNumber(1); + const tx = feature + .sellToLiquidityProvider( + weth.address, + token.address, + constants.NULL_ADDRESS, + constants.ONE_ETHER, + minBuyAmount, + ) + .awaitTransactionSuccessAsync({ from: taker }); + return expect(tx).to.revertWith( + new ZeroExRevertErrors.LiquidityProvider.LiquidityProviderIncompleteSellError( + liquidityProvider.address, + weth.address, + token.address, + constants.ONE_ETHER, + constants.ZERO_AMOUNT, + minBuyAmount, + ), + ); + }); + it('Successfully executes an ETH-ERC20 swap', async () => { + const tx = await feature + .sellToLiquidityProvider( + token.address, + ETH_TOKEN_ADDRESS, + constants.NULL_ADDRESS, + constants.ONE_ETHER, + constants.ZERO_AMOUNT, + ) + .awaitTransactionSuccessAsync({ from: taker, value: constants.ONE_ETHER }); + verifyEventsFromLogs( + tx.logs, + [ + { + inputToken: weth.address, + outputToken: token.address, + inputTokenAmount: constants.ONE_ETHER, + outputTokenAmount: constants.ZERO_AMOUNT, + from: constants.NULL_ADDRESS, + to: taker, + }, + ], + IERC20BridgeEvents.ERC20BridgeTransfer, + ); + }); + it('Successfully executes an ERC20-ETH swap', async () => { + const tx = await feature + .sellToLiquidityProvider( + ETH_TOKEN_ADDRESS, + token.address, + constants.NULL_ADDRESS, + constants.ONE_ETHER, + constants.ZERO_AMOUNT, + ) + .awaitTransactionSuccessAsync({ from: taker }); + verifyEventsFromLogs( + tx.logs, + [ + { + inputToken: token.address, + outputToken: weth.address, + inputTokenAmount: constants.ONE_ETHER, + outputTokenAmount: constants.ZERO_AMOUNT, + from: constants.NULL_ADDRESS, + to: zeroEx.address, + }, + ], + IERC20BridgeEvents.ERC20BridgeTransfer, + ); + }); }); }); diff --git a/contracts/zero-ex/test/wrappers.ts b/contracts/zero-ex/test/wrappers.ts index 1aa12ac0c7..76dac9c6b4 100644 --- a/contracts/zero-ex/test/wrappers.ts +++ b/contracts/zero-ex/test/wrappers.ts @@ -73,6 +73,7 @@ export * from '../test/generated-wrappers/ownable_feature'; export * from '../test/generated-wrappers/pay_taker_transformer'; export * from '../test/generated-wrappers/signature_validator_feature'; export * from '../test/generated-wrappers/simple_function_registry_feature'; +export * from '../test/generated-wrappers/test_bridge'; export * from '../test/generated-wrappers/test_call_target'; export * from '../test/generated-wrappers/test_delegate_caller'; export * from '../test/generated-wrappers/test_fill_quote_transformer_bridge'; diff --git a/contracts/zero-ex/tsconfig.json b/contracts/zero-ex/tsconfig.json index 0f0163243e..4cc0fb27af 100644 --- a/contracts/zero-ex/tsconfig.json +++ b/contracts/zero-ex/tsconfig.json @@ -97,6 +97,7 @@ "test/generated-artifacts/PayTakerTransformer.json", "test/generated-artifacts/SignatureValidatorFeature.json", "test/generated-artifacts/SimpleFunctionRegistryFeature.json", + "test/generated-artifacts/TestBridge.json", "test/generated-artifacts/TestCallTarget.json", "test/generated-artifacts/TestDelegateCaller.json", "test/generated-artifacts/TestFillQuoteTransformerBridge.json", diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 52c2565aa3..74c518ddc4 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -54,4 +54,5 @@ export const ZeroExRevertErrors = { Wallet: require('./revert_errors/zero-ex/wallet_revert_errors'), MetaTransactions: require('./revert_errors/zero-ex/meta_transaction_revert_errors'), SignatureValidator: require('./revert_errors/zero-ex/signature_validator_revert_errors'), + LiquidityProvider: require('./revert_errors/zero-ex/liquidity_provider_revert_errors'), }; diff --git a/packages/utils/src/revert_errors/zero-ex/liquidity_provider_revert_errors.ts b/packages/utils/src/revert_errors/zero-ex/liquidity_provider_revert_errors.ts new file mode 100644 index 0000000000..5adafe0127 --- /dev/null +++ b/packages/utils/src/revert_errors/zero-ex/liquidity_provider_revert_errors.ts @@ -0,0 +1,47 @@ +import { RevertError } from '../../revert_error'; +import { Numberish } from '../../types'; + +// tslint:disable:max-classes-per-file +export class LiquidityProviderIncompleteSellError extends RevertError { + constructor( + providerAddress?: string, + makerToken?: string, + takerToken?: string, + sellAmount?: Numberish, + boughtAmount?: Numberish, + minBuyAmount?: Numberish, + ) { + super( + 'LiquidityProviderIncompleteSellError', + 'LiquidityProviderIncompleteSellError(address providerAddress, address makerToken, address takerToken, uint256 sellAmount, uint256 boughtAmount, uint256 minBuyAmount)', + { + providerAddress, + makerToken, + takerToken, + sellAmount, + boughtAmount, + minBuyAmount, + }, + ); + } +} + +export class NoLiquidityProviderForMarketError extends RevertError { + constructor(xAsset?: string, yAsset?: string) { + super( + 'NoLiquidityProviderForMarketError', + 'NoLiquidityProviderForMarketError(address xAsset, address yAsset)', + { + xAsset, + yAsset, + }, + ); + } +} + +const types = [LiquidityProviderIncompleteSellError, NoLiquidityProviderForMarketError]; + +// Register the types we've defined. +for (const type of types) { + RevertError.registerType(type); +} From 12ff4ec43863e8686cd985d4f1f95dc5d2fa2e14 Mon Sep 17 00:00:00 2001 From: Michael Zhu Date: Thu, 10 Sep 2020 11:14:27 -0700 Subject: [PATCH 15/32] Add tests for exchange proxy gas overhead accounting in the path optimizer --- .../src/utils/market_operation_utils/index.ts | 2 + .../test/market_operation_utils_test.ts | 86 +++++++++++++++++++ 2 files changed, 88 insertions(+) diff --git a/packages/asset-swapper/src/utils/market_operation_utils/index.ts b/packages/asset-swapper/src/utils/market_operation_utils/index.ts index 998099b140..ecfb7b6a47 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/index.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/index.ts @@ -367,6 +367,7 @@ export class MarketOperationUtils { maxFallbackSlippage: _opts.maxFallbackSlippage, excludedSources: _opts.excludedSources, feeSchedule: _opts.feeSchedule, + exchangeProxyOverhead: _opts.exchangeProxyOverhead, allowFallback: _opts.allowFallback, shouldBatchBridgeOrders: _opts.shouldBatchBridgeOrders, }); @@ -404,6 +405,7 @@ export class MarketOperationUtils { maxFallbackSlippage: _opts.maxFallbackSlippage, excludedSources: _opts.excludedSources, feeSchedule: _opts.feeSchedule, + exchangeProxyOverhead: _opts.exchangeProxyOverhead, allowFallback: _opts.allowFallback, shouldBatchBridgeOrders: _opts.shouldBatchBridgeOrders, }); diff --git a/packages/asset-swapper/test/market_operation_utils_test.ts b/packages/asset-swapper/test/market_operation_utils_test.ts index 1e3323c97a..f00c2736bf 100644 --- a/packages/asset-swapper/test/market_operation_utils_test.ts +++ b/packages/asset-swapper/test/market_operation_utils_test.ts @@ -22,6 +22,7 @@ import { BUY_SOURCE_FILTER, POSITIVE_INF, SELL_SOURCE_FILTER, + SOURCE_FLAGS, ZERO_AMOUNT, } from '../src/utils/market_operation_utils/constants'; import { createFills } from '../src/utils/market_operation_utils/fills'; @@ -930,6 +931,49 @@ describe('MarketOperationUtils tests', () => { [ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Curve], ]); }); + it('factors in exchange proxy gas overhead', async () => { + // Uniswap has a slightly better rate than LiquidityProvider, + // but LiquidityProvider is better accounting for the EP gas overhead. + const rates: RatesBySource = { + [ERC20BridgeSource.Native]: [0.01, 0.01, 0.01, 0.01], + [ERC20BridgeSource.Uniswap]: [1, 1, 1, 1], + [ERC20BridgeSource.LiquidityProvider]: [0.9999, 0.9999, 0.9999, 0.9999], + }; + replaceSamplerOps({ + getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates), + getMedianSellRate: createGetMedianSellRate(ETH_TO_MAKER_RATE), + }); + const optimizer = new MarketOperationUtils( + MOCK_SAMPLER, + contractAddresses, + ORDER_DOMAIN, + randomAddress(), // liquidity provider registry + ); + const gasPrice = 100e9; // 100 gwei + const exchangeProxyOverhead = (sourceFlags: number) => + sourceFlags === SOURCE_FLAGS.LiquidityProvider + ? new BigNumber(3e4).times(gasPrice) + : new BigNumber(1.3e5).times(gasPrice); + const improvedOrdersResponse = await optimizer.getMarketSellOrdersAsync( + createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), + FILL_AMOUNT, + { + ...DEFAULT_OPTS, + numSamples: 4, + excludedSources: [ + ...DEFAULT_OPTS.excludedSources, + ERC20BridgeSource.Eth2Dai, + ERC20BridgeSource.Kyber, + ERC20BridgeSource.Bancor, + ], + exchangeProxyOverhead, + }, + ); + const improvedOrders = improvedOrdersResponse.optimizedOrders; + const orderSources = improvedOrders.map(o => o.fills[0].source); + const expectedSources = [ERC20BridgeSource.LiquidityProvider]; + expect(orderSources).to.deep.eq(expectedSources); + }); }); describe('getMarketBuyOrdersAsync()', () => { @@ -1322,6 +1366,48 @@ describe('MarketOperationUtils tests', () => { [ERC20BridgeSource.Native], ]); }); + it('factors in exchange proxy gas overhead', async () => { + // Uniswap has a slightly better rate than LiquidityProvider, + // but LiquidityProvider is better accounting for the EP gas overhead. + const rates: RatesBySource = { + [ERC20BridgeSource.Native]: [0.01, 0.01, 0.01, 0.01], + [ERC20BridgeSource.Uniswap]: [1, 1, 1, 1], + [ERC20BridgeSource.LiquidityProvider]: [0.9999, 0.9999, 0.9999, 0.9999], + }; + replaceSamplerOps({ + getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates), + getMedianSellRate: createGetMedianSellRate(ETH_TO_TAKER_RATE), + }); + const optimizer = new MarketOperationUtils( + MOCK_SAMPLER, + contractAddresses, + ORDER_DOMAIN, + randomAddress(), // liquidity provider registry + ); + const gasPrice = 100e9; // 100 gwei + const exchangeProxyOverhead = (sourceFlags: number) => + sourceFlags === SOURCE_FLAGS.LiquidityProvider + ? new BigNumber(3e4).times(gasPrice) + : new BigNumber(1.3e5).times(gasPrice); + const improvedOrdersResponse = await optimizer.getMarketBuyOrdersAsync( + createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), + FILL_AMOUNT, + { + ...DEFAULT_OPTS, + numSamples: 4, + excludedSources: [ + ...DEFAULT_OPTS.excludedSources, + ERC20BridgeSource.Eth2Dai, + ERC20BridgeSource.Kyber, + ], + exchangeProxyOverhead, + }, + ); + const improvedOrders = improvedOrdersResponse.optimizedOrders; + const orderSources = improvedOrders.map(o => o.fills[0].source); + const expectedSources = [ERC20BridgeSource.LiquidityProvider]; + expect(orderSources).to.deep.eq(expectedSources); + }); }); }); From 290a04a0ad22154c6291eddda2c56183417cf163 Mon Sep 17 00:00:00 2001 From: Michael Zhu Date: Thu, 10 Sep 2020 11:42:40 -0700 Subject: [PATCH 16/32] Appease linter --- contracts/zero-ex/package.json | 1 + .../utils/market_operation_utils/constants.ts | 2 +- .../src/utils/market_operation_utils/fills.ts | 8 ++++---- .../src/utils/market_operation_utils/index.ts | 2 +- .../src/utils/market_operation_utils/path.ts | 16 ++++++++-------- .../market_operation_utils/path_optimizer.ts | 4 ++-- .../utils/market_operation_utils/rate_utils.ts | 11 +++++++++++ .../src/utils/swap_quote_calculator.ts | 2 +- 8 files changed, 29 insertions(+), 17 deletions(-) diff --git a/contracts/zero-ex/package.json b/contracts/zero-ex/package.json index 55d941e368..ca8e98467f 100644 --- a/contracts/zero-ex/package.json +++ b/contracts/zero-ex/package.json @@ -55,6 +55,7 @@ "devDependencies": { "@0x/abi-gen": "^5.3.1", "@0x/contracts-gen": "^2.0.10", + "@0x/contracts-erc20": "^3.2.1", "@0x/contracts-test-utils": "^5.3.4", "@0x/dev-utils": "^3.3.0", "@0x/order-utils": "^10.3.0", diff --git a/packages/asset-swapper/src/utils/market_operation_utils/constants.ts b/packages/asset-swapper/src/utils/market_operation_utils/constants.ts index 8b4c7dd2e5..ce48b44fb4 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/constants.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/constants.ts @@ -3,7 +3,7 @@ import { BigNumber } from '@0x/utils'; import { SourceFilters } from './source_filters'; import { CurveFunctionSelectors, CurveInfo, ERC20BridgeSource, GetMarketOrdersOpts } from './types'; -// tslint:disable: custom-no-magic-numbers +// tslint:disable: custom-no-magic-numbers no-bitwise /** * Valid sources for market sell. diff --git a/packages/asset-swapper/src/utils/market_operation_utils/fills.ts b/packages/asset-swapper/src/utils/market_operation_utils/fills.ts index 24ce21bbec..4806c71c09 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/fills.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/fills.ts @@ -143,10 +143,10 @@ function dexSamplesToFills( // We need not worry about Kyber fills going to UniswapReserve as the input amount // we fill is the same as we sampled. I.e we received [0,20,30] output from [1,2,3] input // and we only fill [2,3] on Kyber (as 1 returns 0 output) - samples = samples.filter(q => !q.output.isZero()); - for (let i = 0; i < samples.length; i++) { - const sample = samples[i]; - const prevSample = i === 0 ? undefined : samples[i - 1]; + const nonzeroSamples = samples.filter(q => !q.output.isZero()); + for (let i = 0; i < nonzeroSamples.length; i++) { + const sample = nonzeroSamples[i]; + const prevSample = i === 0 ? undefined : nonzeroSamples[i - 1]; const { source, fillData } = sample; const input = sample.input.minus(prevSample ? prevSample.input : 0); const output = sample.output.minus(prevSample ? prevSample.output : 0); diff --git a/packages/asset-swapper/src/utils/market_operation_utils/index.ts b/packages/asset-swapper/src/utils/market_operation_utils/index.ts index ecfb7b6a47..603bfe982a 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/index.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/index.ts @@ -579,7 +579,7 @@ export class MarketOperationUtils { ethToInputRate, exchangeProxyOverhead: opts.exchangeProxyOverhead || (() => ZERO_AMOUNT), }; - let optimalPath = await findOptimalPathAsync(side, fills, inputAmount, opts.runLimit, optimizerOpts); + const optimalPath = await findOptimalPathAsync(side, fills, inputAmount, opts.runLimit, optimizerOpts); if (optimalPath === undefined) { throw new Error(AggregationError.NoOptimalPath); } diff --git a/packages/asset-swapper/src/utils/market_operation_utils/path.ts b/packages/asset-swapper/src/utils/market_operation_utils/path.ts index 89333f6925..e75e589081 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/path.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/path.ts @@ -46,13 +46,6 @@ export class Path { protected _size: PathSize = { input: ZERO_AMOUNT, output: ZERO_AMOUNT }; protected _adjustedSize: PathSize = { input: ZERO_AMOUNT, output: ZERO_AMOUNT }; - protected constructor( - protected readonly side: MarketOperation, - public fills: ReadonlyArray, - protected readonly targetInput: BigNumber, - public readonly pathPenaltyOpts: PathPenaltyOpts, - ) {} - public static create( side: MarketOperation, fills: ReadonlyArray, @@ -77,6 +70,13 @@ export class Path { return clonedPath; } + protected constructor( + protected readonly side: MarketOperation, + public fills: ReadonlyArray, + protected readonly targetInput: BigNumber, + public readonly pathPenaltyOpts: PathPenaltyOpts, + ) {} + public append(fill: Fill): this { (this.fills as Fill[]).push(fill); this.sourceFlags |= fill.flags; @@ -101,7 +101,7 @@ export class Path { const nativeFills = this.fills.filter(f => f.source === ERC20BridgeSource.Native); this.fills = [...nativeFills.filter(f => f !== lastNativeFillIfExists), ...fallback.fills]; // Recompute the source flags - this.sourceFlags = this.fills.reduce((flags, fill) => (flags |= fill.flags), 0); + this.sourceFlags = this.fills.reduce((flags, fill) => flags | fill.flags, 0); return this; } diff --git a/packages/asset-swapper/src/utils/market_operation_utils/path_optimizer.ts b/packages/asset-swapper/src/utils/market_operation_utils/path_optimizer.ts index 31244d2299..5f0ce4b679 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/path_optimizer.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/path_optimizer.ts @@ -43,7 +43,7 @@ function mixPaths( pathB: Path, targetInput: BigNumber, maxSteps: number, - rateBySourcePathId: { [id: string]: BigNumber }, + rates: { [id: string]: BigNumber }, ): Path { const _maxSteps = Math.max(maxSteps, 32); let steps = 0; @@ -76,7 +76,7 @@ function mixPaths( // chances of walking ideal, valid paths first. const sortedFills = allFills.sort((a, b) => { if (a.sourcePathId !== b.sourcePathId) { - return rateBySourcePathId[b.sourcePathId].comparedTo(rateBySourcePathId[a.sourcePathId]); + return rates[b.sourcePathId].comparedTo(rates[a.sourcePathId]); } return a.index - b.index; }); diff --git a/packages/asset-swapper/src/utils/market_operation_utils/rate_utils.ts b/packages/asset-swapper/src/utils/market_operation_utils/rate_utils.ts index 713972cb33..af6af94eef 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/rate_utils.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/rate_utils.ts @@ -5,6 +5,10 @@ import { MarketOperation } from '../../types'; import { ZERO_AMOUNT } from './constants'; import { DexSample, ERC20BridgeSource, FeeSchedule, MultiHopFillData } from './types'; +/** + * Returns the fee-adjusted rate of a two-hop quote. Returns zero if the + * quote falls short of the target input. + */ export function getTwoHopAdjustedRate( side: MarketOperation, twoHopQuote: DexSample, @@ -21,6 +25,10 @@ export function getTwoHopAdjustedRate( return side === MarketOperation.Sell ? adjustedOutput.div(input) : input.div(adjustedOutput); } +/** + * Computes the "complete" rate given the input/output of a path. + * This value penalizes the path if it falls short of the target input. + */ export function getCompleteRate( side: MarketOperation, input: BigNumber, @@ -40,6 +48,9 @@ export function getCompleteRate( return input.div(output).times(input.div(targetInput)); } +/** + * Computes the rate given the input/output of a path. + */ export function getRate(side: MarketOperation, input: BigNumber, output: BigNumber): BigNumber { if (input.eq(0) || output.eq(0)) { return ZERO_AMOUNT; diff --git a/packages/asset-swapper/src/utils/swap_quote_calculator.ts b/packages/asset-swapper/src/utils/swap_quote_calculator.ts index 3f4e56eb10..9ed44ba2c2 100644 --- a/packages/asset-swapper/src/utils/swap_quote_calculator.ts +++ b/packages/asset-swapper/src/utils/swap_quote_calculator.ts @@ -139,7 +139,7 @@ export class SwapQuoteCalculator { feeSchedule: _.mapValues(opts.feeSchedule, gasCost => (fillData?: FillData) => gasCost === undefined ? 0 : gasPrice.times(gasCost(fillData)), ), - exchangeProxyOverhead: (sourceFlags: number) => gasPrice.times(opts.exchangeProxyOverhead!(sourceFlags)), + exchangeProxyOverhead: flags => gasPrice.times(opts.exchangeProxyOverhead(flags)), }; const firstOrderMakerAssetData = !!prunedOrders[0] From 46e512a27c17348036bdcf3df2c3bf9ff72aa833 Mon Sep 17 00:00:00 2001 From: Michael Zhu Date: Thu, 10 Sep 2020 13:45:33 -0700 Subject: [PATCH 17/32] Fix test-doc-generation --- packages/asset-swapper/src/index.ts | 1 + packages/contract-wrappers/src/index.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/asset-swapper/src/index.ts b/packages/asset-swapper/src/index.ts index 79e86a3f75..5f0cf98d06 100644 --- a/packages/asset-swapper/src/index.ts +++ b/packages/asset-swapper/src/index.ts @@ -134,6 +134,7 @@ export { CurveInfo, DexSample, ERC20BridgeSource, + ExchangeProxyOverhead, FeeSchedule, Fill, FillData, diff --git a/packages/contract-wrappers/src/index.ts b/packages/contract-wrappers/src/index.ts index f4025e4d01..1f19988892 100644 --- a/packages/contract-wrappers/src/index.ts +++ b/packages/contract-wrappers/src/index.ts @@ -125,6 +125,7 @@ export { IZeroExContract, IZeroExEventArgs, IZeroExEvents, + IZeroExLiquidityProviderForMarketUpdatedEventArgs, IZeroExMetaTransactionExecutedEventArgs, IZeroExMigratedEventArgs, IZeroExOwnershipTransferredEventArgs, From 961273a2ff3ae3acf81a73e1e7de07905526b403 Mon Sep 17 00:00:00 2001 From: Michael Zhu Date: Mon, 21 Sep 2020 21:27:37 -0700 Subject: [PATCH 18/32] Fix broken test --- packages/asset-swapper/test/market_operation_utils_test.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/asset-swapper/test/market_operation_utils_test.ts b/packages/asset-swapper/test/market_operation_utils_test.ts index f00c2736bf..688c1bcc39 100644 --- a/packages/asset-swapper/test/market_operation_utils_test.ts +++ b/packages/asset-swapper/test/market_operation_utils_test.ts @@ -265,11 +265,7 @@ describe('MarketOperationUtils tests', () => { 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. + // Sort fills by descending rate. .map(o => { return o.fills .slice() From 33caae705eeb4de36ff8c13a5f149e175593fbf6 Mon Sep 17 00:00:00 2001 From: Michael Zhu Date: Mon, 28 Sep 2020 15:43:41 -0700 Subject: [PATCH 19/32] Update changelogs --- contracts/zero-ex/CHANGELOG.json | 4 ++++ packages/asset-swapper/CHANGELOG.json | 4 ++++ packages/contract-artifacts/CHANGELOG.json | 4 ++++ packages/contract-wrappers/CHANGELOG.json | 4 ++++ packages/utils/CHANGELOG.json | 4 ++++ 5 files changed, 20 insertions(+) diff --git a/contracts/zero-ex/CHANGELOG.json b/contracts/zero-ex/CHANGELOG.json index 7a2ccc6a7c..dd950575f6 100644 --- a/contracts/zero-ex/CHANGELOG.json +++ b/contracts/zero-ex/CHANGELOG.json @@ -57,6 +57,10 @@ { "note": "Fix versioning (`_encodeVersion()`) bug", "pr": 2703 + }, + { + "note": "Added LiquidityProviderFeature", + "pr": 2691 } ] }, diff --git a/packages/asset-swapper/CHANGELOG.json b/packages/asset-swapper/CHANGELOG.json index cc4d84e97a..71d531e7c1 100644 --- a/packages/asset-swapper/CHANGELOG.json +++ b/packages/asset-swapper/CHANGELOG.json @@ -133,6 +133,10 @@ { "note": "Respect max slippage in EP consumer", "pr": 2712 + }, + { + "note": "Introduced Path class, exchangeProxyOverhead parameter", + "pr": 2691 } ] }, diff --git a/packages/contract-artifacts/CHANGELOG.json b/packages/contract-artifacts/CHANGELOG.json index 096af3b0fe..7702754f78 100644 --- a/packages/contract-artifacts/CHANGELOG.json +++ b/packages/contract-artifacts/CHANGELOG.json @@ -17,6 +17,10 @@ { "note": "Regenerate artifacts", "pr": 2703 + }, + { + "note": "Update IZeroEx artifact for LiquidityProviderFeature", + "pr": 2691 } ] }, diff --git a/packages/contract-wrappers/CHANGELOG.json b/packages/contract-wrappers/CHANGELOG.json index 80deb3687a..dffa364b75 100644 --- a/packages/contract-wrappers/CHANGELOG.json +++ b/packages/contract-wrappers/CHANGELOG.json @@ -17,6 +17,10 @@ { "note": "Regenerate wrappers", "pr": 2703 + }, + { + "note": "Update IZeroEx wrapper for LiquidityProviderFeature", + "pr": 2691 } ] }, diff --git a/packages/utils/CHANGELOG.json b/packages/utils/CHANGELOG.json index 2ccbfca7dc..eda013cdf4 100644 --- a/packages/utils/CHANGELOG.json +++ b/packages/utils/CHANGELOG.json @@ -9,6 +9,10 @@ { "note": "Add EP flavor of `IllegalReentrancyError`.", "pr": 2657 + }, + { + "note": "Added LiquidityProviderFeature errors", + "pr": 2691 } ] }, From 31e90e314c041d4e093df704d4a17136911bf6f1 Mon Sep 17 00:00:00 2001 From: Michael Zhu Date: Mon, 5 Oct 2020 10:14:10 -0700 Subject: [PATCH 20/32] lint --- .../asset-swapper/src/utils/market_operation_utils/index.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/asset-swapper/src/utils/market_operation_utils/index.ts b/packages/asset-swapper/src/utils/market_operation_utils/index.ts index 603bfe982a..995933fcbe 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/index.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/index.ts @@ -30,6 +30,7 @@ import { DexOrderSampler, getSampleAmounts } from './sampler'; import { SourceFilters } from './source_filters'; import { AggregationError, + CollapsedFill, DexSample, ERC20BridgeSource, ExchangeProxyOverhead, @@ -617,9 +618,9 @@ export class MarketOperationUtils { const collapsedPath = optimalPath.collapse(orderOpts); return { optimizedOrders: collapsedPath.orders, - liquidityDelivered: collapsedPath.collapsedFills, + liquidityDelivered: collapsedPath.collapsedFills as CollapsedFill[], sourceFlags: collapsedPath.sourceFlags, - } as OptimizerResult; + }; } } From 86ff5c53bb9e0a9ddc1250386af783838c0e4e24 Mon Sep 17 00:00:00 2001 From: Michael Zhu Date: Mon, 5 Oct 2020 17:33:13 -0700 Subject: [PATCH 21/32] account for EP gas overhead in multihop --- .../src/utils/market_operation_utils/index.ts | 1 + .../market_operation_utils/multihop_utils.ts | 23 ++++++++++++++++--- .../market_operation_utils/rate_utils.ts | 9 +++++--- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/packages/asset-swapper/src/utils/market_operation_utils/index.ts b/packages/asset-swapper/src/utils/market_operation_utils/index.ts index 995933fcbe..38c10a1f26 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/index.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/index.ts @@ -589,6 +589,7 @@ export class MarketOperationUtils { const { adjustedRate: bestTwoHopRate, quote: bestTwoHopQuote } = getBestTwoHopQuote( marketSideLiquidity, opts.feeSchedule, + opts.exchangeProxyOverhead, ); if (bestTwoHopQuote && bestTwoHopRate.isGreaterThan(optimalPathRate)) { const twoHopOrders = createOrdersFromTwoHopSample(bestTwoHopQuote, orderOpts); diff --git a/packages/asset-swapper/src/utils/market_operation_utils/multihop_utils.ts b/packages/asset-swapper/src/utils/market_operation_utils/multihop_utils.ts index 146f0b7826..9d6832b858 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/multihop_utils.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/multihop_utils.ts @@ -3,7 +3,14 @@ import * as _ from 'lodash'; import { ZERO_AMOUNT } from './constants'; import { getTwoHopAdjustedRate } from './rate_utils'; -import { DexSample, FeeSchedule, MarketSideLiquidity, MultiHopFillData, TokenAdjacencyGraph } from './types'; +import { + DexSample, + ExchangeProxyOverhead, + FeeSchedule, + MarketSideLiquidity, + MultiHopFillData, + TokenAdjacencyGraph, +} from './types'; /** * Given a token pair, returns the intermediate tokens to consider for two-hop routes. @@ -36,18 +43,28 @@ export function getIntermediateTokens( export function getBestTwoHopQuote( marketSideLiquidity: MarketSideLiquidity, feeSchedule?: FeeSchedule, + exchangeProxyOverhead?: ExchangeProxyOverhead, ): { quote: DexSample | undefined; adjustedRate: BigNumber } { const { side, inputAmount, ethToOutputRate, twoHopQuotes } = marketSideLiquidity; if (twoHopQuotes.length === 0) { return { adjustedRate: ZERO_AMOUNT, quote: undefined }; } const best = twoHopQuotes - .map(quote => getTwoHopAdjustedRate(side, quote, inputAmount, ethToOutputRate, feeSchedule)) + .map(quote => + getTwoHopAdjustedRate(side, quote, inputAmount, ethToOutputRate, feeSchedule, exchangeProxyOverhead), + ) .reduce( (prev, curr, i) => curr.isGreaterThan(prev.adjustedRate) ? { adjustedRate: curr, quote: twoHopQuotes[i] } : prev, { - adjustedRate: getTwoHopAdjustedRate(side, twoHopQuotes[0], inputAmount, ethToOutputRate, feeSchedule), + adjustedRate: getTwoHopAdjustedRate( + side, + twoHopQuotes[0], + inputAmount, + ethToOutputRate, + feeSchedule, + exchangeProxyOverhead, + ), quote: twoHopQuotes[0], }, ); diff --git a/packages/asset-swapper/src/utils/market_operation_utils/rate_utils.ts b/packages/asset-swapper/src/utils/market_operation_utils/rate_utils.ts index af6af94eef..43d007b623 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/rate_utils.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/rate_utils.ts @@ -2,8 +2,8 @@ import { BigNumber } from '@0x/utils'; import { MarketOperation } from '../../types'; -import { ZERO_AMOUNT } from './constants'; -import { DexSample, ERC20BridgeSource, FeeSchedule, MultiHopFillData } from './types'; +import { SOURCE_FLAGS, ZERO_AMOUNT } from './constants'; +import { DexSample, ERC20BridgeSource, ExchangeProxyOverhead, FeeSchedule, MultiHopFillData } from './types'; /** * Returns the fee-adjusted rate of a two-hop quote. Returns zero if the @@ -15,12 +15,15 @@ export function getTwoHopAdjustedRate( targetInput: BigNumber, ethToOutputRate: BigNumber, fees: FeeSchedule = {}, + exchangeProxyOverhead: ExchangeProxyOverhead = () => ZERO_AMOUNT, ): BigNumber { const { output, input, fillData } = twoHopQuote; if (input.isLessThan(targetInput) || output.isZero()) { return ZERO_AMOUNT; } - const penalty = ethToOutputRate.times(fees[ERC20BridgeSource.MultiHop]!(fillData)); + const penalty = ethToOutputRate.times( + exchangeProxyOverhead(SOURCE_FLAGS.MultiHop).plus(fees[ERC20BridgeSource.MultiHop]!(fillData)), + ); const adjustedOutput = side === MarketOperation.Sell ? output.minus(penalty) : output.plus(penalty); return side === MarketOperation.Sell ? adjustedOutput.div(input) : input.div(adjustedOutput); } From 7698f215175746fcf098f23819567a82223655aa Mon Sep 17 00:00:00 2001 From: Jacob Evans Date: Tue, 6 Oct 2020 15:55:51 +1000 Subject: [PATCH 22/32] =?UTF-8?q?feat:=20[asset-swapper]=20Shell=20=20?= =?UTF-8?q?=F0=9F=90=9A=20(#2722)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: [asset-swapper] Shell * Deployed and Changelogs * Glue up the deployed address * Glue for FQT rollup --- contracts/asset-proxy/CHANGELOG.json | 4 + .../contracts/src/bridges/ShellBridge.sol | 96 +++++++++++++++ .../contracts/src/interfaces/IShell.sol | 34 ++++++ contracts/asset-proxy/package.json | 2 +- contracts/asset-proxy/src/artifacts.ts | 4 + contracts/asset-proxy/src/wrappers.ts | 2 + contracts/asset-proxy/test/artifacts.ts | 4 + contracts/asset-proxy/test/wrappers.ts | 2 + contracts/asset-proxy/tsconfig.json | 4 + .../contracts/src/DeploymentConstants.sol | 12 ++ contracts/zero-ex/CHANGELOG.json | 4 + .../transformers/bridges/BridgeAdapter.sol | 11 ++ .../bridges/mixins/MixinAdapterAddresses.sol | 2 + .../bridges/mixins/MixinShell.sol | 84 +++++++++++++ contracts/zero-ex/package.json | 2 +- contracts/zero-ex/test/artifacts.ts | 2 + .../fill_quote_transformer_test.ts | 2 + contracts/zero-ex/test/wrappers.ts | 1 + contracts/zero-ex/tsconfig.json | 1 + packages/asset-swapper/CHANGELOG.json | 4 + .../contracts/src/ERC20BridgeSampler.sol | 2 + .../contracts/src/ShellSampler.sol | 110 ++++++++++++++++++ .../contracts/src/interfaces/IShell.sol | 42 +++++++ packages/asset-swapper/package.json | 2 +- .../utils/market_operation_utils/constants.ts | 2 + .../utils/market_operation_utils/orders.ts | 2 + .../sampler_operations.ts | 30 +++++ .../src/utils/market_operation_utils/types.ts | 1 + packages/asset-swapper/test/artifacts.ts | 4 + .../test/market_operation_utils_test.ts | 5 + packages/asset-swapper/test/wrappers.ts | 2 + packages/asset-swapper/tsconfig.json | 2 + packages/contract-addresses/CHANGELOG.json | 4 + packages/contract-addresses/addresses.json | 5 + packages/contract-addresses/src/index.ts | 1 + packages/migrations/src/migration.ts | 3 + 36 files changed, 491 insertions(+), 3 deletions(-) create mode 100644 contracts/asset-proxy/contracts/src/bridges/ShellBridge.sol create mode 100644 contracts/asset-proxy/contracts/src/interfaces/IShell.sol create mode 100644 contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinShell.sol create mode 100644 packages/asset-swapper/contracts/src/ShellSampler.sol create mode 100644 packages/asset-swapper/contracts/src/interfaces/IShell.sol diff --git a/contracts/asset-proxy/CHANGELOG.json b/contracts/asset-proxy/CHANGELOG.json index 61ddbe1aaa..9e56655e4c 100644 --- a/contracts/asset-proxy/CHANGELOG.json +++ b/contracts/asset-proxy/CHANGELOG.json @@ -25,6 +25,10 @@ { "note": "Reworked `KyberBridge`", "pr": 2683 + }, + { + "note": "Added `ShellBridge`", + "pr": 2722 } ] }, diff --git a/contracts/asset-proxy/contracts/src/bridges/ShellBridge.sol b/contracts/asset-proxy/contracts/src/bridges/ShellBridge.sol new file mode 100644 index 0000000000..85f62e2e52 --- /dev/null +++ b/contracts/asset-proxy/contracts/src/bridges/ShellBridge.sol @@ -0,0 +1,96 @@ +/* + + Copyright 2019 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.5.9; +pragma experimental ABIEncoderV2; + +import "@0x/contracts-erc20/contracts/src/interfaces/IERC20Token.sol"; +import "@0x/contracts-erc20/contracts/src/LibERC20Token.sol"; +import "@0x/contracts-exchange-libs/contracts/src/IWallet.sol"; +import "@0x/contracts-utils/contracts/src/DeploymentConstants.sol"; +import "../interfaces/IERC20Bridge.sol"; +import "../interfaces/IShell.sol"; + + +contract ShellBridge is + IERC20Bridge, + IWallet, + DeploymentConstants +{ + + /// @dev Swaps specified tokens against the Shell contract + /// @param toTokenAddress The token to give to `to`. + /// @param from The maker (this contract). + /// @param to The recipient of the bought tokens. + /// @param amount Minimum amount of `toTokenAddress` tokens to buy. + /// @param bridgeData The abi-encoded "from" token address. + /// @return success The magic bytes if successful. + // solhint-disable no-unused-vars + function bridgeTransferFrom( + address toTokenAddress, + address from, + address to, + uint256 amount, + bytes calldata bridgeData + ) + external + returns (bytes4 success) + { + // Decode the bridge data to get the `fromTokenAddress`. + (address fromTokenAddress) = abi.decode(bridgeData, (address)); + + uint256 fromTokenBalance = IERC20Token(fromTokenAddress).balanceOf(address(this)); + IShell exchange = IShell(_getShellAddress()); + // Grant an allowance to the exchange to spend `fromTokenAddress` token. + LibERC20Token.approveIfBelow(fromTokenAddress, address(exchange), fromTokenBalance); + + // Try to sell all of this contract's `fromTokenAddress` token balance. + uint256 boughtAmount = exchange.originSwap( + fromTokenAddress, + toTokenAddress, + fromTokenBalance, + amount, // min amount + block.timestamp + 1 + ); + LibERC20Token.transfer(toTokenAddress, to, boughtAmount); + + emit ERC20BridgeTransfer( + fromTokenAddress, + toTokenAddress, + fromTokenBalance, + boughtAmount, + from, + to + ); + return BRIDGE_SUCCESS; + } + + /// @dev `SignatureType.Wallet` callback, so that this bridge can be the maker + /// and sign for itself in orders. Always succeeds. + /// @return magicValue Magic success bytes, always. + function isValidSignature( + bytes32, + bytes calldata + ) + external + view + returns (bytes4 magicValue) + { + return LEGACY_WALLET_MAGIC_VALUE; + } +} diff --git a/contracts/asset-proxy/contracts/src/interfaces/IShell.sol b/contracts/asset-proxy/contracts/src/interfaces/IShell.sol new file mode 100644 index 0000000000..91bc5f6315 --- /dev/null +++ b/contracts/asset-proxy/contracts/src/interfaces/IShell.sol @@ -0,0 +1,34 @@ +/* + + Copyright 2020 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.5.9; + + +interface IShell { + + function originSwap( + address from, + address to, + uint256 fromAmount, + uint256 minTargetAmount, + uint256 deadline + ) + external + returns (uint256 toAmount); +} + diff --git a/contracts/asset-proxy/package.json b/contracts/asset-proxy/package.json index cf4e4f3222..100112bb76 100644 --- a/contracts/asset-proxy/package.json +++ b/contracts/asset-proxy/package.json @@ -38,7 +38,7 @@ "docs:json": "typedoc --excludePrivate --excludeExternals --excludeProtected --ignoreCompilerErrors --target ES5 --tsconfig typedoc-tsconfig.json --json $JSON_FILE_PATH $PROJECT_FILES" }, "config": { - "abis": "./test/generated-artifacts/@(BalancerBridge|BancorBridge|ChaiBridge|CurveBridge|DexForwarderBridge|DydxBridge|ERC1155Proxy|ERC20BridgeProxy|ERC20Proxy|ERC721Proxy|Eth2DaiBridge|IAssetData|IAssetProxy|IAssetProxyDispatcher|IAuthorizable|IBalancerPool|IBancorNetwork|IChai|ICurve|IDydx|IDydxBridge|IERC20Bridge|IEth2Dai|IGasToken|IKyberNetworkProxy|IMStable|IMooniswap|IUniswapExchange|IUniswapExchangeFactory|IUniswapV2Router01|KyberBridge|MStableBridge|MixinAssetProxyDispatcher|MixinAuthorizable|MixinGasToken|MooniswapBridge|MultiAssetProxy|Ownable|StaticCallProxy|SushiSwapBridge|TestBancorBridge|TestChaiBridge|TestDexForwarderBridge|TestDydxBridge|TestERC20Bridge|TestEth2DaiBridge|TestKyberBridge|TestStaticCallTarget|TestUniswapBridge|TestUniswapV2Bridge|UniswapBridge|UniswapV2Bridge).json", + "abis": "./test/generated-artifacts/@(BalancerBridge|BancorBridge|ChaiBridge|CurveBridge|DexForwarderBridge|DydxBridge|ERC1155Proxy|ERC20BridgeProxy|ERC20Proxy|ERC721Proxy|Eth2DaiBridge|IAssetData|IAssetProxy|IAssetProxyDispatcher|IAuthorizable|IBalancerPool|IBancorNetwork|IChai|ICurve|IDydx|IDydxBridge|IERC20Bridge|IEth2Dai|IGasToken|IKyberNetworkProxy|IMStable|IMooniswap|IShell|IUniswapExchange|IUniswapExchangeFactory|IUniswapV2Router01|KyberBridge|MStableBridge|MixinAssetProxyDispatcher|MixinAuthorizable|MixinGasToken|MooniswapBridge|MultiAssetProxy|Ownable|ShellBridge|StaticCallProxy|SushiSwapBridge|TestBancorBridge|TestChaiBridge|TestDexForwarderBridge|TestDydxBridge|TestERC20Bridge|TestEth2DaiBridge|TestKyberBridge|TestStaticCallTarget|TestUniswapBridge|TestUniswapV2Bridge|UniswapBridge|UniswapV2Bridge).json", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually." }, "repository": { diff --git a/contracts/asset-proxy/src/artifacts.ts b/contracts/asset-proxy/src/artifacts.ts index 5385d5d000..8030e115ea 100644 --- a/contracts/asset-proxy/src/artifacts.ts +++ b/contracts/asset-proxy/src/artifacts.ts @@ -32,6 +32,7 @@ import * as IGasToken from '../generated-artifacts/IGasToken.json'; import * as IKyberNetworkProxy from '../generated-artifacts/IKyberNetworkProxy.json'; import * as IMooniswap from '../generated-artifacts/IMooniswap.json'; import * as IMStable from '../generated-artifacts/IMStable.json'; +import * as IShell from '../generated-artifacts/IShell.json'; import * as IUniswapExchange from '../generated-artifacts/IUniswapExchange.json'; import * as IUniswapExchangeFactory from '../generated-artifacts/IUniswapExchangeFactory.json'; import * as IUniswapV2Router01 from '../generated-artifacts/IUniswapV2Router01.json'; @@ -43,6 +44,7 @@ import * as MooniswapBridge from '../generated-artifacts/MooniswapBridge.json'; import * as MStableBridge from '../generated-artifacts/MStableBridge.json'; import * as MultiAssetProxy from '../generated-artifacts/MultiAssetProxy.json'; import * as Ownable from '../generated-artifacts/Ownable.json'; +import * as ShellBridge from '../generated-artifacts/ShellBridge.json'; import * as StaticCallProxy from '../generated-artifacts/StaticCallProxy.json'; import * as SushiSwapBridge from '../generated-artifacts/SushiSwapBridge.json'; import * as TestBancorBridge from '../generated-artifacts/TestBancorBridge.json'; @@ -78,6 +80,7 @@ export const artifacts = { MStableBridge: MStableBridge as ContractArtifact, MixinGasToken: MixinGasToken as ContractArtifact, MooniswapBridge: MooniswapBridge as ContractArtifact, + ShellBridge: ShellBridge as ContractArtifact, SushiSwapBridge: SushiSwapBridge as ContractArtifact, UniswapBridge: UniswapBridge as ContractArtifact, UniswapV2Bridge: UniswapV2Bridge as ContractArtifact, @@ -97,6 +100,7 @@ export const artifacts = { IKyberNetworkProxy: IKyberNetworkProxy as ContractArtifact, IMStable: IMStable as ContractArtifact, IMooniswap: IMooniswap as ContractArtifact, + IShell: IShell as ContractArtifact, IUniswapExchange: IUniswapExchange as ContractArtifact, IUniswapExchangeFactory: IUniswapExchangeFactory as ContractArtifact, IUniswapV2Router01: IUniswapV2Router01 as ContractArtifact, diff --git a/contracts/asset-proxy/src/wrappers.ts b/contracts/asset-proxy/src/wrappers.ts index 8107eaf766..b764ad29d3 100644 --- a/contracts/asset-proxy/src/wrappers.ts +++ b/contracts/asset-proxy/src/wrappers.ts @@ -30,6 +30,7 @@ export * from '../generated-wrappers/i_gas_token'; export * from '../generated-wrappers/i_kyber_network_proxy'; export * from '../generated-wrappers/i_m_stable'; export * from '../generated-wrappers/i_mooniswap'; +export * from '../generated-wrappers/i_shell'; export * from '../generated-wrappers/i_uniswap_exchange'; export * from '../generated-wrappers/i_uniswap_exchange_factory'; export * from '../generated-wrappers/i_uniswap_v2_router01'; @@ -41,6 +42,7 @@ export * from '../generated-wrappers/mixin_gas_token'; export * from '../generated-wrappers/mooniswap_bridge'; export * from '../generated-wrappers/multi_asset_proxy'; export * from '../generated-wrappers/ownable'; +export * from '../generated-wrappers/shell_bridge'; export * from '../generated-wrappers/static_call_proxy'; export * from '../generated-wrappers/sushi_swap_bridge'; export * from '../generated-wrappers/test_bancor_bridge'; diff --git a/contracts/asset-proxy/test/artifacts.ts b/contracts/asset-proxy/test/artifacts.ts index aa01f1dc52..89223873ad 100644 --- a/contracts/asset-proxy/test/artifacts.ts +++ b/contracts/asset-proxy/test/artifacts.ts @@ -32,6 +32,7 @@ import * as IGasToken from '../test/generated-artifacts/IGasToken.json'; import * as IKyberNetworkProxy from '../test/generated-artifacts/IKyberNetworkProxy.json'; import * as IMooniswap from '../test/generated-artifacts/IMooniswap.json'; import * as IMStable from '../test/generated-artifacts/IMStable.json'; +import * as IShell from '../test/generated-artifacts/IShell.json'; import * as IUniswapExchange from '../test/generated-artifacts/IUniswapExchange.json'; import * as IUniswapExchangeFactory from '../test/generated-artifacts/IUniswapExchangeFactory.json'; import * as IUniswapV2Router01 from '../test/generated-artifacts/IUniswapV2Router01.json'; @@ -43,6 +44,7 @@ import * as MooniswapBridge from '../test/generated-artifacts/MooniswapBridge.js import * as MStableBridge from '../test/generated-artifacts/MStableBridge.json'; import * as MultiAssetProxy from '../test/generated-artifacts/MultiAssetProxy.json'; import * as Ownable from '../test/generated-artifacts/Ownable.json'; +import * as ShellBridge from '../test/generated-artifacts/ShellBridge.json'; import * as StaticCallProxy from '../test/generated-artifacts/StaticCallProxy.json'; import * as SushiSwapBridge from '../test/generated-artifacts/SushiSwapBridge.json'; import * as TestBancorBridge from '../test/generated-artifacts/TestBancorBridge.json'; @@ -78,6 +80,7 @@ export const artifacts = { MStableBridge: MStableBridge as ContractArtifact, MixinGasToken: MixinGasToken as ContractArtifact, MooniswapBridge: MooniswapBridge as ContractArtifact, + ShellBridge: ShellBridge as ContractArtifact, SushiSwapBridge: SushiSwapBridge as ContractArtifact, UniswapBridge: UniswapBridge as ContractArtifact, UniswapV2Bridge: UniswapV2Bridge as ContractArtifact, @@ -97,6 +100,7 @@ export const artifacts = { IKyberNetworkProxy: IKyberNetworkProxy as ContractArtifact, IMStable: IMStable as ContractArtifact, IMooniswap: IMooniswap as ContractArtifact, + IShell: IShell as ContractArtifact, IUniswapExchange: IUniswapExchange as ContractArtifact, IUniswapExchangeFactory: IUniswapExchangeFactory as ContractArtifact, IUniswapV2Router01: IUniswapV2Router01 as ContractArtifact, diff --git a/contracts/asset-proxy/test/wrappers.ts b/contracts/asset-proxy/test/wrappers.ts index 4ae6089533..5cbcafb606 100644 --- a/contracts/asset-proxy/test/wrappers.ts +++ b/contracts/asset-proxy/test/wrappers.ts @@ -30,6 +30,7 @@ export * from '../test/generated-wrappers/i_gas_token'; export * from '../test/generated-wrappers/i_kyber_network_proxy'; export * from '../test/generated-wrappers/i_m_stable'; export * from '../test/generated-wrappers/i_mooniswap'; +export * from '../test/generated-wrappers/i_shell'; export * from '../test/generated-wrappers/i_uniswap_exchange'; export * from '../test/generated-wrappers/i_uniswap_exchange_factory'; export * from '../test/generated-wrappers/i_uniswap_v2_router01'; @@ -41,6 +42,7 @@ export * from '../test/generated-wrappers/mixin_gas_token'; export * from '../test/generated-wrappers/mooniswap_bridge'; export * from '../test/generated-wrappers/multi_asset_proxy'; export * from '../test/generated-wrappers/ownable'; +export * from '../test/generated-wrappers/shell_bridge'; export * from '../test/generated-wrappers/static_call_proxy'; export * from '../test/generated-wrappers/sushi_swap_bridge'; export * from '../test/generated-wrappers/test_bancor_bridge'; diff --git a/contracts/asset-proxy/tsconfig.json b/contracts/asset-proxy/tsconfig.json index ccb4c12320..f51d58b3ac 100644 --- a/contracts/asset-proxy/tsconfig.json +++ b/contracts/asset-proxy/tsconfig.json @@ -30,6 +30,7 @@ "generated-artifacts/IKyberNetworkProxy.json", "generated-artifacts/IMStable.json", "generated-artifacts/IMooniswap.json", + "generated-artifacts/IShell.json", "generated-artifacts/IUniswapExchange.json", "generated-artifacts/IUniswapExchangeFactory.json", "generated-artifacts/IUniswapV2Router01.json", @@ -41,6 +42,7 @@ "generated-artifacts/MooniswapBridge.json", "generated-artifacts/MultiAssetProxy.json", "generated-artifacts/Ownable.json", + "generated-artifacts/ShellBridge.json", "generated-artifacts/StaticCallProxy.json", "generated-artifacts/SushiSwapBridge.json", "generated-artifacts/TestBancorBridge.json", @@ -82,6 +84,7 @@ "test/generated-artifacts/IKyberNetworkProxy.json", "test/generated-artifacts/IMStable.json", "test/generated-artifacts/IMooniswap.json", + "test/generated-artifacts/IShell.json", "test/generated-artifacts/IUniswapExchange.json", "test/generated-artifacts/IUniswapExchangeFactory.json", "test/generated-artifacts/IUniswapV2Router01.json", @@ -93,6 +96,7 @@ "test/generated-artifacts/MooniswapBridge.json", "test/generated-artifacts/MultiAssetProxy.json", "test/generated-artifacts/Ownable.json", + "test/generated-artifacts/ShellBridge.json", "test/generated-artifacts/StaticCallProxy.json", "test/generated-artifacts/SushiSwapBridge.json", "test/generated-artifacts/TestBancorBridge.json", diff --git a/contracts/utils/contracts/src/DeploymentConstants.sol b/contracts/utils/contracts/src/DeploymentConstants.sol index 9993d27dd5..72749921c6 100644 --- a/contracts/utils/contracts/src/DeploymentConstants.sol +++ b/contracts/utils/contracts/src/DeploymentConstants.sol @@ -56,6 +56,8 @@ contract DeploymentConstants { address constant private MUSD_ADDRESS = 0xe2f2a5C287993345a840Db3B0845fbC70f5935a5; /// @dev Mainnet address of the Mooniswap Registry contract address constant private MOONISWAP_REGISTRY = 0x71CD6666064C3A1354a3B4dca5fA1E2D3ee7D303; + /// @dev Mainnet address of the Shell contract + address constant private SHELL_CONTRACT = 0x2E703D658f8dd21709a7B458967aB4081F8D3d05; // // Ropsten addresses /////////////////////////////////////////////////////// // /// @dev Mainnet address of the WETH contract. @@ -296,4 +298,14 @@ contract DeploymentConstants { { return MOONISWAP_REGISTRY; } + + /// @dev An overridable way to retrieve the Shell contract address. + /// @return registry The Shell contract address. + function _getShellAddress() + internal + view + returns (address) + { + return SHELL_CONTRACT; + } } diff --git a/contracts/zero-ex/CHANGELOG.json b/contracts/zero-ex/CHANGELOG.json index dd950575f6..2bf7c0f902 100644 --- a/contracts/zero-ex/CHANGELOG.json +++ b/contracts/zero-ex/CHANGELOG.json @@ -61,6 +61,10 @@ { "note": "Added LiquidityProviderFeature", "pr": 2691 + }, + { + "note": "Added `Shell` into FQT", + "pr": 2722 } ] }, diff --git a/contracts/zero-ex/contracts/src/transformers/bridges/BridgeAdapter.sol b/contracts/zero-ex/contracts/src/transformers/bridges/BridgeAdapter.sol index bd90f82928..761ec15da1 100644 --- a/contracts/zero-ex/contracts/src/transformers/bridges/BridgeAdapter.sol +++ b/contracts/zero-ex/contracts/src/transformers/bridges/BridgeAdapter.sol @@ -26,6 +26,7 @@ import "./mixins/MixinKyber.sol"; import "./mixins/MixinMooniswap.sol"; import "./mixins/MixinMStable.sol"; import "./mixins/MixinOasis.sol"; +import "./mixins/MixinShell.sol"; import "./mixins/MixinUniswap.sol"; import "./mixins/MixinUniswapV2.sol"; import "./mixins/MixinZeroExBridge.sol"; @@ -38,6 +39,7 @@ contract BridgeAdapter is MixinMooniswap, MixinMStable, MixinOasis, + MixinShell, MixinUniswap, MixinUniswapV2, MixinZeroExBridge @@ -49,6 +51,7 @@ contract BridgeAdapter is address private immutable MOONISWAP_BRIDGE_ADDRESS; address private immutable MSTABLE_BRIDGE_ADDRESS; address private immutable OASIS_BRIDGE_ADDRESS; + address private immutable SHELL_BRIDGE_ADDRESS; address private immutable UNISWAP_BRIDGE_ADDRESS; address private immutable UNISWAP_V2_BRIDGE_ADDRESS; @@ -76,6 +79,7 @@ contract BridgeAdapter is MixinMooniswap(addresses) MixinMStable(addresses) MixinOasis(addresses) + MixinShell(addresses) MixinUniswap(addresses) MixinUniswapV2(addresses) MixinZeroExBridge() @@ -86,6 +90,7 @@ contract BridgeAdapter is MOONISWAP_BRIDGE_ADDRESS = addresses.mooniswapBridge; MSTABLE_BRIDGE_ADDRESS = addresses.mStableBridge; OASIS_BRIDGE_ADDRESS = addresses.oasisBridge; + SHELL_BRIDGE_ADDRESS = addresses.shellBridge; UNISWAP_BRIDGE_ADDRESS = addresses.uniswapBridge; UNISWAP_V2_BRIDGE_ADDRESS = addresses.uniswapV2Bridge; } @@ -159,6 +164,12 @@ contract BridgeAdapter is sellAmount, bridgeData ); + } else if (bridgeAddress == SHELL_BRIDGE_ADDRESS) { + boughtAmount = _tradeShell( + buyToken, + sellAmount, + bridgeData + ); } else { boughtAmount = _tradeZeroExBridge( bridgeAddress, diff --git a/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinAdapterAddresses.sol b/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinAdapterAddresses.sol index 7a08ad2973..c9d13f5731 100644 --- a/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinAdapterAddresses.sol +++ b/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinAdapterAddresses.sol @@ -29,6 +29,7 @@ contract MixinAdapterAddresses address mooniswapBridge; address mStableBridge; address oasisBridge; + address shellBridge; address uniswapBridge; address uniswapV2Bridge; // Exchanges @@ -37,6 +38,7 @@ contract MixinAdapterAddresses address uniswapV2Router; address uniswapExchangeFactory; address mStable; + address shell; // Other address weth; } diff --git a/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinShell.sol b/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinShell.sol new file mode 100644 index 0000000000..11fd88599f --- /dev/null +++ b/contracts/zero-ex/contracts/src/transformers/bridges/mixins/MixinShell.sol @@ -0,0 +1,84 @@ + +/* + + Copyright 2020 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.6.5; +pragma experimental ABIEncoderV2; + +import "@0x/contracts-erc20/contracts/src/v06/LibERC20TokenV06.sol"; +import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol"; +import "./MixinAdapterAddresses.sol"; + +interface IShell { + + function originSwap( + address from, + address to, + uint256 fromAmount, + uint256 minTargetAmount, + uint256 deadline + ) + external + returns (uint256 toAmount); +} + + + +contract MixinShell is + MixinAdapterAddresses +{ + using LibERC20TokenV06 for IERC20TokenV06; + + /// @dev Mainnet address of the `Shell` contract. + IShell private immutable SHELL; + + constructor(AdapterAddresses memory addresses) + public + { + SHELL = IShell(addresses.shell); + } + + function _tradeShell( + IERC20TokenV06 buyToken, + uint256 sellAmount, + bytes memory bridgeData + ) + internal + returns (uint256 boughtAmount) + { + (address fromTokenAddress) = abi.decode(bridgeData, (address)); + + // Grant the Shell contract an allowance to sell the first token. + IERC20TokenV06(fromTokenAddress).approveIfBelow( + address(SHELL), + sellAmount + ); + + uint256 buyAmount = SHELL.originSwap( + fromTokenAddress, + address(buyToken), + // Sell all tokens we hold. + sellAmount, + // Minimum buy amount. + 1, + // deadline + block.timestamp + 1 + ); + return buyAmount; + } +} diff --git a/contracts/zero-ex/package.json b/contracts/zero-ex/package.json index ca8e98467f..72cce95de7 100644 --- a/contracts/zero-ex/package.json +++ b/contracts/zero-ex/package.json @@ -41,7 +41,7 @@ "config": { "publicInterfaceContracts": "IZeroEx,ZeroEx,FullMigration,InitialMigration,IFlashWallet,IAllowanceTarget,IERC20Transformer,IOwnableFeature,ISimpleFunctionRegistryFeature,ITokenSpenderFeature,ITransformERC20Feature,FillQuoteTransformer,PayTakerTransformer,WethTransformer,OwnableFeature,SimpleFunctionRegistryFeature,TransformERC20Feature,TokenSpenderFeature,AffiliateFeeTransformer,SignatureValidatorFeature,MetaTransactionsFeature,LogMetadataTransformer,BridgeAdapter,LiquidityProviderFeature", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.", - "abis": "./test/generated-artifacts/@(AffiliateFeeTransformer|AllowanceTarget|BootstrapFeature|BridgeAdapter|FillQuoteTransformer|FixinCommon|FixinEIP712|FixinReentrancyGuard|FlashWallet|FullMigration|IAllowanceTarget|IBootstrapFeature|IBridgeAdapter|IERC20Bridge|IERC20Transformer|IExchange|IFeature|IFlashWallet|IGasToken|ILiquidityProviderFeature|IMetaTransactionsFeature|IOwnableFeature|ISignatureValidatorFeature|ISimpleFunctionRegistryFeature|ITestSimpleFunctionRegistryFeature|ITokenSpenderFeature|ITransformERC20Feature|IUniswapFeature|IZeroEx|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibLiquidityProviderRichErrors|LibLiquidityProviderStorage|LibMetaTransactionsRichErrors|LibMetaTransactionsStorage|LibMigrate|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibReentrancyGuardStorage|LibSignatureRichErrors|LibSignedCallData|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibSpenderRichErrors|LibStorage|LibTokenSpenderStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|LiquidityProviderFeature|LogMetadataTransformer|MetaTransactionsFeature|MixinAdapterAddresses|MixinBalancer|MixinCurve|MixinKyber|MixinMStable|MixinMooniswap|MixinOasis|MixinUniswap|MixinUniswapV2|MixinZeroExBridge|OwnableFeature|PayTakerTransformer|SignatureValidatorFeature|SimpleFunctionRegistryFeature|TestBridge|TestCallTarget|TestDelegateCaller|TestFillQuoteTransformerBridge|TestFillQuoteTransformerExchange|TestFillQuoteTransformerHost|TestFullMigration|TestInitialMigration|TestMetaTransactionsTransformERC20Feature|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestTokenSpender|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerBase|TestTransformerDeployerTransformer|TestTransformerHost|TestWeth|TestWethTransformerHost|TestZeroExFeature|TokenSpenderFeature|TransformERC20Feature|Transformer|TransformerDeployer|UniswapFeature|WethTransformer|ZeroEx).json" + "abis": "./test/generated-artifacts/@(AffiliateFeeTransformer|AllowanceTarget|BootstrapFeature|BridgeAdapter|FillQuoteTransformer|FixinCommon|FixinEIP712|FixinReentrancyGuard|FlashWallet|FullMigration|IAllowanceTarget|IBootstrapFeature|IBridgeAdapter|IERC20Bridge|IERC20Transformer|IExchange|IFeature|IFlashWallet|IGasToken|ILiquidityProviderFeature|IMetaTransactionsFeature|IOwnableFeature|ISignatureValidatorFeature|ISimpleFunctionRegistryFeature|ITestSimpleFunctionRegistryFeature|ITokenSpenderFeature|ITransformERC20Feature|IUniswapFeature|IZeroEx|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibLiquidityProviderRichErrors|LibLiquidityProviderStorage|LibMetaTransactionsRichErrors|LibMetaTransactionsStorage|LibMigrate|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibReentrancyGuardStorage|LibSignatureRichErrors|LibSignedCallData|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibSpenderRichErrors|LibStorage|LibTokenSpenderStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|LiquidityProviderFeature|LogMetadataTransformer|MetaTransactionsFeature|MixinAdapterAddresses|MixinBalancer|MixinCurve|MixinKyber|MixinMStable|MixinMooniswap|MixinOasis|MixinShell|MixinUniswap|MixinUniswapV2|MixinZeroExBridge|OwnableFeature|PayTakerTransformer|SignatureValidatorFeature|SimpleFunctionRegistryFeature|TestBridge|TestCallTarget|TestDelegateCaller|TestFillQuoteTransformerBridge|TestFillQuoteTransformerExchange|TestFillQuoteTransformerHost|TestFullMigration|TestInitialMigration|TestMetaTransactionsTransformERC20Feature|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestTokenSpender|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerBase|TestTransformerDeployerTransformer|TestTransformerHost|TestWeth|TestWethTransformerHost|TestZeroExFeature|TokenSpenderFeature|TransformERC20Feature|Transformer|TransformerDeployer|UniswapFeature|WethTransformer|ZeroEx).json" }, "repository": { "type": "git", diff --git a/contracts/zero-ex/test/artifacts.ts b/contracts/zero-ex/test/artifacts.ts index 6de120b62e..ee85274846 100644 --- a/contracts/zero-ex/test/artifacts.ts +++ b/contracts/zero-ex/test/artifacts.ts @@ -68,6 +68,7 @@ import * as MixinKyber from '../test/generated-artifacts/MixinKyber.json'; import * as MixinMooniswap from '../test/generated-artifacts/MixinMooniswap.json'; import * as MixinMStable from '../test/generated-artifacts/MixinMStable.json'; import * as MixinOasis from '../test/generated-artifacts/MixinOasis.json'; +import * as MixinShell from '../test/generated-artifacts/MixinShell.json'; import * as MixinUniswap from '../test/generated-artifacts/MixinUniswap.json'; import * as MixinUniswapV2 from '../test/generated-artifacts/MixinUniswapV2.json'; import * as MixinZeroExBridge from '../test/generated-artifacts/MixinZeroExBridge.json'; @@ -176,6 +177,7 @@ export const artifacts = { MixinMStable: MixinMStable as ContractArtifact, MixinMooniswap: MixinMooniswap as ContractArtifact, MixinOasis: MixinOasis as ContractArtifact, + MixinShell: MixinShell as ContractArtifact, MixinUniswap: MixinUniswap as ContractArtifact, MixinUniswapV2: MixinUniswapV2 as ContractArtifact, MixinZeroExBridge: MixinZeroExBridge as ContractArtifact, diff --git a/contracts/zero-ex/test/transformers/fill_quote_transformer_test.ts b/contracts/zero-ex/test/transformers/fill_quote_transformer_test.ts index 3ad6c5a6d4..fbeffdf643 100644 --- a/contracts/zero-ex/test/transformers/fill_quote_transformer_test.ts +++ b/contracts/zero-ex/test/transformers/fill_quote_transformer_test.ts @@ -73,6 +73,8 @@ blockchainTests.resets('FillQuoteTransformer', env => { uniswapExchangeFactory: NULL_ADDRESS, mStable: NULL_ADDRESS, weth: NULL_ADDRESS, + shellBridge: NULL_ADDRESS, + shell: NULL_ADDRESS, }, ); transformer = await FillQuoteTransformerContract.deployFrom0xArtifactAsync( diff --git a/contracts/zero-ex/test/wrappers.ts b/contracts/zero-ex/test/wrappers.ts index 76dac9c6b4..30812a7248 100644 --- a/contracts/zero-ex/test/wrappers.ts +++ b/contracts/zero-ex/test/wrappers.ts @@ -66,6 +66,7 @@ export * from '../test/generated-wrappers/mixin_kyber'; export * from '../test/generated-wrappers/mixin_m_stable'; export * from '../test/generated-wrappers/mixin_mooniswap'; export * from '../test/generated-wrappers/mixin_oasis'; +export * from '../test/generated-wrappers/mixin_shell'; export * from '../test/generated-wrappers/mixin_uniswap'; export * from '../test/generated-wrappers/mixin_uniswap_v2'; export * from '../test/generated-wrappers/mixin_zero_ex_bridge'; diff --git a/contracts/zero-ex/tsconfig.json b/contracts/zero-ex/tsconfig.json index 4cc0fb27af..457630a96d 100644 --- a/contracts/zero-ex/tsconfig.json +++ b/contracts/zero-ex/tsconfig.json @@ -90,6 +90,7 @@ "test/generated-artifacts/MixinMStable.json", "test/generated-artifacts/MixinMooniswap.json", "test/generated-artifacts/MixinOasis.json", + "test/generated-artifacts/MixinShell.json", "test/generated-artifacts/MixinUniswap.json", "test/generated-artifacts/MixinUniswapV2.json", "test/generated-artifacts/MixinZeroExBridge.json", diff --git a/packages/asset-swapper/CHANGELOG.json b/packages/asset-swapper/CHANGELOG.json index 71d531e7c1..16e90e191a 100644 --- a/packages/asset-swapper/CHANGELOG.json +++ b/packages/asset-swapper/CHANGELOG.json @@ -137,6 +137,10 @@ { "note": "Introduced Path class, exchangeProxyOverhead parameter", "pr": 2691 + }, + { + "note": "Added `Shell`", + "pr": 2722 } ] }, diff --git a/packages/asset-swapper/contracts/src/ERC20BridgeSampler.sol b/packages/asset-swapper/contracts/src/ERC20BridgeSampler.sol index db844350e0..75b542fc0e 100644 --- a/packages/asset-swapper/contracts/src/ERC20BridgeSampler.sol +++ b/packages/asset-swapper/contracts/src/ERC20BridgeSampler.sol @@ -28,6 +28,7 @@ import "./MultiBridgeSampler.sol"; import "./MStableSampler.sol"; import "./MooniswapSampler.sol"; import "./NativeOrderSampler.sol"; +import "./ShellSampler.sol"; import "./SushiSwapSampler.sol"; import "./TwoHopSampler.sol"; import "./UniswapSampler.sol"; @@ -44,6 +45,7 @@ contract ERC20BridgeSampler is MooniswapSampler, MultiBridgeSampler, NativeOrderSampler, + ShellSampler, SushiSwapSampler, TwoHopSampler, UniswapSampler, diff --git a/packages/asset-swapper/contracts/src/ShellSampler.sol b/packages/asset-swapper/contracts/src/ShellSampler.sol new file mode 100644 index 0000000000..9642211a29 --- /dev/null +++ b/packages/asset-swapper/contracts/src/ShellSampler.sol @@ -0,0 +1,110 @@ +/* + + Copyright 2020 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.5.9; +pragma experimental ABIEncoderV2; + +import "@0x/contracts-utils/contracts/src/DeploymentConstants.sol"; +import "./interfaces/IShell.sol"; + +contract ShellSampler is + DeploymentConstants +{ + /// @dev Default gas limit for Shell calls. + uint256 constant private DEFAULT_CALL_GAS = 300e3; // 300k + + /// @dev Sample sell quotes from the Shell contract + /// @param takerToken Address of the taker token (what to sell). + /// @param makerToken Address of the maker token (what to buy). + /// @param takerTokenAmounts Taker token sell amount for each sample. + /// @return makerTokenAmounts Maker amounts bought at each taker token + /// amount. + function sampleSellsFromShell( + address takerToken, + address makerToken, + uint256[] memory takerTokenAmounts + ) + public + view + returns (uint256[] memory makerTokenAmounts) + { + // Initialize array of maker token amounts. + uint256 numSamples = takerTokenAmounts.length; + makerTokenAmounts = new uint256[](numSamples); + + for (uint256 i = 0; i < numSamples; i++) { + (bool didSucceed, bytes memory resultData) = + address(_getShellAddress()).staticcall.gas(DEFAULT_CALL_GAS)( + abi.encodeWithSelector( + IShell(0).viewOriginSwap.selector, + takerToken, + makerToken, + takerTokenAmounts[i] + )); + uint256 buyAmount = 0; + if (didSucceed) { + buyAmount = abi.decode(resultData, (uint256)); + } + // Exit early if the amount is too high for the source to serve + if (buyAmount == 0) { + break; + } + makerTokenAmounts[i] = buyAmount; + } + } + + /// @dev Sample buy quotes from Shell contract + /// @param takerToken Address of the taker token (what to sell). + /// @param makerToken Address of the maker token (what to buy). + /// @param makerTokenAmounts Maker token buy amount for each sample. + /// @return takerTokenAmounts Taker amounts sold at each maker token + /// amount. + function sampleBuysFromShell( + address takerToken, + address makerToken, + uint256[] memory makerTokenAmounts + ) + public + view + returns (uint256[] memory takerTokenAmounts) + { + // Initialize array of maker token amounts. + uint256 numSamples = makerTokenAmounts.length; + takerTokenAmounts = new uint256[](numSamples); + + for (uint256 i = 0; i < numSamples; i++) { + (bool didSucceed, bytes memory resultData) = + address(_getShellAddress()).staticcall.gas(DEFAULT_CALL_GAS)( + abi.encodeWithSelector( + IShell(0).viewTargetSwap.selector, + takerToken, + makerToken, + makerTokenAmounts[i] + )); + uint256 sellAmount = 0; + if (didSucceed) { + sellAmount = abi.decode(resultData, (uint256)); + } + // Exit early if the amount is too high for the source to serve + if (sellAmount == 0) { + break; + } + takerTokenAmounts[i] = sellAmount; + } + } +} diff --git a/packages/asset-swapper/contracts/src/interfaces/IShell.sol b/packages/asset-swapper/contracts/src/interfaces/IShell.sol new file mode 100644 index 0000000000..61caf5ceb3 --- /dev/null +++ b/packages/asset-swapper/contracts/src/interfaces/IShell.sol @@ -0,0 +1,42 @@ +/* + + Copyright 2020 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.5.9; + + +interface IShell { + + function viewOriginSwap ( + address from, + address to, + uint256 fromAmount + ) + external + view + returns (uint256 toAmount); + + function viewTargetSwap ( + address from, + address to, + uint256 toAmount + ) + external + view + returns (uint256 fromAmount); +} + diff --git a/packages/asset-swapper/package.json b/packages/asset-swapper/package.json index f4d5d2ad19..8673709871 100644 --- a/packages/asset-swapper/package.json +++ b/packages/asset-swapper/package.json @@ -38,7 +38,7 @@ "config": { "publicInterfaceContracts": "ERC20BridgeSampler,ILiquidityProvider,ILiquidityProviderRegistry,DummyLiquidityProviderRegistry,DummyLiquidityProvider", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.", - "abis": "./test/generated-artifacts/@(ApproximateBuys|BalancerSampler|CurveSampler|DummyLiquidityProvider|DummyLiquidityProviderRegistry|ERC20BridgeSampler|Eth2DaiSampler|IBalancer|ICurve|IEth2Dai|IKyberNetwork|ILiquidityProvider|ILiquidityProviderRegistry|IMStable|IMooniswap|IMultiBridge|IUniswapExchangeQuotes|IUniswapV2Router01|KyberSampler|LiquidityProviderSampler|MStableSampler|MooniswapSampler|MultiBridgeSampler|NativeOrderSampler|SamplerUtils|SushiSwapSampler|TestERC20BridgeSampler|TestNativeOrderSampler|TwoHopSampler|UniswapSampler|UniswapV2Sampler).json", + "abis": "./test/generated-artifacts/@(ApproximateBuys|BalancerSampler|CurveSampler|DummyLiquidityProvider|DummyLiquidityProviderRegistry|ERC20BridgeSampler|Eth2DaiSampler|IBalancer|ICurve|IEth2Dai|IKyberNetwork|ILiquidityProvider|ILiquidityProviderRegistry|IMStable|IMooniswap|IMultiBridge|IShell|IUniswapExchangeQuotes|IUniswapV2Router01|KyberSampler|LiquidityProviderSampler|MStableSampler|MooniswapSampler|MultiBridgeSampler|NativeOrderSampler|SamplerUtils|ShellSampler|SushiSwapSampler|TestERC20BridgeSampler|TestNativeOrderSampler|TwoHopSampler|UniswapSampler|UniswapV2Sampler).json", "postpublish": { "assets": [] } diff --git a/packages/asset-swapper/src/utils/market_operation_utils/constants.ts b/packages/asset-swapper/src/utils/market_operation_utils/constants.ts index ce48b44fb4..f0c4961800 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/constants.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/constants.ts @@ -22,6 +22,7 @@ export const SELL_SOURCE_FILTER = new SourceFilters([ ERC20BridgeSource.Mooniswap, ERC20BridgeSource.Swerve, ERC20BridgeSource.SushiSwap, + ERC20BridgeSource.Shell, ERC20BridgeSource.MultiHop, ]); @@ -40,6 +41,7 @@ export const BUY_SOURCE_FILTER = new SourceFilters( // ERC20BridgeSource.Bancor, // FIXME: Disabled until Bancor SDK supports buy quotes ERC20BridgeSource.MStable, ERC20BridgeSource.Mooniswap, + ERC20BridgeSource.Shell, ERC20BridgeSource.Swerve, ERC20BridgeSource.SushiSwap, ERC20BridgeSource.MultiHop, diff --git a/packages/asset-swapper/src/utils/market_operation_utils/orders.ts b/packages/asset-swapper/src/utils/market_operation_utils/orders.ts index 67cfbc118e..3a0129f222 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/orders.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/orders.ts @@ -209,6 +209,8 @@ function getBridgeAddressFromFill(fill: CollapsedFill, opts: CreateOrderFromPath return opts.contractAddresses.mStableBridge; case ERC20BridgeSource.Mooniswap: return opts.contractAddresses.mooniswapBridge; + case ERC20BridgeSource.Shell: + return opts.contractAddresses.shellBridge; default: break; } diff --git a/packages/asset-swapper/src/utils/market_operation_utils/sampler_operations.ts b/packages/asset-swapper/src/utils/market_operation_utils/sampler_operations.ts index 9ef979049c..1ecf83e7a0 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/sampler_operations.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/sampler_operations.ts @@ -734,6 +734,32 @@ export class SamplerOperations { }); } + public getShellSellQuotes( + makerToken: string, + takerToken: string, + takerFillAmounts: BigNumber[], + ): SourceQuoteOperation { + return new SamplerContractOperation({ + source: ERC20BridgeSource.Shell, + contract: this._samplerContract, + function: this._samplerContract.sampleSellsFromShell, + params: [takerToken, makerToken, takerFillAmounts], + }); + } + + public getShellBuyQuotes( + makerToken: string, + takerToken: string, + makerFillAmounts: BigNumber[], + ): SourceQuoteOperation { + return new SamplerContractOperation({ + source: ERC20BridgeSource.Shell, + contract: this._samplerContract, + function: this._samplerContract.sampleBuysFromShell, + params: [takerToken, makerToken, makerFillAmounts], + }); + } + public getMedianSellRate( sources: ERC20BridgeSource[], makerToken: string, @@ -971,6 +997,8 @@ export class SamplerOperations { .map(poolAddress => this.getBalancerSellQuotes(poolAddress, makerToken, takerToken, takerFillAmounts), ); + case ERC20BridgeSource.Shell: + return this.getShellSellQuotes(makerToken, takerToken, takerFillAmounts); default: throw new Error(`Unsupported sell sample source: ${source}`); } @@ -1058,6 +1086,8 @@ export class SamplerOperations { .map(poolAddress => this.getBalancerBuyQuotes(poolAddress, makerToken, takerToken, makerFillAmounts), ); + case ERC20BridgeSource.Shell: + return this.getShellBuyQuotes(makerToken, takerToken, makerFillAmounts); default: throw new Error(`Unsupported buy sample source: ${source}`); } diff --git a/packages/asset-swapper/src/utils/market_operation_utils/types.ts b/packages/asset-swapper/src/utils/market_operation_utils/types.ts index c6af06a62e..a91c38d1ae 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/types.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/types.ts @@ -41,6 +41,7 @@ export enum ERC20BridgeSource { MStable = 'mStable', Mooniswap = 'Mooniswap', MultiHop = 'MultiHop', + Shell = 'Shell', Swerve = 'Swerve', SushiSwap = 'SushiSwap', } diff --git a/packages/asset-swapper/test/artifacts.ts b/packages/asset-swapper/test/artifacts.ts index 8e39d7129c..04d6172f9e 100644 --- a/packages/asset-swapper/test/artifacts.ts +++ b/packages/asset-swapper/test/artifacts.ts @@ -21,6 +21,7 @@ import * as ILiquidityProviderRegistry from '../test/generated-artifacts/ILiquid import * as IMooniswap from '../test/generated-artifacts/IMooniswap.json'; import * as IMStable from '../test/generated-artifacts/IMStable.json'; import * as IMultiBridge from '../test/generated-artifacts/IMultiBridge.json'; +import * as IShell from '../test/generated-artifacts/IShell.json'; import * as IUniswapExchangeQuotes from '../test/generated-artifacts/IUniswapExchangeQuotes.json'; import * as IUniswapV2Router01 from '../test/generated-artifacts/IUniswapV2Router01.json'; import * as KyberSampler from '../test/generated-artifacts/KyberSampler.json'; @@ -30,6 +31,7 @@ import * as MStableSampler from '../test/generated-artifacts/MStableSampler.json import * as MultiBridgeSampler from '../test/generated-artifacts/MultiBridgeSampler.json'; import * as NativeOrderSampler from '../test/generated-artifacts/NativeOrderSampler.json'; import * as SamplerUtils from '../test/generated-artifacts/SamplerUtils.json'; +import * as ShellSampler from '../test/generated-artifacts/ShellSampler.json'; import * as SushiSwapSampler from '../test/generated-artifacts/SushiSwapSampler.json'; import * as TestERC20BridgeSampler from '../test/generated-artifacts/TestERC20BridgeSampler.json'; import * as TestNativeOrderSampler from '../test/generated-artifacts/TestNativeOrderSampler.json'; @@ -50,6 +52,7 @@ export const artifacts = { MultiBridgeSampler: MultiBridgeSampler as ContractArtifact, NativeOrderSampler: NativeOrderSampler as ContractArtifact, SamplerUtils: SamplerUtils as ContractArtifact, + ShellSampler: ShellSampler as ContractArtifact, SushiSwapSampler: SushiSwapSampler as ContractArtifact, TwoHopSampler: TwoHopSampler as ContractArtifact, UniswapSampler: UniswapSampler as ContractArtifact, @@ -62,6 +65,7 @@ export const artifacts = { ILiquidityProviderRegistry: ILiquidityProviderRegistry as ContractArtifact, IMStable: IMStable as ContractArtifact, IMultiBridge: IMultiBridge as ContractArtifact, + IShell: IShell as ContractArtifact, IUniswapExchangeQuotes: IUniswapExchangeQuotes as ContractArtifact, IUniswapV2Router01: IUniswapV2Router01 as ContractArtifact, DummyLiquidityProvider: DummyLiquidityProvider as ContractArtifact, diff --git a/packages/asset-swapper/test/market_operation_utils_test.ts b/packages/asset-swapper/test/market_operation_utils_test.ts index 688c1bcc39..8c074996e9 100644 --- a/packages/asset-swapper/test/market_operation_utils_test.ts +++ b/packages/asset-swapper/test/market_operation_utils_test.ts @@ -50,6 +50,7 @@ const DEFAULT_EXCLUDED = [ ERC20BridgeSource.Swerve, ERC20BridgeSource.SushiSwap, ERC20BridgeSource.MultiHop, + ERC20BridgeSource.Shell, ]; const BUY_SOURCES = BUY_SOURCE_FILTER.sources; const SELL_SOURCES = SELL_SOURCE_FILTER.sources; @@ -108,6 +109,8 @@ describe('MarketOperationUtils tests', () => { return ERC20BridgeSource.Mooniswap; case contractAddresses.sushiswapBridge.toLowerCase(): return ERC20BridgeSource.SushiSwap; + case contractAddresses.shellBridge.toLowerCase(): + return ERC20BridgeSource.Shell; default: break; } @@ -301,6 +304,7 @@ describe('MarketOperationUtils tests', () => { [ERC20BridgeSource.Swerve]: _.times(NUM_SAMPLES, () => 0), [ERC20BridgeSource.SushiSwap]: _.times(NUM_SAMPLES, () => 0), [ERC20BridgeSource.MultiHop]: _.times(NUM_SAMPLES, () => 0), + [ERC20BridgeSource.Shell]: _.times(NUM_SAMPLES, () => 0), }; const DEFAULT_RATES: RatesBySource = { @@ -346,6 +350,7 @@ describe('MarketOperationUtils tests', () => { [ERC20BridgeSource.Mooniswap]: { poolAddress: randomAddress() }, [ERC20BridgeSource.Native]: { order: createOrder() }, [ERC20BridgeSource.MultiHop]: {}, + [ERC20BridgeSource.Shell]: {}, }; const DEFAULT_OPS = { diff --git a/packages/asset-swapper/test/wrappers.ts b/packages/asset-swapper/test/wrappers.ts index a130c86c4a..d3c2facda8 100644 --- a/packages/asset-swapper/test/wrappers.ts +++ b/packages/asset-swapper/test/wrappers.ts @@ -19,6 +19,7 @@ export * from '../test/generated-wrappers/i_liquidity_provider_registry'; export * from '../test/generated-wrappers/i_m_stable'; export * from '../test/generated-wrappers/i_mooniswap'; export * from '../test/generated-wrappers/i_multi_bridge'; +export * from '../test/generated-wrappers/i_shell'; export * from '../test/generated-wrappers/i_uniswap_exchange_quotes'; export * from '../test/generated-wrappers/i_uniswap_v2_router01'; export * from '../test/generated-wrappers/kyber_sampler'; @@ -28,6 +29,7 @@ export * from '../test/generated-wrappers/mooniswap_sampler'; export * from '../test/generated-wrappers/multi_bridge_sampler'; export * from '../test/generated-wrappers/native_order_sampler'; export * from '../test/generated-wrappers/sampler_utils'; +export * from '../test/generated-wrappers/shell_sampler'; export * from '../test/generated-wrappers/sushi_swap_sampler'; export * from '../test/generated-wrappers/test_erc20_bridge_sampler'; export * from '../test/generated-wrappers/test_native_order_sampler'; diff --git a/packages/asset-swapper/tsconfig.json b/packages/asset-swapper/tsconfig.json index 9ef3281121..1682949b49 100644 --- a/packages/asset-swapper/tsconfig.json +++ b/packages/asset-swapper/tsconfig.json @@ -24,6 +24,7 @@ "test/generated-artifacts/IMStable.json", "test/generated-artifacts/IMooniswap.json", "test/generated-artifacts/IMultiBridge.json", + "test/generated-artifacts/IShell.json", "test/generated-artifacts/IUniswapExchangeQuotes.json", "test/generated-artifacts/IUniswapV2Router01.json", "test/generated-artifacts/KyberSampler.json", @@ -33,6 +34,7 @@ "test/generated-artifacts/MultiBridgeSampler.json", "test/generated-artifacts/NativeOrderSampler.json", "test/generated-artifacts/SamplerUtils.json", + "test/generated-artifacts/ShellSampler.json", "test/generated-artifacts/SushiSwapSampler.json", "test/generated-artifacts/TestERC20BridgeSampler.json", "test/generated-artifacts/TestNativeOrderSampler.json", diff --git a/packages/contract-addresses/CHANGELOG.json b/packages/contract-addresses/CHANGELOG.json index 4546251d08..40bd17abed 100644 --- a/packages/contract-addresses/CHANGELOG.json +++ b/packages/contract-addresses/CHANGELOG.json @@ -49,6 +49,10 @@ { "note": "Deploy `BancorBridge` on Mainnet", "pr": 2699 + }, + { + "note": "Deploy `ShellBridge` on Mainnet", + "pr": 2722 } ] }, diff --git a/packages/contract-addresses/addresses.json b/packages/contract-addresses/addresses.json index 0791047775..f91e85b474 100644 --- a/packages/contract-addresses/addresses.json +++ b/packages/contract-addresses/addresses.json @@ -43,6 +43,7 @@ "mStableBridge": "0x2bf04fcea05f0989a14d9afa37aa376baca6b2b3", "mooniswapBridge": "0x02b7eca484ad960fca3f7709e0b2ac81eec3069c", "sushiswapBridge": "0x47ed0262a0b688dcb836d254c6a2e96b6c48a9f5", + "shellBridge": "0x21fb3862eed7911e0f8219a077247b849846728d", "transformers": { "wethTransformer": "0x68c0bb685099dc7cb5c5ce2b26185945b357383e", "payTakerTransformer": "0x49b9df2c58491764cf40cb052dd4243df63622c7", @@ -94,6 +95,7 @@ "mStableBridge": "0x0000000000000000000000000000000000000000", "mooniswapBridge": "0x0000000000000000000000000000000000000000", "sushiswapBridge": "0x0000000000000000000000000000000000000000", + "shellBridge": "0x0000000000000000000000000000000000000000", "transformers": { "wethTransformer": "0x8d822fe2b42f60531203e288f5f357fa79474437", "payTakerTransformer": "0x150652244723102faeaefa4c79597d097ffa26c6", @@ -145,6 +147,7 @@ "mStableBridge": "0x0000000000000000000000000000000000000000", "mooniswapBridge": "0x0000000000000000000000000000000000000000", "sushiswapBridge": "0x0000000000000000000000000000000000000000", + "shellBridge": "0x0000000000000000000000000000000000000000", "transformers": { "wethTransformer": "0x8d822fe2b42f60531203e288f5f357fa79474437", "payTakerTransformer": "0x150652244723102faeaefa4c79597d097ffa26c6", @@ -196,6 +199,7 @@ "mStableBridge": "0x0000000000000000000000000000000000000000", "mooniswapBridge": "0x0000000000000000000000000000000000000000", "sushiswapBridge": "0x0000000000000000000000000000000000000000", + "shellBridge": "0x0000000000000000000000000000000000000000", "transformers": { "wethTransformer": "0x9ce35b5ee9e710535e3988e3f8731d9ca9dba17d", "payTakerTransformer": "0x5a53e7b02a83aa9f60ccf4e424f0442c255bc977", @@ -247,6 +251,7 @@ "mStableBridge": "0x0000000000000000000000000000000000000000", "mooniswapBridge": "0x0000000000000000000000000000000000000000", "sushiswapBridge": "0x0000000000000000000000000000000000000000", + "shellBridge": "0x0000000000000000000000000000000000000000", "transformers": { "wethTransformer": "0xc6b0d3c45a6b5092808196cb00df5c357d55e1d5", "payTakerTransformer": "0x7209185959d7227fb77274e1e88151d7c4c368d3", diff --git a/packages/contract-addresses/src/index.ts b/packages/contract-addresses/src/index.ts index 066ec0af12..8aeabd8df3 100644 --- a/packages/contract-addresses/src/index.ts +++ b/packages/contract-addresses/src/index.ts @@ -44,6 +44,7 @@ export interface ContractAddresses { mStableBridge: string; mooniswapBridge: string; sushiswapBridge: string; + shellBridge: string; transformers: { wethTransformer: string; payTakerTransformer: string; diff --git a/packages/migrations/src/migration.ts b/packages/migrations/src/migration.ts index bbe34d55b4..8c96994fba 100644 --- a/packages/migrations/src/migration.ts +++ b/packages/migrations/src/migration.ts @@ -324,6 +324,8 @@ export async function runMigrationsAsync( uniswapV2Router: NULL_ADDRESS, uniswapExchangeFactory: NULL_ADDRESS, mStable: NULL_ADDRESS, + shellBridge: NULL_ADDRESS, + shell: NULL_ADDRESS, weth: etherToken.address, }, ); @@ -401,6 +403,7 @@ export async function runMigrationsAsync( mStableBridge: NULL_ADDRESS, mooniswapBridge: NULL_ADDRESS, sushiswapBridge: NULL_ADDRESS, + shellBridge: NULL_ADDRESS, exchangeProxy: exchangeProxy.address, exchangeProxyAllowanceTarget: exchangeProxyAllowanceTargetAddress, exchangeProxyTransformerDeployer: txDefaults.from, From cd9e408ea35405ead4eaef05e58e9963441a19d2 Mon Sep 17 00:00:00 2001 From: Jacob Evans Date: Tue, 6 Oct 2020 21:09:46 +1000 Subject: [PATCH 23/32] fix: exchange proxy overhead scaled by gas price (#2723) --- packages/asset-swapper/CHANGELOG.json | 4 ++++ packages/asset-swapper/src/utils/swap_quote_calculator.ts | 6 +++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/asset-swapper/CHANGELOG.json b/packages/asset-swapper/CHANGELOG.json index 16e90e191a..81f87838c8 100644 --- a/packages/asset-swapper/CHANGELOG.json +++ b/packages/asset-swapper/CHANGELOG.json @@ -141,6 +141,10 @@ { "note": "Added `Shell`", "pr": 2722 + }, + { + "note": "Fix exchange proxy overhead gas being scaled by gas price", + "pr": 2723 } ] }, diff --git a/packages/asset-swapper/src/utils/swap_quote_calculator.ts b/packages/asset-swapper/src/utils/swap_quote_calculator.ts index 9ed44ba2c2..22f09fe4d6 100644 --- a/packages/asset-swapper/src/utils/swap_quote_calculator.ts +++ b/packages/asset-swapper/src/utils/swap_quote_calculator.ts @@ -25,10 +25,9 @@ import { GetMarketOrdersOpts, OptimizedMarketOrder, } from './market_operation_utils/types'; -import { getTokenFromAssetData, isSupportedAssetDataInOrders } from './utils'; - import { QuoteReport } from './quote_report_generator'; import { QuoteFillResult, simulateBestCaseFill, simulateWorstCaseFill } from './quote_simulation'; +import { getTokenFromAssetData, isSupportedAssetDataInOrders } from './utils'; // TODO(dave4506) How do we want to reintroduce InsufficientAssetLiquidityError? export class SwapQuoteCalculator { @@ -195,7 +194,8 @@ export class SwapQuoteCalculator { opts.gasSchedule, quoteReport, ); - const exchangeProxyOverhead = _opts.exchangeProxyOverhead(sourceFlags).toNumber(); + // Use the raw gas, not scaled by gas price + const exchangeProxyOverhead = opts.exchangeProxyOverhead(sourceFlags).toNumber(); swapQuote.bestCaseQuoteInfo.gas += exchangeProxyOverhead; swapQuote.worstCaseQuoteInfo.gas += exchangeProxyOverhead; return swapQuote; From 36546480b100e760da7b3b8f1e6258d8f2860675 Mon Sep 17 00:00:00 2001 From: Jacob Evans Date: Wed, 7 Oct 2020 17:36:44 +1000 Subject: [PATCH 24/32] fix: [asset-swapper] Catch Uint256BinOp which can occur (#2724) * fix: [asset-swapper] Catch Uint256BinOp which can occur * Perform a local bounds check --- .../contracts/src/ApproximateBuys.sol | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/packages/asset-swapper/contracts/src/ApproximateBuys.sol b/packages/asset-swapper/contracts/src/ApproximateBuys.sol index a6776b3457..15c5e16826 100644 --- a/packages/asset-swapper/contracts/src/ApproximateBuys.sol +++ b/packages/asset-swapper/contracts/src/ApproximateBuys.sol @@ -78,16 +78,22 @@ contract ApproximateBuys { for (uint256 i = 0; i < makerTokenAmounts.length; i++) { for (uint256 iter = 0; iter < APPROXIMATE_BUY_MAX_ITERATIONS; iter++) { // adjustedSellAmount = previousSellAmount * (target/actual) * JUMP_MULTIPLIER - sellAmount = LibMath.getPartialAmountCeil( + sellAmount = _safeGetPartialAmountCeil( makerTokenAmounts[i], buyAmount, sellAmount ); - sellAmount = LibMath.getPartialAmountCeil( + if (sellAmount == 0) { + break; + } + sellAmount = _safeGetPartialAmountCeil( (ONE_HUNDED_PERCENT_BPS + APPROXIMATE_BUY_TARGET_EPSILON_BPS), ONE_HUNDED_PERCENT_BPS, sellAmount ); + if (sellAmount == 0) { + break; + } uint256 _buyAmount = opts.getSellQuoteCallback( opts.takerTokenData, opts.makerTokenData, @@ -112,11 +118,26 @@ contract ApproximateBuys { // We do our best to close in on the requested amount, but we can either over buy or under buy and exit // if we hit a max iteration limit // We scale the sell amount to get the approximate target - takerTokenAmounts[i] = LibMath.getPartialAmountCeil( + takerTokenAmounts[i] = _safeGetPartialAmountCeil( makerTokenAmounts[i], buyAmount, sellAmount ); } } + + function _safeGetPartialAmountCeil( + uint256 numerator, + uint256 denominator, + uint256 target + ) + internal + view + returns (uint256 partialAmount) + { + if (numerator == 0 || target == 0 || denominator == 0) return 0; + uint256 c = numerator * target; + if (c / numerator != target) return 0; + return (c + (denominator - 1)) / denominator; + } } From 5d8e35fb68dbd902cb8edbef5aea545f94e7d444 Mon Sep 17 00:00:00 2001 From: "F. Eugene Aumson" Date: Wed, 7 Oct 2020 12:01:43 -0400 Subject: [PATCH 25/32] asset-swapp: isBlacklisted in rfqtMakerInteraction --- packages/asset-swapper/src/utils/quote_requestor.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/asset-swapper/src/utils/quote_requestor.ts b/packages/asset-swapper/src/utils/quote_requestor.ts index ea889e5ebd..644ef1d8a6 100644 --- a/packages/asset-swapper/src/utils/quote_requestor.ts +++ b/packages/asset-swapper/src/utils/quote_requestor.ts @@ -337,10 +337,9 @@ export class QuoteRequestor { const result: Array<{ response: ResponseT; makerUri: string }> = []; await Promise.all( Object.keys(this._rfqtAssetOfferings).map(async url => { - if ( - this._makerSupportsPair(url, makerAssetData, takerAssetData) && - !rfqMakerBlacklist.isMakerBlacklisted(url) - ) { + if (rfqMakerBlacklist.isMakerBlacklisted(url)) { + this._infoLogger({ rfqtMakerInteraction: { url, quoteType, isBlacklisted: true } }); + } else if (this._makerSupportsPair(url, makerAssetData, takerAssetData)) { const requestParamsWithBigNumbers = { takerAddress: options.takerAddress, ...inferQueryParams(marketOperation, makerAssetData, takerAssetData, assetFillAmount), @@ -358,7 +357,8 @@ export class QuoteRequestor { : undefined, }; - const partialLogEntry = { url, quoteType, requestParams }; + const partialLogEntry = { url, quoteType, requestParams, isBlacklisted: false }; + const timeBeforeAwait = Date.now(); const maxResponseTimeMs = options.makerEndpointMaxResponseTimeMs === undefined From 98e6aa4bac7bb33c94f09f0823bf39643ffc600d Mon Sep 17 00:00:00 2001 From: Michael Zhu Date: Wed, 7 Oct 2020 18:38:37 -0700 Subject: [PATCH 26/32] add abi encoder support for uint80 lol --- packages/utils/CHANGELOG.json | 4 ++++ packages/utils/src/abi_encoder/evm_data_types/uint.ts | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/utils/CHANGELOG.json b/packages/utils/CHANGELOG.json index eda013cdf4..945fd294fa 100644 --- a/packages/utils/CHANGELOG.json +++ b/packages/utils/CHANGELOG.json @@ -13,6 +13,10 @@ { "note": "Added LiquidityProviderFeature errors", "pr": 2691 + }, + { + "note": "Added abi encoder support for uint80 lol", + "pr": 2728 } ] }, diff --git a/packages/utils/src/abi_encoder/evm_data_types/uint.ts b/packages/utils/src/abi_encoder/evm_data_types/uint.ts index 1e447075c8..153da7f7f5 100644 --- a/packages/utils/src/abi_encoder/evm_data_types/uint.ts +++ b/packages/utils/src/abi_encoder/evm_data_types/uint.ts @@ -10,7 +10,7 @@ import * as EncoderMath from '../utils/math'; export class UIntDataType extends AbstractBlobDataType { private static readonly _MATCHER = RegExp( - '^uint(8|16|24|32|40|48|56|64|72|88|96|104|112|120|128|136|144|152|160|168|176|184|192|200|208|216|224|232|240|248|256){0,1}$', + '^uint(8|16|24|32|40|48|56|64|72|80|88|96|104|112|120|128|136|144|152|160|168|176|184|192|200|208|216|224|232|240|248|256){0,1}$', ); private static readonly _SIZE_KNOWN_AT_COMPILE_TIME: boolean = true; private static readonly _MAX_WIDTH: number = 256; From 6e954385cec5d7068a3ef5b89d0e0f58cd2df22f Mon Sep 17 00:00:00 2001 From: "F. Eugene Aumson" Date: Thu, 8 Oct 2020 09:15:58 -0400 Subject: [PATCH 27/32] Include requestParams in maker-blacklisted log Addresses review comment https://github.com/0xProject/0x-monorepo/pull/2726#discussion_r501332160 --- .../src/utils/quote_requestor.ts | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/packages/asset-swapper/src/utils/quote_requestor.ts b/packages/asset-swapper/src/utils/quote_requestor.ts index 644ef1d8a6..83d48e9bfa 100644 --- a/packages/asset-swapper/src/utils/quote_requestor.ts +++ b/packages/asset-swapper/src/utils/quote_requestor.ts @@ -334,31 +334,31 @@ export class QuoteRequestor { options: RfqtRequestOpts, quoteType: 'firm' | 'indicative', ): Promise> { + const requestParamsWithBigNumbers = { + takerAddress: options.takerAddress, + ...inferQueryParams(marketOperation, makerAssetData, takerAssetData, assetFillAmount), + }; + + // convert BigNumbers to strings + // so they are digestible by axios + const requestParams = { + ...requestParamsWithBigNumbers, + sellAmountBaseUnits: requestParamsWithBigNumbers.sellAmountBaseUnits + ? requestParamsWithBigNumbers.sellAmountBaseUnits.toString() + : undefined, + buyAmountBaseUnits: requestParamsWithBigNumbers.buyAmountBaseUnits + ? requestParamsWithBigNumbers.buyAmountBaseUnits.toString() + : undefined, + }; + const result: Array<{ response: ResponseT; makerUri: string }> = []; await Promise.all( Object.keys(this._rfqtAssetOfferings).map(async url => { - if (rfqMakerBlacklist.isMakerBlacklisted(url)) { - this._infoLogger({ rfqtMakerInteraction: { url, quoteType, isBlacklisted: true } }); + const isBlacklisted = rfqMakerBlacklist.isMakerBlacklisted(url); + const partialLogEntry = { url, quoteType, requestParams, isBlacklisted }; + if (isBlacklisted) { + this._infoLogger({ rfqtMakerInteraction: { ...partialLogEntry } }); } else if (this._makerSupportsPair(url, makerAssetData, takerAssetData)) { - const requestParamsWithBigNumbers = { - takerAddress: options.takerAddress, - ...inferQueryParams(marketOperation, makerAssetData, takerAssetData, assetFillAmount), - }; - - // convert BigNumbers to strings - // so they are digestible by axios - const requestParams = { - ...requestParamsWithBigNumbers, - sellAmountBaseUnits: requestParamsWithBigNumbers.sellAmountBaseUnits - ? requestParamsWithBigNumbers.sellAmountBaseUnits.toString() - : undefined, - buyAmountBaseUnits: requestParamsWithBigNumbers.buyAmountBaseUnits - ? requestParamsWithBigNumbers.buyAmountBaseUnits.toString() - : undefined, - }; - - const partialLogEntry = { url, quoteType, requestParams, isBlacklisted: false }; - const timeBeforeAwait = Date.now(); const maxResponseTimeMs = options.makerEndpointMaxResponseTimeMs === undefined From c8886febb9819573721eaf23a2cb8c5ca46e21f2 Mon Sep 17 00:00:00 2001 From: Alex Kroeger Date: Wed, 7 Oct 2020 13:00:01 -0700 Subject: [PATCH 28/32] removed v0-specific code in asset-swapper --- .../exchange_swap_quote_consumer.ts | 115 ----- .../forwarder_swap_quote_consumer.ts | 198 -------- .../quote_consumers/swap_quote_consumer.ts | 11 +- .../utils/market_operation_utils/constants.ts | 1 - .../src/utils/market_operation_utils/index.ts | 4 - .../utils/market_operation_utils/orders.ts | 42 -- .../src/utils/market_operation_utils/path.ts | 12 +- .../src/utils/market_operation_utils/types.ts | 5 - .../test/exchange_swap_quote_consumer_test.ts | 289 ------------ .../forwarder_swap_quote_consumer_test.ts | 440 ------------------ .../test/market_operation_utils_test.ts | 59 --- 11 files changed, 5 insertions(+), 1171 deletions(-) delete mode 100644 packages/asset-swapper/src/quote_consumers/exchange_swap_quote_consumer.ts delete mode 100644 packages/asset-swapper/src/quote_consumers/forwarder_swap_quote_consumer.ts delete mode 100644 packages/asset-swapper/test/exchange_swap_quote_consumer_test.ts delete mode 100644 packages/asset-swapper/test/forwarder_swap_quote_consumer_test.ts diff --git a/packages/asset-swapper/src/quote_consumers/exchange_swap_quote_consumer.ts b/packages/asset-swapper/src/quote_consumers/exchange_swap_quote_consumer.ts deleted file mode 100644 index 99168ed10c..0000000000 --- a/packages/asset-swapper/src/quote_consumers/exchange_swap_quote_consumer.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { ContractAddresses } from '@0x/contract-addresses'; -import { ExchangeContract } from '@0x/contract-wrappers'; -import { providerUtils } from '@0x/utils'; -import { SupportedProvider, ZeroExProvider } from '@0x/web3-wrapper'; -import * as _ from 'lodash'; - -import { constants } from '../constants'; -import { - CalldataInfo, - MarketOperation, - SwapQuote, - SwapQuoteConsumerBase, - SwapQuoteConsumerOpts, - SwapQuoteExecutionOpts, - SwapQuoteGetOutputOpts, -} from '../types'; -import { assert } from '../utils/assert'; -import { swapQuoteConsumerUtils } from '../utils/swap_quote_consumer_utils'; - -export class ExchangeSwapQuoteConsumer implements SwapQuoteConsumerBase { - public readonly provider: ZeroExProvider; - public readonly chainId: number; - - private readonly _exchangeContract: ExchangeContract; - - constructor( - supportedProvider: SupportedProvider, - public readonly contractAddresses: ContractAddresses, - options: Partial = {}, - ) { - const { chainId } = _.merge({}, constants.DEFAULT_SWAP_QUOTER_OPTS, options); - assert.isNumber('chainId', chainId); - const provider = providerUtils.standardizeOrThrow(supportedProvider); - this.provider = provider; - this.chainId = chainId; - this._exchangeContract = new ExchangeContract(contractAddresses.exchange, supportedProvider); - } - - public async getCalldataOrThrowAsync( - quote: SwapQuote, - _opts: Partial = {}, - ): Promise { - assert.isValidSwapQuote('quote', quote); - const { orders } = quote; - const signatures = _.map(orders, o => o.signature); - - let calldataHexString; - if (quote.type === MarketOperation.Buy) { - calldataHexString = this._exchangeContract - .marketBuyOrdersFillOrKill(orders, quote.makerAssetFillAmount, signatures) - .getABIEncodedTransactionData(); - } else { - calldataHexString = this._exchangeContract - .marketSellOrdersFillOrKill(orders, quote.takerAssetFillAmount, signatures) - .getABIEncodedTransactionData(); - } - - return { - calldataHexString, - ethAmount: quote.worstCaseQuoteInfo.protocolFeeInWeiAmount, - toAddress: this._exchangeContract.address, - allowanceTarget: this.contractAddresses.erc20Proxy, - }; - } - - public async executeSwapQuoteOrThrowAsync( - quote: SwapQuote, - opts: Partial, - ): Promise { - assert.isValidSwapQuote('quote', quote); - - const { takerAddress, gasLimit, ethAmount } = opts; - - if (takerAddress !== undefined) { - assert.isETHAddressHex('takerAddress', takerAddress); - } - if (gasLimit !== undefined) { - assert.isNumber('gasLimit', gasLimit); - } - if (ethAmount !== undefined) { - assert.isBigNumber('ethAmount', ethAmount); - } - const { orders, gasPrice } = quote; - const signatures = orders.map(o => o.signature); - - const finalTakerAddress = await swapQuoteConsumerUtils.getTakerAddressOrThrowAsync(this.provider, opts); - const value = ethAmount || quote.worstCaseQuoteInfo.protocolFeeInWeiAmount; - let txHash: string; - if (quote.type === MarketOperation.Buy) { - const { makerAssetFillAmount } = quote; - txHash = await this._exchangeContract - .marketBuyOrdersFillOrKill(orders, makerAssetFillAmount, signatures) - .sendTransactionAsync({ - from: finalTakerAddress, - gas: gasLimit, - gasPrice, - value, - }); - } else { - const { takerAssetFillAmount } = quote; - txHash = await this._exchangeContract - .marketSellOrdersFillOrKill(orders, takerAssetFillAmount, signatures) - .sendTransactionAsync({ - from: finalTakerAddress, - gas: gasLimit, - gasPrice, - value, - }); - } - // TODO(dorothy-zbornak): Handle signature request denied - // (see contract-wrappers/decorators) - // and ExchangeRevertErrors.IncompleteFillError. - return txHash; - } -} diff --git a/packages/asset-swapper/src/quote_consumers/forwarder_swap_quote_consumer.ts b/packages/asset-swapper/src/quote_consumers/forwarder_swap_quote_consumer.ts deleted file mode 100644 index ed1e7aae95..0000000000 --- a/packages/asset-swapper/src/quote_consumers/forwarder_swap_quote_consumer.ts +++ /dev/null @@ -1,198 +0,0 @@ -import { ContractAddresses } from '@0x/contract-addresses'; -import { ForwarderContract } from '@0x/contract-wrappers'; -import { assetDataUtils } from '@0x/order-utils'; -import { providerUtils } from '@0x/utils'; -import { SupportedProvider, ZeroExProvider } from '@0x/web3-wrapper'; -import * as _ from 'lodash'; - -import { constants } from '../constants'; -import { - CalldataInfo, - MarketOperation, - SwapQuote, - SwapQuoteConsumerBase, - SwapQuoteConsumerOpts, - SwapQuoteExecutionOpts, - SwapQuoteGetOutputOpts, -} from '../types'; -import { affiliateFeeUtils } from '../utils/affiliate_fee_utils'; -import { assert } from '../utils/assert'; -import { swapQuoteConsumerUtils } from '../utils/swap_quote_consumer_utils'; - -const { NULL_ADDRESS } = constants; - -export class ForwarderSwapQuoteConsumer implements SwapQuoteConsumerBase { - public readonly provider: ZeroExProvider; - public readonly chainId: number; - public buyQuoteSellAmountScalingFactor = 1.0001; // 100% + 1 bps - - private readonly _forwarder: ForwarderContract; - - constructor( - supportedProvider: SupportedProvider, - public readonly contractAddresses: ContractAddresses, - options: Partial = {}, - ) { - const { chainId } = _.merge({}, constants.DEFAULT_SWAP_QUOTER_OPTS, options); - assert.isNumber('chainId', chainId); - const provider = providerUtils.standardizeOrThrow(supportedProvider); - this.provider = provider; - this.chainId = chainId; - this._forwarder = new ForwarderContract(contractAddresses.forwarder, supportedProvider); - } - - /** - * Given a SwapQuote, returns 'CalldataInfo' for a forwarder extension call. See type definition of CalldataInfo for more information. - * @param quote An object that conforms to SwapQuote. See type definition for more information. - * @param opts Options for getting CalldataInfo. See type definition for more information. - */ - public async getCalldataOrThrowAsync( - quote: SwapQuote, - opts: Partial = {}, - ): Promise { - assert.isValidForwarderSwapQuote('quote', quote, this._getEtherTokenAssetDataOrThrow()); - const { extensionContractOpts } = { ...constants.DEFAULT_FORWARDER_SWAP_QUOTE_GET_OPTS, ...opts }; - assert.isValidForwarderExtensionContractOpts('extensionContractOpts', extensionContractOpts); - const { feeRecipient, feePercentage } = extensionContractOpts; - const { orders, worstCaseQuoteInfo } = quote; - - const normalizedFeeRecipientAddress = feeRecipient.toLowerCase(); - const signatures = _.map(orders, o => o.signature); - const ethAmountWithFees = affiliateFeeUtils.getTotalEthAmountWithAffiliateFee( - { - ...worstCaseQuoteInfo, - // HACK(dorothy-zbornak): The forwarder contract has a rounding bug - // that causes buys of low-decimal tokens to not complete. - // Scaling the max sell amount by 1bps seems to be sufficient to - // overcome this. - ...(quote.type === MarketOperation.Buy - ? { - // tslint:disable-next-line: custom-no-magic-numbers - totalTakerAssetAmount: worstCaseQuoteInfo.totalTakerAssetAmount - .times(this.buyQuoteSellAmountScalingFactor) - .integerValue(), - } - : {}), - }, - feePercentage, - ); - const feeAmount = affiliateFeeUtils.getFeeAmount(worstCaseQuoteInfo, feePercentage); - - let calldataHexString; - if (quote.type === MarketOperation.Buy) { - calldataHexString = this._forwarder - .marketBuyOrdersWithEth( - orders, - quote.makerAssetFillAmount, - signatures, - [feeAmount], - [normalizedFeeRecipientAddress], - ) - .getABIEncodedTransactionData(); - } else { - calldataHexString = this._forwarder - .marketSellAmountWithEth( - orders, - quote.takerAssetFillAmount, - signatures, - [feeAmount], - [normalizedFeeRecipientAddress], - ) - .getABIEncodedTransactionData(); - } - - return { - calldataHexString, - toAddress: this._forwarder.address, - ethAmount: ethAmountWithFees, - allowanceTarget: NULL_ADDRESS, - }; - } - - /** - * Given a SwapQuote and desired rate (in Eth), attempt to execute the swap. - * @param quote An object that conforms to SwapQuote. See type definition for more information. - * @param opts Options for getting CalldataInfo. See type definition for more information. - */ - public async executeSwapQuoteOrThrowAsync( - quote: SwapQuote, - opts: Partial, - ): Promise { - assert.isValidForwarderSwapQuote('quote', quote, this._getEtherTokenAssetDataOrThrow()); - - const { ethAmount: providedEthAmount, takerAddress, gasLimit, extensionContractOpts } = { - ...constants.DEFAULT_FORWARDER_SWAP_QUOTE_EXECUTE_OPTS, - ...opts, - }; - - assert.isValidForwarderExtensionContractOpts('extensionContractOpts', extensionContractOpts); - - const { feeRecipient, feePercentage } = extensionContractOpts; - - if (providedEthAmount !== undefined) { - assert.isBigNumber('ethAmount', providedEthAmount); - } - if (takerAddress !== undefined) { - assert.isETHAddressHex('takerAddress', takerAddress); - } - if (gasLimit !== undefined) { - assert.isNumber('gasLimit', gasLimit); - } - const { orders, gasPrice } = quote; // tslint:disable-line:no-unused-variable - const signatures = orders.map(o => o.signature); - - // get taker address - const finalTakerAddress = await swapQuoteConsumerUtils.getTakerAddressOrThrowAsync(this.provider, opts); - // if no ethAmount is provided, default to the worst totalTakerAssetAmount - const ethAmountWithFees = - providedEthAmount || - affiliateFeeUtils.getTotalEthAmountWithAffiliateFee(quote.worstCaseQuoteInfo, feePercentage); - const feeAmount = affiliateFeeUtils.getFeeAmount( - { - ...quote.worstCaseQuoteInfo, - // HACK(dorothy-zbornak): The forwarder contract has a rounding bug - // that causes buys of low-decimal tokens to not complete. - // Scaling the max sell amount by 1bps seems to be sufficient to - // overcome this. - ...(quote.type === MarketOperation.Buy - ? { - // tslint:disable-next-line: custom-no-magic-numbers - totalTakerAssetAmount: quote.worstCaseQuoteInfo.totalTakerAssetAmount - .times(this.buyQuoteSellAmountScalingFactor) - .integerValue(), - } - : {}), - }, - feePercentage, - ); - let txHash: string; - if (quote.type === MarketOperation.Buy) { - const { makerAssetFillAmount } = quote; - txHash = await this._forwarder - .marketBuyOrdersWithEth(orders, makerAssetFillAmount, signatures, [feeAmount], [feeRecipient]) - .sendTransactionAsync({ - from: finalTakerAddress, - gas: gasLimit, - gasPrice, - value: ethAmountWithFees, - }); - } else { - txHash = await this._forwarder - .marketSellAmountWithEth(orders, quote.takerAssetFillAmount, signatures, [feeAmount], [feeRecipient]) - .sendTransactionAsync({ - from: finalTakerAddress, - gas: gasLimit, - gasPrice, - value: ethAmountWithFees, - }); - } - // TODO(dorothy-zbornak): Handle signature request denied - // (see contract-wrappers/decorators) - // and ForwarderRevertErrors.CompleteBuyFailed. - return txHash; - } - - private _getEtherTokenAssetDataOrThrow(): string { - return assetDataUtils.encodeERC20AssetData(this.contractAddresses.etherToken); - } -} diff --git a/packages/asset-swapper/src/quote_consumers/swap_quote_consumer.ts b/packages/asset-swapper/src/quote_consumers/swap_quote_consumer.ts index 029663978c..8a5bea1691 100644 --- a/packages/asset-swapper/src/quote_consumers/swap_quote_consumer.ts +++ b/packages/asset-swapper/src/quote_consumers/swap_quote_consumer.ts @@ -18,15 +18,11 @@ import { assert } from '../utils/assert'; import { swapQuoteConsumerUtils } from '../utils/swap_quote_consumer_utils'; import { ExchangeProxySwapQuoteConsumer } from './exchange_proxy_swap_quote_consumer'; -import { ExchangeSwapQuoteConsumer } from './exchange_swap_quote_consumer'; -import { ForwarderSwapQuoteConsumer } from './forwarder_swap_quote_consumer'; export class SwapQuoteConsumer implements SwapQuoteConsumerBase { public readonly provider: ZeroExProvider; public readonly chainId: number; - private readonly _exchangeConsumer: ExchangeSwapQuoteConsumer; - private readonly _forwarderConsumer: ForwarderSwapQuoteConsumer; private readonly _contractAddresses: ContractAddresses; private readonly _exchangeProxyConsumer: ExchangeProxySwapQuoteConsumer; @@ -45,8 +41,6 @@ export class SwapQuoteConsumer implements SwapQuoteConsumerBase { this.provider = provider; this.chainId = chainId; this._contractAddresses = options.contractAddresses || getContractAddressesForChainOrThrow(chainId); - this._exchangeConsumer = new ExchangeSwapQuoteConsumer(supportedProvider, this._contractAddresses, options); - this._forwarderConsumer = new ForwarderSwapQuoteConsumer(supportedProvider, this._contractAddresses, options); this._exchangeProxyConsumer = new ExchangeProxySwapQuoteConsumer( supportedProvider, this._contractAddresses, @@ -100,13 +94,12 @@ export class SwapQuoteConsumer implements SwapQuoteConsumerBase { } private async _getConsumerForSwapQuoteAsync(opts: Partial): Promise { + // ( akroeger)leaving this switch to use different contracts in the future switch (opts.useExtensionContract) { - case ExtensionContractType.Forwarder: - return this._forwarderConsumer; case ExtensionContractType.ExchangeProxy: return this._exchangeProxyConsumer; default: - return this._exchangeConsumer; + return this._exchangeProxyConsumer; } } } diff --git a/packages/asset-swapper/src/utils/market_operation_utils/constants.ts b/packages/asset-swapper/src/utils/market_operation_utils/constants.ts index f0c4961800..621c6802a9 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/constants.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/constants.ts @@ -62,7 +62,6 @@ export const DEFAULT_GET_MARKET_ORDERS_OPTS: GetMarketOrdersOpts = { gasSchedule: {}, exchangeProxyOverhead: () => ZERO_AMOUNT, allowFallback: true, - shouldBatchBridgeOrders: true, shouldGenerateQuoteReport: false, }; diff --git a/packages/asset-swapper/src/utils/market_operation_utils/index.ts b/packages/asset-swapper/src/utils/market_operation_utils/index.ts index 38c10a1f26..58fcf395c5 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/index.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/index.ts @@ -370,7 +370,6 @@ export class MarketOperationUtils { feeSchedule: _opts.feeSchedule, exchangeProxyOverhead: _opts.exchangeProxyOverhead, allowFallback: _opts.allowFallback, - shouldBatchBridgeOrders: _opts.shouldBatchBridgeOrders, }); // Compute Quote Report and return the results. @@ -408,7 +407,6 @@ export class MarketOperationUtils { feeSchedule: _opts.feeSchedule, exchangeProxyOverhead: _opts.exchangeProxyOverhead, allowFallback: _opts.allowFallback, - shouldBatchBridgeOrders: _opts.shouldBatchBridgeOrders, }); let quoteReport: QuoteReport | undefined; if (_opts.shouldGenerateQuoteReport) { @@ -508,7 +506,6 @@ export class MarketOperationUtils { excludedSources: _opts.excludedSources, feeSchedule: _opts.feeSchedule, allowFallback: _opts.allowFallback, - shouldBatchBridgeOrders: _opts.shouldBatchBridgeOrders, }, ); return optimizedOrders; @@ -555,7 +552,6 @@ export class MarketOperationUtils { orderDomain: this._orderDomain, contractAddresses: this.contractAddresses, bridgeSlippage: opts.bridgeSlippage || 0, - shouldBatchBridgeOrders: !!opts.shouldBatchBridgeOrders, }; // Convert native orders and dex quotes into `Fill` objects. diff --git a/packages/asset-swapper/src/utils/market_operation_utils/orders.ts b/packages/asset-swapper/src/utils/market_operation_utils/orders.ts index 3a0129f222..e4d04a1606 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/orders.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/orders.ts @@ -150,7 +150,6 @@ export interface CreateOrderFromPathOpts { orderDomain: OrderDomain; contractAddresses: ContractAddresses; bridgeSlippage: number; - shouldBatchBridgeOrders: boolean; } export function createOrdersFromTwoHopSample( @@ -331,47 +330,6 @@ export function createBridgeOrder( }; } -export function createBatchedBridgeOrder(fills: CollapsedFill[], opts: CreateOrderFromPathOpts): OptimizedMarketOrder { - const [makerToken, takerToken] = getMakerTakerTokens(opts); - let totalMakerAssetAmount = ZERO_AMOUNT; - let totalTakerAssetAmount = ZERO_AMOUNT; - const batchedBridgeData: DexForwaderBridgeData = { - inputToken: takerToken, - calls: [], - }; - for (const fill of fills) { - const bridgeOrder = createBridgeOrder(fill, makerToken, takerToken, opts); - totalMakerAssetAmount = totalMakerAssetAmount.plus(bridgeOrder.makerAssetAmount); - totalTakerAssetAmount = totalTakerAssetAmount.plus(bridgeOrder.takerAssetAmount); - const { bridgeAddress, bridgeData: orderBridgeData } = assetDataUtils.decodeAssetDataOrThrow( - bridgeOrder.makerAssetData, - ) as ERC20BridgeAssetData; - batchedBridgeData.calls.push({ - target: bridgeAddress, - bridgeData: orderBridgeData, - inputTokenAmount: bridgeOrder.takerAssetAmount, - outputTokenAmount: bridgeOrder.makerAssetAmount, - }); - } - const batchedBridgeAddress = opts.contractAddresses.dexForwarderBridge; - const batchedMakerAssetData = assetDataUtils.encodeERC20BridgeAssetData( - makerToken, - batchedBridgeAddress, - dexForwarderBridgeDataEncoder.encode(batchedBridgeData), - ); - return { - fills, - makerAssetData: batchedMakerAssetData, - takerAssetData: assetDataUtils.encodeERC20AssetData(takerToken), - makerAddress: batchedBridgeAddress, - makerAssetAmount: totalMakerAssetAmount, - takerAssetAmount: totalTakerAssetAmount, - fillableMakerAssetAmount: totalMakerAssetAmount, - fillableTakerAssetAmount: totalTakerAssetAmount, - ...createCommonBridgeOrderFields(opts.orderDomain), - }; -} - export function getMakerTakerTokens(opts: CreateOrderFromPathOpts): [string, string] { const makerToken = opts.side === MarketOperation.Sell ? opts.outputToken : opts.inputToken; const takerToken = opts.side === MarketOperation.Sell ? opts.inputToken : opts.outputToken; diff --git a/packages/asset-swapper/src/utils/market_operation_utils/path.ts b/packages/asset-swapper/src/utils/market_operation_utils/path.ts index e75e589081..f849aa4304 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/path.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/path.ts @@ -4,7 +4,6 @@ import { MarketOperation } from '../../types'; import { POSITIVE_INF, SOURCE_FLAGS, ZERO_AMOUNT } from './constants'; import { - createBatchedBridgeOrder, createBridgeOrder, createNativeOrder, CreateOrderFromPathOpts, @@ -123,14 +122,9 @@ export class Path { } contiguousBridgeFills.push(collapsedFills[j]); } - // Always use DexForwarderBridge unless configured not to - if (!opts.shouldBatchBridgeOrders) { - this.orders.push(createBridgeOrder(contiguousBridgeFills[0], makerToken, takerToken, opts)); - i += 1; - } else { - this.orders.push(createBatchedBridgeOrder(contiguousBridgeFills, opts)); - i += contiguousBridgeFills.length; - } + + this.orders.push(createBridgeOrder(contiguousBridgeFills[0], makerToken, takerToken, opts)); + i += 1; } return this as CollapsedPath; } diff --git a/packages/asset-swapper/src/utils/market_operation_utils/types.ts b/packages/asset-swapper/src/utils/market_operation_utils/types.ts index a91c38d1ae..0afc145d47 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/types.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/types.ts @@ -287,11 +287,6 @@ export interface GetMarketOrdersOpts { */ allowFallback: boolean; rfqt?: GetMarketOrdersRfqtOpts; - /** - * Whether to combine contiguous bridge orders into a single DexForwarderBridge - * order. Defaults to `true`. - */ - shouldBatchBridgeOrders: boolean; /** * Whether to generate a quote report */ diff --git a/packages/asset-swapper/test/exchange_swap_quote_consumer_test.ts b/packages/asset-swapper/test/exchange_swap_quote_consumer_test.ts deleted file mode 100644 index fd1ca162a7..0000000000 --- a/packages/asset-swapper/test/exchange_swap_quote_consumer_test.ts +++ /dev/null @@ -1,289 +0,0 @@ -import { ContractAddresses } from '@0x/contract-addresses'; -import { ERC20TokenContract, ExchangeContract } from '@0x/contract-wrappers'; -import { constants as devConstants, OrderFactory } from '@0x/contracts-test-utils'; -import { BlockchainLifecycle, tokenUtils } from '@0x/dev-utils'; -import { migrateOnceAsync } from '@0x/migrations'; -import { assetDataUtils } from '@0x/order-utils'; -import { BigNumber } from '@0x/utils'; -import * as chai from 'chai'; -import 'mocha'; - -import { SwapQuote } from '../src'; -import { constants } from '../src/constants'; -import { ExchangeSwapQuoteConsumer } from '../src/quote_consumers/exchange_swap_quote_consumer'; -import { MarketOperation, SignedOrderWithFillableAmounts } from '../src/types'; - -import { chaiSetup } from './utils/chai_setup'; -import { getFullyFillableSwapQuoteWithNoFeesAsync } from './utils/swap_quote'; -import { provider, web3Wrapper } from './utils/web3_wrapper'; - -chaiSetup.configure(); -const expect = chai.expect; -const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); - -const GAS_PRICE = new BigNumber(devConstants.DEFAULT_GAS_PRICE); -const ONE_ETH_IN_WEI = new BigNumber(1000000000000000000); -const TESTRPC_CHAIN_ID = devConstants.TESTRPC_CHAIN_ID; -const UNLIMITED_ALLOWANCE = new BigNumber(2).pow(256).minus(1); // tslint:disable-line:custom-no-magic-numbers - -const PARTIAL_PRUNED_SIGNED_ORDERS_FEELESS: Array> = [ - { - takerAssetAmount: new BigNumber(5).multipliedBy(ONE_ETH_IN_WEI), - makerAssetAmount: new BigNumber(2).multipliedBy(ONE_ETH_IN_WEI), - fillableTakerAssetAmount: new BigNumber(5).multipliedBy(ONE_ETH_IN_WEI), - fillableMakerAssetAmount: new BigNumber(2).multipliedBy(ONE_ETH_IN_WEI), - }, - { - takerAssetAmount: new BigNumber(3).multipliedBy(ONE_ETH_IN_WEI), - makerAssetAmount: new BigNumber(3).multipliedBy(ONE_ETH_IN_WEI), - fillableTakerAssetAmount: new BigNumber(3).multipliedBy(ONE_ETH_IN_WEI), - fillableMakerAssetAmount: new BigNumber(3).multipliedBy(ONE_ETH_IN_WEI), - }, - { - takerAssetAmount: new BigNumber(2).multipliedBy(ONE_ETH_IN_WEI), - makerAssetAmount: new BigNumber(5).multipliedBy(ONE_ETH_IN_WEI), - fillableTakerAssetAmount: new BigNumber(2).multipliedBy(ONE_ETH_IN_WEI), - fillableMakerAssetAmount: new BigNumber(5).multipliedBy(ONE_ETH_IN_WEI), - }, -]; - -const expectMakerAndTakerBalancesAsyncFactory = ( - erc20TokenContract: ERC20TokenContract, - makerAddress: string, - takerAddress: string, -) => async (expectedMakerBalance: BigNumber, expectedTakerBalance: BigNumber) => { - const makerBalance = await erc20TokenContract.balanceOf(makerAddress).callAsync(); - const takerBalance = await erc20TokenContract.balanceOf(takerAddress).callAsync(); - expect(makerBalance).to.bignumber.equal(expectedMakerBalance); - expect(takerBalance).to.bignumber.equal(expectedTakerBalance); -}; - -describe('ExchangeSwapQuoteConsumer', () => { - let userAddresses: string[]; - let erc20MakerTokenContract: ERC20TokenContract; - let erc20TakerTokenContract: ERC20TokenContract; - let coinbaseAddress: string; - let makerAddress: string; - let takerAddress: string; - let orderFactory: OrderFactory; - let feeRecipient: string; - let makerTokenAddress: string; - let takerTokenAddress: string; - let makerAssetData: string; - let takerAssetData: string; - let contractAddresses: ContractAddresses; - let exchangeContract: ExchangeContract; - - const chainId = TESTRPC_CHAIN_ID; - - let orders: SignedOrderWithFillableAmounts[]; - let marketSellSwapQuote: SwapQuote; - let marketBuySwapQuote: SwapQuote; - let swapQuoteConsumer: ExchangeSwapQuoteConsumer; - let expectMakerAndTakerBalancesForMakerAssetAsync: ( - expectedMakerBalance: BigNumber, - expectedTakerBalance: BigNumber, - ) => Promise; - let expectMakerAndTakerBalancesForTakerAssetAsync: ( - expectedMakerBalance: BigNumber, - expectedTakerBalance: BigNumber, - ) => Promise; - - before(async () => { - contractAddresses = await migrateOnceAsync(provider); - await blockchainLifecycle.startAsync(); - userAddresses = await web3Wrapper.getAvailableAddressesAsync(); - [coinbaseAddress, takerAddress, makerAddress, feeRecipient] = userAddresses; - [makerTokenAddress, takerTokenAddress] = tokenUtils.getDummyERC20TokenAddresses(); - [makerAssetData, takerAssetData] = [ - assetDataUtils.encodeERC20AssetData(makerTokenAddress), - assetDataUtils.encodeERC20AssetData(takerTokenAddress), - ]; - erc20MakerTokenContract = new ERC20TokenContract(makerTokenAddress, provider); - erc20TakerTokenContract = new ERC20TokenContract(takerTokenAddress, provider); - exchangeContract = new ExchangeContract(contractAddresses.exchange, provider); - // Configure order defaults - const defaultOrderParams = { - ...devConstants.STATIC_ORDER_PARAMS, - makerAddress, - takerAddress, - makerAssetData, - takerAssetData, - makerFeeAssetData: constants.NULL_ERC20_ASSET_DATA, - takerFeeAssetData: constants.NULL_ERC20_ASSET_DATA, - makerFee: constants.ZERO_AMOUNT, - takerFee: constants.ZERO_AMOUNT, - feeRecipientAddress: feeRecipient, - exchangeAddress: contractAddresses.exchange, - chainId, - }; - const privateKey = devConstants.TESTRPC_PRIVATE_KEYS[userAddresses.indexOf(makerAddress)]; - orderFactory = new OrderFactory(privateKey, defaultOrderParams); - expectMakerAndTakerBalancesForTakerAssetAsync = expectMakerAndTakerBalancesAsyncFactory( - erc20TakerTokenContract, - makerAddress, - takerAddress, - ); - expectMakerAndTakerBalancesForMakerAssetAsync = expectMakerAndTakerBalancesAsyncFactory( - erc20MakerTokenContract, - makerAddress, - takerAddress, - ); - }); - after(async () => { - await blockchainLifecycle.revertAsync(); - }); - beforeEach(async () => { - await blockchainLifecycle.startAsync(); - orders = []; - for (const partialOrder of PARTIAL_PRUNED_SIGNED_ORDERS_FEELESS) { - const order = await orderFactory.newSignedOrderAsync(partialOrder); - const prunedOrder = { - ...order, - ...partialOrder, - }; - orders.push(prunedOrder as SignedOrderWithFillableAmounts); - } - - marketSellSwapQuote = await getFullyFillableSwapQuoteWithNoFeesAsync( - makerAssetData, - takerAssetData, - orders, - MarketOperation.Sell, - GAS_PRICE, - ); - - marketBuySwapQuote = await getFullyFillableSwapQuoteWithNoFeesAsync( - makerAssetData, - takerAssetData, - orders, - MarketOperation.Buy, - GAS_PRICE, - ); - - swapQuoteConsumer = new ExchangeSwapQuoteConsumer(provider, contractAddresses, { - chainId, - }); - - await erc20MakerTokenContract - .transfer(makerAddress, marketBuySwapQuote.worstCaseQuoteInfo.makerAssetAmount) - .sendTransactionAsync({ - from: coinbaseAddress, - }); - await erc20TakerTokenContract - .transfer(takerAddress, marketBuySwapQuote.worstCaseQuoteInfo.totalTakerAssetAmount) - .sendTransactionAsync({ - from: coinbaseAddress, - }); - await erc20MakerTokenContract - .approve(contractAddresses.erc20Proxy, UNLIMITED_ALLOWANCE) - .sendTransactionAsync({ from: makerAddress }); - await erc20TakerTokenContract - .approve(contractAddresses.erc20Proxy, UNLIMITED_ALLOWANCE) - .sendTransactionAsync({ from: takerAddress }); - }); - afterEach(async () => { - await blockchainLifecycle.revertAsync(); - }); - describe('#executeSwapQuoteOrThrowAsync', () => { - /* - * Testing that SwapQuoteConsumer logic correctly performs a execution (doesn't throw or revert) - * Does not test the validity of the state change performed by the forwarder smart contract - */ - it('should perform a marketSell execution when provided a MarketSell type swapQuote', async () => { - await expectMakerAndTakerBalancesForMakerAssetAsync( - new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI), - constants.ZERO_AMOUNT, - ); - await expectMakerAndTakerBalancesForTakerAssetAsync( - constants.ZERO_AMOUNT, - new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI), - ); - await swapQuoteConsumer.executeSwapQuoteOrThrowAsync(marketSellSwapQuote, { - takerAddress, - gasLimit: 4000000, - }); - await expectMakerAndTakerBalancesForMakerAssetAsync( - constants.ZERO_AMOUNT, - new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI), - ); - await expectMakerAndTakerBalancesForTakerAssetAsync( - new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI), - constants.ZERO_AMOUNT, - ); - }); - it('should perform a marketBuy execution when provided a MarketBuy type swapQuote', async () => { - await expectMakerAndTakerBalancesForMakerAssetAsync( - new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI), - constants.ZERO_AMOUNT, - ); - await expectMakerAndTakerBalancesForTakerAssetAsync( - constants.ZERO_AMOUNT, - new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI), - ); - await swapQuoteConsumer.executeSwapQuoteOrThrowAsync(marketBuySwapQuote, { - takerAddress, - gasLimit: 4000000, - }); - await expectMakerAndTakerBalancesForMakerAssetAsync( - constants.ZERO_AMOUNT, - new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI), - ); - await expectMakerAndTakerBalancesForTakerAssetAsync( - new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI), - constants.ZERO_AMOUNT, - ); - }); - }); - - describe('#getCalldataOrThrow', () => { - describe('valid swap quote', async () => { - it('provide correct and optimized calldata options with default options for a marketSell SwapQuote (no affiliate fees)', async () => { - await expectMakerAndTakerBalancesForMakerAssetAsync( - new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI), - constants.ZERO_AMOUNT, - ); - const { calldataHexString, toAddress, ethAmount } = await swapQuoteConsumer.getCalldataOrThrowAsync( - marketSellSwapQuote, - {}, - ); - expect(toAddress).to.deep.equal(exchangeContract.address); - await web3Wrapper.sendTransactionAsync({ - from: takerAddress, - to: toAddress, - data: calldataHexString, - gas: 4000000, - gasPrice: GAS_PRICE, - value: ethAmount, - }); - await expectMakerAndTakerBalancesForMakerAssetAsync( - constants.ZERO_AMOUNT, - new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI), - ); - }); - it('provide correct and optimized calldata options with default options for a marketBuy SwapQuote (no affiliate fees)', async () => { - await expectMakerAndTakerBalancesForMakerAssetAsync( - new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI), - constants.ZERO_AMOUNT, - ); - const { calldataHexString, toAddress, ethAmount } = await swapQuoteConsumer.getCalldataOrThrowAsync( - marketBuySwapQuote, - {}, - ); - expect(toAddress).to.deep.equal(exchangeContract.address); - await web3Wrapper.sendTransactionAsync({ - from: takerAddress, - to: toAddress, - data: calldataHexString, - gas: 4000000, - gasPrice: GAS_PRICE, - value: ethAmount, - }); - await expectMakerAndTakerBalancesForMakerAssetAsync( - constants.ZERO_AMOUNT, - new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI), - ); - }); - }); - }); -}); diff --git a/packages/asset-swapper/test/forwarder_swap_quote_consumer_test.ts b/packages/asset-swapper/test/forwarder_swap_quote_consumer_test.ts deleted file mode 100644 index f1f1273fbb..0000000000 --- a/packages/asset-swapper/test/forwarder_swap_quote_consumer_test.ts +++ /dev/null @@ -1,440 +0,0 @@ -import { ContractAddresses } from '@0x/contract-addresses'; -import { ERC20TokenContract, ForwarderContract } from '@0x/contract-wrappers'; -import { constants as devConstants, OrderFactory } from '@0x/contracts-test-utils'; -import { BlockchainLifecycle, tokenUtils } from '@0x/dev-utils'; -import { migrateOnceAsync } from '@0x/migrations'; -import { assetDataUtils } from '@0x/order-utils'; -import { BigNumber } from '@0x/utils'; -import * as chai from 'chai'; -import 'mocha'; - -import { SwapQuote } from '../src'; -import { constants } from '../src/constants'; -import { ForwarderSwapQuoteConsumer } from '../src/quote_consumers/forwarder_swap_quote_consumer'; -import { MarketOperation, SignedOrderWithFillableAmounts } from '../src/types'; - -import { chaiSetup } from './utils/chai_setup'; -import { getFullyFillableSwapQuoteWithNoFeesAsync } from './utils/swap_quote'; -import { provider, web3Wrapper } from './utils/web3_wrapper'; - -chaiSetup.configure(); -const expect = chai.expect; -const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); - -const GAS_PRICE = new BigNumber(devConstants.DEFAULT_GAS_PRICE); -const ONE_ETH_IN_WEI = new BigNumber(1000000000000000000); -const TESTRPC_CHAIN_ID = devConstants.TESTRPC_CHAIN_ID; - -const UNLIMITED_ALLOWANCE_IN_BASE_UNITS = new BigNumber(2).pow(256).minus(1); // tslint:disable-line:custom-no-magic-numbers -const FEE_PERCENTAGE = 0.05; -const PARTIAL_PRUNED_SIGNED_ORDERS_FEELESS: Array> = [ - { - takerAssetAmount: new BigNumber(2).multipliedBy(ONE_ETH_IN_WEI), - makerAssetAmount: new BigNumber(2).multipliedBy(ONE_ETH_IN_WEI), - fillableTakerAssetAmount: new BigNumber(2).multipliedBy(ONE_ETH_IN_WEI), - fillableMakerAssetAmount: new BigNumber(2).multipliedBy(ONE_ETH_IN_WEI), - }, - { - takerAssetAmount: new BigNumber(1).multipliedBy(ONE_ETH_IN_WEI), - makerAssetAmount: new BigNumber(3).multipliedBy(ONE_ETH_IN_WEI), - fillableTakerAssetAmount: new BigNumber(1).multipliedBy(ONE_ETH_IN_WEI), - fillableMakerAssetAmount: new BigNumber(3).multipliedBy(ONE_ETH_IN_WEI), - }, - { - takerAssetAmount: new BigNumber(1).multipliedBy(ONE_ETH_IN_WEI), - makerAssetAmount: new BigNumber(5).multipliedBy(ONE_ETH_IN_WEI), - fillableTakerAssetAmount: new BigNumber(1).multipliedBy(ONE_ETH_IN_WEI), - fillableMakerAssetAmount: new BigNumber(5).multipliedBy(ONE_ETH_IN_WEI), - }, -]; - -const expectMakerAndTakerBalancesAsyncFactory = ( - erc20TokenContract: ERC20TokenContract, - makerAddress: string, - takerAddress: string, -) => async (expectedMakerBalance: BigNumber, expectedTakerBalance: BigNumber) => { - const makerBalance = await erc20TokenContract.balanceOf(makerAddress).callAsync(); - const takerBalance = await erc20TokenContract.balanceOf(takerAddress).callAsync(); - expect(makerBalance).to.bignumber.equal(expectedMakerBalance); - expect(takerBalance).to.bignumber.equal(expectedTakerBalance); -}; - -describe('ForwarderSwapQuoteConsumer', () => { - let userAddresses: string[]; - let coinbaseAddress: string; - let makerAddress: string; - let takerAddress: string; - let feeRecipient: string; - let makerTokenAddress: string; - let takerTokenAddress: string; - let makerAssetData: string; - let takerAssetData: string; - let orderFactory: OrderFactory; - let invalidOrderFactory: OrderFactory; - let wethAssetData: string; - let contractAddresses: ContractAddresses; - let erc20TokenContract: ERC20TokenContract; - let forwarderContract: ForwarderContract; - - let orders: SignedOrderWithFillableAmounts[]; - let invalidOrders: SignedOrderWithFillableAmounts[]; - let marketSellSwapQuote: SwapQuote; - let marketBuySwapQuote: SwapQuote; - let invalidMarketBuySwapQuote: SwapQuote; - let swapQuoteConsumer: ForwarderSwapQuoteConsumer; - let expectMakerAndTakerBalancesAsync: ( - expectedMakerBalance: BigNumber, - expectedTakerBalance: BigNumber, - ) => Promise; - const chainId = TESTRPC_CHAIN_ID; - - before(async () => { - contractAddresses = await migrateOnceAsync(provider); - await blockchainLifecycle.startAsync(); - userAddresses = await web3Wrapper.getAvailableAddressesAsync(); - [coinbaseAddress, takerAddress, makerAddress, feeRecipient] = userAddresses; - [makerTokenAddress, takerTokenAddress] = tokenUtils.getDummyERC20TokenAddresses(); - erc20TokenContract = new ERC20TokenContract(makerTokenAddress, provider); - forwarderContract = new ForwarderContract(contractAddresses.forwarder, provider); - [makerAssetData, takerAssetData, wethAssetData] = [ - assetDataUtils.encodeERC20AssetData(makerTokenAddress), - assetDataUtils.encodeERC20AssetData(takerTokenAddress), - assetDataUtils.encodeERC20AssetData(contractAddresses.etherToken), - ]; - // Configure order defaults - const defaultOrderParams = { - ...devConstants.STATIC_ORDER_PARAMS, - makerAddress, - takerAddress: constants.NULL_ADDRESS, - makerAssetData, - takerAssetData: wethAssetData, - makerFeeAssetData: constants.NULL_ERC20_ASSET_DATA, - takerFeeAssetData: constants.NULL_ERC20_ASSET_DATA, - makerFee: constants.ZERO_AMOUNT, - takerFee: constants.ZERO_AMOUNT, - feeRecipientAddress: feeRecipient, - exchangeAddress: contractAddresses.exchange, - chainId, - }; - const invalidDefaultOrderParams = { - ...defaultOrderParams, - ...{ - takerAssetData, - }, - }; - const privateKey = devConstants.TESTRPC_PRIVATE_KEYS[userAddresses.indexOf(makerAddress)]; - orderFactory = new OrderFactory(privateKey, defaultOrderParams); - expectMakerAndTakerBalancesAsync = expectMakerAndTakerBalancesAsyncFactory( - erc20TokenContract, - makerAddress, - takerAddress, - ); - invalidOrderFactory = new OrderFactory(privateKey, invalidDefaultOrderParams); - }); - after(async () => { - await blockchainLifecycle.revertAsync(); - }); - beforeEach(async () => { - await blockchainLifecycle.startAsync(); - const UNLIMITED_ALLOWANCE = UNLIMITED_ALLOWANCE_IN_BASE_UNITS; - - const totalFillableAmount = new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI); - - await erc20TokenContract.transfer(makerAddress, totalFillableAmount).sendTransactionAsync({ - from: coinbaseAddress, - }); - - await erc20TokenContract - .approve(contractAddresses.erc20Proxy, UNLIMITED_ALLOWANCE) - .sendTransactionAsync({ from: makerAddress }); - - await forwarderContract.approveMakerAssetProxy(makerAssetData).sendTransactionAsync({ from: makerAddress }); - - orders = []; - for (const partialOrder of PARTIAL_PRUNED_SIGNED_ORDERS_FEELESS) { - const order = await orderFactory.newSignedOrderAsync(partialOrder); - const prunedOrder = { - ...order, - ...partialOrder, - }; - orders.push(prunedOrder as SignedOrderWithFillableAmounts); - } - - invalidOrders = []; - for (const partialOrder of PARTIAL_PRUNED_SIGNED_ORDERS_FEELESS) { - const order = await invalidOrderFactory.newSignedOrderAsync(partialOrder); - const prunedOrder = { - ...order, - ...partialOrder, - }; - invalidOrders.push(prunedOrder as SignedOrderWithFillableAmounts); - } - - marketSellSwapQuote = await getFullyFillableSwapQuoteWithNoFeesAsync( - makerAssetData, - wethAssetData, - orders, - MarketOperation.Sell, - GAS_PRICE, - ); - - marketBuySwapQuote = await getFullyFillableSwapQuoteWithNoFeesAsync( - makerAssetData, - wethAssetData, - orders, - MarketOperation.Buy, - GAS_PRICE, - ); - - invalidMarketBuySwapQuote = await getFullyFillableSwapQuoteWithNoFeesAsync( - makerAssetData, - takerAssetData, - invalidOrders, - MarketOperation.Buy, - GAS_PRICE, - ); - - swapQuoteConsumer = new ForwarderSwapQuoteConsumer(provider, contractAddresses, { - chainId, - }); - swapQuoteConsumer.buyQuoteSellAmountScalingFactor = 1; - }); - afterEach(async () => { - await blockchainLifecycle.revertAsync(); - }); - describe('#executeSwapQuoteOrThrowAsync', () => { - describe('validation', () => { - it('should throw if swapQuote provided is not a valid forwarder SwapQuote (taker asset is wEth)', async () => { - expect( - swapQuoteConsumer.executeSwapQuoteOrThrowAsync(invalidMarketBuySwapQuote, { takerAddress }), - ).to.be.rejectedWith( - `Expected quote.orders[0] to have takerAssetData set as ${wethAssetData}, but is ${takerAssetData}`, - ); - }); - }); - - // TODO(david) test execution of swap quotes with fee orders - describe('valid swap quote', () => { - /* - * Testing that SwapQuoteConsumer logic correctly performs a execution (doesn't throw or revert) - * Does not test the validity of the state change performed by the forwarder smart contract - */ - it('should perform a marketBuy execution when provided a MarketBuy type swapQuote', async () => { - await expectMakerAndTakerBalancesAsync( - new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI), - constants.ZERO_AMOUNT, - ); - await swapQuoteConsumer.executeSwapQuoteOrThrowAsync(marketBuySwapQuote, { - takerAddress, - gasLimit: 4000000, - ethAmount: new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI), - }); - await expectMakerAndTakerBalancesAsync( - constants.ZERO_AMOUNT, - new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI), - ); - }); - - it('should perform a marketSell execution when provided a MarketSell type swapQuote', async () => { - await expectMakerAndTakerBalancesAsync( - new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI), - constants.ZERO_AMOUNT, - ); - await swapQuoteConsumer.executeSwapQuoteOrThrowAsync(marketSellSwapQuote, { - takerAddress, - gasLimit: 4000000, - }); - await expectMakerAndTakerBalancesAsync( - constants.ZERO_AMOUNT, - new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI), - ); - }); - - it('should perform a marketBuy execution with affiliate fees', async () => { - await expectMakerAndTakerBalancesAsync( - new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI), - constants.ZERO_AMOUNT, - ); - const feeRecipientEthBalanceBefore = await web3Wrapper.getBalanceInWeiAsync(feeRecipient); - await swapQuoteConsumer.executeSwapQuoteOrThrowAsync(marketBuySwapQuote, { - takerAddress, - gasLimit: 4000000, - extensionContractOpts: { - feePercentage: 0.05, - feeRecipient, - }, - }); - await expectMakerAndTakerBalancesAsync( - constants.ZERO_AMOUNT, - new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI), - ); - const feeRecipientEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(feeRecipient); - const totalEthSpent = marketBuySwapQuote.bestCaseQuoteInfo.totalTakerAssetAmount.plus( - marketBuySwapQuote.bestCaseQuoteInfo.protocolFeeInWeiAmount, - ); - expect(feeRecipientEthBalanceAfter.minus(feeRecipientEthBalanceBefore)).to.bignumber.equal( - new BigNumber(FEE_PERCENTAGE).times(totalEthSpent), - ); - }); - - it('should perform a marketSell execution with affiliate fees', async () => { - await expectMakerAndTakerBalancesAsync( - new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI), - constants.ZERO_AMOUNT, - ); - const feeRecipientEthBalanceBefore = await web3Wrapper.getBalanceInWeiAsync(feeRecipient); - await swapQuoteConsumer.executeSwapQuoteOrThrowAsync(marketSellSwapQuote, { - takerAddress, - gasLimit: 4000000, - extensionContractOpts: { - feePercentage: 0.05, - feeRecipient, - }, - }); - await expectMakerAndTakerBalancesAsync( - constants.ZERO_AMOUNT, - new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI), - ); - const feeRecipientEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(feeRecipient); - const totalEthSpent = marketBuySwapQuote.bestCaseQuoteInfo.totalTakerAssetAmount.plus( - marketBuySwapQuote.bestCaseQuoteInfo.protocolFeeInWeiAmount, - ); - expect(feeRecipientEthBalanceAfter.minus(feeRecipientEthBalanceBefore)).to.bignumber.equal( - new BigNumber(FEE_PERCENTAGE).times(totalEthSpent), - ); - }); - }); - }); - - describe('#getCalldataOrThrow', () => { - describe('validation', () => { - it('should throw if swap quote provided is not a valid forwarder SwapQuote (taker asset is WETH)', async () => { - expect(swapQuoteConsumer.getCalldataOrThrowAsync(invalidMarketBuySwapQuote, {})).to.be.rejectedWith( - `Expected quote.orders[0] to have takerAssetData set as ${wethAssetData}, but is ${takerAssetData}`, - ); - }); - }); - - describe('valid swap quote', async () => { - it('provide correct and optimized calldata options with default options for a marketSell SwapQuote (no affiliate fees)', async () => { - await expectMakerAndTakerBalancesAsync( - new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI), - constants.ZERO_AMOUNT, - ); - const { calldataHexString, toAddress, ethAmount } = await swapQuoteConsumer.getCalldataOrThrowAsync( - marketSellSwapQuote, - {}, - ); - expect(toAddress).to.deep.equal(forwarderContract.address); - await web3Wrapper.sendTransactionAsync({ - from: takerAddress, - to: toAddress, - data: calldataHexString, - value: ethAmount, - gasPrice: GAS_PRICE, - gas: 4000000, - }); - await expectMakerAndTakerBalancesAsync( - constants.ZERO_AMOUNT, - new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI), - ); - }); - it('provide correct and optimized calldata options with default options for a marketBuy SwapQuote (no affiliate fees)', async () => { - await expectMakerAndTakerBalancesAsync( - new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI), - constants.ZERO_AMOUNT, - ); - const { calldataHexString, toAddress, ethAmount } = await swapQuoteConsumer.getCalldataOrThrowAsync( - marketBuySwapQuote, - {}, - ); - expect(toAddress).to.deep.equal(contractAddresses.forwarder); - await web3Wrapper.sendTransactionAsync({ - from: takerAddress, - to: toAddress, - data: calldataHexString, - value: ethAmount, - gasPrice: GAS_PRICE, - gas: 4000000, - }); - await expectMakerAndTakerBalancesAsync( - constants.ZERO_AMOUNT, - new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI), - ); - }); - it('provide correct and optimized calldata options with affiliate fees for a marketSell SwapQuote', async () => { - await expectMakerAndTakerBalancesAsync( - new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI), - constants.ZERO_AMOUNT, - ); - const feeRecipientEthBalanceBefore = await web3Wrapper.getBalanceInWeiAsync(feeRecipient); - const { calldataHexString, toAddress, ethAmount } = await swapQuoteConsumer.getCalldataOrThrowAsync( - marketSellSwapQuote, - { - extensionContractOpts: { - feePercentage: 0.05, - feeRecipient, - }, - }, - ); - expect(toAddress).to.deep.equal(contractAddresses.forwarder); - await web3Wrapper.sendTransactionAsync({ - from: takerAddress, - to: toAddress, - data: calldataHexString, - value: ethAmount, - gasPrice: GAS_PRICE, - gas: 4000000, - }); - await expectMakerAndTakerBalancesAsync( - constants.ZERO_AMOUNT, - new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI), - ); - const totalEthSpent = marketBuySwapQuote.bestCaseQuoteInfo.totalTakerAssetAmount.plus( - marketBuySwapQuote.bestCaseQuoteInfo.protocolFeeInWeiAmount, - ); - const feeRecipientEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(feeRecipient); - expect(feeRecipientEthBalanceAfter.minus(feeRecipientEthBalanceBefore)).to.bignumber.equal( - new BigNumber(FEE_PERCENTAGE).times(totalEthSpent), - ); - }); - it('provide correct and optimized calldata options with affiliate fees for a marketBuy SwapQuote', async () => { - await expectMakerAndTakerBalancesAsync( - new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI), - constants.ZERO_AMOUNT, - ); - const feeRecipientEthBalanceBefore = await web3Wrapper.getBalanceInWeiAsync(feeRecipient); - const { calldataHexString, toAddress, ethAmount } = await swapQuoteConsumer.getCalldataOrThrowAsync( - marketBuySwapQuote, - { - extensionContractOpts: { - feePercentage: 0.05, - feeRecipient, - }, - }, - ); - expect(toAddress).to.deep.equal(contractAddresses.forwarder); - await web3Wrapper.sendTransactionAsync({ - from: takerAddress, - to: toAddress, - data: calldataHexString, - value: ethAmount, - gasPrice: GAS_PRICE, - gas: 4000000, - }); - await expectMakerAndTakerBalancesAsync( - constants.ZERO_AMOUNT, - new BigNumber(10).multipliedBy(ONE_ETH_IN_WEI), - ); - const totalEthSpent = marketBuySwapQuote.bestCaseQuoteInfo.totalTakerAssetAmount.plus( - marketBuySwapQuote.bestCaseQuoteInfo.protocolFeeInWeiAmount, - ); - const feeRecipientEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(feeRecipient); - expect(feeRecipientEthBalanceAfter.minus(feeRecipientEthBalanceBefore)).to.bignumber.equal( - new BigNumber(FEE_PERCENTAGE).times(totalEthSpent), - ); - }); - }); - }); - // tslint:disable-next-line: max-file-line-count -}); diff --git a/packages/asset-swapper/test/market_operation_utils_test.ts b/packages/asset-swapper/test/market_operation_utils_test.ts index 8c074996e9..1b0b0c9377 100644 --- a/packages/asset-swapper/test/market_operation_utils_test.ts +++ b/packages/asset-swapper/test/market_operation_utils_test.ts @@ -468,7 +468,6 @@ describe('MarketOperationUtils tests', () => { maxFallbackSlippage: 100, excludedSources: DEFAULT_EXCLUDED, allowFallback: false, - shouldBatchBridgeOrders: false, }; beforeEach(() => { @@ -883,7 +882,6 @@ describe('MarketOperationUtils tests', () => { excludedSources: SELL_SOURCES.concat(ERC20BridgeSource.Bancor), numSamples: 4, bridgeSlippage: 0, - shouldBatchBridgeOrders: false, }, ); const result = ordersAndReport.optimizedOrders; @@ -901,37 +899,6 @@ describe('MarketOperationUtils tests', () => { expect(getSellQuotesParams.liquidityProviderAddress).is.eql(registryAddress); }); - it('batches contiguous bridge sources', async () => { - const rates: RatesBySource = {}; - rates[ERC20BridgeSource.Uniswap] = [1, 0.01, 0.01, 0.01]; - rates[ERC20BridgeSource.Native] = [0.5, 0.01, 0.01, 0.01]; - rates[ERC20BridgeSource.Eth2Dai] = [0.49, 0.01, 0.01, 0.01]; - rates[ERC20BridgeSource.Curve] = [0.48, 0.01, 0.01, 0.01]; - replaceSamplerOps({ - getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates), - }); - const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync( - createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), - FILL_AMOUNT, - { - ...DEFAULT_OPTS, - numSamples: 4, - excludedSources: [ - ERC20BridgeSource.Kyber, - ..._.without(DEFAULT_OPTS.excludedSources, ERC20BridgeSource.Curve), - ], - shouldBatchBridgeOrders: true, - }, - ); - const improvedOrders = improvedOrdersResponse.optimizedOrders; - expect(improvedOrders).to.be.length(3); - const orderFillSources = getSortedOrderSources(MarketOperation.Sell, improvedOrders); - expect(orderFillSources).to.deep.eq([ - [ERC20BridgeSource.Uniswap], - [ERC20BridgeSource.Native], - [ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Curve], - ]); - }); it('factors in exchange proxy gas overhead', async () => { // Uniswap has a slightly better rate than LiquidityProvider, // but LiquidityProvider is better accounting for the EP gas overhead. @@ -990,7 +957,6 @@ describe('MarketOperationUtils tests', () => { maxFallbackSlippage: 100, excludedSources: DEFAULT_EXCLUDED, allowFallback: false, - shouldBatchBridgeOrders: false, }; beforeEach(() => { @@ -1342,31 +1308,6 @@ describe('MarketOperationUtils tests', () => { expect(orderSources.slice(firstSources.length).sort()).to.deep.eq(secondSources.sort()); }); - it('batches contiguous bridge sources', async () => { - const rates: RatesBySource = { ...ZERO_RATES }; - rates[ERC20BridgeSource.Native] = [0.3, 0.01, 0.01, 0.01]; - rates[ERC20BridgeSource.Eth2Dai] = [0.49, 0.02, 0.01, 0.01]; - rates[ERC20BridgeSource.Uniswap] = [0.48, 0.01, 0.01, 0.01]; - replaceSamplerOps({ - getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates), - }); - const improvedOrdersResponse = await marketOperationUtils.getMarketBuyOrdersAsync( - createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), - FILL_AMOUNT, - { - ...DEFAULT_OPTS, - numSamples: 4, - shouldBatchBridgeOrders: true, - }, - ); - const improvedOrders = improvedOrdersResponse.optimizedOrders; - expect(improvedOrders).to.be.length(2); - const orderFillSources = getSortedOrderSources(MarketOperation.Sell, improvedOrders); - expect(orderFillSources).to.deep.eq([ - [ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Uniswap], - [ERC20BridgeSource.Native], - ]); - }); it('factors in exchange proxy gas overhead', async () => { // Uniswap has a slightly better rate than LiquidityProvider, // but LiquidityProvider is better accounting for the EP gas overhead. From 629d48c76605740fc1f5e934f43fafe1f14f5ec9 Mon Sep 17 00:00:00 2001 From: Alex Kroeger Date: Wed, 7 Oct 2020 14:30:30 -0700 Subject: [PATCH 29/32] removed unused functions --- .../utils/market_operation_utils/orders.ts | 26 +------------------ .../test/market_operation_utils_test.ts | 17 ------------ 2 files changed, 1 insertion(+), 42 deletions(-) diff --git a/packages/asset-swapper/src/utils/market_operation_utils/orders.ts b/packages/asset-swapper/src/utils/market_operation_utils/orders.ts index e4d04a1606..955875a271 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/orders.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/orders.ts @@ -1,7 +1,7 @@ import { ContractAddresses } from '@0x/contract-addresses'; import { assetDataUtils, ERC20AssetData, generatePseudoRandomSalt, orderCalculationUtils } from '@0x/order-utils'; import { RFQTIndicativeQuote } from '@0x/quote-server'; -import { ERC20BridgeAssetData, SignedOrder } from '@0x/types'; +import { SignedOrder } from '@0x/types'; import { AbiEncoder, BigNumber } from '@0x/utils'; import { MarketOperation, SignedOrderWithFillableAmounts } from '../../types'; @@ -40,30 +40,6 @@ import { // tslint:disable completed-docs no-unnecessary-type-assertion -interface DexForwaderBridgeData { - inputToken: string; - calls: Array<{ - target: string; - inputTokenAmount: BigNumber; - outputTokenAmount: BigNumber; - bridgeData: string; - }>; -} - -const dexForwarderBridgeDataEncoder = AbiEncoder.create([ - { name: 'inputToken', type: 'address' }, - { - name: 'calls', - type: 'tuple[]', - components: [ - { name: 'target', type: 'address' }, - { name: 'inputTokenAmount', type: 'uint256' }, - { name: 'outputTokenAmount', type: 'uint256' }, - { name: 'bridgeData', type: 'bytes' }, - ], - }, -]); - export function createDummyOrderForSampler( makerAssetData: string, takerAssetData: string, diff --git a/packages/asset-swapper/test/market_operation_utils_test.ts b/packages/asset-swapper/test/market_operation_utils_test.ts index 1b0b0c9377..facd6aebda 100644 --- a/packages/asset-swapper/test/market_operation_utils_test.ts +++ b/packages/asset-swapper/test/market_operation_utils_test.ts @@ -265,23 +265,6 @@ describe('MarketOperationUtils tests', () => { return rates; } - function getSortedOrderSources(side: MarketOperation, orders: OptimizedMarketOrder[]): ERC20BridgeSource[][] { - return ( - orders - // 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 { From 1adb56f092c58b2f9f25131cb7ccf330ba8b2ec4 Mon Sep 17 00:00:00 2001 From: Alex Kroeger Date: Wed, 7 Oct 2020 14:58:15 -0700 Subject: [PATCH 30/32] prettier :facepalm: --- .../asset-swapper/src/utils/market_operation_utils/path.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/asset-swapper/src/utils/market_operation_utils/path.ts b/packages/asset-swapper/src/utils/market_operation_utils/path.ts index f849aa4304..8a633753f7 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/path.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/path.ts @@ -3,12 +3,7 @@ import { BigNumber } from '@0x/utils'; import { MarketOperation } from '../../types'; import { POSITIVE_INF, SOURCE_FLAGS, ZERO_AMOUNT } from './constants'; -import { - createBridgeOrder, - createNativeOrder, - CreateOrderFromPathOpts, - getMakerTakerTokens, -} from './orders'; +import { createBridgeOrder, createNativeOrder, CreateOrderFromPathOpts, getMakerTakerTokens } from './orders'; import { getCompleteRate, getRate } from './rate_utils'; import { CollapsedFill, From 40fe12a86b907237acd9559befa0353361d67cd6 Mon Sep 17 00:00:00 2001 From: Alex Kroeger Date: Thu, 8 Oct 2020 16:52:14 -0700 Subject: [PATCH 31/32] Updated changelog --- packages/asset-swapper/CHANGELOG.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/asset-swapper/CHANGELOG.json b/packages/asset-swapper/CHANGELOG.json index 81f87838c8..18d4a6999f 100644 --- a/packages/asset-swapper/CHANGELOG.json +++ b/packages/asset-swapper/CHANGELOG.json @@ -145,6 +145,10 @@ { "note": "Fix exchange proxy overhead gas being scaled by gas price", "pr": 2723 + }, + { + "note": "Remove v0-specifc code from asset-swapper", + "pr": 2725 } ] }, From 408e66e8b4c7603fa9e09bc0595deb00fe3104b2 Mon Sep 17 00:00:00 2001 From: Alex Kroeger Date: Thu, 8 Oct 2020 16:54:16 -0700 Subject: [PATCH 32/32] altered changelog wording --- packages/asset-swapper/CHANGELOG.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/asset-swapper/CHANGELOG.json b/packages/asset-swapper/CHANGELOG.json index 18d4a6999f..2757460f37 100644 --- a/packages/asset-swapper/CHANGELOG.json +++ b/packages/asset-swapper/CHANGELOG.json @@ -147,7 +147,7 @@ "pr": 2723 }, { - "note": "Remove v0-specifc code from asset-swapper", + "note": "Remove 0x-API swap/v0-specifc code from asset-swapper", "pr": 2725 } ]