Update asset-swapper to support MultiplexFeature (#168)

* Update asset-swapper to support MultiplexFeature

* Address PR feedback

* Update changelogs
This commit is contained in:
mzhu25 2021-03-16 22:20:33 -07:00 committed by GitHub
parent 22e1ed35d3
commit 3d4c03c9df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 406 additions and 103 deletions

View File

@ -13,6 +13,10 @@
{ {
"note": "Add BatchFillNativeOrdersFeature and MultiplexFeature", "note": "Add BatchFillNativeOrdersFeature and MultiplexFeature",
"pr": 140 "pr": 140
},
{
"note": "Export MultiplexFeatureContract",
"pr": 168
} }
] ]
}, },

View File

@ -45,6 +45,7 @@ export {
ITransformERC20FeatureContract, ITransformERC20FeatureContract,
IZeroExContract, IZeroExContract,
LogMetadataTransformerContract, LogMetadataTransformerContract,
MultiplexFeatureContract,
PayTakerTransformerContract, PayTakerTransformerContract,
PositiveSlippageFeeTransformerContract, PositiveSlippageFeeTransformerContract,
WethTransformerContract, WethTransformerContract,

View File

@ -9,6 +9,10 @@
{ {
"note": "Enable the ability to send RFQT requests thru a proxy", "note": "Enable the ability to send RFQT requests thru a proxy",
"pr": 159 "pr": 159
},
{
"note": "Add support for MultiplexFeature",
"pr": 168
} }
] ]
}, },

View File

@ -61,6 +61,8 @@
"@0x/base-contract": "^6.2.18", "@0x/base-contract": "^6.2.18",
"@0x/contract-addresses": "^5.11.0", "@0x/contract-addresses": "^5.11.0",
"@0x/contract-wrappers": "^13.13.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/dev-utils": "^4.2.1",
"@0x/json-schemas": "^5.4.1", "@0x/json-schemas": "^5.4.1",
"@0x/protocol-utils": "^1.3.0", "@0x/protocol-utils": "^1.3.0",
@ -93,7 +95,6 @@
"@0x/contracts-gen": "^2.0.32", "@0x/contracts-gen": "^2.0.32",
"@0x/contracts-test-utils": "^5.3.22", "@0x/contracts-test-utils": "^5.3.22",
"@0x/contracts-utils": "^4.7.4", "@0x/contracts-utils": "^4.7.4",
"@0x/contracts-zero-ex": "^0.19.0",
"@0x/mesh-rpc-client": "^9.4.2", "@0x/mesh-rpc-client": "^9.4.2",
"@0x/migrations": "^7.0.0", "@0x/migrations": "^7.0.0",
"@0x/sol-compiler": "^4.6.1", "@0x/sol-compiler": "^4.6.1",

View File

@ -1,5 +1,6 @@
import { ContractAddresses } from '@0x/contract-addresses'; 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 { import {
encodeAffiliateFeeTransformerData, encodeAffiliateFeeTransformerData,
encodeCurveLiquidityProviderData, encodeCurveLiquidityProviderData,
@ -8,7 +9,6 @@ import {
encodePositiveSlippageFeeTransformerData, encodePositiveSlippageFeeTransformerData,
encodeWethTransformerData, encodeWethTransformerData,
ETH_TOKEN_ADDRESS, ETH_TOKEN_ADDRESS,
FillQuoteTransformerData,
FillQuoteTransformerOrderType, FillQuoteTransformerOrderType,
FillQuoteTransformerSide, FillQuoteTransformerSide,
findTransformerNonce, findTransformerNonce,
@ -23,7 +23,6 @@ import {
CalldataInfo, CalldataInfo,
ExchangeProxyContractOpts, ExchangeProxyContractOpts,
MarketBuySwapQuote, MarketBuySwapQuote,
MarketOperation,
MarketSellSwapQuote, MarketSellSwapQuote,
SwapQuote, SwapQuote,
SwapQuoteConsumerBase, SwapQuoteConsumerBase,
@ -36,24 +35,30 @@ import {
CURVE_LIQUIDITY_PROVIDER_BY_CHAIN_ID, CURVE_LIQUIDITY_PROVIDER_BY_CHAIN_ID,
MOONISWAP_LIQUIDITY_PROVIDER_BY_CHAIN_ID, MOONISWAP_LIQUIDITY_PROVIDER_BY_CHAIN_ID,
} from '../utils/market_operation_utils/constants'; } from '../utils/market_operation_utils/constants';
import { import { poolEncoder } from '../utils/market_operation_utils/orders';
createBridgeDataForBridgeOrder,
getERC20BridgeSourceToBridgeSource,
poolEncoder,
} from '../utils/market_operation_utils/orders';
import { import {
CurveFillData, CurveFillData,
ERC20BridgeSource, ERC20BridgeSource,
LiquidityProviderFillData, LiquidityProviderFillData,
MooniswapFillData, MooniswapFillData,
NativeLimitOrderFillData,
NativeRfqOrderFillData,
OptimizedMarketBridgeOrder, OptimizedMarketBridgeOrder,
OptimizedMarketOrder,
OptimizedMarketOrderBase,
UniswapV2FillData, UniswapV2FillData,
} from '../utils/market_operation_utils/types'; } 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 // tslint:disable-next-line:custom-no-magic-numbers
const MAX_UINT256 = new BigNumber(2).pow(256).minus(1); const MAX_UINT256 = new BigNumber(2).pow(256).minus(1);
const { NULL_ADDRESS, NULL_BYTES, ZERO_AMOUNT } = constants; const { NULL_ADDRESS, NULL_BYTES, ZERO_AMOUNT } = constants;
@ -70,6 +75,7 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
}; };
private readonly _exchangeProxy: IZeroExContract; private readonly _exchangeProxy: IZeroExContract;
private readonly _multiplex: MultiplexFeatureContract;
constructor( constructor(
supportedProvider: SupportedProvider, supportedProvider: SupportedProvider,
@ -83,6 +89,7 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
this.chainId = chainId; this.chainId = chainId;
this.contractAddresses = contractAddresses; this.contractAddresses = contractAddresses;
this._exchangeProxy = new IZeroExContract(contractAddresses.exchangeProxy, supportedProvider); this._exchangeProxy = new IZeroExContract(contractAddresses.exchangeProxy, supportedProvider);
this._multiplex = new MultiplexFeatureContract(contractAddresses.exchangeProxy, supportedProvider);
this.transformerNonces = { this.transformerNonces = {
wethTransformer: findTransformerNonce( wethTransformer: findTransformerNonce(
contractAddresses.transformers.wethTransformer, 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. // Build up the transforms.
const transforms = []; const transforms = [];
if (isFromETH) { if (isFromETH) {
@ -380,91 +406,145 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
): Promise<string> { ): Promise<string> {
throw new Error('Execution not supported for Exchange Proxy quotes'); throw new Error('Execution not supported for Exchange Proxy quotes');
} }
}
function isDirectSwapCompatible( private _encodeMultiplexBatchFillCalldata(quote: SwapQuote): string {
quote: SwapQuote, const wrappedBatchCalls = [];
opts: ExchangeProxyContractOpts, for_loop: for (const [i, order] of quote.orders.entries()) {
directSources: ERC20BridgeSource[], switch_statement: switch (order.source) {
): boolean { case ERC20BridgeSource.Native:
// Must not be a mtx. if (order.type !== FillQuoteTransformerOrderType.Rfq) {
if (opts.isMetaTransaction) { // Should never happen because we check `isMultiplexBatchFillCompatible`
return false; // before calling this function.
} throw new Error('Multiplex batch fill only supported for RFQ native orders');
// Must not have an affiliate fee. }
if (!opts.affiliateFee.buyTokenFeeAmount.eq(0) || !opts.affiliateFee.sellTokenFeeAmount.eq(0)) { wrappedBatchCalls.push({
return false; selector: this._exchangeProxy.getSelector('_fillRfqOrder'),
} sellAmount: order.takerAmount,
// Must not have a positive slippage fee. data: multiplexRfqEncoder.encode({
if (opts.affiliateFee.feeType === AffiliateFeeType.PositiveSlippageFee) { order: order.fillData.order,
return false; signature: order.fillData.signature,
} }),
// Must be a single order. });
if (quote.orders.length !== 1) { break switch_statement;
return false; case ERC20BridgeSource.UniswapV2:
} case ERC20BridgeSource.SushiSwap:
const order = quote.orders[0]; wrappedBatchCalls.push({
if (!directSources.includes(order.source)) { selector: this._multiplex.getSelector('_sellToUniswap'),
return false; sellAmount: order.takerAmount,
} data: multiplexUniswapEncoder.encode({
// VIP does not support selling the entire balance tokens: (order.fillData as UniswapV2FillData).tokenAddressPath,
if (opts.shouldSellEntireBalance) { isSushi: order.source === ERC20BridgeSource.SushiSwap,
return false; }),
} });
return true; break switch_statement;
} case ERC20BridgeSource.LiquidityProvider:
wrappedBatchCalls.push({
function isBuyQuote(quote: SwapQuote): quote is MarketBuySwapQuote { selector: this._multiplex.getSelector('_sellToLiquidityProvider'),
return quote.type === MarketOperation.Buy; sellAmount: order.takerAmount,
} data: multiplexPlpEncoder.encode({
provider: (order.fillData as LiquidityProviderFillData).poolAddress,
function isOptimizedBridgeOrder(x: OptimizedMarketOrder): x is OptimizedMarketBridgeOrder { auxiliaryData: NULL_BYTES,
return x.type === FillQuoteTransformerOrderType.Bridge; }),
} });
break switch_statement;
function isOptimizedLimitOrder(x: OptimizedMarketOrder): x is OptimizedMarketOrderBase<NativeLimitOrderFillData> { default:
return x.type === FillQuoteTransformerOrderType.Limit; const fqtData = encodeFillQuoteTransformerData({
} side: FillQuoteTransformerSide.Sell,
sellToken: quote.takerToken,
function isOptimizedRfqOrder(x: OptimizedMarketOrder): x is OptimizedMarketOrderBase<NativeRfqOrderFillData> { buyToken: quote.makerToken,
return x.type === FillQuoteTransformerOrderType.Rfq; ...getFQTTransformerDataFromOptimizedOrders(quote.orders.slice(i)),
} refundReceiver: NULL_ADDRESS,
fillAmount: MAX_UINT256,
function getFQTTransformerDataFromOptimizedOrders( });
orders: OptimizedMarketOrder[], const transformations = [
): Pick<FillQuoteTransformerData, 'bridgeOrders' | 'limitOrders' | 'rfqOrders' | 'fillSequence'> { { deploymentNonce: this.transformerNonces.fillQuoteTransformer, data: fqtData },
const fqtData: Pick<FillQuoteTransformerData, 'bridgeOrders' | 'limitOrders' | 'rfqOrders' | 'fillSequence'> = { {
bridgeOrders: [], deploymentNonce: this.transformerNonces.payTakerTransformer,
limitOrders: [], data: encodePayTakerTransformerData({
rfqOrders: [], tokens: [quote.takerToken, quote.makerToken],
fillSequence: [], amounts: [],
}; }),
},
for (const order of orders) { ];
if (isOptimizedBridgeOrder(order)) { wrappedBatchCalls.push({
fqtData.bridgeOrders.push({ selector: this._exchangeProxy.getSelector('_transformERC20'),
bridgeData: createBridgeDataForBridgeOrder(order), sellAmount: BigNumber.sum(...quote.orders.slice(i).map(o => o.takerAmount)),
makerTokenAmount: order.makerAmount, data: multiplexTransformERC20Encoder.encode({
takerTokenAmount: order.takerAmount, transformations,
source: getERC20BridgeSourceToBridgeSource(order.source), ethValue: constants.ZERO_AMOUNT,
}); }),
} else if (isOptimizedLimitOrder(order)) { });
fqtData.limitOrders.push({ break for_loop;
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 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;
} }

View File

@ -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' },
]);

View File

@ -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<NativeLimitOrderFillData> {
return x.type === FillQuoteTransformerOrderType.Limit;
}
function isOptimizedRfqOrder(x: OptimizedMarketOrder): x is OptimizedMarketOrderBase<NativeRfqOrderFillData> {
return x.type === FillQuoteTransformerOrderType.Rfq;
}
/**
* Converts the given `OptimizedMarketOrder`s into bridge, limit, and RFQ orders for
* FillQuoteTransformer.
*/
export function getFQTTransformerDataFromOptimizedOrders(
orders: OptimizedMarketOrder[],
): Pick<FillQuoteTransformerData, 'bridgeOrders' | 'limitOrders' | 'rfqOrders' | 'fillSequence'> {
const fqtData: Pick<FillQuoteTransformerData, 'bridgeOrders' | 'limitOrders' | 'rfqOrders' | 'fillSequence'> = {
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;
}

View File

@ -102,9 +102,18 @@ export const PROTOCOL_FEE_MULTIPLIER = new BigNumber(70000);
*/ */
export const FEE_QUOTE_SOURCES = [ERC20BridgeSource.Uniswap, ERC20BridgeSource.UniswapV2]; 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 = { const MIRROR_WRAPPED_TOKENS = {

View File

@ -83,7 +83,7 @@ function nativeOrdersToFills(
// Create a single path from all orders. // Create a single path from all orders.
let fills: Array<Fill & { adjustedRate: BigNumber }> = []; let fills: Array<Fill & { adjustedRate: BigNumber }> = [];
for (const o of orders) { for (const o of orders) {
const { fillableTakerAmount, fillableTakerFeeAmount, fillableMakerAmount } = o; const { fillableTakerAmount, fillableTakerFeeAmount, fillableMakerAmount, type } = o;
const makerAmount = fillableMakerAmount; const makerAmount = fillableMakerAmount;
const takerAmount = fillableTakerAmount.plus(fillableTakerFeeAmount); const takerAmount = fillableTakerAmount.plus(fillableTakerFeeAmount);
const input = side === MarketOperation.Sell ? takerAmount : makerAmount; const input = side === MarketOperation.Sell ? takerAmount : makerAmount;
@ -114,11 +114,11 @@ function nativeOrdersToFills(
adjustedOutput, adjustedOutput,
input: clippedInput, input: clippedInput,
output: clippedOutput, output: clippedOutput,
flags: SOURCE_FLAGS[ERC20BridgeSource.Native], flags: SOURCE_FLAGS[type === FillQuoteTransformerOrderType.Rfq ? 'RfqOrder' : 'LimitOrder'],
index: 0, // TBD index: 0, // TBD
parent: undefined, // TBD parent: undefined, // TBD
source: ERC20BridgeSource.Native, source: ERC20BridgeSource.Native,
type: o.type, type,
fillData: { ...o }, fillData: { ...o },
}); });
} }

View File

@ -5,6 +5,8 @@ import { MarketOperation } from '../../types';
import { SOURCE_FLAGS, ZERO_AMOUNT } from './constants'; import { SOURCE_FLAGS, ZERO_AMOUNT } from './constants';
import { DexSample, ERC20BridgeSource, ExchangeProxyOverhead, FeeSchedule, MultiHopFillData } from './types'; 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 * Returns the fee-adjusted rate of a two-hop quote. Returns zero if the
* quote falls short of the target input. * quote falls short of the target input.
@ -22,7 +24,11 @@ export function getTwoHopAdjustedRate(
return ZERO_AMOUNT; return ZERO_AMOUNT;
} }
const penalty = outputAmountPerEth.times( 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); const adjustedOutput = side === MarketOperation.Sell ? output.minus(penalty) : output.plus(penalty);
return side === MarketOperation.Sell ? adjustedOutput.div(input) : input.div(adjustedOutput); return side === MarketOperation.Sell ? adjustedOutput.div(input) : input.div(adjustedOutput);