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:
Lawrence Forman 2021-04-13 19:24:50 -04:00 committed by GitHub
parent 9e152912fe
commit dfb7b3de8f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 76 additions and 22 deletions

View File

@ -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",

View File

@ -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();
}

View File

@ -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,

View File

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

View File

@ -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"]