Apply slippage to non-native orders [TKR-39] (#198)
* `@0x/asset-swapper`: Apply slippage to FQT bridge orders * review comments Co-authored-by: Lawrence Forman <me@merklejerk.com>
This commit is contained in:
parent
9e152912fe
commit
dfb7b3de8f
@ -1,4 +1,13 @@
|
|||||||
[
|
[
|
||||||
|
{
|
||||||
|
"version": "6.5.3",
|
||||||
|
"changes": [
|
||||||
|
{
|
||||||
|
"note": "Apply slippage to bridge orders in consumer",
|
||||||
|
"pr": 198
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"timestamp": 1618314654,
|
"timestamp": 1618314654,
|
||||||
"version": "6.5.2",
|
"version": "6.5.2",
|
||||||
|
@ -23,6 +23,7 @@ import {
|
|||||||
CalldataInfo,
|
CalldataInfo,
|
||||||
ExchangeProxyContractOpts,
|
ExchangeProxyContractOpts,
|
||||||
MarketBuySwapQuote,
|
MarketBuySwapQuote,
|
||||||
|
MarketOperation,
|
||||||
MarketSellSwapQuote,
|
MarketSellSwapQuote,
|
||||||
SwapQuote,
|
SwapQuote,
|
||||||
SwapQuoteConsumerBase,
|
SwapQuoteConsumerBase,
|
||||||
@ -43,6 +44,7 @@ import {
|
|||||||
LiquidityProviderFillData,
|
LiquidityProviderFillData,
|
||||||
MooniswapFillData,
|
MooniswapFillData,
|
||||||
OptimizedMarketBridgeOrder,
|
OptimizedMarketBridgeOrder,
|
||||||
|
OptimizedMarketOrder,
|
||||||
UniswapV2FillData,
|
UniswapV2FillData,
|
||||||
} from '../utils/market_operation_utils/types';
|
} from '../utils/market_operation_utils/types';
|
||||||
|
|
||||||
@ -136,7 +138,10 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
|
|||||||
const buyToken = quote.makerToken;
|
const buyToken = quote.makerToken;
|
||||||
|
|
||||||
// Take the bounds from the worst case
|
// Take the bounds from the worst case
|
||||||
const sellAmount = quote.worstCaseQuoteInfo.totalTakerAmount;
|
const sellAmount = BigNumber.max(
|
||||||
|
quote.bestCaseQuoteInfo.totalTakerAmount,
|
||||||
|
quote.worstCaseQuoteInfo.totalTakerAmount,
|
||||||
|
);
|
||||||
let minBuyAmount = quote.worstCaseQuoteInfo.makerAmount;
|
let minBuyAmount = quote.worstCaseQuoteInfo.makerAmount;
|
||||||
let ethAmount = quote.worstCaseQuoteInfo.protocolFeeInWeiAmount;
|
let ethAmount = quote.worstCaseQuoteInfo.protocolFeeInWeiAmount;
|
||||||
|
|
||||||
@ -144,13 +149,15 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
|
|||||||
ethAmount = ethAmount.plus(sellAmount);
|
ethAmount = ethAmount.plus(sellAmount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const slippedOrders = slipNonNativeOrders(quote);
|
||||||
|
|
||||||
// VIP routes.
|
// VIP routes.
|
||||||
if (
|
if (
|
||||||
this.chainId === ChainId.Mainnet &&
|
this.chainId === ChainId.Mainnet &&
|
||||||
isDirectSwapCompatible(quote, optsWithDefaults, [ERC20BridgeSource.UniswapV2, ERC20BridgeSource.SushiSwap])
|
isDirectSwapCompatible(quote, optsWithDefaults, [ERC20BridgeSource.UniswapV2, ERC20BridgeSource.SushiSwap])
|
||||||
) {
|
) {
|
||||||
const source = quote.orders[0].source;
|
const source = slippedOrders[0].source;
|
||||||
const fillData = (quote.orders[0] as OptimizedMarketBridgeOrder<UniswapV2FillData>).fillData;
|
const fillData = (slippedOrders[0] as OptimizedMarketBridgeOrder<UniswapV2FillData>).fillData;
|
||||||
return {
|
return {
|
||||||
calldataHexString: this._exchangeProxy
|
calldataHexString: this._exchangeProxy
|
||||||
.sellToUniswap(
|
.sellToUniswap(
|
||||||
@ -183,8 +190,8 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
|
|||||||
ERC20BridgeSource.SushiSwap,
|
ERC20BridgeSource.SushiSwap,
|
||||||
])
|
])
|
||||||
) {
|
) {
|
||||||
const source = quote.orders[0].source;
|
const source = slippedOrders[0].source;
|
||||||
const fillData = (quote.orders[0] as OptimizedMarketBridgeOrder<UniswapV2FillData>).fillData;
|
const fillData = (slippedOrders[0] as OptimizedMarketBridgeOrder<UniswapV2FillData>).fillData;
|
||||||
return {
|
return {
|
||||||
calldataHexString: this._exchangeProxy
|
calldataHexString: this._exchangeProxy
|
||||||
.sellToPancakeSwap(
|
.sellToPancakeSwap(
|
||||||
@ -213,7 +220,7 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
|
|||||||
this.chainId === ChainId.Mainnet &&
|
this.chainId === ChainId.Mainnet &&
|
||||||
isDirectSwapCompatible(quote, optsWithDefaults, [ERC20BridgeSource.LiquidityProvider])
|
isDirectSwapCompatible(quote, optsWithDefaults, [ERC20BridgeSource.LiquidityProvider])
|
||||||
) {
|
) {
|
||||||
const fillData = (quote.orders[0] as OptimizedMarketBridgeOrder<LiquidityProviderFillData>).fillData;
|
const fillData = (slippedOrders[0] as OptimizedMarketBridgeOrder<LiquidityProviderFillData>).fillData;
|
||||||
const target = fillData.poolAddress;
|
const target = fillData.poolAddress;
|
||||||
return {
|
return {
|
||||||
calldataHexString: this._exchangeProxy
|
calldataHexString: this._exchangeProxy
|
||||||
@ -238,7 +245,7 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
|
|||||||
this.chainId === ChainId.Mainnet &&
|
this.chainId === ChainId.Mainnet &&
|
||||||
isDirectSwapCompatible(quote, optsWithDefaults, [ERC20BridgeSource.Curve, ERC20BridgeSource.Swerve])
|
isDirectSwapCompatible(quote, optsWithDefaults, [ERC20BridgeSource.Curve, ERC20BridgeSource.Swerve])
|
||||||
) {
|
) {
|
||||||
const fillData = quote.orders[0].fills[0].fillData as CurveFillData;
|
const fillData = slippedOrders[0].fills[0].fillData as CurveFillData;
|
||||||
return {
|
return {
|
||||||
calldataHexString: this._exchangeProxy
|
calldataHexString: this._exchangeProxy
|
||||||
.sellToLiquidityProvider(
|
.sellToLiquidityProvider(
|
||||||
@ -267,7 +274,7 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
|
|||||||
this.chainId === ChainId.Mainnet &&
|
this.chainId === ChainId.Mainnet &&
|
||||||
isDirectSwapCompatible(quote, optsWithDefaults, [ERC20BridgeSource.Mooniswap])
|
isDirectSwapCompatible(quote, optsWithDefaults, [ERC20BridgeSource.Mooniswap])
|
||||||
) {
|
) {
|
||||||
const fillData = quote.orders[0].fills[0].fillData as MooniswapFillData;
|
const fillData = slippedOrders[0].fills[0].fillData as MooniswapFillData;
|
||||||
return {
|
return {
|
||||||
calldataHexString: this._exchangeProxy
|
calldataHexString: this._exchangeProxy
|
||||||
.sellToLiquidityProvider(
|
.sellToLiquidityProvider(
|
||||||
@ -289,7 +296,7 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
|
|||||||
|
|
||||||
if (this.chainId === ChainId.Mainnet && isMultiplexBatchFillCompatible(quote, optsWithDefaults)) {
|
if (this.chainId === ChainId.Mainnet && isMultiplexBatchFillCompatible(quote, optsWithDefaults)) {
|
||||||
return {
|
return {
|
||||||
calldataHexString: this._encodeMultiplexBatchFillCalldata(quote),
|
calldataHexString: this._encodeMultiplexBatchFillCalldata({ ...quote, orders: slippedOrders }),
|
||||||
ethAmount,
|
ethAmount,
|
||||||
toAddress: this._exchangeProxy.address,
|
toAddress: this._exchangeProxy.address,
|
||||||
allowanceTarget: this._exchangeProxy.address,
|
allowanceTarget: this._exchangeProxy.address,
|
||||||
@ -298,7 +305,10 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
|
|||||||
}
|
}
|
||||||
if (this.chainId === ChainId.Mainnet && isMultiplexMultiHopFillCompatible(quote, optsWithDefaults)) {
|
if (this.chainId === ChainId.Mainnet && isMultiplexMultiHopFillCompatible(quote, optsWithDefaults)) {
|
||||||
return {
|
return {
|
||||||
calldataHexString: this._encodeMultiplexMultiHopFillCalldata(quote, optsWithDefaults),
|
calldataHexString: this._encodeMultiplexMultiHopFillCalldata(
|
||||||
|
{ ...quote, orders: slippedOrders },
|
||||||
|
optsWithDefaults,
|
||||||
|
),
|
||||||
ethAmount,
|
ethAmount,
|
||||||
toAddress: this._exchangeProxy.address,
|
toAddress: this._exchangeProxy.address,
|
||||||
allowanceTarget: this._exchangeProxy.address,
|
allowanceTarget: this._exchangeProxy.address,
|
||||||
@ -321,10 +331,10 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
|
|||||||
|
|
||||||
// If it's two hop we have an intermediate token this is needed to encode the individual FQT
|
// If it's two hop we have an intermediate token this is needed to encode the individual FQT
|
||||||
// and we also want to ensure no dust amount is left in the flash wallet
|
// and we also want to ensure no dust amount is left in the flash wallet
|
||||||
const intermediateToken = quote.isTwoHop ? quote.orders[0].makerToken : NULL_ADDRESS;
|
const intermediateToken = quote.isTwoHop ? slippedOrders[0].makerToken : NULL_ADDRESS;
|
||||||
// This transformer will fill the quote.
|
// This transformer will fill the quote.
|
||||||
if (quote.isTwoHop) {
|
if (quote.isTwoHop) {
|
||||||
const [firstHopOrder, secondHopOrder] = quote.orders;
|
const [firstHopOrder, secondHopOrder] = slippedOrders;
|
||||||
transforms.push({
|
transforms.push({
|
||||||
deploymentNonce: this.transformerNonces.fillQuoteTransformer,
|
deploymentNonce: this.transformerNonces.fillQuoteTransformer,
|
||||||
data: encodeFillQuoteTransformerData({
|
data: encodeFillQuoteTransformerData({
|
||||||
@ -349,14 +359,13 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const fillAmount = isBuyQuote(quote) ? quote.makerTokenFillAmount : quote.takerTokenFillAmount;
|
const fillAmount = isBuyQuote(quote) ? quote.makerTokenFillAmount : quote.takerTokenFillAmount;
|
||||||
|
|
||||||
transforms.push({
|
transforms.push({
|
||||||
deploymentNonce: this.transformerNonces.fillQuoteTransformer,
|
deploymentNonce: this.transformerNonces.fillQuoteTransformer,
|
||||||
data: encodeFillQuoteTransformerData({
|
data: encodeFillQuoteTransformerData({
|
||||||
side: isBuyQuote(quote) ? FillQuoteTransformerSide.Buy : FillQuoteTransformerSide.Sell,
|
side: isBuyQuote(quote) ? FillQuoteTransformerSide.Buy : FillQuoteTransformerSide.Sell,
|
||||||
sellToken,
|
sellToken,
|
||||||
buyToken,
|
buyToken,
|
||||||
...getFQTTransformerDataFromOptimizedOrders(quote.orders),
|
...getFQTTransformerDataFromOptimizedOrders(slippedOrders),
|
||||||
refundReceiver: refundReceiver || NULL_ADDRESS,
|
refundReceiver: refundReceiver || NULL_ADDRESS,
|
||||||
fillAmount: !isBuyQuote(quote) && shouldSellEntireBalance ? MAX_UINT256 : fillAmount,
|
fillAmount: !isBuyQuote(quote) && shouldSellEntireBalance ? MAX_UINT256 : fillAmount,
|
||||||
}),
|
}),
|
||||||
@ -598,3 +607,38 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
|
|||||||
.getABIEncodedTransactionData();
|
.getABIEncodedTransactionData();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function slipNonNativeOrders(quote: MarketSellSwapQuote | MarketBuySwapQuote): OptimizedMarketOrder[] {
|
||||||
|
const slippage = getMaxQuoteSlippageRate(quote);
|
||||||
|
if (!slippage) {
|
||||||
|
return quote.orders;
|
||||||
|
}
|
||||||
|
return quote.orders.map(o => {
|
||||||
|
if (o.source === ERC20BridgeSource.Native) {
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...o,
|
||||||
|
...(quote.type === MarketOperation.Sell
|
||||||
|
? { makerAmount: o.makerAmount.times(1 - slippage).integerValue(BigNumber.ROUND_DOWN) }
|
||||||
|
: { takerAmount: o.takerAmount.times(1 + slippage).integerValue(BigNumber.ROUND_UP) }),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMaxQuoteSlippageRate(quote: MarketBuySwapQuote | MarketSellSwapQuote): number {
|
||||||
|
if (quote.type === MarketOperation.Buy) {
|
||||||
|
// (worstCaseTaker - bestCaseTaker) / bestCaseTaker
|
||||||
|
// where worstCaseTaker >= bestCaseTaker
|
||||||
|
return quote.worstCaseQuoteInfo.takerAmount
|
||||||
|
.minus(quote.bestCaseQuoteInfo.takerAmount)
|
||||||
|
.div(quote.bestCaseQuoteInfo.takerAmount)
|
||||||
|
.toNumber();
|
||||||
|
}
|
||||||
|
// (bestCaseMaker - worstCaseMaker) / bestCaseMaker
|
||||||
|
// where bestCaseMaker >= worstCaseMaker
|
||||||
|
return quote.bestCaseQuoteInfo.makerAmount
|
||||||
|
.minus(quote.worstCaseQuoteInfo.makerAmount)
|
||||||
|
.div(quote.bestCaseQuoteInfo.makerAmount)
|
||||||
|
.toNumber();
|
||||||
|
}
|
||||||
|
@ -591,9 +591,10 @@ function calculateTwoHopQuoteInfo(
|
|||||||
: secondHopOrder.makerAmount,
|
: secondHopOrder.makerAmount,
|
||||||
takerAmount: MarketOperation.Sell
|
takerAmount: MarketOperation.Sell
|
||||||
? firstHopOrder.takerAmount
|
? firstHopOrder.takerAmount
|
||||||
: // tslint:disable-next-line: binary-expression-operand-order
|
: firstHopOrder.takerAmount.times(1 + slippage).integerValue(),
|
||||||
firstHopOrder.takerAmount.times(1 + slippage).integerValue(),
|
totalTakerAmount: MarketOperation.Sell
|
||||||
totalTakerAmount: firstHopOrder.takerAmount,
|
? firstHopOrder.takerAmount
|
||||||
|
: firstHopOrder.takerAmount.times(1 + slippage).integerValue(),
|
||||||
feeTakerTokenAmount: constants.ZERO_AMOUNT,
|
feeTakerTokenAmount: constants.ZERO_AMOUNT,
|
||||||
protocolFeeInWeiAmount: constants.ZERO_AMOUNT,
|
protocolFeeInWeiAmount: constants.ZERO_AMOUNT,
|
||||||
gas,
|
gas,
|
||||||
|
@ -129,11 +129,10 @@ export function simulateWorstCaseFill(quoteInfo: QuoteFillInfo): QuoteFillResult
|
|||||||
};
|
};
|
||||||
// Adjust the output by 1-slippage for the worst case if it is a sell
|
// Adjust the output by 1-slippage for the worst case if it is a sell
|
||||||
// Adjust the output by 1+slippage for the worst case if it is a buy
|
// Adjust the output by 1+slippage for the worst case if it is a buy
|
||||||
const outputMultiplier =
|
result.output =
|
||||||
quoteInfo.side === MarketOperation.Sell
|
quoteInfo.side === MarketOperation.Sell
|
||||||
? new BigNumber(1).minus(opts.slippage)
|
? result.output.times(1 - opts.slippage).integerValue(BigNumber.ROUND_DOWN)
|
||||||
: new BigNumber(1).plus(opts.slippage);
|
: result.output.times(1 + opts.slippage).integerValue(BigNumber.ROUND_UP);
|
||||||
result.output = result.output.times(outputMultiplier).integerValue();
|
|
||||||
return fromIntermediateQuoteFillResult(result, quoteInfo);
|
return fromIntermediateQuoteFillResult(result, quoteInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
"extends": ["@0x/tslint-config"],
|
"extends": ["@0x/tslint-config"],
|
||||||
"rules": {
|
"rules": {
|
||||||
"max-file-line-count": false
|
"max-file-line-count": false,
|
||||||
|
"binary-expression-operand-order": false
|
||||||
},
|
},
|
||||||
"linterOptions": {
|
"linterOptions": {
|
||||||
"exclude": ["src/artifacts.ts", "test/artifacts.ts"]
|
"exclude": ["src/artifacts.ts", "test/artifacts.ts"]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user