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",
"pr": 140
},
{
"note": "Export MultiplexFeatureContract",
"pr": 168
}
]
},

View File

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

View File

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

View File

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

View File

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

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 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 = {

View File

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

View File

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