@0x/asset-swapper: Support more varied curves.

This commit is contained in:
Lawrence Forman 2020-07-14 16:32:36 -04:00 committed by Lawrence Forman
parent b6700af6a8
commit 0450e430f1
8 changed files with 168 additions and 96 deletions

View File

@ -1,6 +1,6 @@
[ [
{ {
"version": "4.6.1", "version": "4.7.0",
"changes": [ "changes": [
{ {
"note": "Allow an empty override for sampler overrides", "note": "Allow an empty override for sampler overrides",
@ -9,6 +9,10 @@
{ {
"note": "Potentially heavy CPU functions inside the optimizer now yield to the event loop. As such they are now async.", "note": "Potentially heavy CPU functions inside the optimizer now yield to the event loop. As such they are now async.",
"pr": 2637 "pr": 2637
},
{
"note": "Support more varied curves",
"pr": 2633
} }
] ]
}, },

View File

@ -72,6 +72,7 @@ export {
BalancerFillData, BalancerFillData,
CollapsedFill, CollapsedFill,
CurveFillData, CurveFillData,
CurveInfo,
ERC20BridgeSource, ERC20BridgeSource,
FeeSchedule, FeeSchedule,
FillData, FillData,
@ -80,6 +81,7 @@ export {
NativeFillData, NativeFillData,
OptimizedMarketOrder, OptimizedMarketOrder,
UniswapV2FillData, UniswapV2FillData,
CurveFunctionSelectors,
} from './utils/market_operation_utils/types'; } from './utils/market_operation_utils/types';
export { ProtocolFeeUtils } from './utils/protocol_fee_utils'; export { ProtocolFeeUtils } from './utils/protocol_fee_utils';
export { QuoteRequestor } from './utils/quote_requestor'; export { QuoteRequestor } from './utils/quote_requestor';

View File

@ -1,6 +1,6 @@
import { BigNumber } from '@0x/utils'; import { BigNumber } from '@0x/utils';
import { ERC20BridgeSource, FakeBuyOpts, GetMarketOrdersOpts } from './types'; import { CurveFunctionSelectors, CurveInfo, ERC20BridgeSource, GetMarketOrdersOpts } from './types';
// tslint:disable: custom-no-magic-numbers // tslint:disable: custom-no-magic-numbers
@ -42,11 +42,6 @@ export const DEFAULT_GET_MARKET_ORDERS_OPTS: GetMarketOrdersOpts = {
shouldBatchBridgeOrders: true, shouldBatchBridgeOrders: true,
}; };
export const DEFAULT_FAKE_BUY_OPTS: FakeBuyOpts = {
targetSlippageBps: new BigNumber(5),
maxIterations: new BigNumber(5),
};
/** /**
* Sources to poll for ETH fee price estimates. * Sources to poll for ETH fee price estimates.
*/ */
@ -60,34 +55,82 @@ export const FEE_QUOTE_SOURCES = [
/** /**
* Mainnet Curve configuration * Mainnet Curve configuration
*/ */
export const MAINNET_CURVE_CONTRACTS: { [curveAddress: string]: string[] } = { export const MAINNET_CURVE_INFOS: { [name: string]: CurveInfo } = {
'0xa2b47e3d5c44877cca798226b7b8118f9bfb7a56': [ // Busted?
'0x6b175474e89094c44da98b954eedeac495271d0f', // DAI // DaiUsdc: {
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // USDC // exchangeFunctionSelector: CurveFunctionSelectors.exchange_underlying,
// sellQuoteFunctionSelector: CurveFunctionSelectors.get_dy_underlying,
// buyQuoteFunctionSelector: CurveFunctionSelectors.get_dx_underlying,
// poolAddress: '0xa2b47e3d5c44877cca798226b7b8118f9bfb7a56',
// tokens: ['0x6b175474e89094c44da98b954eedeac495271d0f', '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'],
// },
// Busted?
// DaiUsdcUsdt: {
// exchangeFunctionSelector: CurveFunctionSelectors.exchange_underlying,
// sellQuoteFunctionSelector: CurveFunctionSelectors.get_dy_underlying,
// buyQuoteFunctionSelector: CurveFunctionSelectors.get_dx_underlying,
// poolAddress: '0x52ea46506b9cc5ef470c5bf89f17dc28bb35d85c',
// tokens: [
// '0x6b175474e89094c44da98b954eedeac495271d0f',
// '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
// '0xdac17f958d2ee523a2206206994597c13d831ec7',
// ],
// },
DaiUsdcUsdtTusd: {
exchangeFunctionSelector: CurveFunctionSelectors.exchange_underlying,
sellQuoteFunctionSelector: CurveFunctionSelectors.get_dy_underlying,
buyQuoteFunctionSelector: CurveFunctionSelectors.get_dx_underlying,
poolAddress: '0x45f783cce6b7ff23b2ab2d70e416cdb7d6055f51',
tokens: [
'0x6b175474e89094c44da98b954eedeac495271d0f',
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
'0xdac17f958d2ee523a2206206994597c13d831ec7',
'0x0000000000085d4780b73119b644ae5ecd22b376',
], ],
'0x52ea46506b9cc5ef470c5bf89f17dc28bb35d85c': [ },
'0x6b175474e89094c44da98b954eedeac495271d0f', // DAI // Looks like it's dying.
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // USDC DaiUsdcUsdtBusd: {
'0xdac17f958d2ee523a2206206994597c13d831ec7', // USDT exchangeFunctionSelector: CurveFunctionSelectors.exchange_underlying,
sellQuoteFunctionSelector: CurveFunctionSelectors.get_dy_underlying,
buyQuoteFunctionSelector: CurveFunctionSelectors.get_dx_underlying,
poolAddress: '0x79a8c46dea5ada233abaffd40f3a0a2b1e5a4f27',
tokens: [
'0x6b175474e89094c44da98b954eedeac495271d0f',
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
'0xdac17f958d2ee523a2206206994597c13d831ec7',
'0x4fabb145d64652a948d72533023f6e7a623c7c53',
], ],
'0x45f783cce6b7ff23b2ab2d70e416cdb7d6055f51': [ },
'0x6b175474e89094c44da98b954eedeac495271d0f', // DAI DaiUsdcUsdtSusd: {
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // USDC exchangeFunctionSelector: CurveFunctionSelectors.exchange_underlying,
'0xdac17f958d2ee523a2206206994597c13d831ec7', // USDT sellQuoteFunctionSelector: CurveFunctionSelectors.get_dy_underlying,
'0x0000000000085d4780b73119b644ae5ecd22b376', // TUSD buyQuoteFunctionSelector: CurveFunctionSelectors.get_dx_underlying,
poolAddress: '0xa5407eae9ba41422680e2e00537571bcc53efbfd',
tokens: [
'0x6b175474e89094c44da98b954eedeac495271d0f',
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
'0xdac17f958d2ee523a2206206994597c13d831ec7',
'0x57ab1ec28d129707052df4df418d58a2d46d5f51',
], ],
'0x79a8c46dea5ada233abaffd40f3a0a2b1e5a4f27': [ },
'0x6b175474e89094c44da98b954eedeac495271d0f', // DAI RenbtcWbtc: {
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // USDC exchangeFunctionSelector: CurveFunctionSelectors.exchange,
'0xdac17f958d2ee523a2206206994597c13d831ec7', // USDT sellQuoteFunctionSelector: CurveFunctionSelectors.get_dy,
'0x4fabb145d64652a948d72533023f6e7a623c7c53', // BUSD buyQuoteFunctionSelector: CurveFunctionSelectors.None,
], poolAddress: '0x93054188d876f558f4a66b2ef1d97d16edf0895b',
'0xa5407eae9ba41422680e2e00537571bcc53efbfd': [ tokens: ['0xeb4c2781e4eba804ce9a9803c67d0893436bb27d', '0x2260fac5e5542a773aa44fbcfedf7c193bc2c599'],
'0x6b175474e89094c44da98b954eedeac495271d0f', // DAI },
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // USDC RenbtcWbtcSbtc: {
'0xdac17f958d2ee523a2206206994597c13d831ec7', // USDT exchangeFunctionSelector: CurveFunctionSelectors.exchange,
'0x57ab1ec28d129707052df4df418d58a2d46d5f51', // SUSD sellQuoteFunctionSelector: CurveFunctionSelectors.get_dy,
buyQuoteFunctionSelector: CurveFunctionSelectors.None,
poolAddress: '0x7fc77b5c7614e1533320ea6ddc2eb61fa00a9714',
tokens: [
'0xeb4c2781e4eba804ce9a9803c67d0893436bb27d',
'0x2260fac5e5542a773aa44fbcfedf7c193bc2c599',
'0xfe18be6b3bd88a2d2a7f928d00292e7a9963cfc6',
], ],
},
}; };
export const ERC20_PROXY_ID = '0xf47261b0'; export const ERC20_PROXY_ID = '0xf47261b0';

View File

@ -1,8 +1,7 @@
import { MAINNET_CURVE_CONTRACTS } from './constants'; import { MAINNET_CURVE_INFOS } from './constants';
import { CurveInfo } from './types';
// tslint:disable completed-docs // tslint:disable completed-docs
export function getCurveAddressesForPair(takerToken: string, makerToken: string): string[] { export function getCurveInfosForPair(takerToken: string, makerToken: string): CurveInfo[] {
return Object.keys(MAINNET_CURVE_CONTRACTS).filter(a => return Object.values(MAINNET_CURVE_INFOS).filter(c => [makerToken, takerToken].every(t => c.tokens.includes(t)));
[makerToken, takerToken].every(t => MAINNET_CURVE_CONTRACTS[a].includes(t)),
);
} }

View File

@ -219,10 +219,11 @@ function createBridgeOrder(fill: CollapsedFill, opts: CreateOrderFromPathOpts):
makerToken, makerToken,
bridgeAddress, bridgeAddress,
createCurveBridgeData( createCurveBridgeData(
curveFillData.poolAddress, curveFillData.curve.poolAddress,
curveFillData.curve.exchangeFunctionSelector,
takerToken,
curveFillData.fromTokenIdx, curveFillData.fromTokenIdx,
curveFillData.toTokenIdx, curveFillData.toTokenIdx,
1, // "version"
), ),
); );
break; break;
@ -341,17 +342,25 @@ function createBalancerBridgeData(takerToken: string, poolAddress: string): stri
function createCurveBridgeData( function createCurveBridgeData(
curveAddress: string, curveAddress: string,
exchangeFunctionSelector: string,
takerToken: string,
fromTokenIdx: number, fromTokenIdx: number,
toTokenIdx: number, toTokenIdx: number,
version: number,
): string { ): string {
const curveBridgeDataEncoder = AbiEncoder.create([ const curveBridgeDataEncoder = AbiEncoder.create([
{ name: 'curveAddress', type: 'address' }, { name: 'curveAddress', type: 'address' },
{ name: 'exchangeFunctionSelector', type: 'bytes4' },
{ name: 'fromTokenAddress', type: 'address' },
{ name: 'fromTokenIdx', type: 'int128' }, { name: 'fromTokenIdx', type: 'int128' },
{ name: 'toTokenIdx', type: 'int128' }, { name: 'toTokenIdx', type: 'int128' },
{ name: 'version', type: 'int128' },
]); ]);
return curveBridgeDataEncoder.encode([curveAddress, fromTokenIdx, toTokenIdx, version]); return curveBridgeDataEncoder.encode([
curveAddress,
exchangeFunctionSelector,
takerToken,
fromTokenIdx,
toTokenIdx,
]);
} }
function createUniswapV2BridgeData(tokenAddressPath: string[]): string { function createUniswapV2BridgeData(tokenAddressPath: string[]): string {

View File

@ -3,15 +3,14 @@ import * as _ from 'lodash';
import { BigNumber, ERC20BridgeSource, SignedOrder } from '../..'; import { BigNumber, ERC20BridgeSource, SignedOrder } from '../..';
import { BalancerPool, BalancerPoolsCache, computeBalancerBuyQuote, computeBalancerSellQuote } from './balancer_utils'; import { BalancerPool, BalancerPoolsCache, computeBalancerBuyQuote, computeBalancerSellQuote } from './balancer_utils';
import { DEFAULT_FAKE_BUY_OPTS, MAINNET_CURVE_CONTRACTS } from './constants'; import { getCurveInfosForPair } from './curve_utils';
import { getCurveAddressesForPair } from './curve_utils';
import { getMultiBridgeIntermediateToken } from './multibridge_utils'; import { getMultiBridgeIntermediateToken } from './multibridge_utils';
import { import {
BalancerFillData, BalancerFillData,
BatchedOperation, BatchedOperation,
CurveFillData, CurveFillData,
CurveInfo,
DexSample, DexSample,
FakeBuyOpts,
SourceQuoteOperation, SourceQuoteOperation,
UniswapV2FillData, UniswapV2FillData,
} from './types'; } from './types';
@ -58,17 +57,12 @@ export const samplerOperations = {
}, },
}; };
}, },
getKyberBuyQuotes( getKyberBuyQuotes(makerToken: string, takerToken: string, makerFillAmounts: BigNumber[]): SourceQuoteOperation {
makerToken: string,
takerToken: string,
makerFillAmounts: BigNumber[],
fakeBuyOpts: FakeBuyOpts = DEFAULT_FAKE_BUY_OPTS,
): SourceQuoteOperation {
return { return {
source: ERC20BridgeSource.Kyber, source: ERC20BridgeSource.Kyber,
encodeCall: contract => { encodeCall: contract => {
return contract return contract
.sampleBuysFromKyberNetwork(takerToken, makerToken, makerFillAmounts, fakeBuyOpts) .sampleBuysFromKyberNetwork(takerToken, makerToken, makerFillAmounts)
.getABIEncodedTransactionData(); .getABIEncodedTransactionData();
}, },
handleCallResultsAsync: async (contract, callResults) => { handleCallResultsAsync: async (contract, callResults) => {
@ -162,19 +156,12 @@ export const samplerOperations = {
makerToken: string, makerToken: string,
takerToken: string, takerToken: string,
makerFillAmounts: BigNumber[], makerFillAmounts: BigNumber[],
fakeBuyOpts: FakeBuyOpts = DEFAULT_FAKE_BUY_OPTS,
): SourceQuoteOperation { ): SourceQuoteOperation {
return { return {
source: ERC20BridgeSource.LiquidityProvider, source: ERC20BridgeSource.LiquidityProvider,
encodeCall: contract => { encodeCall: contract => {
return contract return contract
.sampleBuysFromLiquidityProviderRegistry( .sampleBuysFromLiquidityProviderRegistry(registryAddress, takerToken, makerToken, makerFillAmounts)
registryAddress,
takerToken,
makerToken,
makerFillAmounts,
fakeBuyOpts,
)
.getABIEncodedTransactionData(); .getABIEncodedTransactionData();
}, },
handleCallResultsAsync: async (contract, callResults) => { handleCallResultsAsync: async (contract, callResults) => {
@ -237,7 +224,7 @@ export const samplerOperations = {
}; };
}, },
getCurveSellQuotes( getCurveSellQuotes(
curveAddress: string, curve: CurveInfo,
fromTokenIdx: number, fromTokenIdx: number,
toTokenIdx: number, toTokenIdx: number,
takerFillAmounts: BigNumber[], takerFillAmounts: BigNumber[],
@ -245,14 +232,18 @@ export const samplerOperations = {
return { return {
source: ERC20BridgeSource.Curve, source: ERC20BridgeSource.Curve,
fillData: { fillData: {
poolAddress: curveAddress, curve,
fromTokenIdx, fromTokenIdx,
toTokenIdx, toTokenIdx,
}, },
encodeCall: contract => { encodeCall: contract => {
return contract return contract
.sampleSellsFromCurve( .sampleSellsFromCurve(
curveAddress, {
poolAddress: curve.poolAddress,
sellQuoteFunctionSelector: curve.sellQuoteFunctionSelector,
buyQuoteFunctionSelector: curve.buyQuoteFunctionSelector,
},
new BigNumber(fromTokenIdx), new BigNumber(fromTokenIdx),
new BigNumber(toTokenIdx), new BigNumber(toTokenIdx),
takerFillAmounts, takerFillAmounts,
@ -265,7 +256,7 @@ export const samplerOperations = {
}; };
}, },
getCurveBuyQuotes( getCurveBuyQuotes(
curveAddress: string, curve: CurveInfo,
fromTokenIdx: number, fromTokenIdx: number,
toTokenIdx: number, toTokenIdx: number,
makerFillAmounts: BigNumber[], makerFillAmounts: BigNumber[],
@ -273,14 +264,18 @@ export const samplerOperations = {
return { return {
source: ERC20BridgeSource.Curve, source: ERC20BridgeSource.Curve,
fillData: { fillData: {
poolAddress: curveAddress, curve,
fromTokenIdx, fromTokenIdx,
toTokenIdx, toTokenIdx,
}, },
encodeCall: contract => { encodeCall: contract => {
return contract return contract
.sampleBuysFromCurve( .sampleBuysFromCurve(
curveAddress, {
poolAddress: curve.poolAddress,
sellQuoteFunctionSelector: curve.sellQuoteFunctionSelector,
buyQuoteFunctionSelector: curve.buyQuoteFunctionSelector,
},
new BigNumber(fromTokenIdx), new BigNumber(fromTokenIdx),
new BigNumber(toTokenIdx), new BigNumber(toTokenIdx),
makerFillAmounts, makerFillAmounts,
@ -416,11 +411,11 @@ export const samplerOperations = {
case ERC20BridgeSource.Kyber: case ERC20BridgeSource.Kyber:
return samplerOperations.getKyberSellQuotes(makerToken, takerToken, takerFillAmounts); return samplerOperations.getKyberSellQuotes(makerToken, takerToken, takerFillAmounts);
case ERC20BridgeSource.Curve: case ERC20BridgeSource.Curve:
return getCurveAddressesForPair(takerToken, makerToken).map(curveAddress => return getCurveInfosForPair(takerToken, makerToken).map(curve =>
samplerOperations.getCurveSellQuotes( samplerOperations.getCurveSellQuotes(
curveAddress, curve,
MAINNET_CURVE_CONTRACTS[curveAddress].indexOf(takerToken), curve.tokens.indexOf(takerToken),
MAINNET_CURVE_CONTRACTS[curveAddress].indexOf(makerToken), curve.tokens.indexOf(makerToken),
takerFillAmounts, takerFillAmounts,
), ),
); );
@ -502,7 +497,6 @@ export const samplerOperations = {
wethAddress: string, wethAddress: string,
balancerPoolsCache?: BalancerPoolsCache, balancerPoolsCache?: BalancerPoolsCache,
liquidityProviderRegistryAddress?: string, liquidityProviderRegistryAddress?: string,
fakeBuyOpts: FakeBuyOpts = DEFAULT_FAKE_BUY_OPTS,
): Promise<BatchedOperation<DexSample[][]>> => { ): Promise<BatchedOperation<DexSample[][]>> => {
const subOps = _.flatten( const subOps = _.flatten(
await Promise.all( await Promise.all(
@ -527,18 +521,13 @@ export const samplerOperations = {
} }
return ops; return ops;
case ERC20BridgeSource.Kyber: case ERC20BridgeSource.Kyber:
return samplerOperations.getKyberBuyQuotes( return samplerOperations.getKyberBuyQuotes(makerToken, takerToken, makerFillAmounts);
makerToken,
takerToken,
makerFillAmounts,
fakeBuyOpts,
);
case ERC20BridgeSource.Curve: case ERC20BridgeSource.Curve:
return getCurveAddressesForPair(takerToken, makerToken).map(curveAddress => return getCurveInfosForPair(takerToken, makerToken).map(curve =>
samplerOperations.getCurveBuyQuotes( samplerOperations.getCurveBuyQuotes(
curveAddress, curve,
MAINNET_CURVE_CONTRACTS[curveAddress].indexOf(takerToken), curve.tokens.indexOf(takerToken),
MAINNET_CURVE_CONTRACTS[curveAddress].indexOf(makerToken), curve.tokens.indexOf(makerToken),
makerFillAmounts, makerFillAmounts,
), ),
); );
@ -553,7 +542,6 @@ export const samplerOperations = {
makerToken, makerToken,
takerToken, takerToken,
makerFillAmounts, makerFillAmounts,
fakeBuyOpts,
); );
case ERC20BridgeSource.Balancer: case ERC20BridgeSource.Balancer:
if (balancerPoolsCache === undefined) { if (balancerPoolsCache === undefined) {

View File

@ -37,6 +37,32 @@ export enum ERC20BridgeSource {
Balancer = 'Balancer', Balancer = 'Balancer',
} }
// tslint:disable: enum-naming
/**
* Curve contract function selectors.
*/
export enum CurveFunctionSelectors {
None = '0x00000000',
exchange = '0x3df02124',
exchange_underlying = '0xa6417ed6',
get_dy_underlying = '0x07211ef7',
get_dx_underlying = '0x0e71d1b9',
get_dy = '0x5e0d443f',
get_dx = '0x67df02ca',
}
// tslint:enable: enum-naming
/**
* Configuration info on a Curve pool.
*/
export interface CurveInfo {
exchangeFunctionSelector: CurveFunctionSelectors;
sellQuoteFunctionSelector: CurveFunctionSelectors;
buyQuoteFunctionSelector: CurveFunctionSelectors;
poolAddress: string;
tokens: string[];
}
// Internal `fillData` field for `Fill` objects. // Internal `fillData` field for `Fill` objects.
export interface FillData {} export interface FillData {}
@ -46,9 +72,9 @@ export interface NativeFillData extends FillData {
} }
export interface CurveFillData extends FillData { export interface CurveFillData extends FillData {
poolAddress: string;
fromTokenIdx: number; fromTokenIdx: number;
toTokenIdx: number; toTokenIdx: number;
curve: CurveInfo;
} }
export interface BalancerFillData extends FillData { export interface BalancerFillData extends FillData {
@ -228,12 +254,3 @@ export interface SourceQuoteOperation<TFillData extends FillData = FillData> ext
source: ERC20BridgeSource; source: ERC20BridgeSource;
fillData?: TFillData; fillData?: TFillData;
} }
/**
* Used in the ERC20BridgeSampler when a source does not natively
* support sampling via a specific buy amount.
*/
export interface FakeBuyOpts {
targetSlippageBps: BigNumber;
maxIterations: BigNumber;
}

View File

@ -302,7 +302,17 @@ describe('MarketOperationUtils tests', () => {
const DEFAULT_FILL_DATA: FillDataBySource = { const DEFAULT_FILL_DATA: FillDataBySource = {
[ERC20BridgeSource.UniswapV2]: { tokenAddressPath: [] }, [ERC20BridgeSource.UniswapV2]: { tokenAddressPath: [] },
[ERC20BridgeSource.Balancer]: { poolAddress: randomAddress() }, [ERC20BridgeSource.Balancer]: { poolAddress: randomAddress() },
[ERC20BridgeSource.Curve]: { poolAddress: randomAddress(), fromTokenIdx: 0, toTokenIdx: 1 }, [ERC20BridgeSource.Curve]: {
curve: {
poolAddress: randomAddress(),
tokens: [TAKER_TOKEN, MAKER_TOKEN],
exchangeFunctionSelector: hexUtils.random(4),
sellQuoteFunctionSelector: hexUtils.random(4),
buyQuoteFunctionSelector: hexUtils.random(4),
},
fromTokenIdx: 0,
toTokenIdx: 1,
},
}; };
const DEFAULT_OPS = { const DEFAULT_OPS = {