diff --git a/contracts/asset-proxy/CHANGELOG.json b/contracts/asset-proxy/CHANGELOG.json index b05613fb2a..278f85db45 100644 --- a/contracts/asset-proxy/CHANGELOG.json +++ b/contracts/asset-proxy/CHANGELOG.json @@ -21,6 +21,10 @@ { "note": "Add `DexForwaderBridge` bridge contract.", "pr": 2525 + }, + { + "note": "Add Gas Token freeing to `DexForwaderBridge` contract.", + "pr": 2536 } ] }, diff --git a/contracts/asset-proxy/contracts/src/bridges/DexForwarderBridge.sol b/contracts/asset-proxy/contracts/src/bridges/DexForwarderBridge.sol index 70409719db..9faf278041 100644 --- a/contracts/asset-proxy/contracts/src/bridges/DexForwarderBridge.sol +++ b/contracts/asset-proxy/contracts/src/bridges/DexForwarderBridge.sol @@ -23,15 +23,19 @@ import "@0x/contracts-erc20/contracts/src/interfaces/IERC20Token.sol"; import "@0x/contracts-erc20/contracts/src/LibERC20Token.sol"; import "@0x/contracts-exchange-libs/contracts/src/IWallet.sol"; import "@0x/contracts-exchange-libs/contracts/src/LibMath.sol"; +import "@0x/contracts-utils/contracts/src/DeploymentConstants.sol"; import "@0x/contracts-utils/contracts/src/LibBytes.sol"; import "@0x/contracts-utils/contracts/src/LibSafeMath.sol"; import "../interfaces/IERC20Bridge.sol"; +import "./MixinGasToken.sol"; // solhint-disable space-after-comma, indent contract DexForwarderBridge is IERC20Bridge, - IWallet + IWallet, + DeploymentConstants, + MixinGasToken { using LibSafeMath for uint256; @@ -68,8 +72,10 @@ contract DexForwarderBridge is bytes calldata bridgeData ) external + freesGasTokensFromCollector returns (bytes4 success) { + require(msg.sender == _getERC20BridgeProxyAddress(), "DexForwarderBridge/SENDER_NOT_AUTHORIZED"); TransferFromState memory state; ( state.inputToken, @@ -84,16 +90,15 @@ contract DexForwarderBridge is break; } - BridgeCall memory call = state.calls[i]; // Compute token amounts. state.callInputTokenAmount = LibSafeMath.min256( - call.inputTokenAmount, + state.calls[i].inputTokenAmount, state.initialInputTokenBalance.safeSub(state.totalInputTokenSold) ); state.callOutputTokenAmount = LibMath.getPartialAmountFloor( state.callInputTokenAmount, - call.inputTokenAmount, - call.outputTokenAmount + state.calls[i].inputTokenAmount, + state.calls[i].outputTokenAmount ); // Execute the call in a new context so we can recoup transferred @@ -101,13 +106,13 @@ contract DexForwarderBridge is (bool didSucceed, ) = address(this) .call(abi.encodeWithSelector( this.executeBridgeCall.selector, - call.target, + state.calls[i].target, to, state.inputToken, outputToken, state.callInputTokenAmount, state.callOutputTokenAmount, - call.bridgeData + state.calls[i].bridgeData )); if (didSucceed) { diff --git a/contracts/asset-proxy/contracts/test/TestDexForwarderBridge.sol b/contracts/asset-proxy/contracts/test/TestDexForwarderBridge.sol index c0ada6c30b..21755ac591 100644 --- a/contracts/asset-proxy/contracts/test/TestDexForwarderBridge.sol +++ b/contracts/asset-proxy/contracts/test/TestDexForwarderBridge.sol @@ -156,6 +156,14 @@ contract TestDexForwarderBridge is ITestDexForwarderBridge, DexForwarderBridge { + address private AUTHORIZED_ADDRESS; // solhint-disable-line var-name-mixedcase + + function setAuthorized(address authorized) + public + { + AUTHORIZED_ADDRESS = authorized; + } + function createBridge( bytes4 returnCode, string memory revertError @@ -217,4 +225,20 @@ contract TestDexForwarderBridge is function balanceOf(address token, address owner) public view returns (uint256) { return TestDexForwarderBridgeTestToken(token).balanceOf(owner); } + + function _getGstAddress() + internal + view + returns (address gst) + { + return address(0); + } + + function _getERC20BridgeProxyAddress() + internal + view + returns (address erc20BridgeProxyAddress) + { + return AUTHORIZED_ADDRESS; + } } diff --git a/contracts/asset-proxy/test/dex_forwarder_bridge.ts b/contracts/asset-proxy/test/dex_forwarder_bridge.ts index e5032f558a..687dc02c1e 100644 --- a/contracts/asset-proxy/test/dex_forwarder_bridge.ts +++ b/contracts/asset-proxy/test/dex_forwarder_bridge.ts @@ -8,7 +8,7 @@ import { randomAddress, shortZip, } from '@0x/contracts-test-utils'; -import { BigNumber, hexUtils } from '@0x/utils'; +import { BigNumber, hexUtils, NULL_ADDRESS } from '@0x/utils'; import { DecodedLogs } from 'ethereum-types'; import * as _ from 'lodash'; @@ -31,6 +31,7 @@ blockchainTests.resets('DexForwarderBridge unit tests', env => { const BRIDGE_FAILURE = '0xffffffff'; const BRIDGE_REVERT_ERROR = 'oopsie'; const INCOMPLETE_FILL_REVERT = 'DexForwarderBridge/INCOMPLETE_FILL'; + const NOT_AUTHORIZED_REVERT = 'DexForwarderBridge/SENDER_NOT_AUTHORIZED'; const DEFAULTS = { toAddress: randomAddress(), }; @@ -47,6 +48,7 @@ blockchainTests.resets('DexForwarderBridge unit tests', env => { await callAndTransactAsync(testContract.createToken()), await callAndTransactAsync(testContract.createToken()), ]; + await callAndTransactAsync(testContract.setAuthorized(env.txDefaults.from as string)); }); async function callAndTransactAsync(fnCall: ContractTxFunctionObj): Promise { @@ -186,6 +188,18 @@ blockchainTests.resets('DexForwarderBridge unit tests', env => { ).to.revertWith(INCOMPLETE_FILL_REVERT); }); + it('fails if not authorized', async () => { + const calls = goodBridgeCalls.slice(0, 1); + const bridgeData = dexForwarderBridgeDataEncoder.encode({ + inputToken, + calls, + }); + await callAndTransactAsync(testContract.setAuthorized(NULL_ADDRESS)); + return expect(callBridgeTransferFromAsync({ bridgeData, sellAmount: new BigNumber(1) })).to.revertWith( + NOT_AUTHORIZED_REVERT, + ); + }); + it('succeeds with one bridge call', async () => { const calls = goodBridgeCalls.slice(0, 1); const bridgeData = dexForwarderBridgeDataEncoder.encode({ diff --git a/packages/asset-swapper/CHANGELOG.json b/packages/asset-swapper/CHANGELOG.json index 21f2552386..0ba1b4975d 100644 --- a/packages/asset-swapper/CHANGELOG.json +++ b/packages/asset-swapper/CHANGELOG.json @@ -37,6 +37,10 @@ { "note": "Fix `getBatchMarketBuyOrdersAsync` throwing NO_OPTIMAL_PATH", "pr": 2533 + }, + { + "note": "Add DFB support + refactor swap quote calculator utils", + "pr": 2536 } ] }, diff --git a/packages/asset-swapper/src/index.ts b/packages/asset-swapper/src/index.ts index fd5d5a2a4c..d1d918c563 100644 --- a/packages/asset-swapper/src/index.ts +++ b/packages/asset-swapper/src/index.ts @@ -12,7 +12,7 @@ export { SRAPollingOrderProviderOpts, SRAWebsocketOrderProviderOpts, } from '@0x/orderbook'; -export { APIOrder, Asset, AssetPairsItem, Order, SignedOrder } from '@0x/types'; +export { APIOrder, Asset, AssetPairsItem, SignedOrder } from '@0x/types'; export { BigNumber } from '@0x/utils'; export { DataItem, diff --git a/packages/asset-swapper/src/swap_quoter.ts b/packages/asset-swapper/src/swap_quoter.ts index 1ddbe95ad2..d87bef6a5b 100644 --- a/packages/asset-swapper/src/swap_quoter.ts +++ b/packages/asset-swapper/src/swap_quoter.ts @@ -179,7 +179,7 @@ export class SwapQuoter { }, liquidityProviderRegistryAddress, ); - this._swapQuoteCalculator = new SwapQuoteCalculator(this._protocolFeeUtils, this._marketOperationUtils); + this._swapQuoteCalculator = new SwapQuoteCalculator(this._marketOperationUtils); } /** diff --git a/packages/asset-swapper/src/utils/market_operation_utils/constants.ts b/packages/asset-swapper/src/utils/market_operation_utils/constants.ts index 6a37a77850..392edbe432 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/constants.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/constants.ts @@ -34,6 +34,7 @@ export const DEFAULT_GET_MARKET_ORDERS_OPTS: GetMarketOrdersOpts = { feeSchedule: {}, gasSchedule: {}, allowFallback: true, + shouldBatchBridgeOrders: true, }; /** diff --git a/packages/asset-swapper/src/utils/market_operation_utils/fills.ts b/packages/asset-swapper/src/utils/market_operation_utils/fills.ts index 11a472caef..a5090acb3a 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/fills.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/fills.ts @@ -233,27 +233,25 @@ export function clipPathToInput(path: Fill[], targetInput: BigNumber = POSITIVE_ return clipped; } -export function collapsePath(side: MarketOperation, path: Fill[]): CollapsedFill[] { +export function collapsePath(path: Fill[]): CollapsedFill[] { const collapsed: Array = []; for (const fill of path) { - const makerAssetAmount = side === MarketOperation.Sell ? fill.output : fill.input; - const takerAssetAmount = side === MarketOperation.Sell ? fill.input : fill.output; const source = fill.source; if (collapsed.length !== 0 && source !== ERC20BridgeSource.Native) { const prevFill = collapsed[collapsed.length - 1]; // If the last fill is from the same source, merge them. if (prevFill.source === source) { - prevFill.totalMakerAssetAmount = prevFill.totalMakerAssetAmount.plus(makerAssetAmount); - prevFill.totalTakerAssetAmount = prevFill.totalTakerAssetAmount.plus(takerAssetAmount); - prevFill.subFills.push({ makerAssetAmount, takerAssetAmount }); + prevFill.input = prevFill.input.plus(fill.input); + prevFill.output = prevFill.output.plus(fill.output); + prevFill.subFills.push(fill); continue; } } collapsed.push({ source: fill.source, - totalMakerAssetAmount: makerAssetAmount, - totalTakerAssetAmount: takerAssetAmount, - subFills: [{ makerAssetAmount, takerAssetAmount }], + input: fill.input, + output: fill.output, + subFills: [fill], nativeOrder: fill.source === ERC20BridgeSource.Native ? (fill.fillData as NativeFillData).order : undefined, }); } diff --git a/packages/asset-swapper/src/utils/market_operation_utils/index.ts b/packages/asset-swapper/src/utils/market_operation_utils/index.ts index 5f8ed0b3ec..371fcce13f 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/index.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/index.ts @@ -107,6 +107,7 @@ export class MarketOperationUtils { excludedSources: _opts.excludedSources, feeSchedule: _opts.feeSchedule, allowFallback: _opts.allowFallback, + shouldBatchBridgeOrders: _opts.shouldBatchBridgeOrders, }); } @@ -180,6 +181,7 @@ export class MarketOperationUtils { excludedSources: _opts.excludedSources, feeSchedule: _opts.feeSchedule, allowFallback: _opts.allowFallback, + shouldBatchBridgeOrders: _opts.shouldBatchBridgeOrders, }); } @@ -254,6 +256,7 @@ export class MarketOperationUtils { excludedSources: _opts.excludedSources, feeSchedule: _opts.feeSchedule, allowFallback: _opts.allowFallback, + shouldBatchBridgeOrders: _opts.shouldBatchBridgeOrders, }); } catch (e) { // It's possible for one of the pairs to have no path @@ -278,6 +281,7 @@ export class MarketOperationUtils { excludedSources?: ERC20BridgeSource[]; feeSchedule?: { [source: string]: BigNumber }; allowFallback?: boolean; + shouldBatchBridgeOrders?: boolean; liquidityProviderAddress?: string; }): OptimizedMarketOrder[] { const { inputToken, outputToken, side, inputAmount } = opts; @@ -327,6 +331,7 @@ export class MarketOperationUtils { contractAddresses: this.contractAddresses, bridgeSlippage: opts.bridgeSlippage || 0, liquidityProviderAddress: opts.liquidityProviderAddress, + shouldBatchBridgeOrders: !!opts.shouldBatchBridgeOrders, }); } diff --git a/packages/asset-swapper/src/utils/market_operation_utils/orders.ts b/packages/asset-swapper/src/utils/market_operation_utils/orders.ts index 6bef4ba01b..30c2f16d8c 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/orders.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/orders.ts @@ -1,6 +1,6 @@ import { ContractAddresses } from '@0x/contract-addresses'; import { assetDataUtils, ERC20AssetData, generatePseudoRandomSalt, orderCalculationUtils } from '@0x/order-utils'; -import { SignedOrder } from '@0x/types'; +import { ERC20BridgeAssetData, SignedOrder } from '@0x/types'; import { AbiEncoder, BigNumber } from '@0x/utils'; import { MarketOperation, SignedOrderWithFillableAmounts } from '../../types'; @@ -26,7 +26,31 @@ import { OrderDomain, } from './types'; -// tslint:disable completed-docs +// tslint:disable completed-docs no-unnecessary-type-assertion + +interface DexForwaderBridgeData { + inputToken: string; + calls: Array<{ + target: string; + inputTokenAmount: BigNumber; + outputTokenAmount: BigNumber; + bridgeData: string; + }>; +} + +const dexForwarderBridgeDataEncoder = AbiEncoder.create([ + { name: 'inputToken', type: 'address' }, + { + name: 'calls', + type: 'tuple[]', + components: [ + { name: 'target', type: 'address' }, + { name: 'inputTokenAmount', type: 'uint256' }, + { name: 'outputTokenAmount', type: 'uint256' }, + { name: 'bridgeData', type: 'bytes' }, + ], + }, +]); export function createDummyOrderForSampler( makerAssetData: string, @@ -71,12 +95,7 @@ export function convertNativeOrderToFullyFillableOptimizedOrders(order: SignedOr fillableMakerAssetAmount: order.makerAssetAmount, fillableTakerAssetAmount: order.takerAssetAmount, fillableTakerFeeAmount: order.takerFee, - fill: { - source: ERC20BridgeSource.Native, - totalMakerAssetAmount: order.makerAssetAmount, - totalTakerAssetAmount: order.takerAssetAmount, - subFills: [], - }, + fills: [], }; } @@ -119,18 +138,43 @@ export interface CreateOrderFromPathOpts { orderDomain: OrderDomain; contractAddresses: ContractAddresses; bridgeSlippage: number; + shouldBatchBridgeOrders: boolean; liquidityProviderAddress?: string; } // Convert sell fills into orders. export function createOrdersFromPath(path: Fill[], opts: CreateOrderFromPathOpts): OptimizedMarketOrder[] { - const collapsedPath = collapsePath(opts.side, path); + const collapsedPath = collapsePath(path); const orders: OptimizedMarketOrder[] = []; - for (const fill of collapsedPath) { - if (fill.source === ERC20BridgeSource.Native) { - orders.push(createNativeOrder(fill)); + for (let i = 0; i < collapsedPath.length; ) { + if (collapsedPath[i].source === ERC20BridgeSource.Native) { + orders.push(createNativeOrder(collapsedPath[i])); + ++i; + continue; + } + // Liquidity Provider must be called by ERC20BridgeProxy + if (collapsedPath[i].source === ERC20BridgeSource.LiquidityProvider) { + orders.push(createBridgeOrder(collapsedPath[i], opts)); + ++i; + continue; + } + // If there are contiguous bridge orders, we can batch them together. + const contiguousBridgeFills = [collapsedPath[i]]; + for (let j = i + 1; j < collapsedPath.length; ++j) { + if ( + collapsedPath[j].source === ERC20BridgeSource.Native || + collapsedPath[j].source === ERC20BridgeSource.LiquidityProvider + ) { + break; + } + contiguousBridgeFills.push(collapsedPath[j]); + } + if (contiguousBridgeFills.length === 1 || !opts.shouldBatchBridgeOrders) { + orders.push(createBridgeOrder(contiguousBridgeFills[0], opts)); + i += 1; } else { - orders.push(createBridgeOrder(fill, opts)); + orders.push(createBatchedBridgeOrder(contiguousBridgeFills, opts)); + i += contiguousBridgeFills.length; } } return orders; @@ -161,8 +205,7 @@ function getBridgeAddressFromSource(source: ERC20BridgeSource, opts: CreateOrder } function createBridgeOrder(fill: CollapsedFill, opts: CreateOrderFromPathOpts): OptimizedMarketOrder { - const takerToken = opts.side === MarketOperation.Sell ? opts.inputToken : opts.outputToken; - const makerToken = opts.side === MarketOperation.Sell ? opts.outputToken : opts.inputToken; + const [makerToken, takerToken] = getMakerTakerTokens(opts); const bridgeAddress = getBridgeAddressFromSource(fill.source, opts); let makerAssetData; @@ -182,14 +225,67 @@ function createBridgeOrder(fill: CollapsedFill, opts: CreateOrderFromPathOpts): createBridgeData(takerToken), ); } + const [slippedMakerAssetAmount, slippedTakerAssetAmount] = getSlippedBridgeAssetAmounts(fill, opts); return { - makerAddress: bridgeAddress, + fills: [fill], makerAssetData, takerAssetData: assetDataUtils.encodeERC20AssetData(takerToken), - ...createCommonBridgeOrderFields(fill, opts), + makerAddress: bridgeAddress, + makerAssetAmount: slippedMakerAssetAmount, + takerAssetAmount: slippedTakerAssetAmount, + fillableMakerAssetAmount: slippedMakerAssetAmount, + fillableTakerAssetAmount: slippedTakerAssetAmount, + ...createCommonBridgeOrderFields(opts), }; } +function createBatchedBridgeOrder(fills: CollapsedFill[], opts: CreateOrderFromPathOpts): OptimizedMarketOrder { + const [makerToken, takerToken] = getMakerTakerTokens(opts); + let totalMakerAssetAmount = ZERO_AMOUNT; + let totalTakerAssetAmount = ZERO_AMOUNT; + const batchedBridgeData: DexForwaderBridgeData = { + inputToken: takerToken, + calls: [], + }; + for (const fill of fills) { + const bridgeOrder = createBridgeOrder(fill, opts); + totalMakerAssetAmount = totalMakerAssetAmount.plus(bridgeOrder.makerAssetAmount); + totalTakerAssetAmount = totalTakerAssetAmount.plus(bridgeOrder.takerAssetAmount); + const { bridgeAddress, bridgeData: orderBridgeData } = assetDataUtils.decodeAssetDataOrThrow( + bridgeOrder.makerAssetData, + ) as ERC20BridgeAssetData; + batchedBridgeData.calls.push({ + target: bridgeAddress, + bridgeData: orderBridgeData, + inputTokenAmount: bridgeOrder.takerAssetAmount, + outputTokenAmount: bridgeOrder.makerAssetAmount, + }); + } + const batchedBridgeAddress = opts.contractAddresses.dexForwarderBridge; + const batchedMakerAssetData = assetDataUtils.encodeERC20BridgeAssetData( + makerToken, + batchedBridgeAddress, + dexForwarderBridgeDataEncoder.encode(batchedBridgeData), + ); + return { + fills, + makerAssetData: batchedMakerAssetData, + takerAssetData: assetDataUtils.encodeERC20AssetData(takerToken), + makerAddress: batchedBridgeAddress, + makerAssetAmount: totalMakerAssetAmount, + takerAssetAmount: totalTakerAssetAmount, + fillableMakerAssetAmount: totalMakerAssetAmount, + fillableTakerAssetAmount: totalTakerAssetAmount, + ...createCommonBridgeOrderFields(opts), + }; +} + +function getMakerTakerTokens(opts: CreateOrderFromPathOpts): [string, string] { + const makerToken = opts.side === MarketOperation.Sell ? opts.outputToken : opts.inputToken; + const takerToken = opts.side === MarketOperation.Sell ? opts.inputToken : opts.outputToken; + return [makerToken, takerToken]; +} + function createBridgeData(tokenAddress: string): string { const encoder = AbiEncoder.create([{ name: 'tokenAddress', type: 'address' }]); return encoder.encode({ tokenAddress }); @@ -210,22 +306,36 @@ function createCurveBridgeData( return curveBridgeDataEncoder.encode([curveAddress, fromTokenIdx, toTokenIdx, version]); } +function getSlippedBridgeAssetAmounts(fill: CollapsedFill, opts: CreateOrderFromPathOpts): [BigNumber, BigNumber] { + return [ + // Maker asset amount. + opts.side === MarketOperation.Sell + ? fill.output.times(1 - opts.bridgeSlippage).integerValue(BigNumber.ROUND_DOWN) + : fill.input, + // Taker asset amount. + opts.side === MarketOperation.Sell + ? fill.input + : fill.output.times(opts.bridgeSlippage + 1).integerValue(BigNumber.ROUND_UP), + ]; +} + type CommonBridgeOrderFields = Pick< OptimizedMarketOrder, - Exclude + Exclude< + keyof OptimizedMarketOrder, + | 'fills' + | 'makerAddress' + | 'makerAssetData' + | 'takerAssetData' + | 'makerAssetAmount' + | 'takerAssetAmount' + | 'fillableMakerAssetAmount' + | 'fillableTakerAssetAmount' + > >; -function createCommonBridgeOrderFields(fill: CollapsedFill, opts: CreateOrderFromPathOpts): CommonBridgeOrderFields { - const makerAssetAmountAdjustedWithSlippage = - opts.side === MarketOperation.Sell - ? fill.totalMakerAssetAmount.times(1 - opts.bridgeSlippage).integerValue(BigNumber.ROUND_DOWN) - : fill.totalMakerAssetAmount; - const takerAssetAmountAdjustedWithSlippage = - opts.side === MarketOperation.Sell - ? fill.totalTakerAssetAmount - : fill.totalTakerAssetAmount.times(opts.bridgeSlippage + 1).integerValue(BigNumber.ROUND_UP); +function createCommonBridgeOrderFields(opts: CreateOrderFromPathOpts): CommonBridgeOrderFields { return { - fill, takerAddress: NULL_ADDRESS, senderAddress: NULL_ADDRESS, feeRecipientAddress: NULL_ADDRESS, @@ -235,10 +345,6 @@ function createCommonBridgeOrderFields(fill: CollapsedFill, opts: CreateOrderFro takerFeeAssetData: NULL_BYTES, makerFee: ZERO_AMOUNT, takerFee: ZERO_AMOUNT, - makerAssetAmount: makerAssetAmountAdjustedWithSlippage, - fillableMakerAssetAmount: makerAssetAmountAdjustedWithSlippage, - takerAssetAmount: takerAssetAmountAdjustedWithSlippage, - fillableTakerAssetAmount: takerAssetAmountAdjustedWithSlippage, fillableTakerFeeAmount: ZERO_AMOUNT, signature: WALLET_SIGNATURE, ...opts.orderDomain, @@ -247,12 +353,7 @@ function createCommonBridgeOrderFields(fill: CollapsedFill, opts: CreateOrderFro function createNativeOrder(fill: CollapsedFill): OptimizedMarketOrder { return { - fill: { - source: fill.source, - totalMakerAssetAmount: fill.totalMakerAssetAmount, - totalTakerAssetAmount: fill.totalTakerAssetAmount, - subFills: fill.subFills, - }, + fills: [fill], ...(fill as NativeCollapsedFill).nativeOrder, }; } diff --git a/packages/asset-swapper/src/utils/market_operation_utils/types.ts b/packages/asset-swapper/src/utils/market_operation_utils/types.ts index 323297c7ca..b2d372a6e3 100644 --- a/packages/asset-swapper/src/utils/market_operation_utils/types.ts +++ b/packages/asset-swapper/src/utils/market_operation_utils/types.ts @@ -97,19 +97,19 @@ export interface CollapsedFill { */ source: ERC20BridgeSource; /** - * Total maker asset amount. + * Total input amount (sum of `subFill`s) */ - totalMakerAssetAmount: BigNumber; + input: BigNumber; /** - * Total taker asset amount. + * Total output amount (sum of `subFill`s) */ - totalTakerAssetAmount: BigNumber; + output: BigNumber; /** - * All the fill asset amounts that were collapsed into this node. + * Quantities of all the fills that were collapsed. */ subFills: Array<{ - makerAssetAmount: BigNumber; - takerAssetAmount: BigNumber; + input: BigNumber; + output: BigNumber; }>; } @@ -127,7 +127,7 @@ export interface OptimizedMarketOrder extends SignedOrderWithFillableAmounts { /** * The optimized fills that generated this order. */ - fill: CollapsedFill; + fills: CollapsedFill[]; } /** @@ -180,9 +180,14 @@ export interface GetMarketOrdersOpts { gasSchedule: { [source: string]: number }; /** * Whether to pad the quote with a redundant fallback quote using different - * sources. + * sources. Defaults to `true`. */ allowFallback: boolean; + /** + * Whether to combine contiguous bridge orders into a single DexForwarderBridge + * order. Defaults to `true`. + */ + shouldBatchBridgeOrders: boolean; } /** diff --git a/packages/asset-swapper/src/utils/protocol_fee_utils.ts b/packages/asset-swapper/src/utils/protocol_fee_utils.ts index a505c7e2c2..11c42cae9b 100644 --- a/packages/asset-swapper/src/utils/protocol_fee_utils.ts +++ b/packages/asset-swapper/src/utils/protocol_fee_utils.ts @@ -1,4 +1,3 @@ -import { Order } from '@0x/types'; import { BigNumber } from '@0x/utils'; import * as heartbeats from 'heartbeats'; @@ -15,12 +14,6 @@ export class ProtocolFeeUtils { this._initializeHeartBeat(); } - // TODO(dave4506) at some point, we should add a heart beat to the multiplier, or some RPC call to fetch latest multiplier. - // tslint:disable-next-line:prefer-function-over-method - public async getProtocolFeeMultiplierAsync(): Promise { - return constants.PROTOCOL_FEE_MULTIPLIER; - } - public async getGasPriceEstimationOrThrowAsync(shouldHardRefresh?: boolean): Promise { if (this.gasPriceEstimation.eq(constants.ZERO_AMOUNT)) { return this._getGasPriceFromGasStationOrThrowAsync(); @@ -39,18 +32,6 @@ export class ProtocolFeeUtils { this._gasPriceHeart.kill(); } - /** - * Calculates protocol fee with protofol fee multiplier for each fill. - */ - public async calculateWorstCaseProtocolFeeAsync( - orders: T[], - gasPrice: BigNumber, - ): Promise { - const protocolFeeMultiplier = await this.getProtocolFeeMultiplierAsync(); - const protocolFee = new BigNumber(orders.length).times(protocolFeeMultiplier).times(gasPrice); - return protocolFee; - } - // tslint:disable-next-line: prefer-function-over-method private async _getGasPriceFromGasStationOrThrowAsync(): Promise { try { diff --git a/packages/asset-swapper/src/utils/quote_simulation.ts b/packages/asset-swapper/src/utils/quote_simulation.ts new file mode 100644 index 0000000000..0b96f1729e --- /dev/null +++ b/packages/asset-swapper/src/utils/quote_simulation.ts @@ -0,0 +1,351 @@ +import { BigNumber } from '@0x/utils'; + +import { constants } from '../constants'; +import { MarketOperation } from '../types'; + +import { CollapsedFill, ERC20BridgeSource, OptimizedMarketOrder } from './market_operation_utils/types'; +import { isOrderTakerFeePayableWithMakerAsset, isOrderTakerFeePayableWithTakerAsset } from './utils'; + +const { PROTOCOL_FEE_MULTIPLIER, ZERO_AMOUNT } = constants; +const { ROUND_DOWN, ROUND_UP } = BigNumber; + +// tslint:disable completed-docs + +export interface QuoteFillResult { + // Maker asset bought. + makerAssetAmount: BigNumber; + // Taker asset sold. + takerAssetAmount: BigNumber; + // Taker fees that can be paid with the maker asset. + takerFeeMakerAssetAmount: BigNumber; + // Taker fees that can be paid with the taker asset. + takerFeeTakerAssetAmount: BigNumber; + // Total maker asset amount bought (including fees). + totalMakerAssetAmount: BigNumber; + // Total taker asset amount sold (including fees). + totalTakerAssetAmount: BigNumber; + // Protocol fees paid. + protocolFeeAmount: BigNumber; + // (Estimated) gas used. + gas: number; + // Fill amounts by source. + // For sells, this is the taker assets sold. + // For buys, this is the maker assets bought. + fillAmountBySource: { [source: string]: BigNumber }; +} + +interface IntermediateQuoteFillResult { + // Input tokens filled. Taker asset for sells, maker asset for buys. + input: BigNumber; + // Output tokens filled. Maker asset for sells, taker asset for buys. + output: BigNumber; + // Taker fees that can be paid with the input token. + // Positive for sells, negative for buys. + inputFee: BigNumber; + // Taker fees that can be paid with the output token. + // Negative for sells, positive for buys. + outputFee: BigNumber; + // Protocol fees paid. + protocolFee: BigNumber; + // (Estimated) gas used. + gas: number; + // Input amounts filled by sources. + inputBySource: { [source: string]: BigNumber }; +} + +const EMPTY_QUOTE_INTERMEDIATE_FILL_RESULT = { + input: ZERO_AMOUNT, + output: ZERO_AMOUNT, + outputFee: ZERO_AMOUNT, + inputFee: ZERO_AMOUNT, + protocolFee: ZERO_AMOUNT, + gas: 0, +}; + +export interface QuoteFillInfo { + orders: OptimizedMarketOrder[]; + fillAmount: BigNumber; + gasPrice: BigNumber; + side: MarketOperation; + opts: Partial; +} + +export interface QuoteFillInfoOpts { + gasSchedule: { [soruce: string]: number }; + protocolFeeMultiplier: BigNumber; +} + +const DEFAULT_SIMULATED_FILL_QUOTE_INFO_OPTS: QuoteFillInfoOpts = { + gasSchedule: {}, + protocolFeeMultiplier: PROTOCOL_FEE_MULTIPLIER, +}; + +export interface QuoteFillOrderCall { + order: OptimizedMarketOrder; + // Total input amount defined in the order. + totalOrderInput: BigNumber; + // Total output amount defined in the order. + totalOrderOutput: BigNumber; + // Total fees payable with input token, defined in the order. + // Positive for sells, negative for buys. + totalOrderInputFee: BigNumber; + // Total fees payable with output token, defined in the order. + // Negative for sells, positive for buys. + totalOrderOutputFee: BigNumber; +} + +// Simulates filling a quote in the best case. +export function simulateBestCaseFill(quoteInfo: QuoteFillInfo): QuoteFillResult { + const opts = { + ...DEFAULT_SIMULATED_FILL_QUOTE_INFO_OPTS, + ...quoteInfo.opts, + }; + const result = fillQuoteOrders( + quoteInfo.side, + createBestCaseFillOrderCalls(quoteInfo), + quoteInfo.fillAmount, + quoteInfo.gasPrice.times(opts.protocolFeeMultiplier), + opts.gasSchedule, + ); + return fromIntermediateQuoteFillResult(result, quoteInfo); +} + +// Simulates filling a quote in the worst case. +export function simulateWorstCaseFill(quoteInfo: QuoteFillInfo): QuoteFillResult { + const opts = { + ...DEFAULT_SIMULATED_FILL_QUOTE_INFO_OPTS, + ...quoteInfo.opts, + }; + const protocolFeePerFillOrder = quoteInfo.gasPrice.times(opts.protocolFeeMultiplier); + const result = { + ...fillQuoteOrders( + quoteInfo.side, + createWorstCaseFillOrderCalls(quoteInfo), + quoteInfo.fillAmount, + protocolFeePerFillOrder, + opts.gasSchedule, + ), + // Worst case gas and protocol fee is hitting all orders. + gas: getTotalGasUsedBySources( + getFlattenedFillsFromOrders(quoteInfo.orders).map(s => s.source), + opts.gasSchedule, + ), + protocolFee: protocolFeePerFillOrder.times(quoteInfo.orders.length), + }; + return fromIntermediateQuoteFillResult(result, quoteInfo); +} + +export function fillQuoteOrders( + side: MarketOperation, + fillOrders: QuoteFillOrderCall[], + inputAmount: BigNumber, + protocolFeePerFillOrder: BigNumber, + gasSchedule: { [source: string]: number }, +): IntermediateQuoteFillResult { + const result: IntermediateQuoteFillResult = { + ...EMPTY_QUOTE_INTERMEDIATE_FILL_RESULT, + inputBySource: {}, + }; + let remainingInput = inputAmount; + for (const fo of fillOrders) { + if (remainingInput.lte(0)) { + break; + } + for (const fill of fo.order.fills) { + if (remainingInput.lte(0)) { + break; + } + const { source } = fill; + result.gas += gasSchedule[source] || 0; + result.inputBySource[source] = result.inputBySource[source] || ZERO_AMOUNT; + + // Actual rates are rarely linear, so fill subfills individually to + // get a better approximation of fill size. + for (const subFill of fill.subFills) { + if (remainingInput.lte(0)) { + break; + } + const filledInput = solveForInputFillAmount( + remainingInput, + subFill.input, + fo.totalOrderInput, + fo.totalOrderInputFee, + ); + const filledOutput = subFill.output.times(filledInput.div(subFill.input)); + const filledInputFee = filledInput.div(fo.totalOrderInput).times(fo.totalOrderInputFee); + const filledOutputFee = filledOutput.div(fo.totalOrderOutput).times(fo.totalOrderOutputFee); + + result.inputBySource[source] = result.inputBySource[source].plus(filledInput); + result.input = result.input.plus(filledInput); + result.output = result.output.plus(filledOutput); + result.inputFee = result.inputFee.plus(filledInputFee); + result.outputFee = result.outputFee.plus(filledOutputFee); + remainingInput = remainingInput.minus(filledInput.plus(filledInputFee)); + } + } + result.protocolFee = result.protocolFee.plus(protocolFeePerFillOrder); + } + return result; +} + +function solveForInputFillAmount( + remainingInput: BigNumber, + fillableInput: BigNumber, + totalOrderInput: BigNumber, + totalOrderInputFee: BigNumber, +): BigNumber { + // When accounting for input token taker fees, the effective input amount is + // given by: + // i' = i + f * i / o + // where: + // i' - The effective input amount, including fees + // i - An input amount + // f - totalOrderInputFee + // o - totalOrderInput + // Solving for i we get: + // i = (i' * o) / (f + o) + const denom = totalOrderInput.plus(totalOrderInputFee); + if (denom.eq(0)) { + // A zero denominator would imply an order whose fees are >= the input + // token amount. + // For sells, takerFeeAmount >= takerAssetAmount (technically OK but really undesirable). + // For buys, takerFeeAmount >= makerAssetAmount (losing all your returns to fees). + return fillableInput; + } + return BigNumber.min( + fillableInput, + // let i' = remainingInput + remainingInput.times(totalOrderInput).div(denom), + ); +} + +function createBestCaseFillOrderCalls(quoteInfo: QuoteFillInfo): QuoteFillOrderCall[] { + const { orders, side } = quoteInfo; + return orders.map(o => ({ + order: o, + ...(side === MarketOperation.Sell + ? { + totalOrderInput: o.takerAssetAmount, + totalOrderOutput: o.makerAssetAmount, + totalOrderInputFee: isOrderTakerFeePayableWithTakerAsset(o) ? o.takerFee : ZERO_AMOUNT, + totalOrderOutputFee: isOrderTakerFeePayableWithMakerAsset(o) ? o.takerFee.negated() : ZERO_AMOUNT, + } + : // Buy + { + totalOrderInput: o.makerAssetAmount, + totalOrderOutput: o.takerAssetAmount, + totalOrderInputFee: isOrderTakerFeePayableWithMakerAsset(o) ? o.takerFee.negated() : ZERO_AMOUNT, + totalOrderOutputFee: isOrderTakerFeePayableWithTakerAsset(o) ? o.takerFee : ZERO_AMOUNT, + }), + })); +} + +function createWorstCaseFillOrderCalls(quoteInfo: QuoteFillInfo): QuoteFillOrderCall[] { + // Reuse best case fill orders. + return createBestCaseFillOrderCalls(quoteInfo) + .map(fo => ({ + ...fo, + order: { + ...fo.order, + // Apply slippage to order fills and reverse them. + fills: getSlippedOrderFills(fo.order, quoteInfo.side).reverse(), + }, + // Reverse the orders. + })) + .reverse(); +} + +// Apply order slippage to its fill paths. +function getSlippedOrderFills(order: OptimizedMarketOrder, side: MarketOperation): CollapsedFill[] { + const totalInput = BigNumber.sum(...order.fills.map(f => f.input)); + const totalOutput = BigNumber.sum(...order.fills.map(f => f.output)); + const inputScaling = + side === MarketOperation.Sell + ? order.fillableTakerAssetAmount.div(totalInput) + : order.fillableMakerAssetAmount.div(totalInput); + const outputScaling = + side === MarketOperation.Sell + ? order.fillableMakerAssetAmount.div(totalOutput) + : order.fillableTakerAssetAmount.div(totalOutput); + return order.fills.map(f => ({ + ...f, + input: f.input.times(inputScaling), + output: f.output.times(outputScaling), + subFills: f.subFills.map(sf => ({ + ...sf, + input: sf.input.times(inputScaling), + output: sf.output.times(outputScaling), + })), + })); +} + +function roundInputAmount(amount: BigNumber, side: MarketOperation): BigNumber { + return amount.integerValue(side === MarketOperation.Sell ? ROUND_UP : ROUND_DOWN); +} + +function roundOutputAmount(amount: BigNumber, side: MarketOperation): BigNumber { + return amount.integerValue(side === MarketOperation.Sell ? ROUND_DOWN : ROUND_UP); +} + +function roundIntermediateFillResult( + ir: IntermediateQuoteFillResult, + side: MarketOperation, +): IntermediateQuoteFillResult { + return { + input: roundInputAmount(ir.input, side), + output: roundOutputAmount(ir.output, side), + inputFee: roundInputAmount(ir.inputFee, side), + outputFee: roundOutputAmount(ir.outputFee, side), + protocolFee: ir.protocolFee.integerValue(ROUND_UP), + gas: Math.ceil(ir.gas), + inputBySource: Object.assign( + {}, + ...Object.entries(ir.inputBySource).map(([k, v]) => ({ [k]: roundInputAmount(v, side) })), + ), + }; +} + +function fromIntermediateQuoteFillResult(ir: IntermediateQuoteFillResult, quoteInfo: QuoteFillInfo): QuoteFillResult { + const { side } = quoteInfo; + const _ir = roundIntermediateFillResult(ir, side); + return { + ...(side === MarketOperation.Sell + ? // Sell + { + makerAssetAmount: _ir.output, + takerAssetAmount: _ir.input, + takerFeeMakerAssetAmount: _ir.outputFee, + takerFeeTakerAssetAmount: _ir.inputFee, + totalMakerAssetAmount: _ir.output.plus(_ir.outputFee), + totalTakerAssetAmount: _ir.input.plus(_ir.inputFee), + } + : // Buy + { + makerAssetAmount: _ir.input, + takerAssetAmount: _ir.output, + takerFeeMakerAssetAmount: _ir.inputFee, + takerFeeTakerAssetAmount: _ir.outputFee, + totalMakerAssetAmount: _ir.input.plus(_ir.inputFee), + totalTakerAssetAmount: _ir.output.plus(_ir.outputFee), + }), + protocolFeeAmount: _ir.protocolFee, + gas: _ir.gas, + fillAmountBySource: _ir.inputBySource, + }; +} + +export function getFlattenedFillsFromOrders(orders: OptimizedMarketOrder[]): CollapsedFill[] { + const fills = []; + for (const o of orders) { + fills.push(...o.fills); + } + return fills; +} + +function getTotalGasUsedBySources(sources: ERC20BridgeSource[], gasSchedule: { [source: string]: number }): number { + let gasUsed = 0; + for (const s of sources) { + gasUsed += gasSchedule[s] || 0; + } + return gasUsed; +} diff --git a/packages/asset-swapper/src/utils/swap_quote_calculator.ts b/packages/asset-swapper/src/utils/swap_quote_calculator.ts index 8f08cc1454..7ce60afbbc 100644 --- a/packages/asset-swapper/src/utils/swap_quote_calculator.ts +++ b/packages/asset-swapper/src/utils/swap_quote_calculator.ts @@ -1,9 +1,8 @@ -import { assetDataUtils, orderCalculationUtils } from '@0x/order-utils'; +import { assetDataUtils } from '@0x/order-utils'; import { AssetProxyId, SignedOrder } from '@0x/types'; import { BigNumber } from '@0x/utils'; import * as _ from 'lodash'; -import { constants } from '../constants'; import { CalculateSwapQuoteOpts, MarketBuySwapQuote, @@ -17,24 +16,18 @@ import { SwapQuoterError, } from '../types'; -import { fillableAmountsUtils } from './fillable_amounts_utils'; import { MarketOperationUtils } from './market_operation_utils'; import { convertNativeOrderToFullyFillableOptimizedOrders } from './market_operation_utils/orders'; -import { ERC20BridgeSource, GetMarketOrdersOpts, OptimizedMarketOrder } from './market_operation_utils/types'; -import { ProtocolFeeUtils } from './protocol_fee_utils'; -import { - isOrderTakerFeePayableWithMakerAsset, - isOrderTakerFeePayableWithTakerAsset, - isSupportedAssetDataInOrders, -} from './utils'; +import { GetMarketOrdersOpts, OptimizedMarketOrder } from './market_operation_utils/types'; +import { isSupportedAssetDataInOrders } from './utils'; + +import { QuoteFillResult, simulateBestCaseFill, simulateWorstCaseFill } from './quote_simulation'; // TODO(dave4506) How do we want to reintroduce InsufficientAssetLiquidityError? export class SwapQuoteCalculator { - private readonly _protocolFeeUtils: ProtocolFeeUtils; private readonly _marketOperationUtils: MarketOperationUtils; - constructor(protocolFeeUtils: ProtocolFeeUtils, marketOperationUtils: MarketOperationUtils) { - this._protocolFeeUtils = protocolFeeUtils; + constructor(marketOperationUtils: MarketOperationUtils) { this._marketOperationUtils = marketOperationUtils; } @@ -99,7 +92,7 @@ export class SwapQuoteCalculator { batchSignedOrders.map(async (orders, i) => { if (orders) { const { makerAssetData, takerAssetData } = batchPrunedOrders[i][0]; - return this._createSwapQuoteAsync( + return createSwapQuote( makerAssetData, takerAssetData, orders, @@ -163,7 +156,7 @@ export class SwapQuoteCalculator { // assetData information for the result const { makerAssetData, takerAssetData } = prunedOrders[0]; - return this._createSwapQuoteAsync( + return createSwapQuote( makerAssetData, takerAssetData, resultOrders, @@ -173,324 +166,75 @@ export class SwapQuoteCalculator { opts.gasSchedule, ); } - private async _createSwapQuoteAsync( - makerAssetData: string, - takerAssetData: string, - resultOrders: OptimizedMarketOrder[], - operation: MarketOperation, - assetFillAmount: BigNumber, - gasPrice: BigNumber, - gasSchedule: { [source: string]: number }, - ): Promise { - const bestCaseQuoteInfo = await this._calculateQuoteInfoAsync( - resultOrders, - assetFillAmount, - gasPrice, - gasSchedule, - operation, - ); - const worstCaseQuoteInfo = await this._calculateQuoteInfoAsync( - resultOrders, - assetFillAmount, - gasPrice, - gasSchedule, - operation, - true, - ); +} - const breakdown = getSwapQuoteOrdersBreakdown(resultOrders, operation); +function createSwapQuote( + makerAssetData: string, + takerAssetData: string, + resultOrders: OptimizedMarketOrder[], + operation: MarketOperation, + assetFillAmount: BigNumber, + gasPrice: BigNumber, + gasSchedule: { [source: string]: number }, +): SwapQuote { + const bestCaseFillResult = simulateBestCaseFill({ + gasPrice, + orders: resultOrders, + side: operation, + fillAmount: assetFillAmount, + opts: { gasSchedule }, + }); - const quoteBase: SwapQuoteBase = { - takerAssetData, - makerAssetData, - // Remove fill metadata. - orders: resultOrders.map(o => _.omit(o, 'fill')) as SignedOrderWithFillableAmounts[], - bestCaseQuoteInfo, - worstCaseQuoteInfo, - gasPrice, - sourceBreakdown: breakdown, - }; + const worstCaseFillResult = simulateWorstCaseFill({ + gasPrice, + orders: resultOrders, + side: operation, + fillAmount: assetFillAmount, + opts: { gasSchedule }, + }); - if (operation === MarketOperation.Buy) { - return { - ...quoteBase, - type: MarketOperation.Buy, - makerAssetFillAmount: assetFillAmount, - }; - } else { - return { - ...quoteBase, - type: MarketOperation.Sell, - takerAssetFillAmount: assetFillAmount, - }; - } - } + const quoteBase: SwapQuoteBase = { + takerAssetData, + makerAssetData, + gasPrice, + bestCaseQuoteInfo: fillResultsToQuoteInfo(bestCaseFillResult), + worstCaseQuoteInfo: fillResultsToQuoteInfo(worstCaseFillResult), + sourceBreakdown: getSwapQuoteOrdersBreakdown(bestCaseFillResult.fillAmountBySource), + // Remove fill metadata. + orders: resultOrders.map(o => _.omit(o, 'fills')) as SignedOrderWithFillableAmounts[], + }; - // tslint:disable-next-line: prefer-function-over-method - private async _calculateQuoteInfoAsync( - orders: OptimizedMarketOrder[], - assetFillAmount: BigNumber, - gasPrice: BigNumber, - gasSchedule: { [source: string]: number }, - operation: MarketOperation, - worstCase: boolean = false, - ): Promise { + if (operation === MarketOperation.Buy) { return { - ...(operation === MarketOperation.Buy - ? await this._calculateMarketBuyQuoteInfoAsync(orders, assetFillAmount, gasPrice, worstCase) - : await this._calculateMarketSellQuoteInfoAsync(orders, assetFillAmount, gasPrice, worstCase)), - gas: getGasUsedByOrders(orders, gasSchedule), + ...quoteBase, + type: MarketOperation.Buy, + makerAssetFillAmount: assetFillAmount, }; - } - - private async _calculateMarketSellQuoteInfoAsync( - orders: OptimizedMarketOrder[], - takerAssetSellAmount: BigNumber, - gasPrice: BigNumber, - worstCase: boolean = false, - ): Promise { - let totalMakerAssetAmount = constants.ZERO_AMOUNT; - let totalTakerAssetAmount = constants.ZERO_AMOUNT; - let totalFeeTakerAssetAmount = constants.ZERO_AMOUNT; - let remainingTakerAssetFillAmount = takerAssetSellAmount; - const filledOrders = [] as OptimizedMarketOrder[]; - const _orders = !worstCase ? orders : orders.slice().reverse(); - for (const order of _orders) { - let makerAssetAmount = constants.ZERO_AMOUNT; - let takerAssetAmount = constants.ZERO_AMOUNT; - let feeTakerAssetAmount = constants.ZERO_AMOUNT; - if (remainingTakerAssetFillAmount.lte(0)) { - break; - } - if (order.fill.source === ERC20BridgeSource.Native) { - const adjustedFillableMakerAssetAmount = fillableAmountsUtils.getMakerAssetAmountSwappedAfterOrderFees( - order, - ); - const adjustedFillableTakerAssetAmount = fillableAmountsUtils.getTakerAssetAmountSwappedAfterOrderFees( - order, - ); - const takerAssetAmountWithFees = BigNumber.min( - remainingTakerAssetFillAmount, - adjustedFillableTakerAssetAmount, - ); - const takerAssetAmountBreakDown = getTakerAssetAmountBreakDown(order, takerAssetAmountWithFees); - takerAssetAmount = takerAssetAmountBreakDown.takerAssetAmount; - feeTakerAssetAmount = takerAssetAmountBreakDown.feeTakerAssetAmount; - makerAssetAmount = takerAssetAmountWithFees - .div(adjustedFillableTakerAssetAmount) - .times(adjustedFillableMakerAssetAmount) - .integerValue(BigNumber.ROUND_DOWN); - } else { - // This is a collapsed bridge order. - // Because collapsed bridge orders actually fill at different rates, - // we can iterate over the uncollapsed fills to get the actual - // asset amounts transfered. - // We can also assume there are no fees and the order is not - // partially filled. - - // Infer the bridge slippage from the difference between the fill - // size and the actual order asset amounts. - const makerAssetBridgeSlippage = !worstCase - ? constants.ONE_AMOUNT - : order.makerAssetAmount.div(order.fill.totalMakerAssetAmount); - const takerAssetBridgeSlippage = !worstCase - ? constants.ONE_AMOUNT - : order.takerAssetAmount.div(order.fill.totalTakerAssetAmount); - // Consecutively fill the subfills in this order. - const subFills = !worstCase ? order.fill.subFills : order.fill.subFills.slice().reverse(); - for (const subFill of subFills) { - if (remainingTakerAssetFillAmount.minus(takerAssetAmount).lte(0)) { - break; - } - const partialTakerAssetAmount = subFill.takerAssetAmount.times(takerAssetBridgeSlippage); - const partialMakerAssetAmount = subFill.makerAssetAmount.times(makerAssetBridgeSlippage); - const partialTakerAssetFillAmount = BigNumber.min( - partialTakerAssetAmount, - remainingTakerAssetFillAmount.minus(takerAssetAmount), - ); - const partialMakerAssetFillAmount = partialTakerAssetFillAmount - .div(partialTakerAssetAmount) - .times(partialMakerAssetAmount) - .integerValue(BigNumber.ROUND_DOWN); - takerAssetAmount = takerAssetAmount.plus(partialTakerAssetFillAmount); - makerAssetAmount = makerAssetAmount.plus(partialMakerAssetFillAmount); - } - } - totalMakerAssetAmount = totalMakerAssetAmount.plus(makerAssetAmount); - totalTakerAssetAmount = totalTakerAssetAmount.plus(takerAssetAmount); - totalFeeTakerAssetAmount = totalFeeTakerAssetAmount.plus(feeTakerAssetAmount); - remainingTakerAssetFillAmount = remainingTakerAssetFillAmount - .minus(takerAssetAmount) - .minus(feeTakerAssetAmount); - filledOrders.push(order); - } - const protocolFeeInWeiAmount = await this._protocolFeeUtils.calculateWorstCaseProtocolFeeAsync( - !worstCase ? filledOrders : orders, - gasPrice, - ); + } else { return { - feeTakerAssetAmount: totalFeeTakerAssetAmount, - takerAssetAmount: totalTakerAssetAmount, - totalTakerAssetAmount: totalFeeTakerAssetAmount.plus(totalTakerAssetAmount), - makerAssetAmount: totalMakerAssetAmount, - protocolFeeInWeiAmount, - gas: 0, - }; - } - - private async _calculateMarketBuyQuoteInfoAsync( - orders: OptimizedMarketOrder[], - makerAssetBuyAmount: BigNumber, - gasPrice: BigNumber, - worstCase: boolean = false, - ): Promise { - let totalMakerAssetAmount = constants.ZERO_AMOUNT; - let totalTakerAssetAmount = constants.ZERO_AMOUNT; - let totalFeeTakerAssetAmount = constants.ZERO_AMOUNT; - let remainingMakerAssetFillAmount = makerAssetBuyAmount; - const filledOrders = [] as OptimizedMarketOrder[]; - const _orders = !worstCase ? orders : orders.slice().reverse(); - for (const order of _orders) { - let makerAssetAmount = constants.ZERO_AMOUNT; - let takerAssetAmount = constants.ZERO_AMOUNT; - let feeTakerAssetAmount = constants.ZERO_AMOUNT; - if (remainingMakerAssetFillAmount.lte(0)) { - break; - } - if (order.fill.source === ERC20BridgeSource.Native) { - const adjustedFillableMakerAssetAmount = fillableAmountsUtils.getMakerAssetAmountSwappedAfterOrderFees( - order, - ); - const adjustedFillableTakerAssetAmount = fillableAmountsUtils.getTakerAssetAmountSwappedAfterOrderFees( - order, - ); - makerAssetAmount = BigNumber.min(remainingMakerAssetFillAmount, adjustedFillableMakerAssetAmount); - const takerAssetAmountWithFees = makerAssetAmount - .div(adjustedFillableMakerAssetAmount) - .multipliedBy(adjustedFillableTakerAssetAmount) - .integerValue(BigNumber.ROUND_UP); - const takerAssetAmountBreakDown = getTakerAssetAmountBreakDown(order, takerAssetAmountWithFees); - takerAssetAmount = takerAssetAmountBreakDown.takerAssetAmount; - feeTakerAssetAmount = takerAssetAmountBreakDown.feeTakerAssetAmount; - } else { - // This is a collapsed bridge order. - // Because collapsed bridge orders actually fill at different rates, - // we can iterate over the uncollapsed fills to get the actual - // asset amounts transfered. - // We can also assume there are no fees and the order is not - // partially filled. - - // Infer the bridge slippage from the difference between the fill - // size and the actual order asset amounts. - const makerAssetBridgeSlippage = !worstCase - ? constants.ONE_AMOUNT - : order.makerAssetAmount.div(order.fill.totalMakerAssetAmount); - const takerAssetBridgeSlippage = !worstCase - ? constants.ONE_AMOUNT - : order.takerAssetAmount.div(order.fill.totalTakerAssetAmount); - // Consecutively fill the subfills in this order. - const subFills = !worstCase ? order.fill.subFills : order.fill.subFills.slice().reverse(); - for (const subFill of subFills) { - if (remainingMakerAssetFillAmount.minus(makerAssetAmount).lte(0)) { - break; - } - const partialTakerAssetAmount = subFill.takerAssetAmount.times(takerAssetBridgeSlippage); - const partialMakerAssetAmount = subFill.makerAssetAmount.times(makerAssetBridgeSlippage); - const partialMakerAssetFillAmount = BigNumber.min( - partialMakerAssetAmount, - remainingMakerAssetFillAmount.minus(makerAssetAmount), - ); - const partialTakerAssetFillAmount = partialMakerAssetFillAmount - .div(partialMakerAssetAmount) - .times(partialTakerAssetAmount) - .integerValue(BigNumber.ROUND_UP); - takerAssetAmount = takerAssetAmount.plus(partialTakerAssetFillAmount); - makerAssetAmount = makerAssetAmount.plus(partialMakerAssetFillAmount); - } - } - totalMakerAssetAmount = totalMakerAssetAmount.plus(makerAssetAmount); - totalTakerAssetAmount = totalTakerAssetAmount.plus(takerAssetAmount); - totalFeeTakerAssetAmount = totalFeeTakerAssetAmount.plus(feeTakerAssetAmount); - remainingMakerAssetFillAmount = remainingMakerAssetFillAmount.minus(makerAssetAmount); - filledOrders.push(order); - } - const protocolFeeInWeiAmount = await this._protocolFeeUtils.calculateWorstCaseProtocolFeeAsync( - !worstCase ? filledOrders : orders, - gasPrice, - ); - return { - feeTakerAssetAmount: totalFeeTakerAssetAmount, - takerAssetAmount: totalTakerAssetAmount, - totalTakerAssetAmount: totalFeeTakerAssetAmount.plus(totalTakerAssetAmount), - makerAssetAmount: totalMakerAssetAmount, - protocolFeeInWeiAmount, - gas: 0, + ...quoteBase, + type: MarketOperation.Sell, + takerAssetFillAmount: assetFillAmount, }; } } -function getSwapQuoteOrdersBreakdown( - orders: OptimizedMarketOrder[], - operation: MarketOperation, -): SwapQuoteOrdersBreakdown { - const orderAmounts = - operation === MarketOperation.Buy - ? orders.map(o => o.fill.totalMakerAssetAmount) - : orders.map(o => o.fill.totalTakerAssetAmount); - const amountsBySource: SwapQuoteOrdersBreakdown = {}; - orders.forEach((o, i) => { - const source = o.fill.source; - amountsBySource[source] = orderAmounts[i].plus(amountsBySource[source] || 0); - }); - const totalAmount = BigNumber.sum(0, ...orderAmounts); +function getSwapQuoteOrdersBreakdown(fillAmountBySource: { [source: string]: BigNumber }): SwapQuoteOrdersBreakdown { + const totalFillAmount = BigNumber.sum(...Object.values(fillAmountBySource)); const breakdown: SwapQuoteOrdersBreakdown = {}; - for (const [source, amount] of Object.entries(amountsBySource)) { - breakdown[source] = amount.div(totalAmount); - } + Object.entries(fillAmountBySource).forEach(([source, fillAmount]) => { + breakdown[source] = fillAmount.div(totalFillAmount); + }); return breakdown; } -function getTakerAssetAmountBreakDown( - order: SignedOrderWithFillableAmounts, - takerAssetAmountWithFees: BigNumber, -): { feeTakerAssetAmount: BigNumber; takerAssetAmount: BigNumber } { - if (isOrderTakerFeePayableWithTakerAsset(order)) { - const adjustedTakerAssetAmount = order.takerAssetAmount.plus(order.takerFee); - const filledRatio = takerAssetAmountWithFees.div(adjustedTakerAssetAmount); - const takerAssetAmount = filledRatio.multipliedBy(order.takerAssetAmount).integerValue(BigNumber.ROUND_CEIL); - return { - takerAssetAmount, - feeTakerAssetAmount: takerAssetAmountWithFees.minus(takerAssetAmount), - }; - } else if (isOrderTakerFeePayableWithMakerAsset(order)) { - if (takerAssetAmountWithFees.isZero()) { - return { - takerAssetAmount: constants.ZERO_AMOUNT, - feeTakerAssetAmount: constants.ZERO_AMOUNT, - }; - } - const takerFeeAmount = orderCalculationUtils.getTakerFeeAmount(order, takerAssetAmountWithFees); - const makerAssetFillAmount = orderCalculationUtils.getMakerFillAmount(order, takerAssetAmountWithFees); - const takerAssetAmount = takerFeeAmount - .div(makerAssetFillAmount) - .multipliedBy(takerAssetAmountWithFees) - .integerValue(BigNumber.ROUND_UP); - return { - takerAssetAmount, - feeTakerAssetAmount: takerAssetAmountWithFees.minus(takerAssetAmount), - }; - } +function fillResultsToQuoteInfo(fr: QuoteFillResult): SwapQuoteInfo { return { - feeTakerAssetAmount: constants.ZERO_AMOUNT, - takerAssetAmount: takerAssetAmountWithFees, + makerAssetAmount: fr.totalMakerAssetAmount, + takerAssetAmount: fr.takerAssetAmount, + totalTakerAssetAmount: fr.totalTakerAssetAmount, + feeTakerAssetAmount: fr.takerFeeTakerAssetAmount, + protocolFeeInWeiAmount: fr.protocolFeeAmount, + gas: fr.gas, }; } - -function getGasUsedByOrders(orders: OptimizedMarketOrder[], gasSchedule: { [source: string]: number }): number { - let totalUsage = 0; - for (const order of orders) { - totalUsage += gasSchedule[order.fill.source] || 0; - } - return totalUsage; -} -// tslint:disable: max-file-line-count diff --git a/packages/asset-swapper/test/exchange_swap_quote_consumer_test.ts b/packages/asset-swapper/test/exchange_swap_quote_consumer_test.ts index 3321aaac5f..06254a924b 100644 --- a/packages/asset-swapper/test/exchange_swap_quote_consumer_test.ts +++ b/packages/asset-swapper/test/exchange_swap_quote_consumer_test.ts @@ -12,7 +12,6 @@ import { SwapQuote } from '../src'; import { constants } from '../src/constants'; import { ExchangeSwapQuoteConsumer } from '../src/quote_consumers/exchange_swap_quote_consumer'; import { MarketOperation, SignedOrderWithFillableAmounts } from '../src/types'; -import { ProtocolFeeUtils } from '../src/utils/protocol_fee_utils'; import { chaiSetup } from './utils/chai_setup'; import { getFullyFillableSwapQuoteWithNoFeesAsync } from './utils/swap_quote'; @@ -60,7 +59,6 @@ const expectMakerAndTakerBalancesAsyncFactory = ( }; describe('ExchangeSwapQuoteConsumer', () => { - let protocolFeeUtils: ProtocolFeeUtils; let userAddresses: string[]; let erc20MakerTokenContract: ERC20TokenContract; let erc20TakerTokenContract: ERC20TokenContract; @@ -123,7 +121,6 @@ describe('ExchangeSwapQuoteConsumer', () => { }; const privateKey = devConstants.TESTRPC_PRIVATE_KEYS[userAddresses.indexOf(makerAddress)]; orderFactory = new OrderFactory(privateKey, defaultOrderParams); - protocolFeeUtils = new ProtocolFeeUtils(constants.PROTOCOL_FEE_UTILS_POLLING_INTERVAL_IN_MS, new BigNumber(1)); expectMakerAndTakerBalancesForTakerAssetAsync = expectMakerAndTakerBalancesAsyncFactory( erc20TakerTokenContract, makerAddress, @@ -156,7 +153,6 @@ describe('ExchangeSwapQuoteConsumer', () => { orders, MarketOperation.Sell, GAS_PRICE, - protocolFeeUtils, ); marketBuySwapQuote = await getFullyFillableSwapQuoteWithNoFeesAsync( @@ -165,7 +161,6 @@ describe('ExchangeSwapQuoteConsumer', () => { orders, MarketOperation.Buy, GAS_PRICE, - protocolFeeUtils, ); swapQuoteConsumer = new ExchangeSwapQuoteConsumer(provider, contractAddresses, { diff --git a/packages/asset-swapper/test/forwarder_swap_quote_consumer_test.ts b/packages/asset-swapper/test/forwarder_swap_quote_consumer_test.ts index 0113fe86ee..3c80e630e3 100644 --- a/packages/asset-swapper/test/forwarder_swap_quote_consumer_test.ts +++ b/packages/asset-swapper/test/forwarder_swap_quote_consumer_test.ts @@ -12,7 +12,6 @@ import { SwapQuote } from '../src'; import { constants } from '../src/constants'; import { ForwarderSwapQuoteConsumer } from '../src/quote_consumers/forwarder_swap_quote_consumer'; import { MarketOperation, SignedOrderWithFillableAmounts } from '../src/types'; -import { ProtocolFeeUtils } from '../src/utils/protocol_fee_utils'; import { chaiSetup } from './utils/chai_setup'; import { getFullyFillableSwapQuoteWithNoFeesAsync } from './utils/swap_quote'; @@ -61,7 +60,6 @@ const expectMakerAndTakerBalancesAsyncFactory = ( }; describe('ForwarderSwapQuoteConsumer', () => { - let protocolFeeUtils: ProtocolFeeUtils; let userAddresses: string[]; let coinbaseAddress: string; let makerAddress: string; @@ -126,7 +124,6 @@ describe('ForwarderSwapQuoteConsumer', () => { }; const privateKey = devConstants.TESTRPC_PRIVATE_KEYS[userAddresses.indexOf(makerAddress)]; orderFactory = new OrderFactory(privateKey, defaultOrderParams); - protocolFeeUtils = new ProtocolFeeUtils(constants.PROTOCOL_FEE_UTILS_POLLING_INTERVAL_IN_MS, new BigNumber(1)); expectMakerAndTakerBalancesAsync = expectMakerAndTakerBalancesAsyncFactory( erc20TokenContract, makerAddress, @@ -179,7 +176,6 @@ describe('ForwarderSwapQuoteConsumer', () => { orders, MarketOperation.Sell, GAS_PRICE, - protocolFeeUtils, ); marketBuySwapQuote = await getFullyFillableSwapQuoteWithNoFeesAsync( @@ -188,7 +184,6 @@ describe('ForwarderSwapQuoteConsumer', () => { orders, MarketOperation.Buy, GAS_PRICE, - protocolFeeUtils, ); invalidMarketBuySwapQuote = await getFullyFillableSwapQuoteWithNoFeesAsync( @@ -197,7 +192,6 @@ describe('ForwarderSwapQuoteConsumer', () => { invalidOrders, MarketOperation.Buy, GAS_PRICE, - protocolFeeUtils, ); swapQuoteConsumer = new ForwarderSwapQuoteConsumer(provider, contractAddresses, { diff --git a/packages/asset-swapper/test/market_operation_utils_test.ts b/packages/asset-swapper/test/market_operation_utils_test.ts index 87a809204c..05635af16c 100644 --- a/packages/asset-swapper/test/market_operation_utils_test.ts +++ b/packages/asset-swapper/test/market_operation_utils_test.ts @@ -299,6 +299,7 @@ describe('MarketOperationUtils tests', () => { maxFallbackSlippage: 100, excludedSources: Object.keys(DEFAULT_CURVE_OPTS) as ERC20BridgeSource[], allowFallback: false, + shouldBatchBridgeOrders: false, }; beforeEach(() => { @@ -422,7 +423,7 @@ describe('MarketOperationUtils tests', () => { ); expect(improvedOrders).to.not.be.length(0); for (const order of improvedOrders) { - const expectedMakerAmount = order.fill.totalMakerAssetAmount; + const expectedMakerAmount = order.fills[0].output; const slippage = 1 - order.makerAssetAmount.div(expectedMakerAmount.plus(1)).toNumber(); assertRoughlyEquals(slippage, bridgeSlippage, 1); } @@ -442,7 +443,7 @@ describe('MarketOperationUtils tests', () => { FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4 }, ); - const orderSources = improvedOrders.map(o => o.fill.source); + const orderSources = improvedOrders.map(o => o.fills[0].source); const expectedSources = [ ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Uniswap, @@ -466,7 +467,7 @@ describe('MarketOperationUtils tests', () => { FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4 }, ); - const orderSources = improvedOrders.map(o => o.fill.source); + const orderSources = improvedOrders.map(o => o.fills[0].source); if (orderSources.includes(ERC20BridgeSource.Kyber)) { expect(orderSources).to.not.include(ERC20BridgeSource.Uniswap); expect(orderSources).to.not.include(ERC20BridgeSource.Eth2Dai); @@ -501,7 +502,7 @@ describe('MarketOperationUtils tests', () => { FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4, feeSchedule }, ); - const orderSources = improvedOrders.map(o => o.fill.source); + const orderSources = improvedOrders.map(o => o.fills[0].source); const expectedSources = [ ERC20BridgeSource.Native, ERC20BridgeSource.Uniswap, @@ -536,7 +537,7 @@ describe('MarketOperationUtils tests', () => { FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4, feeSchedule }, ); - const orderSources = improvedOrders.map(o => o.fill.source); + const orderSources = improvedOrders.map(o => o.fills[0].source); const expectedSources = [ ERC20BridgeSource.Native, ERC20BridgeSource.Eth2Dai, @@ -561,7 +562,7 @@ describe('MarketOperationUtils tests', () => { FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4 }, ); - const orderSources = improvedOrders.map(o => o.fill.source); + const orderSources = improvedOrders.map(o => o.fills[0].source); const expectedSources = [ ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Uniswap, @@ -584,7 +585,7 @@ describe('MarketOperationUtils tests', () => { FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4, allowFallback: true }, ); - const orderSources = improvedOrders.map(o => o.fill.source); + const orderSources = improvedOrders.map(o => o.fills[0].source); const firstSources = [ ERC20BridgeSource.Native, ERC20BridgeSource.Native, @@ -610,7 +611,7 @@ describe('MarketOperationUtils tests', () => { FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4, allowFallback: true, maxFallbackSlippage: 0.5 }, ); - const orderSources = improvedOrders.map(o => o.fill.source); + const orderSources = improvedOrders.map(o => o.fills[0].source); const firstSources = [ERC20BridgeSource.Native, ERC20BridgeSource.Native, ERC20BridgeSource.Uniswap]; const secondSources: ERC20BridgeSource[] = []; expect(orderSources.slice(0, firstSources.length).sort()).to.deep.eq(firstSources.sort()); @@ -672,6 +673,37 @@ describe('MarketOperationUtils tests', () => { expect(getLiquidityProviderParams.makerToken).is.eql(yAsset); expect(getLiquidityProviderParams.takerToken).is.eql(xAsset); }); + + it('batches contiguous bridge sources', async () => { + const rates: RatesBySource = {}; + rates[ERC20BridgeSource.Uniswap] = [1, 0.01, 0.01, 0.01]; + rates[ERC20BridgeSource.Native] = [0.5, 0.01, 0.01, 0.01]; + rates[ERC20BridgeSource.Eth2Dai] = [0.49, 0.01, 0.01, 0.01]; + rates[ERC20BridgeSource.CurveUsdcDai] = [0.48, 0.01, 0.01, 0.01]; + replaceSamplerOps({ + getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates), + }); + const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync( + createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), + FILL_AMOUNT, + { + ...DEFAULT_OPTS, + numSamples: 4, + excludedSources: [ + ERC20BridgeSource.Kyber, + ..._.without(DEFAULT_OPTS.excludedSources, ERC20BridgeSource.CurveUsdcDai), + ], + shouldBatchBridgeOrders: true, + }, + ); + expect(improvedOrders).to.be.length(3); + const orderFillSources = improvedOrders.map(o => o.fills.map(f => f.source)); + expect(orderFillSources).to.deep.eq([ + [ERC20BridgeSource.Uniswap], + [ERC20BridgeSource.Native], + [ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.CurveUsdcDai], + ]); + }); }); describe('getMarketBuyOrdersAsync()', () => { @@ -687,6 +719,7 @@ describe('MarketOperationUtils tests', () => { maxFallbackSlippage: 100, excludedSources: Object.keys(DEFAULT_CURVE_OPTS) as ERC20BridgeSource[], allowFallback: false, + shouldBatchBridgeOrders: false, }; beforeEach(() => { @@ -789,7 +822,7 @@ describe('MarketOperationUtils tests', () => { } }); - it('generates bridge orders with correct taker amount', async () => { + it('generates bridge orders with correct maker amount', async () => { const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync( // Pass in empty orders to prevent native orders from being used. ORDERS.map(o => ({ ...o, makerAssetAmount: constants.ZERO_AMOUNT })), @@ -810,7 +843,7 @@ describe('MarketOperationUtils tests', () => { ); expect(improvedOrders).to.not.be.length(0); for (const order of improvedOrders) { - const expectedTakerAmount = order.fill.totalTakerAssetAmount; + const expectedTakerAmount = order.fills[0].output; const slippage = order.takerAssetAmount.div(expectedTakerAmount.plus(1)).toNumber() - 1; assertRoughlyEquals(slippage, bridgeSlippage, 1); } @@ -829,7 +862,7 @@ describe('MarketOperationUtils tests', () => { FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4 }, ); - const orderSources = improvedOrders.map(o => o.fill.source); + const orderSources = improvedOrders.map(o => o.fills[0].source); const expectedSources = [ ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Uniswap, @@ -865,7 +898,7 @@ describe('MarketOperationUtils tests', () => { FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4, feeSchedule }, ); - const orderSources = improvedOrders.map(o => o.fill.source); + const orderSources = improvedOrders.map(o => o.fills[0].source); const expectedSources = [ ERC20BridgeSource.Uniswap, ERC20BridgeSource.Eth2Dai, @@ -899,7 +932,7 @@ describe('MarketOperationUtils tests', () => { FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4, feeSchedule }, ); - const orderSources = improvedOrders.map(o => o.fill.source); + const orderSources = improvedOrders.map(o => o.fills[0].source); const expectedSources = [ ERC20BridgeSource.Native, ERC20BridgeSource.Eth2Dai, @@ -921,7 +954,7 @@ describe('MarketOperationUtils tests', () => { FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4, allowFallback: true }, ); - const orderSources = improvedOrders.map(o => o.fill.source); + const orderSources = improvedOrders.map(o => o.fills[0].source); const firstSources = [ ERC20BridgeSource.Native, ERC20BridgeSource.Native, @@ -946,12 +979,37 @@ describe('MarketOperationUtils tests', () => { FILL_AMOUNT, { ...DEFAULT_OPTS, numSamples: 4, allowFallback: true, maxFallbackSlippage: 0.5 }, ); - const orderSources = improvedOrders.map(o => o.fill.source); + const orderSources = improvedOrders.map(o => o.fills[0].source); const firstSources = [ERC20BridgeSource.Native, ERC20BridgeSource.Native, ERC20BridgeSource.Uniswap]; const secondSources: ERC20BridgeSource[] = []; expect(orderSources.slice(0, firstSources.length).sort()).to.deep.eq(firstSources.sort()); expect(orderSources.slice(firstSources.length).sort()).to.deep.eq(secondSources.sort()); }); + + it('batches contiguous bridge sources', async () => { + const rates: RatesBySource = {}; + rates[ERC20BridgeSource.Native] = [0.5, 0.01, 0.01, 0.01]; + rates[ERC20BridgeSource.Eth2Dai] = [0.49, 0.01, 0.01, 0.01]; + rates[ERC20BridgeSource.Uniswap] = [0.48, 0.47, 0.01, 0.01]; + replaceSamplerOps({ + getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates), + }); + const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync( + createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]), + FILL_AMOUNT, + { + ...DEFAULT_OPTS, + numSamples: 4, + shouldBatchBridgeOrders: true, + }, + ); + expect(improvedOrders).to.be.length(2); + const orderFillSources = improvedOrders.map(o => o.fills.map(f => f.source)); + expect(orderFillSources).to.deep.eq([ + [ERC20BridgeSource.Native], + [ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Uniswap], + ]); + }); }); }); }); diff --git a/packages/asset-swapper/test/quote_simulation_test.ts b/packages/asset-swapper/test/quote_simulation_test.ts new file mode 100644 index 0000000000..45b3d5a74b --- /dev/null +++ b/packages/asset-swapper/test/quote_simulation_test.ts @@ -0,0 +1,948 @@ +import { + assertIntegerRoughlyEquals, + constants, + expect, + getRandomInteger, + randomAddress, +} from '@0x/contracts-test-utils'; +import { assetDataUtils } from '@0x/order-utils'; +import { BigNumber } from '@0x/utils'; +import * as _ from 'lodash'; + +import { MarketOperation } from '../src/types'; +import { CollapsedFill, ERC20BridgeSource, OptimizedMarketOrder } from '../src/utils/market_operation_utils/types'; +import { + fillQuoteOrders, + QuoteFillOrderCall, + simulateBestCaseFill, + simulateWorstCaseFill, +} from '../src/utils/quote_simulation'; + +// tslint:disable: custom-no-magic-numbers + +describe('quote_simulation tests', async () => { + const { NULL_ADDRESS } = constants; + const ZERO = new BigNumber(0); + const ONE = new BigNumber(1); + const MAKER_TOKEN = randomAddress(); + const TAKER_TOKEN = randomAddress(); + const DEFAULT_MAKER_ASSET_DATA = assetDataUtils.encodeERC20AssetData(MAKER_TOKEN); + const DEFAULT_TAKER_ASSET_DATA = assetDataUtils.encodeERC20AssetData(TAKER_TOKEN); + const EPS = 1e7; // Some precision lost when crafting these orders. + const GAS_SCHEDULE = { [ERC20BridgeSource.Native]: 1 }; + + function createQuoteFillOrders( + opts: Partial<{ + fillableInput: BigNumber; + fillableOutput: BigNumber; + inputFeeRate: number; + outputFeeRate: number; + count: number; + fillsCount: number; + side: MarketOperation; + }> = {}, + ): QuoteFillOrderCall[] { + const { fillableInput, fillableOutput, inputFeeRate, outputFeeRate, count, fillsCount, side } = { + fillableInput: getRandomOrderSize(), + fillableOutput: getRandomOrderSize(), + inputFeeRate: 0, + outputFeeRate: 0, + count: 3, + fillsCount: 3, + side: MarketOperation.Sell, + ...opts, + }; + const _inputFeeRate = side === MarketOperation.Sell ? inputFeeRate : -inputFeeRate; + const _outputFeeRate = side === MarketOperation.Sell ? -outputFeeRate : outputFeeRate; + + const fillableInputs = subdivideAmount(fillableInput, count); + const fillableOutputs = subdivideAmount(fillableOutput, count); + const filledInputs = subdivideAmount(fillableInput.times(0.5), count); + const filledOutputs: BigNumber[] = []; + const totalInputs: BigNumber[] = []; + const totalOutputs: BigNumber[] = []; + const inputFees: BigNumber[] = []; + const outputFees: BigNumber[] = []; + _.times(count).forEach(i => { + const f = filledInputs[i].div(fillableInputs[i]); + filledOutputs.push(fillableOutputs[i].times(f).integerValue(BigNumber.ROUND_DOWN)); + totalInputs.push(fillableInputs[i].plus(filledInputs[i])); + totalOutputs.push(fillableOutputs[i].plus(filledOutputs[i])); + inputFees.push(totalInputs[i].times(_inputFeeRate).integerValue()); + outputFees.push(totalOutputs[i].times(_outputFeeRate).integerValue()); + }); + return _.times(count, i => { + return { + order: createQuoteFillOrderOrder(totalInputs[i], totalOutputs[i], { + side, + fillsCount, + filledInput: filledInputs[i], + takerInputFee: inputFees[i].abs(), + takerOutputFee: outputFees[i].abs(), + }), + totalOrderInput: totalInputs[i], + totalOrderOutput: totalOutputs[i], + totalOrderInputFee: inputFees[i], + totalOrderOutputFee: outputFees[i], + }; + }); + } + + function createQuoteFillOrderOrder( + input: BigNumber, + output: BigNumber, + opts: Partial<{ + filledInput: BigNumber; + fillsCount: number; + side: MarketOperation; + takerInputFee: BigNumber; + takerOutputFee: BigNumber; + }> = {}, + ): OptimizedMarketOrder { + const { filledInput, fillsCount, side, takerInputFee, takerOutputFee } = { + side: MarketOperation.Sell, + filledInput: ZERO, + fillsCount: 3, + takerInputFee: ZERO, + takerOutputFee: ZERO, + ...opts, + }; + const filledOutput = filledInput + .div(input) + .times(output) + .integerValue(BigNumber.ROUND_DOWN); + const fillableInput = input.minus(filledInput); + const fillableOutput = output.minus(filledOutput); + const makerAssetAmount = side === MarketOperation.Sell ? output : input; + const takerAssetAmount = side === MarketOperation.Sell ? input : output; + const fillableMakerAssetAmount = side === MarketOperation.Sell ? fillableOutput : fillableInput; + const fillableTakerAssetAmount = side === MarketOperation.Sell ? fillableInput : fillableOutput; + const takerFee = BigNumber.max(takerInputFee, takerOutputFee); + let takerFeeAssetData = '0x'; + if (!takerInputFee.eq(0)) { + takerFeeAssetData = side === MarketOperation.Sell ? DEFAULT_TAKER_ASSET_DATA : DEFAULT_MAKER_ASSET_DATA; + } else if (!takerOutputFee.eq(0)) { + takerFeeAssetData = side === MarketOperation.Sell ? DEFAULT_MAKER_ASSET_DATA : DEFAULT_TAKER_ASSET_DATA; + } + const fillableTakerFeeAmount = fillableTakerAssetAmount + .div(takerAssetAmount) + .times(takerFee) + .integerValue(BigNumber.ROUND_DOWN); + return { + makerAssetAmount, + takerAssetAmount, + fillableTakerAssetAmount, + fillableMakerAssetAmount, + fillableTakerFeeAmount, + takerFee, + takerFeeAssetData, + fills: createOrderCollapsedFills(fillableInput, fillableOutput, fillsCount), + chainId: 1, + exchangeAddress: NULL_ADDRESS, + expirationTimeSeconds: ZERO, + feeRecipientAddress: NULL_ADDRESS, + senderAddress: NULL_ADDRESS, + makerAddress: NULL_ADDRESS, + takerAddress: NULL_ADDRESS, + makerAssetData: DEFAULT_MAKER_ASSET_DATA, + takerAssetData: DEFAULT_TAKER_ASSET_DATA, + makerFeeAssetData: '0x', + salt: ZERO, + makerFee: ZERO, + signature: '0x', + }; + } + + function createOrderCollapsedFills(input: BigNumber, output: BigNumber, count: number): CollapsedFill[] { + const inputs = subdivideAmount(input, count); + const outputs = subdivideAmount(output, count); + return _.times(count, i => { + const subFillInputs = subdivideAmount(inputs[i], count); + const subFillOutputs = subdivideAmount(outputs[i], count); + return { + source: ERC20BridgeSource.Native, + input: inputs[i], + output: outputs[i], + subFills: _.times(count, j => ({ + input: subFillInputs[j], + output: subFillOutputs[j], + })), + }; + }); + } + + function countCollapsedFills(fillOrders: QuoteFillOrderCall[] | OptimizedMarketOrder[]): number { + let count = 0; + if ((fillOrders as any)[0].fills) { + const orders = (fillOrders as any) as OptimizedMarketOrder[]; + for (const o of orders) { + count += o.fills.length; + } + } else { + const orders = (fillOrders as any) as QuoteFillOrderCall[]; + for (const fo of orders) { + count += fo.order.fills.length; + } + } + return count; + } + + function randomSide(): MarketOperation { + return _.sampleSize(Object.values(MarketOperation), 1)[0]; + } + + function getRandomOrderSize(): BigNumber { + return getRandomInteger('100e18', '1000e18'); + } + + function getRandomFeeRate(): number { + return _.random(0.01, 0.25, true); + } + + function assertEqualRates(actual: number | BigNumber, expected: number | BigNumber): void { + expect(new BigNumber(actual).times(1e4).integerValue()).to.bignumber.eq( + new BigNumber(expected).times(1e4).integerValue(), + ); + } + + function subdivideAmount(amount: BigNumber, count: number): BigNumber[] { + const amounts = []; + for (let i = 0; i < count; ++i) { + const remaining = amount.minus(BigNumber.sum(0, ...amounts)); + if (i !== count - 1) { + amounts.push(remaining.times(Math.random()).integerValue()); + } else { + amounts.push(remaining.integerValue()); + } + } + return amounts; + } + + describe('fillQuoteOrders()', () => { + describe('single order', () => { + it('can exactly fill one order', () => { + const side = randomSide(); + const fillsCount = _.random(1, 3); + const fillableInput = getRandomOrderSize(); + const fillableOutput = getRandomOrderSize(); + const fillOrders = createQuoteFillOrders({ + fillableInput, + fillableOutput, + side, + fillsCount, + count: 1, + }); + const result = fillQuoteOrders(side, fillOrders, fillableInput, ONE, GAS_SCHEDULE); + const totalFilledInput = result.input.plus(result.inputFee); + const totalFilledOutput = result.output.plus(result.outputFee); + expect(totalFilledInput).to.bignumber.eq(fillableInput); + assertIntegerRoughlyEquals(totalFilledOutput, fillableOutput, EPS); + expect(result.protocolFee).to.bignumber.eq(1); + expect(result.gas).to.eq(fillsCount); + }); + + it('can partially fill one simple order', () => { + const side = randomSide(); + const fillsCount = 1; + const fillableInput = getRandomOrderSize(); + const fillableOutput = getRandomOrderSize(); + const fillOrders = createQuoteFillOrders({ + fillableInput, + fillableOutput, + side, + fillsCount, + count: 1, + }); + const inputFillAmount = fillableInput.times(2 / 3).integerValue(); + const result = fillQuoteOrders(side, fillOrders, inputFillAmount, ONE, GAS_SCHEDULE); + const totalFilledInput = result.input.plus(result.inputFee); + const totalFilledOutput = result.output.plus(result.outputFee); + expect(totalFilledInput).to.bignumber.eq(inputFillAmount); + const expectedOutputFilledAmount = inputFillAmount + .div(fillableInput) + .times(fillableOutput) + .integerValue(); + assertIntegerRoughlyEquals(totalFilledOutput, expectedOutputFilledAmount, EPS); + expect(result.protocolFee).to.bignumber.eq(1); + expect(result.gas).to.eq(1); + }); + + it('can partially fill one batched order', () => { + const side = randomSide(); + const fillsCount = 3; + const fillableInput = getRandomOrderSize(); + const fillableOutput = getRandomOrderSize(); + const fillOrders = createQuoteFillOrders({ + fillableInput, + fillableOutput, + side, + fillsCount, + count: 1, + }); + const inputFillAmount = fillableInput.times(2 / 3).integerValue(); + const result = fillQuoteOrders(side, fillOrders, inputFillAmount, ONE, GAS_SCHEDULE); + const totalFilledInput = result.input.plus(result.inputFee); + const totalFilledOutput = result.output.plus(result.outputFee); + expect(totalFilledInput).to.bignumber.eq(inputFillAmount); + expect(totalFilledOutput).to.bignumber.lt(fillableOutput); + expect(result.protocolFee).to.bignumber.eq(1); + expect(result.gas).to.gte(1); + expect(result.gas).to.lte(fillsCount); + }); + + it('does not over fill one order', () => { + const side = randomSide(); + const fillsCount = _.random(1, 3); + const fillableInput = getRandomOrderSize(); + const fillableOutput = getRandomOrderSize(); + const fillOrders = createQuoteFillOrders({ + fillableInput, + fillableOutput, + side, + fillsCount, + count: 1, + }); + const inputFillAmount = fillableInput.times(3 / 2).integerValue(); + const result = fillQuoteOrders(side, fillOrders, inputFillAmount, ONE, GAS_SCHEDULE); + const totalFilledInput = result.input.plus(result.inputFee); + const totalFilledOutput = result.output.plus(result.outputFee); + expect(totalFilledInput).to.bignumber.eq(fillableInput); + assertIntegerRoughlyEquals(totalFilledOutput, fillableOutput, EPS); + expect(result.protocolFee).to.bignumber.eq(1); + expect(result.gas).to.eq(fillsCount); + }); + + it('can exactly fill one order with input fees', () => { + const side = randomSide(); + const fillsCount = _.random(1, 3); + const fillableInput = getRandomOrderSize(); + const fillableOutput = getRandomOrderSize(); + const inputFeeRate = getRandomFeeRate(); + const fillOrders = createQuoteFillOrders({ + fillableInput, + fillableOutput, + inputFeeRate, + side, + fillsCount, + count: 1, + }); + const signedInputFeeRate = side === MarketOperation.Sell ? inputFeeRate : -inputFeeRate; + const totalFillableInput = fillableInput.times(signedInputFeeRate + 1).integerValue(); + const result = fillQuoteOrders(side, fillOrders, totalFillableInput, ONE, GAS_SCHEDULE); + const totalFilledInput = result.input.plus(result.inputFee); + const totalFilledOutput = result.output.plus(result.outputFee); + assertIntegerRoughlyEquals(totalFilledInput, totalFillableInput, EPS); + assertIntegerRoughlyEquals(totalFilledOutput, fillableOutput, EPS); + assertEqualRates(result.inputFee.div(result.input), signedInputFeeRate); + expect(result.protocolFee).to.bignumber.eq(1); + expect(result.gas).to.eq(fillsCount); + }); + + it('can partially fill one order with input fees', () => { + const side = randomSide(); + const fillsCount = _.random(1, 3); + const fillableInput = getRandomOrderSize(); + const fillableOutput = getRandomOrderSize(); + const inputFeeRate = getRandomFeeRate(); + const fillOrders = createQuoteFillOrders({ + fillableInput, + fillableOutput, + inputFeeRate, + side, + fillsCount, + count: 1, + }); + const signedInputFeeRate = side === MarketOperation.Sell ? inputFeeRate : -inputFeeRate; + const totalFillableInput = fillableInput.times(signedInputFeeRate + 1).integerValue(); + const inputFillAmount = totalFillableInput.times(2 / 3).integerValue(); + const result = fillQuoteOrders(side, fillOrders, inputFillAmount, ONE, GAS_SCHEDULE); + const totalFilledInput = result.input.plus(result.inputFee); + const totalFilledOutput = result.output.plus(result.outputFee); + assertIntegerRoughlyEquals(totalFilledInput, inputFillAmount, EPS); + expect(totalFilledOutput).to.bignumber.lt(fillableOutput); + assertEqualRates(result.inputFee.div(result.input), signedInputFeeRate); + expect(result.protocolFee).to.bignumber.eq(1); + expect(result.gas).to.lte(fillsCount); + }); + + it('does not over fill one order with input fees', () => { + const side = randomSide(); + const fillsCount = _.random(1, 3); + const fillableInput = getRandomOrderSize(); + const fillableOutput = getRandomOrderSize(); + const inputFeeRate = getRandomFeeRate(); + const fillOrders = createQuoteFillOrders({ + fillableInput, + fillableOutput, + inputFeeRate, + side, + fillsCount, + count: 1, + }); + const signedInputFeeRate = side === MarketOperation.Sell ? inputFeeRate : -inputFeeRate; + const totalFillableInput = fillableInput.times(signedInputFeeRate + 1).integerValue(); + const inputFillAmount = totalFillableInput.times(3 / 2).integerValue(); + const result = fillQuoteOrders(side, fillOrders, inputFillAmount, ONE, GAS_SCHEDULE); + const totalFilledInput = result.input.plus(result.inputFee); + const totalFilledOutput = result.output.plus(result.outputFee); + assertIntegerRoughlyEquals(totalFilledInput, totalFillableInput, EPS); + assertIntegerRoughlyEquals(totalFilledOutput, fillableOutput, EPS); + assertEqualRates(result.inputFee.div(result.input), signedInputFeeRate); + expect(result.protocolFee).to.bignumber.eq(1); + expect(result.gas).to.eq(fillsCount); + }); + + it('can exactly fill one order with output fees', () => { + const side = randomSide(); + const fillsCount = _.random(1, 3); + const fillableInput = getRandomOrderSize(); + const fillableOutput = getRandomOrderSize(); + const outputFeeRate = getRandomFeeRate(); + const fillOrders = createQuoteFillOrders({ + fillableInput, + fillableOutput, + outputFeeRate, + side, + fillsCount, + count: 1, + }); + const signedOutputFeeRate = side === MarketOperation.Sell ? -outputFeeRate : outputFeeRate; + const totalFillableOutput = fillableOutput.times(signedOutputFeeRate + 1).integerValue(); + const result = fillQuoteOrders(side, fillOrders, fillableInput, ONE, GAS_SCHEDULE); + const totalFilledInput = result.input.plus(result.inputFee); + const totalFilledOutput = result.output.plus(result.outputFee); + assertIntegerRoughlyEquals(totalFilledInput, fillableInput, EPS); + assertIntegerRoughlyEquals(totalFilledOutput, totalFillableOutput, EPS); + assertEqualRates(result.outputFee.div(result.output), signedOutputFeeRate); + expect(result.protocolFee).to.bignumber.eq(1); + expect(result.gas).to.eq(fillsCount); + }); + + it('can partial fill one order with output fees', () => { + const side = randomSide(); + const fillsCount = _.random(1, 3); + const fillableInput = getRandomOrderSize(); + const fillableOutput = getRandomOrderSize(); + const outputFeeRate = getRandomFeeRate(); + const fillOrders = createQuoteFillOrders({ + fillableInput, + fillableOutput, + outputFeeRate, + side, + fillsCount, + count: 1, + }); + const signedOutputFeeRate = side === MarketOperation.Sell ? -outputFeeRate : outputFeeRate; + const totalFillableOutput = fillableOutput.times(signedOutputFeeRate + 1).integerValue(); + const inputFillAmount = fillableInput.times(2 / 3).integerValue(); + const result = fillQuoteOrders(side, fillOrders, inputFillAmount, ONE, GAS_SCHEDULE); + const totalFilledInput = result.input.plus(result.inputFee); + const totalFilledOutput = result.output.plus(result.outputFee); + assertIntegerRoughlyEquals(totalFilledInput, inputFillAmount, EPS); + expect(totalFilledOutput).to.bignumber.lt(totalFillableOutput); + assertEqualRates(result.outputFee.div(result.output), signedOutputFeeRate); + expect(result.protocolFee).to.bignumber.eq(1); + expect(result.gas).to.lte(fillsCount); + }); + + it('does not over fill one order with output fees', () => { + const side = randomSide(); + const fillsCount = _.random(1, 3); + const fillableInput = getRandomOrderSize(); + const fillableOutput = getRandomOrderSize(); + const outputFeeRate = getRandomFeeRate(); + const fillOrders = createQuoteFillOrders({ + fillableInput, + fillableOutput, + outputFeeRate, + side, + fillsCount, + count: 1, + }); + const signedOutputFeeRate = side === MarketOperation.Sell ? -outputFeeRate : outputFeeRate; + const totalFillableOutput = fillableOutput.times(signedOutputFeeRate + 1).integerValue(); + const inputFillAmount = fillableInput.times(3 / 2).integerValue(); + const result = fillQuoteOrders(side, fillOrders, inputFillAmount, ONE, GAS_SCHEDULE); + const totalFilledInput = result.input.plus(result.inputFee); + const totalFilledOutput = result.output.plus(result.outputFee); + assertIntegerRoughlyEquals(totalFilledInput, fillableInput, EPS); + assertIntegerRoughlyEquals(totalFilledOutput, totalFillableOutput, EPS); + assertEqualRates(result.outputFee.div(result.output), signedOutputFeeRate); + expect(result.protocolFee).to.bignumber.eq(1); + expect(result.gas).to.eq(fillsCount); + }); + }); + + describe('multiple orders', () => { + it('can exactly fill orders', () => { + const side = randomSide(); + const fillableInput = getRandomOrderSize(); + const fillableOutput = getRandomOrderSize(); + const fillOrders = createQuoteFillOrders({ fillableInput, fillableOutput, side }); + const result = fillQuoteOrders(side, fillOrders, fillableInput, ONE, GAS_SCHEDULE); + const totalFilledInput = result.input.plus(result.inputFee); + const totalFilledOutput = result.output.plus(result.outputFee); + expect(totalFilledInput).to.bignumber.eq(fillableInput); + expect(totalFilledOutput).to.bignumber.eq(fillableOutput); + expect(result.protocolFee).to.bignumber.eq(fillOrders.length); + expect(result.gas).to.eq(countCollapsedFills(fillOrders)); + }); + + it('can partial fill orders', () => { + const side = randomSide(); + const fillableInput = getRandomOrderSize(); + const fillableOutput = getRandomOrderSize(); + const inputFillAmount = fillableInput.times(2 / 3).integerValue(); + const fillOrders = createQuoteFillOrders({ fillableInput, fillableOutput, side }); + const result = fillQuoteOrders(side, fillOrders, inputFillAmount, ONE, GAS_SCHEDULE); + const totalFilledInput = result.input.plus(result.inputFee); + const totalFilledOutput = result.output.plus(result.outputFee); + expect(totalFilledInput).to.bignumber.eq(inputFillAmount); + expect(totalFilledOutput).to.bignumber.lt(fillableOutput); + expect(result.protocolFee).to.bignumber.gte(1); + }); + + it('does not over fill orders', () => { + const side = randomSide(); + const fillableInput = getRandomOrderSize(); + const fillableOutput = getRandomOrderSize(); + const inputFillAmount = fillableInput.times(3 / 2).integerValue(); + const fillOrders = createQuoteFillOrders({ fillableInput, fillableOutput, side }); + const result = fillQuoteOrders(side, fillOrders, inputFillAmount, ONE, GAS_SCHEDULE); + const totalFilledInput = result.input.plus(result.inputFee); + const totalFilledOutput = result.output.plus(result.outputFee); + expect(totalFilledInput).to.bignumber.eq(fillableInput); + expect(totalFilledOutput).to.bignumber.eq(fillableOutput); + expect(result.protocolFee).to.bignumber.eq(fillOrders.length); + expect(result.gas).to.eq(countCollapsedFills(fillOrders)); + }); + + it('can exactly fill orders with input fees', () => { + const side = randomSide(); + const fillableInput = getRandomOrderSize(); + const fillableOutput = getRandomOrderSize(); + const inputFeeRate = getRandomFeeRate(); + const fillOrders = createQuoteFillOrders({ + fillableInput, + fillableOutput, + inputFeeRate, + side, + }); + const signedInputFeeRate = side === MarketOperation.Sell ? inputFeeRate : -inputFeeRate; + const totalFillableInput = fillableInput.times(signedInputFeeRate + 1).integerValue(); + const result = fillQuoteOrders(side, fillOrders, totalFillableInput, ONE, GAS_SCHEDULE); + const totalFilledInput = result.input.plus(result.inputFee); + const totalFilledOutput = result.output.plus(result.outputFee); + assertIntegerRoughlyEquals(totalFilledInput, totalFillableInput, EPS); + assertIntegerRoughlyEquals(totalFilledOutput, fillableOutput, EPS); + assertEqualRates(result.inputFee.div(result.input), signedInputFeeRate); + expect(result.protocolFee).to.bignumber.eq(fillOrders.length); + expect(result.gas).to.eq(countCollapsedFills(fillOrders)); + }); + + it('can partial fill orders with input fees', () => { + const side = randomSide(); + const fillableInput = getRandomOrderSize(); + const fillableOutput = getRandomOrderSize(); + const inputFeeRate = getRandomFeeRate(); + const fillOrders = createQuoteFillOrders({ + fillableInput, + fillableOutput, + inputFeeRate, + side, + }); + const signedInputFeeRate = side === MarketOperation.Sell ? inputFeeRate : -inputFeeRate; + const totalFillableInput = fillableInput.times(signedInputFeeRate + 1).integerValue(); + const inputFillAmount = totalFillableInput.times(2 / 3).integerValue(); + const result = fillQuoteOrders(side, fillOrders, inputFillAmount, ONE, GAS_SCHEDULE); + const totalFilledInput = result.input.plus(result.inputFee); + const totalFilledOutput = result.output.plus(result.outputFee); + assertIntegerRoughlyEquals(totalFilledInput, inputFillAmount, EPS); + expect(totalFilledOutput).to.bignumber.lt(fillableOutput); + assertEqualRates(result.inputFee.div(result.input), signedInputFeeRate); + expect(result.protocolFee).to.bignumber.lte(fillOrders.length); + expect(result.gas).to.lte(countCollapsedFills(fillOrders)); + }); + + it('does not over fill orders with input fees', () => { + const side = randomSide(); + const fillableInput = getRandomOrderSize(); + const fillableOutput = getRandomOrderSize(); + const inputFeeRate = getRandomFeeRate(); + const fillOrders = createQuoteFillOrders({ + fillableInput, + fillableOutput, + inputFeeRate, + side, + }); + const signedInputFeeRate = side === MarketOperation.Sell ? inputFeeRate : -inputFeeRate; + const totalFillableInput = fillableInput.times(signedInputFeeRate + 1).integerValue(); + const inputFillAmount = totalFillableInput.times(3 / 2).integerValue(); + const result = fillQuoteOrders(side, fillOrders, inputFillAmount, ONE, GAS_SCHEDULE); + const totalFilledInput = result.input.plus(result.inputFee); + const totalFilledOutput = result.output.plus(result.outputFee); + assertIntegerRoughlyEquals(totalFilledInput, totalFillableInput, EPS); + assertIntegerRoughlyEquals(totalFilledOutput, fillableOutput, EPS); + assertEqualRates(result.inputFee.div(result.input), signedInputFeeRate); + expect(result.protocolFee).to.bignumber.eq(fillOrders.length); + expect(result.gas).to.eq(countCollapsedFills(fillOrders)); + }); + + it('can exactly fill orders with output fees', () => { + const side = randomSide(); + const fillableInput = getRandomOrderSize(); + const fillableOutput = getRandomOrderSize(); + const outputFeeRate = getRandomFeeRate(); + const fillOrders = createQuoteFillOrders({ + fillableInput, + fillableOutput, + outputFeeRate, + side, + }); + const signedOutputFeeRate = side === MarketOperation.Sell ? -outputFeeRate : outputFeeRate; + const totalFillableOutput = fillableOutput.times(signedOutputFeeRate + 1).integerValue(); + const result = fillQuoteOrders(side, fillOrders, fillableInput, ONE, GAS_SCHEDULE); + const totalFilledInput = result.input.plus(result.inputFee); + const totalFilledOutput = result.output.plus(result.outputFee); + assertIntegerRoughlyEquals(totalFilledInput, fillableInput, EPS); + assertIntegerRoughlyEquals(totalFilledOutput, totalFillableOutput, EPS); + assertEqualRates(result.outputFee.div(result.output), signedOutputFeeRate); + expect(result.protocolFee).to.bignumber.eq(fillOrders.length); + expect(result.gas).to.eq(countCollapsedFills(fillOrders)); + }); + + it('can partial fill orders with output fees', () => { + const side = randomSide(); + const fillableInput = getRandomOrderSize(); + const fillableOutput = getRandomOrderSize(); + const outputFeeRate = getRandomFeeRate(); + const fillOrders = createQuoteFillOrders({ + fillableInput, + fillableOutput, + outputFeeRate, + side, + }); + const signedOutputFeeRate = side === MarketOperation.Sell ? -outputFeeRate : outputFeeRate; + const totalFillableOutput = fillableOutput.times(signedOutputFeeRate + 1).integerValue(); + const inputFillAmount = fillableInput.times(2 / 3).integerValue(); + const result = fillQuoteOrders(side, fillOrders, inputFillAmount, ONE, GAS_SCHEDULE); + const totalFilledInput = result.input.plus(result.inputFee); + const totalFilledOutput = result.output.plus(result.outputFee); + assertIntegerRoughlyEquals(totalFilledInput, inputFillAmount, EPS); + expect(totalFilledOutput).to.bignumber.lt(totalFillableOutput); + assertEqualRates(result.outputFee.div(result.output), signedOutputFeeRate); + expect(result.protocolFee).to.bignumber.lte(fillOrders.length); + expect(result.gas).to.lte(countCollapsedFills(fillOrders)); + }); + + it('does not over fill orders with output fees', () => { + const side = randomSide(); + const fillableInput = getRandomOrderSize(); + const fillableOutput = getRandomOrderSize(); + const outputFeeRate = getRandomFeeRate(); + const fillOrders = createQuoteFillOrders({ + fillableInput, + fillableOutput, + outputFeeRate, + side, + }); + const signedOutputFeeRate = side === MarketOperation.Sell ? -outputFeeRate : outputFeeRate; + const totalFillableOutput = fillableOutput.times(signedOutputFeeRate + 1).integerValue(); + const inputFillAmount = fillableInput.times(3 / 2).integerValue(); + const result = fillQuoteOrders(side, fillOrders, inputFillAmount, ONE, GAS_SCHEDULE); + const totalFilledInput = result.input.plus(result.inputFee); + const totalFilledOutput = result.output.plus(result.outputFee); + assertIntegerRoughlyEquals(totalFilledInput, fillableInput, EPS); + assertIntegerRoughlyEquals(totalFilledOutput, totalFillableOutput, EPS); + assertEqualRates(result.outputFee.div(result.output), signedOutputFeeRate); + expect(result.protocolFee).to.bignumber.eq(fillOrders.length); + expect(result.gas).to.eq(countCollapsedFills(fillOrders)); + }); + }); + }); + + function slipOrder( + order: OptimizedMarketOrder, + orderSlippage: number, + side: MarketOperation, + ): OptimizedMarketOrder { + const makerScaling = side === MarketOperation.Sell ? 1 - orderSlippage : 1; + const takerScaling = side === MarketOperation.Sell ? 1 : orderSlippage + 1; + return { + ...order, + makerAssetAmount: order.makerAssetAmount.times(makerScaling), + fillableMakerAssetAmount: order.fillableMakerAssetAmount.times(makerScaling), + takerAssetAmount: order.takerAssetAmount.times(takerScaling), + fillableTakerAssetAmount: order.fillableTakerAssetAmount.times(takerScaling), + }; + } + + describe('simulateBestCaseFill()', () => { + it('ignores order slippage', async () => { + const side = randomSide(); + const fillableInput = getRandomOrderSize(); + const fillableOutput = getRandomOrderSize(); + const orderSlippage = getRandomFeeRate(); + const orders = createQuoteFillOrders({ + fillableInput, + fillableOutput, + side, + }).map(fo => slipOrder(fo.order, orderSlippage, side)); + const result = simulateBestCaseFill({ + orders, + side, + fillAmount: fillableInput, + gasPrice: ONE, + opts: { gasSchedule: GAS_SCHEDULE }, + }); + if (side === MarketOperation.Sell) { + expect(result.totalMakerAssetAmount).to.be.bignumber.eq(fillableOutput); + expect(result.totalTakerAssetAmount).to.be.bignumber.eq(fillableInput); + } else { + expect(result.totalMakerAssetAmount).to.be.bignumber.eq(fillableInput); + expect(result.totalTakerAssetAmount).to.be.bignumber.eq(fillableOutput); + } + }); + + it('can fully fill orders', async () => { + const side = randomSide(); + const fillableInput = getRandomOrderSize(); + const fillableOutput = getRandomOrderSize(); + const orders = createQuoteFillOrders({ + fillableInput, + fillableOutput, + side, + }).map(fo => fo.order); + const result = simulateBestCaseFill({ + orders, + side, + fillAmount: fillableInput, + gasPrice: ONE, + opts: { gasSchedule: GAS_SCHEDULE }, + }); + expect(result.gas).to.eq(countCollapsedFills(orders)); + expect(result.protocolFeeAmount).to.bignumber.gt(orders.length); + expect(result.takerFeeTakerAssetAmount).to.bignumber.eq(0); + expect(result.takerFeeMakerAssetAmount).to.bignumber.eq(0); + expect(result.makerAssetAmount).to.bignumber.eq(result.totalMakerAssetAmount); + expect(result.takerAssetAmount).to.bignumber.eq(result.totalTakerAssetAmount); + if (side === MarketOperation.Sell) { + expect(result.totalMakerAssetAmount).to.be.bignumber.eq(fillableOutput); + expect(result.totalTakerAssetAmount).to.be.bignumber.eq(fillableInput); + } else { + expect(result.totalMakerAssetAmount).to.be.bignumber.eq(fillableInput); + expect(result.totalTakerAssetAmount).to.be.bignumber.eq(fillableOutput); + } + }); + + it('can partial fill orders', async () => { + const side = randomSide(); + const fillableInput = getRandomOrderSize(); + const fillableOutput = getRandomOrderSize(); + const orders = createQuoteFillOrders({ + fillableInput, + fillableOutput, + side, + }).map(fo => fo.order); + const inputFillAmount = fillableInput.times(Math.random()).integerValue(); + const result = simulateBestCaseFill({ + orders, + side, + fillAmount: inputFillAmount, + gasPrice: ONE, + opts: { gasSchedule: GAS_SCHEDULE }, + }); + expect(result.gas).to.gt(0); + expect(result.protocolFeeAmount).to.bignumber.gt(0); + expect(result.takerFeeTakerAssetAmount).to.bignumber.eq(0); + expect(result.takerFeeMakerAssetAmount).to.bignumber.eq(0); + expect(result.makerAssetAmount).to.bignumber.eq(result.totalMakerAssetAmount); + expect(result.takerAssetAmount).to.bignumber.eq(result.totalTakerAssetAmount); + if (side === MarketOperation.Sell) { + expect(result.totalMakerAssetAmount).to.be.bignumber.lt(fillableOutput); + expect(result.totalTakerAssetAmount).to.be.bignumber.eq(inputFillAmount); + } else { + expect(result.totalMakerAssetAmount).to.be.bignumber.eq(inputFillAmount); + expect(result.totalTakerAssetAmount).to.be.bignumber.lt(fillableOutput); + } + }); + + it('can fully fill orders with input fees', async () => { + const side = randomSide(); + const fillableInput = getRandomOrderSize(); + const fillableOutput = getRandomOrderSize(); + const inputFeeRate = getRandomFeeRate(); + const orders = createQuoteFillOrders({ + fillableInput, + fillableOutput, + inputFeeRate, + side, + }).map(fo => fo.order); + const signedInputFeeRate = side === MarketOperation.Sell ? inputFeeRate : -inputFeeRate; + const totalFillableInput = fillableInput.times(signedInputFeeRate + 1).integerValue(); + const result = simulateBestCaseFill({ + orders, + side, + fillAmount: totalFillableInput, + gasPrice: ONE, + opts: { gasSchedule: GAS_SCHEDULE }, + }); + expect(result.gas).to.eq(countCollapsedFills(orders)); + expect(result.protocolFeeAmount).to.bignumber.gt(orders.length); + if (side === MarketOperation.Sell) { + assertIntegerRoughlyEquals(result.takerAssetAmount, fillableInput, EPS); + assertIntegerRoughlyEquals(result.totalTakerAssetAmount, totalFillableInput, EPS); + assertIntegerRoughlyEquals(result.makerAssetAmount, fillableOutput, EPS); + assertIntegerRoughlyEquals(result.totalMakerAssetAmount, fillableOutput, EPS); + expect(result.makerAssetAmount).to.bignumber.eq(result.totalMakerAssetAmount); + expect(result.takerFeeMakerAssetAmount).to.bignumber.eq(0); + } else { + assertIntegerRoughlyEquals(result.makerAssetAmount, fillableInput, EPS); + assertIntegerRoughlyEquals(result.totalMakerAssetAmount, totalFillableInput, EPS); + assertIntegerRoughlyEquals(result.takerAssetAmount, fillableOutput, EPS); + assertIntegerRoughlyEquals(result.totalTakerAssetAmount, fillableOutput, EPS); + expect(result.takerAssetAmount).to.bignumber.eq(result.totalTakerAssetAmount); + expect(result.takerFeeTakerAssetAmount).to.bignumber.eq(0); + } + }); + + it('can partially fill orders with input fees', async () => { + const side = randomSide(); + const fillableInput = getRandomOrderSize(); + const fillableOutput = getRandomOrderSize(); + const inputFeeRate = getRandomFeeRate(); + const orders = createQuoteFillOrders({ + fillableInput, + fillableOutput, + inputFeeRate, + side, + }).map(fo => fo.order); + const signedInputFeeRate = side === MarketOperation.Sell ? inputFeeRate : -inputFeeRate; + const totalFillableInput = fillableInput.times(signedInputFeeRate + 1).integerValue(); + const inputFillAmount = totalFillableInput.times(2 / 3).integerValue(); + const result = simulateBestCaseFill({ + orders, + side, + fillAmount: inputFillAmount, + gasPrice: ONE, + opts: { gasSchedule: GAS_SCHEDULE }, + }); + expect(result.gas).to.gt(0); + expect(result.protocolFeeAmount).to.bignumber.gt(0); + if (side === MarketOperation.Sell) { + assertIntegerRoughlyEquals(result.totalTakerAssetAmount, inputFillAmount, EPS); + expect(result.makerAssetAmount).to.bignumber.lt(fillableOutput); + expect(result.makerAssetAmount).to.bignumber.eq(result.totalMakerAssetAmount); + expect(result.takerFeeMakerAssetAmount).to.bignumber.eq(0); + } else { + assertIntegerRoughlyEquals(result.totalMakerAssetAmount, inputFillAmount, EPS); + expect(result.takerAssetAmount).to.bignumber.lt(fillableOutput); + expect(result.takerAssetAmount).to.bignumber.eq(result.totalTakerAssetAmount); + expect(result.takerFeeTakerAssetAmount).to.bignumber.eq(0); + } + }); + + it('can fully fill orders with output fees', async () => { + const side = randomSide(); + const fillableInput = getRandomOrderSize(); + const fillableOutput = getRandomOrderSize(); + const outputFeeRate = getRandomFeeRate(); + const orders = createQuoteFillOrders({ + fillableInput, + fillableOutput, + outputFeeRate, + side, + }).map(fo => fo.order); + const signedOutputFeeRate = side === MarketOperation.Sell ? -outputFeeRate : outputFeeRate; + const totalFillableOutput = fillableOutput.times(signedOutputFeeRate + 1).integerValue(); + const result = simulateBestCaseFill({ + orders, + side, + fillAmount: fillableInput, + gasPrice: ONE, + opts: { gasSchedule: GAS_SCHEDULE }, + }); + expect(result.gas).to.eq(countCollapsedFills(orders)); + expect(result.protocolFeeAmount).to.bignumber.gt(orders.length); + if (side === MarketOperation.Sell) { + assertIntegerRoughlyEquals(result.takerAssetAmount, fillableInput, EPS); + assertIntegerRoughlyEquals(result.totalTakerAssetAmount, fillableInput, EPS); + assertIntegerRoughlyEquals(result.makerAssetAmount, fillableOutput, EPS); + assertIntegerRoughlyEquals(result.totalMakerAssetAmount, totalFillableOutput, EPS); + expect(result.takerAssetAmount).to.bignumber.eq(result.totalTakerAssetAmount); + expect(result.takerFeeTakerAssetAmount).to.bignumber.eq(0); + } else { + assertIntegerRoughlyEquals(result.makerAssetAmount, fillableInput, EPS); + assertIntegerRoughlyEquals(result.totalMakerAssetAmount, fillableInput, EPS); + assertIntegerRoughlyEquals(result.takerAssetAmount, fillableOutput, EPS); + assertIntegerRoughlyEquals(result.totalTakerAssetAmount, totalFillableOutput, EPS); + expect(result.makerAssetAmount).to.bignumber.eq(result.totalMakerAssetAmount); + expect(result.takerFeeMakerAssetAmount).to.bignumber.eq(0); + } + }); + + it('can partially fill orders with output fees', async () => { + const side = randomSide(); + const fillableInput = getRandomOrderSize(); + const fillableOutput = getRandomOrderSize(); + const outputFeeRate = getRandomFeeRate(); + const orders = createQuoteFillOrders({ + fillableInput, + fillableOutput, + outputFeeRate, + side, + }).map(fo => fo.order); + const inputFillAmount = fillableInput.times(2 / 3).integerValue(); + const result = simulateBestCaseFill({ + orders, + side, + fillAmount: inputFillAmount, + gasPrice: ONE, + opts: { gasSchedule: GAS_SCHEDULE }, + }); + expect(result.gas).to.gt(0); + expect(result.protocolFeeAmount).to.bignumber.gt(0); + if (side === MarketOperation.Sell) { + assertIntegerRoughlyEquals(result.totalTakerAssetAmount, inputFillAmount, EPS); + expect(result.makerAssetAmount).to.bignumber.lt(fillableOutput); + expect(result.takerAssetAmount).to.bignumber.eq(result.totalTakerAssetAmount); + expect(result.takerFeeTakerAssetAmount).to.bignumber.eq(0); + } else { + assertIntegerRoughlyEquals(result.totalMakerAssetAmount, inputFillAmount, EPS); + expect(result.takerAssetAmount).to.bignumber.lt(fillableOutput); + expect(result.makerAssetAmount).to.bignumber.eq(result.totalMakerAssetAmount); + expect(result.takerFeeMakerAssetAmount).to.bignumber.eq(0); + } + }); + }); + + describe('simulateWorstCaseFill()', () => { + it('includes order slippage', async () => { + const side = randomSide(); + const fillableInput = getRandomOrderSize(); + const fillableOutput = getRandomOrderSize(); + const orderSlippage = getRandomFeeRate(); + const orders = createQuoteFillOrders({ + fillableInput, + fillableOutput, + side, + }).map(fo => slipOrder(fo.order, orderSlippage, side)); + const result = simulateWorstCaseFill({ + orders, + side, + fillAmount: fillableInput, + gasPrice: ONE, + opts: { gasSchedule: GAS_SCHEDULE }, + }); + if (side === MarketOperation.Sell) { + const slippedOutput = fillableOutput.times(1 - orderSlippage).integerValue(); + assertIntegerRoughlyEquals(result.totalMakerAssetAmount, slippedOutput); + assertIntegerRoughlyEquals(result.totalTakerAssetAmount, fillableInput); + } else { + const slippedOutput = fillableOutput.times(orderSlippage + 1).integerValue(); + assertIntegerRoughlyEquals(result.totalMakerAssetAmount, fillableInput); + assertIntegerRoughlyEquals(result.totalTakerAssetAmount, slippedOutput); + } + }); + }); +}); // tslint:disable: max-file-line-count diff --git a/packages/asset-swapper/test/swap_quote_calculator_test.ts b/packages/asset-swapper/test/swap_quote_calculator_test.ts deleted file mode 100644 index 4c77a8b38b..0000000000 --- a/packages/asset-swapper/test/swap_quote_calculator_test.ts +++ /dev/null @@ -1,908 +0,0 @@ -// tslint:disable:max-file-line-count -// TODO(dorothy-zbornak): Skipping these tests for now because they're a -// nightmare to maintain. We should replace them with simpler unit tests. -/* -import { constants as devConstants } from '@0x/contracts-test-utils'; -import { BlockchainLifecycle } from '@0x/dev-utils'; -import { ContractAddresses, migrateOnceAsync } from '@0x/migrations'; -import { BigNumber } from '@0x/utils'; -import * as chai from 'chai'; -import * as _ from 'lodash'; -import 'mocha'; - -import { constants } from '../src/constants'; -import { CalculateSwapQuoteOpts, SignedOrderWithFillableAmounts } from '../src/types'; -import { MarketOperationUtils } from '../src/utils/market_operation_utils/'; -import { DEFAULT_GET_MARKET_ORDERS_OPTS, SELL_SOURCES } from '../src/utils/market_operation_utils/constants'; -import { DexOrderSampler } from '../src/utils/market_operation_utils/sampler'; -import { ProtocolFeeUtils } from '../src/utils/protocol_fee_utils'; -import { SwapQuoteCalculator } from '../src/utils/swap_quote_calculator'; - -import { chaiSetup } from './utils/chai_setup'; -import { MockSamplerContract } from './utils/mock_sampler_contract'; -import { protocolFeeUtilsMock } from './utils/mocks'; -import { testOrders } from './utils/test_orders'; -import { baseUnitAmount } from './utils/utils'; -import { provider, web3Wrapper } from './utils/web3_wrapper'; - -chaiSetup.configure(); -const expect = chai.expect; -const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); - -const GAS_PRICE = new BigNumber(devConstants.DEFAULT_GAS_PRICE); -const ONE_ETH_IN_WEI = new BigNumber(1000000000000000000); -// const MIXED_TEST_ORDERS = _.concat( -// testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS, -// testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET, -// testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET, -// ); -const TESTRPC_CHAIN_ID = devConstants.TESTRPC_CHAIN_ID; - -// Excludes all non native sources -const CALCULATE_SWAP_QUOTE_OPTS: CalculateSwapQuoteOpts = { - ...DEFAULT_GET_MARKET_ORDERS_OPTS, - ...{ - excludedSources: SELL_SOURCES, - }, -}; - -function createSamplerFromSignedOrdersWithFillableAmounts( - signedOrders: SignedOrderWithFillableAmounts[], -): DexOrderSampler { - const sampleDexHandler = (takerToken: string, makerToken: string, amounts: BigNumber[]) => { - return amounts.map(() => constants.ZERO_AMOUNT); - }; - return new DexOrderSampler( - new MockSamplerContract({ - getOrderFillableMakerAssetAmounts: (orders, signatures) => - orders.map((o, i) => signedOrders[i].fillableMakerAssetAmount), - getOrderFillableTakerAssetAmounts: (orders, signatures) => - orders.map((o, i) => signedOrders[i].fillableTakerAssetAmount), - sampleSellsFromEth2Dai: sampleDexHandler, - sampleSellsFromKyberNetwork: sampleDexHandler, - sampleSellsFromUniswap: sampleDexHandler, - sampleBuysFromEth2Dai: sampleDexHandler, - sampleBuysFromUniswap: sampleDexHandler, - }), - ); -} - -// tslint:disable:custom-no-magic-numbers -describe.skip('swapQuoteCalculator', () => { - let protocolFeeUtils: ProtocolFeeUtils; - let contractAddresses: ContractAddresses; - - before(async () => { - contractAddresses = await migrateOnceAsync(provider); - protocolFeeUtils = protocolFeeUtilsMock().object; - await blockchainLifecycle.startAsync(); - }); - after(async () => { - await blockchainLifecycle.revertAsync(); - }); - beforeEach(async () => { - await blockchainLifecycle.startAsync(); - }); - afterEach(async () => { - await blockchainLifecycle.revertAsync(); - }); - - describe('#calculateMarketSellSwapQuote', () => { - // TODO(dave4506) InsufficientLiquidityError is not thrown anymore, consider how to test for insufficient liquidity - // describe('InsufficientLiquidityError', () => { - // it('should throw if not enough taker asset liquidity (multiple feeless orders)', async () => { - // const sampler = createSamplerFromSignedOrdersWithFillableAmounts(testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS); - // const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, { - // exchangeAddress: contractAddresses.exchange, - // chainId: TESTRPC_CHAIN_ID, - // }); - // const swapQuoteCalculator = new SwapQuoteCalculator( protocolFeeUtils, marketOperationUtils); - // const errorFunction = async () => { - // await swapQuoteCalculator.calculateMarketSellSwapQuoteAsync( - // testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS, - // baseUnitAmount(10), - // 0, - // GAS_PRICE, - // CALCULATE_SWAP_QUOTE_OPTS, - // ); - // }; - // await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(9)); - // }); - // it('should throw if not enough taker asset liquidity (multiple feeless orders with 20% slippage)', async () => { - // const errorFunction = async () => { - // const sampler = createSamplerFromSignedOrdersWithFillableAmounts(testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS); - // const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, { - // exchangeAddress: contractAddresses.exchange, - // chainId: TESTRPC_CHAIN_ID, - // }); - // const swapQuoteCalculator = new SwapQuoteCalculator( protocolFeeUtils, marketOperationUtils); - // await swapQuoteCalculator.calculateMarketSellSwapQuoteAsync( - // testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS, - // baseUnitAmount(10), - // 0.2, - // GAS_PRICE, - // CALCULATE_SWAP_QUOTE_OPTS, - // ); - // }; - // await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(7.5)); - // }); - // it('should throw if not enough taker asset liquidity (multiple takerAsset denominated fee orders with no slippage)', async () => { - // const sampler = createSamplerFromSignedOrdersWithFillableAmounts(testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET); - // const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, { - // exchangeAddress: contractAddresses.exchange, - // chainId: TESTRPC_CHAIN_ID, - // }); - // const swapQuoteCalculator = new SwapQuoteCalculator( protocolFeeUtils, marketOperationUtils); - // const errorFunction = async () => { - // await swapQuoteCalculator.calculateMarketSellSwapQuoteAsync( - // testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET, - // baseUnitAmount(20), - // 0, - // GAS_PRICE, - // CALCULATE_SWAP_QUOTE_OPTS, - // ); - // }; - // await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(15)); - // }); - // it('should throw if not enough taker asset liquidity (multiple takerAsset denominated fee orders with 20% slippage)', async () => { - // const sampler = createSamplerFromSignedOrdersWithFillableAmounts(testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET); - // const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, { - // exchangeAddress: contractAddresses.exchange, - // chainId: TESTRPC_CHAIN_ID, - // }); - // const swapQuoteCalculator = new SwapQuoteCalculator( protocolFeeUtils, marketOperationUtils); - // const errorFunction = async () => { - // await swapQuoteCalculator.calculateMarketSellSwapQuoteAsync( - // testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET, - // baseUnitAmount(20), - // 0.2, - // GAS_PRICE, - // CALCULATE_SWAP_QUOTE_OPTS, - // ); - // }; - // await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(12.5)); - // }); - // it('should throw if not enough taker asset liquidity (multiple makerAsset denominated fee orders with no slippage)', async () => { - // const sampler = createSamplerFromSignedOrdersWithFillableAmounts(testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET); - // const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, { - // exchangeAddress: contractAddresses.exchange, - // chainId: TESTRPC_CHAIN_ID, - // }); - // const swapQuoteCalculator = new SwapQuoteCalculator( protocolFeeUtils, marketOperationUtils); - // const errorFunction = async () => { - // await swapQuoteCalculator.calculateMarketSellSwapQuoteAsync( - // testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET, - // baseUnitAmount(10), - // 0, - // GAS_PRICE, - // CALCULATE_SWAP_QUOTE_OPTS, - // ); - // }; - // await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(9)); - // }); - // it('should throw if not enough taker asset liquidity (multiple makerAsset denominated fee orders with 20% slippage)', async () => { - // const sampler = createSamplerFromSignedOrdersWithFillableAmounts(testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET); - // const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, { - // exchangeAddress: contractAddresses.exchange, - // chainId: TESTRPC_CHAIN_ID, - // }); - // const swapQuoteCalculator = new SwapQuoteCalculator( protocolFeeUtils, marketOperationUtils); - // const errorFunction = async () => { - // await swapQuoteCalculator.calculateMarketSellSwapQuoteAsync( - // testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET, - // baseUnitAmount(10), - // 0.2, - // GAS_PRICE, - // CALCULATE_SWAP_QUOTE_OPTS, - // ); - // }; - // await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(7.5)); - // }); - // it('should throw if not enough taker asset liquidity (multiple mixed feeType orders with no slippage)', async () => { - // const sampler = createSamplerFromSignedOrdersWithFillableAmounts(MIXED_TEST_ORDERS); - // const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, { - // exchangeAddress: contractAddresses.exchange, - // chainId: TESTRPC_CHAIN_ID, - // }); - // const swapQuoteCalculator = new SwapQuoteCalculator( protocolFeeUtils, marketOperationUtils); - // const errorFunction = async () => { - // await swapQuoteCalculator.calculateMarketSellSwapQuoteAsync( - // MIXED_TEST_ORDERS, - // baseUnitAmount(40), - // 0, - // GAS_PRICE, - // CALCULATE_SWAP_QUOTE_OPTS, - // ); - // }; - // await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(33)); - // }); - // it('should throw if not enough taker asset liquidity (multiple mixed feeTyoe orders with 20% slippage)', async () => { - // const sampler = createSamplerFromSignedOrdersWithFillableAmounts(MIXED_TEST_ORDERS); - // const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, { - // exchangeAddress: contractAddresses.exchange, - // chainId: TESTRPC_CHAIN_ID, - // }); - // const swapQuoteCalculator = new SwapQuoteCalculator( protocolFeeUtils, marketOperationUtils); - // const errorFunction = async () => { - // await swapQuoteCalculator.calculateMarketSellSwapQuoteAsync( - // MIXED_TEST_ORDERS, - // baseUnitAmount(40), - // 0.2, - // GAS_PRICE, - // CALCULATE_SWAP_QUOTE_OPTS, - // ); - // }; - // await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(27.5)); - // }); - // }); - it('calculates a correct swapQuote with no slippage (feeless orders)', async () => { - const assetSellAmount = baseUnitAmount(0.5); - const slippagePercentage = 0; - const sampler = createSamplerFromSignedOrdersWithFillableAmounts( - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS, - ); - const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, { - exchangeAddress: contractAddresses.exchange, - chainId: TESTRPC_CHAIN_ID, - }); - const swapQuoteCalculator = new SwapQuoteCalculator(protocolFeeUtils, marketOperationUtils); - const swapQuote = await swapQuoteCalculator.calculateMarketSellSwapQuoteAsync( - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS, - assetSellAmount, - slippagePercentage, - GAS_PRICE, - CALCULATE_SWAP_QUOTE_OPTS, - ); - // test if orders are correct - expect(swapQuote.orders).to.deep.equal([testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS[0]]); - expect(swapQuote.takerAssetFillAmount).to.bignumber.equal(assetSellAmount); - // test if rates are correct - expect(swapQuote.bestCaseQuoteInfo).to.deep.equal({ - feeTakerAssetAmount: baseUnitAmount(0), - takerAssetAmount: assetSellAmount, - totalTakerAssetAmount: assetSellAmount, - makerAssetAmount: baseUnitAmount(3), - protocolFeeInWeiAmount: baseUnitAmount(15, 4), - }); - expect(swapQuote.worstCaseQuoteInfo).to.deep.equal({ - feeTakerAssetAmount: baseUnitAmount(0), - takerAssetAmount: assetSellAmount, - totalTakerAssetAmount: assetSellAmount, - makerAssetAmount: baseUnitAmount(3), - protocolFeeInWeiAmount: baseUnitAmount(15, 4), - }); - }); - it('calculates a correct swapQuote with slippage (feeless orders)', async () => { - const assetSellAmount = baseUnitAmount(4); - const slippagePercentage = 0.2; - const sampler = createSamplerFromSignedOrdersWithFillableAmounts( - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS, - ); - const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, { - exchangeAddress: contractAddresses.exchange, - chainId: TESTRPC_CHAIN_ID, - }); - const swapQuoteCalculator = new SwapQuoteCalculator(protocolFeeUtils, marketOperationUtils); - const swapQuote = await swapQuoteCalculator.calculateMarketSellSwapQuoteAsync( - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS, - assetSellAmount, - slippagePercentage, - GAS_PRICE, - CALCULATE_SWAP_QUOTE_OPTS, - ); - // test if orders are correct - expect(swapQuote.orders).to.deep.equal([ - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS[0], - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS[1], - ]); - expect(swapQuote.takerAssetFillAmount).to.bignumber.equal(assetSellAmount); - // test if rates are correct - expect(swapQuote.bestCaseQuoteInfo).to.deep.equal({ - feeTakerAssetAmount: baseUnitAmount(0), - takerAssetAmount: assetSellAmount, - totalTakerAssetAmount: assetSellAmount, - makerAssetAmount: baseUnitAmount(9), - protocolFeeInWeiAmount: baseUnitAmount(30, 4), - }); - expect(swapQuote.worstCaseQuoteInfo).to.deep.equal({ - feeTakerAssetAmount: baseUnitAmount(0), - takerAssetAmount: assetSellAmount, - totalTakerAssetAmount: assetSellAmount, - makerAssetAmount: baseUnitAmount(1.6), - protocolFeeInWeiAmount: baseUnitAmount(45, 4), - }); - }); - it('calculates a correct swapQuote with no slippage (takerAsset denominated fee orders)', async () => { - const assetSellAmount = baseUnitAmount(4); - const slippagePercentage = 0; - const sampler = createSamplerFromSignedOrdersWithFillableAmounts( - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET, - ); - const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, { - exchangeAddress: contractAddresses.exchange, - chainId: TESTRPC_CHAIN_ID, - }); - const swapQuoteCalculator = new SwapQuoteCalculator(protocolFeeUtils, marketOperationUtils); - const swapQuote = await swapQuoteCalculator.calculateMarketSellSwapQuoteAsync( - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET, - assetSellAmount, - slippagePercentage, - GAS_PRICE, - CALCULATE_SWAP_QUOTE_OPTS, - ); - // test if orders are correct - expect(swapQuote.orders).to.deep.equal([ - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET[0], - ]); - expect(swapQuote.takerAssetFillAmount).to.bignumber.equal(assetSellAmount); - // test if rates are correct - expect(swapQuote.bestCaseQuoteInfo).to.deep.equal({ - feeTakerAssetAmount: baseUnitAmount(3), - takerAssetAmount: assetSellAmount.minus(baseUnitAmount(3)), - totalTakerAssetAmount: assetSellAmount, - makerAssetAmount: baseUnitAmount(6), - protocolFeeInWeiAmount: baseUnitAmount(15, 4), - }); - expect(swapQuote.worstCaseQuoteInfo).to.deep.equal({ - feeTakerAssetAmount: baseUnitAmount(3), - takerAssetAmount: assetSellAmount.minus(baseUnitAmount(3)), - totalTakerAssetAmount: assetSellAmount, - makerAssetAmount: baseUnitAmount(6), - protocolFeeInWeiAmount: baseUnitAmount(15, 4), - }); - }); - it('calculates a correct swapQuote with slippage (takerAsset denominated fee orders)', async () => { - const assetSellAmount = baseUnitAmount(3); - const slippagePercentage = 0.5; - const sampler = createSamplerFromSignedOrdersWithFillableAmounts( - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET, - ); - const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, { - exchangeAddress: contractAddresses.exchange, - chainId: TESTRPC_CHAIN_ID, - }); - const swapQuoteCalculator = new SwapQuoteCalculator(protocolFeeUtils, marketOperationUtils); - const swapQuote = await swapQuoteCalculator.calculateMarketSellSwapQuoteAsync( - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET, - assetSellAmount, - slippagePercentage, - GAS_PRICE, - CALCULATE_SWAP_QUOTE_OPTS, - ); - // test if orders are correct - expect(swapQuote.orders).to.deep.equal([ - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET[0], - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET[2], - ]); - expect(swapQuote.takerAssetFillAmount).to.bignumber.equal(assetSellAmount); - // test if rates are correct - expect(swapQuote.bestCaseQuoteInfo).to.deep.equal({ - feeTakerAssetAmount: baseUnitAmount(2.25), - takerAssetAmount: assetSellAmount.minus(baseUnitAmount(2.25)), - totalTakerAssetAmount: assetSellAmount, - makerAssetAmount: baseUnitAmount(4.5), - protocolFeeInWeiAmount: baseUnitAmount(15, 4), - }); - expect(swapQuote.worstCaseQuoteInfo).to.deep.equal({ - feeTakerAssetAmount: baseUnitAmount(1.2), - takerAssetAmount: assetSellAmount.minus(baseUnitAmount(1.2)), - totalTakerAssetAmount: assetSellAmount, - makerAssetAmount: baseUnitAmount(1.8), - protocolFeeInWeiAmount: baseUnitAmount(30, 4), - }); - }); - it('calculates a correct swapQuote with no slippage (makerAsset denominated fee orders)', async () => { - const assetSellAmount = baseUnitAmount(4); - const slippagePercentage = 0; - const sampler = createSamplerFromSignedOrdersWithFillableAmounts( - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET, - ); - const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, { - exchangeAddress: contractAddresses.exchange, - chainId: TESTRPC_CHAIN_ID, - }); - const swapQuoteCalculator = new SwapQuoteCalculator(protocolFeeUtils, marketOperationUtils); - const swapQuote = await swapQuoteCalculator.calculateMarketSellSwapQuoteAsync( - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET, - assetSellAmount, - slippagePercentage, - GAS_PRICE, - CALCULATE_SWAP_QUOTE_OPTS, - ); - // test if orders are correct - expect(swapQuote.orders).to.deep.equal([ - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET[1], - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET[2], - ]); - expect(swapQuote.takerAssetFillAmount).to.bignumber.equal(assetSellAmount); - // test if rates are correct - expect(swapQuote.bestCaseQuoteInfo).to.deep.equal({ - feeTakerAssetAmount: baseUnitAmount(1.5).minus(1), - takerAssetAmount: assetSellAmount.minus(baseUnitAmount(1.5)).plus(1), - totalTakerAssetAmount: assetSellAmount, - makerAssetAmount: baseUnitAmount(4), - protocolFeeInWeiAmount: baseUnitAmount(30, 4), - }); - expect(swapQuote.worstCaseQuoteInfo).to.deep.equal({ - feeTakerAssetAmount: baseUnitAmount(1.5).minus(1), - takerAssetAmount: assetSellAmount.minus(baseUnitAmount(1.5)).plus(1), - totalTakerAssetAmount: assetSellAmount, - makerAssetAmount: baseUnitAmount(4), - protocolFeeInWeiAmount: baseUnitAmount(30, 4), - }); - }); - it('calculates a correct swapQuote with slippage (makerAsset denominated fee orders)', async () => { - const assetSellAmount = baseUnitAmount(4); - const slippagePercentage = 0.5; - const sampler = createSamplerFromSignedOrdersWithFillableAmounts( - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET, - ); - const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, { - exchangeAddress: contractAddresses.exchange, - chainId: TESTRPC_CHAIN_ID, - }); - const swapQuoteCalculator = new SwapQuoteCalculator(protocolFeeUtils, marketOperationUtils); - const swapQuote = await swapQuoteCalculator.calculateMarketSellSwapQuoteAsync( - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET, - assetSellAmount, - slippagePercentage, - GAS_PRICE, - CALCULATE_SWAP_QUOTE_OPTS, - ); - // test if orders are correct - expect(swapQuote.orders).to.deep.equal([ - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET[1], - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET[2], - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET[0], - ]); - expect(swapQuote.takerAssetFillAmount).to.bignumber.equal(assetSellAmount); - // test if rates are correct - expect(swapQuote.bestCaseQuoteInfo).to.deep.equal({ - feeTakerAssetAmount: baseUnitAmount(1.5).minus(1), - takerAssetAmount: assetSellAmount.minus(baseUnitAmount(1.5)).plus(1), - totalTakerAssetAmount: assetSellAmount, - makerAssetAmount: baseUnitAmount(4), - protocolFeeInWeiAmount: baseUnitAmount(30, 4), - }); - expect(swapQuote.worstCaseQuoteInfo).to.deep.equal({ - feeTakerAssetAmount: baseUnitAmount(2), - takerAssetAmount: assetSellAmount.minus(baseUnitAmount(2)), - totalTakerAssetAmount: assetSellAmount, - makerAssetAmount: baseUnitAmount(0.8), - protocolFeeInWeiAmount: baseUnitAmount(45, 4), - }); - }); - }); - describe('#calculateMarketBuySwapQuoteAsync', () => { - // TODO(dave4506) InsufficientLiquidityError is not thrown anymore, consider how to test for insufficient liquidity - // describe('InsufficientLiquidityError', () => { - // it('should throw if not enough maker asset liquidity (multiple feeless orders)', async () => { - // const sampler = createSamplerFromSignedOrdersWithFillableAmounts(testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS); - // const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, { - // exchangeAddress: contractAddresses.exchange, - // chainId: TESTRPC_CHAIN_ID, - // }); - // const swapQuoteCalculator = new SwapQuoteCalculator( protocolFeeUtils, marketOperationUtils); - // const errorFunction = async () => { - // await swapQuoteCalculator.calculateMarketBuySwapQuoteAsync( - // testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS, - // baseUnitAmount(12), - // 0, - // GAS_PRICE, - // CALCULATE_SWAP_QUOTE_OPTS, - // ); - // }; - // await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(10)); - // }); - // it('should throw if not enough taker asset liquidity (multiple feeless orders with 20% slippage)', async () => { - // const sampler = createSamplerFromSignedOrdersWithFillableAmounts(testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS); - // const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, { - // exchangeAddress: contractAddresses.exchange, - // chainId: TESTRPC_CHAIN_ID, - // }); - // const swapQuoteCalculator = new SwapQuoteCalculator( protocolFeeUtils, marketOperationUtils); - // const errorFunction = async () => { - // await swapQuoteCalculator.calculateMarketBuySwapQuoteAsync( - // testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS, - // baseUnitAmount(10), - // 0.6, - // GAS_PRICE, - // CALCULATE_SWAP_QUOTE_OPTS, - // ); - // }; - // await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(6.25)); - // }); - // it('should throw if not enough taker asset liquidity (multiple takerAsset denominated fee orders with no slippage)', async () => { - // const sampler = createSamplerFromSignedOrdersWithFillableAmounts(testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET); - // const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, { - // exchangeAddress: contractAddresses.exchange, - // chainId: TESTRPC_CHAIN_ID, - // }); - // const swapQuoteCalculator = new SwapQuoteCalculator( protocolFeeUtils, marketOperationUtils); - // const errorFunction = async () => { - // await swapQuoteCalculator.calculateMarketBuySwapQuoteAsync( - // testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET, - // baseUnitAmount(12), - // 0, - // GAS_PRICE, - // CALCULATE_SWAP_QUOTE_OPTS, - // ); - // }; - // await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(10)); - // }); - // it('should throw if not enough taker asset liquidity (multiple takerAsset denominated fee orders with 20% slippage)', async () => { - // const sampler = createSamplerFromSignedOrdersWithFillableAmounts(testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET); - // const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, { - // exchangeAddress: contractAddresses.exchange, - // chainId: TESTRPC_CHAIN_ID, - // }); - // const swapQuoteCalculator = new SwapQuoteCalculator( protocolFeeUtils, marketOperationUtils); - // const errorFunction = async () => { - // await swapQuoteCalculator.calculateMarketBuySwapQuoteAsync( - // testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET, - // baseUnitAmount(12), - // 0.6, - // GAS_PRICE, - // CALCULATE_SWAP_QUOTE_OPTS, - // ); - // }; - // await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(6.25)); - // }); - // it('should throw if not enough taker asset liquidity (multiple makerAsset denominated fee orders with no slippage)', async () => { - // const sampler = createSamplerFromSignedOrdersWithFillableAmounts(testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET); - // const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, { - // exchangeAddress: contractAddresses.exchange, - // chainId: TESTRPC_CHAIN_ID, - // }); - // const swapQuoteCalculator = new SwapQuoteCalculator( protocolFeeUtils, marketOperationUtils); - // const errorFunction = async () => { - // await swapQuoteCalculator.calculateMarketBuySwapQuoteAsync( - // testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET, - // baseUnitAmount(6), - // 0, - // GAS_PRICE, - // CALCULATE_SWAP_QUOTE_OPTS, - // ); - // }; - // await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(5)); - // }); - // it('should throw if not enough taker asset liquidity (multiple makerAsset denominated fee orders with 20% slippage)', async () => { - // const sampler = createSamplerFromSignedOrdersWithFillableAmounts(testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET); - // const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, { - // exchangeAddress: contractAddresses.exchange, - // chainId: TESTRPC_CHAIN_ID, - // }); - // const swapQuoteCalculator = new SwapQuoteCalculator( protocolFeeUtils, marketOperationUtils); - // const errorFunction = async () => { - // await swapQuoteCalculator.calculateMarketBuySwapQuoteAsync( - // testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET, - // baseUnitAmount(6), - // 0.6, - // GAS_PRICE, - // CALCULATE_SWAP_QUOTE_OPTS, - // ); - // }; - // await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(3.125)); - // }); - // it('should throw if not enough taker asset liquidity (multiple mixed feeType orders with no slippage)', async () => { - // const sampler = createSamplerFromSignedOrdersWithFillableAmounts(MIXED_TEST_ORDERS); - // const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, { - // exchangeAddress: contractAddresses.exchange, - // chainId: TESTRPC_CHAIN_ID, - // }); - // const swapQuoteCalculator = new SwapQuoteCalculator( protocolFeeUtils, marketOperationUtils); - // const errorFunction = async () => { - // await swapQuoteCalculator.calculateMarketBuySwapQuoteAsync( - // MIXED_TEST_ORDERS, - // baseUnitAmount(40), - // 0, - // GAS_PRICE, - // CALCULATE_SWAP_QUOTE_OPTS, - // ); - // }; - // await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(25)); - // }); - // it('should throw if not enough taker asset liquidity (multiple mixed feeTyoe orders with 20% slippage)', async () => { - // const sampler = createSamplerFromSignedOrdersWithFillableAmounts(MIXED_TEST_ORDERS); - // const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, { - // exchangeAddress: contractAddresses.exchange, - // chainId: TESTRPC_CHAIN_ID, - // }); - // const swapQuoteCalculator = new SwapQuoteCalculator( protocolFeeUtils, marketOperationUtils); - // const errorFunction = async () => { - // await swapQuoteCalculator.calculateMarketBuySwapQuoteAsync( - // MIXED_TEST_ORDERS, - // baseUnitAmount(40), - // 0.6, - // GAS_PRICE, - // CALCULATE_SWAP_QUOTE_OPTS, - // ); - // }; - // await testHelpers.expectInsufficientLiquidityErrorAsync(expect, errorFunction, baseUnitAmount(15.625)); - // }); - // }); - it('calculates a correct swapQuote with no slippage (feeless orders)', async () => { - const assetBuyAmount = baseUnitAmount(3); - const slippagePercentage = 0; - const sampler = createSamplerFromSignedOrdersWithFillableAmounts( - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS, - ); - const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, { - exchangeAddress: contractAddresses.exchange, - chainId: TESTRPC_CHAIN_ID, - }); - const swapQuoteCalculator = new SwapQuoteCalculator(protocolFeeUtils, marketOperationUtils); - const swapQuote = await swapQuoteCalculator.calculateMarketBuySwapQuoteAsync( - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS, - assetBuyAmount, - slippagePercentage, - GAS_PRICE, - CALCULATE_SWAP_QUOTE_OPTS, - ); - // test if orders are correct - expect(swapQuote.orders).to.deep.equal([testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS[0]]); - expect(swapQuote.makerAssetFillAmount).to.bignumber.equal(assetBuyAmount); - // test if rates are correct - expect(swapQuote.bestCaseQuoteInfo).to.deep.equal({ - feeTakerAssetAmount: baseUnitAmount(0), - takerAssetAmount: baseUnitAmount(0.5), - totalTakerAssetAmount: baseUnitAmount(0.5), - makerAssetAmount: assetBuyAmount, - protocolFeeInWeiAmount: baseUnitAmount(15, 4), - }); - expect(swapQuote.worstCaseQuoteInfo).to.deep.equal({ - feeTakerAssetAmount: baseUnitAmount(0), - takerAssetAmount: baseUnitAmount(0.5), - totalTakerAssetAmount: baseUnitAmount(0.5), - makerAssetAmount: assetBuyAmount, - protocolFeeInWeiAmount: baseUnitAmount(15, 4), - }); - }); - it('calculates a correct swapQuote with slippage (feeless orders)', async () => { - const assetBuyAmount = baseUnitAmount(5); - const slippagePercentage = 0.5; - const sampler = createSamplerFromSignedOrdersWithFillableAmounts( - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS, - ); - const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, { - exchangeAddress: contractAddresses.exchange, - chainId: TESTRPC_CHAIN_ID, - }); - const swapQuoteCalculator = new SwapQuoteCalculator(protocolFeeUtils, marketOperationUtils); - const swapQuote = await swapQuoteCalculator.calculateMarketBuySwapQuoteAsync( - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS, - assetBuyAmount, - slippagePercentage, - GAS_PRICE, - CALCULATE_SWAP_QUOTE_OPTS, - ); - // test if orders are correct - expect(swapQuote.orders).to.deep.equal([ - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS[0], - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEELESS[2], - ]); - expect(swapQuote.makerAssetFillAmount).to.bignumber.equal(assetBuyAmount); - - const takerAssetAmount = new BigNumber(5) - .div(new BigNumber(6)) - .multipliedBy(ONE_ETH_IN_WEI) - .integerValue(BigNumber.ROUND_CEIL); - // test if rates are correct - expect(swapQuote.bestCaseQuoteInfo).to.deep.equal({ - feeTakerAssetAmount: baseUnitAmount(0), - takerAssetAmount, - totalTakerAssetAmount: takerAssetAmount, - makerAssetAmount: assetBuyAmount, - protocolFeeInWeiAmount: baseUnitAmount(15, 4), - }); - expect(swapQuote.worstCaseQuoteInfo).to.deep.equal({ - feeTakerAssetAmount: baseUnitAmount(0), - takerAssetAmount: baseUnitAmount(20) - .div(6) - .integerValue(BigNumber.ROUND_UP), - totalTakerAssetAmount: baseUnitAmount(20) - .div(6) - .integerValue(BigNumber.ROUND_UP), - makerAssetAmount: assetBuyAmount, - protocolFeeInWeiAmount: baseUnitAmount(30, 4), - }); - }); - it('calculates a correct swapQuote with no slippage (takerAsset denominated fee orders)', async () => { - const assetBuyAmount = baseUnitAmount(3); - const slippagePercentage = 0; - const sampler = createSamplerFromSignedOrdersWithFillableAmounts( - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET, - ); - const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, { - exchangeAddress: contractAddresses.exchange, - chainId: TESTRPC_CHAIN_ID, - }); - const swapQuoteCalculator = new SwapQuoteCalculator(protocolFeeUtils, marketOperationUtils); - const swapQuote = await swapQuoteCalculator.calculateMarketBuySwapQuoteAsync( - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET, - assetBuyAmount, - slippagePercentage, - GAS_PRICE, - CALCULATE_SWAP_QUOTE_OPTS, - ); - // test if orders are correct - expect(swapQuote.orders).to.deep.equal([ - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET[0], - ]); - expect(swapQuote.makerAssetFillAmount).to.bignumber.equal(assetBuyAmount); - // test if rates are correct - expect(swapQuote.bestCaseQuoteInfo).to.deep.equal({ - feeTakerAssetAmount: baseUnitAmount(1.5), - takerAssetAmount: baseUnitAmount(0.5), - totalTakerAssetAmount: baseUnitAmount(2), - makerAssetAmount: assetBuyAmount, - protocolFeeInWeiAmount: baseUnitAmount(15, 4), - }); - expect(swapQuote.worstCaseQuoteInfo).to.deep.equal({ - feeTakerAssetAmount: baseUnitAmount(1.5), - takerAssetAmount: baseUnitAmount(0.5), - totalTakerAssetAmount: baseUnitAmount(2), - makerAssetAmount: assetBuyAmount, - protocolFeeInWeiAmount: baseUnitAmount(15, 4), - }); - }); - it('calculates a correct swapQuote with slippage (takerAsset denominated fee orders)', async () => { - const assetBuyAmount = baseUnitAmount(5); - const slippagePercentage = 0.5; - const sampler = createSamplerFromSignedOrdersWithFillableAmounts( - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET, - ); - const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, { - exchangeAddress: contractAddresses.exchange, - chainId: TESTRPC_CHAIN_ID, - }); - const swapQuoteCalculator = new SwapQuoteCalculator(protocolFeeUtils, marketOperationUtils); - const swapQuote = await swapQuoteCalculator.calculateMarketBuySwapQuoteAsync( - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET, - assetBuyAmount, - slippagePercentage, - GAS_PRICE, - CALCULATE_SWAP_QUOTE_OPTS, - ); - const fiveSixthEthInWei = new BigNumber(5) - .div(new BigNumber(6)) - .multipliedBy(ONE_ETH_IN_WEI) - .integerValue(BigNumber.ROUND_CEIL); - // test if orders are correct - expect(swapQuote.orders).to.deep.equal([ - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET[0], - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_TAKER_ASSET[2], - ]); - expect(swapQuote.makerAssetFillAmount).to.bignumber.equal(assetBuyAmount); - // test if rates are correct - expect(swapQuote.bestCaseQuoteInfo).to.deep.equal({ - feeTakerAssetAmount: baseUnitAmount(2.5), - takerAssetAmount: fiveSixthEthInWei, - totalTakerAssetAmount: baseUnitAmount(2.5).plus(fiveSixthEthInWei), - makerAssetAmount: assetBuyAmount, - protocolFeeInWeiAmount: baseUnitAmount(15, 4), - }); - expect(swapQuote.worstCaseQuoteInfo).to.deep.equal({ - feeTakerAssetAmount: baseUnitAmount(3), - takerAssetAmount: baseUnitAmount(10) - .div(3) - .integerValue(BigNumber.ROUND_UP), // 3.3333... - totalTakerAssetAmount: baseUnitAmount(19) - .div(3) - .integerValue(BigNumber.ROUND_UP), // 6.3333... - makerAssetAmount: assetBuyAmount, - protocolFeeInWeiAmount: baseUnitAmount(30, 4), - }); - }); - it('calculates a correct swapQuote with no slippage (makerAsset denominated fee orders)', async () => { - const assetBuyAmount = baseUnitAmount(1); - const slippagePercentage = 0; - const sampler = createSamplerFromSignedOrdersWithFillableAmounts( - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET, - ); - const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, { - exchangeAddress: contractAddresses.exchange, - chainId: TESTRPC_CHAIN_ID, - }); - const swapQuoteCalculator = new SwapQuoteCalculator(protocolFeeUtils, marketOperationUtils); - const swapQuote = await swapQuoteCalculator.calculateMarketBuySwapQuoteAsync( - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET, - assetBuyAmount, - slippagePercentage, - GAS_PRICE, - CALCULATE_SWAP_QUOTE_OPTS, - ); - // test if orders are correct - expect(swapQuote.orders).to.deep.equal([ - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET[1], - ]); - expect(swapQuote.makerAssetFillAmount).to.bignumber.equal(assetBuyAmount); - // test if rates are correct - expect(swapQuote.bestCaseQuoteInfo).to.deep.equal({ - feeTakerAssetAmount: baseUnitAmount(0.5) - .div(3) - .integerValue(BigNumber.ROUND_UP), // 0.16666... - takerAssetAmount: baseUnitAmount(0.5) - .div(3) - .integerValue(BigNumber.ROUND_UP), // 0.1666... - totalTakerAssetAmount: baseUnitAmount(1) - .div(3) - .integerValue(BigNumber.ROUND_UP), // 0.3333... - makerAssetAmount: assetBuyAmount, - protocolFeeInWeiAmount: baseUnitAmount(15, 4), - }); - expect(swapQuote.worstCaseQuoteInfo).to.deep.equal({ - feeTakerAssetAmount: baseUnitAmount(0.5) - .div(3) - .integerValue(BigNumber.ROUND_UP), // 0.16666... - takerAssetAmount: baseUnitAmount(0.5) - .div(3) - .integerValue(BigNumber.ROUND_UP), // 0.1666... - totalTakerAssetAmount: baseUnitAmount(1) - .div(3) - .integerValue(BigNumber.ROUND_UP), // 0.3333... - makerAssetAmount: assetBuyAmount, - protocolFeeInWeiAmount: baseUnitAmount(15, 4), - }); - }); - it('calculates a correct swapQuote with slippage (makerAsset denominated fee orders)', async () => { - const assetBuyAmount = baseUnitAmount(2.5); - const slippagePercentage = 0.48; - const sampler = createSamplerFromSignedOrdersWithFillableAmounts( - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET, - ); - const marketOperationUtils = new MarketOperationUtils(sampler, contractAddresses, { - exchangeAddress: contractAddresses.exchange, - chainId: TESTRPC_CHAIN_ID, - }); - const swapQuoteCalculator = new SwapQuoteCalculator(protocolFeeUtils, marketOperationUtils); - const swapQuote = await swapQuoteCalculator.calculateMarketBuySwapQuoteAsync( - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET, - assetBuyAmount, - slippagePercentage, - GAS_PRICE, - CALCULATE_SWAP_QUOTE_OPTS, - ); - // test if orders are correct - expect(swapQuote.orders).to.deep.equal([ - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET[1], - testOrders.SIGNED_ORDERS_WITH_FILLABLE_AMOUNTS_FEE_IN_MAKER_ASSET[2], - ]); - expect(swapQuote.makerAssetFillAmount).to.bignumber.equal(assetBuyAmount); - // test if rates are correct - expect(swapQuote.worstCaseQuoteInfo).to.deep.equal({ - feeTakerAssetAmount: baseUnitAmount(1.25).minus(1), - takerAssetAmount: baseUnitAmount(2.25).plus(1), - totalTakerAssetAmount: baseUnitAmount(3.5), - makerAssetAmount: assetBuyAmount, - protocolFeeInWeiAmount: baseUnitAmount(30, 4), - }); - - const oneThirdEthInWei = new BigNumber(1) - .div(new BigNumber(3)) - .multipliedBy(ONE_ETH_IN_WEI) - .integerValue(BigNumber.ROUND_CEIL); - const oneSixthEthInWei = new BigNumber(1) - .div(new BigNumber(6)) - .multipliedBy(ONE_ETH_IN_WEI) - .integerValue(BigNumber.ROUND_CEIL); - expect(swapQuote.bestCaseQuoteInfo).to.deep.equal({ - feeTakerAssetAmount: baseUnitAmount(4) - .plus(oneSixthEthInWei) - .multipliedBy(0.1) - .integerValue(BigNumber.ROUND_CEIL), - takerAssetAmount: baseUnitAmount(4) - .plus(oneSixthEthInWei) - .multipliedBy(0.1) - .integerValue(BigNumber.ROUND_CEIL), - totalTakerAssetAmount: baseUnitAmount(8) - .plus(oneThirdEthInWei) - .multipliedBy(0.1) - .integerValue(BigNumber.ROUND_CEIL), - makerAssetAmount: assetBuyAmount, - protocolFeeInWeiAmount: baseUnitAmount(15, 4), - }); - }); - }); -}); -*/ diff --git a/packages/asset-swapper/test/swap_quote_consumer_utils_test.ts b/packages/asset-swapper/test/swap_quote_consumer_utils_test.ts index 5c80acaee0..3fe2fe4919 100644 --- a/packages/asset-swapper/test/swap_quote_consumer_utils_test.ts +++ b/packages/asset-swapper/test/swap_quote_consumer_utils_test.ts @@ -11,7 +11,6 @@ import 'mocha'; import { SwapQuote, SwapQuoteConsumer } from '../src'; import { constants } from '../src/constants'; import { ExtensionContractType, MarketOperation, SignedOrderWithFillableAmounts } from '../src/types'; -import { ProtocolFeeUtils } from '../src/utils/protocol_fee_utils'; import { chaiSetup } from './utils/chai_setup'; import { getFullyFillableSwapQuoteWithNoFeesAsync } from './utils/swap_quote'; @@ -69,7 +68,6 @@ const PARTIAL_LARGE_PRUNED_SIGNED_ORDERS: Array { let wethContract: WETH9Contract; - let protocolFeeUtils: ProtocolFeeUtils; let userAddresses: string[]; let makerAddress: string; let takerAddress: string; @@ -119,7 +117,6 @@ describe('swapQuoteConsumerUtils', () => { }; const privateKey = devConstants.TESTRPC_PRIVATE_KEYS[userAddresses.indexOf(makerAddress)]; orderFactory = new OrderFactory(privateKey, defaultOrderParams); - protocolFeeUtils = new ProtocolFeeUtils(constants.PROTOCOL_FEE_UTILS_POLLING_INTERVAL_IN_MS, new BigNumber(1)); forwarderOrderFactory = new OrderFactory(privateKey, defaultForwarderOrderParams); swapQuoteConsumer = new SwapQuoteConsumer(provider, { @@ -128,7 +125,6 @@ describe('swapQuoteConsumerUtils', () => { }); after(async () => { await blockchainLifecycle.revertAsync(); - await protocolFeeUtils.destroyAsync(); }); beforeEach(async () => { await blockchainLifecycle.startAsync(); @@ -182,7 +178,6 @@ describe('swapQuoteConsumerUtils', () => { forwarderOrders, MarketOperation.Sell, GAS_PRICE, - protocolFeeUtils, ); largeForwarderSwapQuote = await getFullyFillableSwapQuoteWithNoFeesAsync( @@ -191,7 +186,6 @@ describe('swapQuoteConsumerUtils', () => { largeForwarderOrders, MarketOperation.Sell, GAS_PRICE, - protocolFeeUtils, ); exchangeSwapQuote = await getFullyFillableSwapQuoteWithNoFeesAsync( @@ -200,7 +194,6 @@ describe('swapQuoteConsumerUtils', () => { exchangeOrders, MarketOperation.Sell, GAS_PRICE, - protocolFeeUtils, ); }); diff --git a/packages/asset-swapper/test/utils/mocks.ts b/packages/asset-swapper/test/utils/mocks.ts index 4b21904040..109c4b7d09 100644 --- a/packages/asset-swapper/test/utils/mocks.ts +++ b/packages/asset-swapper/test/utils/mocks.ts @@ -9,8 +9,6 @@ import { SwapQuoter } from '../../src/swap_quoter'; import { SignedOrderWithFillableAmounts } from '../../src/types'; import { ProtocolFeeUtils } from '../../src/utils/protocol_fee_utils'; -const PROTOCOL_FEE_MULTIPLIER = 150000; - // tslint:disable: max-classes-per-file class OrderbookClass extends Orderbook { @@ -57,10 +55,6 @@ const partiallyMockedSwapQuoter = (provider: Web3ProviderEngine, orderbook: Orde }; class ProtocolFeeUtilsClass extends ProtocolFeeUtils { - // tslint:disable-next-line:prefer-function-over-method - public async getProtocolFeeMultiplierAsync(): Promise { - return new BigNumber(PROTOCOL_FEE_MULTIPLIER); - } // tslint:disable-next-line:prefer-function-over-method public async getGasPriceEstimationOrThrowAsync(_shouldHardRefresh?: boolean): Promise { return new BigNumber(devConstants.DEFAULT_GAS_PRICE); diff --git a/packages/asset-swapper/test/utils/swap_quote.ts b/packages/asset-swapper/test/utils/swap_quote.ts index c347602961..de3bde6e72 100644 --- a/packages/asset-swapper/test/utils/swap_quote.ts +++ b/packages/asset-swapper/test/utils/swap_quote.ts @@ -4,7 +4,6 @@ import * as _ from 'lodash'; import { ERC20BridgeSource } from '../../src'; import { constants } from '../../src/constants'; import { MarketOperation, SignedOrderWithFillableAmounts, SwapQuote } from '../../src/types'; -import { ProtocolFeeUtils } from '../../src/utils/protocol_fee_utils'; /** * Creates a swap quote given orders. @@ -15,16 +14,16 @@ export async function getFullyFillableSwapQuoteWithNoFeesAsync( orders: SignedOrderWithFillableAmounts[], operation: MarketOperation, gasPrice: BigNumber, - protocolFeeUtils: ProtocolFeeUtils, ): Promise { const makerAssetFillAmount = BigNumber.sum(...[0, ...orders.map(o => o.makerAssetAmount)]); const totalTakerAssetAmount = BigNumber.sum(...[0, ...orders.map(o => o.takerAssetAmount)]); + const protocolFeePerOrder = constants.PROTOCOL_FEE_MULTIPLIER.times(gasPrice); const quoteInfo = { makerAssetAmount: makerAssetFillAmount, feeTakerAssetAmount: constants.ZERO_AMOUNT, takerAssetAmount: totalTakerAssetAmount, totalTakerAssetAmount, - protocolFeeInWeiAmount: await protocolFeeUtils.calculateWorstCaseProtocolFeeAsync(orders, gasPrice), + protocolFeeInWeiAmount: protocolFeePerOrder.times(orders.length), gas: 200e3, }; diff --git a/packages/contract-addresses/CHANGELOG.json b/packages/contract-addresses/CHANGELOG.json index e1739a3faa..f19125053b 100644 --- a/packages/contract-addresses/CHANGELOG.json +++ b/packages/contract-addresses/CHANGELOG.json @@ -29,6 +29,14 @@ { "note": "Redeploy `Forwarder` on all networks", "pr": 2521 + }, + { + "note": "Redeploy `DexForwarderBridge` on Mainnet with Gas Token freeing", + "pr": 2536 + }, + { + "note": "Revert to older Curve Bridge (without Gas Tokens)", + "pr": 2536 } ] }, diff --git a/packages/contract-addresses/addresses.json b/packages/contract-addresses/addresses.json index 45e9820fa8..273b87f4f5 100644 --- a/packages/contract-addresses/addresses.json +++ b/packages/contract-addresses/addresses.json @@ -28,9 +28,9 @@ "godsUnchainedValidator": "0x09a379ef7218bcfd8913faa8b281ebc5a2e0bc04", "broker": "0xd4690a51044db77d91d7aa8f7a3a5ad5da331af0", "chainlinkStopLimit": "0xeb27220f95f364e1d9531992c48613f231839f53", - "curveBridge": "0x1cf6ccc7e15d0d99a9498f37e16ba65b5c54bdd0", + "curveBridge": "0x6dc7950423ada9f56fb2c93a23edb787f1e29088", "maximumGasPrice": "0xe2bfd35306495d11e3c9db0d8de390cda24563cf", - "dexForwarderBridge": "0xa96844087062acf8556ca06a27702c6d19f87e57" + "dexForwarderBridge": "0x5591360f8c7640fea5771c9682d6b5ecb776e1f8" }, "3": { "erc20Proxy": "0xb1408f4c245a23c31b98d2c626777d4c0d766caa",