diff --git a/package-lock.json b/package-lock.json index b4ecc411a..280d558d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,7 @@ "packages": { "": { "name": "@uniswap/smart-order-router", - "version": "2.5.13", + "version": "2.5.14", "license": "GPL", "dependencies": { "@bitauth/libauth": "^1.17.1", diff --git a/src/abis/gasPriceOracle.json b/src/abis/gasPriceOracle.json new file mode 100644 index 000000000..735c1b695 --- /dev/null +++ b/src/abis/gasPriceOracle.json @@ -0,0 +1,298 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "_owner", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "DecimalsUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "GasPriceUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "L1BaseFeeUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "OverheadUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "ScalarUpdated", + "type": "event" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "gasPrice", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "_data", + "type": "bytes" + } + ], + "name": "getL1Fee", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "_data", + "type": "bytes" + } + ], + "name": "getL1GasUsed", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "l1BaseFee", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "overhead", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "scalar", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_decimals", + "type": "uint256" + } + ], + "name": "setDecimals", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_gasPrice", + "type": "uint256" + } + ], + "name": "setGasPrice", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_baseFee", + "type": "uint256" + } + ], + "name": "setL1BaseFee", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_overhead", + "type": "uint256" + } + ], + "name": "setOverhead", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_scalar", + "type": "uint256" + } + ], + "name": "setScalar", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/src/providers/multicall-provider.ts b/src/providers/multicall-provider.ts index 654276a4a..4d782e980 100644 --- a/src/providers/multicall-provider.ts +++ b/src/providers/multicall-provider.ts @@ -26,6 +26,18 @@ export type CallSameFunctionOnContractWithMultipleParams< additionalConfig?: TAdditionalConfig; }; +export type CallMultipleFunctionsOnSameContractParams< + TFunctionParams, + TAdditionalConfig = any +> = { + address: string; + contractInterface: Interface; + functionNames: string[]; + functionParams?: TFunctionParams[]; + providerConfig?: ProviderConfig; + additionalConfig?: TAdditionalConfig; +}; + export type SuccessResult = { success: true; result: TReturn; @@ -96,4 +108,17 @@ export abstract class IMulticallProvider { blockNumber: BigNumber; results: Result[]; }>; + + public abstract callMultipleFunctionsOnSameContract< + TFunctionParams extends any[] | undefined, + TReturn = any + >( + params: CallMultipleFunctionsOnSameContractParams< + TFunctionParams, + TMulticallConfig + > + ): Promise<{ + blockNumber: BigNumber; + results: Result[]; + }>; } diff --git a/src/providers/multicall-uniswap-provider.ts b/src/providers/multicall-uniswap-provider.ts index 9d02d0cbc..6a59f199f 100644 --- a/src/providers/multicall-uniswap-provider.ts +++ b/src/providers/multicall-uniswap-provider.ts @@ -7,6 +7,7 @@ import { ChainId } from '../util'; import { UNISWAP_MULTICALL_ADDRESS } from '../util/addresses'; import { log } from '../util/log'; import { + CallMultipleFunctionsOnSameContractParams, CallSameFunctionOnContractWithMultipleParams, CallSameFunctionOnMultipleContractsParams, IMulticallProvider, @@ -228,4 +229,99 @@ export class UniswapMulticallProvider extends IMulticallProvider( + params: CallMultipleFunctionsOnSameContractParams< + TFunctionParams, + UniswapMulticallConfig + > + ): Promise<{ + blockNumber: BigNumber; + results: Result[]; + approxGasUsedPerSuccessCall: number; + }> { + const { + address, + contractInterface, + functionNames, + functionParams, + additionalConfig, + providerConfig, + } = params; + + const gasLimitPerCall = + additionalConfig?.gasLimitPerCallOverride ?? this.gasLimitPerCall; + const blockNumberOverride = providerConfig?.blockNumber ?? undefined; + + const calls = _.map(functionNames, (functionName, i) => { + const fragment = contractInterface.getFunction(functionName); + const param = functionParams ? functionParams[i] : []; + const callData = contractInterface.encodeFunctionData(fragment, param); + return { + target: address, + callData, + gasLimit: gasLimitPerCall, + }; + }); + + log.debug( + { calls }, + `About to multicall for ${functionNames.length} functions at address ${address} with ${functionParams?.length} different sets of params` + ); + + const { blockNumber, returnData: aggregateResults } = + await this.multicallContract.callStatic.multicall(calls, { + blockTag: blockNumberOverride, + }); + + const results: Result[] = []; + + const gasUsedForSuccess: number[] = []; + for (let i = 0; i < aggregateResults.length; i++) { + const fragment = contractInterface.getFunction(functionNames[i]!); + const { success, returnData, gasUsed } = aggregateResults[i]!; + + // Return data "0x" is sometimes returned for invalid pools. + if (!success || returnData.length <= 2) { + log.debug( + { result: aggregateResults[i] }, + `Invalid result calling ${functionNames[i]} with ${ + functionParams ? functionParams[i] : '0' + } params` + ); + results.push({ + success: false, + returnData, + }); + continue; + } + + gasUsedForSuccess.push(gasUsed.toNumber()); + + results.push({ + success: true, + result: contractInterface.decodeFunctionResult( + fragment, + returnData + ) as unknown as TReturn, + }); + } + + log.debug( + { results, functionNames, address }, + `Results for multicall for ${ + functionNames.length + } functions at address ${address} with ${ + functionParams ? functionParams.length : ' 0' + } different sets of params. Results as of block ${blockNumber}` + ); + return { + blockNumber, + results, + approxGasUsedPerSuccessCall: stats.percentile(gasUsedForSuccess, 99), + }; + } } diff --git a/src/providers/swap-router-provider.ts b/src/providers/swap-router-provider.ts index cd371cd32..16831add2 100644 --- a/src/providers/swap-router-provider.ts +++ b/src/providers/swap-router-provider.ts @@ -9,7 +9,7 @@ type TokenApprovalTypes = { approvalTokenOut: ApprovalTypes; }; -const SWAP_ROUTER_ADDRESS = '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45'; +const SWAP_ROUTER_ADDRESS = '0x075B36dE1Bd11cb361c5B3B1E80A9ab0e7aa8a60'; /** * Provider for accessing the SwapRouter02 Contract . diff --git a/src/providers/v3/gas-data-provider.ts b/src/providers/v3/gas-data-provider.ts new file mode 100644 index 000000000..51d077c6f --- /dev/null +++ b/src/providers/v3/gas-data-provider.ts @@ -0,0 +1,81 @@ +import { BigNumber } from 'ethers'; +import { GasPriceOracle__factory } from '../../types/other/factories/GasPriceOracle__factory'; +import { ChainId, log, OVM_GASPRICE_ADDRESS } from '../../util'; +import { IMulticallProvider } from '../multicall-provider'; + +export type OptimismGasData = { + l1BaseFee: BigNumber; + scalar: BigNumber; + decimals: BigNumber; + overhead: BigNumber; +}; + +/** + * Provider for getting Optimism gas constants. + * + * @export + * @interface IOptimismGasDataProvider + */ +export interface IOptimismGasDataProvider { + /** + * Gets the data constants needed to calculate the l1 security fee on Optimism. + * @returns An OptimismGasData object that includes the l1BaseFee, + * scalar, decimals, and overhead values. + */ + getGasData(): Promise; +} + +export class OptimismGasDataProvider implements IOptimismGasDataProvider { + protected gasOracleAddress: string; + + constructor( + protected chainId: ChainId, + protected multicall2Provider: IMulticallProvider, + gasPriceAddress?: string + ) { + if (chainId != ChainId.OPTIMISM && chainId != ChainId.OPTIMISTIC_KOVAN) { + throw new Error('This data provider is used only on optimism networks.'); + } + this.gasOracleAddress = gasPriceAddress ?? OVM_GASPRICE_ADDRESS; + } + + public async getGasData(): Promise { + const funcNames = ['l1BaseFee', 'scalar', 'decimals', 'overhead']; + const tx = + await this.multicall2Provider.callMultipleFunctionsOnSameContract< + undefined, + [BigNumber] + >({ + address: this.gasOracleAddress, + contractInterface: GasPriceOracle__factory.createInterface(), + functionNames: funcNames, + }); + + if ( + !tx.results[0]?.success || + !tx.results[1]?.success || + !tx.results[2]?.success || + !tx.results[3]?.success + ) { + log.info( + { results: tx.results }, + 'Failed to get gas constants data from the optimism gas oracle' + ); + throw new Error( + 'Failed to get gas constants data from the optimism gas oracle' + ); + } + + const { result: l1BaseFee } = tx.results![0]; + const { result: scalar } = tx.results![1]; + const { result: decimals } = tx.results![2]; + const { result: overhead } = tx.results![3]; + + return { + l1BaseFee: l1BaseFee[0], + scalar: scalar[0], + decimals: decimals[0], + overhead: overhead[0], + }; + } +} diff --git a/src/routers/alpha-router/alpha-router.ts b/src/routers/alpha-router/alpha-router.ts index 87af08a7d..978326d79 100644 --- a/src/routers/alpha-router/alpha-router.ts +++ b/src/routers/alpha-router/alpha-router.ts @@ -2,12 +2,11 @@ import DEFAULT_TOKEN_LIST from '@uniswap/default-token-list'; import { Protocol, SwapRouter, Trade } from '@uniswap/router-sdk'; import { Currency, Fraction, Token, TradeType } from '@uniswap/sdk-core'; import { TokenList } from '@uniswap/token-lists'; -import { Pair, Route as V2RouteRaw } from '@uniswap/v2-sdk'; +import { Pair } from '@uniswap/v2-sdk'; import { MethodParameters, Pool, Position, - Route as V3RouteRaw, SqrtPriceMath, TickMath, } from '@uniswap/v3-sdk'; @@ -57,6 +56,10 @@ import { IV2PoolProvider, V2PoolProvider, } from '../../providers/v2/pool-provider'; +import { + IOptimismGasDataProvider, + OptimismGasDataProvider, +} from '../../providers/v3/gas-data-provider'; import { IV3PoolProvider, V3PoolProvider, @@ -67,8 +70,17 @@ import { } from '../../providers/v3/quote-provider'; import { IV3SubgraphProvider } from '../../providers/v3/subgraph-provider'; import { CurrencyAmount } from '../../util/amounts'; -import { ChainId, ID_TO_CHAIN_ID, ID_TO_NETWORK_NAME } from '../../util/chains'; +import { + ChainId, + ID_TO_CHAIN_ID, + ID_TO_NETWORK_NAME, + V2_SUPPORTED, +} from '../../util/chains'; import { log } from '../../util/log'; +import { + buildSwapMethodParameters, + buildTrade, +} from '../../util/methodParameters'; import { metric, MetricLoggerUnit } from '../../util/metric'; import { poolToString, routeToString } from '../../util/routes'; import { UNSUPPORTED_TOKENS } from '../../util/unsupported-tokens'; @@ -178,6 +190,11 @@ export type AlphaRouterParams = { */ swapRouterProvider?: ISwapRouterProvider; + /** + * Calls the optimism gas oracle contract to fetch constants for calculating the l1 security fee. + */ + optimismGasDataProvider?: IOptimismGasDataProvider; + /** * A token validator for detecting fee-on-transfer tokens or tokens that can't be transferred. */ @@ -299,6 +316,7 @@ export class AlphaRouter protected v2GasModelFactory: IV2GasModelFactory; protected tokenValidatorProvider?: ITokenValidatorProvider; protected blockedTokenListProvider?: ITokenListProvider; + protected optimismGasDataProvider?: IOptimismGasDataProvider; constructor({ chainId, @@ -316,6 +334,7 @@ export class AlphaRouter v3GasModelFactory, v2GasModelFactory, swapRouterProvider, + optimismGasDataProvider, tokenValidatorProvider, }: AlphaRouterParams) { this.chainId = chainId; @@ -505,6 +524,11 @@ export class AlphaRouter this.swapRouterProvider = swapRouterProvider ?? new SwapRouterProvider(this.multicall2Provider); + if (chainId == ChainId.OPTIMISM || chainId == ChainId.OPTIMISTIC_KOVAN) { + this.optimismGasDataProvider = + optimismGasDataProvider ?? + new OptimismGasDataProvider(chainId, this.multicall2Provider); + } if (tokenValidatorProvider) { this.tokenValidatorProvider = tokenValidatorProvider; } else if (this.chainId == ChainId.MAINNET) { @@ -765,8 +789,9 @@ export class AlphaRouter const protocolsSet = new Set(protocols ?? []); if ( - protocolsSet.size == 0 || - (protocolsSet.has(Protocol.V2) && protocolsSet.has(Protocol.V3)) + (protocolsSet.size == 0 || + (protocolsSet.has(Protocol.V2) && protocolsSet.has(Protocol.V3))) && + V2_SUPPORTED.includes(this.chainId) ) { log.info({ protocols, tradeType }, 'Routing across all protocols'); quotePromises.push( @@ -794,7 +819,10 @@ export class AlphaRouter ) ); } else { - if (protocolsSet.has(Protocol.V3)) { + if ( + protocolsSet.has(Protocol.V3) || + (protocolsSet.size == 0 && !V2_SUPPORTED.includes(this.chainId)) + ) { log.info({ protocols, swapType: tradeType }, 'Routing across V3'); quotePromises.push( this.getV3Quotes( @@ -871,7 +899,7 @@ export class AlphaRouter } = swapRouteRaw; // Build Trade object that represents the optimal swap. - const trade = this.buildTrade( + const trade = buildTrade( currencyIn, currencyOut, tradeType, @@ -883,7 +911,7 @@ export class AlphaRouter // If user provided recipient, deadline etc. we also generate the calldata required to execute // the swap and return it too. if (swapConfig) { - methodParameters = this.buildSwapMethodParameters(trade, swapConfig); + methodParameters = buildSwapMethodParameters(trade, swapConfig); } metric.putMetric( @@ -1031,7 +1059,8 @@ export class AlphaRouter this.chainId, gasPriceWei, this.v3PoolProvider, - quoteToken + quoteToken, + this.optimismGasDataProvider ); metric.putMetric( @@ -1249,168 +1278,6 @@ export class AlphaRouter return [percents, amounts]; } - private buildTrade( - tokenInCurrency: Currency, - tokenOutCurrency: Currency, - tradeType: TTradeType, - routeAmounts: RouteWithValidQuote[] - ): Trade { - const [v3RouteAmounts, v2RouteAmounts] = _.partition( - routeAmounts, - (routeAmount) => routeAmount.protocol == Protocol.V3 - ); - - const v3Routes = _.map< - V3RouteWithValidQuote, - { - routev3: V3RouteRaw; - inputAmount: CurrencyAmount; - outputAmount: CurrencyAmount; - } - >( - v3RouteAmounts as V3RouteWithValidQuote[], - (routeAmount: V3RouteWithValidQuote) => { - const { route, amount, quote } = routeAmount; - - // The route, amount and quote are all in terms of wrapped tokens. - // When constructing the Trade object the inputAmount/outputAmount must - // use native currencies if specified by the user. This is so that the Trade knows to wrap/unwrap. - if (tradeType == TradeType.EXACT_INPUT) { - const amountCurrency = CurrencyAmount.fromFractionalAmount( - tokenInCurrency, - amount.numerator, - amount.denominator - ); - const quoteCurrency = CurrencyAmount.fromFractionalAmount( - tokenOutCurrency, - quote.numerator, - quote.denominator - ); - - const routeRaw = new V3RouteRaw( - route.pools, - amountCurrency.currency, - quoteCurrency.currency - ); - - return { - routev3: routeRaw, - inputAmount: amountCurrency, - outputAmount: quoteCurrency, - }; - } else { - const quoteCurrency = CurrencyAmount.fromFractionalAmount( - tokenInCurrency, - quote.numerator, - quote.denominator - ); - - const amountCurrency = CurrencyAmount.fromFractionalAmount( - tokenOutCurrency, - amount.numerator, - amount.denominator - ); - - const routeCurrency = new V3RouteRaw( - route.pools, - quoteCurrency.currency, - amountCurrency.currency - ); - - return { - routev3: routeCurrency, - inputAmount: quoteCurrency, - outputAmount: amountCurrency, - }; - } - } - ); - - const v2Routes = _.map< - V2RouteWithValidQuote, - { - routev2: V2RouteRaw; - inputAmount: CurrencyAmount; - outputAmount: CurrencyAmount; - } - >( - v2RouteAmounts as V2RouteWithValidQuote[], - (routeAmount: V2RouteWithValidQuote) => { - const { route, amount, quote } = routeAmount; - - // The route, amount and quote are all in terms of wrapped tokens. - // When constructing the Trade object the inputAmount/outputAmount must - // use native currencies if specified by the user. This is so that the Trade knows to wrap/unwrap. - if (tradeType == TradeType.EXACT_INPUT) { - const amountCurrency = CurrencyAmount.fromFractionalAmount( - tokenInCurrency, - amount.numerator, - amount.denominator - ); - const quoteCurrency = CurrencyAmount.fromFractionalAmount( - tokenOutCurrency, - quote.numerator, - quote.denominator - ); - - const routeV2SDK = new V2RouteRaw( - route.pairs, - amountCurrency.currency, - quoteCurrency.currency - ); - - return { - routev2: routeV2SDK, - inputAmount: amountCurrency, - outputAmount: quoteCurrency, - }; - } else { - const quoteCurrency = CurrencyAmount.fromFractionalAmount( - tokenInCurrency, - quote.numerator, - quote.denominator - ); - - const amountCurrency = CurrencyAmount.fromFractionalAmount( - tokenOutCurrency, - amount.numerator, - amount.denominator - ); - - const routeV2SDK = new V2RouteRaw( - route.pairs, - quoteCurrency.currency, - amountCurrency.currency - ); - - return { - routev2: routeV2SDK, - inputAmount: quoteCurrency, - outputAmount: amountCurrency, - }; - } - } - ); - - const trade = new Trade({ v2Routes, v3Routes, tradeType }); - - return trade; - } - - private buildSwapMethodParameters( - trade: Trade, - swapConfig: SwapOptions - ): MethodParameters { - const { recipient, slippageTolerance, deadline, inputTokenPermit } = - swapConfig; - return SwapRouter.swapCallParameters(trade, { - recipient, - slippageTolerance, - deadlineOrPreviousBlockhash: deadline, - inputTokenPermit, - }); - } - private async buildSwapAndAddMethodParameters( trade: Trade, swapAndAddOptions: SwapAndAddOptions, diff --git a/src/routers/alpha-router/functions/best-swap-route.ts b/src/routers/alpha-router/functions/best-swap-route.ts index e0e29cc48..bbda85b9c 100644 --- a/src/routers/alpha-router/functions/best-swap-route.ts +++ b/src/routers/alpha-router/functions/best-swap-route.ts @@ -450,7 +450,6 @@ export function getBestSwapRouteBy( Date.now() - postSplitNow, MetricLoggerUnit.Milliseconds ); - return { quote, quoteGasAdjusted, diff --git a/src/routers/alpha-router/gas-models/gas-model.ts b/src/routers/alpha-router/gas-models/gas-model.ts index 93a4e8839..013cc9394 100644 --- a/src/routers/alpha-router/gas-models/gas-model.ts +++ b/src/routers/alpha-router/gas-models/gas-model.ts @@ -31,6 +31,7 @@ import { WBTC_GÖRLI, } from '../../../providers/token-provider'; import { IV2PoolProvider } from '../../../providers/v2/pool-provider'; +import { IOptimismGasDataProvider } from '../../../providers/v3/gas-data-provider'; import { IV3PoolProvider } from '../../../providers/v3/pool-provider'; import { CurrencyAmount } from '../../../util/amounts'; import { ChainId } from '../../../util/chains'; @@ -98,7 +99,8 @@ export abstract class IV3GasModelFactory { chainId: number, gasPriceWei: BigNumber, poolProvider: IV3PoolProvider, - inTermsOfToken: Token + inTermsOfToken: Token, + optimismGasDataProvider?: IOptimismGasDataProvider ): Promise>; } diff --git a/src/routers/alpha-router/gas-models/v3/gas-costs.ts b/src/routers/alpha-router/gas-models/v3/gas-costs.ts new file mode 100644 index 000000000..d112199e2 --- /dev/null +++ b/src/routers/alpha-router/gas-models/v3/gas-costs.ts @@ -0,0 +1,60 @@ +import { BigNumber } from 'ethers'; +import { ChainId } from '../../../..'; + +//l2 execution fee on optimism is roughly the same as mainnet +export const BASE_SWAP_COST = (id: ChainId): BigNumber => { + switch (id) { + case ChainId.MAINNET: + case ChainId.ROPSTEN: + case ChainId.RINKEBY: + case ChainId.GÖRLI: + case ChainId.OPTIMISM: + case ChainId.OPTIMISTIC_KOVAN: + case ChainId.KOVAN: + return BigNumber.from(2000); + case ChainId.ARBITRUM_ONE: + case ChainId.ARBITRUM_RINKEBY: + return BigNumber.from(700000); + case ChainId.POLYGON: + case ChainId.POLYGON_MUMBAI: + return BigNumber.from(2000); + } +}; +export const COST_PER_INIT_TICK = (id: ChainId): BigNumber => { + switch (id) { + case ChainId.MAINNET: + case ChainId.ROPSTEN: + case ChainId.RINKEBY: + case ChainId.GÖRLI: + case ChainId.KOVAN: + return BigNumber.from(31000); + case ChainId.OPTIMISM: + case ChainId.OPTIMISTIC_KOVAN: + return BigNumber.from(31000); + case ChainId.ARBITRUM_ONE: + case ChainId.ARBITRUM_RINKEBY: + return BigNumber.from(54000); + case ChainId.POLYGON: + case ChainId.POLYGON_MUMBAI: + return BigNumber.from(31000); + } +}; + +export const COST_PER_HOP = (id: ChainId): BigNumber => { + switch (id) { + case ChainId.MAINNET: + case ChainId.ROPSTEN: + case ChainId.RINKEBY: + case ChainId.GÖRLI: + case ChainId.KOVAN: + case ChainId.OPTIMISM: + case ChainId.OPTIMISTIC_KOVAN: + return BigNumber.from(80000); + case ChainId.ARBITRUM_ONE: + case ChainId.ARBITRUM_RINKEBY: + return BigNumber.from(80000); + case ChainId.POLYGON: + case ChainId.POLYGON_MUMBAI: + return BigNumber.from(80000); + } +}; diff --git a/src/routers/alpha-router/gas-models/v3/v3-heuristic-gas-model.ts b/src/routers/alpha-router/gas-models/v3/v3-heuristic-gas-model.ts index ffedbb99e..433564ac9 100644 --- a/src/routers/alpha-router/gas-models/v3/v3-heuristic-gas-model.ts +++ b/src/routers/alpha-router/gas-models/v3/v3-heuristic-gas-model.ts @@ -1,31 +1,31 @@ import { BigNumber } from '@ethersproject/bignumber'; -import { Token } from '@uniswap/sdk-core'; +import { Percent, Token } from '@uniswap/sdk-core'; import { FeeAmount, Pool } from '@uniswap/v3-sdk'; import _ from 'lodash'; -import { WRAPPED_NATIVE_CURRENCY } from '../../../..'; +import { SwapOptions, WRAPPED_NATIVE_CURRENCY } from '../../../..'; +import { + IOptimismGasDataProvider, + OptimismGasData, +} from '../../../../providers/v3/gas-data-provider'; import { IV3PoolProvider } from '../../../../providers/v3/pool-provider'; import { ChainId } from '../../../../util'; import { CurrencyAmount } from '../../../../util/amounts'; import { log } from '../../../../util/log'; +import { + buildSwapMethodParameters, + buildTrade, +} from '../../../../util/methodParameters'; import { V3RouteWithValidQuote } from '../../entities/route-with-valid-quote'; import { IGasModel, IV3GasModelFactory, usdGasTokensByChain, } from '../gas-model'; - -// Constant cost for doing any swap regardless of pools. -const BASE_SWAP_COST = BigNumber.from(2000); - -// Cost for crossing an initialized tick. -const COST_PER_INIT_TICK = BigNumber.from(31000); +import { BASE_SWAP_COST, COST_PER_HOP, COST_PER_INIT_TICK } from './gas-costs'; // Cost for crossing an uninitialized tick. const COST_PER_UNINIT_TICK = BigNumber.from(0); -// Constant per pool swap in the route. -const COST_PER_HOP = BigNumber.from(80000); - /** * Computes a gas estimate for a V3 swap using heuristics. * Considers number of hops in the route, number of ticks crossed @@ -53,9 +53,14 @@ export class V3HeuristicGasModelFactory extends IV3GasModelFactory { chainId: ChainId, gasPriceWei: BigNumber, poolProvider: IV3PoolProvider, - token: Token + token: Token, + optimismGasDataProvider?: IOptimismGasDataProvider // this is the quoteToken ): Promise> { + const optimismGasData = optimismGasDataProvider + ? await optimismGasDataProvider.getGasData() + : undefined; + // If our quote token is WETH, we don't need to convert our gas use to be in terms // of the quote token in order to produce a gas adjusted amount. // We do return a gas use in USD however, so we still convert to usd. @@ -74,10 +79,11 @@ export class V3HeuristicGasModelFactory extends IV3GasModelFactory { gasCostInToken: CurrencyAmount; gasCostInUSD: CurrencyAmount; } => { - const { gasCostNativeCurrency, gasUse } = this.estimateGas( + const { totalGasCostNativeCurrency, totalGasUse } = this.estimateGas( routeWithValidQuote, gasPriceWei, - chainId + chainId, + optimismGasData ); const token0 = usdPool.token0.address == nativeCurrency.address; @@ -87,12 +93,12 @@ export class V3HeuristicGasModelFactory extends IV3GasModelFactory { : usdPool.token1Price; const gasCostInTermsOfUSD: CurrencyAmount = nativeTokenPrice.quote( - gasCostNativeCurrency + totalGasCostNativeCurrency ) as CurrencyAmount; return { - gasEstimate: gasUse, - gasCostInToken: gasCostNativeCurrency, + gasEstimate: totalGasUse, + gasCostInToken: totalGasCostNativeCurrency, gasCostInUSD: gasCostInTermsOfUSD, }; }; @@ -127,10 +133,11 @@ export class V3HeuristicGasModelFactory extends IV3GasModelFactory { gasCostInToken: CurrencyAmount; gasCostInUSD: CurrencyAmount; } => { - const { gasCostNativeCurrency, gasUse } = this.estimateGas( + const { totalGasCostNativeCurrency, totalGasUse } = this.estimateGas( routeWithValidQuote, gasPriceWei, - chainId + chainId, + optimismGasData ); if (!nativePool) { @@ -138,7 +145,7 @@ export class V3HeuristicGasModelFactory extends IV3GasModelFactory { `Unable to find ${nativeCurrency.symbol} pool with the quote token, ${token.symbol} to produce gas adjusted costs. Route will not account for gas.` ); return { - gasEstimate: gasUse, + gasEstimate: totalGasUse, gasCostInToken: CurrencyAmount.fromRawAmount(token, 0), gasCostInUSD: CurrencyAmount.fromRawAmount(usdToken, 0), }; @@ -155,14 +162,14 @@ export class V3HeuristicGasModelFactory extends IV3GasModelFactory { try { // native token is base currency gasCostInTermsOfQuoteToken = nativeTokenPrice.quote( - gasCostNativeCurrency + totalGasCostNativeCurrency ) as CurrencyAmount; } catch (err) { log.info( { nativeTokenPriceBase: nativeTokenPrice.baseCurrency, nativeTokenPriceQuote: nativeTokenPrice.quoteCurrency, - gasCostInEth: gasCostNativeCurrency.currency, + gasCostInEth: totalGasCostNativeCurrency.currency, }, 'Debug eth price token issue' ); @@ -180,14 +187,14 @@ export class V3HeuristicGasModelFactory extends IV3GasModelFactory { let gasCostInTermsOfUSD: CurrencyAmount; try { gasCostInTermsOfUSD = nativeTokenPriceUSDPool.quote( - gasCostNativeCurrency + totalGasCostNativeCurrency ) as CurrencyAmount; } catch (err) { log.info( { usdT1: usdPool.token0.symbol, usdT2: usdPool.token1.symbol, - gasCostInNativeToken: gasCostNativeCurrency.currency.symbol, + gasCostInNativeToken: totalGasCostNativeCurrency.currency.symbol, }, 'Failed to compute USD gas price' ); @@ -195,7 +202,7 @@ export class V3HeuristicGasModelFactory extends IV3GasModelFactory { } return { - gasEstimate: gasUse, + gasEstimate: totalGasUse, gasCostInToken: gasCostInTermsOfQuoteToken, gasCostInUSD: gasCostInTermsOfUSD!, }; @@ -209,32 +216,59 @@ export class V3HeuristicGasModelFactory extends IV3GasModelFactory { private estimateGas( routeWithValidQuote: V3RouteWithValidQuote, gasPriceWei: BigNumber, - chainId: ChainId + chainId: ChainId, + gasData?: OptimismGasData ) { - const totalInitializedTicksCrossed = Math.max( - 1, - _.sum(routeWithValidQuote.initializedTicksCrossedList) + const totalInitializedTicksCrossed = BigNumber.from( + Math.max(1, _.sum(routeWithValidQuote.initializedTicksCrossedList)) ); const totalHops = BigNumber.from(routeWithValidQuote.route.pools.length); - const hopsGasUse = COST_PER_HOP.mul(totalHops); - const tickGasUse = COST_PER_INIT_TICK.mul(totalInitializedTicksCrossed); + const hopsGasUse = COST_PER_HOP(chainId).mul(totalHops); + const tickGasUse = COST_PER_INIT_TICK(chainId).mul( + totalInitializedTicksCrossed + ); const uninitializedTickGasUse = COST_PER_UNINIT_TICK.mul(0); - const gasUse = BASE_SWAP_COST.add(hopsGasUse) + let l1GasUsed = BigNumber.from(0); + let l1Fee = BigNumber.from(0); + if (chainId == ChainId.OPTIMISM || chainId == ChainId.OPTIMISTIC_KOVAN) { + // account for the L1 security fee + // create dummy swapConfig to get calldata bytes + const swapConfig: SwapOptions = { + recipient: '0x0000000000000000000000000000000000000001', + deadline: 100, + slippageTolerance: new Percent(5, 10_000), + }; + [l1GasUsed, l1Fee] = this.calculateOptimismToL1SecurityFee( + routeWithValidQuote, + swapConfig, + gasData! + ); + } + const baseGasUse = BASE_SWAP_COST(chainId) + .add(hopsGasUse) .add(tickGasUse) .add(uninitializedTickGasUse); - const totalGasCostWei = gasPriceWei.mul(gasUse); + const baseGasCostWei = gasPriceWei.mul(baseGasUse); + + // total gas cost including l1 security fee if on optimism + const totalGasUse = l1GasUsed.add(baseGasUse); + const totalGasCostWei = l1Fee.add(baseGasCostWei); const wrappedCurrency = WRAPPED_NATIVE_CURRENCY[chainId]!; - const gasCostNativeCurrency = CurrencyAmount.fromRawAmount( + const totalGasCostNativeCurrency = CurrencyAmount.fromRawAmount( wrappedCurrency, totalGasCostWei.toString() ); - return { gasCostNativeCurrency, gasUse }; + return { + totalGasCostNativeCurrency, + totalInitializedTicksCrossed, + totalGasUse, + }; } private async getHighestLiquidityNativePool( @@ -341,4 +375,50 @@ export class V3HeuristicGasModelFactory extends IV3GasModelFactory { return maxPool; } + + /** + * To avoid having a call to optimism's L1 security fee contract for every route and amount combination, + * we replicate the gas cost accounting here. + */ + private calculateOptimismToL1SecurityFee( + route: V3RouteWithValidQuote, + swapConfig: SwapOptions, + gasData: OptimismGasData + ): [BigNumber, BigNumber] { + const { l1BaseFee, scalar, decimals, overhead } = gasData; + + // build trade for swap calldata + const trade = buildTrade( + route.amount.currency, + route.quote.currency, + route.tradeType, + [route] + ); + const data = buildSwapMethodParameters(trade, swapConfig).calldata; + const l1GasUsed = this.getOptimismToL1GasUsed(data, overhead); + const l1Fee = l1GasUsed.mul(l1BaseFee); + const unscaled = l1Fee.mul(scalar); + // scaled = unscaled / (10 ** decimals) + const scaledConversion = BigNumber.from(10).pow(decimals); + const scaled = unscaled.div(scaledConversion); + return [l1GasUsed, scaled]; + } + + private getOptimismToL1GasUsed(data: string, overhead: BigNumber): BigNumber { + // data is hex encoded + const dataArr: string[] = data.slice(2).match(/.{1,2}/g)!; + const numBytes = dataArr.length; + let count = 0; + for (let i = 0; i < numBytes; i += 1) { + const byte = parseInt(dataArr[i]!, 16); + if (byte == 0) { + count += 4; + } else { + count += 16; + } + } + const unsigned = overhead.add(count); + const signedConversion = 68 * 16; + return unsigned.add(signedConversion); + } } diff --git a/src/routers/router.ts b/src/routers/router.ts index c39b0f64f..9339f44d5 100644 --- a/src/routers/router.ts +++ b/src/routers/router.ts @@ -16,7 +16,6 @@ import { import { BigNumber } from 'ethers'; import { CurrencyAmount } from '../util/amounts'; import { RouteWithValidQuote } from './alpha-router'; - export class V3Route extends V3RouteRaw {} export class V2Route extends V2RouteRaw {} diff --git a/src/util/addresses.ts b/src/util/addresses.ts index 9b05ff37a..543366199 100644 --- a/src/util/addresses.ts +++ b/src/util/addresses.ts @@ -4,6 +4,8 @@ import { ChainId } from './chains'; export const V3_CORE_FACTORY_ADDRESS = FACTORY_ADDRESS; export const QUOTER_V2_ADDRESS = '0x61fFE014bA17989E743c5F6cB21bF9697530B21e'; +export const OVM_GASPRICE_ADDRESS = + '0x420000000000000000000000000000000000000F'; export const TICK_LENS_ADDRESS = '0xbfd8137f7d1516D3ea5cA83523914859ec47F573'; export const NONFUNGIBLE_POSITION_MANAGER_ADDRESS = '0xC36442b4a4522E871399CD717aBDD847Ab11FE88'; diff --git a/src/util/chains.ts b/src/util/chains.ts index c5c28a5b8..030556d22 100644 --- a/src/util/chains.ts +++ b/src/util/chains.ts @@ -13,6 +13,14 @@ export enum ChainId { POLYGON_MUMBAI = 80001, } +export const V2_SUPPORTED = [ + ChainId.MAINNET, + ChainId.KOVAN, + ChainId.GÖRLI, + ChainId.RINKEBY, + ChainId.ROPSTEN, +]; + export const ID_TO_CHAIN_ID = (id: number): ChainId => { switch (id) { case 1: diff --git a/src/util/methodParameters.ts b/src/util/methodParameters.ts new file mode 100644 index 000000000..75dcc89eb --- /dev/null +++ b/src/util/methodParameters.ts @@ -0,0 +1,173 @@ +import { Protocol, SwapRouter, Trade } from '@uniswap/router-sdk'; +import { Currency, TradeType } from '@uniswap/sdk-core'; +import { Route as V2RouteRaw } from '@uniswap/v2-sdk'; +import { MethodParameters, Route as V3RouteRaw } from '@uniswap/v3-sdk'; +import _ from 'lodash'; +import { + CurrencyAmount, + RouteWithValidQuote, + SwapOptions, + V2RouteWithValidQuote, + V3RouteWithValidQuote, +} from '..'; +export function buildTrade( + tokenInCurrency: Currency, + tokenOutCurrency: Currency, + tradeType: TTradeType, + routeAmounts: RouteWithValidQuote[] +): Trade { + const [v3RouteAmounts, v2RouteAmounts] = _.partition( + routeAmounts, + (routeAmount) => routeAmount.protocol == Protocol.V3 + ); + + const v3Routes = _.map< + V3RouteWithValidQuote, + { + routev3: V3RouteRaw; + inputAmount: CurrencyAmount; + outputAmount: CurrencyAmount; + } + >( + v3RouteAmounts as V3RouteWithValidQuote[], + (routeAmount: V3RouteWithValidQuote) => { + const { route, amount, quote } = routeAmount; + + // The route, amount and quote are all in terms of wrapped tokens. + // When constructing the Trade object the inputAmount/outputAmount must + // use native currencies if specified by the user. This is so that the Trade knows to wrap/unwrap. + if (tradeType == TradeType.EXACT_INPUT) { + const amountCurrency = CurrencyAmount.fromFractionalAmount( + tokenInCurrency, + amount.numerator, + amount.denominator + ); + const quoteCurrency = CurrencyAmount.fromFractionalAmount( + tokenOutCurrency, + quote.numerator, + quote.denominator + ); + + const routeRaw = new V3RouteRaw( + route.pools, + amountCurrency.currency, + quoteCurrency.currency + ); + + return { + routev3: routeRaw, + inputAmount: amountCurrency, + outputAmount: quoteCurrency, + }; + } else { + const quoteCurrency = CurrencyAmount.fromFractionalAmount( + tokenInCurrency, + quote.numerator, + quote.denominator + ); + + const amountCurrency = CurrencyAmount.fromFractionalAmount( + tokenOutCurrency, + amount.numerator, + amount.denominator + ); + + const routeCurrency = new V3RouteRaw( + route.pools, + quoteCurrency.currency, + amountCurrency.currency + ); + + return { + routev3: routeCurrency, + inputAmount: quoteCurrency, + outputAmount: amountCurrency, + }; + } + } + ); + + const v2Routes = _.map< + V2RouteWithValidQuote, + { + routev2: V2RouteRaw; + inputAmount: CurrencyAmount; + outputAmount: CurrencyAmount; + } + >( + v2RouteAmounts as V2RouteWithValidQuote[], + (routeAmount: V2RouteWithValidQuote) => { + const { route, amount, quote } = routeAmount; + + // The route, amount and quote are all in terms of wrapped tokens. + // When constructing the Trade object the inputAmount/outputAmount must + // use native currencies if specified by the user. This is so that the Trade knows to wrap/unwrap. + if (tradeType == TradeType.EXACT_INPUT) { + const amountCurrency = CurrencyAmount.fromFractionalAmount( + tokenInCurrency, + amount.numerator, + amount.denominator + ); + const quoteCurrency = CurrencyAmount.fromFractionalAmount( + tokenOutCurrency, + quote.numerator, + quote.denominator + ); + + const routeV2SDK = new V2RouteRaw( + route.pairs, + amountCurrency.currency, + quoteCurrency.currency + ); + + return { + routev2: routeV2SDK, + inputAmount: amountCurrency, + outputAmount: quoteCurrency, + }; + } else { + const quoteCurrency = CurrencyAmount.fromFractionalAmount( + tokenInCurrency, + quote.numerator, + quote.denominator + ); + + const amountCurrency = CurrencyAmount.fromFractionalAmount( + tokenOutCurrency, + amount.numerator, + amount.denominator + ); + + const routeV2SDK = new V2RouteRaw( + route.pairs, + quoteCurrency.currency, + amountCurrency.currency + ); + + return { + routev2: routeV2SDK, + inputAmount: quoteCurrency, + outputAmount: amountCurrency, + }; + } + } + ); + + const trade = new Trade({ v2Routes, v3Routes, tradeType }); + + return trade; +} + +export function buildSwapMethodParameters( + trade: Trade, + swapConfig: SwapOptions +): MethodParameters { + const { recipient, slippageTolerance, deadline, inputTokenPermit } = + swapConfig; + return SwapRouter.swapCallParameters(trade, { + recipient, + slippageTolerance, + deadlineOrPreviousBlockhash: deadline, + inputTokenPermit, + }); +}