feat: ERC20BridgeSampler Unlock Kyber collisions (#2575)
* feat: ERC20BridgeSampler Unlock Kyber collisions * Updated fallback strategy * Address comments * Eth2Dai hop sampler * Update packages/asset-swapper/src/utils/market_operation_utils/index.ts Co-authored-by: Lawrence Forman <lawrence@0xproject.com> * Set DFB expiry to 2hr Co-authored-by: Lawrence Forman <lawrence@0xproject.com>
This commit is contained in:
parent
fe36cd86bb
commit
fb0311e675
@ -11,8 +11,12 @@
|
||||
"pr": 2551
|
||||
},
|
||||
{
|
||||
"note": "Added `sampleBuysFromKyberNetwork` ",
|
||||
"note": "Added `sampleBuysFromKyberNetwork`",
|
||||
"pr": 2551
|
||||
},
|
||||
{
|
||||
"note": "Use `searchBestRate` in Kyber samples. Return 0 when Uniswap/Eth2Dai reserve",
|
||||
"pr": 2575
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -29,6 +29,7 @@ import "./IDevUtils.sol";
|
||||
import "./IERC20BridgeSampler.sol";
|
||||
import "./IEth2Dai.sol";
|
||||
import "./IKyberNetwork.sol";
|
||||
import "./IKyberNetworkProxy.sol";
|
||||
import "./IUniswapExchangeQuotes.sol";
|
||||
import "./ICurve.sol";
|
||||
import "./ILiquidityProvider.sol";
|
||||
@ -52,6 +53,10 @@ contract ERC20BridgeSampler is
|
||||
uint256 constant internal CURVE_CALL_GAS = 600e3; // 600k
|
||||
/// @dev Default gas limit for liquidity provider calls.
|
||||
uint256 constant internal DEFAULT_CALL_GAS = 200e3; // 200k
|
||||
/// @dev The Kyber Uniswap Reserve address
|
||||
address constant internal KYBER_UNIWAP_RESERVE = 0x31E085Afd48a1d6e51Cc193153d625e8f0514C7F;
|
||||
/// @dev The Kyber Eth2Dai Reserve address
|
||||
address constant internal KYBER_ETH2DAI_RESERVE = 0x1E158c0e93c30d24e918Ef83d1e0bE23595C3c0f;
|
||||
|
||||
address private _devUtilsAddress;
|
||||
|
||||
@ -183,33 +188,33 @@ contract ERC20BridgeSampler is
|
||||
returns (uint256[] memory makerTokenAmounts)
|
||||
{
|
||||
_assertValidPair(makerToken, takerToken);
|
||||
address _takerToken = takerToken == _getWethAddress() ? KYBER_ETH_ADDRESS : takerToken;
|
||||
address _makerToken = makerToken == _getWethAddress() ? KYBER_ETH_ADDRESS : makerToken;
|
||||
uint256 takerTokenDecimals = _getTokenDecimals(takerToken);
|
||||
uint256 makerTokenDecimals = _getTokenDecimals(makerToken);
|
||||
uint256 numSamples = takerTokenAmounts.length;
|
||||
makerTokenAmounts = new uint256[](numSamples);
|
||||
address wethAddress = _getWethAddress();
|
||||
uint256 value;
|
||||
address reserve;
|
||||
for (uint256 i = 0; i < numSamples; i++) {
|
||||
(bool didSucceed, bytes memory resultData) =
|
||||
_getKyberNetworkProxyAddress().staticcall.gas(KYBER_CALL_GAS)(
|
||||
abi.encodeWithSelector(
|
||||
IKyberNetwork(0).getExpectedRate.selector,
|
||||
_takerToken,
|
||||
_makerToken,
|
||||
takerTokenAmounts[i]
|
||||
));
|
||||
uint256 rate = 0;
|
||||
if (didSucceed) {
|
||||
rate = abi.decode(resultData, (uint256));
|
||||
} else {
|
||||
break;
|
||||
if (takerToken == wethAddress || makerToken == wethAddress) {
|
||||
// Direct ETH based trade
|
||||
(value, reserve) = _sampleSellFromKyberNetwork(takerToken, makerToken, takerTokenAmounts[i]);
|
||||
// If this fills on an on-chain reserve we remove it as that can introduce collisions
|
||||
if (reserve == KYBER_UNIWAP_RESERVE || reserve == KYBER_ETH2DAI_RESERVE) {
|
||||
value = 0;
|
||||
}
|
||||
makerTokenAmounts[i] =
|
||||
rate *
|
||||
takerTokenAmounts[i] *
|
||||
10 ** makerTokenDecimals /
|
||||
10 ** takerTokenDecimals /
|
||||
10 ** 18;
|
||||
} else {
|
||||
// Hop to ETH
|
||||
(value, reserve) = _sampleSellFromKyberNetwork(takerToken, wethAddress, takerTokenAmounts[i]);
|
||||
if (value != 0) {
|
||||
address otherReserve;
|
||||
(value, otherReserve) = _sampleSellFromKyberNetwork(wethAddress, makerToken, value);
|
||||
// If this fills on Eth2Dai it is ok as we queried a different market
|
||||
// If this fills on Uniswap on both legs then this is a hard collision
|
||||
if (reserve == KYBER_UNIWAP_RESERVE && reserve == otherReserve) {
|
||||
value = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
makerTokenAmounts[i] = value;
|
||||
}
|
||||
}
|
||||
|
||||
@ -277,6 +282,31 @@ contract ERC20BridgeSampler is
|
||||
}
|
||||
}
|
||||
|
||||
/// @dev Sample sell quotes from Eth2Dai/Oasis using a hop to an intermediate token.
|
||||
/// I.e WBTC/DAI via ETH or WBTC/ETH via DAI
|
||||
/// @param takerToken Address of the taker token (what to sell).
|
||||
/// @param makerToken Address of the maker token (what to buy).
|
||||
/// @param intermediateToken Address of the token to hop to.
|
||||
/// @param takerTokenAmounts Taker token sell amount for each sample.
|
||||
/// @return makerTokenAmounts Maker amounts bought at each taker token
|
||||
/// amount.
|
||||
function sampleSellsFromEth2DaiHop(
|
||||
address takerToken,
|
||||
address makerToken,
|
||||
address intermediateToken,
|
||||
uint256[] memory takerTokenAmounts
|
||||
)
|
||||
public
|
||||
view
|
||||
returns (uint256[] memory makerTokenAmounts)
|
||||
{
|
||||
if (makerToken == intermediateToken || takerToken == intermediateToken) {
|
||||
return makerTokenAmounts;
|
||||
}
|
||||
uint256[] memory intermediateAmounts = sampleSellsFromEth2Dai(takerToken, intermediateToken, takerTokenAmounts);
|
||||
makerTokenAmounts = sampleSellsFromEth2Dai(intermediateToken, makerToken, intermediateAmounts);
|
||||
}
|
||||
|
||||
/// @dev Sample buy quotes from Eth2Dai/Oasis.
|
||||
/// @param takerToken Address of the taker token (what to sell).
|
||||
/// @param makerToken Address of the maker token (what to buy).
|
||||
@ -807,4 +837,51 @@ contract ERC20BridgeSampler is
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function _sampleSellFromKyberNetwork(
|
||||
address takerToken,
|
||||
address makerToken,
|
||||
uint256 takerTokenAmount
|
||||
)
|
||||
private
|
||||
view
|
||||
returns (uint256 makerTokenAmount, address reserve)
|
||||
{
|
||||
address _takerToken = takerToken == _getWethAddress() ? KYBER_ETH_ADDRESS : takerToken;
|
||||
address _makerToken = makerToken == _getWethAddress() ? KYBER_ETH_ADDRESS : makerToken;
|
||||
uint256 takerTokenDecimals = _getTokenDecimals(takerToken);
|
||||
uint256 makerTokenDecimals = _getTokenDecimals(makerToken);
|
||||
(bool didSucceed, bytes memory resultData) = _getKyberNetworkProxyAddress().staticcall.gas(DEFAULT_CALL_GAS)(
|
||||
abi.encodeWithSelector(
|
||||
IKyberNetworkProxy(0).kyberNetworkContract.selector
|
||||
));
|
||||
if (!didSucceed) {
|
||||
return (0, address(0));
|
||||
}
|
||||
address kyberNetworkContract = abi.decode(resultData, (address));
|
||||
(didSucceed, resultData) =
|
||||
kyberNetworkContract.staticcall.gas(KYBER_CALL_GAS)(
|
||||
abi.encodeWithSelector(
|
||||
IKyberNetwork(0).searchBestRate.selector,
|
||||
_takerToken,
|
||||
_makerToken,
|
||||
takerTokenAmount,
|
||||
false // usePermissionless
|
||||
));
|
||||
uint256 rate = 0;
|
||||
address reserve;
|
||||
if (didSucceed) {
|
||||
(reserve, rate) = abi.decode(resultData, (address, uint256));
|
||||
} else {
|
||||
return (0, address(0));
|
||||
}
|
||||
makerTokenAmount =
|
||||
rate *
|
||||
takerTokenAmount *
|
||||
10 ** makerTokenDecimals /
|
||||
10 ** takerTokenDecimals /
|
||||
10 ** 18;
|
||||
|
||||
return (makerTokenAmount, reserve);
|
||||
}
|
||||
}
|
||||
|
@ -21,12 +21,13 @@ pragma solidity ^0.5.9;
|
||||
|
||||
interface IKyberNetwork {
|
||||
|
||||
function getExpectedRate(
|
||||
function searchBestRate(
|
||||
address fromToken,
|
||||
address toToken,
|
||||
uint256 fromAmount
|
||||
uint256 fromAmount,
|
||||
bool usePermissionless
|
||||
)
|
||||
external
|
||||
view
|
||||
returns (uint256 expectedRate, uint256 slippageRate);
|
||||
returns (address reserve, uint256 expectedRate);
|
||||
}
|
||||
|
@ -0,0 +1,34 @@
|
||||
/*
|
||||
|
||||
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 IKyberNetworkProxy {
|
||||
|
||||
function kyberNetworkContract() external view returns (address);
|
||||
|
||||
function getExpectedRate(
|
||||
address fromToken,
|
||||
address toToken,
|
||||
uint256 fromAmount
|
||||
)
|
||||
external
|
||||
view
|
||||
returns (uint256 expectedRate, uint256 slippageRate);
|
||||
}
|
@ -24,7 +24,7 @@ import "@0x/contracts-exchange-libs/contracts/src/LibOrder.sol";
|
||||
import "../src/ERC20BridgeSampler.sol";
|
||||
import "../src/IEth2Dai.sol";
|
||||
import "../src/IDevUtils.sol";
|
||||
import "../src/IKyberNetwork.sol";
|
||||
import "../src/IKyberNetworkProxy.sol";
|
||||
|
||||
|
||||
library LibDeterministicQuotes {
|
||||
@ -202,7 +202,30 @@ contract TestERC20BridgeSamplerKyberNetwork is
|
||||
bytes32 constant private SALT = 0x0ff3ca9d46195c39f9a12afb74207b4970349fb3cfb1e459bbf170298d326bc7;
|
||||
address constant public ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
|
||||
|
||||
// Deterministic `IKyberNetwork.getExpectedRate()`.
|
||||
function kyberNetworkContract()
|
||||
external
|
||||
view
|
||||
returns (address)
|
||||
{
|
||||
return address(this);
|
||||
}
|
||||
|
||||
// IKyberNetwork not exposed via IKyberNetworkProxy
|
||||
function searchBestRate(
|
||||
address fromToken,
|
||||
address toToken,
|
||||
uint256 fromAmount,
|
||||
bool // usePermissionless
|
||||
)
|
||||
external
|
||||
view
|
||||
returns (address reserve, uint256 expectedRate)
|
||||
{
|
||||
(expectedRate, ) = this.getExpectedRate(fromToken, toToken, fromAmount);
|
||||
return (address(this), expectedRate);
|
||||
}
|
||||
|
||||
// Deterministic `IKyberNetworkProxy.getExpectedRate()`.
|
||||
function getExpectedRate(
|
||||
address fromToken,
|
||||
address toToken,
|
||||
|
@ -38,7 +38,7 @@
|
||||
"config": {
|
||||
"publicInterfaceContracts": "ERC20BridgeSampler,IERC20BridgeSampler,ILiquidityProvider,ILiquidityProviderRegistry,DummyLiquidityProviderRegistry,DummyLiquidityProvider",
|
||||
"abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.",
|
||||
"abis": "./test/generated-artifacts/@(DummyLiquidityProvider|DummyLiquidityProviderRegistry|ERC20BridgeSampler|ICurve|IDevUtils|IERC20BridgeSampler|IEth2Dai|IKyberNetwork|ILiquidityProvider|ILiquidityProviderRegistry|IUniswapExchangeQuotes|TestERC20BridgeSampler).json"
|
||||
"abis": "./test/generated-artifacts/@(DummyLiquidityProvider|DummyLiquidityProviderRegistry|ERC20BridgeSampler|ICurve|IDevUtils|IERC20BridgeSampler|IEth2Dai|IKyberNetwork|IKyberNetworkProxy|ILiquidityProvider|ILiquidityProviderRegistry|IUniswapExchangeQuotes|TestERC20BridgeSampler).json"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -13,6 +13,7 @@ import * as IDevUtils from '../test/generated-artifacts/IDevUtils.json';
|
||||
import * as IERC20BridgeSampler from '../test/generated-artifacts/IERC20BridgeSampler.json';
|
||||
import * as IEth2Dai from '../test/generated-artifacts/IEth2Dai.json';
|
||||
import * as IKyberNetwork from '../test/generated-artifacts/IKyberNetwork.json';
|
||||
import * as IKyberNetworkProxy from '../test/generated-artifacts/IKyberNetworkProxy.json';
|
||||
import * as ILiquidityProvider from '../test/generated-artifacts/ILiquidityProvider.json';
|
||||
import * as ILiquidityProviderRegistry from '../test/generated-artifacts/ILiquidityProviderRegistry.json';
|
||||
import * as IUniswapExchangeQuotes from '../test/generated-artifacts/IUniswapExchangeQuotes.json';
|
||||
@ -26,6 +27,7 @@ export const artifacts = {
|
||||
IERC20BridgeSampler: IERC20BridgeSampler as ContractArtifact,
|
||||
IEth2Dai: IEth2Dai as ContractArtifact,
|
||||
IKyberNetwork: IKyberNetwork as ContractArtifact,
|
||||
IKyberNetworkProxy: IKyberNetworkProxy as ContractArtifact,
|
||||
ILiquidityProvider: ILiquidityProvider as ContractArtifact,
|
||||
ILiquidityProviderRegistry: ILiquidityProviderRegistry as ContractArtifact,
|
||||
IUniswapExchangeQuotes: IUniswapExchangeQuotes as ContractArtifact,
|
||||
|
@ -343,7 +343,8 @@ blockchainTests('erc20-bridge-sampler', env => {
|
||||
|
||||
it('can quote token -> token', async () => {
|
||||
const sampleAmounts = getSampleAmounts(TAKER_TOKEN);
|
||||
const [expectedQuotes] = getDeterministicSellQuotes(TAKER_TOKEN, MAKER_TOKEN, ['Kyber'], sampleAmounts);
|
||||
const [takerToEthQuotes] = getDeterministicSellQuotes(TAKER_TOKEN, WETH_ADDRESS, ['Kyber'], sampleAmounts);
|
||||
const [expectedQuotes] = getDeterministicSellQuotes(WETH_ADDRESS, MAKER_TOKEN, ['Kyber'], takerToEthQuotes);
|
||||
const quotes = await testContract
|
||||
.sampleSellsFromKyberNetwork(TAKER_TOKEN, MAKER_TOKEN, sampleAmounts)
|
||||
.callAsync();
|
||||
@ -445,7 +446,8 @@ blockchainTests('erc20-bridge-sampler', env => {
|
||||
|
||||
it('can quote token -> token', async () => {
|
||||
const sampleAmounts = getSampleAmounts(TAKER_TOKEN);
|
||||
const [expectedQuotes] = getDeterministicBuyQuotes(TAKER_TOKEN, MAKER_TOKEN, ['Kyber'], sampleAmounts);
|
||||
const [ethToMakerQuotes] = getDeterministicBuyQuotes(WETH_ADDRESS, MAKER_TOKEN, ['Kyber'], sampleAmounts);
|
||||
const [expectedQuotes] = getDeterministicBuyQuotes(TAKER_TOKEN, WETH_ADDRESS, ['Kyber'], ethToMakerQuotes);
|
||||
const quotes = await testContract
|
||||
.sampleBuysFromKyberNetwork(TAKER_TOKEN, MAKER_TOKEN, sampleAmounts, FAKE_BUY_OPTS)
|
||||
.callAsync();
|
||||
|
@ -11,6 +11,7 @@ export * from '../test/generated-wrappers/i_dev_utils';
|
||||
export * from '../test/generated-wrappers/i_erc20_bridge_sampler';
|
||||
export * from '../test/generated-wrappers/i_eth2_dai';
|
||||
export * from '../test/generated-wrappers/i_kyber_network';
|
||||
export * from '../test/generated-wrappers/i_kyber_network_proxy';
|
||||
export * from '../test/generated-wrappers/i_liquidity_provider';
|
||||
export * from '../test/generated-wrappers/i_liquidity_provider_registry';
|
||||
export * from '../test/generated-wrappers/i_uniswap_exchange_quotes';
|
||||
|
@ -17,6 +17,7 @@
|
||||
"test/generated-artifacts/IERC20BridgeSampler.json",
|
||||
"test/generated-artifacts/IEth2Dai.json",
|
||||
"test/generated-artifacts/IKyberNetwork.json",
|
||||
"test/generated-artifacts/IKyberNetworkProxy.json",
|
||||
"test/generated-artifacts/ILiquidityProvider.json",
|
||||
"test/generated-artifacts/ILiquidityProviderRegistry.json",
|
||||
"test/generated-artifacts/IUniswapExchangeQuotes.json",
|
||||
|
@ -65,6 +65,10 @@
|
||||
{
|
||||
"note": "Apply Native order penalty inline with the target amount",
|
||||
"pr": 2565
|
||||
},
|
||||
{
|
||||
"note": "Remove Kyber exclusion when Uniswap/Eth2Dai is present",
|
||||
"pr": 2575
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -12,8 +12,8 @@
|
||||
"watch": "tsc -w -p tsconfig.json",
|
||||
"build:ci": "yarn build",
|
||||
"lint": "tslint --format stylish --project . && yarn prettier",
|
||||
"prettier": "prettier --check 'src/**/*.{ts,tsx,json,md}' --config ../../.prettierrc",
|
||||
"fix": "tslint --fix --format stylish --project .",
|
||||
"prettier": "prettier --write '**/*.{ts,tsx,json,md}' --config ../../.prettierrc",
|
||||
"fix": "tslint --fix --format stylish --project . && yarn prettier",
|
||||
"test": "yarn run_mocha",
|
||||
"rebuild_and_test": "run-s clean build test",
|
||||
"test:coverage": "nyc npm run test --all && yarn coverage:report:lcov",
|
||||
|
@ -119,18 +119,17 @@ function dexQuotesToPaths(
|
||||
fees: { [source: string]: BigNumber },
|
||||
): Fill[][] {
|
||||
const paths: Fill[][] = [];
|
||||
for (const quote of dexQuotes) {
|
||||
for (let quote of dexQuotes) {
|
||||
const path: Fill[] = [];
|
||||
// Drop any non-zero entries. This can occur if the any fills on Kyber were UniswapReserves
|
||||
// We need not worry about Kyber fills going to UniswapReserve as the input amount
|
||||
// we fill is the same as we sampled. I.e we received [0,20,30] output from [1,2,3] input
|
||||
// and we only fill [2,3] on Kyber (as 1 returns 0 output)
|
||||
quote = quote.filter(q => !q.output.isZero());
|
||||
for (let i = 0; i < quote.length; i++) {
|
||||
const sample = quote[i];
|
||||
const prevSample = i === 0 ? undefined : quote[i - 1];
|
||||
const source = sample.source;
|
||||
// Stop if the sample has zero output, which can occur if the source
|
||||
// cannot fill the full amount.
|
||||
// TODO(dorothy-zbornak): Sometimes Kyber will dip to zero then pick back up.
|
||||
if (sample.output.eq(0)) {
|
||||
break;
|
||||
}
|
||||
const input = sample.input.minus(prevSample ? prevSample.input : 0);
|
||||
const output = sample.output.minus(prevSample ? prevSample.output : 0);
|
||||
const penalty =
|
||||
@ -162,12 +161,6 @@ function sourceToFillFlags(source: ERC20BridgeSource): number {
|
||||
if (source === ERC20BridgeSource.Kyber) {
|
||||
return FillFlags.Kyber;
|
||||
}
|
||||
if (source === ERC20BridgeSource.Eth2Dai) {
|
||||
return FillFlags.ConflictsWithKyber;
|
||||
}
|
||||
if (source === ERC20BridgeSource.Uniswap) {
|
||||
return FillFlags.ConflictsWithKyber;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
@ -7,30 +7,11 @@ import { RfqtIndicativeQuoteResponse } from '../quote_requestor';
|
||||
import { difference } from '../utils';
|
||||
|
||||
import { BUY_SOURCES, DEFAULT_GET_MARKET_ORDERS_OPTS, FEE_QUOTE_SOURCES, ONE_ETHER, SELL_SOURCES } from './constants';
|
||||
import {
|
||||
createFillPaths,
|
||||
getFallbackSourcePaths,
|
||||
getPathAdjustedRate,
|
||||
getPathAdjustedSlippage,
|
||||
getPathSize,
|
||||
} from './fills';
|
||||
import {
|
||||
createOrdersFromPath,
|
||||
createSignedOrdersFromRfqtIndicativeQuotes,
|
||||
createSignedOrdersWithFillableAmounts,
|
||||
getNativeOrderTokens,
|
||||
} from './orders';
|
||||
import { createFillPaths, getPathAdjustedRate, getPathAdjustedSlippage } from './fills';
|
||||
import { createOrdersFromPath, createSignedOrdersFromRfqtIndicativeQuotes, createSignedOrdersWithFillableAmounts, getNativeOrderTokens } from './orders';
|
||||
import { findOptimalPath } from './path_optimizer';
|
||||
import { DexOrderSampler, getSampleAmounts } from './sampler';
|
||||
import {
|
||||
AggregationError,
|
||||
DexSample,
|
||||
ERC20BridgeSource,
|
||||
Fill,
|
||||
GetMarketOrdersOpts,
|
||||
OptimizedMarketOrder,
|
||||
OrderDomain,
|
||||
} from './types';
|
||||
import { AggregationError, DexSample, ERC20BridgeSource, GetMarketOrdersOpts, OptimizedMarketOrder, OrderDomain } from './types';
|
||||
|
||||
async function getRfqtIndicativeQuotesAsync(
|
||||
makerAssetData: string,
|
||||
@ -343,32 +324,42 @@ export class MarketOperationUtils {
|
||||
feeSchedule: opts.feeSchedule,
|
||||
});
|
||||
// Find the optimal path.
|
||||
const optimalPath = findOptimalPath(side, paths, inputAmount, opts.runLimit) || [];
|
||||
// TODO(dorothy-zbornak): Ensure the slippage on the optimal path is <= maxFallbackSlippage
|
||||
// once we decide on a good baseline.
|
||||
let optimalPath = findOptimalPath(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.
|
||||
let fallbackPath: Fill[] = [];
|
||||
const nativeSubPath = optimalPath.filter(f => f.source === ERC20BridgeSource.Native);
|
||||
if (opts.allowFallback && nativeSubPath.length !== 0) {
|
||||
// The fallback path is, at most, as large as the native path.
|
||||
const fallbackInputAmount = BigNumber.min(inputAmount, getPathSize(nativeSubPath, inputAmount)[0]);
|
||||
fallbackPath =
|
||||
findOptimalPath(side, getFallbackSourcePaths(optimalPath, paths), fallbackInputAmount, opts.runLimit) ||
|
||||
[];
|
||||
// We create a fallback path that is exclusive of Native liquidity
|
||||
// This is the optimal on-chain path for the entire input amount
|
||||
const nonNativePaths = paths.filter(p => p.length > 0 && p[0].source !== ERC20BridgeSource.Native);
|
||||
const nonNativeOptimalPath = findOptimalPath(side, nonNativePaths, inputAmount, opts.runLimit) || [];
|
||||
// Calculate the slippage of on-chain sources compared to the most optimal path
|
||||
const fallbackSlippage = getPathAdjustedSlippage(
|
||||
side,
|
||||
fallbackPath,
|
||||
fallbackInputAmount,
|
||||
nonNativeOptimalPath,
|
||||
inputAmount,
|
||||
getPathAdjustedRate(side, optimalPath, inputAmount),
|
||||
);
|
||||
if (fallbackSlippage > maxFallbackSlippage) {
|
||||
fallbackPath = [];
|
||||
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
|
||||
// want to fully fill when it gets prepended to the front below
|
||||
const [last, penultimateIfExists] = optimalPath.slice().reverse();
|
||||
const lastNativeFillIfExists =
|
||||
last.source === ERC20BridgeSource.Native &&
|
||||
penultimateIfExists &&
|
||||
penultimateIfExists.source !== ERC20BridgeSource.Native
|
||||
? last
|
||||
: undefined;
|
||||
// By prepending native paths to the front they cannot split on-chain sources and incur
|
||||
// an additional protocol fee. I.e [Uniswap,Native,Kyber] becomes [Native,Uniswap,Kyber]
|
||||
// In the previous step we dropped any hanging Native partial fills, as to not fully fill
|
||||
optimalPath = [...nativeSubPath.filter(f => f !== lastNativeFillIfExists), ...nonNativeOptimalPath];
|
||||
}
|
||||
}
|
||||
return createOrdersFromPath([...optimalPath, ...fallbackPath], {
|
||||
return createOrdersFromPath(optimalPath, {
|
||||
side,
|
||||
inputToken,
|
||||
outputToken,
|
||||
|
@ -7,25 +7,9 @@ import { MarketOperation, SignedOrderWithFillableAmounts } from '../../types';
|
||||
import { RfqtIndicativeQuoteResponse } from '../quote_requestor';
|
||||
import { getCurveInfo, isCurveSource } from '../source_utils';
|
||||
|
||||
import {
|
||||
ERC20_PROXY_ID,
|
||||
NULL_ADDRESS,
|
||||
NULL_BYTES,
|
||||
ONE_HOUR_IN_SECONDS,
|
||||
ONE_SECOND_MS,
|
||||
WALLET_SIGNATURE,
|
||||
ZERO_AMOUNT,
|
||||
} from './constants';
|
||||
import { ERC20_PROXY_ID, NULL_ADDRESS, NULL_BYTES, ONE_HOUR_IN_SECONDS, ONE_SECOND_MS, WALLET_SIGNATURE, ZERO_AMOUNT } from './constants';
|
||||
import { collapsePath } from './fills';
|
||||
import {
|
||||
AggregationError,
|
||||
CollapsedFill,
|
||||
ERC20BridgeSource,
|
||||
Fill,
|
||||
NativeCollapsedFill,
|
||||
OptimizedMarketOrder,
|
||||
OrderDomain,
|
||||
} from './types';
|
||||
import { AggregationError, CollapsedFill, ERC20BridgeSource, Fill, NativeCollapsedFill, OptimizedMarketOrder, OrderDomain } from './types';
|
||||
|
||||
// tslint:disable completed-docs no-unnecessary-type-assertion
|
||||
|
||||
@ -332,7 +316,8 @@ function createCommonBridgeOrderFields(opts: CreateOrderFromPathOpts): CommonBri
|
||||
senderAddress: NULL_ADDRESS,
|
||||
feeRecipientAddress: NULL_ADDRESS,
|
||||
salt: generatePseudoRandomSalt(),
|
||||
expirationTimeSeconds: new BigNumber(Math.floor(Date.now() / ONE_SECOND_MS) + ONE_HOUR_IN_SECONDS),
|
||||
// 2 hours from now
|
||||
expirationTimeSeconds: new BigNumber(Math.floor(Date.now() / ONE_SECOND_MS) + ONE_HOUR_IN_SECONDS * 2),
|
||||
makerFeeAssetData: NULL_BYTES,
|
||||
takerFeeAssetData: NULL_BYTES,
|
||||
makerFee: ZERO_AMOUNT,
|
||||
|
@ -19,8 +19,6 @@ export function findOptimalPath(
|
||||
runLimit?: number,
|
||||
): Fill[] | undefined {
|
||||
let optimalPath = paths[0] || [];
|
||||
// TODO(dorothy-zbornak): Convex paths (like kyber) should technically always be
|
||||
// inserted at the front of the path because a partial fill can invalidate them.
|
||||
for (const path of paths.slice(1)) {
|
||||
optimalPath = mixPaths(side, optimalPath, path, targetInput, runLimit);
|
||||
}
|
||||
|
@ -466,29 +466,6 @@ describe('MarketOperationUtils tests', () => {
|
||||
expect(orderSources.sort()).to.deep.eq(expectedSources.sort());
|
||||
});
|
||||
|
||||
it('Kyber is exclusive against Uniswap and Eth2Dai', async () => {
|
||||
const rates: RatesBySource = {};
|
||||
rates[ERC20BridgeSource.Native] = [0.3, 0.2, 0.1, 0.05];
|
||||
rates[ERC20BridgeSource.Uniswap] = [0.5, 0.05, 0.05, 0.05];
|
||||
rates[ERC20BridgeSource.Eth2Dai] = [0.6, 0.05, 0.05, 0.05];
|
||||
rates[ERC20BridgeSource.Kyber] = [0.4, 0.05, 0.05, 0.05];
|
||||
replaceSamplerOps({
|
||||
getSellQuotes: createGetMultipleSellQuotesOperationFromRates(rates),
|
||||
});
|
||||
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync(
|
||||
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
|
||||
FILL_AMOUNT,
|
||||
{ ...DEFAULT_OPTS, numSamples: 4 },
|
||||
);
|
||||
const orderSources = improvedOrders.map(o => o.fills[0].source);
|
||||
if (orderSources.includes(ERC20BridgeSource.Kyber)) {
|
||||
expect(orderSources).to.not.include(ERC20BridgeSource.Uniswap);
|
||||
expect(orderSources).to.not.include(ERC20BridgeSource.Eth2Dai);
|
||||
} else {
|
||||
expect(orderSources).to.not.include(ERC20BridgeSource.Kyber);
|
||||
}
|
||||
});
|
||||
|
||||
const ETH_TO_MAKER_RATE = 1.5;
|
||||
|
||||
it('factors in fees for native orders', async () => {
|
||||
@ -605,7 +582,7 @@ describe('MarketOperationUtils tests', () => {
|
||||
ERC20BridgeSource.Native,
|
||||
ERC20BridgeSource.Uniswap,
|
||||
];
|
||||
const secondSources = [ERC20BridgeSource.Eth2Dai];
|
||||
const secondSources = [ERC20BridgeSource.Eth2Dai, ERC20BridgeSource.Kyber];
|
||||
expect(orderSources.slice(0, firstSources.length).sort()).to.deep.eq(firstSources.sort());
|
||||
expect(orderSources.slice(firstSources.length).sort()).to.deep.eq(secondSources.sort());
|
||||
});
|
||||
@ -622,7 +599,7 @@ describe('MarketOperationUtils tests', () => {
|
||||
const improvedOrders = await marketOperationUtils.getMarketSellOrdersAsync(
|
||||
createOrdersFromSellRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
|
||||
FILL_AMOUNT,
|
||||
{ ...DEFAULT_OPTS, numSamples: 4, allowFallback: true, maxFallbackSlippage: 0.5 },
|
||||
{ ...DEFAULT_OPTS, numSamples: 4, allowFallback: true, maxFallbackSlippage: 0.25 },
|
||||
);
|
||||
const orderSources = improvedOrders.map(o => o.fills[0].source);
|
||||
const firstSources = [ERC20BridgeSource.Native, ERC20BridgeSource.Native, ERC20BridgeSource.Uniswap];
|
||||
@ -990,7 +967,7 @@ describe('MarketOperationUtils tests', () => {
|
||||
const improvedOrders = await marketOperationUtils.getMarketBuyOrdersAsync(
|
||||
createOrdersFromBuyRates(FILL_AMOUNT, rates[ERC20BridgeSource.Native]),
|
||||
FILL_AMOUNT,
|
||||
{ ...DEFAULT_OPTS, numSamples: 4, allowFallback: true, maxFallbackSlippage: 0.5 },
|
||||
{ ...DEFAULT_OPTS, numSamples: 4, allowFallback: true, maxFallbackSlippage: 0.25 },
|
||||
);
|
||||
const orderSources = improvedOrders.map(o => o.fills[0].source);
|
||||
const firstSources = [ERC20BridgeSource.Native, ERC20BridgeSource.Native, ERC20BridgeSource.Uniswap];
|
||||
|
@ -53,6 +53,10 @@
|
||||
{
|
||||
"note": "Redeploy ERC20BridgeSampler on kovan",
|
||||
"pr": 2570
|
||||
},
|
||||
{
|
||||
"note": "Redeploy ERC20BridgeSampler on Mainnet using `Kyber.searchBestRate`",
|
||||
"pr": 2575
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -20,7 +20,7 @@
|
||||
"devUtils": "0x74134cf88b21383713e096a5ecf59e297dc7f547",
|
||||
"erc20BridgeProxy": "0x8ed95d1746bf1e4dab58d8ed4724f1ef95b20db0",
|
||||
"uniswapBridge": "0x36691c4f426eb8f42f150ebde43069a31cb080ad",
|
||||
"erc20BridgeSampler": "0xabee9a41f928c3b3b799b239a0f524343c7260c5",
|
||||
"erc20BridgeSampler": "0xc0154b14cc60a6661171fdc817f759429a57e184",
|
||||
"kyberBridge": "0x1c29670f7a77f1052d30813a0a4f632c78a02610",
|
||||
"eth2DaiBridge": "0x991c745401d5b5e469b8c3e2cb02c748f08754f1",
|
||||
"chaiBridge": "0x77c31eba23043b9a72d13470f3a3a311344d7438",
|
||||
|
File diff suppressed because one or more lines are too long
@ -619,6 +619,37 @@ export class ERC20BridgeSamplerContract extends BaseContract {
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [
|
||||
{
|
||||
name: 'takerToken',
|
||||
type: 'address',
|
||||
},
|
||||
{
|
||||
name: 'makerToken',
|
||||
type: 'address',
|
||||
},
|
||||
{
|
||||
name: 'intermediateToken',
|
||||
type: 'address',
|
||||
},
|
||||
{
|
||||
name: 'takerTokenAmounts',
|
||||
type: 'uint256[]',
|
||||
},
|
||||
],
|
||||
name: 'sampleSellsFromEth2DaiHop',
|
||||
outputs: [
|
||||
{
|
||||
name: 'makerTokenAmounts',
|
||||
type: 'uint256[]',
|
||||
},
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function',
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [
|
||||
@ -1231,6 +1262,49 @@ export class ERC20BridgeSamplerContract extends BaseContract {
|
||||
},
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Sample sell quotes from Eth2Dai/Oasis using a hop to an intermediate token.
|
||||
* I.e WBTC/DAI via ETH or WBTC/ETH via DAI
|
||||
* @param takerToken Address of the taker token (what to sell).
|
||||
* @param makerToken Address of the maker token (what to buy).
|
||||
* @param intermediateToken Address of the token to hop to.
|
||||
* @param takerTokenAmounts Taker token sell amount for each sample.
|
||||
* @returns makerTokenAmounts Maker amounts bought at each taker token amount.
|
||||
*/
|
||||
public sampleSellsFromEth2DaiHop(
|
||||
takerToken: string,
|
||||
makerToken: string,
|
||||
intermediateToken: string,
|
||||
takerTokenAmounts: BigNumber[],
|
||||
): ContractFunctionObj<BigNumber[]> {
|
||||
const self = (this as any) as ERC20BridgeSamplerContract;
|
||||
assert.isString('takerToken', takerToken);
|
||||
assert.isString('makerToken', makerToken);
|
||||
assert.isString('intermediateToken', intermediateToken);
|
||||
assert.isArray('takerTokenAmounts', takerTokenAmounts);
|
||||
const functionSignature = 'sampleSellsFromEth2DaiHop(address,address,address,uint256[])';
|
||||
|
||||
return {
|
||||
async callAsync(callData: Partial<CallData> = {}, defaultBlock?: BlockParam): Promise<BigNumber[]> {
|
||||
BaseContract._assertCallParams(callData, defaultBlock);
|
||||
const rawCallResult = await self._performCallAsync(
|
||||
{ ...callData, data: this.getABIEncodedTransactionData() },
|
||||
defaultBlock,
|
||||
);
|
||||
const abiEncoder = self._lookupAbiEncoder(functionSignature);
|
||||
BaseContract._throwIfUnexpectedEmptyCallResult(rawCallResult, abiEncoder);
|
||||
return abiEncoder.strictDecodeReturnValue<BigNumber[]>(rawCallResult);
|
||||
},
|
||||
getABIEncodedTransactionData(): string {
|
||||
return self._strictEncodeArguments(functionSignature, [
|
||||
takerToken.toLowerCase(),
|
||||
makerToken.toLowerCase(),
|
||||
intermediateToken.toLowerCase(),
|
||||
takerTokenAmounts,
|
||||
]);
|
||||
},
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Sample sell quotes from Kyber.
|
||||
* @param takerToken Address of the taker token (what to sell).
|
||||
|
Loading…
x
Reference in New Issue
Block a user