feat: Improve Uniswap V3 gas schedule (#397)

* feat: UniswapV3Sampler use QuoterV2 for sells WIP

* feat: UniswapV3Sampler for QuoterV2 buys WIP

* refactor: separate logic to remove stack too deep issue

* feat: Use initializedTicksCrossed from Uniswap QuoterV2 for gas est.

* fix: use Quoter gasUsed instead of estimating gas from pools + ticks

* refactor: clean up UniswapV3Sampler & remove old Quoter interface

* refactor: unify code for buys and sells while handling stack too deep

* fix: use mean gas price from all sample estimating UniV3 gas schedule

* fix: fallback to legacy Uniswap V3 gas estimate if we can't get gasUsed

* refactor: use named function instead of fat arrow

* chore: add asset-swapper changelog entry
This commit is contained in:
Kim Persson 2022-02-10 11:16:24 +01:00 committed by GitHub
parent 25dd6bc79a
commit 84e4819e6e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 147 additions and 47 deletions

View File

@ -9,6 +9,10 @@
{ {
"note": "Fix incorrect output scaling when input is less than desired amount, update fast-abi", "note": "Fix incorrect output scaling when input is less than desired amount, update fast-abi",
"pr": 401 "pr": 401
},
{
"note": "Improve Uniswap V3 gas schedule",
"pr": 397
} }
] ]
}, },

View File

@ -22,17 +22,43 @@ pragma experimental ABIEncoderV2;
import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol"; import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol";
interface IUniswapV3Quoter { interface IUniswapV3QuoterV2 {
function factory() function factory()
external external
view view
returns (IUniswapV3Factory factory); returns (IUniswapV3Factory factory);
// @notice Returns the amount out received for a given exact input swap without executing the swap
// @param path The path of the swap, i.e. each token pair and the pool fee
// @param amountIn The amount of the first token to swap
// @return amountOut The amount of the last token that would be received
// @return sqrtPriceX96AfterList List of the sqrt price after the swap for each pool in the path
// @return initializedTicksCrossedList List of the initialized ticks that the swap crossed for each pool in the path
// @return gasEstimate The estimate of the gas that the swap consumes
function quoteExactInput(bytes memory path, uint256 amountIn) function quoteExactInput(bytes memory path, uint256 amountIn)
external external
returns (uint256 amountOut); returns (
uint256 amountOut,
uint160[] memory sqrtPriceX96AfterList,
uint32[] memory initializedTicksCrossedList,
uint256 gasEstimate
);
// @notice Returns the amount in required for a given exact output swap without executing the swap
// @param path The path of the swap, i.e. each token pair and the pool fee. Path must be provided in reverse order
// @param amountOut The amount of the last token to receive
// @return amountIn The amount of first token required to be paid
// @return sqrtPriceX96AfterList List of the sqrt price after the swap for each pool in the path
// @return initializedTicksCrossedList List of the initialized ticks that the swap crossed for each pool in the path
// @return gasEstimate The estimate of the gas that the swap consumes
function quoteExactOutput(bytes memory path, uint256 amountOut) function quoteExactOutput(bytes memory path, uint256 amountOut)
external external
returns (uint256 amountIn); returns (
uint256 amountIn,
uint160[] memory sqrtPriceX96AfterList,
uint32[] memory initializedTicksCrossedList,
uint256 gasEstimate
);
} }
interface IUniswapV3Factory { interface IUniswapV3Factory {
@ -61,14 +87,15 @@ contract UniswapV3Sampler
/// @return makerTokenAmounts Maker amounts bought at each taker token /// @return makerTokenAmounts Maker amounts bought at each taker token
/// amount. /// amount.
function sampleSellsFromUniswapV3( function sampleSellsFromUniswapV3(
IUniswapV3Quoter quoter, IUniswapV3QuoterV2 quoter,
IERC20TokenV06[] memory path, IERC20TokenV06[] memory path,
uint256[] memory takerTokenAmounts uint256[] memory takerTokenAmounts
) )
public public
returns ( returns (
bytes[] memory uniswapPaths, bytes[] memory uniswapPaths,
uint256[] memory makerTokenAmounts uint256[] memory makerTokenAmounts,
uint256[] memory uniswapGasUsed
) )
{ {
IUniswapV3Pool[][] memory poolPaths = IUniswapV3Pool[][] memory poolPaths =
@ -76,31 +103,39 @@ contract UniswapV3Sampler
makerTokenAmounts = new uint256[](takerTokenAmounts.length); makerTokenAmounts = new uint256[](takerTokenAmounts.length);
uniswapPaths = new bytes[](takerTokenAmounts.length); uniswapPaths = new bytes[](takerTokenAmounts.length);
uniswapGasUsed = new uint256[](takerTokenAmounts.length);
for (uint256 i = 0; i < takerTokenAmounts.length; ++i) { for (uint256 i = 0; i < takerTokenAmounts.length; ++i) {
// Pick the best result from all the paths. // Pick the best result from all the paths.
bytes memory topUniswapPath;
uint256 topBuyAmount = 0; uint256 topBuyAmount = 0;
for (uint256 j = 0; j < poolPaths.length; ++j) { for (uint256 j = 0; j < poolPaths.length; ++j) {
bytes memory uniswapPath = _toUniswapPath(path, poolPaths[j]); bytes memory uniswapPath = _toUniswapPath(path, poolPaths[j]);
try try quoter.quoteExactInput
quoter.quoteExactInput { gas: QUOTE_GAS }
{ gas: QUOTE_GAS } (uniswapPath, takerTokenAmounts[i])
(uniswapPath, takerTokenAmounts[i]) returns (
returns (uint256 buyAmount) uint256 buyAmount,
uint160[] memory, /* sqrtPriceX96AfterList */
uint32[] memory, /* initializedTicksCrossedList */
uint256 gasUsed
)
{ {
if (topBuyAmount <= buyAmount) { if (topBuyAmount <= buyAmount) {
topBuyAmount = buyAmount; topBuyAmount = buyAmount;
topUniswapPath = uniswapPath; uniswapPaths[i] = uniswapPath;
uniswapGasUsed[i] = gasUsed;
} }
} catch { } } catch {}
} }
// Break early if we can't complete the buys. // Break early if we can't complete the sells.
if (topBuyAmount == 0) { if (topBuyAmount == 0) {
// HACK(kimpers): To avoid too many local variables, paths and gas used is set directly in the loop
// then reset if no valid valid quote was found
uniswapPaths[i] = "";
uniswapGasUsed[i] = 0;
break; break;
} }
makerTokenAmounts[i] = topBuyAmount; makerTokenAmounts[i] = topBuyAmount;
uniswapPaths[i] = topUniswapPath;
} }
} }
@ -112,14 +147,15 @@ contract UniswapV3Sampler
/// @return takerTokenAmounts Taker amounts sold at each maker token /// @return takerTokenAmounts Taker amounts sold at each maker token
/// amount. /// amount.
function sampleBuysFromUniswapV3( function sampleBuysFromUniswapV3(
IUniswapV3Quoter quoter, IUniswapV3QuoterV2 quoter,
IERC20TokenV06[] memory path, IERC20TokenV06[] memory path,
uint256[] memory makerTokenAmounts uint256[] memory makerTokenAmounts
) )
public public
returns ( returns (
bytes[] memory uniswapPaths, bytes[] memory uniswapPaths,
uint256[] memory takerTokenAmounts uint256[] memory takerTokenAmounts,
uint256[] memory uniswapGasUsed
) )
{ {
IUniswapV3Pool[][] memory poolPaths = IUniswapV3Pool[][] memory poolPaths =
@ -128,11 +164,12 @@ contract UniswapV3Sampler
takerTokenAmounts = new uint256[](makerTokenAmounts.length); takerTokenAmounts = new uint256[](makerTokenAmounts.length);
uniswapPaths = new bytes[](makerTokenAmounts.length); uniswapPaths = new bytes[](makerTokenAmounts.length);
uniswapGasUsed = new uint256[](makerTokenAmounts.length);
for (uint256 i = 0; i < makerTokenAmounts.length; ++i) { for (uint256 i = 0; i < makerTokenAmounts.length; ++i) {
uint256 topSellAmount;
// Pick the best result from all the paths. // Pick the best result from all the paths.
bytes memory topUniswapPath;
uint256 topSellAmount = 0;
for (uint256 j = 0; j < poolPaths.length; ++j) { for (uint256 j = 0; j < poolPaths.length; ++j) {
// quoter requires path to be reversed for buys. // quoter requires path to be reversed for buys.
bytes memory uniswapPath = _toUniswapPath( bytes memory uniswapPath = _toUniswapPath(
@ -143,21 +180,30 @@ contract UniswapV3Sampler
quoter.quoteExactOutput quoter.quoteExactOutput
{ gas: QUOTE_GAS } { gas: QUOTE_GAS }
(uniswapPath, makerTokenAmounts[i]) (uniswapPath, makerTokenAmounts[i])
returns (uint256 sellAmount) returns (
uint256 sellAmount,
uint160[] memory, /* sqrtPriceX96AfterList */
uint32[] memory, /* initializedTicksCrossedList */
uint256 gasUsed
)
{ {
if (topSellAmount == 0 || topSellAmount >= sellAmount) { if (topSellAmount == 0 || topSellAmount >= sellAmount) {
topSellAmount = sellAmount; topSellAmount = sellAmount;
// But the output path should still be encoded for sells. // But the output path should still be encoded for sells.
topUniswapPath = _toUniswapPath(path, poolPaths[j]); uniswapPaths[i] = _toUniswapPath(path, poolPaths[j]);
uniswapGasUsed[i] = gasUsed;
} }
} catch {} } catch {}
} }
// Break early if we can't complete the buys. // Break early if we can't complete the buys.
if (topSellAmount == 0) { if (topSellAmount == 0) {
// HACK(kimpers): To avoid too many local variables, paths and gas used is set directly in the loop
// then reset if no valid valid quote was found
uniswapPaths[i] = "";
uniswapGasUsed[i] = 0;
break; break;
} }
takerTokenAmounts[i] = topSellAmount; takerTokenAmounts[i] = topSellAmount;
uniswapPaths[i] = topUniswapPath;
} }
} }
@ -236,6 +282,7 @@ contract UniswapV3Sampler
function _reverseTokenPath(IERC20TokenV06[] memory tokenPath) function _reverseTokenPath(IERC20TokenV06[] memory tokenPath)
private private
pure
returns (IERC20TokenV06[] memory reversed) returns (IERC20TokenV06[] memory reversed)
{ {
reversed = new IERC20TokenV06[](tokenPath.length); reversed = new IERC20TokenV06[](tokenPath.length);
@ -246,6 +293,7 @@ contract UniswapV3Sampler
function _reversePoolPath(IUniswapV3Pool[] memory poolPath) function _reversePoolPath(IUniswapV3Pool[] memory poolPath)
private private
pure
returns (IUniswapV3Pool[] memory reversed) returns (IUniswapV3Pool[] memory reversed)
{ {
reversed = new IUniswapV3Pool[](poolPath.length); reversed = new IUniswapV3Pool[](poolPath.length);

View File

@ -2,6 +2,7 @@ import { ChainId, getContractAddressesForChainOrThrow } from '@0x/contract-addre
import { FillQuoteTransformerOrderType } from '@0x/protocol-utils'; import { FillQuoteTransformerOrderType } from '@0x/protocol-utils';
import { BigNumber } from '@0x/utils'; import { BigNumber } from '@0x/utils';
import { formatBytes32String } from '@ethersproject/strings'; import { formatBytes32String } from '@ethersproject/strings';
import * as _ from 'lodash';
import { TokenAdjacencyGraphBuilder } from '../token_adjacency_graph_builder'; import { TokenAdjacencyGraphBuilder } from '../token_adjacency_graph_builder';
@ -17,7 +18,9 @@ import {
ERC20BridgeSource, ERC20BridgeSource,
FeeSchedule, FeeSchedule,
FillData, FillData,
FinalUniswapV3FillData,
GetMarketOrdersOpts, GetMarketOrdersOpts,
isFinalUniswapV3FillData,
KyberSamplerOpts, KyberSamplerOpts,
LidoInfo, LidoInfo,
LiquidityProviderFillData, LiquidityProviderFillData,
@ -2039,19 +2042,19 @@ export const BEETHOVEN_X_SUBGRAPH_URL_BY_CHAIN = valueByChainId<string>(
export const UNISWAPV3_CONFIG_BY_CHAIN_ID = valueByChainId( export const UNISWAPV3_CONFIG_BY_CHAIN_ID = valueByChainId(
{ {
[ChainId.Mainnet]: { [ChainId.Mainnet]: {
quoter: '0xb27308f9f90d607463bb33ea1bebb41c27ce5ab6', quoter: '0x61ffe014ba17989e743c5f6cb21bf9697530b21e',
router: '0xe592427a0aece92de3edee1f18e0157c05861564', router: '0xe592427a0aece92de3edee1f18e0157c05861564',
}, },
[ChainId.Ropsten]: { [ChainId.Ropsten]: {
quoter: '0x2f9e608fd881861b8916257b76613cb22ee0652c', quoter: '0x61ffe014ba17989e743c5f6cb21bf9697530b21e',
router: '0x03782388516e94fcd4c18666303601a12aa729ea', router: '0x03782388516e94fcd4c18666303601a12aa729ea',
}, },
[ChainId.Polygon]: { [ChainId.Polygon]: {
quoter: '0xb27308f9f90d607463bb33ea1bebb41c27ce5ab6', quoter: '0x61ffe014ba17989e743c5f6cb21bf9697530b21e',
router: '0xe592427a0aece92de3edee1f18e0157c05861564', router: '0xe592427a0aece92de3edee1f18e0157c05861564',
}, },
[ChainId.Optimism]: { [ChainId.Optimism]: {
quoter: '0xb27308f9f90d607463bb33ea1bebb41c27ce5ab6', quoter: '0x61ffe014ba17989e743c5f6cb21bf9697530b21e',
router: '0xe592427a0aece92de3edee1f18e0157c05861564', router: '0xe592427a0aece92de3edee1f18e0157c05861564',
}, },
}, },
@ -2334,11 +2337,33 @@ export const DEFAULT_GAS_SCHEDULE: Required<FeeSchedule> = {
return gas; return gas;
}, },
[ERC20BridgeSource.UniswapV3]: (fillData?: FillData) => { [ERC20BridgeSource.UniswapV3]: (fillData?: FillData) => {
let gas = 100e3; const uniFillData = fillData as UniswapV3FillData | FinalUniswapV3FillData;
const path = (fillData as UniswapV3FillData).tokenAddressPath; // NOTE: This base value was heuristically chosen by looking at how much it generally
if (path.length > 2) { // underestimated gas usage
gas += (path.length - 2) * 32e3; // +32k for each hop. const base = 34e3; // 34k base
let gas = base;
if (isFinalUniswapV3FillData(uniFillData)) {
gas += uniFillData.gasUsed;
} else {
// NOTE: We don't actually know which of the paths would be used in the router
// therefore we estimate using the mean of gas prices returned from UniswapV3
// For the best case scenario (least amount of hops & ticks) this will
// over estimate the gas usage
const pathAmountsWithGasUsed = uniFillData.pathAmounts.filter(p => p.gasUsed > 0);
const meanGasUsedForPath = Math.round(_.meanBy(pathAmountsWithGasUsed, p => p.gasUsed));
gas += meanGasUsedForPath;
} }
// If we for some reason could not read `gasUsed` when sampling
// fall back to legacy gas estimation
if (gas === base) {
gas = 100e3;
const path = uniFillData.tokenAddressPath;
if (path.length > 2) {
gas += (path.length - 2) * 32e3; // +32k for each hop.
}
}
return gas; return gas;
}, },
[ERC20BridgeSource.Lido]: () => 226e3, [ERC20BridgeSource.Lido]: () => 226e3,

View File

@ -36,6 +36,7 @@ import {
ShellFillData, ShellFillData,
UniswapV2FillData, UniswapV2FillData,
UniswapV3FillData, UniswapV3FillData,
UniswapV3PathAmount,
} from './types'; } from './types';
// tslint:disable completed-docs // tslint:disable completed-docs
@ -387,11 +388,14 @@ function createFinalBridgeOrderFillDataFromCollapsedFill(fill: CollapsedFill): F
switch (fill.source) { switch (fill.source) {
case ERC20BridgeSource.UniswapV3: { case ERC20BridgeSource.UniswapV3: {
const fd = fill.fillData as UniswapV3FillData; const fd = fill.fillData as UniswapV3FillData;
return { const { uniswapPath, gasUsed } = getBestUniswapV3PathAmountForInputAmount(fd, fill.input);
const finalFillData: FinalUniswapV3FillData = {
router: fd.router, router: fd.router,
tokenAddressPath: fd.tokenAddressPath, tokenAddressPath: fd.tokenAddressPath,
uniswapPath: getBestUniswapV3PathForInputAmount(fd, fill.input), uniswapPath,
gasUsed,
}; };
return finalFillData;
} }
default: default:
break; break;
@ -399,18 +403,21 @@ function createFinalBridgeOrderFillDataFromCollapsedFill(fill: CollapsedFill): F
return fill.fillData; return fill.fillData;
} }
function getBestUniswapV3PathForInputAmount(fillData: UniswapV3FillData, inputAmount: BigNumber): string { function getBestUniswapV3PathAmountForInputAmount(
fillData: UniswapV3FillData,
inputAmount: BigNumber,
): UniswapV3PathAmount {
if (fillData.pathAmounts.length === 0) { if (fillData.pathAmounts.length === 0) {
throw new Error(`No Uniswap V3 paths`); throw new Error(`No Uniswap V3 paths`);
} }
// Find the best path that can satisfy `inputAmount`. // Find the best path that can satisfy `inputAmount`.
// Assumes `fillData.pathAmounts` is sorted ascending. // Assumes `fillData.pathAmounts` is sorted ascending.
for (const { inputAmount: pathInputAmount, uniswapPath } of fillData.pathAmounts) { for (const pathAmount of fillData.pathAmounts) {
if (pathInputAmount.gte(inputAmount)) { if (pathAmount.inputAmount.gte(inputAmount)) {
return uniswapPath; return pathAmount;
} }
} }
return fillData.pathAmounts[fillData.pathAmounts.length - 1].uniswapPath; return fillData.pathAmounts[fillData.pathAmounts.length - 1];
} }
export function getMakerTakerTokens(opts: CreateOrderFromPathOpts): [string, string] { export function getMakerTakerTokens(opts: CreateOrderFromPathOpts): [string, string] {

View File

@ -767,16 +767,17 @@ export class SamplerOperations {
function: this._samplerContract.sampleSellsFromUniswapV3, function: this._samplerContract.sampleSellsFromUniswapV3,
params: [quoter, tokenAddressPath, takerFillAmounts], params: [quoter, tokenAddressPath, takerFillAmounts],
callback: (callResults: string, fillData: UniswapV3FillData): BigNumber[] => { callback: (callResults: string, fillData: UniswapV3FillData): BigNumber[] => {
const [paths, samples] = this._samplerContract.getABIDecodedReturnData<[string[], BigNumber[]]>( const [paths, samples, gasUsed] = this._samplerContract.getABIDecodedReturnData<
'sampleSellsFromUniswapV3', [string[], BigNumber[], BigNumber[]]
callResults, >('sampleSellsFromUniswapV3', callResults);
);
fillData.router = router; fillData.router = router;
fillData.tokenAddressPath = tokenAddressPath; fillData.tokenAddressPath = tokenAddressPath;
fillData.pathAmounts = paths.map((uniswapPath, i) => ({ fillData.pathAmounts = paths.map((uniswapPath, i) => ({
uniswapPath, uniswapPath,
inputAmount: takerFillAmounts[i], inputAmount: takerFillAmounts[i],
gasUsed: gasUsed[i].toNumber(),
})); }));
return samples; return samples;
}, },
}); });
@ -795,15 +796,15 @@ export class SamplerOperations {
function: this._samplerContract.sampleBuysFromUniswapV3, function: this._samplerContract.sampleBuysFromUniswapV3,
params: [quoter, tokenAddressPath, makerFillAmounts], params: [quoter, tokenAddressPath, makerFillAmounts],
callback: (callResults: string, fillData: UniswapV3FillData): BigNumber[] => { callback: (callResults: string, fillData: UniswapV3FillData): BigNumber[] => {
const [paths, samples] = this._samplerContract.getABIDecodedReturnData<[string[], BigNumber[]]>( const [paths, samples, gasUsed] = this._samplerContract.getABIDecodedReturnData<
'sampleBuysFromUniswapV3', [string[], BigNumber[], BigNumber[]]
callResults, >('sampleBuysFromUniswapV3', callResults);
);
fillData.router = router; fillData.router = router;
fillData.tokenAddressPath = tokenAddressPath; fillData.tokenAddressPath = tokenAddressPath;
fillData.pathAmounts = paths.map((uniswapPath, i) => ({ fillData.pathAmounts = paths.map((uniswapPath, i) => ({
uniswapPath, uniswapPath,
inputAmount: makerFillAmounts[i], inputAmount: makerFillAmounts[i],
gasUsed: gasUsed[i].toNumber(),
})); }));
return samples; return samples;
}, },

View File

@ -268,19 +268,34 @@ export interface HopInfo {
returnData: string; returnData: string;
} }
export interface UniswapV3PathAmount {
uniswapPath: string;
inputAmount: BigNumber;
gasUsed: number;
}
export interface UniswapV3FillData extends FillData { export interface UniswapV3FillData extends FillData {
tokenAddressPath: string[]; tokenAddressPath: string[];
router: string; router: string;
pathAmounts: Array<{ uniswapPath: string; inputAmount: BigNumber }>; pathAmounts: UniswapV3PathAmount[];
} }
export interface KyberDmmFillData extends UniswapV2FillData { export interface KyberDmmFillData extends UniswapV2FillData {
poolsPath: string[]; poolsPath: string[];
} }
export interface FinalUniswapV3FillData extends Omit<UniswapV3FillData, 'uniswapPaths'> { /**
* Determines whether FillData is UniswapV3FillData or FinalUniswapV3FillData
*/
export function isFinalUniswapV3FillData(
data: UniswapV3FillData | FinalUniswapV3FillData,
): data is FinalUniswapV3FillData {
return !!(data as FinalUniswapV3FillData).uniswapPath;
}
export interface FinalUniswapV3FillData extends Omit<UniswapV3FillData, 'pathAmounts'> {
// The uniswap-encoded path that can fll the maximum input amount. // The uniswap-encoded path that can fll the maximum input amount.
uniswapPath: string; uniswapPath: string;
gasUsed: number;
} }
export interface LidoFillData extends FillData { export interface LidoFillData extends FillData {