Compare commits

...

13 Commits

Author SHA1 Message Date
Github Actions
2fdca24d4e Publish
- @0x/asset-swapper@16.46.0
2022-01-11 01:10:02 +00:00
Github Actions
42ec0b144e Updated CHANGELOGS & MD docs 2022-01-11 01:09:58 +00:00
Jacob Evans
3f6ce78b46 chore: Enable Curve ETH/CVX (#394)
* chore: Enable Curve ETH/CVX

* pr number
2022-01-11 09:32:07 +10:00
Github Actions
c1300c1068 Publish
- @0x/asset-swapper@16.45.2
2022-01-10 15:09:26 +00:00
Github Actions
9a641cfab6 Updated CHANGELOGS & MD docs 2022-01-10 15:09:23 +00:00
Kim Persson
60345d4465 fix: don't create fills for 0 output samples and negative adjusted rate orders (#387)
* fix: don't try to create fills for 0 output samples

* fix: negative adjusted output native orders causing undefined fills

* fix: make sure to use the same sourcePathId for fills from same source

* fix: should be same sourcePathId within the same DexSample[]

* fix: split native orders into 13 samples to align with interpolation

* chore: add changelog entry for asset-swapper
2022-01-10 14:55:03 +01:00
Github Actions
11dfea47a6 Publish
- @0x/asset-swapper@16.45.1
2022-01-05 05:08:43 +00:00
Github Actions
55e9dd39a2 Updated CHANGELOGS & MD docs 2022-01-05 05:08:41 +00:00
Jacob Evans
1993929bed chore: Celo Update certain tokens since Optics v2 (#390)
* chore: Celo Update certain tokens since Optics v2

* Changelog
2022-01-05 14:44:29 +10:00
Oskar Paolini
e1d81de517 fixes axios object dumping in logs (#345) 2022-01-05 09:46:01 +10:00
Github Actions
a6b3a21635 Publish
- @0x/asset-swapper@16.45.0
2022-01-04 15:00:16 +00:00
Github Actions
fd59cdc2db Updated CHANGELOGS & MD docs 2022-01-04 15:00:13 +00:00
Jacob Evans
98e11b5189 feat: Capture Routing timing metrics (#388) 2022-01-04 15:42:14 +01:00
8 changed files with 212 additions and 65 deletions

View File

@@ -1,4 +1,44 @@
[
{
"version": "16.46.0",
"changes": [
{
"note": "Enable `Curve` ETH/CVX pool",
"pr": 394
}
],
"timestamp": 1641863395
},
{
"version": "16.45.2",
"changes": [
{
"note": "Handle 0 output samples and negative adjusted rate native orders in routing",
"pr": 387
}
],
"timestamp": 1641827361
},
{
"version": "16.45.1",
"changes": [
{
"note": "Update `Celo` intermediate tokens",
"pr": 390
}
],
"timestamp": 1641359319
},
{
"version": "16.45.0",
"changes": [
{
"note": "Capture router timings",
"pr": 388
}
],
"timestamp": 1641308410
},
{
"version": "16.44.0",
"changes": [

View File

@@ -5,6 +5,22 @@ Edit the package's CHANGELOG.json file only.
CHANGELOG
## v16.46.0 - _January 11, 2022_
* Enable `Curve` ETH/CVX pool (#394)
## v16.45.2 - _January 10, 2022_
* Handle 0 output samples and negative adjusted rate native orders in routing (#387)
## v16.45.1 - _January 5, 2022_
* Update `Celo` intermediate tokens (#390)
## v16.45.0 - _January 4, 2022_
* Capture router timings (#388)
## v16.44.0 - _December 29, 2021_
* Update neon-router and use router estimated output amount (#354)

View File

@@ -1,6 +1,6 @@
{
"name": "@0x/asset-swapper",
"version": "16.44.0",
"version": "16.46.0",
"engines": {
"node": ">=6.12"
},

View File

@@ -223,7 +223,17 @@ export async function returnQuoteFromAltMMAsync<ResponseT>(
cancelToken,
})
.catch(err => {
warningLogger(err, `Alt RFQ MM request failed`);
if (err.response) {
// request was made and market maker responded
warningLogger(
{ data: err.response.data, status: err.response.status, headers: err.response.headers },
`Alt RFQ MM request failed`,
);
} else if (err.request) {
warningLogger({}, 'Alt RFQ MM no response received');
} else {
warningLogger({ err: err.message }, 'Failed to construct Alt RFQ MM request');
}
throw new Error(`Alt RFQ MM request failed`);
});

View File

@@ -462,6 +462,7 @@ export const MAINNET_TOKENS = {
CRV: '0xd533a949740bb3306d119cc777fa900ba034cd52',
MIM: '0x99d8a9c45b2eca8864373a26d1459e3dff1e17f3',
EURT: '0xc581b735a1688071a1746c968e0798d642ede491',
CVX: '0x4e3fbd56cd56c3e72c1403e103b45db9da5b9d2b',
};
export const BSC_TOKENS = {
@@ -510,9 +511,23 @@ export const AVALANCHE_TOKENS = {
};
export const CELO_TOKENS = {
WETH: '0xe919f65739c26a42616b7b8eedc6b5524d1e3ac4',
WCELO: '0x471ece3750da237f93b8e339c536989b8978a438',
// Some of these tokens are Optics bridge? tokens which
// had an issue and migrated from v1 to v2
WETHv1: '0xe919f65739c26a42616b7b8eedc6b5524d1e3ac4',
WETH: '0x122013fd7df1c6f636a5bb8f03108e876548b455',
WBTC: '0xbaab46e28388d2779e6e31fd00cf0e5ad95e327b',
cUSD: '0x765de816845861e75a25fca122bb6898b8b1282a',
// ??
WBTCv1: '0xd629eb00deced2a080b7ec630ef6ac117e614f1b',
cETH: '0x2def4285787d58a2f811af24755a8150622f4361',
UBE: '0x00be915b9dcf56a3cbe739d9b9c202ca692409ec',
// Moolah
mCELO: '0x7d00cd74ff385c955ea3d79e47bf06bd7386387d',
mCUSD: '0x918146359264c492bd6934071c6bd31c854edbc3',
mCEUR: '0xe273ad7ee11dcfaa87383ad5977ee1504ac07568',
amCUSD: '0x64defa3544c695db8c535d289d843a189aa26b98',
MOO: '0x17700282592d6917f6a73d0bf8accf4d578c131e',
};
export const FANTOM_TOKENS = {
@@ -578,6 +593,7 @@ export const CURVE_POOLS = {
mim: '0x5a6a4d54456819380173272a5e8e9b9904bdf41b',
eurt: '0xfd5db7463a3ab53fd211b4af195c5bccc1a03890',
ethcrv: '0x8301ae4fc9c624d1d396cbdaa1ed877821d7c511',
ethcvx: '0xb576491f1e6e5e62f1d8f26062ee822b40b0e0d4',
};
export const CURVE_V2_POOLS = {
@@ -713,7 +729,7 @@ export const DEFAULT_INTERMEDIATE_TOKENS_BY_CHAIN_ID = valueByChainId<string[]>(
AVALANCHE_TOKENS.USDC,
],
[ChainId.Fantom]: [FANTOM_TOKENS.WFTM, FANTOM_TOKENS.WETH, FANTOM_TOKENS.DAI, FANTOM_TOKENS.USDC],
[ChainId.Celo]: [CELO_TOKENS.mCUSD, CELO_TOKENS.WETH, CELO_TOKENS.WCELO],
[ChainId.Celo]: [CELO_TOKENS.WCELO, CELO_TOKENS.mCUSD, CELO_TOKENS.WETH, CELO_TOKENS.amCUSD, CELO_TOKENS.WBTC],
[ChainId.Optimism]: [OPTIMISM_TOKENS.WETH, OPTIMISM_TOKENS.DAI, OPTIMISM_TOKENS.USDC],
},
[],
@@ -1059,6 +1075,17 @@ export const CURVE_MAINNET_INFOS: { [name: string]: CurveInfo } = {
sellQuoteFunctionSelector: CurveFunctionSelectors.get_dy_uint256,
exchangeFunctionSelector: CurveFunctionSelectors.exchange_underlying_uint256,
},
[CURVE_POOLS.ethcvx]: {
...createCurveExchangePool({
// This pool uses ETH
tokens: [MAINNET_TOKENS.WETH, MAINNET_TOKENS.CVX],
pool: CURVE_POOLS.ethcvx,
gasSchedule: 350e3,
}),
// This pool has a custom get_dy and exchange selector with uint256
sellQuoteFunctionSelector: CurveFunctionSelectors.get_dy_uint256,
exchangeFunctionSelector: CurveFunctionSelectors.exchange_underlying_uint256,
},
};
export const CURVE_V2_MAINNET_INFOS: { [name: string]: CurveInfo } = {

View File

@@ -533,9 +533,17 @@ export class MarketOperationUtils {
opts.feeSchedule,
this._sampler.chainId,
opts.neonRouterNumSamples,
opts.samplerMetrics,
);
} else {
optimalPath = await findOptimalPathJSAsync(side, fills, inputAmount, opts.runLimit, penaltyOpts);
optimalPath = await findOptimalPathJSAsync(
side,
fills,
inputAmount,
opts.runLimit,
opts.samplerMetrics,
penaltyOpts,
);
}
const optimalPathRate = optimalPath ? optimalPath.adjustedRate() : ZERO_AMOUNT;
@@ -599,6 +607,7 @@ export class MarketOperationUtils {
exchangeProxyOverhead: _opts.exchangeProxyOverhead,
gasPrice: _opts.gasPrice,
neonRouterNumSamples: _opts.neonRouterNumSamples,
samplerMetrics: _opts.samplerMetrics,
};
if (nativeOrders.length === 0) {
@@ -810,6 +819,7 @@ export class MarketOperationUtils {
opts.feeSchedule,
this._sampler.chainId,
opts.neonRouterNumSamples,
undefined, // hack: set sampler metrics to undefined to avoid fallback timings
);
} else {
const sturdyFills = fills.filter(p => p.length > 0 && !fragileSources.includes(p[0].source));
@@ -818,6 +828,7 @@ export class MarketOperationUtils {
sturdyFills,
inputAmount,
opts.runLimit,
undefined, // hack: set sampler metrics to undefined to avoid fallback timings
sturdyPenaltyOpts,
);
}

View File

@@ -1,18 +1,17 @@
import { assert } from '@0x/assert';
import { ChainId } from '@0x/contract-addresses';
import { OptimizerCapture, route, SerializedPath } from '@0x/neon-router';
import { BigNumber } from '@0x/utils';
import { BigNumber, hexUtils } from '@0x/utils';
import * as _ from 'lodash';
import { performance } from 'perf_hooks';
import { DEFAULT_INFO_LOGGER } from '../../constants';
import { MarketOperation, NativeOrderWithFillableAmounts } from '../../types';
import { VIP_ERC20_BRIDGE_SOURCES_BY_CHAIN_ID } from '../market_operation_utils/constants';
import { dexSamplesToFills, ethToOutputAmount, nativeOrdersToFills } from './fills';
import { DEFAULT_PATH_PENALTY_OPTS, Path, PathPenaltyOpts } from './path';
import { getRate } from './rate_utils';
import { DexSample, ERC20BridgeSource, FeeSchedule, Fill, FillData } from './types';
import { DexSample, ERC20BridgeSource, FeeSchedule, Fill, FillData, SamplerMetrics } from './types';
// tslint:disable: prefer-for-of custom-no-magic-numbers completed-docs no-bitwise
@@ -77,16 +76,25 @@ function findRoutesAndCreateOptimalPath(
fees: FeeSchedule,
neonRouterNumSamples: number,
): Path | undefined {
const createFill = (sample: DexSample) =>
dexSamplesToFills(side, [sample], opts.outputAmountPerEth, opts.inputAmountPerEth, fees)[0];
const createFill = (sample: DexSample): Fill | undefined => {
const fills = dexSamplesToFills(side, [sample], opts.outputAmountPerEth, opts.inputAmountPerEth, fees);
// NOTE: If the sample has 0 output dexSamplesToFills will return [] because no fill can be created
if (fills.length === 0) {
return undefined;
}
return fills[0];
};
const samplesAndNativeOrdersWithResults: Array<DexSample[] | NativeOrderWithFillableAmounts[]> = [];
const serializedPaths: SerializedPath[] = [];
const sampleSourcePathIds: string[] = [];
for (const singleSourceSamples of samples) {
if (singleSourceSamples.length === 0) {
continue;
}
const sourcePathId = hexUtils.random();
const singleSourceSamplesWithOutput = [...singleSourceSamples];
for (let i = singleSourceSamples.length - 1; i >= 0; i--) {
if (singleSourceSamples[i].output.isZero()) {
@@ -125,8 +133,10 @@ function findRoutesAndCreateOptimalPath(
samplesAndNativeOrdersWithResults.push(singleSourceSamplesWithOutput);
serializedPaths.push(serializedPath);
sampleSourcePathIds.push(sourcePathId);
}
const nativeOrdersourcePathId = hexUtils.random();
for (const [idx, nativeOrder] of nativeOrders.entries()) {
const { input: normalizedOrderInput, output: normalizedOrderOutput } = nativeOrderToNormalizedAmounts(
side,
@@ -137,32 +147,25 @@ function findRoutesAndCreateOptimalPath(
if (normalizedOrderInput.isLessThanOrEqualTo(0) || normalizedOrderOutput.isLessThanOrEqualTo(0)) {
continue;
}
// HACK: the router requires at minimum 3 samples as a basis for interpolation
const inputs = [
0,
normalizedOrderInput
.dividedBy(2)
.integerValue()
.toNumber(),
normalizedOrderInput.integerValue().toNumber(),
];
const outputs = [
0,
normalizedOrderOutput
.dividedBy(2)
.integerValue()
.toNumber(),
normalizedOrderOutput.integerValue().toNumber(),
];
// NOTE: same fee no matter if full or partial fill
const fee = calculateOuputFee(side, nativeOrder, opts.outputAmountPerEth, opts.inputAmountPerEth, fees)
.integerValue()
.toNumber();
const outputFees = [fee, fee, fee];
// NOTE: ids can be the same for all fake samples
const id = `${ERC20BridgeSource.Native}-${serializedPaths.length}-${idx}`;
const ids = [id, id, id];
// HACK: due to an issue with the Rust router interpolation we need to create exactly 13 samples from the native order
const ids = [];
const inputs = [];
const outputs = [];
const outputFees = [];
for (let i = 1; i <= 13; i++) {
const fraction = i / 13;
const currentInput = BigNumber.min(normalizedOrderInput.times(fraction), normalizedOrderInput);
const currentOutput = BigNumber.min(normalizedOrderOutput.times(fraction), normalizedOrderOutput);
const id = `${ERC20BridgeSource.Native}-${serializedPaths.length}-${idx}-${i}`;
inputs.push(currentInput.integerValue().toNumber());
outputs.push(currentOutput.integerValue().toNumber());
outputFees.push(fee);
ids.push(id);
}
const serializedPath: SerializedPath = {
ids,
@@ -173,6 +176,7 @@ function findRoutesAndCreateOptimalPath(
samplesAndNativeOrdersWithResults.push([nativeOrder]);
serializedPaths.push(serializedPath);
sampleSourcePathIds.push(nativeOrdersourcePathId);
}
if (serializedPaths.length === 0) {
@@ -185,16 +189,9 @@ function findRoutesAndCreateOptimalPath(
pathsIn: serializedPaths,
};
const before = performance.now();
const allSourcesRustRoute = new Float64Array(rustArgs.pathsIn.length);
const strategySourcesOutputAmounts = new Float64Array(rustArgs.pathsIn.length);
route(rustArgs, allSourcesRustRoute, strategySourcesOutputAmounts, neonRouterNumSamples);
DEFAULT_INFO_LOGGER(
{ router: 'neon-router', performanceMs: performance.now() - before, type: 'real' },
'Rust router real routing performance',
);
assert.assert(
rustArgs.pathsIn.length === allSourcesRustRoute.length,
'different number of sources in the Router output than the input',
@@ -208,12 +205,13 @@ function findRoutesAndCreateOptimalPath(
allSourcesRustRoute,
samplesAndNativeOrdersWithResults,
strategySourcesOutputAmounts,
sampleSourcePathIds,
);
const adjustedFills: Fill[] = [];
const totalRoutedAmount = BigNumber.sum(...allSourcesRustRoute);
const scale = input.dividedBy(totalRoutedAmount);
for (const [routeInput, routeSamplesAndNativeOrders, outputAmount] of routesAndSamplesAndOutputs) {
for (const [routeInput, routeSamplesAndNativeOrders, outputAmount, sourcePathId] of routesAndSamplesAndOutputs) {
if (!routeInput || !routeSamplesAndNativeOrders || !outputAmount || !Number.isFinite(outputAmount)) {
continue;
}
@@ -233,14 +231,21 @@ function findRoutesAndCreateOptimalPath(
opts.outputAmountPerEth,
opts.inputAmountPerEth,
fees,
)[0];
// NOTE: For Limit/RFQ orders we are done here. No need to scale output
adjustedFills.push(nativeFill);
)[0] as Fill | undefined;
// Note: If the order has an adjusted rate of less than or equal to 0 it will be skipped
// and nativeFill will be `undefined`
if (nativeFill) {
// NOTE: For Limit/RFQ orders we are done here. No need to scale output
adjustedFills.push({ ...nativeFill, sourcePathId: sourcePathId ?? hexUtils.random() });
}
continue;
}
// NOTE: For DexSamples only
let fill = createFill(current);
if (!fill) {
continue;
}
const routeSamples = routeSamplesAndNativeOrders as Array<DexSample<FillData>>;
// Descend to approach a closer fill for fillData which may not be consistent
// throughout the path (UniswapV3) and for a closer guesstimate at
@@ -249,38 +254,40 @@ function findRoutesAndCreateOptimalPath(
assert.assert(routeSamples.length >= 1, 'Found no sample to use for source');
for (let k = routeSamples.length - 1; k >= 0; k--) {
if (k === 0) {
fill = createFill(routeSamples[0]);
fill = createFill(routeSamples[0]) ?? fill;
}
if (rustInputAdjusted.isGreaterThan(routeSamples[k].input)) {
const left = routeSamples[k];
const right = routeSamples[k + 1];
if (left && right) {
fill = createFill({
...right, // default to the greater (for gas used)
input: rustInputAdjusted,
output: new BigNumber(outputAmount),
});
fill =
createFill({
...right, // default to the greater (for gas used)
input: rustInputAdjusted,
output: new BigNumber(outputAmount),
}) ?? fill;
} else {
assert.assert(Boolean(left || right), 'No valid sample to use');
fill = createFill(left || right);
fill = createFill(left || right) ?? fill;
}
break;
}
}
// TODO(kimpers): remove once we have solved the rounding/precision loss issues in the Rust router
const scaleOutput = (output: BigNumber) =>
const scaleOutput = (fillInput: BigNumber, output: BigNumber) =>
output
.dividedBy(fill.input)
.dividedBy(fillInput)
.times(rustInputAdjusted)
.decimalPlaces(0, side === MarketOperation.Sell ? BigNumber.ROUND_FLOOR : BigNumber.ROUND_CEIL);
adjustedFills.push({
...fill,
input: rustInputAdjusted,
output: scaleOutput(fill.output),
adjustedOutput: scaleOutput(fill.adjustedOutput),
output: scaleOutput(fill.input, fill.output),
adjustedOutput: scaleOutput(fill.input, fill.adjustedOutput),
index: 0,
parent: undefined,
sourcePathId: sourcePathId ?? hexUtils.random(),
});
}
@@ -302,14 +309,10 @@ export function findOptimalRustPathFromSamples(
fees: FeeSchedule,
chainId: ChainId,
neonRouterNumSamples: number,
samplerMetrics?: SamplerMetrics,
): Path | undefined {
const before = performance.now();
const logPerformance = () =>
DEFAULT_INFO_LOGGER(
{ router: 'neon-router', performanceMs: performance.now() - before, type: 'total' },
'Rust router total routing performance',
);
const beforeAllTimeMs = performance.now();
let beforeTimeMs = performance.now();
const allSourcesPath = findRoutesAndCreateOptimalPath(
side,
samples,
@@ -319,6 +322,13 @@ export function findOptimalRustPathFromSamples(
fees,
neonRouterNumSamples,
);
// tslint:disable-next-line: no-unused-expression
samplerMetrics &&
samplerMetrics.logRouterDetails({
router: 'neon-router',
type: 'all',
timingMs: performance.now() - beforeTimeMs,
});
if (!allSourcesPath) {
return undefined;
}
@@ -328,6 +338,7 @@ export function findOptimalRustPathFromSamples(
// HACK(kimpers): The Rust router currently doesn't account for VIP sources correctly
// we need to try to route them in isolation and compare with the results all sources
if (vipSources.length > 0) {
beforeTimeMs = performance.now();
const vipSourcesSet = new Set(vipSources);
const vipSourcesSamples = samples.filter(s => s[0] && vipSourcesSet.has(s[0].source));
@@ -341,6 +352,13 @@ export function findOptimalRustPathFromSamples(
fees,
neonRouterNumSamples,
);
// tslint:disable-next-line: no-unused-expression
samplerMetrics &&
samplerMetrics.logRouterDetails({
router: 'neon-router',
type: 'vip',
timingMs: performance.now() - beforeTimeMs,
});
const { input: allSourcesInput, output: allSourcesOutput } = allSourcesPath.adjustedSize();
// NOTE: For sell quotes input is the taker asset and for buy quotes input is the maker asset
@@ -353,13 +371,18 @@ export function findOptimalRustPathFromSamples(
const allSourcesAdjustedRateWithFqtOverhead = getRate(side, allSourcesInput, outputWithFqtOverhead);
if (vipSourcesPath?.adjustedRate().isGreaterThan(allSourcesAdjustedRateWithFqtOverhead)) {
logPerformance();
return vipSourcesPath;
}
}
}
// tslint:disable-next-line: no-unused-expression
samplerMetrics &&
samplerMetrics.logRouterDetails({
router: 'neon-router',
type: 'total',
timingMs: performance.now() - beforeAllTimeMs,
});
logPerformance();
return allSourcesPath;
}
@@ -372,8 +395,10 @@ export async function findOptimalPathJSAsync(
fills: Fill[][],
targetInput: BigNumber,
runLimit: number = 2 ** 8,
samplerMetrics?: SamplerMetrics,
opts: PathPenaltyOpts = DEFAULT_PATH_PENALTY_OPTS,
): Promise<Path | undefined> {
const beforeTimeMs = performance.now();
// Sort fill arrays by descending adjusted completed rate.
// Remove any paths which cannot impact the optimal path
const sortedPaths = reducePaths(fillsToSortedPaths(fills, side, targetInput, opts), side);
@@ -387,7 +412,15 @@ export async function findOptimalPathJSAsync(
// Yield to event loop.
await Promise.resolve();
}
return optimalPath.isComplete() ? optimalPath : undefined;
const finalPath = optimalPath.isComplete() ? optimalPath : undefined;
// tslint:disable-next-line: no-unused-expression
samplerMetrics &&
samplerMetrics.logRouterDetails({
router: 'js',
type: 'total',
timingMs: performance.now() - beforeTimeMs,
});
return finalPath;
}
// Sort fill arrays by descending adjusted completed rate.

View File

@@ -518,6 +518,15 @@ export interface SamplerMetrics {
* @param blockNumber block number of the sampler call
*/
logBlockNumber(blockNumber: BigNumber): void;
/**
* Logs the routing timings
*
* @param data.router The router type (neon-router or js)
* @param data.type The type of timing being recorded (e.g total timing, all sources timing or vip timing)
* @param data.timingMs The timing in milliseconds
*/
logRouterDetails(data: { router: 'neon-router' | 'js'; type: 'all' | 'vip' | 'total'; timingMs: number }): void;
}
/**
@@ -604,6 +613,7 @@ export interface GenerateOptimizedOrdersOpts {
shouldBatchBridgeOrders?: boolean;
gasPrice: BigNumber;
neonRouterNumSamples: number;
samplerMetrics?: SamplerMetrics;
}
export interface ComparisonPrice {