Feature/bunny hop (#2647)

* `@0x/contracts-erc20-bridge-sampler`: Add TwoHopSampler + refactor

* `@0x/asset-swapper`: Refactor + add two-hop skeleton

* Round out two-hop support in asset-swapper

* Add BalancerSampler, use it for two-hop quotes

* Fix bugs discovered from simbot

* rebases are hard

* Add intermediate token to MultiHop source breakdown

* Fix market buy bugs

* Use hybrid on-chain/off-chain sampling for Balancer

* Another day, another rebase

* Update changelogs

* Address PR feedback, CI fixes

* Address more PR feedback
This commit is contained in:
mzhu25 2020-08-26 15:20:09 -07:00 committed by GitHub
parent 4f78f55c2a
commit bab34c2d21
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
68 changed files with 2714 additions and 2078 deletions

View File

@ -11,6 +11,7 @@
},
"scripts": {
"build": "yarn pre_build && tsc -b",
"build:ts": "tsc -b",
"build:ci": "yarn build",
"pre_build": "run-s compile contracts:gen generate_contract_wrappers contracts:copy",
"test": "yarn run_mocha",

View File

@ -97,7 +97,7 @@ export function MakerMixin<TBase extends Constructor>(Base: TBase): TBase & Cons
const [makerToken, makerFeeToken, takerToken, takerFeeToken] = Pseudorandom.sampleSize(
this.actor.deployment.tokens.erc20,
4, // tslint:disable-line:custom-no-magic-numbers
);
)!;
// Maker and taker set balances/allowances to guarantee that the fill succeeds.
// Amounts are chosen to be within each actor's balance (divided by 8, in case
@ -146,7 +146,7 @@ export function MakerMixin<TBase extends Constructor>(Base: TBase): TBase & Cons
const [leftMakerToken, leftTakerToken, makerFeeToken, takerFeeToken] = Pseudorandom.sampleSize(
this.actor.deployment.tokens.erc20,
4, // tslint:disable-line:custom-no-magic-numbers
);
)!;
const rightMakerToken = leftTakerToken;
const rightTakerToken = leftMakerToken;

View File

@ -144,7 +144,7 @@ function _getParameterNames(func: (...args: any[]) => any): string[] {
.replace(/[/][/].*$/gm, '') // strip single-line comments
.replace(/\s+/g, '') // strip white space
.replace(/[/][*][^/*]*[*][/]/g, '') // strip multi-line comments
.split('){', 1)[0]
.split(/\){|\)=>/, 1)[0]
.replace(/^[^(]*[(]/, '') // extract the parameters
.replace(/=[^,]+/g, '') // strip any ES6 defaults
.split(',')

View File

@ -61,6 +61,18 @@
{
"note": "Added `Mooniswap`",
"pr": 2675
},
{
"note": "Added two-hop support",
"pr": 2647
},
{
"note": "Move ERC20BridgeSampler interfaces into `interfaces` directory",
"pr": 2647
},
{
"note": "Use on-chain sampling (sometimes) for Balancer",
"pr": 2647
}
]
},

View File

@ -0,0 +1,143 @@
/*
Copyright 2019 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.5.9;
pragma experimental ABIEncoderV2;
import "./interfaces/IBalancer.sol";
contract BalancerSampler {
/// @dev Base gas limit for Balancer calls.
uint256 constant private BALANCER_CALL_GAS = 300e3; // 300k
struct BalancerState {
uint256 takerTokenBalance;
uint256 makerTokenBalance;
uint256 takerTokenWeight;
uint256 makerTokenWeight;
uint256 swapFee;
}
/// @dev Sample sell quotes from Balancer.
/// @param poolAddress Address of the Balancer pool to query.
/// @param takerToken Address of the taker token (what to sell).
/// @param makerToken Address of the maker token (what to buy).
/// @param takerTokenAmounts Taker token sell amount for each sample.
/// @return makerTokenAmounts Maker amounts bought at each taker token
/// amount.
function sampleSellsFromBalancer(
address poolAddress,
address takerToken,
address makerToken,
uint256[] memory takerTokenAmounts
)
public
view
returns (uint256[] memory makerTokenAmounts)
{
IBalancer pool = IBalancer(poolAddress);
uint256 numSamples = takerTokenAmounts.length;
makerTokenAmounts = new uint256[](numSamples);
if (!pool.isBound(takerToken) || !pool.isBound(makerToken)) {
return makerTokenAmounts;
}
BalancerState memory poolState;
poolState.takerTokenBalance = pool.getBalance(takerToken);
poolState.makerTokenBalance = pool.getBalance(makerToken);
poolState.takerTokenWeight = pool.getDenormalizedWeight(takerToken);
poolState.makerTokenWeight = pool.getDenormalizedWeight(makerToken);
poolState.swapFee = pool.getSwapFee();
for (uint256 i = 0; i < numSamples; i++) {
(bool didSucceed, bytes memory resultData) =
poolAddress.staticcall.gas(BALANCER_CALL_GAS)(
abi.encodeWithSelector(
pool.calcOutGivenIn.selector,
poolState.takerTokenBalance,
poolState.takerTokenWeight,
poolState.makerTokenBalance,
poolState.makerTokenWeight,
takerTokenAmounts[i],
poolState.swapFee
));
uint256 buyAmount = 0;
if (didSucceed) {
buyAmount = abi.decode(resultData, (uint256));
} else {
break;
}
makerTokenAmounts[i] = buyAmount;
}
}
/// @dev Sample buy quotes from Balancer.
/// @param poolAddress Address of the Balancer pool to query.
/// @param takerToken Address of the taker token (what to sell).
/// @param makerToken Address of the maker token (what to buy).
/// @param makerTokenAmounts Maker token buy amount for each sample.
/// @return takerTokenAmounts Taker amounts sold at each maker token
/// amount.
function sampleBuysFromBalancer(
address poolAddress,
address takerToken,
address makerToken,
uint256[] memory makerTokenAmounts
)
public
view
returns (uint256[] memory takerTokenAmounts)
{
IBalancer pool = IBalancer(poolAddress);
uint256 numSamples = makerTokenAmounts.length;
takerTokenAmounts = new uint256[](numSamples);
if (!pool.isBound(takerToken) || !pool.isBound(makerToken)) {
return takerTokenAmounts;
}
BalancerState memory poolState;
poolState.takerTokenBalance = pool.getBalance(takerToken);
poolState.makerTokenBalance = pool.getBalance(makerToken);
poolState.takerTokenWeight = pool.getDenormalizedWeight(takerToken);
poolState.makerTokenWeight = pool.getDenormalizedWeight(makerToken);
poolState.swapFee = pool.getSwapFee();
for (uint256 i = 0; i < numSamples; i++) {
(bool didSucceed, bytes memory resultData) =
poolAddress.staticcall.gas(BALANCER_CALL_GAS)(
abi.encodeWithSelector(
pool.calcInGivenOut.selector,
poolState.takerTokenBalance,
poolState.takerTokenWeight,
poolState.makerTokenBalance,
poolState.makerTokenWeight,
makerTokenAmounts[i],
poolState.swapFee
));
uint256 sellAmount = 0;
if (didSucceed) {
sellAmount = abi.decode(resultData, (uint256));
} else {
break;
}
takerTokenAmounts[i] = sellAmount;
}
}
}

View File

@ -19,7 +19,7 @@
pragma solidity ^0.5.9;
pragma experimental ABIEncoderV2;
import "./ICurve.sol";
import "./interfaces/ICurve.sol";
import "./ApproximateBuys.sol";
import "./SamplerUtils.sol";

View File

@ -19,6 +19,7 @@
pragma solidity ^0.5.9;
pragma experimental ABIEncoderV2;
import "./BalancerSampler.sol";
import "./CurveSampler.sol";
import "./Eth2DaiSampler.sol";
import "./KyberSampler.sol";
@ -29,9 +30,11 @@ import "./MooniswapSampler.sol";
import "./NativeOrderSampler.sol";
import "./UniswapSampler.sol";
import "./UniswapV2Sampler.sol";
import "./TwoHopSampler.sol";
contract ERC20BridgeSampler is
BalancerSampler,
CurveSampler,
Eth2DaiSampler,
KyberSampler,
@ -40,6 +43,7 @@ contract ERC20BridgeSampler is
MooniswapSampler,
MultiBridgeSampler,
NativeOrderSampler,
TwoHopSampler,
UniswapSampler,
UniswapV2Sampler
{

View File

@ -20,7 +20,7 @@ pragma solidity ^0.5.9;
pragma experimental ABIEncoderV2;
import "@0x/contracts-utils/contracts/src/DeploymentConstants.sol";
import "./IEth2Dai.sol";
import "./interfaces/IEth2Dai.sol";
import "./SamplerUtils.sol";

View File

@ -20,10 +20,10 @@ pragma solidity ^0.5.9;
pragma experimental ABIEncoderV2;
import "@0x/contracts-utils/contracts/src/DeploymentConstants.sol";
import "./IKyberNetwork.sol";
import "./IKyberNetworkProxy.sol";
import "./IKyberStorage.sol";
import "./IKyberHintHandler.sol";
import "./interfaces/IKyberNetwork.sol";
import "./interfaces/IKyberNetworkProxy.sol";
import "./interfaces/IKyberStorage.sol";
import "./interfaces/IKyberHintHandler.sol";
import "./ApproximateBuys.sol";
import "./SamplerUtils.sol";

View File

@ -20,8 +20,8 @@ pragma solidity ^0.5.9;
pragma experimental ABIEncoderV2;
import "@0x/contracts-utils/contracts/src/LibBytes.sol";
import "./ILiquidityProvider.sol";
import "./ILiquidityProviderRegistry.sol";
import "./interfaces/ILiquidityProvider.sol";
import "./interfaces/ILiquidityProviderRegistry.sol";
import "./ApproximateBuys.sol";
import "./SamplerUtils.sol";
@ -48,21 +48,21 @@ contract LiquidityProviderSampler is
)
public
view
returns (uint256[] memory makerTokenAmounts)
returns (uint256[] memory makerTokenAmounts, address providerAddress)
{
// Initialize array of maker token amounts.
uint256 numSamples = takerTokenAmounts.length;
makerTokenAmounts = new uint256[](numSamples);
// Query registry for provider address.
address providerAddress = getLiquidityProviderFromRegistry(
providerAddress = _getLiquidityProviderFromRegistry(
registryAddress,
takerToken,
makerToken
);
// If provider doesn't exist, return all zeros.
if (providerAddress == address(0)) {
return makerTokenAmounts;
return (makerTokenAmounts, providerAddress);
}
for (uint256 i = 0; i < numSamples; i++) {
@ -101,9 +101,14 @@ contract LiquidityProviderSampler is
)
public
view
returns (uint256[] memory takerTokenAmounts)
returns (uint256[] memory takerTokenAmounts, address providerAddress)
{
return _sampleApproximateBuys(
providerAddress = _getLiquidityProviderFromRegistry(
registryAddress,
takerToken,
makerToken
);
takerTokenAmounts = _sampleApproximateBuys(
ApproximateBuyQuoteOpts({
makerTokenData: abi.encode(makerToken, registryAddress),
takerTokenData: abi.encode(takerToken, registryAddress),
@ -119,12 +124,12 @@ contract LiquidityProviderSampler is
/// @param takerToken Taker asset managed by liquidity provider.
/// @param makerToken Maker asset managed by liquidity provider.
/// @return providerAddress Address of the liquidity provider.
function getLiquidityProviderFromRegistry(
function _getLiquidityProviderFromRegistry(
address registryAddress,
address takerToken,
address makerToken
)
public
private
view
returns (address providerAddress)
{
@ -167,6 +172,7 @@ contract LiquidityProviderSampler is
return 0;
}
// solhint-disable-next-line indent
return abi.decode(resultData, (uint256[]))[0];
(uint256[] memory amounts, ) = abi.decode(resultData, (uint256[], address));
return amounts[0];
}
}

View File

@ -21,7 +21,7 @@ pragma experimental ABIEncoderV2;
import "@0x/contracts-utils/contracts/src/DeploymentConstants.sol";
import "@0x/contracts-utils/contracts/src/LibBytes.sol";
import "./IMStable.sol";
import "./interfaces/IMStable.sol";
import "./ApproximateBuys.sol";
import "./SamplerUtils.sol";

View File

@ -19,7 +19,7 @@
pragma solidity ^0.5.9;
pragma experimental ABIEncoderV2;
import "./IMultiBridge.sol";
import "./interfaces/IMultiBridge.sol";
contract MultiBridgeSampler {

View File

@ -0,0 +1,125 @@
/*
Copyright 2019 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.5.9;
pragma experimental ABIEncoderV2;
import "@0x/contracts-utils/contracts/src/LibBytes.sol";
contract TwoHopSampler {
using LibBytes for bytes;
struct HopInfo {
uint256 sourceIndex;
bytes returnData;
}
function sampleTwoHopSell(
bytes[] memory firstHopCalls,
bytes[] memory secondHopCalls,
uint256 sellAmount
)
public
view
returns (
HopInfo memory firstHop,
HopInfo memory secondHop,
uint256 buyAmount
)
{
uint256 intermediateAssetAmount = 0;
for (uint256 i = 0; i != firstHopCalls.length; ++i) {
firstHopCalls[i].writeUint256(firstHopCalls[i].length - 32, sellAmount);
(bool didSucceed, bytes memory returnData) = address(this).staticcall(firstHopCalls[i]);
if (didSucceed) {
uint256 amount = returnData.readUint256(returnData.length - 32);
if (amount > intermediateAssetAmount) {
intermediateAssetAmount = amount;
firstHop.sourceIndex = i;
firstHop.returnData = returnData;
}
}
}
if (intermediateAssetAmount == 0) {
return (firstHop, secondHop, buyAmount);
}
for (uint256 j = 0; j != secondHopCalls.length; ++j) {
secondHopCalls[j].writeUint256(secondHopCalls[j].length - 32, intermediateAssetAmount);
(bool didSucceed, bytes memory returnData) = address(this).staticcall(secondHopCalls[j]);
if (didSucceed) {
uint256 amount = returnData.readUint256(returnData.length - 32);
if (amount > buyAmount) {
buyAmount = amount;
secondHop.sourceIndex = j;
secondHop.returnData = returnData;
}
}
}
}
function sampleTwoHopBuy(
bytes[] memory firstHopCalls,
bytes[] memory secondHopCalls,
uint256 buyAmount
)
public
view
returns (
HopInfo memory firstHop,
HopInfo memory secondHop,
uint256 sellAmount
)
{
sellAmount = uint256(-1);
uint256 intermediateAssetAmount = uint256(-1);
for (uint256 j = 0; j != secondHopCalls.length; ++j) {
secondHopCalls[j].writeUint256(secondHopCalls[j].length - 32, buyAmount);
(bool didSucceed, bytes memory returnData) = address(this).staticcall(secondHopCalls[j]);
if (didSucceed) {
uint256 amount = returnData.readUint256(returnData.length - 32);
if (
amount > 0 &&
amount < intermediateAssetAmount
) {
intermediateAssetAmount = amount;
secondHop.sourceIndex = j;
secondHop.returnData = returnData;
}
}
}
if (intermediateAssetAmount == uint256(-1)) {
return (firstHop, secondHop, sellAmount);
}
for (uint256 i = 0; i != firstHopCalls.length; ++i) {
firstHopCalls[i].writeUint256(firstHopCalls[i].length - 32, intermediateAssetAmount);
(bool didSucceed, bytes memory returnData) = address(this).staticcall(firstHopCalls[i]);
if (didSucceed) {
uint256 amount = returnData.readUint256(returnData.length - 32);
if (
amount > 0 &&
amount < sellAmount
) {
sellAmount = amount;
firstHop.sourceIndex = i;
firstHop.returnData = returnData;
}
}
}
}
}

View File

@ -25,7 +25,7 @@ import "@0x/contracts-exchange-libs/contracts/src/LibOrder.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 "./IUniswapExchangeQuotes.sol";
import "./interfaces/IUniswapExchangeQuotes.sol";
import "./SamplerUtils.sol";

View File

@ -20,7 +20,7 @@ pragma solidity ^0.5.9;
pragma experimental ABIEncoderV2;
import "@0x/contracts-utils/contracts/src/DeploymentConstants.sol";
import "./IUniswapV2Router01.sol";
import "./interfaces/IUniswapV2Router01.sol";
contract UniswapV2Sampler is

View File

@ -0,0 +1,43 @@
/*
Copyright 2019 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity ^0.5.9;
interface IBalancer {
function isBound(address t) external view returns (bool);
function getDenormalizedWeight(address token) external view returns (uint256);
function getBalance(address token) external view returns (uint256);
function getSwapFee() external view returns (uint256);
function calcOutGivenIn(
uint256 tokenBalanceIn,
uint256 tokenWeightIn,
uint256 tokenBalanceOut,
uint256 tokenWeightOut,
uint256 tokenAmountIn,
uint256 swapFee
) external pure returns (uint256 tokenAmountOut);
function calcInGivenOut(
uint256 tokenBalanceIn,
uint256 tokenWeightIn,
uint256 tokenBalanceOut,
uint256 tokenWeightOut,
uint256 tokenAmountOut,
uint256 swapFee
) external pure returns (uint256 tokenAmountIn);
}

View File

@ -22,9 +22,9 @@ import "@0x/contracts-asset-proxy/contracts/src/interfaces/IUniswapExchangeFacto
import "@0x/contracts-exchange/contracts/src/interfaces/IExchange.sol";
import "@0x/contracts-exchange-libs/contracts/src/LibOrder.sol";
import "../src/ERC20BridgeSampler.sol";
import "../src/IEth2Dai.sol";
import "../src/IKyberNetworkProxy.sol";
import "../src/IUniswapV2Router01.sol";
import "../src/interfaces/IEth2Dai.sol";
import "../src/interfaces/IKyberNetworkProxy.sol";
import "../src/interfaces/IUniswapV2Router01.sol";
library LibDeterministicQuotes {

View File

@ -9,6 +9,7 @@
"types": "lib/src/index.d.ts",
"scripts": {
"build": "yarn pre_build && tsc -b",
"build:ts": "tsc -b",
"watch": "tsc -w -p tsconfig.json",
"watch:contracts": "sol-compiler -w",
"build:ci": "yarn build",
@ -37,7 +38,7 @@
"config": {
"publicInterfaceContracts": "ERC20BridgeSampler,ILiquidityProvider,ILiquidityProviderRegistry,DummyLiquidityProviderRegistry,DummyLiquidityProvider",
"abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.",
"abis": "./test/generated-artifacts/@(ApproximateBuys|CurveSampler|DummyLiquidityProvider|DummyLiquidityProviderRegistry|ERC20BridgeSampler|Eth2DaiSampler|ICurve|IEth2Dai|IKyberHintHandler|IKyberNetwork|IKyberNetworkProxy|IKyberStorage|ILiquidityProvider|ILiquidityProviderRegistry|IMStable|IMooniswap|IMultiBridge|IUniswapExchangeQuotes|IUniswapV2Router01|KyberSampler|LiquidityProviderSampler|MStableSampler|MooniswapSampler|MultiBridgeSampler|NativeOrderSampler|SamplerUtils|TestERC20BridgeSampler|TestNativeOrderSampler|UniswapSampler|UniswapV2Sampler).json",
"abis": "./test/generated-artifacts/@(ApproximateBuys|BalancerSampler|CurveSampler|DummyLiquidityProvider|DummyLiquidityProviderRegistry|ERC20BridgeSampler|Eth2DaiSampler|IBalancer|ICurve|IEth2Dai|IKyberHintHandler|IKyberNetwork|IKyberNetworkProxy|IKyberStorage|ILiquidityProvider|ILiquidityProviderRegistry|IMStable|IMooniswap|IMultiBridge|IUniswapExchangeQuotes|IUniswapV2Router01|KyberSampler|LiquidityProviderSampler|MStableSampler|MooniswapSampler|MultiBridgeSampler|NativeOrderSampler|SamplerUtils|TestERC20BridgeSampler|TestNativeOrderSampler|TwoHopSampler|UniswapSampler|UniswapV2Sampler).json",
"postpublish": {
"assets": []
}
@ -60,6 +61,7 @@
"@0x/base-contract": "^6.2.3",
"@0x/contract-addresses": "^4.11.0",
"@0x/contract-wrappers": "^13.8.0",
"@0x/contracts-erc20-bridge-sampler": "^1.7.0",
"@0x/json-schemas": "^5.1.0",
"@0x/order-utils": "^10.3.0",
"@0x/orderbook": "^2.2.7",

View File

@ -39,10 +39,8 @@ const PROTOCOL_FEE_MULTIPLIER = new BigNumber(70000);
const MARKET_UTILS_AMOUNT_BUFFER_PERCENTAGE = 0.5;
const DEFAULT_SWAP_QUOTER_OPTS: SwapQuoterOpts = {
...{
chainId: MAINNET_CHAIN_ID,
orderRefreshIntervalMs: 10000, // 10 seconds
},
...DEFAULT_ORDER_PRUNER_OPTS,
samplerGasLimit: 250e6,
ethGasStationUrl: ETH_GAS_STATION_API_URL,

View File

@ -1,3 +1,9 @@
export {
AwaitTransactionSuccessOpts,
ContractFunctionObj,
ContractTxFunctionObj,
SendTransactionOpts,
} from '@0x/base-contract';
export { ContractAddresses } from '@0x/contract-addresses';
export { WSOpts } from '@0x/mesh-rpc-client';
export {
@ -28,6 +34,7 @@ export {
AbiDefinition,
BlockParam,
BlockParamLiteral,
CallData,
CompilerOpts,
CompilerSettings,
CompilerSettingsMetadata,
@ -66,6 +73,8 @@ export {
StateMutability,
SupportedProvider,
TupleDataItem,
TxData,
TxDataPayable,
Web3JsProvider,
Web3JsV1Provider,
Web3JsV2Provider,
@ -107,6 +116,11 @@ export {
SwapQuoterRfqtOpts,
} from './types';
export { affiliateFeeUtils } from './utils/affiliate_fee_utils';
export {
Parameters,
SamplerContractCall,
SamplerContractOperation,
} from './utils/market_operation_utils/sampler_contract_operation';
export {
BancorFillData,
BalancerFillData,
@ -114,22 +128,30 @@ export {
CurveFillData,
CurveFunctionSelectors,
CurveInfo,
DexSample,
ERC20BridgeSource,
FeeSchedule,
Fill,
FillData,
FillFlags,
GetMarketOrdersRfqtOpts,
LiquidityProviderFillData,
MarketDepth,
MarketDepthSide,
MultiBridgeFillData,
MultiHopFillData,
NativeCollapsedFill,
NativeFillData,
OptimizedMarketOrder,
SourceInfo,
SourceQuoteOperation,
TokenAdjacencyGraph,
UniswapV2FillData,
} from './utils/market_operation_utils/types';
export { ProtocolFeeUtils } from './utils/protocol_fee_utils';
export {
BridgeReportSource,
MultiHopReportSource,
NativeOrderbookReportSource,
NativeRFQTReportSource,
QuoteReport,
@ -140,3 +162,4 @@ export { rfqtMocker } from './utils/rfqt_mocker';
export { ERC20BridgeSamplerContract } from './wrappers';
import { ERC20BridgeSource } from './utils/market_operation_utils/types';
export type Native = ERC20BridgeSource.Native;
export type MultiHop = ERC20BridgeSource.MultiHop;

View File

@ -1,16 +1,13 @@
import { ContractAddresses } from '@0x/contract-addresses';
import { ITransformERC20Contract } from '@0x/contract-wrappers';
import {
assetDataUtils,
encodeAffiliateFeeTransformerData,
encodeFillQuoteTransformerData,
encodePayTakerTransformerData,
encodeWethTransformerData,
ERC20AssetData,
ETH_TOKEN_ADDRESS,
FillQuoteTransformerSide,
} from '@0x/order-utils';
import { AssetProxyId } from '@0x/types';
import { BigNumber, providerUtils } from '@0x/utils';
import { SupportedProvider, ZeroExProvider } from '@0x/web3-wrapper';
import * as ethjs from 'ethereumjs-util';
@ -30,6 +27,7 @@ import {
SwapQuoteGetOutputOpts,
} from '../types';
import { assert } from '../utils/assert';
import { getTokenFromAssetData } from '../utils/utils';
// tslint:disable-next-line:custom-no-magic-numbers
const MAX_UINT256 = new BigNumber(2).pow(256).minus(1);
@ -108,7 +106,35 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
});
}
const intermediateToken = quote.isTwoHop ? getTokenFromAssetData(quote.orders[0].makerAssetData) : NULL_ADDRESS;
// This transformer will fill the quote.
if (quote.isTwoHop) {
const [firstHopOrder, secondHopOrder] = quote.orders;
transforms.push({
deploymentNonce: this.transformerNonces.fillQuoteTransformer,
data: encodeFillQuoteTransformerData({
sellToken,
buyToken: intermediateToken,
side: FillQuoteTransformerSide.Sell,
fillAmount: firstHopOrder.takerAssetAmount,
maxOrderFillAmounts: [],
orders: [firstHopOrder],
signatures: [firstHopOrder.signature],
}),
});
transforms.push({
deploymentNonce: this.transformerNonces.fillQuoteTransformer,
data: encodeFillQuoteTransformerData({
sellToken: intermediateToken,
buyToken,
side: FillQuoteTransformerSide.Sell,
fillAmount: MAX_UINT256,
maxOrderFillAmounts: [],
orders: [secondHopOrder],
signatures: [secondHopOrder.signature],
}),
});
} else {
transforms.push({
deploymentNonce: this.transformerNonces.fillQuoteTransformer,
data: encodeFillQuoteTransformerData({
@ -121,6 +147,7 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
signatures: quote.orders.map(o => o.signature),
}),
});
}
if (isToETH) {
// Create a WETH unwrapper if going to ETH.
@ -159,7 +186,7 @@ export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase {
transforms.push({
deploymentNonce: this.transformerNonces.payTakerTransformer,
data: encodePayTakerTransformerData({
tokens: [sellToken, buyToken, ETH_TOKEN_ADDRESS],
tokens: [sellToken, buyToken, ETH_TOKEN_ADDRESS].concat(quote.isTwoHop ? intermediateToken : []),
amounts: [],
}),
});
@ -201,15 +228,6 @@ function isBuyQuote(quote: SwapQuote): quote is MarketBuySwapQuote {
return quote.type === MarketOperation.Buy;
}
function getTokenFromAssetData(assetData: string): string {
const data = assetDataUtils.decodeAssetDataOrThrow(assetData);
if (data.assetProxyId !== AssetProxyId.ERC20) {
throw new Error(`Unsupported exchange proxy quote asset type: ${data.assetProxyId}`);
}
// tslint:disable-next-line:no-unnecessary-type-assertion
return (data as ERC20AssetData).tokenAddress;
}
/**
* Find the nonce for a transformer given its deployer.
* If `deployer` is the null address, zero will always be returned.

View File

@ -2,7 +2,7 @@ import { ContractAddresses, getContractAddressesForChainOrThrow } from '@0x/cont
import { DevUtilsContract } from '@0x/contract-wrappers';
import { schemas } from '@0x/json-schemas';
import { assetDataUtils, SignedOrder } from '@0x/order-utils';
import { APIOrder, MeshOrderProviderOpts, Orderbook, SRAPollingOrderProviderOpts } from '@0x/orderbook';
import { MeshOrderProviderOpts, Orderbook, SRAPollingOrderProviderOpts } from '@0x/orderbook';
import { BigNumber, providerUtils } from '@0x/utils';
import { BlockParamLiteral, SupportedProvider, ZeroExProvider } from 'ethereum-types';
import * as _ from 'lodash';
@ -165,6 +165,7 @@ export class SwapQuoter {
samplerGasLimit,
liquidityProviderRegistryAddress,
rfqt,
tokenAdjacencyGraph,
} = _.merge({}, constants.DEFAULT_SWAP_QUOTER_OPTS, options);
const provider = providerUtils.standardizeOrThrow(supportedProvider);
assert.isValidOrderbook('orderbook', orderbook);
@ -210,6 +211,7 @@ export class SwapQuoter {
exchangeAddress: this._contractAddresses.exchange,
},
liquidityProviderRegistryAddress,
tokenAdjacencyGraph,
);
this._swapQuoteCalculator = new SwapQuoteCalculator(this._marketOperationUtils);
}
@ -422,7 +424,7 @@ export class SwapQuoter {
const takerAssetData = assetDataUtils.encodeERC20AssetData(takerTokenAddress);
let [sellOrders, buyOrders] =
options.excludedSources && options.excludedSources.includes(ERC20BridgeSource.Native)
? await Promise.resolve([[] as APIOrder[], [] as APIOrder[]])
? [[], []]
: await Promise.all([
this.orderbook.getOrdersAsync(makerAssetData, takerAssetData),
this.orderbook.getOrdersAsync(takerAssetData, makerAssetData),

View File

@ -2,7 +2,12 @@ import { BlockParam, ContractAddresses, GethCallOverrides } from '@0x/contract-w
import { SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';
import { GetMarketOrdersOpts, OptimizedMarketOrder } from './utils/market_operation_utils/types';
import {
ERC20BridgeSource,
GetMarketOrdersOpts,
OptimizedMarketOrder,
TokenAdjacencyGraph,
} from './utils/market_operation_utils/types';
import { QuoteReport } from './utils/quote_report_generator';
import { LogFunction } from './utils/quote_requestor';
@ -141,8 +146,6 @@ export interface ExchangeProxyContractOpts {
affiliateFee: AffiliateFee;
}
export type SwapQuote = MarketBuySwapQuote | MarketSellSwapQuote;
export interface GetExtensionContractTypeOpts {
takerAddress?: string;
ethAmount?: BigNumber;
@ -165,6 +168,7 @@ export interface SwapQuoteBase {
worstCaseQuoteInfo: SwapQuoteInfo;
sourceBreakdown: SwapQuoteOrdersBreakdown;
quoteReport?: QuoteReport;
isTwoHop: boolean;
}
/**
@ -185,6 +189,8 @@ export interface MarketBuySwapQuote extends SwapQuoteBase {
type: MarketOperation.Buy;
}
export type SwapQuote = MarketBuySwapQuote | MarketSellSwapQuote;
/**
* feeTakerAssetAmount: The amount of takerAsset reserved for paying takerFees when swapping for desired assets.
* takerAssetAmount: The amount of takerAsset swapped for desired makerAsset.
@ -205,9 +211,15 @@ export interface SwapQuoteInfo {
/**
* percentage breakdown of each liquidity source used in quote
*/
export interface SwapQuoteOrdersBreakdown {
[source: string]: BigNumber;
}
export type SwapQuoteOrdersBreakdown = Partial<
{ [key in Exclude<ERC20BridgeSource, typeof ERC20BridgeSource.MultiHop>]: BigNumber } & {
[ERC20BridgeSource.MultiHop]: {
proportion: BigNumber;
intermediateToken: string;
hops: ERC20BridgeSource[];
};
}
>;
/**
* nativeExclusivelyRFQT: if set to `true`, Swap quote will exclude Open Orderbook liquidity.
@ -272,6 +284,7 @@ export interface SwapQuoterOpts extends OrderPrunerOpts {
ethGasStationUrl?: string;
rfqt?: SwapQuoterRfqtOpts;
samplerOverrides?: SamplerOverrides;
tokenAdjacencyGraph?: TokenAdjacencyGraph;
}
/**

View File

@ -19,12 +19,21 @@ export const assert = {
sharedAssert.isHexString(`${variableName}.takerAssetData`, swapQuote.takerAssetData);
sharedAssert.isHexString(`${variableName}.makerAssetData`, swapQuote.makerAssetData);
sharedAssert.doesConformToSchema(`${variableName}.orders`, swapQuote.orders, schemas.signedOrdersSchema);
if (swapQuote.isTwoHop) {
assert.isValidTwoHopSwapQuoteOrders(
`${variableName}.orders`,
swapQuote.orders,
swapQuote.makerAssetData,
swapQuote.takerAssetData,
);
} else {
assert.isValidSwapQuoteOrders(
`${variableName}.orders`,
swapQuote.orders,
swapQuote.makerAssetData,
swapQuote.takerAssetData,
);
}
assert.isValidSwapQuoteInfo(`${variableName}.bestCaseQuoteInfo`, swapQuote.bestCaseQuoteInfo);
assert.isValidSwapQuoteInfo(`${variableName}.worstCaseQuoteInfo`, swapQuote.worstCaseQuoteInfo);
if (swapQuote.type === MarketOperation.Buy) {
@ -54,6 +63,28 @@ export const assert = {
);
});
},
isValidTwoHopSwapQuoteOrders(
variableName: string,
orders: SignedOrder[],
makerAssetData: string,
takerAssetData: string,
): void {
assert.assert(orders.length === 2, `Expected ${variableName}.length to be 2 for a two-hop quote`);
assert.assert(
isAssetDataEquivalent(takerAssetData, orders[0].takerAssetData),
`Expected ${variableName}[0].takerAssetData to be ${takerAssetData} but found ${orders[0].takerAssetData}`,
);
assert.assert(
isAssetDataEquivalent(makerAssetData, orders[1].makerAssetData),
`Expected ${variableName}[1].makerAssetData to be ${makerAssetData} but found ${orders[1].makerAssetData}`,
);
assert.assert(
isAssetDataEquivalent(orders[0].makerAssetData, orders[1].takerAssetData),
`Expected ${variableName}[0].makerAssetData (${
orders[0].makerAssetData
}) to equal ${variableName}[1].takerAssetData (${orders[1].takerAssetData})`,
);
},
isValidOrdersForSwapQuoter<T extends Order>(variableName: string, orders: T[]): void {
_.every(orders, (order: T, index: number) => {
assert.assert(

View File

@ -2,6 +2,10 @@ import { BigNumber } from '@0x/utils';
import { bmath, getPoolsWithTokens, parsePoolData } from '@balancer-labs/sor';
import { Decimal } from 'decimal.js';
import { ERC20BridgeSource } from './types';
// tslint:disable:boolean-naming
export interface BalancerPool {
id: string;
balanceIn: BigNumber;
@ -21,15 +25,15 @@ interface CacheValue {
// tslint:disable:custom-no-magic-numbers
const FIVE_SECONDS_MS = 5 * 1000;
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
const DEFAULT_TIMEOUT_MS = 1000;
const MAX_POOLS_FETCHED = 2;
const MAX_POOLS_FETCHED = 3;
const Decimal20 = Decimal.clone({ precision: 20 });
// tslint:enable:custom-no-magic-numbers
export class BalancerPoolsCache {
constructor(
private readonly _cache: { [key: string]: CacheValue } = {},
public cacheExpiryMs: number = FIVE_SECONDS_MS,
private readonly maxPoolsFetched: number = MAX_POOLS_FETCHED,
) {}
@ -42,10 +46,52 @@ export class BalancerPoolsCache {
return Promise.race([this._getPoolsForPairAsync(takerToken, makerToken), timeout]);
}
protected async _getPoolsForPairAsync(takerToken: string, makerToken: string): Promise<BalancerPool[]> {
public getCachedPoolAddressesForPair(
takerToken: string,
makerToken: string,
cacheExpiryMs?: number,
): string[] | undefined {
const key = JSON.stringify([takerToken, makerToken]);
const value = this._cache[key];
const minTimestamp = Date.now() - this.cacheExpiryMs;
if (cacheExpiryMs === undefined) {
return value === undefined ? [] : value.pools.map(pool => pool.id);
}
const minTimestamp = Date.now() - cacheExpiryMs;
if (value === undefined || value.timestamp < minTimestamp) {
return undefined;
} else {
return value.pools.map(pool => pool.id);
}
}
public howToSampleBalancer(
takerToken: string,
makerToken: string,
excludedSources: ERC20BridgeSource[],
): { onChain: boolean; offChain: boolean } {
// If Balancer is excluded as a source, do not sample.
if (excludedSources.includes(ERC20BridgeSource.Balancer)) {
return { onChain: false, offChain: false };
}
const cachedBalancerPools = this.getCachedPoolAddressesForPair(takerToken, makerToken, ONE_DAY_MS);
// Sample Balancer on-chain (i.e. via the ERC20BridgeSampler contract) if:
// - Cached values are not stale
// - There is at least one Balancer pool for this pair
const onChain = cachedBalancerPools !== undefined && cachedBalancerPools.length > 0;
// Sample Balancer off-chain (i.e. via GraphQL query + `computeBalancerBuy/SellQuote`)
// if cached values are stale
const offChain = cachedBalancerPools === undefined;
return { onChain, offChain };
}
protected async _getPoolsForPairAsync(
takerToken: string,
makerToken: string,
cacheExpiryMs: number = FIVE_SECONDS_MS,
): Promise<BalancerPool[]> {
const key = JSON.stringify([takerToken, makerToken]);
const value = this._cache[key];
const minTimestamp = Date.now() - cacheExpiryMs;
if (value === undefined || value.timestamp < minTimestamp) {
const pools = await this._fetchPoolsForPairAsync(takerToken, makerToken);
const timestamp = Date.now();

View File

@ -31,11 +31,7 @@ export class BancorService {
return this._sdk;
}
public async getQuoteAsync(
fromToken: string,
toToken: string,
amount: BigNumber = new BigNumber(1),
): Promise<Quote<BancorFillData>> {
public async getQuoteAsync(fromToken: string, toToken: string, amount: BigNumber): Promise<Quote<BancorFillData>> {
const sdk = await this.getSDKAsync();
const blockchain = sdk._core.blockchains[BlockchainType.Ethereum] as Ethereum;
const sourceDecimals = await getDecimals(blockchain, fromToken);

View File

@ -138,6 +138,7 @@ export const ONE_ETHER = new BigNumber(1e18);
export const NEGATIVE_INF = new BigNumber('-Infinity');
export const POSITIVE_INF = new BigNumber('Infinity');
export const ZERO_AMOUNT = new BigNumber(0);
export const MAX_UINT256 = new BigNumber(2).pow(256).minus(1);
export const ONE_HOUR_IN_SECONDS = 60 * 60;
export const ONE_SECOND_MS = 1000;
export const NULL_BYTES = '0x';

View File

@ -4,7 +4,7 @@ import { MarketOperation, SignedOrderWithFillableAmounts } from '../../types';
import { fillableAmountsUtils } from '../../utils/fillable_amounts_utils';
import { POSITIVE_INF, ZERO_AMOUNT } from './constants';
import { CollapsedFill, DexSample, ERC20BridgeSource, FeeSchedule, Fill, FillFlags } from './types';
import { CollapsedFill, DexSample, ERC20BridgeSource, FeeSchedule, Fill, FillFlags, MultiHopFillData } from './types';
// tslint:disable: prefer-for-of no-bitwise completed-docs
@ -155,6 +155,22 @@ function dexQuotesToPaths(
return paths;
}
export function getTwoHopAdjustedRate(
side: MarketOperation,
twoHopQuote: DexSample<MultiHopFillData>,
targetInput: BigNumber,
ethToOutputRate: BigNumber,
fees: FeeSchedule = {},
): BigNumber {
const { output, input, fillData } = twoHopQuote;
if (input.isLessThan(targetInput) || output.isZero()) {
return ZERO_AMOUNT;
}
const penalty = ethToOutputRate.times(fees[ERC20BridgeSource.MultiHop]!(fillData));
const adjustedOutput = side === MarketOperation.Sell ? output.minus(penalty) : output.plus(penalty);
return side === MarketOperation.Sell ? adjustedOutput.div(input) : input.div(adjustedOutput);
}
function sourceToFillFlags(source: ERC20BridgeSource): number {
switch (source) {
case ERC20BridgeSource.Uniswap:

View File

@ -1,5 +1,4 @@
import { ContractAddresses } from '@0x/contract-addresses';
import { ZERO_AMOUNT } from '@0x/order-utils';
import { RFQTIndicativeQuote } from '@0x/quote-server';
import { SignedOrder } from '@0x/types';
import { BigNumber, NULL_ADDRESS } from '@0x/utils';
@ -9,11 +8,20 @@ import { MarketOperation } from '../../types';
import { QuoteRequestor } from '../quote_requestor';
import { difference } from '../utils';
import { QuoteReportGenerator } from './../quote_report_generator';
import { BUY_SOURCES, DEFAULT_GET_MARKET_ORDERS_OPTS, FEE_QUOTE_SOURCES, ONE_ETHER, SELL_SOURCES } from './constants';
import { generateQuoteReport } from './../quote_report_generator';
import {
BUY_SOURCES,
DEFAULT_GET_MARKET_ORDERS_OPTS,
FEE_QUOTE_SOURCES,
ONE_ETHER,
SELL_SOURCES,
ZERO_AMOUNT,
} from './constants';
import { createFillPaths, getPathAdjustedRate, getPathAdjustedSlippage } from './fills';
import { getBestTwoHopQuote } from './multihop_utils';
import {
createOrdersFromPath,
createOrdersFromTwoHopSample,
createSignedOrdersFromRfqtIndicativeQuotes,
createSignedOrdersWithFillableAmounts,
getNativeOrderTokens,
@ -28,10 +36,13 @@ import {
GetMarketOrdersOpts,
MarketSideLiquidity,
OptimizedMarketOrder,
OptimizedOrdersAndQuoteReport,
OptimizerResult,
OrderDomain,
TokenAdjacencyGraph,
} from './types';
// tslint:disable:boolean-naming
/**
* Returns a indicative quotes or an empty array if RFQT is not enabled or requested
* @param makerAssetData the maker asset data
@ -70,6 +81,7 @@ export class MarketOperationUtils {
private readonly contractAddresses: ContractAddresses,
private readonly _orderDomain: OrderDomain,
private readonly _liquidityProviderRegistry: string = NULL_ADDRESS,
private readonly _tokenAdjacencyGraph: TokenAdjacencyGraph = {},
) {
this._wethAddress = contractAddresses.etherToken.toLowerCase();
this._multiBridge = contractAddresses.multiBridge.toLowerCase();
@ -94,53 +106,61 @@ export class MarketOperationUtils {
const [makerToken, takerToken] = getNativeOrderTokens(nativeOrders[0]);
const sampleAmounts = getSampleAmounts(takerAmount, _opts.numSamples, _opts.sampleDistributionBase);
const {
onChain: sampleBalancerOnChain,
offChain: sampleBalancerOffChain,
} = this._sampler.balancerPoolsCache.howToSampleBalancer(takerToken, makerToken, _opts.excludedSources);
// Call the sampler contract.
const samplerPromise = this._sampler.executeAsync(
// Get native order fillable amounts.
DexOrderSampler.ops.getOrderFillableTakerAmounts(nativeOrders, this.contractAddresses.exchange),
// Get the custom liquidity provider from registry.
DexOrderSampler.ops.getLiquidityProviderFromRegistry(
this._liquidityProviderRegistry,
makerToken,
takerToken,
),
this._sampler.getOrderFillableTakerAmounts(nativeOrders, this.contractAddresses.exchange),
// Get ETH -> maker token price.
await DexOrderSampler.ops.getMedianSellRateAsync(
this._sampler.getMedianSellRate(
difference(FEE_QUOTE_SOURCES, _opts.excludedSources),
makerToken,
this._wethAddress,
ONE_ETHER,
this._wethAddress,
this._sampler.balancerPoolsCache,
this._liquidityProviderRegistry,
this._multiBridge,
this._sampler.bancorService,
),
// Get ETH -> taker token price.
await DexOrderSampler.ops.getMedianSellRateAsync(
this._sampler.getMedianSellRate(
difference(FEE_QUOTE_SOURCES, _opts.excludedSources),
takerToken,
this._wethAddress,
ONE_ETHER,
this._wethAddress,
this._sampler.balancerPoolsCache,
this._liquidityProviderRegistry,
this._multiBridge,
),
// Get sell quotes for taker -> maker.
await DexOrderSampler.ops.getSellQuotesAsync(
this._sampler.getSellQuotes(
difference(
SELL_SOURCES.concat(this._optionalSources()),
_opts.excludedSources.concat(ERC20BridgeSource.Balancer),
_opts.excludedSources.concat(sampleBalancerOnChain ? [] : ERC20BridgeSource.Balancer),
),
makerToken,
takerToken,
sampleAmounts,
this._wethAddress,
this._sampler.balancerPoolsCache,
this._liquidityProviderRegistry,
this._multiBridge,
this._sampler.bancorService,
),
_opts.excludedSources.includes(ERC20BridgeSource.MultiHop)
? DexOrderSampler.constant([])
: this._sampler.getTwoHopSellQuotes(
difference(
SELL_SOURCES.concat(this._optionalSources()),
_opts.excludedSources.concat(ERC20BridgeSource.MultiBridge),
),
makerToken,
takerToken,
takerAmount,
this._tokenAdjacencyGraph,
this._wethAddress,
this._liquidityProviderRegistry,
),
);
@ -152,45 +172,33 @@ export class MarketOperationUtils {
_opts,
);
const balancerPromise = DexOrderSampler.ops
.getSellQuotesAsync(
difference([ERC20BridgeSource.Balancer], _opts.excludedSources),
makerToken,
takerToken,
sampleAmounts,
this._wethAddress,
this._sampler.balancerPoolsCache,
this._liquidityProviderRegistry,
this._multiBridge,
this._sampler.bancorService,
)
.then(async r => this._sampler.executeAsync(r));
const offChainBalancerPromise = sampleBalancerOffChain
? this._sampler.getBalancerSellQuotesOffChainAsync(makerToken, takerToken, sampleAmounts)
: Promise.resolve([]);
const offChainBancorPromise = _opts.excludedSources.includes(ERC20BridgeSource.Bancor)
? Promise.resolve([])
: this._sampler.getBancorSellQuotesOffChainAsync(makerToken, takerToken, [takerAmount]);
const [
[orderFillableAmounts, liquidityProviderAddress, ethToMakerAssetRate, ethToTakerAssetRate, dexQuotes],
[orderFillableAmounts, ethToMakerAssetRate, ethToTakerAssetRate, dexQuotes, twoHopQuotes],
rfqtIndicativeQuotes,
[balancerQuotes],
] = await Promise.all([samplerPromise, rfqtPromise, balancerPromise]);
offChainBalancerQuotes,
offChainBancorQuotes,
] = await Promise.all([samplerPromise, rfqtPromise, offChainBalancerPromise, offChainBancorPromise]);
// Attach the LiquidityProvider address to the sample fillData
(dexQuotes.find(quotes => quotes[0] && quotes[0].source === ERC20BridgeSource.LiquidityProvider) || []).forEach(
q => (q.fillData = { poolAddress: liquidityProviderAddress }),
);
// Attach the MultiBridge address to the sample fillData
(dexQuotes.find(quotes => quotes[0] && quotes[0].source === ERC20BridgeSource.MultiBridge) || []).forEach(
q => (q.fillData = { poolAddress: this._multiBridge }),
);
return {
side: MarketOperation.Sell,
inputAmount: takerAmount,
inputToken: takerToken,
outputToken: makerToken,
dexQuotes: dexQuotes.concat(balancerQuotes),
dexQuotes: dexQuotes.concat([...offChainBalancerQuotes, offChainBancorQuotes]),
nativeOrders,
orderFillableAmounts,
ethToOutputRate: ethToMakerAssetRate,
ethToInputRate: ethToTakerAssetRate,
rfqtIndicativeQuotes,
twoHopQuotes,
};
}
@ -213,69 +221,72 @@ export class MarketOperationUtils {
const [makerToken, takerToken] = getNativeOrderTokens(nativeOrders[0]);
const sampleAmounts = getSampleAmounts(makerAmount, _opts.numSamples, _opts.sampleDistributionBase);
const {
onChain: sampleBalancerOnChain,
offChain: sampleBalancerOffChain,
} = this._sampler.balancerPoolsCache.howToSampleBalancer(takerToken, makerToken, _opts.excludedSources);
// Call the sampler contract.
const samplerPromise = this._sampler.executeAsync(
// Get native order fillable amounts.
DexOrderSampler.ops.getOrderFillableMakerAmounts(nativeOrders, this.contractAddresses.exchange),
// Get the custom liquidity provider from registry.
DexOrderSampler.ops.getLiquidityProviderFromRegistry(
this._liquidityProviderRegistry,
makerToken,
takerToken,
),
// Get ETH -> maker token price.
await DexOrderSampler.ops.getMedianSellRateAsync(
this._sampler.getOrderFillableMakerAmounts(nativeOrders, this.contractAddresses.exchange),
// Get ETH -> makerToken token price.
this._sampler.getMedianSellRate(
difference(FEE_QUOTE_SOURCES, _opts.excludedSources),
makerToken,
this._wethAddress,
ONE_ETHER,
this._wethAddress,
this._sampler.balancerPoolsCache,
this._liquidityProviderRegistry,
this._multiBridge,
),
// Get ETH -> taker token price.
await DexOrderSampler.ops.getMedianSellRateAsync(
this._sampler.getMedianSellRate(
difference(FEE_QUOTE_SOURCES, _opts.excludedSources),
takerToken,
this._wethAddress,
ONE_ETHER,
this._wethAddress,
this._sampler.balancerPoolsCache,
this._liquidityProviderRegistry,
this._multiBridge,
this._sampler.bancorService,
),
// Get buy quotes for taker -> maker.
await DexOrderSampler.ops.getBuyQuotesAsync(
this._sampler.getBuyQuotes(
difference(
BUY_SOURCES.concat(
this._liquidityProviderRegistry !== NULL_ADDRESS ? [ERC20BridgeSource.LiquidityProvider] : [],
),
_opts.excludedSources.concat([ERC20BridgeSource.Balancer]),
_opts.excludedSources.concat(sampleBalancerOnChain ? [] : ERC20BridgeSource.Balancer),
),
makerToken,
takerToken,
sampleAmounts,
this._wethAddress,
this._sampler.balancerPoolsCache,
this._liquidityProviderRegistry,
this._sampler.bancorService,
),
_opts.excludedSources.includes(ERC20BridgeSource.MultiHop)
? DexOrderSampler.constant([])
: this._sampler.getTwoHopBuyQuotes(
difference(
BUY_SOURCES.concat(
this._liquidityProviderRegistry !== NULL_ADDRESS
? [ERC20BridgeSource.LiquidityProvider]
: [],
),
_opts.excludedSources,
),
makerToken,
takerToken,
makerAmount,
this._tokenAdjacencyGraph,
this._wethAddress,
this._liquidityProviderRegistry,
),
);
const balancerPromise = this._sampler.executeAsync(
await DexOrderSampler.ops.getBuyQuotesAsync(
difference([ERC20BridgeSource.Balancer], _opts.excludedSources),
makerToken,
takerToken,
sampleAmounts,
this._wethAddress,
this._sampler.balancerPoolsCache,
this._liquidityProviderRegistry,
this._sampler.bancorService,
),
);
const offChainBalancerPromise = sampleBalancerOffChain
? this._sampler.getBalancerBuyQuotesOffChainAsync(makerToken, takerToken, sampleAmounts)
: Promise.resolve([]);
const rfqtPromise = getRfqtIndicativeQuotesAsync(
nativeOrders[0].makerAssetData,
@ -285,14 +296,10 @@ export class MarketOperationUtils {
_opts,
);
const [
[orderFillableAmounts, liquidityProviderAddress, ethToMakerAssetRate, ethToTakerAssetRate, dexQuotes],
[orderFillableAmounts, ethToMakerAssetRate, ethToTakerAssetRate, dexQuotes, twoHopQuotes],
rfqtIndicativeQuotes,
[balancerQuotes],
] = await Promise.all([samplerPromise, rfqtPromise, balancerPromise]);
// Attach the LiquidityProvider address to the sample fillData
(dexQuotes.find(quotes => quotes[0] && quotes[0].source === ERC20BridgeSource.LiquidityProvider) || []).forEach(
q => (q.fillData = { poolAddress: liquidityProviderAddress }),
);
offChainBalancerQuotes,
] = await Promise.all([samplerPromise, rfqtPromise, offChainBalancerPromise]);
// Attach the MultiBridge address to the sample fillData
(dexQuotes.find(quotes => quotes[0] && quotes[0].source === ERC20BridgeSource.MultiBridge) || []).forEach(
q => (q.fillData = { poolAddress: this._multiBridge }),
@ -302,12 +309,13 @@ export class MarketOperationUtils {
inputAmount: makerAmount,
inputToken: makerToken,
outputToken: takerToken,
dexQuotes: dexQuotes.concat(balancerQuotes),
dexQuotes: dexQuotes.concat(offChainBalancerQuotes),
nativeOrders,
orderFillableAmounts,
ethToOutputRate: ethToTakerAssetRate,
ethToInputRate: ethToMakerAssetRate,
rfqtIndicativeQuotes,
twoHopQuotes,
};
}
@ -323,7 +331,7 @@ export class MarketOperationUtils {
nativeOrders: SignedOrder[],
takerAmount: BigNumber,
opts?: Partial<GetMarketOrdersOpts>,
): Promise<OptimizedOrdersAndQuoteReport> {
): Promise<OptimizerResult> {
const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts };
const marketSideLiquidity = await this.getMarketSellLiquidityAsync(nativeOrders, takerAmount, _opts);
return this._generateOptimizedOrdersAsync(marketSideLiquidity, {
@ -349,7 +357,7 @@ export class MarketOperationUtils {
nativeOrders: SignedOrder[],
makerAmount: BigNumber,
opts?: Partial<GetMarketOrdersOpts>,
): Promise<OptimizedOrdersAndQuoteReport> {
): Promise<OptimizerResult> {
const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts };
const marketSideLiquidity = await this.getMarketBuyLiquidityAsync(nativeOrders, makerAmount, _opts);
return this._generateOptimizedOrdersAsync(marketSideLiquidity, {
@ -384,35 +392,29 @@ export class MarketOperationUtils {
}
const _opts = { ...DEFAULT_GET_MARKET_ORDERS_OPTS, ...opts };
const sources = difference(BUY_SOURCES, _opts.excludedSources);
const sources = difference(BUY_SOURCES, _opts.excludedSources.concat(ERC20BridgeSource.Balancer));
const ops = [
...batchNativeOrders.map(orders =>
DexOrderSampler.ops.getOrderFillableMakerAmounts(orders, this.contractAddresses.exchange),
this._sampler.getOrderFillableMakerAmounts(orders, this.contractAddresses.exchange),
),
...(await Promise.all(
batchNativeOrders.map(async orders =>
DexOrderSampler.ops.getMedianSellRateAsync(
...batchNativeOrders.map(orders =>
this._sampler.getMedianSellRate(
difference(FEE_QUOTE_SOURCES, _opts.excludedSources),
getNativeOrderTokens(orders[0])[1],
this._wethAddress,
ONE_ETHER,
this._wethAddress,
this._sampler.balancerPoolsCache,
),
),
)),
...(await Promise.all(
batchNativeOrders.map(async (orders, i) =>
DexOrderSampler.ops.getBuyQuotesAsync(
...batchNativeOrders.map((orders, i) =>
this._sampler.getBuyQuotes(
sources,
getNativeOrderTokens(orders[0])[0],
getNativeOrderTokens(orders[0])[1],
[makerAmounts[i]],
this._wethAddress,
this._sampler.balancerPoolsCache,
),
),
)),
];
const executeResults = await this._sampler.executeBatchAsync(ops);
@ -444,6 +446,7 @@ export class MarketOperationUtils {
rfqtIndicativeQuotes: [],
inputToken: makerToken,
outputToken: takerToken,
twoHopQuotes: [],
},
{
bridgeSlippage: _opts.bridgeSlippage,
@ -476,7 +479,7 @@ export class MarketOperationUtils {
shouldBatchBridgeOrders?: boolean;
quoteRequestor?: QuoteRequestor;
},
): Promise<OptimizedOrdersAndQuoteReport> {
): Promise<OptimizerResult> {
const {
inputToken,
outputToken,
@ -488,8 +491,20 @@ export class MarketOperationUtils {
dexQuotes,
ethToOutputRate,
ethToInputRate,
twoHopQuotes,
} = marketSideLiquidity;
const maxFallbackSlippage = opts.maxFallbackSlippage || 0;
const orderOpts = {
side,
inputToken,
outputToken,
orderDomain: this._orderDomain,
contractAddresses: this.contractAddresses,
bridgeSlippage: opts.bridgeSlippage || 0,
shouldBatchBridgeOrders: !!opts.shouldBatchBridgeOrders,
};
// Convert native orders and dex quotes into fill paths.
const paths = createFillPaths({
side,
@ -505,12 +520,33 @@ export class MarketOperationUtils {
excludedSources: opts.excludedSources,
feeSchedule: opts.feeSchedule,
});
// Find the optimal path.
let optimalPath = (await findOptimalPathAsync(side, paths, inputAmount, opts.runLimit)) || [];
if (optimalPath.length === 0) {
throw new Error(AggregationError.NoOptimalPath);
}
// Generate a fallback path if native orders are in the optimal paath.
const optimalPathRate = getPathAdjustedRate(side, optimalPath, inputAmount);
const { adjustedRate: bestTwoHopRate, quote: bestTwoHopQuote } = getBestTwoHopQuote(
marketSideLiquidity,
opts.feeSchedule,
);
if (bestTwoHopRate.isGreaterThan(optimalPathRate)) {
const twoHopOrders = createOrdersFromTwoHopSample(bestTwoHopQuote!, orderOpts);
const twoHopQuoteReport = generateQuoteReport(
side,
_.flatten(dexQuotes),
twoHopQuotes,
nativeOrders,
orderFillableAmounts,
bestTwoHopQuote!,
opts.quoteRequestor,
);
return { optimizedOrders: twoHopOrders, quoteReport: twoHopQuoteReport, isTwoHop: true };
}
// Generate a fallback path if native orders are in the optimal path.
const nativeSubPath = optimalPath.filter(f => f.source === ERC20BridgeSource.Native);
if (opts.allowFallback && nativeSubPath.length !== 0) {
// We create a fallback path that is exclusive of Native liquidity
@ -519,12 +555,7 @@ export class MarketOperationUtils {
const nonNativeOptimalPath =
(await findOptimalPathAsync(side, nonNativePaths, inputAmount, opts.runLimit)) || [];
// Calculate the slippage of on-chain sources compared to the most optimal path
const fallbackSlippage = getPathAdjustedSlippage(
side,
nonNativeOptimalPath,
inputAmount,
getPathAdjustedRate(side, optimalPath, inputAmount),
);
const fallbackSlippage = getPathAdjustedSlippage(side, nonNativeOptimalPath, inputAmount, optimalPathRate);
if (nativeSubPath.length === optimalPath.length || fallbackSlippage <= maxFallbackSlippage) {
// If the last fill is Native and penultimate is not, then the intention was to partial fill
// In this case we drop it entirely as we can't handle a failure at the end and we don't
@ -542,24 +573,17 @@ export class MarketOperationUtils {
optimalPath = [...nativeSubPath.filter(f => f !== lastNativeFillIfExists), ...nonNativeOptimalPath];
}
}
const optimizedOrders = createOrdersFromPath(optimalPath, {
side,
inputToken,
outputToken,
orderDomain: this._orderDomain,
contractAddresses: this.contractAddresses,
bridgeSlippage: opts.bridgeSlippage || 0,
shouldBatchBridgeOrders: !!opts.shouldBatchBridgeOrders,
});
const quoteReport = new QuoteReportGenerator(
const optimizedOrders = createOrdersFromPath(optimalPath, orderOpts);
const quoteReport = generateQuoteReport(
side,
_.flatten(dexQuotes),
twoHopQuotes,
nativeOrders,
orderFillableAmounts,
_.flatten(optimizedOrders.map(o => o.fills)),
_.flatten(optimizedOrders.map(order => order.fills)),
opts.quoteRequestor,
).generateReport();
return { optimizedOrders, quoteReport };
);
return { optimizedOrders, quoteReport, isTwoHop: false };
}
private _optionalSources(): ERC20BridgeSource[] {

View File

@ -0,0 +1,51 @@
import { BigNumber } from '@0x/utils';
import * as _ from 'lodash';
import { ZERO_AMOUNT } from './constants';
import { getTwoHopAdjustedRate } from './fills';
import { DexSample, FeeSchedule, MarketSideLiquidity, MultiHopFillData, TokenAdjacencyGraph } from './types';
/**
* Given a token pair, returns the intermediate tokens to consider for two-hop routes.
*/
export function getIntermediateTokens(
makerToken: string,
takerToken: string,
tokenAdjacencyGraph: TokenAdjacencyGraph,
wethAddress: string,
): string[] {
let intermediateTokens = [];
if (makerToken === wethAddress) {
intermediateTokens = _.get(tokenAdjacencyGraph, takerToken, [] as string[]);
} else if (takerToken === wethAddress) {
intermediateTokens = _.get(tokenAdjacencyGraph, makerToken, [] as string[]);
} else {
intermediateTokens = _.union(
_.intersection(_.get(tokenAdjacencyGraph, takerToken, []), _.get(tokenAdjacencyGraph, makerToken, [])),
[wethAddress],
);
}
return intermediateTokens.filter(
token => token.toLowerCase() !== makerToken.toLowerCase() && token.toLowerCase() !== takerToken.toLowerCase(),
);
}
/**
* Returns the best two-hop quote and the fee-adjusted rate of that quote.
*/
export function getBestTwoHopQuote(
marketSideLiquidity: MarketSideLiquidity,
feeSchedule?: FeeSchedule,
): { quote: DexSample<MultiHopFillData> | undefined; adjustedRate: BigNumber } {
const { side, inputAmount, ethToOutputRate, twoHopQuotes } = marketSideLiquidity;
return twoHopQuotes
.map(quote => getTwoHopAdjustedRate(side, quote, inputAmount, ethToOutputRate, feeSchedule))
.reduce(
(prev, curr, i) =>
curr.isGreaterThan(prev.adjustedRate) ? { adjustedRate: curr, quote: twoHopQuotes[i] } : prev,
{
adjustedRate: ZERO_AMOUNT,
quote: undefined as DexSample<MultiHopFillData> | undefined,
},
);
}

View File

@ -8,6 +8,7 @@ import { MarketOperation, SignedOrderWithFillableAmounts } from '../../types';
import {
ERC20_PROXY_ID,
MAX_UINT256,
NULL_ADDRESS,
NULL_BYTES,
ONE_HOUR_IN_SECONDS,
@ -23,10 +24,12 @@ import {
BancorFillData,
CollapsedFill,
CurveFillData,
DexSample,
ERC20BridgeSource,
Fill,
LiquidityProviderFillData,
MultiBridgeFillData,
MultiHopFillData,
NativeCollapsedFill,
OptimizedMarketOrder,
OrderDomain,
@ -150,6 +153,7 @@ export interface CreateOrderFromPathOpts {
// Convert sell fills into orders.
export function createOrdersFromPath(path: Fill[], opts: CreateOrderFromPathOpts): OptimizedMarketOrder[] {
const [makerToken, takerToken] = getMakerTakerTokens(opts);
const collapsedPath = collapsePath(path);
const orders: OptimizedMarketOrder[] = [];
for (let i = 0; i < collapsedPath.length; ) {
@ -168,7 +172,7 @@ export function createOrdersFromPath(path: Fill[], opts: CreateOrderFromPathOpts
}
// Always use DexForwarderBridge unless configured not to
if (!opts.shouldBatchBridgeOrders) {
orders.push(createBridgeOrder(contiguousBridgeFills[0], opts));
orders.push(createBridgeOrder(contiguousBridgeFills[0], makerToken, takerToken, opts));
i += 1;
} else {
orders.push(createBatchedBridgeOrder(contiguousBridgeFills, opts));
@ -178,9 +182,36 @@ export function createOrdersFromPath(path: Fill[], opts: CreateOrderFromPathOpts
return orders;
}
export function createOrdersFromTwoHopSample(
sample: DexSample<MultiHopFillData>,
opts: CreateOrderFromPathOpts,
): OptimizedMarketOrder[] {
const [makerToken, takerToken] = getMakerTakerTokens(opts);
const { firstHopSource, secondHopSource, intermediateToken } = sample.fillData!;
const firstHopFill: CollapsedFill = {
sourcePathId: '',
source: firstHopSource.source,
input: opts.side === MarketOperation.Sell ? sample.input : ZERO_AMOUNT,
output: opts.side === MarketOperation.Sell ? ZERO_AMOUNT : sample.output,
subFills: [],
fillData: firstHopSource.fillData,
};
const secondHopFill: CollapsedFill = {
sourcePathId: '',
source: secondHopSource.source,
input: opts.side === MarketOperation.Sell ? MAX_UINT256 : sample.input,
output: opts.side === MarketOperation.Sell ? sample.output : MAX_UINT256,
subFills: [],
fillData: secondHopSource.fillData,
};
return [
createBridgeOrder(firstHopFill, intermediateToken, takerToken, opts),
createBridgeOrder(secondHopFill, makerToken, intermediateToken, opts),
];
}
function getBridgeAddressFromFill(fill: CollapsedFill, opts: CreateOrderFromPathOpts): string {
const source = fill.source;
switch (source) {
switch (fill.source) {
case ERC20BridgeSource.Eth2Dai:
return opts.contractAddresses.eth2DaiBridge;
case ERC20BridgeSource.Kyber:
@ -209,8 +240,12 @@ function getBridgeAddressFromFill(fill: CollapsedFill, opts: CreateOrderFromPath
throw new Error(AggregationError.NoBridgeForSource);
}
function createBridgeOrder(fill: CollapsedFill, opts: CreateOrderFromPathOpts): OptimizedMarketOrder {
const [makerToken, takerToken] = getMakerTakerTokens(opts);
function createBridgeOrder(
fill: CollapsedFill,
makerToken: string,
takerToken: string,
opts: CreateOrderFromPathOpts,
): OptimizedMarketOrder {
const bridgeAddress = getBridgeAddressFromFill(fill, opts);
let makerAssetData;
@ -277,7 +312,7 @@ function createBridgeOrder(fill: CollapsedFill, opts: CreateOrderFromPathOpts):
takerAssetAmount: slippedTakerAssetAmount,
fillableMakerAssetAmount: slippedMakerAssetAmount,
fillableTakerAssetAmount: slippedTakerAssetAmount,
...createCommonBridgeOrderFields(opts),
...createCommonBridgeOrderFields(opts.orderDomain),
};
}
@ -290,7 +325,7 @@ function createBatchedBridgeOrder(fills: CollapsedFill[], opts: CreateOrderFromP
calls: [],
};
for (const fill of fills) {
const bridgeOrder = createBridgeOrder(fill, opts);
const bridgeOrder = createBridgeOrder(fill, makerToken, takerToken, opts);
totalMakerAssetAmount = totalMakerAssetAmount.plus(bridgeOrder.makerAssetAmount);
totalTakerAssetAmount = totalTakerAssetAmount.plus(bridgeOrder.takerAssetAmount);
const { bridgeAddress, bridgeData: orderBridgeData } = assetDataUtils.decodeAssetDataOrThrow(
@ -318,7 +353,7 @@ function createBatchedBridgeOrder(fills: CollapsedFill[], opts: CreateOrderFromP
takerAssetAmount: totalTakerAssetAmount,
fillableMakerAssetAmount: totalMakerAssetAmount,
fillableTakerAssetAmount: totalTakerAssetAmount,
...createCommonBridgeOrderFields(opts),
...createCommonBridgeOrderFields(opts.orderDomain),
};
}
@ -395,7 +430,7 @@ function getSlippedBridgeAssetAmounts(fill: CollapsedFill, opts: CreateOrderFrom
// Taker asset amount.
opts.side === MarketOperation.Sell
? fill.input
: fill.output.times(opts.bridgeSlippage + 1).integerValue(BigNumber.ROUND_UP),
: BigNumber.min(fill.output.times(opts.bridgeSlippage + 1).integerValue(BigNumber.ROUND_UP), MAX_UINT256),
];
}
@ -414,7 +449,7 @@ type CommonBridgeOrderFields = Pick<
>
>;
function createCommonBridgeOrderFields(opts: CreateOrderFromPathOpts): CommonBridgeOrderFields {
function createCommonBridgeOrderFields(orderDomain: OrderDomain): CommonBridgeOrderFields {
return {
takerAddress: NULL_ADDRESS,
senderAddress: NULL_ADDRESS,
@ -428,7 +463,7 @@ function createCommonBridgeOrderFields(opts: CreateOrderFromPathOpts): CommonBri
takerFee: ZERO_AMOUNT,
fillableTakerFeeAmount: ZERO_AMOUNT,
signature: WALLET_SIGNATURE,
...opts.orderDomain,
...orderDomain,
};
}

View File

@ -5,7 +5,7 @@ import { ERC20BridgeSamplerContract } from '../../wrappers';
import { BalancerPoolsCache } from './balancer_utils';
import { BancorService } from './bancor_service';
import { samplerOperations } from './sampler_operations';
import { SamplerOperations } from './sampler_operations';
import { BatchedOperation } from './types';
/**
@ -30,19 +30,15 @@ type BatchedOperationResult<T> = T extends BatchedOperation<infer TResult> ? TRe
/**
* Encapsulates interactions with the `ERC20BridgeSampler` contract.
*/
export class DexOrderSampler {
/**
* Composable operations that can be batched in a single transaction,
* for use with `DexOrderSampler.executeAsync()`.
*/
public static ops = samplerOperations;
export class DexOrderSampler extends SamplerOperations {
constructor(
private readonly _samplerContract: ERC20BridgeSamplerContract,
_samplerContract: ERC20BridgeSamplerContract,
private readonly _samplerOverrides?: SamplerOverrides,
public bancorService?: BancorService,
public balancerPoolsCache: BalancerPoolsCache = new BalancerPoolsCache(),
) {}
bancorService?: BancorService,
balancerPoolsCache?: BalancerPoolsCache,
) {
super(_samplerContract, bancorService, balancerPoolsCache);
}
/* Type overloads for `executeAsync()`. Could skip this if we would upgrade TS. */
@ -142,16 +138,14 @@ export class DexOrderSampler {
* Takes an arbitrary length array, but is not typesafe.
*/
public async executeBatchAsync<T extends Array<BatchedOperation<any>>>(ops: T): Promise<any[]> {
const callDatas = ops.map(o => o.encodeCall(this._samplerContract));
const callDatas = ops.map(o => o.encodeCall());
const { overrides, block } = this._samplerOverrides
? this._samplerOverrides
: { overrides: undefined, block: undefined };
// All operations are NOOPs
if (callDatas.every(cd => cd === NULL_BYTES)) {
return Promise.all(
callDatas.map(async (_callData, i) => ops[i].handleCallResultsAsync(this._samplerContract, NULL_BYTES)),
);
return callDatas.map((_callData, i) => ops[i].handleCallResults(NULL_BYTES));
}
// Execute all non-empty calldatas.
const rawCallResults = await this._samplerContract
@ -159,11 +153,9 @@ export class DexOrderSampler {
.callAsync({ overrides }, block);
// Return the parsed results.
let rawCallResultsIdx = 0;
return Promise.all(
callDatas.map(async (callData, i) => {
return callDatas.map((callData, i) => {
const result = callData !== NULL_BYTES ? rawCallResults[rawCallResultsIdx++] : NULL_BYTES;
return ops[i].handleCallResultsAsync(this._samplerContract, result);
}),
);
return ops[i].handleCallResults(result);
});
}
}

View File

@ -0,0 +1,52 @@
import { ContractFunctionObj } from '@0x/base-contract';
import { BigNumber } from '@0x/utils';
import { ERC20BridgeSamplerContract } from '../../wrappers';
import { ERC20BridgeSource, FillData, SourceInfo, SourceQuoteOperation } from './types';
export type Parameters<T> = T extends (...args: infer TArgs) => any ? TArgs : never;
export interface SamplerContractCall<
TFunc extends (...args: any[]) => ContractFunctionObj<any>,
TFillData extends FillData = FillData
> {
contract: ERC20BridgeSamplerContract;
function: TFunc;
params: Parameters<TFunc>;
callback?: (callResults: string, fillData: TFillData) => BigNumber[];
}
export class SamplerContractOperation<
TFunc extends (...args: any[]) => ContractFunctionObj<any>,
TFillData extends FillData = FillData
> implements SourceQuoteOperation<TFillData> {
public readonly source: ERC20BridgeSource;
public fillData: TFillData;
private readonly _samplerContract: ERC20BridgeSamplerContract;
private readonly _samplerFunction: TFunc;
private readonly _params: Parameters<TFunc>;
private readonly _callback?: (callResults: string, fillData: TFillData) => BigNumber[];
constructor(opts: SourceInfo<TFillData> & SamplerContractCall<TFunc, TFillData>) {
this.source = opts.source;
this.fillData = opts.fillData || ({} as TFillData); // tslint:disable-line:no-object-literal-type-assertion
this._samplerContract = opts.contract;
this._samplerFunction = opts.function;
this._params = opts.params;
this._callback = opts.callback;
}
public encodeCall(): string {
return this._samplerFunction
.bind(this._samplerContract)(...this._params)
.getABIEncodedTransactionData();
}
public handleCallResults(callResults: string): BigNumber[] {
if (this._callback !== undefined) {
return this._callback(callResults, this.fillData);
} else {
return this._samplerContract.getABIDecodedReturnData<BigNumber[]>(this._samplerFunction.name, callResults);
}
}
}

View File

@ -4,7 +4,6 @@ import { BigNumber } from '@0x/utils';
import { RfqtRequestOpts, SignedOrderWithFillableAmounts } from '../../types';
import { QuoteRequestor } from '../../utils/quote_requestor';
import { ERC20BridgeSamplerContract } from '../../wrappers';
import { QuoteReport } from '../quote_report_generator';
/**
@ -41,6 +40,7 @@ export enum ERC20BridgeSource {
Bancor = 'Bancor',
MStable = 'mStable',
Mooniswap = 'Mooniswap',
MultiHop = 'MultiHop',
}
// tslint:disable: enum-naming
@ -72,6 +72,11 @@ export interface CurveInfo {
// Internal `fillData` field for `Fill` objects.
export interface FillData {}
export interface SourceInfo<TFillData extends FillData = FillData> {
source: ERC20BridgeSource;
fillData?: TFillData;
}
// `FillData` for native fills.
export interface NativeFillData extends FillData {
order: SignedOrderWithFillableAmounts;
@ -108,14 +113,23 @@ export interface Quote<TFillData = FillData> {
fillData?: TFillData;
}
export interface HopInfo {
sourceIndex: BigNumber;
returnData: string;
}
export interface MultiHopFillData extends FillData {
firstHopSource: SourceQuoteOperation;
secondHopSource: SourceQuoteOperation;
intermediateToken: string;
}
/**
* Represents an individual DEX sample from the sampler contract.
*/
export interface DexSample<TFillData extends FillData = FillData> {
source: ERC20BridgeSource;
export interface DexSample<TFillData extends FillData = FillData> extends SourceInfo<TFillData> {
input: BigNumber;
output: BigNumber;
fillData?: TFillData;
}
/**
@ -131,7 +145,7 @@ export enum FillFlags {
/**
* Represents a node on a fill path.
*/
export interface Fill<TFillData extends FillData = FillData> {
export interface Fill<TFillData extends FillData = FillData> extends SourceInfo<TFillData> {
// Unique ID of the original source path this fill belongs to.
// This is generated when the path is generated and is useful to distinguish
// paths that have the same `source` IDs but are distinct (e.g., Curves).
@ -148,25 +162,16 @@ export interface Fill<TFillData extends FillData = FillData> {
parent?: Fill;
// The index of the fill in the original path.
index: number;
// The source of the fill. See `ERC20BridgeSource`.
source: ERC20BridgeSource;
// Data associated with this this Fill object. Used to reconstruct orders
// from paths.
fillData?: TFillData;
}
/**
* Represents continguous fills on a path that have been merged together.
*/
export interface CollapsedFill<TFillData extends FillData = FillData> {
export interface CollapsedFill<TFillData extends FillData = FillData> extends SourceInfo<TFillData> {
// Unique ID of the original source path this fill belongs to.
// This is generated when the path is generated and is useful to distinguish
// paths that have the same `source` IDs but are distinct (e.g., Curves).
sourcePathId: string;
/**
* The source DEX.
*/
source: ERC20BridgeSource;
/**
* Total input amount (sum of `subFill`s)
*/
@ -182,8 +187,6 @@ export interface CollapsedFill<TFillData extends FillData = FillData> {
input: BigNumber;
output: BigNumber;
}>;
fillData?: TFillData;
}
/**
@ -273,17 +276,19 @@ export interface GetMarketOrdersOpts {
* A composable operation the be run in `DexOrderSampler.executeAsync()`.
*/
export interface BatchedOperation<TResult> {
encodeCall(contract: ERC20BridgeSamplerContract): string;
handleCallResultsAsync(contract: ERC20BridgeSamplerContract, callResults: string): Promise<TResult>;
encodeCall(): string;
handleCallResults(callResults: string): TResult;
}
export interface SourceQuoteOperation<TFillData extends FillData = FillData>
extends BatchedOperation<Array<Quote<TFillData>>> {
source: ERC20BridgeSource;
extends BatchedOperation<BigNumber[]>,
SourceInfo<TFillData> {
readonly source: ERC20BridgeSource;
}
export interface OptimizedOrdersAndQuoteReport {
export interface OptimizerResult {
optimizedOrders: OptimizedMarketOrder[];
isTwoHop: boolean;
quoteReport: QuoteReport;
}
@ -305,4 +310,9 @@ export interface MarketSideLiquidity {
ethToOutputRate: BigNumber;
ethToInputRate: BigNumber;
rfqtIndicativeQuotes: RFQTIndicativeQuote[];
twoHopQuotes: Array<DexSample<MultiHopFillData>>;
}
export interface TokenAdjacencyGraph {
[token: string]: string[];
}

View File

@ -1,11 +1,17 @@
import { orderHashUtils } from '@0x/order-utils';
import { SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';
import * as _ from 'lodash';
import { ERC20BridgeSource, SignedOrder } from '..';
import { MarketOperation } from '../types';
import { CollapsedFill, DexSample, NativeCollapsedFill } from './market_operation_utils/types';
import {
CollapsedFill,
DexSample,
ERC20BridgeSource,
MultiHopFillData,
NativeCollapsedFill,
} from './market_operation_utils/types';
import { QuoteRequestor } from './quote_requestor';
export interface BridgeReportSource {
@ -14,6 +20,13 @@ export interface BridgeReportSource {
takerAmount: BigNumber;
}
export interface MultiHopReportSource {
liquiditySource: ERC20BridgeSource.MultiHop;
makerAmount: BigNumber;
takerAmount: BigNumber;
hopSources: ERC20BridgeSource[];
}
interface NativeReportSourceBase {
liquiditySource: ERC20BridgeSource.Native;
makerAmount: BigNumber;
@ -29,7 +42,11 @@ export interface NativeRFQTReportSource extends NativeReportSourceBase {
isRfqt: true;
makerUri: string;
}
export type QuoteReportSource = BridgeReportSource | NativeOrderbookReportSource | NativeRFQTReportSource;
export type QuoteReportSource =
| BridgeReportSource
| NativeOrderbookReportSource
| NativeRFQTReportSource
| MultiHopReportSource;
export interface QuoteReport {
sourcesConsidered: QuoteReportSource[];
@ -47,62 +64,70 @@ const nativeOrderFromCollapsedFill = (cf: CollapsedFill): SignedOrder | undefine
}
};
export class QuoteReportGenerator {
private readonly _dexQuotes: DexSample[];
private readonly _nativeOrders: SignedOrder[];
private readonly _orderHashesToFillableAmounts: { [orderHash: string]: BigNumber };
private readonly _marketOperation: MarketOperation;
private readonly _collapsedFills: CollapsedFill[];
private readonly _quoteRequestor?: QuoteRequestor;
constructor(
/**
* Generates a report of sources considered while computing the optimized
* swap quote, and the sources ultimately included in the computed quote.
*/
export function generateQuoteReport(
marketOperation: MarketOperation,
dexQuotes: DexSample[],
multiHopQuotes: Array<DexSample<MultiHopFillData>>,
nativeOrders: SignedOrder[],
orderFillableAmounts: BigNumber[],
collapsedFills: CollapsedFill[],
liquidityDelivered: CollapsedFill[] | DexSample<MultiHopFillData>,
quoteRequestor?: QuoteRequestor,
) {
this._dexQuotes = dexQuotes;
this._nativeOrders = nativeOrders;
this._marketOperation = marketOperation;
this._quoteRequestor = quoteRequestor;
this._collapsedFills = collapsedFills;
): QuoteReport {
// convert order fillable amount array to easy to look up hash
if (orderFillableAmounts.length !== nativeOrders.length) {
// length mismatch, abort
this._orderHashesToFillableAmounts = {};
return;
throw new Error('orderFillableAmounts must be the same length as nativeOrders');
}
const orderHashesToFillableAmounts: { [orderHash: string]: BigNumber } = {};
nativeOrders.forEach((nativeOrder, idx) => {
orderHashesToFillableAmounts[orderHashUtils.getOrderHash(nativeOrder)] = orderFillableAmounts[idx];
});
this._orderHashesToFillableAmounts = orderHashesToFillableAmounts;
}
public generateReport(): QuoteReport {
const dexReportSourcesConsidered = this._dexQuotes.map(dq => this._dexSampleToReportSource(dq));
const nativeOrderSourcesConsidered = this._nativeOrders.map(no => this._nativeOrderToReportSource(no));
const dexReportSourcesConsidered = dexQuotes.map(quote => _dexSampleToReportSource(quote, marketOperation));
const nativeOrderSourcesConsidered = nativeOrders.map(order =>
_nativeOrderToReportSource(
order,
orderHashesToFillableAmounts[orderHashUtils.getOrderHash(order)],
quoteRequestor,
),
);
const multiHopSourcesConsidered = multiHopQuotes.map(quote =>
_multiHopSampleToReportSource(quote, marketOperation),
);
const sourcesConsidered = [
...dexReportSourcesConsidered,
...nativeOrderSourcesConsidered,
...multiHopSourcesConsidered,
];
const sourcesConsidered = [...dexReportSourcesConsidered, ...nativeOrderSourcesConsidered];
const sourcesDelivered = this._collapsedFills.map(collapsedFill => {
let sourcesDelivered;
if (Array.isArray(liquidityDelivered)) {
sourcesDelivered = liquidityDelivered.map(collapsedFill => {
const foundNativeOrder = nativeOrderFromCollapsedFill(collapsedFill);
if (foundNativeOrder) {
return this._nativeOrderToReportSource(foundNativeOrder);
return _nativeOrderToReportSource(
foundNativeOrder,
orderHashesToFillableAmounts[orderHashUtils.getOrderHash(foundNativeOrder)],
quoteRequestor,
);
} else {
return this._dexSampleToReportSource(collapsedFill);
return _dexSampleToReportSource(collapsedFill, marketOperation);
}
});
} else {
sourcesDelivered = [_multiHopSampleToReportSource(liquidityDelivered, marketOperation)];
}
return {
sourcesConsidered,
sourcesDelivered,
};
}
}
private _dexSampleToReportSource(ds: DexSample): BridgeReportSource {
function _dexSampleToReportSource(ds: DexSample, marketOperation: MarketOperation): BridgeReportSource {
const liquiditySource = ds.source;
if (liquiditySource === ERC20BridgeSource.Native) {
@ -111,37 +136,67 @@ export class QuoteReportGenerator {
// input and output map to different values
// based on the market operation
if (this._marketOperation === MarketOperation.Buy) {
if (marketOperation === MarketOperation.Buy) {
return {
makerAmount: ds.input,
takerAmount: ds.output,
liquiditySource,
};
} else if (this._marketOperation === MarketOperation.Sell) {
} else if (marketOperation === MarketOperation.Sell) {
return {
makerAmount: ds.output,
takerAmount: ds.input,
liquiditySource,
};
} else {
throw new Error(`Unexpected marketOperation ${this._marketOperation}`);
}
throw new Error(`Unexpected marketOperation ${marketOperation}`);
}
}
private _nativeOrderToReportSource(nativeOrder: SignedOrder): NativeRFQTReportSource | NativeOrderbookReportSource {
function _multiHopSampleToReportSource(
ds: DexSample<MultiHopFillData>,
marketOperation: MarketOperation,
): MultiHopReportSource {
const { firstHopSource: firstHop, secondHopSource: secondHop } = ds.fillData!;
// input and output map to different values
// based on the market operation
if (marketOperation === MarketOperation.Buy) {
return {
liquiditySource: ERC20BridgeSource.MultiHop,
makerAmount: ds.input,
takerAmount: ds.output,
hopSources: [firstHop.source, secondHop.source],
};
} else if (marketOperation === MarketOperation.Sell) {
return {
liquiditySource: ERC20BridgeSource.MultiHop,
makerAmount: ds.output,
takerAmount: ds.input,
hopSources: [firstHop.source, secondHop.source],
};
} else {
throw new Error(`Unexpected marketOperation ${marketOperation}`);
}
}
function _nativeOrderToReportSource(
nativeOrder: SignedOrder,
fillableAmount: BigNumber,
quoteRequestor?: QuoteRequestor,
): NativeRFQTReportSource | NativeOrderbookReportSource {
const orderHash = orderHashUtils.getOrderHash(nativeOrder);
const nativeOrderBase: NativeReportSourceBase = {
liquiditySource: ERC20BridgeSource.Native,
makerAmount: nativeOrder.makerAssetAmount,
takerAmount: nativeOrder.takerAssetAmount,
fillableTakerAmount: this._orderHashesToFillableAmounts[orderHash],
fillableTakerAmount: fillableAmount,
nativeOrder,
orderHash,
};
// if we find this is an rfqt order, label it as such and associate makerUri
const foundRfqtMakerUri = this._quoteRequestor && this._quoteRequestor.getMakerUriForOrderHash(orderHash);
const foundRfqtMakerUri = quoteRequestor && quoteRequestor.getMakerUriForOrderHash(orderHash);
if (foundRfqtMakerUri) {
const rfqtSource: NativeRFQTReportSource = {
...nativeOrderBase,
@ -157,5 +212,4 @@ export class QuoteReportGenerator {
};
return regularNativeOrder;
}
}
}

View File

@ -355,10 +355,8 @@ export class QuoteRequestor {
switch (quoteType) {
case 'firm':
return 'quote';
break;
case 'indicative':
return 'price';
break;
default:
throw new Error(`Unexpected quote type ${quoteType}`);
}

View File

@ -100,10 +100,11 @@ export function simulateBestCaseFill(quoteInfo: QuoteFillInfo): QuoteFillResult
...DEFAULT_SIMULATED_FILL_QUOTE_INFO_OPTS,
...quoteInfo.opts,
};
const protocolFeePerFillOrder = quoteInfo.gasPrice.times(opts.protocolFeeMultiplier);
const result = fillQuoteOrders(
createBestCaseFillOrderCalls(quoteInfo),
quoteInfo.fillAmount,
quoteInfo.gasPrice.times(opts.protocolFeeMultiplier),
protocolFeePerFillOrder,
opts.gasSchedule,
);
return fromIntermediateQuoteFillResult(result, quoteInfo);

View File

@ -3,6 +3,7 @@ import { AssetProxyId, SignedOrder } from '@0x/types';
import { BigNumber } from '@0x/utils';
import * as _ from 'lodash';
import { constants } from '../constants';
import {
CalculateSwapQuoteOpts,
MarketBuySwapQuote,
@ -16,8 +17,14 @@ import {
import { MarketOperationUtils } from './market_operation_utils';
import { convertNativeOrderToFullyFillableOptimizedOrders } from './market_operation_utils/orders';
import { FeeSchedule, FillData, GetMarketOrdersOpts, OptimizedMarketOrder } from './market_operation_utils/types';
import { isSupportedAssetDataInOrders } from './utils';
import {
ERC20BridgeSource,
FeeSchedule,
FillData,
GetMarketOrdersOpts,
OptimizedMarketOrder,
} from './market_operation_utils/types';
import { getTokenFromAssetData, isSupportedAssetDataInOrders } from './utils';
import { QuoteReport } from './quote_report_generator';
import { QuoteFillResult, simulateBestCaseFill, simulateWorstCaseFill } from './quote_simulation';
@ -121,8 +128,9 @@ export class SwapQuoteCalculator {
}
// since prunedOrders do not have fillState, we will add a buffer of fillable orders to consider that some native are orders are partially filled
let optimizedOrders: OptimizedMarketOrder[] | undefined;
let optimizedOrders: OptimizedMarketOrder[];
let quoteReport: QuoteReport | undefined;
let isTwoHop = false;
{
// Scale fees by gas price.
@ -149,6 +157,7 @@ export class SwapQuoteCalculator {
);
optimizedOrders = buyResult.optimizedOrders;
quoteReport = buyResult.quoteReport;
isTwoHop = buyResult.isTwoHop;
} else {
const sellResult = await this._marketOperationUtils.getMarketSellOrdersAsync(
prunedOrders,
@ -157,13 +166,25 @@ export class SwapQuoteCalculator {
);
optimizedOrders = sellResult.optimizedOrders;
quoteReport = sellResult.quoteReport;
isTwoHop = sellResult.isTwoHop;
}
}
}
// assetData information for the result
const { makerAssetData, takerAssetData } = prunedOrders[0];
return createSwapQuote(
return isTwoHop
? createTwoHopSwapQuote(
makerAssetData,
takerAssetData,
optimizedOrders,
operation,
assetFillAmount,
gasPrice,
opts.gasSchedule,
quoteReport,
)
: createSwapQuote(
makerAssetData,
takerAssetData,
optimizedOrders,
@ -211,6 +232,7 @@ function createSwapQuote(
sourceBreakdown: getSwapQuoteOrdersBreakdown(bestCaseFillResult.fillAmountBySource),
orders: optimizedOrders,
quoteReport,
isTwoHop: false,
};
if (operation === MarketOperation.Buy) {
@ -218,14 +240,79 @@ function createSwapQuote(
...quoteBase,
type: MarketOperation.Buy,
makerAssetFillAmount: assetFillAmount,
quoteReport,
};
} else {
return {
...quoteBase,
type: MarketOperation.Sell,
takerAssetFillAmount: assetFillAmount,
};
}
}
function createTwoHopSwapQuote(
makerAssetData: string,
takerAssetData: string,
optimizedOrders: OptimizedMarketOrder[],
operation: MarketOperation,
assetFillAmount: BigNumber,
gasPrice: BigNumber,
gasSchedule: FeeSchedule,
quoteReport?: QuoteReport,
): SwapQuote {
const [firstHopOrder, secondHopOrder] = optimizedOrders;
const [firstHopFill] = firstHopOrder.fills;
const [secondHopFill] = secondHopOrder.fills;
const gas = new BigNumber(
gasSchedule[ERC20BridgeSource.MultiHop]!({
firstHopSource: _.pick(firstHopFill, 'source', 'fillData'),
secondHopSource: _.pick(secondHopFill, 'source', 'fillData'),
}),
).toNumber();
const quoteBase = {
takerAssetData,
makerAssetData,
gasPrice,
bestCaseQuoteInfo: {
makerAssetAmount: operation === MarketOperation.Sell ? secondHopFill.output : secondHopFill.input,
takerAssetAmount: operation === MarketOperation.Sell ? firstHopFill.input : firstHopFill.output,
totalTakerAssetAmount: operation === MarketOperation.Sell ? firstHopFill.input : firstHopFill.output,
feeTakerAssetAmount: constants.ZERO_AMOUNT,
protocolFeeInWeiAmount: constants.ZERO_AMOUNT,
gas,
},
worstCaseQuoteInfo: {
makerAssetAmount: secondHopOrder.makerAssetAmount,
takerAssetAmount: firstHopOrder.takerAssetAmount,
totalTakerAssetAmount: firstHopOrder.takerAssetAmount,
feeTakerAssetAmount: constants.ZERO_AMOUNT,
protocolFeeInWeiAmount: constants.ZERO_AMOUNT,
gas,
},
sourceBreakdown: {
[ERC20BridgeSource.MultiHop]: {
proportion: new BigNumber(1),
intermediateToken: getTokenFromAssetData(secondHopOrder.takerAssetData),
hops: [firstHopFill.source, secondHopFill.source],
},
},
orders: optimizedOrders,
quoteReport,
isTwoHop: true,
};
if (operation === MarketOperation.Buy) {
return {
...quoteBase,
type: MarketOperation.Buy,
makerAssetFillAmount: assetFillAmount,
};
} else {
return {
...quoteBase,
type: MarketOperation.Sell,
takerAssetFillAmount: assetFillAmount,
};
}
}
@ -234,7 +321,7 @@ function getSwapQuoteOrdersBreakdown(fillAmountBySource: { [source: string]: Big
const totalFillAmount = BigNumber.sum(...Object.values(fillAmountBySource));
const breakdown: SwapQuoteOrdersBreakdown = {};
Object.entries(fillAmountBySource).forEach(([source, fillAmount]) => {
breakdown[source] = fillAmount.div(totalFillAmount);
breakdown[source as keyof SwapQuoteOrdersBreakdown] = fillAmount.div(totalFillAmount);
});
return breakdown;
}

View File

@ -113,3 +113,12 @@ export function isERC20EquivalentAssetData(assetData: AssetData): assetData is E
export function difference<T>(a: T[], b: T[]): T[] {
return a.filter(x => b.indexOf(x) === -1);
}
export function getTokenFromAssetData(assetData: string): string {
const data = assetDataUtils.decodeAssetDataOrThrow(assetData);
if (data.assetProxyId !== AssetProxyId.ERC20 && data.assetProxyId !== AssetProxyId.ERC20Bridge) {
throw new Error(`Unsupported exchange proxy quote asset type: ${data.assetProxyId}`);
}
// tslint:disable-next-line:no-unnecessary-type-assertion
return (data as ERC20AssetData).tokenAddress;
}

View File

@ -6,11 +6,13 @@
import { ContractArtifact } from 'ethereum-types';
import * as ApproximateBuys from '../test/generated-artifacts/ApproximateBuys.json';
import * as BalancerSampler from '../test/generated-artifacts/BalancerSampler.json';
import * as CurveSampler from '../test/generated-artifacts/CurveSampler.json';
import * as DummyLiquidityProvider from '../test/generated-artifacts/DummyLiquidityProvider.json';
import * as DummyLiquidityProviderRegistry from '../test/generated-artifacts/DummyLiquidityProviderRegistry.json';
import * as ERC20BridgeSampler from '../test/generated-artifacts/ERC20BridgeSampler.json';
import * as Eth2DaiSampler from '../test/generated-artifacts/Eth2DaiSampler.json';
import * as IBalancer from '../test/generated-artifacts/IBalancer.json';
import * as ICurve from '../test/generated-artifacts/ICurve.json';
import * as IEth2Dai from '../test/generated-artifacts/IEth2Dai.json';
import * as IKyberHintHandler from '../test/generated-artifacts/IKyberHintHandler.json';
@ -33,15 +35,27 @@ import * as NativeOrderSampler from '../test/generated-artifacts/NativeOrderSamp
import * as SamplerUtils from '../test/generated-artifacts/SamplerUtils.json';
import * as TestERC20BridgeSampler from '../test/generated-artifacts/TestERC20BridgeSampler.json';
import * as TestNativeOrderSampler from '../test/generated-artifacts/TestNativeOrderSampler.json';
import * as TwoHopSampler from '../test/generated-artifacts/TwoHopSampler.json';
import * as UniswapSampler from '../test/generated-artifacts/UniswapSampler.json';
import * as UniswapV2Sampler from '../test/generated-artifacts/UniswapV2Sampler.json';
export const artifacts = {
ApproximateBuys: ApproximateBuys as ContractArtifact,
BalancerSampler: BalancerSampler as ContractArtifact,
CurveSampler: CurveSampler as ContractArtifact,
DummyLiquidityProvider: DummyLiquidityProvider as ContractArtifact,
DummyLiquidityProviderRegistry: DummyLiquidityProviderRegistry as ContractArtifact,
ERC20BridgeSampler: ERC20BridgeSampler as ContractArtifact,
Eth2DaiSampler: Eth2DaiSampler as ContractArtifact,
IMooniswap: IMooniswap as ContractArtifact,
KyberSampler: KyberSampler as ContractArtifact,
LiquidityProviderSampler: LiquidityProviderSampler as ContractArtifact,
MStableSampler: MStableSampler as ContractArtifact,
MooniswapSampler: MooniswapSampler as ContractArtifact,
MultiBridgeSampler: MultiBridgeSampler as ContractArtifact,
NativeOrderSampler: NativeOrderSampler as ContractArtifact,
SamplerUtils: SamplerUtils as ContractArtifact,
TwoHopSampler: TwoHopSampler as ContractArtifact,
UniswapSampler: UniswapSampler as ContractArtifact,
UniswapV2Sampler: UniswapV2Sampler as ContractArtifact,
IBalancer: IBalancer as ContractArtifact,
ICurve: ICurve as ContractArtifact,
IEth2Dai: IEth2Dai as ContractArtifact,
IKyberHintHandler: IKyberHintHandler as ContractArtifact,
@ -51,19 +65,11 @@ export const artifacts = {
ILiquidityProvider: ILiquidityProvider as ContractArtifact,
ILiquidityProviderRegistry: ILiquidityProviderRegistry as ContractArtifact,
IMStable: IMStable as ContractArtifact,
IMooniswap: IMooniswap as ContractArtifact,
IMultiBridge: IMultiBridge as ContractArtifact,
IUniswapExchangeQuotes: IUniswapExchangeQuotes as ContractArtifact,
IUniswapV2Router01: IUniswapV2Router01 as ContractArtifact,
KyberSampler: KyberSampler as ContractArtifact,
LiquidityProviderSampler: LiquidityProviderSampler as ContractArtifact,
MStableSampler: MStableSampler as ContractArtifact,
MooniswapSampler: MooniswapSampler as ContractArtifact,
MultiBridgeSampler: MultiBridgeSampler as ContractArtifact,
NativeOrderSampler: NativeOrderSampler as ContractArtifact,
SamplerUtils: SamplerUtils as ContractArtifact,
UniswapSampler: UniswapSampler as ContractArtifact,
UniswapV2Sampler: UniswapV2Sampler as ContractArtifact,
DummyLiquidityProvider: DummyLiquidityProvider as ContractArtifact,
DummyLiquidityProviderRegistry: DummyLiquidityProviderRegistry as ContractArtifact,
TestERC20BridgeSampler: TestERC20BridgeSampler as ContractArtifact,
TestNativeOrderSampler: TestNativeOrderSampler as ContractArtifact,
};

View File

@ -37,6 +37,7 @@ blockchainTests('erc20-bridge-sampler', env => {
const INVALID_TOKEN_PAIR_ERROR = 'ERC20BridgeSampler/INVALID_TOKEN_PAIR';
const MAKER_TOKEN = randomAddress();
const TAKER_TOKEN = randomAddress();
const INTERMEDIATE_TOKEN = randomAddress();
before(async () => {
testContract = await TestERC20BridgeSamplerContract.deployFrom0xArtifactAsync(
@ -262,6 +263,33 @@ blockchainTests('erc20-bridge-sampler', env => {
await testContract.enableFailTrigger().awaitTransactionSuccessAsync({ value: 1 });
}
function expectQuotesWithinRange(
quotes: BigNumber[],
expectedQuotes: BigNumber[],
maxSlippage: BigNumber | number,
): void {
quotes.forEach((_q, i) => {
// If we're within 1 base unit of a low decimal token
// then that's as good as we're going to get (and slippage is "high")
if (
expectedQuotes[i].isZero() ||
BigNumber.max(expectedQuotes[i], quotes[i])
.minus(BigNumber.min(expectedQuotes[i], quotes[i]))
.eq(1)
) {
return;
}
const slippage = quotes[i]
.dividedBy(expectedQuotes[i])
.minus(1)
.decimalPlaces(4);
expect(slippage, `quote[${i}]: ${slippage} ${quotes[i]} ${expectedQuotes[i]}`).to.be.bignumber.gte(0);
expect(slippage, `quote[${i}] ${slippage} ${quotes[i]} ${expectedQuotes[i]}`).to.be.bignumber.lte(
new BigNumber(maxSlippage),
);
});
}
describe('getOrderFillableTakerAssetAmounts()', () => {
it('returns the expected amount for each order', async () => {
const orders = createOrders(MAKER_TOKEN, TAKER_TOKEN);
@ -385,32 +413,6 @@ blockchainTests('erc20-bridge-sampler', env => {
const quotes = await testContract.sampleBuysFromKyberNetwork(TAKER_TOKEN, MAKER_TOKEN, []).callAsync();
expect(quotes).to.deep.eq([]);
});
const expectQuotesWithinRange = (
quotes: BigNumber[],
expectedQuotes: BigNumber[],
maxSlippage: BigNumber | number,
) => {
quotes.forEach((_q, i) => {
// If we're within 1 base unit of a low decimal token
// then that's as good as we're going to get (and slippage is "high")
if (
expectedQuotes[i].isZero() ||
BigNumber.max(expectedQuotes[i], quotes[i])
.minus(BigNumber.min(expectedQuotes[i], quotes[i]))
.eq(1)
) {
return;
}
const slippage = quotes[i]
.dividedBy(expectedQuotes[i])
.minus(1)
.decimalPlaces(4);
expect(slippage, `quote[${i}]: ${slippage} ${quotes[i]} ${expectedQuotes[i]}`).to.be.bignumber.gte(0);
expect(slippage, `quote[${i}] ${slippage} ${quotes[i]} ${expectedQuotes[i]}`).to.be.bignumber.lte(
new BigNumber(maxSlippage),
);
});
};
it('can quote token -> token', async () => {
const sampleAmounts = getSampleAmounts(TAKER_TOKEN);
@ -803,7 +805,7 @@ blockchainTests('erc20-bridge-sampler', env => {
});
});
describe('getLiquidityProviderFromRegistry', () => {
describe('liquidity provider', () => {
const xAsset = randomAddress();
const yAsset = randomAddress();
const sampleAmounts = getSampleAmounts(yAsset);
@ -829,42 +831,28 @@ blockchainTests('erc20-bridge-sampler', env => {
.awaitTransactionSuccessAsync();
});
it('should be able to get the liquidity provider', async () => {
const xyLiquidityProvider = await testContract
.getLiquidityProviderFromRegistry(registryContract.address, xAsset, yAsset)
.callAsync();
const yxLiquidityProvider = await testContract
.getLiquidityProviderFromRegistry(registryContract.address, yAsset, xAsset)
.callAsync();
const unknownLiquidityProvider = await testContract
.getLiquidityProviderFromRegistry(registryContract.address, yAsset, randomAddress())
.callAsync();
expect(xyLiquidityProvider).to.eq(liquidityProvider.address);
expect(yxLiquidityProvider).to.eq(liquidityProvider.address);
expect(unknownLiquidityProvider).to.eq(constants.NULL_ADDRESS);
});
it('should be able to query sells from the liquidity provider', async () => {
const result = await testContract
const [quotes, providerAddress] = await testContract
.sampleSellsFromLiquidityProviderRegistry(registryContract.address, yAsset, xAsset, sampleAmounts)
.callAsync();
result.forEach((value, idx) => {
quotes.forEach((value, idx) => {
expect(value).is.bignumber.eql(sampleAmounts[idx].minus(1));
});
expect(providerAddress).to.equal(liquidityProvider.address);
});
it('should be able to query buys from the liquidity provider', async () => {
const result = await testContract
const [quotes, providerAddress] = await testContract
.sampleBuysFromLiquidityProviderRegistry(registryContract.address, yAsset, xAsset, sampleAmounts)
.callAsync();
result.forEach((value, idx) => {
quotes.forEach((value, idx) => {
expect(value).is.bignumber.eql(sampleAmounts[idx].plus(1));
});
expect(providerAddress).to.equal(liquidityProvider.address);
});
it('should just return zeros if the liquidity provider cannot be found', async () => {
const result = await testContract
const [quotes, providerAddress] = await testContract
.sampleBuysFromLiquidityProviderRegistry(
registryContract.address,
yAsset,
@ -872,18 +860,20 @@ blockchainTests('erc20-bridge-sampler', env => {
sampleAmounts,
)
.callAsync();
result.forEach(value => {
quotes.forEach(value => {
expect(value).is.bignumber.eql(constants.ZERO_AMOUNT);
});
expect(providerAddress).to.equal(constants.NULL_ADDRESS);
});
it('should just return zeros if the registry does not exist', async () => {
const result = await testContract
const [quotes, providerAddress] = await testContract
.sampleBuysFromLiquidityProviderRegistry(randomAddress(), yAsset, xAsset, sampleAmounts)
.callAsync();
result.forEach(value => {
quotes.forEach(value => {
expect(value).is.bignumber.eql(constants.ZERO_AMOUNT);
});
expect(providerAddress).to.equal(constants.NULL_ADDRESS);
});
});
@ -1033,4 +1023,116 @@ blockchainTests('erc20-bridge-sampler', env => {
);
});
});
blockchainTests.resets('TwoHopSampler', () => {
before(async () => {
await testContract
.createTokenExchanges([MAKER_TOKEN, TAKER_TOKEN, INTERMEDIATE_TOKEN])
.awaitTransactionSuccessAsync();
});
it('sampleTwoHopSell', async () => {
// tslint:disable-next-line no-unnecessary-type-assertion
const sellAmount = _.last(getSampleAmounts(TAKER_TOKEN))!;
const uniswapV2FirstHopPath = [TAKER_TOKEN, INTERMEDIATE_TOKEN];
const uniswapV2FirstHop = testContract
.sampleSellsFromUniswapV2(uniswapV2FirstHopPath, [constants.ZERO_AMOUNT])
.getABIEncodedTransactionData();
const uniswapV2SecondHopPath = [INTERMEDIATE_TOKEN, randomAddress(), MAKER_TOKEN];
const uniswapV2SecondHop = testContract
.sampleSellsFromUniswapV2(uniswapV2SecondHopPath, [constants.ZERO_AMOUNT])
.getABIEncodedTransactionData();
const eth2DaiFirstHop = testContract
.sampleSellsFromEth2Dai(TAKER_TOKEN, INTERMEDIATE_TOKEN, [constants.ZERO_AMOUNT])
.getABIEncodedTransactionData();
const eth2DaiSecondHop = testContract
.sampleSellsFromEth2Dai(INTERMEDIATE_TOKEN, MAKER_TOKEN, [constants.ZERO_AMOUNT])
.getABIEncodedTransactionData();
const firstHopQuotes = [
getDeterministicSellQuote(ETH2DAI_SALT, TAKER_TOKEN, INTERMEDIATE_TOKEN, sellAmount),
getDeterministicUniswapV2SellQuote(uniswapV2FirstHopPath, sellAmount),
];
const expectedIntermediateAssetAmount = BigNumber.max(...firstHopQuotes);
const secondHopQuotes = [
getDeterministicSellQuote(
ETH2DAI_SALT,
INTERMEDIATE_TOKEN,
MAKER_TOKEN,
expectedIntermediateAssetAmount,
),
getDeterministicUniswapV2SellQuote(uniswapV2SecondHopPath, expectedIntermediateAssetAmount),
];
const expectedBuyAmount = BigNumber.max(...secondHopQuotes);
const [firstHop, secondHop, buyAmount] = await testContract
.sampleTwoHopSell(
[eth2DaiFirstHop, uniswapV2FirstHop],
[eth2DaiSecondHop, uniswapV2SecondHop],
sellAmount,
)
.callAsync();
expect(firstHop.sourceIndex, 'First hop source index').to.bignumber.equal(
firstHopQuotes.findIndex(quote => quote.isEqualTo(expectedIntermediateAssetAmount)),
);
expect(secondHop.sourceIndex, 'Second hop source index').to.bignumber.equal(
secondHopQuotes.findIndex(quote => quote.isEqualTo(expectedBuyAmount)),
);
expect(buyAmount, 'Two hop buy amount').to.bignumber.equal(expectedBuyAmount);
});
it('sampleTwoHopBuy', async () => {
// tslint:disable-next-line no-unnecessary-type-assertion
const buyAmount = _.last(getSampleAmounts(MAKER_TOKEN))!;
const uniswapV2FirstHopPath = [TAKER_TOKEN, INTERMEDIATE_TOKEN];
const uniswapV2FirstHop = testContract
.sampleBuysFromUniswapV2(uniswapV2FirstHopPath, [constants.ZERO_AMOUNT])
.getABIEncodedTransactionData();
const uniswapV2SecondHopPath = [INTERMEDIATE_TOKEN, randomAddress(), MAKER_TOKEN];
const uniswapV2SecondHop = testContract
.sampleBuysFromUniswapV2(uniswapV2SecondHopPath, [constants.ZERO_AMOUNT])
.getABIEncodedTransactionData();
const eth2DaiFirstHop = testContract
.sampleBuysFromEth2Dai(TAKER_TOKEN, INTERMEDIATE_TOKEN, [constants.ZERO_AMOUNT])
.getABIEncodedTransactionData();
const eth2DaiSecondHop = testContract
.sampleBuysFromEth2Dai(INTERMEDIATE_TOKEN, MAKER_TOKEN, [constants.ZERO_AMOUNT])
.getABIEncodedTransactionData();
const secondHopQuotes = [
getDeterministicBuyQuote(ETH2DAI_SALT, INTERMEDIATE_TOKEN, MAKER_TOKEN, buyAmount),
getDeterministicUniswapV2BuyQuote(uniswapV2SecondHopPath, buyAmount),
];
const expectedIntermediateAssetAmount = BigNumber.min(...secondHopQuotes);
const firstHopQuotes = [
getDeterministicBuyQuote(
ETH2DAI_SALT,
TAKER_TOKEN,
INTERMEDIATE_TOKEN,
expectedIntermediateAssetAmount,
),
getDeterministicUniswapV2BuyQuote(uniswapV2FirstHopPath, expectedIntermediateAssetAmount),
];
const expectedSellAmount = BigNumber.min(...firstHopQuotes);
const [firstHop, secondHop, sellAmount] = await testContract
.sampleTwoHopBuy(
[eth2DaiFirstHop, uniswapV2FirstHop],
[eth2DaiSecondHop, uniswapV2SecondHop],
buyAmount,
)
.callAsync();
expect(firstHop.sourceIndex, 'First hop source index').to.bignumber.equal(
firstHopQuotes.findIndex(quote => quote.isEqualTo(expectedSellAmount)),
);
expect(secondHop.sourceIndex, 'Second hop source index').to.bignumber.equal(
secondHopQuotes.findIndex(quote => quote.isEqualTo(expectedIntermediateAssetAmount)),
);
expect(sellAmount, 'Two hop sell amount').to.bignumber.equal(expectedSellAmount);
});
});
});

View File

@ -18,7 +18,7 @@ import {
computeBalancerSellQuote,
} from '../src/utils/market_operation_utils/balancer_utils';
import { DexOrderSampler, getSampleAmounts } from '../src/utils/market_operation_utils/sampler';
import { ERC20BridgeSource, FillData } from '../src/utils/market_operation_utils/types';
import { ERC20BridgeSource } from '../src/utils/market_operation_utils/types';
import { MockBalancerPoolsCache } from './utils/mock_balancer_pools_cache';
import { MockBancorService } from './utils/mock_bancor_service';
@ -108,7 +108,7 @@ describe('DexSampler tests', () => {
});
const dexOrderSampler = new DexOrderSampler(sampler);
const [fillableAmounts] = await dexOrderSampler.executeAsync(
DexOrderSampler.ops.getOrderFillableMakerAmounts(ORDERS, exchangeAddress),
dexOrderSampler.getOrderFillableMakerAmounts(ORDERS, exchangeAddress),
);
expect(fillableAmounts).to.deep.eq(expectedFillableAmounts);
});
@ -124,7 +124,7 @@ describe('DexSampler tests', () => {
});
const dexOrderSampler = new DexOrderSampler(sampler);
const [fillableAmounts] = await dexOrderSampler.executeAsync(
DexOrderSampler.ops.getOrderFillableTakerAmounts(ORDERS, exchangeAddress),
dexOrderSampler.getOrderFillableTakerAmounts(ORDERS, exchangeAddress),
);
expect(fillableAmounts).to.deep.eq(expectedFillableAmounts);
});
@ -144,36 +144,32 @@ describe('DexSampler tests', () => {
});
const dexOrderSampler = new DexOrderSampler(sampler);
const [fillableAmounts] = await dexOrderSampler.executeAsync(
DexOrderSampler.ops.getKyberSellQuotes(
expectedMakerToken,
expectedTakerToken,
expectedTakerFillAmounts,
),
dexOrderSampler.getKyberSellQuotes(expectedMakerToken, expectedTakerToken, expectedTakerFillAmounts),
);
expect(fillableAmounts.map(q => q.amount)).to.deep.eq(expectedMakerFillAmounts);
expect(fillableAmounts).to.deep.eq(expectedMakerFillAmounts);
});
it('getLiquidityProviderSellQuotes()', async () => {
const expectedMakerToken = randomAddress();
const expectedTakerToken = randomAddress();
const registry = randomAddress();
const poolAddress = randomAddress();
const sampler = new MockSamplerContract({
sampleSellsFromLiquidityProviderRegistry: (registryAddress, takerToken, makerToken, _fillAmounts) => {
expect(registryAddress).to.eq(registry);
expect(takerToken).to.eq(expectedTakerToken);
expect(makerToken).to.eq(expectedMakerToken);
return [toBaseUnitAmount(1001)];
return [[toBaseUnitAmount(1001)], poolAddress];
},
});
const dexOrderSampler = new DexOrderSampler(sampler);
const [result] = await dexOrderSampler.executeAsync(
await DexOrderSampler.ops.getSellQuotesAsync(
dexOrderSampler.getSellQuotes(
[ERC20BridgeSource.LiquidityProvider],
expectedMakerToken,
expectedTakerToken,
[toBaseUnitAmount(1000)],
wethAddress,
dexOrderSampler.balancerPoolsCache,
registry,
),
);
@ -183,7 +179,7 @@ describe('DexSampler tests', () => {
source: 'LiquidityProvider',
output: toBaseUnitAmount(1001),
input: toBaseUnitAmount(1000),
fillData: undefined,
fillData: { poolAddress },
},
],
]);
@ -193,23 +189,23 @@ describe('DexSampler tests', () => {
const expectedMakerToken = randomAddress();
const expectedTakerToken = randomAddress();
const registry = randomAddress();
const poolAddress = randomAddress();
const sampler = new MockSamplerContract({
sampleBuysFromLiquidityProviderRegistry: (registryAddress, takerToken, makerToken, _fillAmounts) => {
expect(registryAddress).to.eq(registry);
expect(takerToken).to.eq(expectedTakerToken);
expect(makerToken).to.eq(expectedMakerToken);
return [toBaseUnitAmount(999)];
return [[toBaseUnitAmount(999)], poolAddress];
},
});
const dexOrderSampler = new DexOrderSampler(sampler);
const [result] = await dexOrderSampler.executeAsync(
await DexOrderSampler.ops.getBuyQuotesAsync(
dexOrderSampler.getBuyQuotes(
[ERC20BridgeSource.LiquidityProvider],
expectedMakerToken,
expectedTakerToken,
[toBaseUnitAmount(1000)],
wethAddress,
dexOrderSampler.balancerPoolsCache,
registry,
),
);
@ -219,7 +215,7 @@ describe('DexSampler tests', () => {
source: 'LiquidityProvider',
output: toBaseUnitAmount(999),
input: toBaseUnitAmount(1000),
fillData: undefined,
fillData: { poolAddress },
},
],
]);
@ -246,13 +242,12 @@ describe('DexSampler tests', () => {
});
const dexOrderSampler = new DexOrderSampler(sampler);
const [result] = await dexOrderSampler.executeAsync(
await DexOrderSampler.ops.getSellQuotesAsync(
dexOrderSampler.getSellQuotes(
[ERC20BridgeSource.MultiBridge],
expectedMakerToken,
expectedTakerToken,
[toBaseUnitAmount(1000)],
randomAddress(),
dexOrderSampler.balancerPoolsCache,
randomAddress(),
multiBridge,
),
@ -263,7 +258,7 @@ describe('DexSampler tests', () => {
source: 'MultiBridge',
output: toBaseUnitAmount(1001),
input: toBaseUnitAmount(1000),
fillData: undefined,
fillData: { poolAddress: multiBridge },
},
],
]);
@ -284,13 +279,9 @@ describe('DexSampler tests', () => {
});
const dexOrderSampler = new DexOrderSampler(sampler);
const [fillableAmounts] = await dexOrderSampler.executeAsync(
DexOrderSampler.ops.getEth2DaiSellQuotes(
expectedMakerToken,
expectedTakerToken,
expectedTakerFillAmounts,
),
dexOrderSampler.getEth2DaiSellQuotes(expectedMakerToken, expectedTakerToken, expectedTakerFillAmounts),
);
expect(fillableAmounts.map(q => q.amount)).to.deep.eq(expectedMakerFillAmounts);
expect(fillableAmounts).to.deep.eq(expectedMakerFillAmounts);
});
it('getUniswapSellQuotes()', async () => {
@ -308,13 +299,9 @@ describe('DexSampler tests', () => {
});
const dexOrderSampler = new DexOrderSampler(sampler);
const [fillableAmounts] = await dexOrderSampler.executeAsync(
DexOrderSampler.ops.getUniswapSellQuotes(
expectedMakerToken,
expectedTakerToken,
expectedTakerFillAmounts,
),
dexOrderSampler.getUniswapSellQuotes(expectedMakerToken, expectedTakerToken, expectedTakerFillAmounts),
);
expect(fillableAmounts.map(q => q.amount)).to.deep.eq(expectedMakerFillAmounts);
expect(fillableAmounts).to.deep.eq(expectedMakerFillAmounts);
});
it('getUniswapV2SellQuotes()', async () => {
@ -331,12 +318,12 @@ describe('DexSampler tests', () => {
});
const dexOrderSampler = new DexOrderSampler(sampler);
const [fillableAmounts] = await dexOrderSampler.executeAsync(
DexOrderSampler.ops.getUniswapV2SellQuotes(
dexOrderSampler.getUniswapV2SellQuotes(
[expectedMakerToken, expectedTakerToken],
expectedTakerFillAmounts,
),
);
expect(fillableAmounts.map(q => q.amount)).to.deep.eq(expectedMakerFillAmounts);
expect(fillableAmounts).to.deep.eq(expectedMakerFillAmounts);
});
it('getEth2DaiBuyQuotes()', async () => {
@ -354,13 +341,9 @@ describe('DexSampler tests', () => {
});
const dexOrderSampler = new DexOrderSampler(sampler);
const [fillableAmounts] = await dexOrderSampler.executeAsync(
DexOrderSampler.ops.getEth2DaiBuyQuotes(
expectedMakerToken,
expectedTakerToken,
expectedMakerFillAmounts,
),
dexOrderSampler.getEth2DaiBuyQuotes(expectedMakerToken, expectedTakerToken, expectedMakerFillAmounts),
);
expect(fillableAmounts.map(q => q.amount)).to.deep.eq(expectedTakerFillAmounts);
expect(fillableAmounts).to.deep.eq(expectedTakerFillAmounts);
});
it('getUniswapBuyQuotes()', async () => {
@ -378,13 +361,9 @@ describe('DexSampler tests', () => {
});
const dexOrderSampler = new DexOrderSampler(sampler);
const [fillableAmounts] = await dexOrderSampler.executeAsync(
DexOrderSampler.ops.getUniswapBuyQuotes(
expectedMakerToken,
expectedTakerToken,
expectedMakerFillAmounts,
),
dexOrderSampler.getUniswapBuyQuotes(expectedMakerToken, expectedTakerToken, expectedMakerFillAmounts),
);
expect(fillableAmounts.map(q => q.amount)).to.deep.eq(expectedTakerFillAmounts);
expect(fillableAmounts).to.deep.eq(expectedTakerFillAmounts);
});
interface RatesBySource {
@ -440,13 +419,12 @@ describe('DexSampler tests', () => {
});
const dexOrderSampler = new DexOrderSampler(sampler);
const [quotes] = await dexOrderSampler.executeAsync(
await DexOrderSampler.ops.getSellQuotesAsync(
dexOrderSampler.getSellQuotes(
sources,
expectedMakerToken,
expectedTakerToken,
expectedTakerFillAmounts,
wethAddress,
dexOrderSampler.balancerPoolsCache,
),
);
const expectedQuotes = sources.map(s =>
@ -457,7 +435,7 @@ describe('DexSampler tests', () => {
fillData:
s === ERC20BridgeSource.UniswapV2
? { tokenAddressPath: [expectedTakerToken, expectedMakerToken] }
: ((undefined as any) as FillData),
: {},
})),
);
const uniswapV2ETHQuotes = [
@ -488,19 +466,14 @@ describe('DexSampler tests', () => {
});
const dexOrderSampler = new DexOrderSampler(
new MockSamplerContract({}),
undefined, // sampler overrides
undefined, // bancor service
undefined,
undefined,
balancerPoolsCache,
);
const [quotes] = await dexOrderSampler.executeAsync(
await DexOrderSampler.ops.getSellQuotesAsync(
[ERC20BridgeSource.Balancer],
const quotes = await dexOrderSampler.getBalancerSellQuotesOffChainAsync(
expectedMakerToken,
expectedTakerToken,
expectedTakerFillAmounts,
wethAddress,
dexOrderSampler.balancerPoolsCache,
),
);
const expectedQuotes = pools.map(p =>
expectedTakerFillAmounts.map(a => ({
@ -535,28 +508,17 @@ describe('DexSampler tests', () => {
bancorService,
undefined, // balancer cache
);
const [quotes] = await dexOrderSampler.executeAsync(
await DexOrderSampler.ops.getSellQuotesAsync(
[ERC20BridgeSource.Bancor],
const quotes = await dexOrderSampler.getBancorSellQuotesOffChainAsync(
expectedMakerToken,
expectedTakerToken,
expectedTakerFillAmounts,
wethAddress,
undefined, // balancer pools cache
undefined, // liquidity provider registry address
undefined, // multibridge address
bancorService,
),
);
const expectedQuotes = [
expectedTakerFillAmounts.map(a => ({
const expectedQuotes = expectedTakerFillAmounts.map(a => ({
source: ERC20BridgeSource.Bancor,
input: a,
output: a.multipliedBy(rate),
fillData: { path: [expectedTakerToken, expectedMakerToken], networkAddress },
})),
];
expect(quotes).to.have.lengthOf(1); // one set per pool
}));
expect(quotes).to.deep.eq(expectedQuotes);
});
it('getBuyQuotes()', async () => {
@ -596,13 +558,12 @@ describe('DexSampler tests', () => {
});
const dexOrderSampler = new DexOrderSampler(sampler);
const [quotes] = await dexOrderSampler.executeAsync(
await DexOrderSampler.ops.getBuyQuotesAsync(
dexOrderSampler.getBuyQuotes(
sources,
expectedMakerToken,
expectedTakerToken,
expectedMakerFillAmounts,
wethAddress,
dexOrderSampler.balancerPoolsCache,
),
);
const expectedQuotes = sources.map(s =>
@ -613,7 +574,7 @@ describe('DexSampler tests', () => {
fillData:
s === ERC20BridgeSource.UniswapV2
? { tokenAddressPath: [expectedTakerToken, expectedMakerToken] }
: ((undefined as any) as FillData),
: {},
})),
);
const uniswapV2ETHQuotes = [
@ -644,19 +605,14 @@ describe('DexSampler tests', () => {
});
const dexOrderSampler = new DexOrderSampler(
new MockSamplerContract({}),
undefined, // sampler overrides
undefined, // bancor service
undefined,
undefined,
balancerPoolsCache,
);
const [quotes] = await dexOrderSampler.executeAsync(
await DexOrderSampler.ops.getBuyQuotesAsync(
[ERC20BridgeSource.Balancer],
const quotes = await dexOrderSampler.getBalancerBuyQuotesOffChainAsync(
expectedMakerToken,
expectedTakerToken,
expectedMakerFillAmounts,
wethAddress,
dexOrderSampler.balancerPoolsCache,
),
);
const expectedQuotes = pools.map(p =>
expectedMakerFillAmounts.map(a => ({
@ -689,8 +645,8 @@ describe('DexSampler tests', () => {
});
const dexOrderSampler = new DexOrderSampler(sampler);
const [fillableMakerAmounts, fillableTakerAmounts] = await dexOrderSampler.executeAsync(
DexOrderSampler.ops.getOrderFillableMakerAmounts(ORDERS, exchangeAddress),
DexOrderSampler.ops.getOrderFillableTakerAmounts(ORDERS, exchangeAddress),
dexOrderSampler.getOrderFillableMakerAmounts(ORDERS, exchangeAddress),
dexOrderSampler.getOrderFillableTakerAmounts(ORDERS, exchangeAddress),
);
expect(fillableMakerAmounts).to.deep.eq(expectedFillableMakerAmounts);
expect(fillableTakerAmounts).to.deep.eq(expectedFillableTakerAmounts);

View File

@ -37,6 +37,7 @@ describe('ExchangeProxySwapQuoteConsumer', () => {
const CHAIN_ID = 1;
const TAKER_TOKEN = randomAddress();
const MAKER_TOKEN = randomAddress();
const INTERMEDIATE_TOKEN = randomAddress();
const TRANSFORMER_DEPLOYER = randomAddress();
const contractAddresses = {
...getContractAddressesForChainOrThrow(CHAIN_ID),
@ -124,6 +125,18 @@ describe('ExchangeProxySwapQuoteConsumer', () => {
} as any;
}
function getRandomTwoHopQuote(side: MarketOperation): MarketBuySwapQuote | MarketSellSwapQuote {
const intermediateTokenAssetData = createAssetData(INTERMEDIATE_TOKEN);
return {
...getRandomQuote(side),
orders: [
{ ...getRandomOrder(), makerAssetData: intermediateTokenAssetData },
{ ...getRandomOrder(), takerAssetData: intermediateTokenAssetData },
],
isTwoHop: true,
} as any;
}
function getRandomSellQuote(): MarketSellSwapQuote {
return getRandomQuote(MarketOperation.Sell) as MarketSellSwapQuote;
}
@ -298,5 +311,52 @@ describe('ExchangeProxySwapQuoteConsumer', () => {
}),
).to.eventually.be.rejectedWith('Affiliate fees denominated in sell token are not yet supported');
});
it('Uses two `FillQuoteTransformer`s if given two-hop sell quote', async () => {
const quote = getRandomTwoHopQuote(MarketOperation.Sell) as MarketSellSwapQuote;
const callInfo = await consumer.getCalldataOrThrowAsync(quote, {
extensionContractOpts: { isTwoHop: true },
});
const callArgs = callDataEncoder.decode(callInfo.calldataHexString) as CallArgs;
expect(callArgs.inputToken).to.eq(TAKER_TOKEN);
expect(callArgs.outputToken).to.eq(MAKER_TOKEN);
expect(callArgs.inputTokenAmount).to.bignumber.eq(quote.worstCaseQuoteInfo.totalTakerAssetAmount);
expect(callArgs.minOutputTokenAmount).to.bignumber.eq(quote.worstCaseQuoteInfo.makerAssetAmount);
expect(callArgs.transformations).to.be.length(3);
expect(
callArgs.transformations[0].deploymentNonce.toNumber() ===
consumer.transformerNonces.fillQuoteTransformer,
);
expect(
callArgs.transformations[1].deploymentNonce.toNumber() ===
consumer.transformerNonces.fillQuoteTransformer,
);
expect(
callArgs.transformations[2].deploymentNonce.toNumber() ===
consumer.transformerNonces.payTakerTransformer,
);
const [firstHopOrder, secondHopOrder] = quote.orders;
const firstHopFillQuoteTransformerData = decodeFillQuoteTransformerData(callArgs.transformations[0].data);
expect(firstHopFillQuoteTransformerData.side).to.eq(FillQuoteTransformerSide.Sell);
expect(firstHopFillQuoteTransformerData.fillAmount).to.bignumber.eq(firstHopOrder.takerAssetAmount);
expect(firstHopFillQuoteTransformerData.orders).to.deep.eq(cleanOrders([firstHopOrder]));
expect(firstHopFillQuoteTransformerData.signatures).to.deep.eq([firstHopOrder.signature]);
expect(firstHopFillQuoteTransformerData.sellToken).to.eq(TAKER_TOKEN);
expect(firstHopFillQuoteTransformerData.buyToken).to.eq(INTERMEDIATE_TOKEN);
const secondHopFillQuoteTransformerData = decodeFillQuoteTransformerData(callArgs.transformations[1].data);
expect(secondHopFillQuoteTransformerData.side).to.eq(FillQuoteTransformerSide.Sell);
expect(secondHopFillQuoteTransformerData.fillAmount).to.bignumber.eq(contractConstants.MAX_UINT256);
expect(secondHopFillQuoteTransformerData.orders).to.deep.eq(cleanOrders([secondHopOrder]));
expect(secondHopFillQuoteTransformerData.signatures).to.deep.eq([secondHopOrder.signature]);
expect(secondHopFillQuoteTransformerData.sellToken).to.eq(INTERMEDIATE_TOKEN);
expect(secondHopFillQuoteTransformerData.buyToken).to.eq(MAKER_TOKEN);
const payTakerTransformerData = decodePayTakerTransformerData(callArgs.transformations[2].data);
expect(payTakerTransformerData.amounts).to.deep.eq([]);
expect(payTakerTransformerData.tokens).to.deep.eq([
TAKER_TOKEN,
MAKER_TOKEN,
ETH_TOKEN_ADDRESS,
INTERMEDIATE_TOKEN,
]);
});
});
});

View File

@ -17,6 +17,7 @@ import * as TypeMoq from 'typemoq';
import { MarketOperation, QuoteRequestor, RfqtRequestOpts, SignedOrderWithFillableAmounts } from '../src';
import { getRfqtIndicativeQuotesAsync, MarketOperationUtils } from '../src/utils/market_operation_utils/';
import { BalancerPoolsCache } from '../src/utils/market_operation_utils/balancer_utils';
import { BUY_SOURCES, POSITIVE_INF, SELL_SOURCES, ZERO_AMOUNT } from '../src/utils/market_operation_utils/constants';
import { createFillPaths } from '../src/utils/market_operation_utils/fills';
import { DexOrderSampler } from '../src/utils/market_operation_utils/sampler';
@ -31,15 +32,6 @@ const TAKER_ASSET_DATA = assetDataUtils.encodeERC20AssetData(TAKER_TOKEN);
describe('MarketOperationUtils tests', () => {
const CHAIN_ID = 1;
const contractAddresses = { ...getContractAddressesForChainOrThrow(CHAIN_ID), multiBridge: NULL_ADDRESS };
let originalSamplerOperations: any;
before(() => {
originalSamplerOperations = DexOrderSampler.ops;
});
after(() => {
DexOrderSampler.ops = originalSamplerOperations;
});
function createOrder(overrides?: Partial<SignedOrder>): SignedOrder {
return {
@ -153,7 +145,7 @@ describe('MarketOperationUtils tests', () => {
fillAmounts: BigNumber[],
wethAddress: string,
liquidityProviderAddress?: string,
) => Promise<DexSample[][]>;
) => DexSample[][];
function createGetMultipleSellQuotesOperationFromRates(rates: RatesBySource): GetMultipleQuotesOperation {
return (
@ -163,7 +155,7 @@ describe('MarketOperationUtils tests', () => {
fillAmounts: BigNumber[],
_wethAddress: string,
) => {
return Promise.resolve(sources.map(s => createSamplesFromRates(s, fillAmounts, rates[s])));
return sources.map(s => createSamplesFromRates(s, fillAmounts, rates[s]));
};
}
@ -181,7 +173,6 @@ describe('MarketOperationUtils tests', () => {
takerToken: string,
fillAmounts: BigNumber[],
wethAddress: string,
_balancerPoolsCache?: any,
liquidityProviderAddress?: string,
) => {
liquidityPoolParams.liquidityProviderAddress = liquidityProviderAddress;
@ -206,9 +197,7 @@ describe('MarketOperationUtils tests', () => {
fillAmounts: BigNumber[],
_wethAddress: string,
) => {
return Promise.resolve(
sources.map(s => createSamplesFromRates(s, fillAmounts, rates[s].map(r => new BigNumber(1).div(r)))),
);
return sources.map(s => createSamplesFromRates(s, fillAmounts, rates[s].map(r => new BigNumber(1).div(r))));
};
}
@ -221,12 +210,6 @@ describe('MarketOperationUtils tests', () => {
liquidityProviderAddress?: string,
) => BigNumber;
type GetLiquidityProviderFromRegistryOperation = (
registryAddress: string,
takerToken: string,
makerToken: string,
) => string;
function createGetMedianSellRate(rate: Numberish): GetMedianRateOperation {
return (
_sources: ERC20BridgeSource[],
@ -239,34 +222,6 @@ describe('MarketOperationUtils tests', () => {
};
}
function getLiquidityProviderFromRegistry(): GetLiquidityProviderFromRegistryOperation {
return (_registryAddress: string, _takerToken: string, _makerToken: string): string => {
return NULL_ADDRESS;
};
}
function getLiquidityProviderFromRegistryAndReturnCallParameters(
liquidityProviderAddress: string = NULL_ADDRESS,
): [
{ registryAddress?: string; takerToken?: string; makerToken?: string },
GetLiquidityProviderFromRegistryOperation
] {
const callArgs: { registryAddress?: string; takerToken?: string; makerToken?: string } = {
registryAddress: undefined,
takerToken: undefined,
makerToken: undefined,
};
const fn = (registryAddress: string, takerToken: string, makerToken: string): string => {
callArgs.makerToken = makerToken;
callArgs.takerToken = takerToken;
if (registryAddress !== constants.NULL_ADDRESS) {
callArgs.registryAddress = registryAddress;
}
return liquidityProviderAddress;
};
return [callArgs, fn];
}
function createDecreasingRates(count: number): BigNumber[] {
const rates: BigNumber[] = [];
const initialRate = getRandomFloat(1e-3, 1e2);
@ -317,6 +272,7 @@ describe('MarketOperationUtils tests', () => {
fromTokenIdx: 0,
toTokenIdx: 1,
},
[ERC20BridgeSource.LiquidityProvider]: { poolAddress: randomAddress() },
};
const DEFAULT_OPS = {
@ -326,19 +282,44 @@ describe('MarketOperationUtils tests', () => {
getOrderFillableMakerAmounts(orders: SignedOrder[]): BigNumber[] {
return orders.map(o => o.makerAssetAmount);
},
getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(DEFAULT_RATES),
getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(DEFAULT_RATES),
getMedianSellRateAsync: createGetMedianSellRate(1),
getLiquidityProviderFromRegistry: getLiquidityProviderFromRegistry(),
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(DEFAULT_RATES),
getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(DEFAULT_RATES),
getMedianSellRate: createGetMedianSellRate(1),
getBalancerSellQuotesOffChainAsync: (
_makerToken: string,
_takerToken: string,
takerFillAmounts: BigNumber[],
) => [
createSamplesFromRates(
ERC20BridgeSource.Balancer,
takerFillAmounts,
createDecreasingRates(takerFillAmounts.length),
DEFAULT_FILL_DATA[ERC20BridgeSource.Balancer],
),
],
getBalancerBuyQuotesOffChainAsync: (
_makerToken: string,
_takerToken: string,
makerFillAmounts: BigNumber[],
) => [
createSamplesFromRates(
ERC20BridgeSource.Balancer,
makerFillAmounts,
createDecreasingRates(makerFillAmounts.length).map(r => new BigNumber(1).div(r)),
DEFAULT_FILL_DATA[ERC20BridgeSource.Balancer],
),
],
getBancorSellQuotesOffChainAsync: (_makerToken: string, _takerToken: string, takerFillAmounts: BigNumber[]) =>
createSamplesFromRates(
ERC20BridgeSource.Bancor,
takerFillAmounts,
createDecreasingRates(takerFillAmounts.length),
DEFAULT_FILL_DATA[ERC20BridgeSource.Bancor],
),
getTwoHopSellQuotes: (..._params: any[]) => [],
getTwoHopBuyQuotes: (..._params: any[]) => [],
};
function replaceSamplerOps(ops: Partial<typeof DEFAULT_OPS> = {}): void {
DexOrderSampler.ops = {
...DEFAULT_OPS,
...ops,
} as any;
}
const MOCK_SAMPLER = ({
async executeAsync(...ops: any[]): Promise<any[]> {
return ops;
@ -346,8 +327,14 @@ describe('MarketOperationUtils tests', () => {
async executeBatchAsync(ops: any[]): Promise<any[]> {
return ops;
},
balancerPoolsCache: new BalancerPoolsCache(),
} as any) as DexOrderSampler;
function replaceSamplerOps(ops: Partial<typeof DEFAULT_OPS> = {}): void {
Object.assign(MOCK_SAMPLER, DEFAULT_OPS);
Object.assign(MOCK_SAMPLER, ops);
}
describe('getRfqtIndicativeQuotesAsync', () => {
const partialRfqt: RfqtRequestOpts = {
apiKey: 'foo',
@ -434,6 +421,7 @@ describe('MarketOperationUtils tests', () => {
ERC20BridgeSource.Balancer,
ERC20BridgeSource.MStable,
ERC20BridgeSource.Mooniswap,
ERC20BridgeSource.Bancor,
],
allowFallback: false,
shouldBatchBridgeOrders: false,
@ -447,9 +435,9 @@ describe('MarketOperationUtils tests', () => {
const numSamples = _.random(1, NUM_SAMPLES);
let actualNumSamples = 0;
replaceSamplerOps({
getSellQuotesAsync: (sources, makerToken, takerToken, amounts, wethAddress) => {
getSellQuotes: (sources, makerToken, takerToken, amounts, wethAddress) => {
actualNumSamples = amounts.length;
return DEFAULT_OPS.getSellQuotesAsync(sources, makerToken, takerToken, amounts, wethAddress);
return DEFAULT_OPS.getSellQuotes(sources, makerToken, takerToken, amounts, wethAddress);
},
});
await marketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, {
@ -462,9 +450,17 @@ describe('MarketOperationUtils tests', () => {
it('polls all DEXes if `excludedSources` is empty', async () => {
let sourcesPolled: ERC20BridgeSource[] = [];
replaceSamplerOps({
getSellQuotesAsync: (sources, makerToken, takerToken, amounts, wethAddress) => {
getSellQuotes: (sources, makerToken, takerToken, amounts, wethAddress) => {
sourcesPolled = sourcesPolled.concat(sources.slice());
return DEFAULT_OPS.getSellQuotesAsync(sources, makerToken, takerToken, amounts, wethAddress);
return DEFAULT_OPS.getSellQuotes(sources, makerToken, takerToken, amounts, wethAddress);
},
getBalancerSellQuotesOffChainAsync: (
makerToken: string,
takerToken: string,
takerFillAmounts: BigNumber[],
) => {
sourcesPolled = sourcesPolled.concat(ERC20BridgeSource.Balancer);
return DEFAULT_OPS.getBalancerSellQuotesOffChainAsync(makerToken, takerToken, takerFillAmounts);
},
});
await marketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, {
@ -480,7 +476,15 @@ describe('MarketOperationUtils tests', () => {
DEFAULT_RATES,
);
replaceSamplerOps({
getSellQuotesAsync: fn,
getSellQuotes: fn,
getBalancerSellQuotesOffChainAsync: (
makerToken: string,
takerToken: string,
takerFillAmounts: BigNumber[],
) => {
args.sources = args.sources.concat(ERC20BridgeSource.Balancer);
return DEFAULT_OPS.getBalancerSellQuotesOffChainAsync(makerToken, takerToken, takerFillAmounts);
},
});
const registryAddress = randomAddress();
const newMarketOperationUtils = new MarketOperationUtils(
@ -503,9 +507,17 @@ describe('MarketOperationUtils tests', () => {
const excludedSources = _.sampleSize(SELL_SOURCES, _.random(1, SELL_SOURCES.length));
let sourcesPolled: ERC20BridgeSource[] = [];
replaceSamplerOps({
getSellQuotesAsync: (sources, makerToken, takerToken, amounts, wethAddress) => {
getSellQuotes: (sources, makerToken, takerToken, amounts, wethAddress) => {
sourcesPolled = sourcesPolled.concat(sources.slice());
return DEFAULT_OPS.getSellQuotesAsync(sources, makerToken, takerToken, amounts, wethAddress);
return DEFAULT_OPS.getSellQuotes(sources, makerToken, takerToken, amounts, wethAddress);
},
getBalancerSellQuotesOffChainAsync: (
makerToken: string,
takerToken: string,
takerFillAmounts: BigNumber[],
) => {
sourcesPolled = sourcesPolled.concat(ERC20BridgeSource.Balancer);
return DEFAULT_OPS.getBalancerSellQuotesOffChainAsync(makerToken, takerToken, takerFillAmounts);
},
});
await marketOperationUtils.getMarketSellOrdersAsync(ORDERS, FILL_AMOUNT, {
@ -576,7 +588,7 @@ describe('MarketOperationUtils tests', () => {
rates[ERC20BridgeSource.Eth2Dai] = [0.6, 0.05, 0.05, 0.05];
rates[ERC20BridgeSource.Kyber] = [0, 0, 0, 0]; // unused
replaceSamplerOps({
getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates),
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates),
});
const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync(
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
@ -614,8 +626,8 @@ describe('MarketOperationUtils tests', () => {
),
};
replaceSamplerOps({
getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates),
getMedianSellRateAsync: createGetMedianSellRate(ETH_TO_MAKER_RATE),
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates),
getMedianSellRate: createGetMedianSellRate(ETH_TO_MAKER_RATE),
});
const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync(
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
@ -652,8 +664,8 @@ describe('MarketOperationUtils tests', () => {
),
};
replaceSamplerOps({
getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates),
getMedianSellRateAsync: createGetMedianSellRate(ETH_TO_MAKER_RATE),
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates),
getMedianSellRate: createGetMedianSellRate(ETH_TO_MAKER_RATE),
});
const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync(
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
@ -678,8 +690,8 @@ describe('MarketOperationUtils tests', () => {
[ERC20BridgeSource.Native]: [0.95, 0.2, 0.2, 0.1],
};
replaceSamplerOps({
getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates),
getMedianSellRateAsync: createGetMedianSellRate(ETH_TO_MAKER_RATE),
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates),
getMedianSellRate: createGetMedianSellRate(ETH_TO_MAKER_RATE),
});
const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync(
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
@ -703,7 +715,7 @@ describe('MarketOperationUtils tests', () => {
rates[ERC20BridgeSource.Eth2Dai] = [0.4, 0.3, 0.01, 0.01];
rates[ERC20BridgeSource.Kyber] = [0.35, 0.2, 0.01, 0.01];
replaceSamplerOps({
getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates),
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates),
});
const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync(
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
@ -724,7 +736,7 @@ describe('MarketOperationUtils tests', () => {
rates[ERC20BridgeSource.Eth2Dai] = [0.49, 0.49, 0.49, 0.49];
rates[ERC20BridgeSource.Kyber] = [0.35, 0.2, 0.01, 0.01];
replaceSamplerOps({
getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates),
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates),
});
const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync(
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
@ -741,7 +753,8 @@ describe('MarketOperationUtils tests', () => {
it('is able to create a order from LiquidityProvider', async () => {
const registryAddress = randomAddress();
const liquidityProviderAddress = randomAddress();
const liquidityProviderAddress = (DEFAULT_FILL_DATA[ERC20BridgeSource.LiquidityProvider] as any)
.poolAddress;
const xAsset = randomAddress();
const yAsset = randomAddress();
const toSell = fromTokenUnitAmount(10);
@ -752,14 +765,10 @@ describe('MarketOperationUtils tests', () => {
[ERC20BridgeSource.LiquidityProvider]: createDecreasingRates(5),
},
);
const [
getLiquidityProviderParams,
getLiquidityProviderFn,
] = getLiquidityProviderFromRegistryAndReturnCallParameters(liquidityProviderAddress);
replaceSamplerOps({
getOrderFillableTakerAmounts: () => [constants.ZERO_AMOUNT],
getSellQuotesAsync: getSellQuotesFn,
getLiquidityProviderFromRegistry: getLiquidityProviderFn,
getSellQuotes: getSellQuotesFn,
});
const sampler = new MarketOperationUtils(
@ -776,7 +785,12 @@ describe('MarketOperationUtils tests', () => {
}),
],
Web3Wrapper.toBaseUnitAmount(10, 18),
{ excludedSources: SELL_SOURCES, numSamples: 4, bridgeSlippage: 0, shouldBatchBridgeOrders: false },
{
excludedSources: SELL_SOURCES.concat(ERC20BridgeSource.Bancor),
numSamples: 4,
bridgeSlippage: 0,
shouldBatchBridgeOrders: false,
},
);
const result = ordersAndReport.optimizedOrders;
expect(result.length).to.eql(1);
@ -791,9 +805,6 @@ describe('MarketOperationUtils tests', () => {
expect(result[0].takerAssetAmount).to.bignumber.eql(toSell);
expect(getSellQuotesParams.sources).contains(ERC20BridgeSource.LiquidityProvider);
expect(getSellQuotesParams.liquidityProviderAddress).is.eql(registryAddress);
expect(getLiquidityProviderParams.registryAddress).is.eql(registryAddress);
expect(getLiquidityProviderParams.makerToken).is.eql(yAsset);
expect(getLiquidityProviderParams.takerToken).is.eql(xAsset);
});
it('batches contiguous bridge sources', async () => {
@ -803,7 +814,7 @@ describe('MarketOperationUtils tests', () => {
rates[ERC20BridgeSource.Eth2Dai] = [0.49, 0.01, 0.01, 0.01];
rates[ERC20BridgeSource.Curve] = [0.48, 0.01, 0.01, 0.01];
replaceSamplerOps({
getSellQuotesAsync: createGetMultipleSellQuotesOperationFromRates(rates),
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates),
});
const improvedOrdersResponse = await marketOperationUtils.getMarketSellOrdersAsync(
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
@ -860,9 +871,9 @@ describe('MarketOperationUtils tests', () => {
const numSamples = _.random(1, 16);
let actualNumSamples = 0;
replaceSamplerOps({
getBuyQuotesAsync: (sources, makerToken, takerToken, amounts, wethAddress) => {
getBuyQuotes: (sources, makerToken, takerToken, amounts, wethAddress) => {
actualNumSamples = amounts.length;
return DEFAULT_OPS.getBuyQuotesAsync(sources, makerToken, takerToken, amounts, wethAddress);
return DEFAULT_OPS.getBuyQuotes(sources, makerToken, takerToken, amounts, wethAddress);
},
});
await marketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, {
@ -875,9 +886,17 @@ describe('MarketOperationUtils tests', () => {
it('polls all DEXes if `excludedSources` is empty', async () => {
let sourcesPolled: ERC20BridgeSource[] = [];
replaceSamplerOps({
getBuyQuotesAsync: (sources, makerToken, takerToken, amounts, wethAddress) => {
getBuyQuotes: (sources, makerToken, takerToken, amounts, wethAddress) => {
sourcesPolled = sourcesPolled.concat(sources.slice());
return DEFAULT_OPS.getBuyQuotesAsync(sources, makerToken, takerToken, amounts, wethAddress);
return DEFAULT_OPS.getBuyQuotes(sources, makerToken, takerToken, amounts, wethAddress);
},
getBalancerBuyQuotesOffChainAsync: (
makerToken: string,
takerToken: string,
makerFillAmounts: BigNumber[],
) => {
sourcesPolled = sourcesPolled.concat(ERC20BridgeSource.Balancer);
return DEFAULT_OPS.getBalancerBuyQuotesOffChainAsync(makerToken, takerToken, makerFillAmounts);
},
});
await marketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, {
@ -893,7 +912,15 @@ describe('MarketOperationUtils tests', () => {
DEFAULT_RATES,
);
replaceSamplerOps({
getBuyQuotesAsync: fn,
getBuyQuotes: fn,
getBalancerBuyQuotesOffChainAsync: (
makerToken: string,
takerToken: string,
makerFillAmounts: BigNumber[],
) => {
args.sources = args.sources.concat(ERC20BridgeSource.Balancer);
return DEFAULT_OPS.getBalancerBuyQuotesOffChainAsync(makerToken, takerToken, makerFillAmounts);
},
});
const registryAddress = randomAddress();
const newMarketOperationUtils = new MarketOperationUtils(
@ -916,9 +943,17 @@ describe('MarketOperationUtils tests', () => {
const excludedSources = _.sampleSize(SELL_SOURCES, _.random(1, SELL_SOURCES.length));
let sourcesPolled: ERC20BridgeSource[] = [];
replaceSamplerOps({
getBuyQuotesAsync: (sources, makerToken, takerToken, amounts, wethAddress) => {
getBuyQuotes: (sources, makerToken, takerToken, amounts, wethAddress) => {
sourcesPolled = sourcesPolled.concat(sources.slice());
return DEFAULT_OPS.getBuyQuotesAsync(sources, makerToken, takerToken, amounts, wethAddress);
return DEFAULT_OPS.getBuyQuotes(sources, makerToken, takerToken, amounts, wethAddress);
},
getBalancerBuyQuotesOffChainAsync: (
makerToken: string,
takerToken: string,
makerFillAmounts: BigNumber[],
) => {
sourcesPolled = sourcesPolled.concat(ERC20BridgeSource.Balancer);
return DEFAULT_OPS.getBalancerBuyQuotesOffChainAsync(makerToken, takerToken, makerFillAmounts);
},
});
await marketOperationUtils.getMarketBuyOrdersAsync(ORDERS, FILL_AMOUNT, {
@ -988,7 +1023,7 @@ describe('MarketOperationUtils tests', () => {
rates[ERC20BridgeSource.Uniswap] = [0.5, 0.05, 0.05, 0.05];
rates[ERC20BridgeSource.Eth2Dai] = [0.6, 0.05, 0.05, 0.05];
replaceSamplerOps({
getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(rates),
getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates),
});
const improvedOrdersResponse = await marketOperationUtils.getMarketBuyOrdersAsync(
createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
@ -1026,8 +1061,8 @@ describe('MarketOperationUtils tests', () => {
),
};
replaceSamplerOps({
getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(rates),
getMedianSellRateAsync: createGetMedianSellRate(ETH_TO_TAKER_RATE),
getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates),
getMedianSellRate: createGetMedianSellRate(ETH_TO_TAKER_RATE),
});
const improvedOrdersResponse = await marketOperationUtils.getMarketBuyOrdersAsync(
createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
@ -1063,8 +1098,8 @@ describe('MarketOperationUtils tests', () => {
),
};
replaceSamplerOps({
getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(rates),
getMedianSellRateAsync: createGetMedianSellRate(ETH_TO_TAKER_RATE),
getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates),
getMedianSellRate: createGetMedianSellRate(ETH_TO_TAKER_RATE),
});
const improvedOrdersResponse = await marketOperationUtils.getMarketBuyOrdersAsync(
createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
@ -1087,7 +1122,7 @@ describe('MarketOperationUtils tests', () => {
rates[ERC20BridgeSource.Uniswap] = [0.6, 0.05, 0.01, 0.01];
rates[ERC20BridgeSource.Eth2Dai] = [0.4, 0.3, 0.01, 0.01];
replaceSamplerOps({
getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(rates),
getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates),
});
const improvedOrdersResponse = await marketOperationUtils.getMarketBuyOrdersAsync(
createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
@ -1107,7 +1142,7 @@ describe('MarketOperationUtils tests', () => {
rates[ERC20BridgeSource.Uniswap] = [1, 1, 0.01, 0.01];
rates[ERC20BridgeSource.Eth2Dai] = [0.49, 0.49, 0.49, 0.49];
replaceSamplerOps({
getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(rates),
getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates),
});
const improvedOrdersResponse = await marketOperationUtils.getMarketBuyOrdersAsync(
createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
@ -1128,7 +1163,7 @@ describe('MarketOperationUtils tests', () => {
rates[ERC20BridgeSource.Eth2Dai] = [0.49, 0.02, 0.01, 0.01];
rates[ERC20BridgeSource.Uniswap] = [0.48, 0.01, 0.01, 0.01];
replaceSamplerOps({
getBuyQuotesAsync: createGetMultipleBuyQuotesOperationFromRates(rates),
getBuyQuotes: createGetMultipleBuyQuotesOperationFromRates(rates),
});
const improvedOrdersResponse = await marketOperationUtils.getMarketBuyOrdersAsync(
createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),

View File

@ -12,15 +12,17 @@ import {
CollapsedFill,
DexSample,
ERC20BridgeSource,
MultiHopFillData,
NativeCollapsedFill,
} from '../src/utils/market_operation_utils/types';
import { QuoteRequestor } from '../src/utils/quote_requestor';
import {
BridgeReportSource,
generateQuoteReport,
MultiHopReportSource,
NativeOrderbookReportSource,
NativeRFQTReportSource,
QuoteReportGenerator,
QuoteReportSource,
} from './../src/utils/quote_report_generator';
import { chaiSetup } from './utils/chai_setup';
@ -47,8 +49,7 @@ const collapsedFillFromNativeOrder = (order: SignedOrder): NativeCollapsedFill =
};
};
describe('QuoteReportGenerator', async () => {
describe('generateReport', async () => {
describe('generateQuoteReport', async () => {
it('should generate report properly for sell', () => {
const marketOperation: MarketOperation = MarketOperation.Sell;
@ -130,14 +131,15 @@ describe('QuoteReportGenerator', async () => {
})
.verifiable(TypeMoq.Times.atLeastOnce());
const orderReport = new QuoteReportGenerator(
const orderReport = generateQuoteReport(
marketOperation,
dexQuotes,
[],
nativeOrders,
orderFillableAmounts,
pathGenerated,
quoteRequestor.object,
).generateReport();
);
const rfqtOrder1Source: NativeRFQTReportSource = {
liquiditySource: ERC20BridgeSource.Native,
@ -238,10 +240,7 @@ describe('QuoteReportGenerator', async () => {
]) as SignedOrder;
}
expect(actualSourceDelivered).to.eql(
expectedSourceDelivered,
`sourceDelivered incorrect at index ${idx}`,
);
expect(actualSourceDelivered).to.eql(expectedSourceDelivered, `sourceDelivered incorrect at index ${idx}`);
});
quoteRequestor.verifyAll();
@ -279,13 +278,14 @@ describe('QuoteReportGenerator', async () => {
const kyber1Fill: CollapsedFill = { ...kyberSample1, subFills: [], sourcePathId: hexUtils.random() };
const pathGenerated: CollapsedFill[] = [orderbookOrder1Fill, uniswap1Fill, kyber1Fill];
const orderReport = new QuoteReportGenerator(
const orderReport = generateQuoteReport(
marketOperation,
dexQuotes,
[],
nativeOrders,
orderFillableAmounts,
pathGenerated,
).generateReport();
);
const orderbookOrder1Source: NativeOrderbookReportSource = {
liquiditySource: ERC20BridgeSource.Native,
@ -345,11 +345,82 @@ describe('QuoteReportGenerator', async () => {
]) as SignedOrder;
}
expect(actualSourceDelivered).to.eql(
expectedSourceDelivered,
`sourceDelivered incorrect at index ${idx}`,
expect(actualSourceDelivered).to.eql(expectedSourceDelivered, `sourceDelivered incorrect at index ${idx}`);
});
});
it('should correctly generate report for a two-hop quote', () => {
const marketOperation: MarketOperation = MarketOperation.Sell;
const kyberSample1: DexSample = {
source: ERC20BridgeSource.Kyber,
input: new BigNumber(10000),
output: new BigNumber(10001),
};
const orderbookOrder1FillableAmount = new BigNumber(1000);
const orderbookOrder1 = testOrderFactory.generateTestSignedOrder({
signature: 'orderbookOrder1',
takerAssetAmount: orderbookOrder1FillableAmount.plus(101),
});
const twoHopSample: DexSample<MultiHopFillData> = {
source: ERC20BridgeSource.MultiHop,
input: new BigNumber(3005),
output: new BigNumber(3006),
fillData: {
intermediateToken: hexUtils.random(20),
firstHopSource: {
source: ERC20BridgeSource.Balancer,
encodeCall: () => '',
handleCallResults: _callResults => [new BigNumber(1337)],
},
secondHopSource: {
source: ERC20BridgeSource.Curve,
encodeCall: () => '',
handleCallResults: _callResults => [new BigNumber(1337)],
},
},
};
const orderReport = generateQuoteReport(
marketOperation,
[kyberSample1],
[twoHopSample],
[orderbookOrder1],
[orderbookOrder1FillableAmount],
twoHopSample,
);
const orderbookOrder1Source: NativeOrderbookReportSource = {
liquiditySource: ERC20BridgeSource.Native,
makerAmount: orderbookOrder1.makerAssetAmount,
takerAmount: orderbookOrder1.takerAssetAmount,
orderHash: orderHashUtils.getOrderHash(orderbookOrder1),
nativeOrder: orderbookOrder1,
fillableTakerAmount: orderbookOrder1FillableAmount,
isRfqt: false,
};
const kyber1Source: BridgeReportSource = {
liquiditySource: ERC20BridgeSource.Kyber,
makerAmount: kyberSample1.output,
takerAmount: kyberSample1.input,
};
const twoHopSource: MultiHopReportSource = {
liquiditySource: ERC20BridgeSource.MultiHop,
makerAmount: twoHopSample.output,
takerAmount: twoHopSample.input,
hopSources: [ERC20BridgeSource.Balancer, ERC20BridgeSource.Curve],
};
const expectedSourcesConsidered: QuoteReportSource[] = [kyber1Source, orderbookOrder1Source, twoHopSource];
expect(orderReport.sourcesConsidered.length).to.eql(expectedSourcesConsidered.length);
orderReport.sourcesConsidered.forEach((actualSourcesConsidered, idx) => {
const expectedSourceConsidered = expectedSourcesConsidered[idx];
expect(actualSourcesConsidered).to.eql(
expectedSourceConsidered,
`sourceConsidered incorrect at index ${idx}`,
);
});
});
expect(orderReport.sourcesDelivered.length).to.eql(1);
expect(orderReport.sourcesDelivered[0]).to.deep.equal(twoHopSource);
});
});

View File

@ -29,7 +29,7 @@ export type SampleSellsLPHandler = (
takerToken: string,
makerToken: string,
takerTokenAmounts: BigNumber[],
) => SampleResults;
) => [SampleResults, string];
export type SampleSellsMultihopHandler = (path: string[], takerTokenAmounts: BigNumber[]) => SampleResults;
export type SampleSellsMBHandler = (
multiBridgeAddress: string,
@ -40,7 +40,7 @@ export type SampleSellsMBHandler = (
) => SampleResults;
const DUMMY_PROVIDER = {
sendAsync: (...args: any[]): any => {
sendAsync: (..._args: any[]): any => {
/* no-op */
},
};
@ -73,7 +73,7 @@ export class MockSamplerContract extends ERC20BridgeSamplerContract {
public batchCall(callDatas: string[]): ContractFunctionObj<string[]> {
return {
...super.batchCall(callDatas),
callAsync: async (...callArgs: any[]) => callDatas.map(callData => this._callEncodedFunction(callData)),
callAsync: async (..._callArgs: any[]) => callDatas.map(callData => this._callEncodedFunction(callData)),
};
}
@ -107,7 +107,7 @@ export class MockSamplerContract extends ERC20BridgeSamplerContract {
takerToken: string,
makerToken: string,
takerAssetAmounts: BigNumber[],
): ContractFunctionObj<GetOrderFillableAssetAmountResult> {
): ContractFunctionObj<BigNumber[]> {
return this._wrapCall(
super.sampleSellsFromKyberNetwork,
this._handlers.sampleSellsFromKyberNetwork,
@ -121,7 +121,7 @@ export class MockSamplerContract extends ERC20BridgeSamplerContract {
takerToken: string,
makerToken: string,
takerAssetAmounts: BigNumber[],
): ContractFunctionObj<GetOrderFillableAssetAmountResult> {
): ContractFunctionObj<BigNumber[]> {
return this._wrapCall(
super.sampleSellsFromEth2Dai,
this._handlers.sampleSellsFromEth2Dai,
@ -135,7 +135,7 @@ export class MockSamplerContract extends ERC20BridgeSamplerContract {
takerToken: string,
makerToken: string,
takerAssetAmounts: BigNumber[],
): ContractFunctionObj<GetOrderFillableAssetAmountResult> {
): ContractFunctionObj<BigNumber[]> {
return this._wrapCall(
super.sampleSellsFromUniswap,
this._handlers.sampleSellsFromUniswap,
@ -145,10 +145,7 @@ export class MockSamplerContract extends ERC20BridgeSamplerContract {
);
}
public sampleSellsFromUniswapV2(
path: string[],
takerAssetAmounts: BigNumber[],
): ContractFunctionObj<GetOrderFillableAssetAmountResult> {
public sampleSellsFromUniswapV2(path: string[], takerAssetAmounts: BigNumber[]): ContractFunctionObj<BigNumber[]> {
return this._wrapCall(
super.sampleSellsFromUniswapV2,
this._handlers.sampleSellsFromUniswapV2,
@ -162,7 +159,7 @@ export class MockSamplerContract extends ERC20BridgeSamplerContract {
takerToken: string,
makerToken: string,
takerAssetAmounts: BigNumber[],
): ContractFunctionObj<GetOrderFillableAssetAmountResult> {
): ContractFunctionObj<[BigNumber[], string]> {
return this._wrapCall(
super.sampleSellsFromLiquidityProviderRegistry,
this._handlers.sampleSellsFromLiquidityProviderRegistry,
@ -179,7 +176,7 @@ export class MockSamplerContract extends ERC20BridgeSamplerContract {
intermediateToken: string,
makerToken: string,
takerAssetAmounts: BigNumber[],
): ContractFunctionObj<GetOrderFillableAssetAmountResult> {
): ContractFunctionObj<BigNumber[]> {
return this._wrapCall(
super.sampleSellsFromMultiBridge,
this._handlers.sampleSellsFromMultiBridge,
@ -195,7 +192,7 @@ export class MockSamplerContract extends ERC20BridgeSamplerContract {
takerToken: string,
makerToken: string,
makerAssetAmounts: BigNumber[],
): ContractFunctionObj<GetOrderFillableAssetAmountResult> {
): ContractFunctionObj<BigNumber[]> {
return this._wrapCall(
super.sampleBuysFromEth2Dai,
this._handlers.sampleBuysFromEth2Dai,
@ -209,7 +206,7 @@ export class MockSamplerContract extends ERC20BridgeSamplerContract {
takerToken: string,
makerToken: string,
makerAssetAmounts: BigNumber[],
): ContractFunctionObj<GetOrderFillableAssetAmountResult> {
): ContractFunctionObj<BigNumber[]> {
return this._wrapCall(
super.sampleBuysFromUniswap,
this._handlers.sampleBuysFromUniswap,
@ -219,10 +216,7 @@ export class MockSamplerContract extends ERC20BridgeSamplerContract {
);
}
public sampleBuysFromUniswapV2(
path: string[],
makerAssetAmounts: BigNumber[],
): ContractFunctionObj<GetOrderFillableAssetAmountResult> {
public sampleBuysFromUniswapV2(path: string[], makerAssetAmounts: BigNumber[]): ContractFunctionObj<BigNumber[]> {
return this._wrapCall(
super.sampleBuysFromUniswapV2,
this._handlers.sampleBuysFromUniswapV2,
@ -241,7 +235,12 @@ export class MockSamplerContract extends ERC20BridgeSamplerContract {
if (handler && this.getSelector(name) === selector) {
const args = this.getABIDecodedTransactionData<any>(name, callData);
const result = (handler as any)(...args);
return this._lookupAbiEncoder(this.getFunctionSignature(name)).encodeReturnValues([result]);
const encoder = this._lookupAbiEncoder(this.getFunctionSignature(name));
if (encoder.getReturnValueDataItem().components!.length === 1) {
return encoder.encodeReturnValues([result]);
} else {
return encoder.encodeReturnValues(result);
}
}
}
if (selector === this.getSelector('batchCall')) {
@ -260,7 +259,7 @@ export class MockSamplerContract extends ERC20BridgeSamplerContract {
): ContractFunctionObj<TResult> {
return {
...superFn.call(this, ...args),
callAsync: async (...callArgs: any[]): Promise<TResult> => {
callAsync: async (..._callArgs: any[]): Promise<TResult> => {
if (!handler) {
throw new Error(`${superFn.name} handler undefined`);
}

View File

@ -39,6 +39,7 @@ export async function getFullyFillableSwapQuoteWithNoFeesAsync(
bestCaseQuoteInfo: quoteInfo,
worstCaseQuoteInfo: quoteInfo,
sourceBreakdown: breakdown,
isTwoHop: false,
};
if (operation === MarketOperation.Buy) {

View File

@ -4,11 +4,13 @@
* -----------------------------------------------------------------------------
*/
export * from '../test/generated-wrappers/approximate_buys';
export * from '../test/generated-wrappers/balancer_sampler';
export * from '../test/generated-wrappers/curve_sampler';
export * from '../test/generated-wrappers/dummy_liquidity_provider';
export * from '../test/generated-wrappers/dummy_liquidity_provider_registry';
export * from '../test/generated-wrappers/erc20_bridge_sampler';
export * from '../test/generated-wrappers/eth2_dai_sampler';
export * from '../test/generated-wrappers/i_balancer';
export * from '../test/generated-wrappers/i_curve';
export * from '../test/generated-wrappers/i_eth2_dai';
export * from '../test/generated-wrappers/i_kyber_hint_handler';
@ -31,5 +33,6 @@ export * from '../test/generated-wrappers/native_order_sampler';
export * from '../test/generated-wrappers/sampler_utils';
export * from '../test/generated-wrappers/test_erc20_bridge_sampler';
export * from '../test/generated-wrappers/test_native_order_sampler';
export * from '../test/generated-wrappers/two_hop_sampler';
export * from '../test/generated-wrappers/uniswap_sampler';
export * from '../test/generated-wrappers/uniswap_v2_sampler';

View File

@ -9,11 +9,13 @@
"generated-artifacts/ILiquidityProvider.json",
"generated-artifacts/ILiquidityProviderRegistry.json",
"test/generated-artifacts/ApproximateBuys.json",
"test/generated-artifacts/BalancerSampler.json",
"test/generated-artifacts/CurveSampler.json",
"test/generated-artifacts/DummyLiquidityProvider.json",
"test/generated-artifacts/DummyLiquidityProviderRegistry.json",
"test/generated-artifacts/ERC20BridgeSampler.json",
"test/generated-artifacts/Eth2DaiSampler.json",
"test/generated-artifacts/IBalancer.json",
"test/generated-artifacts/ICurve.json",
"test/generated-artifacts/IEth2Dai.json",
"test/generated-artifacts/IKyberHintHandler.json",
@ -36,6 +38,7 @@
"test/generated-artifacts/SamplerUtils.json",
"test/generated-artifacts/TestERC20BridgeSampler.json",
"test/generated-artifacts/TestNativeOrderSampler.json",
"test/generated-artifacts/TwoHopSampler.json",
"test/generated-artifacts/UniswapSampler.json",
"test/generated-artifacts/UniswapV2Sampler.json"
]

View File

@ -9,6 +9,10 @@
{
"note": "Update `ERC20BridgeSampler` artifact",
"pr": 2633
},
{
"note": "Remove `ERC20BridgeSampler` artifact",
"pr": 2647
}
]
},

View File

@ -1,475 +0,0 @@
{
"schemaVersion": "2.0.0",
"contractName": "IERC20BridgeSampler",
"compilerOutput": {
"abi": [
{
"constant": true,
"inputs": [{ "internalType": "bytes[]", "name": "callDatas", "type": "bytes[]" }],
"name": "batchCall",
"outputs": [{ "internalType": "bytes[]", "name": "callResults", "type": "bytes[]" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{ "internalType": "address", "name": "registryAddress", "type": "address" },
{ "internalType": "address", "name": "takerToken", "type": "address" },
{ "internalType": "address", "name": "makerToken", "type": "address" }
],
"name": "getLiquidityProviderFromRegistry",
"outputs": [{ "internalType": "address", "name": "providerAddress", "type": "address" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"components": [
{ "internalType": "address", "name": "makerAddress", "type": "address" },
{ "internalType": "address", "name": "takerAddress", "type": "address" },
{ "internalType": "address", "name": "feeRecipientAddress", "type": "address" },
{ "internalType": "address", "name": "senderAddress", "type": "address" },
{ "internalType": "uint256", "name": "makerAssetAmount", "type": "uint256" },
{ "internalType": "uint256", "name": "takerAssetAmount", "type": "uint256" },
{ "internalType": "uint256", "name": "makerFee", "type": "uint256" },
{ "internalType": "uint256", "name": "takerFee", "type": "uint256" },
{ "internalType": "uint256", "name": "expirationTimeSeconds", "type": "uint256" },
{ "internalType": "uint256", "name": "salt", "type": "uint256" },
{ "internalType": "bytes", "name": "makerAssetData", "type": "bytes" },
{ "internalType": "bytes", "name": "takerAssetData", "type": "bytes" },
{ "internalType": "bytes", "name": "makerFeeAssetData", "type": "bytes" },
{ "internalType": "bytes", "name": "takerFeeAssetData", "type": "bytes" }
],
"internalType": "struct LibOrder.Order[]",
"name": "orders",
"type": "tuple[]"
},
{ "internalType": "bytes[]", "name": "orderSignatures", "type": "bytes[]" },
{ "internalType": "address", "name": "devUtilsAddress", "type": "address" }
],
"name": "getOrderFillableMakerAssetAmounts",
"outputs": [
{ "internalType": "uint256[]", "name": "orderFillableMakerAssetAmounts", "type": "uint256[]" }
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"components": [
{ "internalType": "address", "name": "makerAddress", "type": "address" },
{ "internalType": "address", "name": "takerAddress", "type": "address" },
{ "internalType": "address", "name": "feeRecipientAddress", "type": "address" },
{ "internalType": "address", "name": "senderAddress", "type": "address" },
{ "internalType": "uint256", "name": "makerAssetAmount", "type": "uint256" },
{ "internalType": "uint256", "name": "takerAssetAmount", "type": "uint256" },
{ "internalType": "uint256", "name": "makerFee", "type": "uint256" },
{ "internalType": "uint256", "name": "takerFee", "type": "uint256" },
{ "internalType": "uint256", "name": "expirationTimeSeconds", "type": "uint256" },
{ "internalType": "uint256", "name": "salt", "type": "uint256" },
{ "internalType": "bytes", "name": "makerAssetData", "type": "bytes" },
{ "internalType": "bytes", "name": "takerAssetData", "type": "bytes" },
{ "internalType": "bytes", "name": "makerFeeAssetData", "type": "bytes" },
{ "internalType": "bytes", "name": "takerFeeAssetData", "type": "bytes" }
],
"internalType": "struct LibOrder.Order[]",
"name": "orders",
"type": "tuple[]"
},
{ "internalType": "bytes[]", "name": "orderSignatures", "type": "bytes[]" },
{ "internalType": "address", "name": "devUtilsAddress", "type": "address" }
],
"name": "getOrderFillableTakerAssetAmounts",
"outputs": [
{ "internalType": "uint256[]", "name": "orderFillableTakerAssetAmounts", "type": "uint256[]" }
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{ "internalType": "address", "name": "curveAddress", "type": "address" },
{ "internalType": "int128", "name": "fromTokenIdx", "type": "int128" },
{ "internalType": "int128", "name": "toTokenIdx", "type": "int128" },
{ "internalType": "uint256[]", "name": "makerTokenAmounts", "type": "uint256[]" }
],
"name": "sampleBuysFromCurve",
"outputs": [{ "internalType": "uint256[]", "name": "takerTokenAmounts", "type": "uint256[]" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{ "internalType": "address", "name": "takerToken", "type": "address" },
{ "internalType": "address", "name": "makerToken", "type": "address" },
{ "internalType": "uint256[]", "name": "makerTokenAmounts", "type": "uint256[]" }
],
"name": "sampleBuysFromEth2Dai",
"outputs": [{ "internalType": "uint256[]", "name": "takerTokenAmounts", "type": "uint256[]" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{ "internalType": "address", "name": "takerToken", "type": "address" },
{ "internalType": "address", "name": "makerToken", "type": "address" },
{ "internalType": "uint256[]", "name": "makerTokenAmounts", "type": "uint256[]" },
{
"components": [
{ "internalType": "uint256", "name": "targetSlippageBps", "type": "uint256" },
{ "internalType": "uint256", "name": "maxIterations", "type": "uint256" }
],
"internalType": "struct IERC20BridgeSampler.FakeBuyOptions",
"name": "opts",
"type": "tuple"
}
],
"name": "sampleBuysFromKyberNetwork",
"outputs": [{ "internalType": "uint256[]", "name": "takerTokenAmounts", "type": "uint256[]" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{ "internalType": "address", "name": "registryAddress", "type": "address" },
{ "internalType": "address", "name": "takerToken", "type": "address" },
{ "internalType": "address", "name": "makerToken", "type": "address" },
{ "internalType": "uint256[]", "name": "makerTokenAmounts", "type": "uint256[]" },
{
"components": [
{ "internalType": "uint256", "name": "targetSlippageBps", "type": "uint256" },
{ "internalType": "uint256", "name": "maxIterations", "type": "uint256" }
],
"internalType": "struct IERC20BridgeSampler.FakeBuyOptions",
"name": "opts",
"type": "tuple"
}
],
"name": "sampleBuysFromLiquidityProviderRegistry",
"outputs": [{ "internalType": "uint256[]", "name": "takerTokenAmounts", "type": "uint256[]" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{ "internalType": "address", "name": "takerToken", "type": "address" },
{ "internalType": "address", "name": "makerToken", "type": "address" },
{ "internalType": "uint256[]", "name": "makerTokenAmounts", "type": "uint256[]" }
],
"name": "sampleBuysFromUniswap",
"outputs": [{ "internalType": "uint256[]", "name": "takerTokenAmounts", "type": "uint256[]" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{ "internalType": "address[]", "name": "path", "type": "address[]" },
{ "internalType": "uint256[]", "name": "makerTokenAmounts", "type": "uint256[]" }
],
"name": "sampleBuysFromUniswapV2",
"outputs": [{ "internalType": "uint256[]", "name": "takerTokenAmounts", "type": "uint256[]" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{ "internalType": "address", "name": "curveAddress", "type": "address" },
{ "internalType": "int128", "name": "fromTokenIdx", "type": "int128" },
{ "internalType": "int128", "name": "toTokenIdx", "type": "int128" },
{ "internalType": "uint256[]", "name": "takerTokenAmounts", "type": "uint256[]" }
],
"name": "sampleSellsFromCurve",
"outputs": [{ "internalType": "uint256[]", "name": "makerTokenAmounts", "type": "uint256[]" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{ "internalType": "address", "name": "takerToken", "type": "address" },
{ "internalType": "address", "name": "makerToken", "type": "address" },
{ "internalType": "uint256[]", "name": "takerTokenAmounts", "type": "uint256[]" }
],
"name": "sampleSellsFromEth2Dai",
"outputs": [{ "internalType": "uint256[]", "name": "makerTokenAmounts", "type": "uint256[]" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{ "internalType": "address", "name": "takerToken", "type": "address" },
{ "internalType": "address", "name": "makerToken", "type": "address" },
{ "internalType": "uint256[]", "name": "takerTokenAmounts", "type": "uint256[]" }
],
"name": "sampleSellsFromKyberNetwork",
"outputs": [{ "internalType": "uint256[]", "name": "makerTokenAmounts", "type": "uint256[]" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{ "internalType": "address", "name": "registryAddress", "type": "address" },
{ "internalType": "address", "name": "takerToken", "type": "address" },
{ "internalType": "address", "name": "makerToken", "type": "address" },
{ "internalType": "uint256[]", "name": "takerTokenAmounts", "type": "uint256[]" }
],
"name": "sampleSellsFromLiquidityProviderRegistry",
"outputs": [{ "internalType": "uint256[]", "name": "makerTokenAmounts", "type": "uint256[]" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{ "internalType": "address", "name": "multibridge", "type": "address" },
{ "internalType": "address", "name": "takerToken", "type": "address" },
{ "internalType": "address", "name": "intermediateToken", "type": "address" },
{ "internalType": "address", "name": "makerToken", "type": "address" },
{ "internalType": "uint256[]", "name": "takerTokenAmounts", "type": "uint256[]" }
],
"name": "sampleSellsFromMultiBridge",
"outputs": [{ "internalType": "uint256[]", "name": "makerTokenAmounts", "type": "uint256[]" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{ "internalType": "address", "name": "takerToken", "type": "address" },
{ "internalType": "address", "name": "makerToken", "type": "address" },
{ "internalType": "uint256[]", "name": "takerTokenAmounts", "type": "uint256[]" }
],
"name": "sampleSellsFromUniswap",
"outputs": [{ "internalType": "uint256[]", "name": "makerTokenAmounts", "type": "uint256[]" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{ "internalType": "address[]", "name": "path", "type": "address[]" },
{ "internalType": "uint256[]", "name": "takerTokenAmounts", "type": "uint256[]" }
],
"name": "sampleSellsFromUniswapV2",
"outputs": [{ "internalType": "uint256[]", "name": "makerTokenAmounts", "type": "uint256[]" }],
"payable": false,
"stateMutability": "view",
"type": "function"
}
],
"devdoc": {
"methods": {
"batchCall(bytes[])": {
"details": "Call multiple public functions on this contract in a single transaction.",
"params": { "callDatas": "ABI-encoded call data for each function call." },
"return": "callResults ABI-encoded results data for each call."
},
"getLiquidityProviderFromRegistry(address,address,address)": {
"details": "Returns the address of a liquidity provider for the given market (takerToken, makerToken), from a registry of liquidity providers. Returns address(0) if no such provider exists in the registry.",
"params": {
"makerToken": "Maker asset managed by liquidity provider.",
"takerToken": "Taker asset managed by liquidity provider."
},
"return": "providerAddress Address of the liquidity provider."
},
"getOrderFillableMakerAssetAmounts((address,address,address,address,uint256,uint256,uint256,uint256,uint256,uint256,bytes,bytes,bytes,bytes)[],bytes[],address)": {
"details": "Queries the fillable maker asset amounts of native orders.",
"params": {
"devUtilsAddress": "Address to the DevUtils contract.",
"orderSignatures": "Signatures for each respective order in `orders`.",
"orders": "Native orders to query."
},
"return": "orderFillableMakerAssetAmounts How much maker asset can be filled by each order in `orders`."
},
"getOrderFillableTakerAssetAmounts((address,address,address,address,uint256,uint256,uint256,uint256,uint256,uint256,bytes,bytes,bytes,bytes)[],bytes[],address)": {
"details": "Queries the fillable taker asset amounts of native orders.",
"params": {
"devUtilsAddress": "Address to the DevUtils contract.",
"orderSignatures": "Signatures for each respective order in `orders`.",
"orders": "Native orders to query."
},
"return": "orderFillableTakerAssetAmounts How much taker asset can be filled by each order in `orders`."
},
"sampleBuysFromCurve(address,int128,int128,uint256[])": {
"details": "Sample buy quotes from Curve.",
"params": {
"curveAddress": "Address of the Curve contract.",
"fromTokenIdx": "Index of the taker token (what to sell).",
"makerTokenAmounts": "Maker token buy amount for each sample.",
"toTokenIdx": "Index of the maker token (what to buy)."
},
"return": "takerTokenAmounts Taker amounts sold at each maker token amount."
},
"sampleBuysFromEth2Dai(address,address,uint256[])": {
"details": "Sample buy quotes from Eth2Dai/Oasis.",
"params": {
"makerToken": "Address of the maker token (what to buy).",
"makerTokenAmounts": "Maker token buy amount for each sample.",
"takerToken": "Address of the taker token (what to sell)."
},
"return": "takerTokenAmounts Taker amounts sold at each maker token amount."
},
"sampleBuysFromKyberNetwork(address,address,uint256[],(uint256,uint256))": {
"details": "Sample buy quotes from Kyber.",
"params": {
"makerToken": "Address of the maker token (what to buy).",
"makerTokenAmounts": "Maker token buy amount for each sample.",
"opts": "`FakeBuyOptions` specifying target slippage and max iterations.",
"takerToken": "Address of the taker token (what to sell)."
},
"return": "takerTokenAmounts Taker amounts sold at each maker token amount."
},
"sampleBuysFromLiquidityProviderRegistry(address,address,address,uint256[],(uint256,uint256))": {
"details": "Sample buy quotes from an arbitrary on-chain liquidity provider.",
"params": {
"makerToken": "Address of the maker token (what to buy).",
"makerTokenAmounts": "Maker token buy amount for each sample.",
"opts": "`FakeBuyOptions` specifying target slippage and max iterations.",
"registryAddress": "Address of the liquidity provider registry contract.",
"takerToken": "Address of the taker token (what to sell)."
},
"return": "takerTokenAmounts Taker amounts sold at each maker token amount."
},
"sampleBuysFromUniswap(address,address,uint256[])": {
"details": "Sample buy quotes from Uniswap.",
"params": {
"makerToken": "Address of the maker token (what to buy).",
"makerTokenAmounts": "Maker token buy amount for each sample.",
"takerToken": "Address of the taker token (what to sell)."
},
"return": "takerTokenAmounts Taker amounts sold at each maker token amount."
},
"sampleBuysFromUniswapV2(address[],uint256[])": {
"details": "Sample buy quotes from UniswapV2.",
"params": {
"makerTokenAmounts": "Maker token buy amount for each sample.",
"path": "Token route."
},
"return": "takerTokenAmounts Taker amounts sold at each maker token amount."
},
"sampleSellsFromCurve(address,int128,int128,uint256[])": {
"details": "Sample sell quotes from Curve.",
"params": {
"curveAddress": "Address of the Curve contract.",
"fromTokenIdx": "Index of the taker token (what to sell).",
"takerTokenAmounts": "Taker token sell amount for each sample.",
"toTokenIdx": "Index of the maker token (what to buy)."
},
"return": "makerTokenAmounts Maker amounts bought at each taker token amount."
},
"sampleSellsFromEth2Dai(address,address,uint256[])": {
"details": "Sample sell quotes from Eth2Dai/Oasis.",
"params": {
"makerToken": "Address of the maker token (what to buy).",
"takerToken": "Address of the taker token (what to sell).",
"takerTokenAmounts": "Taker token sell amount for each sample."
},
"return": "makerTokenAmounts Maker amounts bought at each taker token amount."
},
"sampleSellsFromKyberNetwork(address,address,uint256[])": {
"details": "Sample sell quotes from Kyber.",
"params": {
"makerToken": "Address of the maker token (what to buy).",
"takerToken": "Address of the taker token (what to sell).",
"takerTokenAmounts": "Taker token sell amount for each sample."
},
"return": "makerTokenAmounts Maker amounts bought at each taker token amount."
},
"sampleSellsFromLiquidityProviderRegistry(address,address,address,uint256[])": {
"details": "Sample sell quotes from an arbitrary on-chain liquidity provider.",
"params": {
"makerToken": "Address of the maker token (what to buy).",
"registryAddress": "Address of the liquidity provider registry contract.",
"takerToken": "Address of the taker token (what to sell).",
"takerTokenAmounts": "Taker token sell amount for each sample."
},
"return": "makerTokenAmounts Maker amounts bought at each taker token amount."
},
"sampleSellsFromMultiBridge(address,address,address,address,uint256[])": {
"details": "Sample sell quotes from MultiBridge.",
"params": {
"intermediateToken": "The address of the intermediate token to use in an indirect route.",
"makerToken": "Address of the maker token (what to buy).",
"multibridge": "Address of the MultiBridge contract.",
"takerToken": "Address of the taker token (what to sell).",
"takerTokenAmounts": "Taker token sell amount for each sample."
},
"return": "makerTokenAmounts Maker amounts bought at each taker token amount."
},
"sampleSellsFromUniswap(address,address,uint256[])": {
"details": "Sample sell quotes from Uniswap.",
"params": {
"makerToken": "Address of the maker token (what to buy).",
"takerToken": "Address of the taker token (what to sell).",
"takerTokenAmounts": "Taker token sell amount for each sample."
},
"return": "makerTokenAmounts Maker amounts bought at each taker token amount."
},
"sampleSellsFromUniswapV2(address[],uint256[])": {
"details": "Sample sell quotes from UniswapV2.",
"params": {
"path": "Token route.",
"takerTokenAmounts": "Taker token sell amount for each sample."
},
"return": "makerTokenAmounts Maker amounts bought at each taker token amount."
}
}
},
"evm": { "bytecode": { "object": "0x" }, "deployedBytecode": { "object": "0x" } }
},
"compiler": {
"name": "solc",
"version": "0.5.17+commit.d19bba13",
"settings": {
"optimizer": {
"enabled": true,
"runs": 1000000,
"details": { "yul": true, "deduplicate": true, "cse": true, "constantOptimizer": true }
},
"outputSelection": {
"*": {
"*": [
"abi",
"devdoc",
"evm.bytecode.object",
"evm.bytecode.sourceMap",
"evm.deployedBytecode.object",
"evm.deployedBytecode.sourceMap"
]
}
},
"evmVersion": "istanbul"
}
},
"chains": {}
}

View File

@ -32,7 +32,7 @@
"wrappers:generate": "abi-gen --abis ${npm_package_config_abis} --output src/generated-wrappers --backend ethers"
},
"config": {
"abis": "../contract-artifacts/artifacts/@(DevUtils|ERC20Token|ERC721Token|Exchange|Forwarder|IAssetData|LibTransactionDecoder|WETH9|Coordinator|Staking|StakingProxy|IERC20BridgeSampler|ERC20BridgeSampler|GodsUnchainedValidator|Broker|ILiquidityProvider|ILiquidityProviderRegistry|MaximumGasPrice|ITransformERC20|IZeroEx).json"
"abis": "../contract-artifacts/artifacts/@(DevUtils|ERC20Token|ERC721Token|Exchange|Forwarder|IAssetData|LibTransactionDecoder|WETH9|Coordinator|Staking|StakingProxy|GodsUnchainedValidator|Broker|ILiquidityProvider|ILiquidityProviderRegistry|MaximumGasPrice|ITransformERC20|IZeroEx).json"
},
"gitpkg": {
"registry": "git@github.com:0xProject/gitpkg-registry.git"

View File

@ -1,7 +1,7 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"target": "es6",
"lib": ["es2017", "dom", "esnext.asynciterable", "es2018.promise"],
"experimentalDecorators": true,
"downlevelIteration": true,