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:
Jacob Evans 2020-05-07 07:56:03 +10:00 committed by GitHub
parent fe36cd86bb
commit fb0311e675
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 327 additions and 132 deletions

View File

@ -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
}
]
},

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
}
]
},

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -53,6 +53,10 @@
{
"note": "Redeploy ERC20BridgeSampler on kovan",
"pr": 2570
},
{
"note": "Redeploy ERC20BridgeSampler on Mainnet using `Kyber.searchBestRate`",
"pr": 2575
}
]
},

View File

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

View File

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