Merge pull request #2536 from 0xProject/feat/asset-swapper/DexForwarderBridge-support
asset-swapper: DFB support + refactors
This commit is contained in:
commit
a80d1f861c
@ -21,6 +21,10 @@
|
|||||||
{
|
{
|
||||||
"note": "Add `DexForwaderBridge` bridge contract.",
|
"note": "Add `DexForwaderBridge` bridge contract.",
|
||||||
"pr": 2525
|
"pr": 2525
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"note": "Add Gas Token freeing to `DexForwaderBridge` contract.",
|
||||||
|
"pr": 2536
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -23,15 +23,19 @@ import "@0x/contracts-erc20/contracts/src/interfaces/IERC20Token.sol";
|
|||||||
import "@0x/contracts-erc20/contracts/src/LibERC20Token.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/IWallet.sol";
|
||||||
import "@0x/contracts-exchange-libs/contracts/src/LibMath.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/LibBytes.sol";
|
||||||
import "@0x/contracts-utils/contracts/src/LibSafeMath.sol";
|
import "@0x/contracts-utils/contracts/src/LibSafeMath.sol";
|
||||||
import "../interfaces/IERC20Bridge.sol";
|
import "../interfaces/IERC20Bridge.sol";
|
||||||
|
import "./MixinGasToken.sol";
|
||||||
|
|
||||||
|
|
||||||
// solhint-disable space-after-comma, indent
|
// solhint-disable space-after-comma, indent
|
||||||
contract DexForwarderBridge is
|
contract DexForwarderBridge is
|
||||||
IERC20Bridge,
|
IERC20Bridge,
|
||||||
IWallet
|
IWallet,
|
||||||
|
DeploymentConstants,
|
||||||
|
MixinGasToken
|
||||||
{
|
{
|
||||||
using LibSafeMath for uint256;
|
using LibSafeMath for uint256;
|
||||||
|
|
||||||
@ -68,8 +72,10 @@ contract DexForwarderBridge is
|
|||||||
bytes calldata bridgeData
|
bytes calldata bridgeData
|
||||||
)
|
)
|
||||||
external
|
external
|
||||||
|
freesGasTokensFromCollector
|
||||||
returns (bytes4 success)
|
returns (bytes4 success)
|
||||||
{
|
{
|
||||||
|
require(msg.sender == _getERC20BridgeProxyAddress(), "DexForwarderBridge/SENDER_NOT_AUTHORIZED");
|
||||||
TransferFromState memory state;
|
TransferFromState memory state;
|
||||||
(
|
(
|
||||||
state.inputToken,
|
state.inputToken,
|
||||||
@ -84,16 +90,15 @@ contract DexForwarderBridge is
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
BridgeCall memory call = state.calls[i];
|
|
||||||
// Compute token amounts.
|
// Compute token amounts.
|
||||||
state.callInputTokenAmount = LibSafeMath.min256(
|
state.callInputTokenAmount = LibSafeMath.min256(
|
||||||
call.inputTokenAmount,
|
state.calls[i].inputTokenAmount,
|
||||||
state.initialInputTokenBalance.safeSub(state.totalInputTokenSold)
|
state.initialInputTokenBalance.safeSub(state.totalInputTokenSold)
|
||||||
);
|
);
|
||||||
state.callOutputTokenAmount = LibMath.getPartialAmountFloor(
|
state.callOutputTokenAmount = LibMath.getPartialAmountFloor(
|
||||||
state.callInputTokenAmount,
|
state.callInputTokenAmount,
|
||||||
call.inputTokenAmount,
|
state.calls[i].inputTokenAmount,
|
||||||
call.outputTokenAmount
|
state.calls[i].outputTokenAmount
|
||||||
);
|
);
|
||||||
|
|
||||||
// Execute the call in a new context so we can recoup transferred
|
// Execute the call in a new context so we can recoup transferred
|
||||||
@ -101,13 +106,13 @@ contract DexForwarderBridge is
|
|||||||
(bool didSucceed, ) = address(this)
|
(bool didSucceed, ) = address(this)
|
||||||
.call(abi.encodeWithSelector(
|
.call(abi.encodeWithSelector(
|
||||||
this.executeBridgeCall.selector,
|
this.executeBridgeCall.selector,
|
||||||
call.target,
|
state.calls[i].target,
|
||||||
to,
|
to,
|
||||||
state.inputToken,
|
state.inputToken,
|
||||||
outputToken,
|
outputToken,
|
||||||
state.callInputTokenAmount,
|
state.callInputTokenAmount,
|
||||||
state.callOutputTokenAmount,
|
state.callOutputTokenAmount,
|
||||||
call.bridgeData
|
state.calls[i].bridgeData
|
||||||
));
|
));
|
||||||
|
|
||||||
if (didSucceed) {
|
if (didSucceed) {
|
||||||
|
@ -156,6 +156,14 @@ contract TestDexForwarderBridge is
|
|||||||
ITestDexForwarderBridge,
|
ITestDexForwarderBridge,
|
||||||
DexForwarderBridge
|
DexForwarderBridge
|
||||||
{
|
{
|
||||||
|
address private AUTHORIZED_ADDRESS; // solhint-disable-line var-name-mixedcase
|
||||||
|
|
||||||
|
function setAuthorized(address authorized)
|
||||||
|
public
|
||||||
|
{
|
||||||
|
AUTHORIZED_ADDRESS = authorized;
|
||||||
|
}
|
||||||
|
|
||||||
function createBridge(
|
function createBridge(
|
||||||
bytes4 returnCode,
|
bytes4 returnCode,
|
||||||
string memory revertError
|
string memory revertError
|
||||||
@ -217,4 +225,20 @@ contract TestDexForwarderBridge is
|
|||||||
function balanceOf(address token, address owner) public view returns (uint256) {
|
function balanceOf(address token, address owner) public view returns (uint256) {
|
||||||
return TestDexForwarderBridgeTestToken(token).balanceOf(owner);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ import {
|
|||||||
randomAddress,
|
randomAddress,
|
||||||
shortZip,
|
shortZip,
|
||||||
} from '@0x/contracts-test-utils';
|
} 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 { DecodedLogs } from 'ethereum-types';
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
|
|
||||||
@ -31,6 +31,7 @@ blockchainTests.resets('DexForwarderBridge unit tests', env => {
|
|||||||
const BRIDGE_FAILURE = '0xffffffff';
|
const BRIDGE_FAILURE = '0xffffffff';
|
||||||
const BRIDGE_REVERT_ERROR = 'oopsie';
|
const BRIDGE_REVERT_ERROR = 'oopsie';
|
||||||
const INCOMPLETE_FILL_REVERT = 'DexForwarderBridge/INCOMPLETE_FILL';
|
const INCOMPLETE_FILL_REVERT = 'DexForwarderBridge/INCOMPLETE_FILL';
|
||||||
|
const NOT_AUTHORIZED_REVERT = 'DexForwarderBridge/SENDER_NOT_AUTHORIZED';
|
||||||
const DEFAULTS = {
|
const DEFAULTS = {
|
||||||
toAddress: randomAddress(),
|
toAddress: randomAddress(),
|
||||||
};
|
};
|
||||||
@ -47,6 +48,7 @@ blockchainTests.resets('DexForwarderBridge unit tests', env => {
|
|||||||
await callAndTransactAsync(testContract.createToken()),
|
await callAndTransactAsync(testContract.createToken()),
|
||||||
await callAndTransactAsync(testContract.createToken()),
|
await callAndTransactAsync(testContract.createToken()),
|
||||||
];
|
];
|
||||||
|
await callAndTransactAsync(testContract.setAuthorized(env.txDefaults.from as string));
|
||||||
});
|
});
|
||||||
|
|
||||||
async function callAndTransactAsync<TResult>(fnCall: ContractTxFunctionObj<TResult>): Promise<TResult> {
|
async function callAndTransactAsync<TResult>(fnCall: ContractTxFunctionObj<TResult>): Promise<TResult> {
|
||||||
@ -186,6 +188,18 @@ blockchainTests.resets('DexForwarderBridge unit tests', env => {
|
|||||||
).to.revertWith(INCOMPLETE_FILL_REVERT);
|
).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 () => {
|
it('succeeds with one bridge call', async () => {
|
||||||
const calls = goodBridgeCalls.slice(0, 1);
|
const calls = goodBridgeCalls.slice(0, 1);
|
||||||
const bridgeData = dexForwarderBridgeDataEncoder.encode({
|
const bridgeData = dexForwarderBridgeDataEncoder.encode({
|
||||||
|
@ -37,6 +37,10 @@
|
|||||||
{
|
{
|
||||||
"note": "Fix `getBatchMarketBuyOrdersAsync` throwing NO_OPTIMAL_PATH",
|
"note": "Fix `getBatchMarketBuyOrdersAsync` throwing NO_OPTIMAL_PATH",
|
||||||
"pr": 2533
|
"pr": 2533
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"note": "Add DFB support + refactor swap quote calculator utils",
|
||||||
|
"pr": 2536
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -12,7 +12,7 @@ export {
|
|||||||
SRAPollingOrderProviderOpts,
|
SRAPollingOrderProviderOpts,
|
||||||
SRAWebsocketOrderProviderOpts,
|
SRAWebsocketOrderProviderOpts,
|
||||||
} from '@0x/orderbook';
|
} 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 { BigNumber } from '@0x/utils';
|
||||||
export {
|
export {
|
||||||
DataItem,
|
DataItem,
|
||||||
|
@ -179,7 +179,7 @@ export class SwapQuoter {
|
|||||||
},
|
},
|
||||||
liquidityProviderRegistryAddress,
|
liquidityProviderRegistryAddress,
|
||||||
);
|
);
|
||||||
this._swapQuoteCalculator = new SwapQuoteCalculator(this._protocolFeeUtils, this._marketOperationUtils);
|
this._swapQuoteCalculator = new SwapQuoteCalculator(this._marketOperationUtils);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -34,6 +34,7 @@ export const DEFAULT_GET_MARKET_ORDERS_OPTS: GetMarketOrdersOpts = {
|
|||||||
feeSchedule: {},
|
feeSchedule: {},
|
||||||
gasSchedule: {},
|
gasSchedule: {},
|
||||||
allowFallback: true,
|
allowFallback: true,
|
||||||
|
shouldBatchBridgeOrders: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -233,27 +233,25 @@ export function clipPathToInput(path: Fill[], targetInput: BigNumber = POSITIVE_
|
|||||||
return clipped;
|
return clipped;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function collapsePath(side: MarketOperation, path: Fill[]): CollapsedFill[] {
|
export function collapsePath(path: Fill[]): CollapsedFill[] {
|
||||||
const collapsed: Array<CollapsedFill | NativeCollapsedFill> = [];
|
const collapsed: Array<CollapsedFill | NativeCollapsedFill> = [];
|
||||||
for (const fill of path) {
|
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;
|
const source = fill.source;
|
||||||
if (collapsed.length !== 0 && source !== ERC20BridgeSource.Native) {
|
if (collapsed.length !== 0 && source !== ERC20BridgeSource.Native) {
|
||||||
const prevFill = collapsed[collapsed.length - 1];
|
const prevFill = collapsed[collapsed.length - 1];
|
||||||
// If the last fill is from the same source, merge them.
|
// If the last fill is from the same source, merge them.
|
||||||
if (prevFill.source === source) {
|
if (prevFill.source === source) {
|
||||||
prevFill.totalMakerAssetAmount = prevFill.totalMakerAssetAmount.plus(makerAssetAmount);
|
prevFill.input = prevFill.input.plus(fill.input);
|
||||||
prevFill.totalTakerAssetAmount = prevFill.totalTakerAssetAmount.plus(takerAssetAmount);
|
prevFill.output = prevFill.output.plus(fill.output);
|
||||||
prevFill.subFills.push({ makerAssetAmount, takerAssetAmount });
|
prevFill.subFills.push(fill);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
collapsed.push({
|
collapsed.push({
|
||||||
source: fill.source,
|
source: fill.source,
|
||||||
totalMakerAssetAmount: makerAssetAmount,
|
input: fill.input,
|
||||||
totalTakerAssetAmount: takerAssetAmount,
|
output: fill.output,
|
||||||
subFills: [{ makerAssetAmount, takerAssetAmount }],
|
subFills: [fill],
|
||||||
nativeOrder: fill.source === ERC20BridgeSource.Native ? (fill.fillData as NativeFillData).order : undefined,
|
nativeOrder: fill.source === ERC20BridgeSource.Native ? (fill.fillData as NativeFillData).order : undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -107,6 +107,7 @@ export class MarketOperationUtils {
|
|||||||
excludedSources: _opts.excludedSources,
|
excludedSources: _opts.excludedSources,
|
||||||
feeSchedule: _opts.feeSchedule,
|
feeSchedule: _opts.feeSchedule,
|
||||||
allowFallback: _opts.allowFallback,
|
allowFallback: _opts.allowFallback,
|
||||||
|
shouldBatchBridgeOrders: _opts.shouldBatchBridgeOrders,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -180,6 +181,7 @@ export class MarketOperationUtils {
|
|||||||
excludedSources: _opts.excludedSources,
|
excludedSources: _opts.excludedSources,
|
||||||
feeSchedule: _opts.feeSchedule,
|
feeSchedule: _opts.feeSchedule,
|
||||||
allowFallback: _opts.allowFallback,
|
allowFallback: _opts.allowFallback,
|
||||||
|
shouldBatchBridgeOrders: _opts.shouldBatchBridgeOrders,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -254,6 +256,7 @@ export class MarketOperationUtils {
|
|||||||
excludedSources: _opts.excludedSources,
|
excludedSources: _opts.excludedSources,
|
||||||
feeSchedule: _opts.feeSchedule,
|
feeSchedule: _opts.feeSchedule,
|
||||||
allowFallback: _opts.allowFallback,
|
allowFallback: _opts.allowFallback,
|
||||||
|
shouldBatchBridgeOrders: _opts.shouldBatchBridgeOrders,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// It's possible for one of the pairs to have no path
|
// It's possible for one of the pairs to have no path
|
||||||
@ -278,6 +281,7 @@ export class MarketOperationUtils {
|
|||||||
excludedSources?: ERC20BridgeSource[];
|
excludedSources?: ERC20BridgeSource[];
|
||||||
feeSchedule?: { [source: string]: BigNumber };
|
feeSchedule?: { [source: string]: BigNumber };
|
||||||
allowFallback?: boolean;
|
allowFallback?: boolean;
|
||||||
|
shouldBatchBridgeOrders?: boolean;
|
||||||
liquidityProviderAddress?: string;
|
liquidityProviderAddress?: string;
|
||||||
}): OptimizedMarketOrder[] {
|
}): OptimizedMarketOrder[] {
|
||||||
const { inputToken, outputToken, side, inputAmount } = opts;
|
const { inputToken, outputToken, side, inputAmount } = opts;
|
||||||
@ -327,6 +331,7 @@ export class MarketOperationUtils {
|
|||||||
contractAddresses: this.contractAddresses,
|
contractAddresses: this.contractAddresses,
|
||||||
bridgeSlippage: opts.bridgeSlippage || 0,
|
bridgeSlippage: opts.bridgeSlippage || 0,
|
||||||
liquidityProviderAddress: opts.liquidityProviderAddress,
|
liquidityProviderAddress: opts.liquidityProviderAddress,
|
||||||
|
shouldBatchBridgeOrders: !!opts.shouldBatchBridgeOrders,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { ContractAddresses } from '@0x/contract-addresses';
|
import { ContractAddresses } from '@0x/contract-addresses';
|
||||||
import { assetDataUtils, ERC20AssetData, generatePseudoRandomSalt, orderCalculationUtils } from '@0x/order-utils';
|
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 { AbiEncoder, BigNumber } from '@0x/utils';
|
||||||
|
|
||||||
import { MarketOperation, SignedOrderWithFillableAmounts } from '../../types';
|
import { MarketOperation, SignedOrderWithFillableAmounts } from '../../types';
|
||||||
@ -26,7 +26,31 @@ import {
|
|||||||
OrderDomain,
|
OrderDomain,
|
||||||
} from './types';
|
} 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(
|
export function createDummyOrderForSampler(
|
||||||
makerAssetData: string,
|
makerAssetData: string,
|
||||||
@ -71,12 +95,7 @@ export function convertNativeOrderToFullyFillableOptimizedOrders(order: SignedOr
|
|||||||
fillableMakerAssetAmount: order.makerAssetAmount,
|
fillableMakerAssetAmount: order.makerAssetAmount,
|
||||||
fillableTakerAssetAmount: order.takerAssetAmount,
|
fillableTakerAssetAmount: order.takerAssetAmount,
|
||||||
fillableTakerFeeAmount: order.takerFee,
|
fillableTakerFeeAmount: order.takerFee,
|
||||||
fill: {
|
fills: [],
|
||||||
source: ERC20BridgeSource.Native,
|
|
||||||
totalMakerAssetAmount: order.makerAssetAmount,
|
|
||||||
totalTakerAssetAmount: order.takerAssetAmount,
|
|
||||||
subFills: [],
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -119,18 +138,43 @@ export interface CreateOrderFromPathOpts {
|
|||||||
orderDomain: OrderDomain;
|
orderDomain: OrderDomain;
|
||||||
contractAddresses: ContractAddresses;
|
contractAddresses: ContractAddresses;
|
||||||
bridgeSlippage: number;
|
bridgeSlippage: number;
|
||||||
|
shouldBatchBridgeOrders: boolean;
|
||||||
liquidityProviderAddress?: string;
|
liquidityProviderAddress?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert sell fills into orders.
|
// Convert sell fills into orders.
|
||||||
export function createOrdersFromPath(path: Fill[], opts: CreateOrderFromPathOpts): OptimizedMarketOrder[] {
|
export function createOrdersFromPath(path: Fill[], opts: CreateOrderFromPathOpts): OptimizedMarketOrder[] {
|
||||||
const collapsedPath = collapsePath(opts.side, path);
|
const collapsedPath = collapsePath(path);
|
||||||
const orders: OptimizedMarketOrder[] = [];
|
const orders: OptimizedMarketOrder[] = [];
|
||||||
for (const fill of collapsedPath) {
|
for (let i = 0; i < collapsedPath.length; ) {
|
||||||
if (fill.source === ERC20BridgeSource.Native) {
|
if (collapsedPath[i].source === ERC20BridgeSource.Native) {
|
||||||
orders.push(createNativeOrder(fill));
|
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 {
|
} else {
|
||||||
orders.push(createBridgeOrder(fill, opts));
|
orders.push(createBatchedBridgeOrder(contiguousBridgeFills, opts));
|
||||||
|
i += contiguousBridgeFills.length;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return orders;
|
return orders;
|
||||||
@ -161,8 +205,7 @@ function getBridgeAddressFromSource(source: ERC20BridgeSource, opts: CreateOrder
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createBridgeOrder(fill: CollapsedFill, opts: CreateOrderFromPathOpts): OptimizedMarketOrder {
|
function createBridgeOrder(fill: CollapsedFill, opts: CreateOrderFromPathOpts): OptimizedMarketOrder {
|
||||||
const takerToken = opts.side === MarketOperation.Sell ? opts.inputToken : opts.outputToken;
|
const [makerToken, takerToken] = getMakerTakerTokens(opts);
|
||||||
const makerToken = opts.side === MarketOperation.Sell ? opts.outputToken : opts.inputToken;
|
|
||||||
const bridgeAddress = getBridgeAddressFromSource(fill.source, opts);
|
const bridgeAddress = getBridgeAddressFromSource(fill.source, opts);
|
||||||
|
|
||||||
let makerAssetData;
|
let makerAssetData;
|
||||||
@ -182,14 +225,67 @@ function createBridgeOrder(fill: CollapsedFill, opts: CreateOrderFromPathOpts):
|
|||||||
createBridgeData(takerToken),
|
createBridgeData(takerToken),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
const [slippedMakerAssetAmount, slippedTakerAssetAmount] = getSlippedBridgeAssetAmounts(fill, opts);
|
||||||
return {
|
return {
|
||||||
makerAddress: bridgeAddress,
|
fills: [fill],
|
||||||
makerAssetData,
|
makerAssetData,
|
||||||
takerAssetData: assetDataUtils.encodeERC20AssetData(takerToken),
|
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 {
|
function createBridgeData(tokenAddress: string): string {
|
||||||
const encoder = AbiEncoder.create([{ name: 'tokenAddress', type: 'address' }]);
|
const encoder = AbiEncoder.create([{ name: 'tokenAddress', type: 'address' }]);
|
||||||
return encoder.encode({ tokenAddress });
|
return encoder.encode({ tokenAddress });
|
||||||
@ -210,22 +306,36 @@ function createCurveBridgeData(
|
|||||||
return curveBridgeDataEncoder.encode([curveAddress, fromTokenIdx, toTokenIdx, version]);
|
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<
|
type CommonBridgeOrderFields = Pick<
|
||||||
OptimizedMarketOrder,
|
OptimizedMarketOrder,
|
||||||
Exclude<keyof OptimizedMarketOrder, 'makerAddress' | 'makerAssetData' | 'takerAssetData'>
|
Exclude<
|
||||||
|
keyof OptimizedMarketOrder,
|
||||||
|
| 'fills'
|
||||||
|
| 'makerAddress'
|
||||||
|
| 'makerAssetData'
|
||||||
|
| 'takerAssetData'
|
||||||
|
| 'makerAssetAmount'
|
||||||
|
| 'takerAssetAmount'
|
||||||
|
| 'fillableMakerAssetAmount'
|
||||||
|
| 'fillableTakerAssetAmount'
|
||||||
|
>
|
||||||
>;
|
>;
|
||||||
|
|
||||||
function createCommonBridgeOrderFields(fill: CollapsedFill, opts: CreateOrderFromPathOpts): CommonBridgeOrderFields {
|
function createCommonBridgeOrderFields(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);
|
|
||||||
return {
|
return {
|
||||||
fill,
|
|
||||||
takerAddress: NULL_ADDRESS,
|
takerAddress: NULL_ADDRESS,
|
||||||
senderAddress: NULL_ADDRESS,
|
senderAddress: NULL_ADDRESS,
|
||||||
feeRecipientAddress: NULL_ADDRESS,
|
feeRecipientAddress: NULL_ADDRESS,
|
||||||
@ -235,10 +345,6 @@ function createCommonBridgeOrderFields(fill: CollapsedFill, opts: CreateOrderFro
|
|||||||
takerFeeAssetData: NULL_BYTES,
|
takerFeeAssetData: NULL_BYTES,
|
||||||
makerFee: ZERO_AMOUNT,
|
makerFee: ZERO_AMOUNT,
|
||||||
takerFee: ZERO_AMOUNT,
|
takerFee: ZERO_AMOUNT,
|
||||||
makerAssetAmount: makerAssetAmountAdjustedWithSlippage,
|
|
||||||
fillableMakerAssetAmount: makerAssetAmountAdjustedWithSlippage,
|
|
||||||
takerAssetAmount: takerAssetAmountAdjustedWithSlippage,
|
|
||||||
fillableTakerAssetAmount: takerAssetAmountAdjustedWithSlippage,
|
|
||||||
fillableTakerFeeAmount: ZERO_AMOUNT,
|
fillableTakerFeeAmount: ZERO_AMOUNT,
|
||||||
signature: WALLET_SIGNATURE,
|
signature: WALLET_SIGNATURE,
|
||||||
...opts.orderDomain,
|
...opts.orderDomain,
|
||||||
@ -247,12 +353,7 @@ function createCommonBridgeOrderFields(fill: CollapsedFill, opts: CreateOrderFro
|
|||||||
|
|
||||||
function createNativeOrder(fill: CollapsedFill): OptimizedMarketOrder {
|
function createNativeOrder(fill: CollapsedFill): OptimizedMarketOrder {
|
||||||
return {
|
return {
|
||||||
fill: {
|
fills: [fill],
|
||||||
source: fill.source,
|
|
||||||
totalMakerAssetAmount: fill.totalMakerAssetAmount,
|
|
||||||
totalTakerAssetAmount: fill.totalTakerAssetAmount,
|
|
||||||
subFills: fill.subFills,
|
|
||||||
},
|
|
||||||
...(fill as NativeCollapsedFill).nativeOrder,
|
...(fill as NativeCollapsedFill).nativeOrder,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -97,19 +97,19 @@ export interface CollapsedFill {
|
|||||||
*/
|
*/
|
||||||
source: ERC20BridgeSource;
|
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<{
|
subFills: Array<{
|
||||||
makerAssetAmount: BigNumber;
|
input: BigNumber;
|
||||||
takerAssetAmount: BigNumber;
|
output: BigNumber;
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -127,7 +127,7 @@ export interface OptimizedMarketOrder extends SignedOrderWithFillableAmounts {
|
|||||||
/**
|
/**
|
||||||
* The optimized fills that generated this order.
|
* The optimized fills that generated this order.
|
||||||
*/
|
*/
|
||||||
fill: CollapsedFill;
|
fills: CollapsedFill[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -180,9 +180,14 @@ export interface GetMarketOrdersOpts {
|
|||||||
gasSchedule: { [source: string]: number };
|
gasSchedule: { [source: string]: number };
|
||||||
/**
|
/**
|
||||||
* Whether to pad the quote with a redundant fallback quote using different
|
* Whether to pad the quote with a redundant fallback quote using different
|
||||||
* sources.
|
* sources. Defaults to `true`.
|
||||||
*/
|
*/
|
||||||
allowFallback: boolean;
|
allowFallback: boolean;
|
||||||
|
/**
|
||||||
|
* Whether to combine contiguous bridge orders into a single DexForwarderBridge
|
||||||
|
* order. Defaults to `true`.
|
||||||
|
*/
|
||||||
|
shouldBatchBridgeOrders: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { Order } from '@0x/types';
|
|
||||||
import { BigNumber } from '@0x/utils';
|
import { BigNumber } from '@0x/utils';
|
||||||
import * as heartbeats from 'heartbeats';
|
import * as heartbeats from 'heartbeats';
|
||||||
|
|
||||||
@ -15,12 +14,6 @@ export class ProtocolFeeUtils {
|
|||||||
this._initializeHeartBeat();
|
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<BigNumber> {
|
|
||||||
return constants.PROTOCOL_FEE_MULTIPLIER;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getGasPriceEstimationOrThrowAsync(shouldHardRefresh?: boolean): Promise<BigNumber> {
|
public async getGasPriceEstimationOrThrowAsync(shouldHardRefresh?: boolean): Promise<BigNumber> {
|
||||||
if (this.gasPriceEstimation.eq(constants.ZERO_AMOUNT)) {
|
if (this.gasPriceEstimation.eq(constants.ZERO_AMOUNT)) {
|
||||||
return this._getGasPriceFromGasStationOrThrowAsync();
|
return this._getGasPriceFromGasStationOrThrowAsync();
|
||||||
@ -39,18 +32,6 @@ export class ProtocolFeeUtils {
|
|||||||
this._gasPriceHeart.kill();
|
this._gasPriceHeart.kill();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculates protocol fee with protofol fee multiplier for each fill.
|
|
||||||
*/
|
|
||||||
public async calculateWorstCaseProtocolFeeAsync<T extends Order>(
|
|
||||||
orders: T[],
|
|
||||||
gasPrice: BigNumber,
|
|
||||||
): Promise<BigNumber> {
|
|
||||||
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
|
// tslint:disable-next-line: prefer-function-over-method
|
||||||
private async _getGasPriceFromGasStationOrThrowAsync(): Promise<BigNumber> {
|
private async _getGasPriceFromGasStationOrThrowAsync(): Promise<BigNumber> {
|
||||||
try {
|
try {
|
||||||
|
351
packages/asset-swapper/src/utils/quote_simulation.ts
Normal file
351
packages/asset-swapper/src/utils/quote_simulation.ts
Normal file
@ -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<QuoteFillInfoOpts>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
@ -1,9 +1,8 @@
|
|||||||
import { assetDataUtils, orderCalculationUtils } from '@0x/order-utils';
|
import { assetDataUtils } from '@0x/order-utils';
|
||||||
import { AssetProxyId, SignedOrder } from '@0x/types';
|
import { AssetProxyId, SignedOrder } from '@0x/types';
|
||||||
import { BigNumber } from '@0x/utils';
|
import { BigNumber } from '@0x/utils';
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
|
|
||||||
import { constants } from '../constants';
|
|
||||||
import {
|
import {
|
||||||
CalculateSwapQuoteOpts,
|
CalculateSwapQuoteOpts,
|
||||||
MarketBuySwapQuote,
|
MarketBuySwapQuote,
|
||||||
@ -17,24 +16,18 @@ import {
|
|||||||
SwapQuoterError,
|
SwapQuoterError,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
|
|
||||||
import { fillableAmountsUtils } from './fillable_amounts_utils';
|
|
||||||
import { MarketOperationUtils } from './market_operation_utils';
|
import { MarketOperationUtils } from './market_operation_utils';
|
||||||
import { convertNativeOrderToFullyFillableOptimizedOrders } from './market_operation_utils/orders';
|
import { convertNativeOrderToFullyFillableOptimizedOrders } from './market_operation_utils/orders';
|
||||||
import { ERC20BridgeSource, GetMarketOrdersOpts, OptimizedMarketOrder } from './market_operation_utils/types';
|
import { GetMarketOrdersOpts, OptimizedMarketOrder } from './market_operation_utils/types';
|
||||||
import { ProtocolFeeUtils } from './protocol_fee_utils';
|
import { isSupportedAssetDataInOrders } from './utils';
|
||||||
import {
|
|
||||||
isOrderTakerFeePayableWithMakerAsset,
|
import { QuoteFillResult, simulateBestCaseFill, simulateWorstCaseFill } from './quote_simulation';
|
||||||
isOrderTakerFeePayableWithTakerAsset,
|
|
||||||
isSupportedAssetDataInOrders,
|
|
||||||
} from './utils';
|
|
||||||
|
|
||||||
// TODO(dave4506) How do we want to reintroduce InsufficientAssetLiquidityError?
|
// TODO(dave4506) How do we want to reintroduce InsufficientAssetLiquidityError?
|
||||||
export class SwapQuoteCalculator {
|
export class SwapQuoteCalculator {
|
||||||
private readonly _protocolFeeUtils: ProtocolFeeUtils;
|
|
||||||
private readonly _marketOperationUtils: MarketOperationUtils;
|
private readonly _marketOperationUtils: MarketOperationUtils;
|
||||||
|
|
||||||
constructor(protocolFeeUtils: ProtocolFeeUtils, marketOperationUtils: MarketOperationUtils) {
|
constructor(marketOperationUtils: MarketOperationUtils) {
|
||||||
this._protocolFeeUtils = protocolFeeUtils;
|
|
||||||
this._marketOperationUtils = marketOperationUtils;
|
this._marketOperationUtils = marketOperationUtils;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,7 +92,7 @@ export class SwapQuoteCalculator {
|
|||||||
batchSignedOrders.map(async (orders, i) => {
|
batchSignedOrders.map(async (orders, i) => {
|
||||||
if (orders) {
|
if (orders) {
|
||||||
const { makerAssetData, takerAssetData } = batchPrunedOrders[i][0];
|
const { makerAssetData, takerAssetData } = batchPrunedOrders[i][0];
|
||||||
return this._createSwapQuoteAsync(
|
return createSwapQuote(
|
||||||
makerAssetData,
|
makerAssetData,
|
||||||
takerAssetData,
|
takerAssetData,
|
||||||
orders,
|
orders,
|
||||||
@ -163,7 +156,7 @@ export class SwapQuoteCalculator {
|
|||||||
|
|
||||||
// assetData information for the result
|
// assetData information for the result
|
||||||
const { makerAssetData, takerAssetData } = prunedOrders[0];
|
const { makerAssetData, takerAssetData } = prunedOrders[0];
|
||||||
return this._createSwapQuoteAsync(
|
return createSwapQuote(
|
||||||
makerAssetData,
|
makerAssetData,
|
||||||
takerAssetData,
|
takerAssetData,
|
||||||
resultOrders,
|
resultOrders,
|
||||||
@ -173,7 +166,9 @@ export class SwapQuoteCalculator {
|
|||||||
opts.gasSchedule,
|
opts.gasSchedule,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
private async _createSwapQuoteAsync(
|
}
|
||||||
|
|
||||||
|
function createSwapQuote(
|
||||||
makerAssetData: string,
|
makerAssetData: string,
|
||||||
takerAssetData: string,
|
takerAssetData: string,
|
||||||
resultOrders: OptimizedMarketOrder[],
|
resultOrders: OptimizedMarketOrder[],
|
||||||
@ -181,34 +176,32 @@ export class SwapQuoteCalculator {
|
|||||||
assetFillAmount: BigNumber,
|
assetFillAmount: BigNumber,
|
||||||
gasPrice: BigNumber,
|
gasPrice: BigNumber,
|
||||||
gasSchedule: { [source: string]: number },
|
gasSchedule: { [source: string]: number },
|
||||||
): Promise<SwapQuote> {
|
): SwapQuote {
|
||||||
const bestCaseQuoteInfo = await this._calculateQuoteInfoAsync(
|
const bestCaseFillResult = simulateBestCaseFill({
|
||||||
resultOrders,
|
|
||||||
assetFillAmount,
|
|
||||||
gasPrice,
|
gasPrice,
|
||||||
gasSchedule,
|
orders: resultOrders,
|
||||||
operation,
|
side: operation,
|
||||||
);
|
fillAmount: assetFillAmount,
|
||||||
const worstCaseQuoteInfo = await this._calculateQuoteInfoAsync(
|
opts: { gasSchedule },
|
||||||
resultOrders,
|
});
|
||||||
assetFillAmount,
|
|
||||||
gasPrice,
|
|
||||||
gasSchedule,
|
|
||||||
operation,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
const breakdown = getSwapQuoteOrdersBreakdown(resultOrders, operation);
|
const worstCaseFillResult = simulateWorstCaseFill({
|
||||||
|
gasPrice,
|
||||||
|
orders: resultOrders,
|
||||||
|
side: operation,
|
||||||
|
fillAmount: assetFillAmount,
|
||||||
|
opts: { gasSchedule },
|
||||||
|
});
|
||||||
|
|
||||||
const quoteBase: SwapQuoteBase = {
|
const quoteBase: SwapQuoteBase = {
|
||||||
takerAssetData,
|
takerAssetData,
|
||||||
makerAssetData,
|
makerAssetData,
|
||||||
// Remove fill metadata.
|
|
||||||
orders: resultOrders.map(o => _.omit(o, 'fill')) as SignedOrderWithFillableAmounts[],
|
|
||||||
bestCaseQuoteInfo,
|
|
||||||
worstCaseQuoteInfo,
|
|
||||||
gasPrice,
|
gasPrice,
|
||||||
sourceBreakdown: breakdown,
|
bestCaseQuoteInfo: fillResultsToQuoteInfo(bestCaseFillResult),
|
||||||
|
worstCaseQuoteInfo: fillResultsToQuoteInfo(worstCaseFillResult),
|
||||||
|
sourceBreakdown: getSwapQuoteOrdersBreakdown(bestCaseFillResult.fillAmountBySource),
|
||||||
|
// Remove fill metadata.
|
||||||
|
orders: resultOrders.map(o => _.omit(o, 'fills')) as SignedOrderWithFillableAmounts[],
|
||||||
};
|
};
|
||||||
|
|
||||||
if (operation === MarketOperation.Buy) {
|
if (operation === MarketOperation.Buy) {
|
||||||
@ -226,271 +219,22 @@ export class SwapQuoteCalculator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// tslint:disable-next-line: prefer-function-over-method
|
function getSwapQuoteOrdersBreakdown(fillAmountBySource: { [source: string]: BigNumber }): SwapQuoteOrdersBreakdown {
|
||||||
private async _calculateQuoteInfoAsync(
|
const totalFillAmount = BigNumber.sum(...Object.values(fillAmountBySource));
|
||||||
orders: OptimizedMarketOrder[],
|
|
||||||
assetFillAmount: BigNumber,
|
|
||||||
gasPrice: BigNumber,
|
|
||||||
gasSchedule: { [source: string]: number },
|
|
||||||
operation: MarketOperation,
|
|
||||||
worstCase: boolean = false,
|
|
||||||
): Promise<SwapQuoteInfo> {
|
|
||||||
return {
|
|
||||||
...(operation === MarketOperation.Buy
|
|
||||||
? await this._calculateMarketBuyQuoteInfoAsync(orders, assetFillAmount, gasPrice, worstCase)
|
|
||||||
: await this._calculateMarketSellQuoteInfoAsync(orders, assetFillAmount, gasPrice, worstCase)),
|
|
||||||
gas: getGasUsedByOrders(orders, gasSchedule),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _calculateMarketSellQuoteInfoAsync(
|
|
||||||
orders: OptimizedMarketOrder[],
|
|
||||||
takerAssetSellAmount: BigNumber,
|
|
||||||
gasPrice: BigNumber,
|
|
||||||
worstCase: boolean = false,
|
|
||||||
): Promise<SwapQuoteInfo> {
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
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<SwapQuoteInfo> {
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
const breakdown: SwapQuoteOrdersBreakdown = {};
|
const breakdown: SwapQuoteOrdersBreakdown = {};
|
||||||
for (const [source, amount] of Object.entries(amountsBySource)) {
|
Object.entries(fillAmountBySource).forEach(([source, fillAmount]) => {
|
||||||
breakdown[source] = amount.div(totalAmount);
|
breakdown[source] = fillAmount.div(totalFillAmount);
|
||||||
}
|
});
|
||||||
return breakdown;
|
return breakdown;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTakerAssetAmountBreakDown(
|
function fillResultsToQuoteInfo(fr: QuoteFillResult): SwapQuoteInfo {
|
||||||
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 {
|
return {
|
||||||
takerAssetAmount,
|
makerAssetAmount: fr.totalMakerAssetAmount,
|
||||||
feeTakerAssetAmount: takerAssetAmountWithFees.minus(takerAssetAmount),
|
takerAssetAmount: fr.takerAssetAmount,
|
||||||
};
|
totalTakerAssetAmount: fr.totalTakerAssetAmount,
|
||||||
} else if (isOrderTakerFeePayableWithMakerAsset(order)) {
|
feeTakerAssetAmount: fr.takerFeeTakerAssetAmount,
|
||||||
if (takerAssetAmountWithFees.isZero()) {
|
protocolFeeInWeiAmount: fr.protocolFeeAmount,
|
||||||
return {
|
gas: fr.gas,
|
||||||
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),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
feeTakerAssetAmount: constants.ZERO_AMOUNT,
|
|
||||||
takerAssetAmount: takerAssetAmountWithFees,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
|
@ -12,7 +12,6 @@ import { SwapQuote } from '../src';
|
|||||||
import { constants } from '../src/constants';
|
import { constants } from '../src/constants';
|
||||||
import { ExchangeSwapQuoteConsumer } from '../src/quote_consumers/exchange_swap_quote_consumer';
|
import { ExchangeSwapQuoteConsumer } from '../src/quote_consumers/exchange_swap_quote_consumer';
|
||||||
import { MarketOperation, SignedOrderWithFillableAmounts } from '../src/types';
|
import { MarketOperation, SignedOrderWithFillableAmounts } from '../src/types';
|
||||||
import { ProtocolFeeUtils } from '../src/utils/protocol_fee_utils';
|
|
||||||
|
|
||||||
import { chaiSetup } from './utils/chai_setup';
|
import { chaiSetup } from './utils/chai_setup';
|
||||||
import { getFullyFillableSwapQuoteWithNoFeesAsync } from './utils/swap_quote';
|
import { getFullyFillableSwapQuoteWithNoFeesAsync } from './utils/swap_quote';
|
||||||
@ -60,7 +59,6 @@ const expectMakerAndTakerBalancesAsyncFactory = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
describe('ExchangeSwapQuoteConsumer', () => {
|
describe('ExchangeSwapQuoteConsumer', () => {
|
||||||
let protocolFeeUtils: ProtocolFeeUtils;
|
|
||||||
let userAddresses: string[];
|
let userAddresses: string[];
|
||||||
let erc20MakerTokenContract: ERC20TokenContract;
|
let erc20MakerTokenContract: ERC20TokenContract;
|
||||||
let erc20TakerTokenContract: ERC20TokenContract;
|
let erc20TakerTokenContract: ERC20TokenContract;
|
||||||
@ -123,7 +121,6 @@ describe('ExchangeSwapQuoteConsumer', () => {
|
|||||||
};
|
};
|
||||||
const privateKey = devConstants.TESTRPC_PRIVATE_KEYS[userAddresses.indexOf(makerAddress)];
|
const privateKey = devConstants.TESTRPC_PRIVATE_KEYS[userAddresses.indexOf(makerAddress)];
|
||||||
orderFactory = new OrderFactory(privateKey, defaultOrderParams);
|
orderFactory = new OrderFactory(privateKey, defaultOrderParams);
|
||||||
protocolFeeUtils = new ProtocolFeeUtils(constants.PROTOCOL_FEE_UTILS_POLLING_INTERVAL_IN_MS, new BigNumber(1));
|
|
||||||
expectMakerAndTakerBalancesForTakerAssetAsync = expectMakerAndTakerBalancesAsyncFactory(
|
expectMakerAndTakerBalancesForTakerAssetAsync = expectMakerAndTakerBalancesAsyncFactory(
|
||||||
erc20TakerTokenContract,
|
erc20TakerTokenContract,
|
||||||
makerAddress,
|
makerAddress,
|
||||||
@ -156,7 +153,6 @@ describe('ExchangeSwapQuoteConsumer', () => {
|
|||||||
orders,
|
orders,
|
||||||
MarketOperation.Sell,
|
MarketOperation.Sell,
|
||||||
GAS_PRICE,
|
GAS_PRICE,
|
||||||
protocolFeeUtils,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
marketBuySwapQuote = await getFullyFillableSwapQuoteWithNoFeesAsync(
|
marketBuySwapQuote = await getFullyFillableSwapQuoteWithNoFeesAsync(
|
||||||
@ -165,7 +161,6 @@ describe('ExchangeSwapQuoteConsumer', () => {
|
|||||||
orders,
|
orders,
|
||||||
MarketOperation.Buy,
|
MarketOperation.Buy,
|
||||||
GAS_PRICE,
|
GAS_PRICE,
|
||||||
protocolFeeUtils,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
swapQuoteConsumer = new ExchangeSwapQuoteConsumer(provider, contractAddresses, {
|
swapQuoteConsumer = new ExchangeSwapQuoteConsumer(provider, contractAddresses, {
|
||||||
|
@ -12,7 +12,6 @@ import { SwapQuote } from '../src';
|
|||||||
import { constants } from '../src/constants';
|
import { constants } from '../src/constants';
|
||||||
import { ForwarderSwapQuoteConsumer } from '../src/quote_consumers/forwarder_swap_quote_consumer';
|
import { ForwarderSwapQuoteConsumer } from '../src/quote_consumers/forwarder_swap_quote_consumer';
|
||||||
import { MarketOperation, SignedOrderWithFillableAmounts } from '../src/types';
|
import { MarketOperation, SignedOrderWithFillableAmounts } from '../src/types';
|
||||||
import { ProtocolFeeUtils } from '../src/utils/protocol_fee_utils';
|
|
||||||
|
|
||||||
import { chaiSetup } from './utils/chai_setup';
|
import { chaiSetup } from './utils/chai_setup';
|
||||||
import { getFullyFillableSwapQuoteWithNoFeesAsync } from './utils/swap_quote';
|
import { getFullyFillableSwapQuoteWithNoFeesAsync } from './utils/swap_quote';
|
||||||
@ -61,7 +60,6 @@ const expectMakerAndTakerBalancesAsyncFactory = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
describe('ForwarderSwapQuoteConsumer', () => {
|
describe('ForwarderSwapQuoteConsumer', () => {
|
||||||
let protocolFeeUtils: ProtocolFeeUtils;
|
|
||||||
let userAddresses: string[];
|
let userAddresses: string[];
|
||||||
let coinbaseAddress: string;
|
let coinbaseAddress: string;
|
||||||
let makerAddress: string;
|
let makerAddress: string;
|
||||||
@ -126,7 +124,6 @@ describe('ForwarderSwapQuoteConsumer', () => {
|
|||||||
};
|
};
|
||||||
const privateKey = devConstants.TESTRPC_PRIVATE_KEYS[userAddresses.indexOf(makerAddress)];
|
const privateKey = devConstants.TESTRPC_PRIVATE_KEYS[userAddresses.indexOf(makerAddress)];
|
||||||
orderFactory = new OrderFactory(privateKey, defaultOrderParams);
|
orderFactory = new OrderFactory(privateKey, defaultOrderParams);
|
||||||
protocolFeeUtils = new ProtocolFeeUtils(constants.PROTOCOL_FEE_UTILS_POLLING_INTERVAL_IN_MS, new BigNumber(1));
|
|
||||||
expectMakerAndTakerBalancesAsync = expectMakerAndTakerBalancesAsyncFactory(
|
expectMakerAndTakerBalancesAsync = expectMakerAndTakerBalancesAsyncFactory(
|
||||||
erc20TokenContract,
|
erc20TokenContract,
|
||||||
makerAddress,
|
makerAddress,
|
||||||
@ -179,7 +176,6 @@ describe('ForwarderSwapQuoteConsumer', () => {
|
|||||||
orders,
|
orders,
|
||||||
MarketOperation.Sell,
|
MarketOperation.Sell,
|
||||||
GAS_PRICE,
|
GAS_PRICE,
|
||||||
protocolFeeUtils,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
marketBuySwapQuote = await getFullyFillableSwapQuoteWithNoFeesAsync(
|
marketBuySwapQuote = await getFullyFillableSwapQuoteWithNoFeesAsync(
|
||||||
@ -188,7 +184,6 @@ describe('ForwarderSwapQuoteConsumer', () => {
|
|||||||
orders,
|
orders,
|
||||||
MarketOperation.Buy,
|
MarketOperation.Buy,
|
||||||
GAS_PRICE,
|
GAS_PRICE,
|
||||||
protocolFeeUtils,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
invalidMarketBuySwapQuote = await getFullyFillableSwapQuoteWithNoFeesAsync(
|
invalidMarketBuySwapQuote = await getFullyFillableSwapQuoteWithNoFeesAsync(
|
||||||
@ -197,7 +192,6 @@ describe('ForwarderSwapQuoteConsumer', () => {
|
|||||||
invalidOrders,
|
invalidOrders,
|
||||||
MarketOperation.Buy,
|
MarketOperation.Buy,
|
||||||
GAS_PRICE,
|
GAS_PRICE,
|
||||||
protocolFeeUtils,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
swapQuoteConsumer = new ForwarderSwapQuoteConsumer(provider, contractAddresses, {
|
swapQuoteConsumer = new ForwarderSwapQuoteConsumer(provider, contractAddresses, {
|
||||||
|
@ -299,6 +299,7 @@ describe('MarketOperationUtils tests', () => {
|
|||||||
maxFallbackSlippage: 100,
|
maxFallbackSlippage: 100,
|
||||||
excludedSources: Object.keys(DEFAULT_CURVE_OPTS) as ERC20BridgeSource[],
|
excludedSources: Object.keys(DEFAULT_CURVE_OPTS) as ERC20BridgeSource[],
|
||||||
allowFallback: false,
|
allowFallback: false,
|
||||||
|
shouldBatchBridgeOrders: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@ -422,7 +423,7 @@ describe('MarketOperationUtils tests', () => {
|
|||||||
);
|
);
|
||||||
expect(improvedOrders).to.not.be.length(0);
|
expect(improvedOrders).to.not.be.length(0);
|
||||||
for (const order of improvedOrders) {
|
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();
|
const slippage = 1 - order.makerAssetAmount.div(expectedMakerAmount.plus(1)).toNumber();
|
||||||
assertRoughlyEquals(slippage, bridgeSlippage, 1);
|
assertRoughlyEquals(slippage, bridgeSlippage, 1);
|
||||||
}
|
}
|
||||||
@ -442,7 +443,7 @@ describe('MarketOperationUtils tests', () => {
|
|||||||
FILL_AMOUNT,
|
FILL_AMOUNT,
|
||||||
{ ...DEFAULT_OPTS, numSamples: 4 },
|
{ ...DEFAULT_OPTS, numSamples: 4 },
|
||||||
);
|
);
|
||||||
const orderSources = improvedOrders.map(o => o.fill.source);
|
const orderSources = improvedOrders.map(o => o.fills[0].source);
|
||||||
const expectedSources = [
|
const expectedSources = [
|
||||||
ERC20BridgeSource.Eth2Dai,
|
ERC20BridgeSource.Eth2Dai,
|
||||||
ERC20BridgeSource.Uniswap,
|
ERC20BridgeSource.Uniswap,
|
||||||
@ -466,7 +467,7 @@ describe('MarketOperationUtils tests', () => {
|
|||||||
FILL_AMOUNT,
|
FILL_AMOUNT,
|
||||||
{ ...DEFAULT_OPTS, numSamples: 4 },
|
{ ...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)) {
|
if (orderSources.includes(ERC20BridgeSource.Kyber)) {
|
||||||
expect(orderSources).to.not.include(ERC20BridgeSource.Uniswap);
|
expect(orderSources).to.not.include(ERC20BridgeSource.Uniswap);
|
||||||
expect(orderSources).to.not.include(ERC20BridgeSource.Eth2Dai);
|
expect(orderSources).to.not.include(ERC20BridgeSource.Eth2Dai);
|
||||||
@ -501,7 +502,7 @@ describe('MarketOperationUtils tests', () => {
|
|||||||
FILL_AMOUNT,
|
FILL_AMOUNT,
|
||||||
{ ...DEFAULT_OPTS, numSamples: 4, feeSchedule },
|
{ ...DEFAULT_OPTS, numSamples: 4, feeSchedule },
|
||||||
);
|
);
|
||||||
const orderSources = improvedOrders.map(o => o.fill.source);
|
const orderSources = improvedOrders.map(o => o.fills[0].source);
|
||||||
const expectedSources = [
|
const expectedSources = [
|
||||||
ERC20BridgeSource.Native,
|
ERC20BridgeSource.Native,
|
||||||
ERC20BridgeSource.Uniswap,
|
ERC20BridgeSource.Uniswap,
|
||||||
@ -536,7 +537,7 @@ describe('MarketOperationUtils tests', () => {
|
|||||||
FILL_AMOUNT,
|
FILL_AMOUNT,
|
||||||
{ ...DEFAULT_OPTS, numSamples: 4, feeSchedule },
|
{ ...DEFAULT_OPTS, numSamples: 4, feeSchedule },
|
||||||
);
|
);
|
||||||
const orderSources = improvedOrders.map(o => o.fill.source);
|
const orderSources = improvedOrders.map(o => o.fills[0].source);
|
||||||
const expectedSources = [
|
const expectedSources = [
|
||||||
ERC20BridgeSource.Native,
|
ERC20BridgeSource.Native,
|
||||||
ERC20BridgeSource.Eth2Dai,
|
ERC20BridgeSource.Eth2Dai,
|
||||||
@ -561,7 +562,7 @@ describe('MarketOperationUtils tests', () => {
|
|||||||
FILL_AMOUNT,
|
FILL_AMOUNT,
|
||||||
{ ...DEFAULT_OPTS, numSamples: 4 },
|
{ ...DEFAULT_OPTS, numSamples: 4 },
|
||||||
);
|
);
|
||||||
const orderSources = improvedOrders.map(o => o.fill.source);
|
const orderSources = improvedOrders.map(o => o.fills[0].source);
|
||||||
const expectedSources = [
|
const expectedSources = [
|
||||||
ERC20BridgeSource.Eth2Dai,
|
ERC20BridgeSource.Eth2Dai,
|
||||||
ERC20BridgeSource.Uniswap,
|
ERC20BridgeSource.Uniswap,
|
||||||
@ -584,7 +585,7 @@ describe('MarketOperationUtils tests', () => {
|
|||||||
FILL_AMOUNT,
|
FILL_AMOUNT,
|
||||||
{ ...DEFAULT_OPTS, numSamples: 4, allowFallback: true },
|
{ ...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 = [
|
const firstSources = [
|
||||||
ERC20BridgeSource.Native,
|
ERC20BridgeSource.Native,
|
||||||
ERC20BridgeSource.Native,
|
ERC20BridgeSource.Native,
|
||||||
@ -610,7 +611,7 @@ describe('MarketOperationUtils tests', () => {
|
|||||||
FILL_AMOUNT,
|
FILL_AMOUNT,
|
||||||
{ ...DEFAULT_OPTS, numSamples: 4, allowFallback: true, maxFallbackSlippage: 0.5 },
|
{ ...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 firstSources = [ERC20BridgeSource.Native, ERC20BridgeSource.Native, ERC20BridgeSource.Uniswap];
|
||||||
const secondSources: ERC20BridgeSource[] = [];
|
const secondSources: ERC20BridgeSource[] = [];
|
||||||
expect(orderSources.slice(0, firstSources.length).sort()).to.deep.eq(firstSources.sort());
|
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.makerToken).is.eql(yAsset);
|
||||||
expect(getLiquidityProviderParams.takerToken).is.eql(xAsset);
|
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()', () => {
|
describe('getMarketBuyOrdersAsync()', () => {
|
||||||
@ -687,6 +719,7 @@ describe('MarketOperationUtils tests', () => {
|
|||||||
maxFallbackSlippage: 100,
|
maxFallbackSlippage: 100,
|
||||||
excludedSources: Object.keys(DEFAULT_CURVE_OPTS) as ERC20BridgeSource[],
|
excludedSources: Object.keys(DEFAULT_CURVE_OPTS) as ERC20BridgeSource[],
|
||||||
allowFallback: false,
|
allowFallback: false,
|
||||||
|
shouldBatchBridgeOrders: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
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(
|
const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync(
|
||||||
// Pass in empty orders to prevent native orders from being used.
|
// Pass in empty orders to prevent native orders from being used.
|
||||||
ORDERS.map(o => ({ ...o, makerAssetAmount: constants.ZERO_AMOUNT })),
|
ORDERS.map(o => ({ ...o, makerAssetAmount: constants.ZERO_AMOUNT })),
|
||||||
@ -810,7 +843,7 @@ describe('MarketOperationUtils tests', () => {
|
|||||||
);
|
);
|
||||||
expect(improvedOrders).to.not.be.length(0);
|
expect(improvedOrders).to.not.be.length(0);
|
||||||
for (const order of improvedOrders) {
|
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;
|
const slippage = order.takerAssetAmount.div(expectedTakerAmount.plus(1)).toNumber() - 1;
|
||||||
assertRoughlyEquals(slippage, bridgeSlippage, 1);
|
assertRoughlyEquals(slippage, bridgeSlippage, 1);
|
||||||
}
|
}
|
||||||
@ -829,7 +862,7 @@ describe('MarketOperationUtils tests', () => {
|
|||||||
FILL_AMOUNT,
|
FILL_AMOUNT,
|
||||||
{ ...DEFAULT_OPTS, numSamples: 4 },
|
{ ...DEFAULT_OPTS, numSamples: 4 },
|
||||||
);
|
);
|
||||||
const orderSources = improvedOrders.map(o => o.fill.source);
|
const orderSources = improvedOrders.map(o => o.fills[0].source);
|
||||||
const expectedSources = [
|
const expectedSources = [
|
||||||
ERC20BridgeSource.Eth2Dai,
|
ERC20BridgeSource.Eth2Dai,
|
||||||
ERC20BridgeSource.Uniswap,
|
ERC20BridgeSource.Uniswap,
|
||||||
@ -865,7 +898,7 @@ describe('MarketOperationUtils tests', () => {
|
|||||||
FILL_AMOUNT,
|
FILL_AMOUNT,
|
||||||
{ ...DEFAULT_OPTS, numSamples: 4, feeSchedule },
|
{ ...DEFAULT_OPTS, numSamples: 4, feeSchedule },
|
||||||
);
|
);
|
||||||
const orderSources = improvedOrders.map(o => o.fill.source);
|
const orderSources = improvedOrders.map(o => o.fills[0].source);
|
||||||
const expectedSources = [
|
const expectedSources = [
|
||||||
ERC20BridgeSource.Uniswap,
|
ERC20BridgeSource.Uniswap,
|
||||||
ERC20BridgeSource.Eth2Dai,
|
ERC20BridgeSource.Eth2Dai,
|
||||||
@ -899,7 +932,7 @@ describe('MarketOperationUtils tests', () => {
|
|||||||
FILL_AMOUNT,
|
FILL_AMOUNT,
|
||||||
{ ...DEFAULT_OPTS, numSamples: 4, feeSchedule },
|
{ ...DEFAULT_OPTS, numSamples: 4, feeSchedule },
|
||||||
);
|
);
|
||||||
const orderSources = improvedOrders.map(o => o.fill.source);
|
const orderSources = improvedOrders.map(o => o.fills[0].source);
|
||||||
const expectedSources = [
|
const expectedSources = [
|
||||||
ERC20BridgeSource.Native,
|
ERC20BridgeSource.Native,
|
||||||
ERC20BridgeSource.Eth2Dai,
|
ERC20BridgeSource.Eth2Dai,
|
||||||
@ -921,7 +954,7 @@ describe('MarketOperationUtils tests', () => {
|
|||||||
FILL_AMOUNT,
|
FILL_AMOUNT,
|
||||||
{ ...DEFAULT_OPTS, numSamples: 4, allowFallback: true },
|
{ ...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 = [
|
const firstSources = [
|
||||||
ERC20BridgeSource.Native,
|
ERC20BridgeSource.Native,
|
||||||
ERC20BridgeSource.Native,
|
ERC20BridgeSource.Native,
|
||||||
@ -946,12 +979,37 @@ describe('MarketOperationUtils tests', () => {
|
|||||||
FILL_AMOUNT,
|
FILL_AMOUNT,
|
||||||
{ ...DEFAULT_OPTS, numSamples: 4, allowFallback: true, maxFallbackSlippage: 0.5 },
|
{ ...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 firstSources = [ERC20BridgeSource.Native, ERC20BridgeSource.Native, ERC20BridgeSource.Uniswap];
|
||||||
const secondSources: ERC20BridgeSource[] = [];
|
const secondSources: ERC20BridgeSource[] = [];
|
||||||
expect(orderSources.slice(0, firstSources.length).sort()).to.deep.eq(firstSources.sort());
|
expect(orderSources.slice(0, firstSources.length).sort()).to.deep.eq(firstSources.sort());
|
||||||
expect(orderSources.slice(firstSources.length).sort()).to.deep.eq(secondSources.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],
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
948
packages/asset-swapper/test/quote_simulation_test.ts
Normal file
948
packages/asset-swapper/test/quote_simulation_test.ts
Normal file
@ -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
|
@ -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),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
*/
|
|
@ -11,7 +11,6 @@ import 'mocha';
|
|||||||
import { SwapQuote, SwapQuoteConsumer } from '../src';
|
import { SwapQuote, SwapQuoteConsumer } from '../src';
|
||||||
import { constants } from '../src/constants';
|
import { constants } from '../src/constants';
|
||||||
import { ExtensionContractType, MarketOperation, SignedOrderWithFillableAmounts } from '../src/types';
|
import { ExtensionContractType, MarketOperation, SignedOrderWithFillableAmounts } from '../src/types';
|
||||||
import { ProtocolFeeUtils } from '../src/utils/protocol_fee_utils';
|
|
||||||
|
|
||||||
import { chaiSetup } from './utils/chai_setup';
|
import { chaiSetup } from './utils/chai_setup';
|
||||||
import { getFullyFillableSwapQuoteWithNoFeesAsync } from './utils/swap_quote';
|
import { getFullyFillableSwapQuoteWithNoFeesAsync } from './utils/swap_quote';
|
||||||
@ -69,7 +68,6 @@ const PARTIAL_LARGE_PRUNED_SIGNED_ORDERS: Array<Partial<SignedOrderWithFillableA
|
|||||||
|
|
||||||
describe('swapQuoteConsumerUtils', () => {
|
describe('swapQuoteConsumerUtils', () => {
|
||||||
let wethContract: WETH9Contract;
|
let wethContract: WETH9Contract;
|
||||||
let protocolFeeUtils: ProtocolFeeUtils;
|
|
||||||
let userAddresses: string[];
|
let userAddresses: string[];
|
||||||
let makerAddress: string;
|
let makerAddress: string;
|
||||||
let takerAddress: string;
|
let takerAddress: string;
|
||||||
@ -119,7 +117,6 @@ describe('swapQuoteConsumerUtils', () => {
|
|||||||
};
|
};
|
||||||
const privateKey = devConstants.TESTRPC_PRIVATE_KEYS[userAddresses.indexOf(makerAddress)];
|
const privateKey = devConstants.TESTRPC_PRIVATE_KEYS[userAddresses.indexOf(makerAddress)];
|
||||||
orderFactory = new OrderFactory(privateKey, defaultOrderParams);
|
orderFactory = new OrderFactory(privateKey, defaultOrderParams);
|
||||||
protocolFeeUtils = new ProtocolFeeUtils(constants.PROTOCOL_FEE_UTILS_POLLING_INTERVAL_IN_MS, new BigNumber(1));
|
|
||||||
forwarderOrderFactory = new OrderFactory(privateKey, defaultForwarderOrderParams);
|
forwarderOrderFactory = new OrderFactory(privateKey, defaultForwarderOrderParams);
|
||||||
|
|
||||||
swapQuoteConsumer = new SwapQuoteConsumer(provider, {
|
swapQuoteConsumer = new SwapQuoteConsumer(provider, {
|
||||||
@ -128,7 +125,6 @@ describe('swapQuoteConsumerUtils', () => {
|
|||||||
});
|
});
|
||||||
after(async () => {
|
after(async () => {
|
||||||
await blockchainLifecycle.revertAsync();
|
await blockchainLifecycle.revertAsync();
|
||||||
await protocolFeeUtils.destroyAsync();
|
|
||||||
});
|
});
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await blockchainLifecycle.startAsync();
|
await blockchainLifecycle.startAsync();
|
||||||
@ -182,7 +178,6 @@ describe('swapQuoteConsumerUtils', () => {
|
|||||||
forwarderOrders,
|
forwarderOrders,
|
||||||
MarketOperation.Sell,
|
MarketOperation.Sell,
|
||||||
GAS_PRICE,
|
GAS_PRICE,
|
||||||
protocolFeeUtils,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
largeForwarderSwapQuote = await getFullyFillableSwapQuoteWithNoFeesAsync(
|
largeForwarderSwapQuote = await getFullyFillableSwapQuoteWithNoFeesAsync(
|
||||||
@ -191,7 +186,6 @@ describe('swapQuoteConsumerUtils', () => {
|
|||||||
largeForwarderOrders,
|
largeForwarderOrders,
|
||||||
MarketOperation.Sell,
|
MarketOperation.Sell,
|
||||||
GAS_PRICE,
|
GAS_PRICE,
|
||||||
protocolFeeUtils,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
exchangeSwapQuote = await getFullyFillableSwapQuoteWithNoFeesAsync(
|
exchangeSwapQuote = await getFullyFillableSwapQuoteWithNoFeesAsync(
|
||||||
@ -200,7 +194,6 @@ describe('swapQuoteConsumerUtils', () => {
|
|||||||
exchangeOrders,
|
exchangeOrders,
|
||||||
MarketOperation.Sell,
|
MarketOperation.Sell,
|
||||||
GAS_PRICE,
|
GAS_PRICE,
|
||||||
protocolFeeUtils,
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -9,8 +9,6 @@ import { SwapQuoter } from '../../src/swap_quoter';
|
|||||||
import { SignedOrderWithFillableAmounts } from '../../src/types';
|
import { SignedOrderWithFillableAmounts } from '../../src/types';
|
||||||
import { ProtocolFeeUtils } from '../../src/utils/protocol_fee_utils';
|
import { ProtocolFeeUtils } from '../../src/utils/protocol_fee_utils';
|
||||||
|
|
||||||
const PROTOCOL_FEE_MULTIPLIER = 150000;
|
|
||||||
|
|
||||||
// tslint:disable: max-classes-per-file
|
// tslint:disable: max-classes-per-file
|
||||||
|
|
||||||
class OrderbookClass extends Orderbook {
|
class OrderbookClass extends Orderbook {
|
||||||
@ -57,10 +55,6 @@ const partiallyMockedSwapQuoter = (provider: Web3ProviderEngine, orderbook: Orde
|
|||||||
};
|
};
|
||||||
|
|
||||||
class ProtocolFeeUtilsClass extends ProtocolFeeUtils {
|
class ProtocolFeeUtilsClass extends ProtocolFeeUtils {
|
||||||
// tslint:disable-next-line:prefer-function-over-method
|
|
||||||
public async getProtocolFeeMultiplierAsync(): Promise<BigNumber> {
|
|
||||||
return new BigNumber(PROTOCOL_FEE_MULTIPLIER);
|
|
||||||
}
|
|
||||||
// tslint:disable-next-line:prefer-function-over-method
|
// tslint:disable-next-line:prefer-function-over-method
|
||||||
public async getGasPriceEstimationOrThrowAsync(_shouldHardRefresh?: boolean): Promise<BigNumber> {
|
public async getGasPriceEstimationOrThrowAsync(_shouldHardRefresh?: boolean): Promise<BigNumber> {
|
||||||
return new BigNumber(devConstants.DEFAULT_GAS_PRICE);
|
return new BigNumber(devConstants.DEFAULT_GAS_PRICE);
|
||||||
|
@ -4,7 +4,6 @@ import * as _ from 'lodash';
|
|||||||
import { ERC20BridgeSource } from '../../src';
|
import { ERC20BridgeSource } from '../../src';
|
||||||
import { constants } from '../../src/constants';
|
import { constants } from '../../src/constants';
|
||||||
import { MarketOperation, SignedOrderWithFillableAmounts, SwapQuote } from '../../src/types';
|
import { MarketOperation, SignedOrderWithFillableAmounts, SwapQuote } from '../../src/types';
|
||||||
import { ProtocolFeeUtils } from '../../src/utils/protocol_fee_utils';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a swap quote given orders.
|
* Creates a swap quote given orders.
|
||||||
@ -15,16 +14,16 @@ export async function getFullyFillableSwapQuoteWithNoFeesAsync(
|
|||||||
orders: SignedOrderWithFillableAmounts[],
|
orders: SignedOrderWithFillableAmounts[],
|
||||||
operation: MarketOperation,
|
operation: MarketOperation,
|
||||||
gasPrice: BigNumber,
|
gasPrice: BigNumber,
|
||||||
protocolFeeUtils: ProtocolFeeUtils,
|
|
||||||
): Promise<SwapQuote> {
|
): Promise<SwapQuote> {
|
||||||
const makerAssetFillAmount = BigNumber.sum(...[0, ...orders.map(o => o.makerAssetAmount)]);
|
const makerAssetFillAmount = BigNumber.sum(...[0, ...orders.map(o => o.makerAssetAmount)]);
|
||||||
const totalTakerAssetAmount = BigNumber.sum(...[0, ...orders.map(o => o.takerAssetAmount)]);
|
const totalTakerAssetAmount = BigNumber.sum(...[0, ...orders.map(o => o.takerAssetAmount)]);
|
||||||
|
const protocolFeePerOrder = constants.PROTOCOL_FEE_MULTIPLIER.times(gasPrice);
|
||||||
const quoteInfo = {
|
const quoteInfo = {
|
||||||
makerAssetAmount: makerAssetFillAmount,
|
makerAssetAmount: makerAssetFillAmount,
|
||||||
feeTakerAssetAmount: constants.ZERO_AMOUNT,
|
feeTakerAssetAmount: constants.ZERO_AMOUNT,
|
||||||
takerAssetAmount: totalTakerAssetAmount,
|
takerAssetAmount: totalTakerAssetAmount,
|
||||||
totalTakerAssetAmount,
|
totalTakerAssetAmount,
|
||||||
protocolFeeInWeiAmount: await protocolFeeUtils.calculateWorstCaseProtocolFeeAsync(orders, gasPrice),
|
protocolFeeInWeiAmount: protocolFeePerOrder.times(orders.length),
|
||||||
gas: 200e3,
|
gas: 200e3,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -29,6 +29,14 @@
|
|||||||
{
|
{
|
||||||
"note": "Redeploy `Forwarder` on all networks",
|
"note": "Redeploy `Forwarder` on all networks",
|
||||||
"pr": 2521
|
"pr": 2521
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"note": "Redeploy `DexForwarderBridge` on Mainnet with Gas Token freeing",
|
||||||
|
"pr": 2536
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"note": "Revert to older Curve Bridge (without Gas Tokens)",
|
||||||
|
"pr": 2536
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -28,9 +28,9 @@
|
|||||||
"godsUnchainedValidator": "0x09a379ef7218bcfd8913faa8b281ebc5a2e0bc04",
|
"godsUnchainedValidator": "0x09a379ef7218bcfd8913faa8b281ebc5a2e0bc04",
|
||||||
"broker": "0xd4690a51044db77d91d7aa8f7a3a5ad5da331af0",
|
"broker": "0xd4690a51044db77d91d7aa8f7a3a5ad5da331af0",
|
||||||
"chainlinkStopLimit": "0xeb27220f95f364e1d9531992c48613f231839f53",
|
"chainlinkStopLimit": "0xeb27220f95f364e1d9531992c48613f231839f53",
|
||||||
"curveBridge": "0x1cf6ccc7e15d0d99a9498f37e16ba65b5c54bdd0",
|
"curveBridge": "0x6dc7950423ada9f56fb2c93a23edb787f1e29088",
|
||||||
"maximumGasPrice": "0xe2bfd35306495d11e3c9db0d8de390cda24563cf",
|
"maximumGasPrice": "0xe2bfd35306495d11e3c9db0d8de390cda24563cf",
|
||||||
"dexForwarderBridge": "0xa96844087062acf8556ca06a27702c6d19f87e57"
|
"dexForwarderBridge": "0x5591360f8c7640fea5771c9682d6b5ecb776e1f8"
|
||||||
},
|
},
|
||||||
"3": {
|
"3": {
|
||||||
"erc20Proxy": "0xb1408f4c245a23c31b98d2c626777d4c0d766caa",
|
"erc20Proxy": "0xb1408f4c245a23c31b98d2c626777d4c0d766caa",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user