From 3d4c03c9df2f496c234743fb13c1e17d55d4062a Mon Sep 17 00:00:00 2001 From: mzhu25 Date: Tue, 16 Mar 2021 22:20:33 -0700 Subject: [PATCH] Update asset-swapper to support MultiplexFeature (#168) * Update asset-swapper to support MultiplexFeature * Address PR feedback * Update changelogs --- contracts/zero-ex/CHANGELOG.json | 4 + contracts/zero-ex/src/index.ts | 1 + packages/asset-swapper/CHANGELOG.json | 4 + packages/asset-swapper/package.json | 3 +- .../exchange_proxy_swap_quote_consumer.ts | 272 +++++++++++------- .../src/quote_consumers/multiplex_encoders.ts | 23 ++ .../quote_consumers/quote_consumer_utils.ts | 175 +++++++++++ .../utils/market_operation_utils/constants.ts | 13 +- .../src/utils/market_operation_utils/fills.ts | 6 +- .../market_operation_utils/rate_utils.ts | 8 +- 10 files changed, 406 insertions(+), 103 deletions(-) create mode 100644 packages/asset-swapper/src/quote_consumers/multiplex_encoders.ts create mode 100644 packages/asset-swapper/src/quote_consumers/quote_consumer_utils.ts diff --git a/contracts/zero-ex/CHANGELOG.json b/contracts/zero-ex/CHANGELOG.json index aa595a0588..4c88884ea8 100644 --- a/contracts/zero-ex/CHANGELOG.json +++ b/contracts/zero-ex/CHANGELOG.json @@ -13,6 +13,10 @@ { "note": "Add BatchFillNativeOrdersFeature and MultiplexFeature", "pr": 140 + }, + { + "note": "Export MultiplexFeatureContract", + "pr": 168 } ] }, diff --git a/contracts/zero-ex/src/index.ts b/contracts/zero-ex/src/index.ts index f8cf1eff4c..4a91cb841d 100644 --- a/contracts/zero-ex/src/index.ts +++ b/contracts/zero-ex/src/index.ts @@ -45,6 +45,7 @@ export { ITransformERC20FeatureContract, IZeroExContract, LogMetadataTransformerContract, + MultiplexFeatureContract, PayTakerTransformerContract, PositiveSlippageFeeTransformerContract, WethTransformerContract, diff --git a/packages/asset-swapper/CHANGELOG.json b/packages/asset-swapper/CHANGELOG.json index 77f500b10c..4bec071422 100644 --- a/packages/asset-swapper/CHANGELOG.json +++ b/packages/asset-swapper/CHANGELOG.json @@ -9,6 +9,10 @@ { "note": "Enable the ability to send RFQT requests thru a proxy", "pr": 159 + }, + { + "note": "Add support for MultiplexFeature", + "pr": 168 } ] }, diff --git a/packages/asset-swapper/package.json b/packages/asset-swapper/package.json index 41e50c6d09..e7ac2be5e7 100644 --- a/packages/asset-swapper/package.json +++ b/packages/asset-swapper/package.json @@ -61,6 +61,8 @@ "@0x/base-contract": "^6.2.18", "@0x/contract-addresses": "^5.11.0", "@0x/contract-wrappers": "^13.13.0", + "@0x/contracts-erc20": "^3.3.4", + "@0x/contracts-zero-ex": "^0.19.0", "@0x/dev-utils": "^4.2.1", "@0x/json-schemas": "^5.4.1", "@0x/protocol-utils": "^1.3.0", @@ -93,7 +95,6 @@ "@0x/contracts-gen": "^2.0.32", "@0x/contracts-test-utils": "^5.3.22", "@0x/contracts-utils": "^4.7.4", - "@0x/contracts-zero-ex": "^0.19.0", "@0x/mesh-rpc-client": "^9.4.2", "@0x/migrations": "^7.0.0", "@0x/sol-compiler": "^4.6.1", diff --git a/packages/asset-swapper/src/quote_consumers/exchange_proxy_swap_quote_consumer.ts b/packages/asset-swapper/src/quote_consumers/exchange_proxy_swap_quote_consumer.ts index f34bff488d..cfc88fd549 100644 --- a/packages/asset-swapper/src/quote_consumers/exchange_proxy_swap_quote_consumer.ts +++ b/packages/asset-swapper/src/quote_consumers/exchange_proxy_swap_quote_consumer.ts @@ -1,5 +1,6 @@ import { ContractAddresses } from '@0x/contract-addresses'; -import { IZeroExContract } from '@0x/contract-wrappers'; +import { WETH9Contract } from '@0x/contracts-erc20'; +import { IZeroExContract, MultiplexFeatureContract } from '@0x/contracts-zero-ex'; import { encodeAffiliateFeeTransformerData, encodeCurveLiquidityProviderData, @@ -8,7 +9,6 @@ import { encodePositiveSlippageFeeTransformerData, encodeWethTransformerData, ETH_TOKEN_ADDRESS, - FillQuoteTransformerData, FillQuoteTransformerOrderType, FillQuoteTransformerSide, findTransformerNonce, @@ -23,7 +23,6 @@ import { CalldataInfo, ExchangeProxyContractOpts, MarketBuySwapQuote, - MarketOperation, MarketSellSwapQuote, SwapQuote, SwapQuoteConsumerBase, @@ -36,24 +35,30 @@ import { CURVE_LIQUIDITY_PROVIDER_BY_CHAIN_ID, MOONISWAP_LIQUIDITY_PROVIDER_BY_CHAIN_ID, } from '../utils/market_operation_utils/constants'; -import { - createBridgeDataForBridgeOrder, - getERC20BridgeSourceToBridgeSource, - poolEncoder, -} from '../utils/market_operation_utils/orders'; +import { poolEncoder } from '../utils/market_operation_utils/orders'; import { CurveFillData, ERC20BridgeSource, LiquidityProviderFillData, MooniswapFillData, - NativeLimitOrderFillData, - NativeRfqOrderFillData, OptimizedMarketBridgeOrder, - OptimizedMarketOrder, - OptimizedMarketOrderBase, UniswapV2FillData, } from '../utils/market_operation_utils/types'; +import { + multiplexPlpEncoder, + multiplexRfqEncoder, + multiplexTransformERC20Encoder, + multiplexUniswapEncoder, +} from './multiplex_encoders'; +import { + getFQTTransformerDataFromOptimizedOrders, + isBuyQuote, + isDirectSwapCompatible, + isMultiplexBatchFillCompatible, + isMultiplexMultiHopFillCompatible, +} from './quote_consumer_utils'; + // tslint:disable-next-line:custom-no-magic-numbers const MAX_UINT256 = new BigNumber(2).pow(256).minus(1); const { NULL_ADDRESS, NULL_BYTES, ZERO_AMOUNT } = constants; @@ -70,6 +75,7 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase { }; private readonly _exchangeProxy: IZeroExContract; + private readonly _multiplex: MultiplexFeatureContract; constructor( supportedProvider: SupportedProvider, @@ -83,6 +89,7 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase { this.chainId = chainId; this.contractAddresses = contractAddresses; this._exchangeProxy = new IZeroExContract(contractAddresses.exchangeProxy, supportedProvider); + this._multiplex = new MultiplexFeatureContract(contractAddresses.exchangeProxy, supportedProvider); this.transformerNonces = { wethTransformer: findTransformerNonce( contractAddresses.transformers.wethTransformer, @@ -229,6 +236,25 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase { }; } + if (isMultiplexBatchFillCompatible(quote, optsWithDefaults)) { + return { + calldataHexString: this._encodeMultiplexBatchFillCalldata(quote), + ethAmount, + toAddress: this._exchangeProxy.address, + allowanceTarget: this._exchangeProxy.address, + gasOverhead: ZERO_AMOUNT, + }; + } + if (isMultiplexMultiHopFillCompatible(quote, optsWithDefaults)) { + return { + calldataHexString: this._encodeMultiplexMultiHopFillCalldata(quote, optsWithDefaults), + ethAmount, + toAddress: this._exchangeProxy.address, + allowanceTarget: this._exchangeProxy.address, + gasOverhead: ZERO_AMOUNT, + }; + } + // Build up the transforms. const transforms = []; if (isFromETH) { @@ -380,91 +406,145 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase { ): Promise { throw new Error('Execution not supported for Exchange Proxy quotes'); } -} -function isDirectSwapCompatible( - quote: SwapQuote, - opts: ExchangeProxyContractOpts, - directSources: ERC20BridgeSource[], -): boolean { - // Must not be a mtx. - if (opts.isMetaTransaction) { - return false; - } - // Must not have an affiliate fee. - if (!opts.affiliateFee.buyTokenFeeAmount.eq(0) || !opts.affiliateFee.sellTokenFeeAmount.eq(0)) { - return false; - } - // Must not have a positive slippage fee. - if (opts.affiliateFee.feeType === AffiliateFeeType.PositiveSlippageFee) { - return false; - } - // Must be a single order. - if (quote.orders.length !== 1) { - return false; - } - const order = quote.orders[0]; - if (!directSources.includes(order.source)) { - return false; - } - // VIP does not support selling the entire balance - if (opts.shouldSellEntireBalance) { - return false; - } - return true; -} - -function isBuyQuote(quote: SwapQuote): quote is MarketBuySwapQuote { - return quote.type === MarketOperation.Buy; -} - -function isOptimizedBridgeOrder(x: OptimizedMarketOrder): x is OptimizedMarketBridgeOrder { - return x.type === FillQuoteTransformerOrderType.Bridge; -} - -function isOptimizedLimitOrder(x: OptimizedMarketOrder): x is OptimizedMarketOrderBase { - return x.type === FillQuoteTransformerOrderType.Limit; -} - -function isOptimizedRfqOrder(x: OptimizedMarketOrder): x is OptimizedMarketOrderBase { - return x.type === FillQuoteTransformerOrderType.Rfq; -} - -function getFQTTransformerDataFromOptimizedOrders( - orders: OptimizedMarketOrder[], -): Pick { - const fqtData: Pick = { - bridgeOrders: [], - limitOrders: [], - rfqOrders: [], - fillSequence: [], - }; - - for (const order of orders) { - if (isOptimizedBridgeOrder(order)) { - fqtData.bridgeOrders.push({ - bridgeData: createBridgeDataForBridgeOrder(order), - makerTokenAmount: order.makerAmount, - takerTokenAmount: order.takerAmount, - source: getERC20BridgeSourceToBridgeSource(order.source), - }); - } else if (isOptimizedLimitOrder(order)) { - fqtData.limitOrders.push({ - order: order.fillData.order, - signature: order.fillData.signature, - maxTakerTokenFillAmount: order.takerAmount, - }); - } else if (isOptimizedRfqOrder(order)) { - fqtData.rfqOrders.push({ - order: order.fillData.order, - signature: order.fillData.signature, - maxTakerTokenFillAmount: order.takerAmount, - }); - } else { - // Should never happen - throw new Error('Unknown Order type'); + private _encodeMultiplexBatchFillCalldata(quote: SwapQuote): string { + const wrappedBatchCalls = []; + for_loop: for (const [i, order] of quote.orders.entries()) { + switch_statement: switch (order.source) { + case ERC20BridgeSource.Native: + if (order.type !== FillQuoteTransformerOrderType.Rfq) { + // Should never happen because we check `isMultiplexBatchFillCompatible` + // before calling this function. + throw new Error('Multiplex batch fill only supported for RFQ native orders'); + } + wrappedBatchCalls.push({ + selector: this._exchangeProxy.getSelector('_fillRfqOrder'), + sellAmount: order.takerAmount, + data: multiplexRfqEncoder.encode({ + order: order.fillData.order, + signature: order.fillData.signature, + }), + }); + break switch_statement; + case ERC20BridgeSource.UniswapV2: + case ERC20BridgeSource.SushiSwap: + wrappedBatchCalls.push({ + selector: this._multiplex.getSelector('_sellToUniswap'), + sellAmount: order.takerAmount, + data: multiplexUniswapEncoder.encode({ + tokens: (order.fillData as UniswapV2FillData).tokenAddressPath, + isSushi: order.source === ERC20BridgeSource.SushiSwap, + }), + }); + break switch_statement; + case ERC20BridgeSource.LiquidityProvider: + wrappedBatchCalls.push({ + selector: this._multiplex.getSelector('_sellToLiquidityProvider'), + sellAmount: order.takerAmount, + data: multiplexPlpEncoder.encode({ + provider: (order.fillData as LiquidityProviderFillData).poolAddress, + auxiliaryData: NULL_BYTES, + }), + }); + break switch_statement; + default: + const fqtData = encodeFillQuoteTransformerData({ + side: FillQuoteTransformerSide.Sell, + sellToken: quote.takerToken, + buyToken: quote.makerToken, + ...getFQTTransformerDataFromOptimizedOrders(quote.orders.slice(i)), + refundReceiver: NULL_ADDRESS, + fillAmount: MAX_UINT256, + }); + const transformations = [ + { deploymentNonce: this.transformerNonces.fillQuoteTransformer, data: fqtData }, + { + deploymentNonce: this.transformerNonces.payTakerTransformer, + data: encodePayTakerTransformerData({ + tokens: [quote.takerToken, quote.makerToken], + amounts: [], + }), + }, + ]; + wrappedBatchCalls.push({ + selector: this._exchangeProxy.getSelector('_transformERC20'), + sellAmount: BigNumber.sum(...quote.orders.slice(i).map(o => o.takerAmount)), + data: multiplexTransformERC20Encoder.encode({ + transformations, + ethValue: constants.ZERO_AMOUNT, + }), + }); + break for_loop; + } } - fqtData.fillSequence.push(order.type); + return this._exchangeProxy + .batchFill( + { + inputToken: quote.takerToken, + outputToken: quote.makerToken, + sellAmount: quote.worstCaseQuoteInfo.totalTakerAmount, + calls: wrappedBatchCalls, + }, + quote.worstCaseQuoteInfo.makerAmount, + ) + .getABIEncodedTransactionData(); + } + + private _encodeMultiplexMultiHopFillCalldata(quote: SwapQuote, opts: ExchangeProxyContractOpts): string { + const weth = new WETH9Contract(NULL_ADDRESS, this.provider); + const wrappedMultiHopCalls = []; + if (opts.isFromETH) { + wrappedMultiHopCalls.push({ + selector: weth.getSelector('deposit'), + data: NULL_BYTES, + }); + } + const [firstHopOrder, secondHopOrder] = quote.orders; + const intermediateToken = firstHopOrder.makerToken; + for (const order of [firstHopOrder, secondHopOrder]) { + switch (order.source) { + case ERC20BridgeSource.UniswapV2: + case ERC20BridgeSource.SushiSwap: + wrappedMultiHopCalls.push({ + selector: this._multiplex.getSelector('_sellToUniswap'), + data: multiplexUniswapEncoder.encode({ + tokens: (order.fillData as UniswapV2FillData).tokenAddressPath, + isSushi: order.source === ERC20BridgeSource.SushiSwap, + }), + }); + break; + case ERC20BridgeSource.LiquidityProvider: + wrappedMultiHopCalls.push({ + selector: this._multiplex.getSelector('_sellToLiquidityProvider'), + data: multiplexPlpEncoder.encode({ + tokens: (order.fillData as LiquidityProviderFillData).poolAddress, + auxiliaryData: NULL_BYTES, + }), + }); + break; + default: + // Note: we'll need to redeploy TransformERC20Feature before we can + // use other sources + // Should never happen because we check `isMultiplexMultiHopFillCompatible` + // before calling this function. + throw new Error(`Multiplex multi-hop unsupported source: ${order.source}`); + } + } + if (opts.isToETH) { + wrappedMultiHopCalls.push({ + selector: weth.getSelector('withdraw'), + data: NULL_BYTES, + }); + } + return this._exchangeProxy + .multiHopFill( + { + tokens: [quote.takerToken, intermediateToken, quote.makerToken], + sellAmount: quote.worstCaseQuoteInfo.totalTakerAmount, + calls: wrappedMultiHopCalls, + }, + quote.worstCaseQuoteInfo.makerAmount, + ) + .getABIEncodedTransactionData(); } - return fqtData; } diff --git a/packages/asset-swapper/src/quote_consumers/multiplex_encoders.ts b/packages/asset-swapper/src/quote_consumers/multiplex_encoders.ts new file mode 100644 index 0000000000..8bfec77e97 --- /dev/null +++ b/packages/asset-swapper/src/quote_consumers/multiplex_encoders.ts @@ -0,0 +1,23 @@ +import { RfqOrder, SIGNATURE_ABI } from '@0x/protocol-utils'; +import { AbiEncoder } from '@0x/utils'; + +export const multiplexTransformERC20Encoder = AbiEncoder.create([ + { + name: 'transformations', + type: 'tuple[]', + components: [{ name: 'deploymentNonce', type: 'uint32' }, { name: 'data', type: 'bytes' }], + }, + { name: 'ethValue', type: 'uint256' }, +]); +export const multiplexRfqEncoder = AbiEncoder.create([ + { name: 'order', type: 'tuple', components: RfqOrder.STRUCT_ABI }, + { name: 'signature', type: 'tuple', components: SIGNATURE_ABI }, +]); +export const multiplexUniswapEncoder = AbiEncoder.create([ + { name: 'tokens', type: 'address[]' }, + { name: 'isSushi', type: 'bool' }, +]); +export const multiplexPlpEncoder = AbiEncoder.create([ + { name: 'provider', type: 'address' }, + { name: 'auxiliaryData', type: 'bytes' }, +]); diff --git a/packages/asset-swapper/src/quote_consumers/quote_consumer_utils.ts b/packages/asset-swapper/src/quote_consumers/quote_consumer_utils.ts new file mode 100644 index 0000000000..b298802e07 --- /dev/null +++ b/packages/asset-swapper/src/quote_consumers/quote_consumer_utils.ts @@ -0,0 +1,175 @@ +import { FillQuoteTransformerData, FillQuoteTransformerOrderType } from '@0x/protocol-utils'; + +import { AffiliateFeeType, ExchangeProxyContractOpts, MarketBuySwapQuote, MarketOperation, SwapQuote } from '../types'; +import { + createBridgeDataForBridgeOrder, + getERC20BridgeSourceToBridgeSource, +} from '../utils/market_operation_utils/orders'; +import { + ERC20BridgeSource, + NativeLimitOrderFillData, + NativeRfqOrderFillData, + OptimizedMarketBridgeOrder, + OptimizedMarketOrder, + OptimizedMarketOrderBase, +} from '../utils/market_operation_utils/types'; + +const MULTIPLEX_BATCH_FILL_SOURCES = [ + ERC20BridgeSource.UniswapV2, + ERC20BridgeSource.SushiSwap, + ERC20BridgeSource.LiquidityProvider, + ERC20BridgeSource.Native, +]; + +/** + * Returns true iff a quote can be filled via `MultiplexFeature.batchFill`. + */ +export function isMultiplexBatchFillCompatible(quote: SwapQuote, opts: ExchangeProxyContractOpts): boolean { + if (requiresTransformERC20(opts)) { + return false; + } + if (quote.isTwoHop) { + return false; + } + // batchFill does not support WETH wrapping/unwrapping at the moment + if (opts.isFromETH || opts.isToETH) { + return false; + } + if (quote.orders.map(o => o.type).includes(FillQuoteTransformerOrderType.Limit)) { + return false; + } + // Use Multiplex if the non-fallback sources are a subset of + // {Uniswap, Sushiswap, RFQ, PLP} + const nonFallbackSources = Object.keys(quote.sourceBreakdown); + return nonFallbackSources.every(source => MULTIPLEX_BATCH_FILL_SOURCES.includes(source as ERC20BridgeSource)); +} + +const MULTIPLEX_MULTIHOP_FILL_SOURCES = [ + ERC20BridgeSource.UniswapV2, + ERC20BridgeSource.SushiSwap, + ERC20BridgeSource.LiquidityProvider, +]; + +/** + * Returns true iff a quote can be filled via `MultiplexFeature.multiHopFill`. + */ +export function isMultiplexMultiHopFillCompatible(quote: SwapQuote, opts: ExchangeProxyContractOpts): boolean { + if (requiresTransformERC20(opts)) { + return false; + } + if (!quote.isTwoHop) { + return false; + } + const [firstHopOrder, secondHopOrder] = quote.orders; + return ( + MULTIPLEX_MULTIHOP_FILL_SOURCES.includes(firstHopOrder.source) && + MULTIPLEX_MULTIHOP_FILL_SOURCES.includes(secondHopOrder.source) + ); +} + +/** + * Returns true iff a quote can be filled via a VIP feature. + */ +export function isDirectSwapCompatible( + quote: SwapQuote, + opts: ExchangeProxyContractOpts, + directSources: ERC20BridgeSource[], +): boolean { + if (requiresTransformERC20(opts)) { + return false; + } + // Must be a single order. + if (quote.orders.length !== 1) { + return false; + } + const order = quote.orders[0]; + if (!directSources.includes(order.source)) { + return false; + } + return true; +} + +/** + * Whether a quote is a market buy or not. + */ +export function isBuyQuote(quote: SwapQuote): quote is MarketBuySwapQuote { + return quote.type === MarketOperation.Buy; +} + +function isOptimizedBridgeOrder(x: OptimizedMarketOrder): x is OptimizedMarketBridgeOrder { + return x.type === FillQuoteTransformerOrderType.Bridge; +} + +function isOptimizedLimitOrder(x: OptimizedMarketOrder): x is OptimizedMarketOrderBase { + return x.type === FillQuoteTransformerOrderType.Limit; +} + +function isOptimizedRfqOrder(x: OptimizedMarketOrder): x is OptimizedMarketOrderBase { + return x.type === FillQuoteTransformerOrderType.Rfq; +} + +/** + * Converts the given `OptimizedMarketOrder`s into bridge, limit, and RFQ orders for + * FillQuoteTransformer. + */ +export function getFQTTransformerDataFromOptimizedOrders( + orders: OptimizedMarketOrder[], +): Pick { + const fqtData: Pick = { + bridgeOrders: [], + limitOrders: [], + rfqOrders: [], + fillSequence: [], + }; + + for (const order of orders) { + if (isOptimizedBridgeOrder(order)) { + fqtData.bridgeOrders.push({ + bridgeData: createBridgeDataForBridgeOrder(order), + makerTokenAmount: order.makerAmount, + takerTokenAmount: order.takerAmount, + source: getERC20BridgeSourceToBridgeSource(order.source), + }); + } else if (isOptimizedLimitOrder(order)) { + fqtData.limitOrders.push({ + order: order.fillData.order, + signature: order.fillData.signature, + maxTakerTokenFillAmount: order.takerAmount, + }); + } else if (isOptimizedRfqOrder(order)) { + fqtData.rfqOrders.push({ + order: order.fillData.order, + signature: order.fillData.signature, + maxTakerTokenFillAmount: order.takerAmount, + }); + } else { + // Should never happen + throw new Error('Unknown Order type'); + } + fqtData.fillSequence.push(order.type); + } + return fqtData; +} + +/** + * Returns true if swap quote must go through `tranformERC20`. + */ +export function requiresTransformERC20(opts: ExchangeProxyContractOpts): boolean { + // Is a mtx. + if (opts.isMetaTransaction) { + return true; + } + // Has an affiliate fee. + if (!opts.affiliateFee.buyTokenFeeAmount.eq(0) || !opts.affiliateFee.sellTokenFeeAmount.eq(0)) { + return true; + } + // Has a positive slippage fee. + if (opts.affiliateFee.feeType === AffiliateFeeType.PositiveSlippageFee) { + return true; + } + // VIP does not support selling the entire balance + if (opts.shouldSellEntireBalance) { + return true; + } + return false; +} 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 4f6c6e5a86..7943657d13 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/constants.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/constants.ts @@ -102,9 +102,18 @@ export const PROTOCOL_FEE_MULTIPLIER = new BigNumber(70000); */ export const FEE_QUOTE_SOURCES = [ERC20BridgeSource.Uniswap, ERC20BridgeSource.UniswapV2]; -export const SOURCE_FLAGS: { [source in ERC20BridgeSource]: number } = Object.assign( +// HACK(mzhu25): Limit and RFQ orders need to be treated as different sources +// when computing the exchange proxy gas overhead. +export const SOURCE_FLAGS: { [source in ERC20BridgeSource]: number } & { + RfqOrder: number; + LimitOrder: number; +} = Object.assign( {}, - ...Object.values(ERC20BridgeSource).map((source: ERC20BridgeSource, index) => ({ [source]: 1 << index })), + ...['RfqOrder', 'LimitOrder', ...Object.values(ERC20BridgeSource)].map( + (source: ERC20BridgeSource | 'RfqOrder' | 'LimitOrder', index) => ({ + [source]: source === ERC20BridgeSource.Native ? 0 : 1 << index, + }), + ), ); const MIRROR_WRAPPED_TOKENS = { 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 e097d0b08a..4d9df4493b 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/fills.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/fills.ts @@ -83,7 +83,7 @@ function nativeOrdersToFills( // Create a single path from all orders. let fills: Array = []; for (const o of orders) { - const { fillableTakerAmount, fillableTakerFeeAmount, fillableMakerAmount } = o; + const { fillableTakerAmount, fillableTakerFeeAmount, fillableMakerAmount, type } = o; const makerAmount = fillableMakerAmount; const takerAmount = fillableTakerAmount.plus(fillableTakerFeeAmount); const input = side === MarketOperation.Sell ? takerAmount : makerAmount; @@ -114,11 +114,11 @@ function nativeOrdersToFills( adjustedOutput, input: clippedInput, output: clippedOutput, - flags: SOURCE_FLAGS[ERC20BridgeSource.Native], + flags: SOURCE_FLAGS[type === FillQuoteTransformerOrderType.Rfq ? 'RfqOrder' : 'LimitOrder'], index: 0, // TBD parent: undefined, // TBD source: ERC20BridgeSource.Native, - type: o.type, + type, fillData: { ...o }, }); } 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 bc2a0207b1..87f309a695 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,8 @@ import { MarketOperation } from '../../types'; import { SOURCE_FLAGS, ZERO_AMOUNT } from './constants'; import { DexSample, ERC20BridgeSource, ExchangeProxyOverhead, FeeSchedule, MultiHopFillData } from './types'; +// tslint:disable:no-bitwise + /** * Returns the fee-adjusted rate of a two-hop quote. Returns zero if the * quote falls short of the target input. @@ -22,7 +24,11 @@ export function getTwoHopAdjustedRate( return ZERO_AMOUNT; } const penalty = outputAmountPerEth.times( - exchangeProxyOverhead(SOURCE_FLAGS.MultiHop).plus(fees[ERC20BridgeSource.MultiHop]!(fillData)), + exchangeProxyOverhead( + SOURCE_FLAGS.MultiHop | + SOURCE_FLAGS[fillData.firstHopSource.source] | + SOURCE_FLAGS[fillData.secondHopSource.source], + ).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);