Update asset-swapper to support MultiplexFeature (#168)
* Update asset-swapper to support MultiplexFeature * Address PR feedback * Update changelogs
This commit is contained in:
parent
22e1ed35d3
commit
3d4c03c9df
@ -13,6 +13,10 @@
|
||||
{
|
||||
"note": "Add BatchFillNativeOrdersFeature and MultiplexFeature",
|
||||
"pr": 140
|
||||
},
|
||||
{
|
||||
"note": "Export MultiplexFeatureContract",
|
||||
"pr": 168
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -45,6 +45,7 @@ export {
|
||||
ITransformERC20FeatureContract,
|
||||
IZeroExContract,
|
||||
LogMetadataTransformerContract,
|
||||
MultiplexFeatureContract,
|
||||
PayTakerTransformerContract,
|
||||
PositiveSlippageFeeTransformerContract,
|
||||
WethTransformerContract,
|
||||
|
@ -9,6 +9,10 @@
|
||||
{
|
||||
"note": "Enable the ability to send RFQT requests thru a proxy",
|
||||
"pr": 159
|
||||
},
|
||||
{
|
||||
"note": "Add support for MultiplexFeature",
|
||||
"pr": 168
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -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",
|
||||
|
@ -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<string> {
|
||||
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<NativeLimitOrderFillData> {
|
||||
return x.type === FillQuoteTransformerOrderType.Limit;
|
||||
}
|
||||
|
||||
function isOptimizedRfqOrder(x: OptimizedMarketOrder): x is OptimizedMarketOrderBase<NativeRfqOrderFillData> {
|
||||
return x.type === FillQuoteTransformerOrderType.Rfq;
|
||||
}
|
||||
|
||||
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');
|
||||
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;
|
||||
}
|
||||
|
@ -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' },
|
||||
]);
|
@ -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;
|
||||
}
|
@ -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 = {
|
||||
|
@ -83,7 +83,7 @@ function nativeOrdersToFills(
|
||||
// Create a single path from all orders.
|
||||
let fills: Array<Fill & { adjustedRate: BigNumber }> = [];
|
||||
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 },
|
||||
});
|
||||
}
|
||||
|
@ -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);
|
||||
|
Loading…
x
Reference in New Issue
Block a user