diff --git a/src/providers/index.ts b/src/providers/index.ts index d62e3b0e2..8a37474e9 100644 --- a/src/providers/index.ts +++ b/src/providers/index.ts @@ -13,6 +13,7 @@ export * from './on-chain-gas-price-provider'; export * from './on-chain-quote-provider'; export * from './static-gas-price-provider'; export * from './swap-router-provider'; +export * from './tenderly-simulation-provider'; export * from './token-provider'; export * from './token-validator-provider'; export * from './uri-subgraph-provider'; diff --git a/src/providers/multicall-uniswap-provider.ts b/src/providers/multicall-uniswap-provider.ts index 40d165578..f27cc72fd 100644 --- a/src/providers/multicall-uniswap-provider.ts +++ b/src/providers/multicall-uniswap-provider.ts @@ -3,8 +3,8 @@ import { BaseProvider } from '@ethersproject/providers'; import _ from 'lodash'; import stats from 'stats-lite'; -import { UniswapInterfaceMulticall } from '../types/v3/UniswapInterfaceMulticall'; import { UniswapInterfaceMulticall__factory } from '../types/v3/factories/UniswapInterfaceMulticall__factory'; +import { UniswapInterfaceMulticall } from '../types/v3/UniswapInterfaceMulticall'; import { ChainId } from '../util'; import { UNISWAP_MULTICALL_ADDRESSES } from '../util/addresses'; import { log } from '../util/log'; diff --git a/src/providers/swap-router-provider.ts b/src/providers/swap-router-provider.ts index 4ff3c4530..936000372 100644 --- a/src/providers/swap-router-provider.ts +++ b/src/providers/swap-router-provider.ts @@ -2,7 +2,7 @@ import { ApprovalTypes } from '@uniswap/router-sdk'; import { Currency, CurrencyAmount } from '@uniswap/sdk-core'; import { SwapRouter02__factory } from '../types/other/factories/SwapRouter02__factory'; -import { log } from '../util'; +import { log, SWAP_ROUTER_ADDRESS } from '../util'; import { IMulticallProvider } from './multicall-provider'; @@ -11,8 +11,6 @@ type TokenApprovalTypes = { approvalTokenOut: ApprovalTypes; }; -const SWAP_ROUTER_ADDRESS = '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45'; - /** * Provider for accessing the SwapRouter02 Contract . * diff --git a/src/providers/tenderly-simulation-provider.ts b/src/providers/tenderly-simulation-provider.ts new file mode 100644 index 000000000..59e495846 --- /dev/null +++ b/src/providers/tenderly-simulation-provider.ts @@ -0,0 +1,329 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import { JsonRpcProvider } from '@ethersproject/providers'; +import axios from 'axios'; +import { BigNumber } from 'ethers/lib/ethers'; + +import { SwapRoute } from '../routers'; +import { Erc20__factory } from '../types/other/factories/Erc20__factory'; +import { SwapRouter02__factory } from '../types/other/factories/SwapRouter02__factory'; +import { ChainId, CurrencyAmount, log, SWAP_ROUTER_ADDRESS } from '../util'; +import { APPROVE_TOKEN_FOR_TRANSFER } from '../util/callData'; +import { + calculateGasUsed, + initSwapRouteFromExisting, +} from '../util/gas-factory-helpers'; + +import { IV2PoolProvider } from './v2/pool-provider'; +import { ArbitrumGasData, OptimismGasData } from './v3/gas-data-provider'; +import { IV3PoolProvider } from './v3/pool-provider'; + +type SimulationResult = { + transaction: { hash: string; gas_used: number; error_message: string }; + simulation: { state_overrides: Record }; +}; + +export type TenderlyResponse = { + config: { + url: string; + method: string; + data: string; + }; + simulation_results: [SimulationResult, SimulationResult]; +}; + +const TENDERLY_BATCH_SIMULATE_API = ( + tenderlyBaseUrl: string, + tenderlyUser: string, + tenderlyProject: string +) => + `${tenderlyBaseUrl}/api/v1/account/${tenderlyUser}/project/${tenderlyProject}/simulate-batch`; + +// We multiply tenderly gas estimate by this estimate to overestimate gas fee +const ESTIMATE_MULTIPLIER = 1.25; + +/** + * Provider for dry running transactions. + * + * @export + * @interface ISimulator + */ +export interface ISimulator { + /** + * Returns a new SwapRoute with updated gas estimates + * All clients that implement this interface must set + * simulationError = true in the returned SwapRoute + * if simulation is not successful + * @returns SwapRoute + */ + simulateTransaction: ( + fromAddress: string, + swapRoute: SwapRoute, + l2GasData?: OptimismGasData | ArbitrumGasData + ) => Promise; +} + +const checkTokenApproved = async ( + fromAddress: string, + inputAmount: CurrencyAmount, + provider: JsonRpcProvider +): Promise => { + const tokenContract = Erc20__factory.connect( + inputAmount.currency.wrapped.address, + provider + ); + const allowance = await tokenContract.allowance( + fromAddress, + SWAP_ROUTER_ADDRESS + ); + // Return true if token allowance is greater than input amount + return allowance.gt(BigNumber.from(inputAmount.quotient.toString())); +}; + +export class FallbackTenderlySimulator implements ISimulator { + private provider: JsonRpcProvider; + private tenderlySimulator: TenderlySimulator; + private v3PoolProvider: IV3PoolProvider; + private v2PoolProvider: IV2PoolProvider; + + constructor( + tenderlyBaseUrl: string, + tenderlyUser: string, + tenderlyProject: string, + tenderlyAccessKey: string, + provider: JsonRpcProvider, + v2PoolProvider: IV2PoolProvider, + v3PoolProvider: IV3PoolProvider, + tenderlySimulator?: TenderlySimulator + ) { + this.tenderlySimulator = + tenderlySimulator ?? + new TenderlySimulator( + tenderlyBaseUrl, + tenderlyUser, + tenderlyProject, + tenderlyAccessKey, + v2PoolProvider, + v3PoolProvider + ); + this.provider = provider; + this.v2PoolProvider = v2PoolProvider; + this.v3PoolProvider = v3PoolProvider; + } + + private async ethEstimateGas( + fromAddress: string, + route: SwapRoute, + l2GasData?: ArbitrumGasData | OptimismGasData + ): Promise { + const currencyIn = route.trade.inputAmount.currency; + const router = SwapRouter02__factory.connect( + SWAP_ROUTER_ADDRESS, + this.provider + ); + const estimatedGasUsed: BigNumber = await router.estimateGas[ + 'multicall(bytes[])' + ]([route.methodParameters!.calldata], { + from: fromAddress, + value: BigNumber.from( + currencyIn.isNative ? route.methodParameters!.value : '0' + ), + }); + const { + estimatedGasUsedUSD, + estimatedGasUsedQuoteToken, + quoteGasAdjusted, + } = await calculateGasUsed( + route.quote.currency.chainId, + route, + estimatedGasUsed, + this.v2PoolProvider, + this.v3PoolProvider, + l2GasData + ); + return initSwapRouteFromExisting( + route, + this.v2PoolProvider, + this.v3PoolProvider, + quoteGasAdjusted, + estimatedGasUsed, + estimatedGasUsedQuoteToken, + estimatedGasUsedUSD + ); + } + + public async simulateTransaction( + fromAddress: string, + swapRoute: SwapRoute, + l2GasData?: ArbitrumGasData | OptimismGasData + ): Promise { + // Make call to eth estimate gas if possible + // For erc20s, we must check if the token allowance is sufficient + const inputAmount = swapRoute.trade.inputAmount; + if ( + inputAmount.currency.isNative || + (await checkTokenApproved(fromAddress, inputAmount, this.provider)) + ) { + try { + const swapRouteWithGasEstimate = await this.ethEstimateGas( + fromAddress, + swapRoute, + l2GasData + ); + return swapRouteWithGasEstimate; + } catch (err) { + log.info({ err: err }, 'Error calling eth estimate gas!'); + return { ...swapRoute, simulationError: true }; + } + } + // simulate via tenderly + try { + return await this.tenderlySimulator.simulateTransaction( + fromAddress, + swapRoute, + l2GasData + ); + } catch (err) { + log.info({ err: err }, 'Failed to simulate via Tenderly!'); + // set error flag to true + return { ...swapRoute, simulationError: true }; + } + } +} +export class TenderlySimulator implements ISimulator { + private tenderlyBaseUrl: string; + private tenderlyUser: string; + private tenderlyProject: string; + private tenderlyAccessKey: string; + private v2PoolProvider: IV2PoolProvider; + private v3PoolProvider: IV3PoolProvider; + + constructor( + tenderlyBaseUrl: string, + tenderlyUser: string, + tenderlyProject: string, + tenderlyAccessKey: string, + v2PoolProvider: IV2PoolProvider, + v3PoolProvider: IV3PoolProvider + ) { + this.tenderlyBaseUrl = tenderlyBaseUrl; + this.tenderlyUser = tenderlyUser; + this.tenderlyProject = tenderlyProject; + this.tenderlyAccessKey = tenderlyAccessKey; + this.v2PoolProvider = v2PoolProvider; + this.v3PoolProvider = v3PoolProvider; + } + + public async simulateTransaction( + fromAddress: string, + swapRoute: SwapRoute, + l2GasData?: ArbitrumGasData | OptimismGasData + ): Promise { + const currencyIn = swapRoute.trade.inputAmount.currency; + const tokenIn = currencyIn.wrapped; + const chainId = tokenIn.chainId; + if ([ChainId.CELO, ChainId.CELO_ALFAJORES].includes(chainId)) { + const msg = 'Celo not supported by Tenderly!'; + log.info(msg); + return { ...swapRoute, simulationError: true }; + } + + if (!swapRoute.methodParameters) { + const msg = 'No calldata provided to simulate transaction' + log.info(msg) + throw new Error(msg); + } + const { calldata } = swapRoute.methodParameters; + log.info( + { + calldata: swapRoute.methodParameters.calldata, + fromAddress: fromAddress, + chainId: chainId, + tokenInAddress: tokenIn.address, + }, + 'Simulating transaction via Tenderly' + ); + + const approve = { + network_id: chainId, + input: APPROVE_TOKEN_FOR_TRANSFER, + to: tokenIn.address, + value: '0', + from: fromAddress, + gasPrice: '0', + gas: 30000000, + }; + + const swap = { + network_id: chainId, + input: calldata, + to: SWAP_ROUTER_ADDRESS, + value: currencyIn.isNative ? swapRoute.methodParameters.value : '0', + from: fromAddress, + gasPrice: '0', + gas: 30000000, + type: 1, + }; + + const body = { simulations: [approve, swap] }; + const opts = { + headers: { + 'X-Access-Key': this.tenderlyAccessKey, + }, + }; + const url = TENDERLY_BATCH_SIMULATE_API( + this.tenderlyBaseUrl, + this.tenderlyUser, + this.tenderlyProject + ); + const resp = (await axios.post(url, body, opts)).data; + + // Validate tenderly response body + if ( + !resp || + resp.simulation_results.length < 2 || + !resp.simulation_results[1].transaction || + resp.simulation_results[1].transaction.error_message + ) { + const msg = `Failed to Simulate Via Tenderly!: ${resp.simulation_results[1].transaction.error_message}`; + log.info( + { err: resp.simulation_results[1].transaction.error_message }, + msg + ); + return { ...swapRoute, simulationError: true }; + } + + log.info( + { approve: resp.simulation_results[0], swap: resp.simulation_results[1] }, + 'Simulated Approval + Swap via Tenderly' + ); + + // Parse the gas used in the simulation response object, and then pad it so that we overestimate. + const estimatedGasUsed = BigNumber.from( + ( + resp.simulation_results[1].transaction.gas_used * ESTIMATE_MULTIPLIER + ).toFixed(0) + ); + + const { + estimatedGasUsedUSD, + estimatedGasUsedQuoteToken, + quoteGasAdjusted, + } = await calculateGasUsed( + chainId, + swapRoute, + estimatedGasUsed, + this.v2PoolProvider, + this.v3PoolProvider, + l2GasData + ); + return initSwapRouteFromExisting( + swapRoute, + this.v2PoolProvider, + this.v3PoolProvider, + quoteGasAdjusted, + estimatedGasUsed, + estimatedGasUsedQuoteToken, + estimatedGasUsedUSD + ); + } +} diff --git a/src/routers/alpha-router/alpha-router.ts b/src/routers/alpha-router/alpha-router.ts index 7450dc203..0516c56e8 100644 --- a/src/routers/alpha-router/alpha-router.ts +++ b/src/routers/alpha-router/alpha-router.ts @@ -26,6 +26,7 @@ import { EIP1559GasPriceProvider, ETHGasStationInfoProvider, IOnChainQuoteProvider, + ISimulator, ISwapRouterProvider, IV2QuoteProvider, IV2SubgraphProvider, @@ -225,6 +226,11 @@ export type AlphaRouterParams = { * Calls the arbitrum gas data contract to fetch constants for calculating the l1 fee. */ arbitrumGasDataProvider?: IL2GasDataProvider; + + /** + * Simulates swaps and returns new SwapRoute with updated gas estimates + */ + simulator?: ISimulator; }; /** @@ -338,8 +344,8 @@ export class AlphaRouter protected v3PoolProvider: IV3PoolProvider; protected onChainQuoteProvider: IOnChainQuoteProvider; protected v2SubgraphProvider: IV2SubgraphProvider; - protected v2PoolProvider: IV2PoolProvider; protected v2QuoteProvider: IV2QuoteProvider; + protected v2PoolProvider: IV2PoolProvider; protected tokenProvider: ITokenProvider; protected gasPriceProvider: IGasPriceProvider; protected swapRouterProvider: ISwapRouterProvider; @@ -351,6 +357,7 @@ export class AlphaRouter protected l2GasDataProvider?: | IL2GasDataProvider | IL2GasDataProvider; + protected simulator?: ISimulator; constructor({ chainId, @@ -372,6 +379,7 @@ export class AlphaRouter optimismGasDataProvider, tokenValidatorProvider, arbitrumGasDataProvider, + simulator, }: AlphaRouterParams) { this.chainId = chainId; this.provider = provider; @@ -385,6 +393,7 @@ export class AlphaRouter new V3PoolProvider(ID_TO_CHAIN_ID(chainId), this.multicall2Provider), new NodeJSCache(new NodeCache({ stdTTL: 360, useClones: false })) ); + this.simulator = simulator; if (onChainQuoteProvider) { this.onChainQuoteProvider = onChainQuoteProvider; @@ -1071,7 +1080,7 @@ export class AlphaRouter this.emitPoolSelectionMetrics(swapRouteRaw, allCandidatePools); - return { + const swapRoute: SwapRoute = { quote, quoteGasAdjusted, estimatedGasUsed, @@ -1083,6 +1092,32 @@ export class AlphaRouter methodParameters, blockNumber: BigNumber.from(await blockNumber), }; + if ( + swapConfig && + swapConfig.simulate && + methodParameters && + methodParameters.calldata + ) { + if (!this.simulator) { + throw new Error('Simulator not initialized!'); + } + const beforeSimulate = Date.now(); + const swapRouteWithSimulation = await this.simulator.simulateTransaction( + swapConfig.simulate.fromAddress, + swapRoute, + this.l2GasDataProvider + ? await this.l2GasDataProvider!.getGasData() + : undefined + ); + metric.putMetric( + 'SimulateTransaction', + Date.now() - beforeSimulate, + MetricLoggerUnit.Milliseconds + ); + return swapRouteWithSimulation; + } + + return swapRoute; } private async applyTokenValidatorToPools( diff --git a/src/routers/alpha-router/gas-models/mixedRoute/mixed-route-heuristic-gas-model.ts b/src/routers/alpha-router/gas-models/mixedRoute/mixed-route-heuristic-gas-model.ts index 71baf2185..1157105d2 100644 --- a/src/routers/alpha-router/gas-models/mixedRoute/mixed-route-heuristic-gas-model.ts +++ b/src/routers/alpha-router/gas-models/mixedRoute/mixed-route-heuristic-gas-model.ts @@ -3,6 +3,7 @@ import { partitionMixedRouteByProtocol } from '@uniswap/router-sdk'; import { Pair } from '@uniswap/v2-sdk'; import { Pool } from '@uniswap/v3-sdk'; import _ from 'lodash'; + import { WRAPPED_NATIVE_CURRENCY } from '../../../..'; import { ChainId, log } from '../../../../util'; import { CurrencyAmount } from '../../../../util/amounts'; @@ -108,7 +109,6 @@ export class MixedRouteHeuristicGasModelFactory extends IOnChainGasModelFactory // If the quote token is not in the native currency, we convert the gas cost to be in terms of the quote token. // We do this by getting the highest liquidity / pool. eg. /ETH pool. const nativeV3Pool: Pool | null = await getHighestLiquidityV3NativePool( - chainId, token, V3poolProvider ); @@ -116,7 +116,7 @@ export class MixedRouteHeuristicGasModelFactory extends IOnChainGasModelFactory let nativeV2Pool: Pair | null; if (V2poolProvider) { /// MixedRoutes - nativeV2Pool = await getV2NativePool(chainId, token, V2poolProvider); + nativeV2Pool = await getV2NativePool(token, V2poolProvider); } const usdToken = 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 25c495d51..be2045f36 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 @@ -13,6 +13,7 @@ import { CurrencyAmount } from '../../../../util/amounts'; import { getHighestLiquidityV3NativePool, getHighestLiquidityV3USDPool, + getL2ToL1GasUsed, } from '../../../../util/gas-factory-helpers'; import { log } from '../../../../util/log'; import { @@ -125,7 +126,6 @@ export class V3HeuristicGasModelFactory extends IOnChainGasModelFactory { // if the inputted token is not in the native currency, quote a native/quote token pool to get the gas cost in terms of the quote token if (!token.equals(nativeCurrency)) { const nativePool: Pool | null = await getHighestLiquidityV3NativePool( - chainId, token, poolProvider ); @@ -195,7 +195,6 @@ export class V3HeuristicGasModelFactory extends IOnChainGasModelFactory { // If the quote token is not in the native currency, we convert the gas cost to be in terms of the quote token. // We do this by getting the highest liquidity / pool. eg. /ETH pool. const nativePool: Pool | null = await getHighestLiquidityV3NativePool( - chainId, token, poolProvider ); @@ -354,7 +353,7 @@ export class V3HeuristicGasModelFactory extends IOnChainGasModelFactory { // build trade for swap calldata const trade = buildTrade(inputToken, outputToken, route.tradeType, routes); const data = buildSwapMethodParameters(trade, swapConfig).calldata; - const l1GasUsed = this.getL2ToL1GasUsed(data, overhead); + const l1GasUsed = getL2ToL1GasUsed(data, overhead); // l1BaseFee is L1 Gas Price on etherscan const l1Fee = l1GasUsed.mul(l1BaseFee); const unscaled = l1Fee.mul(scalar); @@ -386,29 +385,10 @@ export class V3HeuristicGasModelFactory extends IOnChainGasModelFactory { const trade = buildTrade(inputToken, outputToken, route.tradeType, routes); const data = buildSwapMethodParameters(trade, swapConfig).calldata; // calculates gas amounts based on bytes of calldata, use 0 as overhead. - const l1GasUsed = this.getL2ToL1GasUsed(data, BigNumber.from(0)); + const l1GasUsed = getL2ToL1GasUsed(data, BigNumber.from(0)); // multiply by the fee per calldata and add the flat l2 fee let l1Fee = l1GasUsed.mul(perL1CalldataFee); l1Fee = l1Fee.add(perL2TxFee); return [l1GasUsed, l1Fee]; } - - // based on the code from the optimism OVM_GasPriceOracle contract - private getL2ToL1GasUsed(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 ad2c375d0..1fb8859d8 100644 --- a/src/routers/router.ts +++ b/src/routers/router.ts @@ -80,6 +80,10 @@ export type SwapRoute = { * The calldata to execute the swap. Only returned if swapConfig was provided when calling the router. */ methodParameters?: MethodParameters; + /** + * Flag that is true if and only if simulation is requested and simulation fails + */ + simulationError?: boolean; }; export type SwapToRatioRoute = SwapRoute & { @@ -116,6 +120,7 @@ export type SwapOptions = { recipient: string; slippageTolerance: Percent; deadline: number; + simulate?: { fromAddress: string }; inputTokenPermit?: { v: 0 | 1 | 27 | 28; r: string; diff --git a/src/util/addresses.ts b/src/util/addresses.ts index a4be68005..8ca34fb42 100644 --- a/src/util/addresses.ts +++ b/src/util/addresses.ts @@ -48,7 +48,7 @@ export const ARB_GASINFO_ADDRESS = '0x000000000000000000000000000000000000006C'; export const TICK_LENS_ADDRESS = '0xbfd8137f7d1516D3ea5cA83523914859ec47F573'; export const NONFUNGIBLE_POSITION_MANAGER_ADDRESS = '0xC36442b4a4522E871399CD717aBDD847Ab11FE88'; -export const SWAP_ROUTER_ADDRESS = '0xE592427A0AEce92De3Edee1F18E0157C05861564'; +export const SWAP_ROUTER_ADDRESS = '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45'; export const V3_MIGRATOR_ADDRESS = '0xA5644E29708357803b5A882D272c41cC0dF92B34'; export const MULTICALL2_ADDRESS = '0x5BA1e12693Dc8F9c48aAD8770482f4739bEeD696'; diff --git a/src/util/callData.ts b/src/util/callData.ts new file mode 100644 index 000000000..1986e5ea5 --- /dev/null +++ b/src/util/callData.ts @@ -0,0 +1,3 @@ +// Calldata to max-approve our V3 Router contract +export const APPROVE_TOKEN_FOR_TRANSFER = + '0x095ea7b300000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'; diff --git a/src/util/gas-factory-helpers.ts b/src/util/gas-factory-helpers.ts index d70784944..67e2a903d 100644 --- a/src/util/gas-factory-helpers.ts +++ b/src/util/gas-factory-helpers.ts @@ -1,19 +1,59 @@ -import { Token } from '@uniswap/sdk-core'; -import { Pair } from '@uniswap/v2-sdk'; -import { FeeAmount, Pool } from '@uniswap/v3-sdk'; +import { BigNumber } from '@ethersproject/bignumber'; +import { Protocol } from '@uniswap/router-sdk'; +import { Currency, CurrencyAmount, Token, TradeType } from '@uniswap/sdk-core'; +import { Pair } from '@uniswap/v2-sdk/dist/entities'; +import { FeeAmount, MethodParameters, Pool } from '@uniswap/v3-sdk'; import _ from 'lodash'; -import { ChainId, log, WRAPPED_NATIVE_CURRENCY } from '.'; import { IV2PoolProvider } from '../providers'; +import { + ArbitrumGasData, + OptimismGasData, +} from '../providers/v3/gas-data-provider'; import { IV3PoolProvider } from '../providers/v3/pool-provider'; -import { usdGasTokensByChain } from '../routers'; +import { + MixedRouteWithValidQuote, + SwapRoute, + usdGasTokensByChain, + V2RouteWithValidQuote, + V3RouteWithValidQuote, +} from '../routers'; +import { ChainId, log, WRAPPED_NATIVE_CURRENCY } from '../util'; + +import { buildTrade } from './methodParameters'; + +export async function getV2NativePool( + token: Token, + poolProvider: IV2PoolProvider +): Promise { + const chainId = token.chainId as ChainId; + const weth = WRAPPED_NATIVE_CURRENCY[chainId]!; + + const poolAccessor = await poolProvider.getPools([[weth, token]]); + const pool = poolAccessor.getPool(weth, token); + + if (!pool || pool.reserve0.equalTo(0) || pool.reserve1.equalTo(0)) { + log.error( + { + weth, + token, + reserve0: pool?.reserve0.toExact(), + reserve1: pool?.reserve1.toExact(), + }, + `Could not find a valid WETH pool with ${token.symbol} for computing gas costs.` + ); + + return null; + } + + return pool; +} export async function getHighestLiquidityV3NativePool( - chainId: ChainId, token: Token, poolProvider: IV3PoolProvider ): Promise { - const nativeCurrency = WRAPPED_NATIVE_CURRENCY[chainId]!; + const nativeCurrency = WRAPPED_NATIVE_CURRENCY[token.chainId as ChainId]!; const nativePools = _([FeeAmount.HIGH, FeeAmount.MEDIUM, FeeAmount.LOW]) .map<[Token, Token, FeeAmount]>((feeAmount) => { @@ -106,71 +146,286 @@ export async function getHighestLiquidityV3USDPool( return maxPool; } -export async function getV2NativePool( - chainId: ChainId, - token: Token, - poolProvider: IV2PoolProvider -): Promise { - const weth = WRAPPED_NATIVE_CURRENCY[chainId]!; +export function getGasCostInUSD( + usdPool: Pool, + costNativeCurrency: CurrencyAmount +) { + const nativeCurrency = costNativeCurrency.currency; + // convert fee into usd + const nativeTokenPrice = + usdPool.token0.address == nativeCurrency.address + ? usdPool.token0Price + : usdPool.token1Price; - const poolAccessor = await poolProvider.getPools([[weth, token]]); - const pool = poolAccessor.getPool(weth, token); + const gasCostUSD = nativeTokenPrice.quote(costNativeCurrency); + return gasCostUSD; +} - if (!pool || pool.reserve0.equalTo(0) || pool.reserve1.equalTo(0)) { - log.error( - { - weth, - token, - reserve0: pool?.reserve0.toExact(), - reserve1: pool?.reserve1.toExact(), - }, - `Could not find a valid WETH pool with ${token.symbol} for computing gas costs.` - ); +export function getGasCostInNativeCurrency( + nativeCurrency: Token, + gasCostInWei: BigNumber +) { + // wrap fee to native currency + const costNativeCurrency = CurrencyAmount.fromRawAmount( + nativeCurrency, + gasCostInWei.toString() + ); + return costNativeCurrency; +} - return null; - } +export async function getGasCostInQuoteToken( + quoteToken: Token, + nativePool: Pool | Pair, + costNativeCurrency: CurrencyAmount +) { + const nativeTokenPrice = + nativePool.token0.address == quoteToken.address + ? nativePool.token1Price + : nativePool.token0Price; + const gasCostQuoteToken = nativeTokenPrice.quote(costNativeCurrency); + return gasCostQuoteToken; +} - return pool; +export function calculateArbitrumToL1FeeFromCalldata( + calldata: string, + gasData: ArbitrumGasData +): [BigNumber, BigNumber] { + const { perL2TxFee, perL1CalldataFee } = gasData; + // calculates gas amounts based on bytes of calldata, use 0 as overhead. + const l1GasUsed = getL2ToL1GasUsed(calldata, BigNumber.from(0)); + // multiply by the fee per calldata and add the flat l2 fee + let l1Fee = l1GasUsed.mul(perL1CalldataFee); + l1Fee = l1Fee.add(perL2TxFee); + return [l1GasUsed, l1Fee]; } -export async function getHighestLiquidityUSDV2Pool( - chainId: ChainId, - poolProvider: IV2PoolProvider -): Promise { - const usdTokens = usdGasTokensByChain[chainId]; +export function calculateOptimismToL1FeeFromCalldata( + calldata: string, + gasData: OptimismGasData +): [BigNumber, BigNumber] { + const { l1BaseFee, scalar, decimals, overhead } = gasData; - if (!usdTokens) { - throw new Error( - `Could not find a USD token for computing gas costs on ${chainId}` - ); + const l1GasUsed = getL2ToL1GasUsed(calldata, overhead); + // l1BaseFee is L1 Gas Price on etherscan + 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]; +} + +// based on the code from the optimism OVM_GasPriceOracle contract +export function getL2ToL1GasUsed(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); +} - const usdPools = _.map(usdTokens, (usdToken) => [ - usdToken, - WRAPPED_NATIVE_CURRENCY[chainId]!, - ]); - const poolAccessor = await poolProvider.getPools(usdPools); - const poolsRaw = poolAccessor.getAllPools(); - const pools = _.filter( - poolsRaw, - (pool) => pool.reserve0.greaterThan(0) && pool.reserve1.greaterThan(0) +export async function calculateGasUsed( + chainId: ChainId, + route: SwapRoute, + simulatedGasUsed: BigNumber, + v2PoolProvider: IV2PoolProvider, + v3PoolProvider: IV3PoolProvider, + l2GasData?: ArbitrumGasData | OptimismGasData +) { + const quoteToken = route.quote.currency.wrapped; + const gasPriceWei = route.gasPriceWei; + // calculate L2 to L1 security fee if relevant + let l2toL1FeeInWei = BigNumber.from(0); + if ([ChainId.ARBITRUM_ONE, ChainId.ARBITRUM_RINKEBY].includes(chainId)) { + l2toL1FeeInWei = calculateArbitrumToL1FeeFromCalldata( + route.methodParameters!.calldata, + l2GasData as ArbitrumGasData + )[1]; + } else if ([ChainId.OPTIMISM, ChainId.OPTIMISTIC_KOVAN].includes(chainId)) { + l2toL1FeeInWei = calculateOptimismToL1FeeFromCalldata( + route.methodParameters!.calldata, + l2GasData as OptimismGasData + )[1]; + } + + // add l2 to l1 fee and wrap fee to native currency + const gasCostInWei = gasPriceWei.mul(simulatedGasUsed).add(l2toL1FeeInWei); + const nativeCurrency = WRAPPED_NATIVE_CURRENCY[chainId]; + const costNativeCurrency = getGasCostInNativeCurrency( + nativeCurrency, + gasCostInWei ); - if (pools.length == 0) { - log.error( - { pools }, - `Could not find a USD/WETH pool for computing gas costs.` - ); - throw new Error(`Can't find USD/WETH pool for computing gas costs.`); - } + const usdPool: Pool = await getHighestLiquidityV3USDPool( + chainId, + v3PoolProvider + ); - const maxPool = _.maxBy(pools, (pool) => { - if (pool.token0.equals(WRAPPED_NATIVE_CURRENCY[chainId]!)) { - return parseFloat(pool.reserve0.toSignificant(2)); + const gasCostUSD = await getGasCostInUSD(usdPool, costNativeCurrency); + + let gasCostQuoteToken = costNativeCurrency; + // get fee in terms of quote token + if (!quoteToken.equals(nativeCurrency)) { + const nativePools = await Promise.all([ + getHighestLiquidityV3NativePool(quoteToken, v3PoolProvider), + getV2NativePool(quoteToken, v2PoolProvider), + ]); + const nativePool = nativePools.find((pool) => pool !== null); + + if (!nativePool) { + log.info( + 'Could not find any V2 or V3 pools to convert the cost into the quote token' + ); + gasCostQuoteToken = CurrencyAmount.fromRawAmount(quoteToken, 0); } else { - return parseFloat(pool.reserve1.toSignificant(2)); + gasCostQuoteToken = await getGasCostInQuoteToken( + quoteToken, + nativePool, + costNativeCurrency + ); } - }) as Pair; + } - return maxPool; + // Adjust quote for gas fees + let quoteGasAdjusted; + if (route.trade.tradeType == TradeType.EXACT_OUTPUT) { + // Exact output - need more of tokenIn to get the desired amount of tokenOut + quoteGasAdjusted = route.quote.add(gasCostQuoteToken); + } else { + // Exact input - can get less of tokenOut due to fees + quoteGasAdjusted = route.quote.subtract(gasCostQuoteToken); + } + + return { + estimatedGasUsedUSD: gasCostUSD, + estimatedGasUsedQuoteToken: gasCostQuoteToken, + quoteGasAdjusted: quoteGasAdjusted, + }; +} + +export function initSwapRouteFromExisting( + swapRoute: SwapRoute, + v2PoolProvider: IV2PoolProvider, + v3PoolProvider: IV3PoolProvider, + quoteGasAdjusted: CurrencyAmount, + estimatedGasUsed: BigNumber, + estimatedGasUsedQuoteToken: CurrencyAmount, + estimatedGasUsedUSD: CurrencyAmount +) { + const currencyIn = swapRoute.trade.inputAmount.currency; + const currencyOut = swapRoute.trade.outputAmount.currency; + const tradeType = swapRoute.trade.tradeType.valueOf() + ? TradeType.EXACT_OUTPUT + : TradeType.EXACT_INPUT; + const routesWithValidQuote = swapRoute.route.map((route) => { + switch (route.protocol) { + case Protocol.V3: + return new V3RouteWithValidQuote({ + amount: CurrencyAmount.fromFractionalAmount( + route.amount.currency, + route.amount.numerator, + route.amount.denominator + ), + rawQuote: BigNumber.from(route.rawQuote), + sqrtPriceX96AfterList: route.sqrtPriceX96AfterList.map((num) => + BigNumber.from(num) + ), + initializedTicksCrossedList: [...route.initializedTicksCrossedList], + quoterGasEstimate: BigNumber.from(route.gasEstimate), + percent: route.percent, + route: route.route, + gasModel: route.gasModel, + quoteToken: new Token( + currencyIn.chainId, + route.quoteToken.address, + route.quoteToken.decimals, + route.quoteToken.symbol, + route.quoteToken.name + ), + tradeType: tradeType, + v3PoolProvider: v3PoolProvider, + }); + case Protocol.V2: + return new V2RouteWithValidQuote({ + amount: CurrencyAmount.fromFractionalAmount( + route.amount.currency, + route.amount.numerator, + route.amount.denominator + ), + rawQuote: BigNumber.from(route.rawQuote), + percent: route.percent, + route: route.route, + gasModel: route.gasModel, + quoteToken: new Token( + currencyIn.chainId, + route.quoteToken.address, + route.quoteToken.decimals, + route.quoteToken.symbol, + route.quoteToken.name + ), + tradeType: tradeType, + v2PoolProvider: v2PoolProvider, + }); + case Protocol.MIXED: + return new MixedRouteWithValidQuote({ + amount: CurrencyAmount.fromFractionalAmount( + route.amount.currency, + route.amount.numerator, + route.amount.denominator + ), + rawQuote: BigNumber.from(route.rawQuote), + sqrtPriceX96AfterList: route.sqrtPriceX96AfterList.map((num) => + BigNumber.from(num) + ), + initializedTicksCrossedList: [...route.initializedTicksCrossedList], + quoterGasEstimate: BigNumber.from(route.gasEstimate), + percent: route.percent, + route: route.route, + mixedRouteGasModel: route.gasModel, + v2PoolProvider, + quoteToken: new Token( + currencyIn.chainId, + route.quoteToken.address, + route.quoteToken.decimals, + route.quoteToken.symbol, + route.quoteToken.name + ), + tradeType: tradeType, + v3PoolProvider: v3PoolProvider, + }); + } + }); + const trade = buildTrade( + currencyIn, + currencyOut, + tradeType, + routesWithValidQuote + ); + return { + quote: swapRoute.quote, + quoteGasAdjusted, + estimatedGasUsed, + estimatedGasUsedQuoteToken, + estimatedGasUsedUSD, + gasPriceWei: BigNumber.from(swapRoute.gasPriceWei), + trade, + route: routesWithValidQuote, + blockNumber: BigNumber.from(swapRoute.blockNumber), + methodParameters: swapRoute.methodParameters + ? ({ + calldata: swapRoute.methodParameters.calldata, + value: swapRoute.methodParameters.value, + } as MethodParameters) + : undefined, + } as SwapRoute; } diff --git a/test/integ/routers/alpha-router/alpha-router.integration.test.ts b/test/integ/routers/alpha-router/alpha-router.integration.test.ts index 5facc8fa9..5d915d2ef 100644 --- a/test/integ/routers/alpha-router/alpha-router.integration.test.ts +++ b/test/integ/routers/alpha-router/alpha-router.integration.test.ts @@ -13,6 +13,7 @@ import { import { AlphaRouter, AlphaRouterConfig, + CachingV3PoolProvider, CEUR_CELO, CEUR_CELO_ALFAJORES, ChainId, @@ -20,11 +21,13 @@ import { CUSD_CELO_ALFAJORES, DAI_MAINNET, DAI_ON, + FallbackTenderlySimulator, ID_TO_NETWORK_NAME, ID_TO_PROVIDER, MixedRoute, nativeOnChain, NATIVE_CURRENCY, + NodeJSCache, OnChainQuoteProvider, parseAmount, SUPPORTED_CHAINS, @@ -35,14 +38,17 @@ import { USDC_MAINNET, USDC_ON, USDT_MAINNET, + V2PoolProvider, V2Route, V2_SUPPORTED, + V3PoolProvider, V3Route, WBTC_GNOSIS, WBTC_MOONBEAM, WETH9, WNATIVE_ON, } from '../../../../src'; +import { WHALES } from '../../../test-util/whales'; import 'jest-environment-hardhat'; @@ -61,6 +67,7 @@ import _ from 'lodash'; import { StaticGasPriceProvider } from '../../../../src/providers/static-gas-price-provider'; import { DEFAULT_ROUTING_CONFIG_BY_CHAIN } from '../../../../src/routers/alpha-router/config'; import { getBalanceAndApprove } from '../../../test-util/getBalanceAndApprove'; +import NodeCache from 'node-cache'; const SWAP_ROUTER_V2 = '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45'; const SLIPPAGE = new Percent(5, 100); // 5% or 10_000? @@ -116,7 +123,8 @@ describe('alpha router integration', () => { const executeSwap = async ( methodParameters: MethodParameters, tokenIn: Currency, - tokenOut: Currency + tokenOut: Currency, + gasLimit?: BigNumber, ): Promise<{ tokenInAfter: CurrencyAmount; tokenInBefore: CurrencyAmount; @@ -142,8 +150,13 @@ describe('alpha router integration', () => { type: 1, }; - const transactionResponse: providers.TransactionResponse = - await alice.sendTransaction(transaction); + let transactionResponse: providers.TransactionResponse + if(gasLimit) { + transactionResponse = await alice.sendTransaction({...transaction, gasLimit: gasLimit}); + } else { + transactionResponse = await alice.sendTransaction(transaction) + } + const receipt = await transactionResponse.wait(); expect(receipt.status == 1).toBe(true); // Check for txn success @@ -228,12 +241,12 @@ describe('alpha router integration', () => { methodParameters: MethodParameters | undefined, tradeType: TradeType, checkTokenInAmount?: number, - checkTokenOutAmount?: number + checkTokenOutAmount?: number, + estimatedGasUsed?: BigNumber, ) => { expect(methodParameters).not.toBeUndefined(); const { tokenInBefore, tokenInAfter, tokenOutBefore, tokenOutAfter } = - await executeSwap(methodParameters!, tokenIn, tokenOut!); - + await executeSwap(methodParameters!, tokenIn, tokenOut!, estimatedGasUsed); if (tradeType == TradeType.EXACT_INPUT) { if (checkTokenInAmount) { expect( @@ -332,10 +345,21 @@ describe('alpha router integration', () => { ); expect(aliceUNIBalance).toEqual(parseAmount('1000', UNI_MAINNET)); + const v3PoolProvider = new CachingV3PoolProvider( + ChainId.MAINNET, + new V3PoolProvider(ChainId.MAINNET, multicall2Provider), + new NodeJSCache(new NodeCache({ stdTTL: 360, useClones: false })) + ); + const v2PoolProvider = new V2PoolProvider(ChainId.MAINNET, multicall2Provider); + + const simulator = new FallbackTenderlySimulator(process.env.TENDERLY_BASE_URL!, process.env.TENDERLY_USER!, process.env.TENDERLY_PROJECT!, process.env.TENDERLY_ACCESS_KEY!, hardhat.providers[0]!,v2PoolProvider, v3PoolProvider) alphaRouter = new AlphaRouter({ chainId: ChainId.MAINNET, provider: hardhat.providers[0]!, multicall2Provider, + v2PoolProvider, + v3PoolProvider, + simulator }); }); @@ -344,7 +368,7 @@ describe('alpha router integration', () => { */ for (const tradeType of [TradeType.EXACT_INPUT, TradeType.EXACT_OUTPUT]) { describe(`${ID_TO_NETWORK_NAME(1)} alpha - ${tradeType}`, () => { - describe(`+ simulate swap`, () => { + describe(`+ Execute on Hardhat Fork`, () => { it('erc20 -> erc20', async () => { // declaring these to reduce confusion const tokenIn = USDC_MAINNET; @@ -795,6 +819,350 @@ describe('alpha router integration', () => { ); }); }); + describe(`+ Simulate on Tenderly + Execute on Hardhat fork`, () => { + it('erc20 -> erc20', async () => { + // declaring these to reduce confusion + const tokenIn = USDC_MAINNET; + const tokenOut = USDT_MAINNET; + const amount = + tradeType == TradeType.EXACT_INPUT + ? parseAmount('100', tokenIn) + : parseAmount('100', tokenOut); + + const swap = await alphaRouter.route( + amount, + getQuoteToken(tokenIn, tokenOut, tradeType), + tradeType, + { + recipient: alice._address, + slippageTolerance: SLIPPAGE, + deadline: parseDeadline(360), + simulate: {fromAddress: WHALES(tokenIn)} + }, + { + ...ROUTING_CONFIG, + } + ); + + expect(swap).toBeDefined(); + expect(swap).not.toBeNull(); + + const { quote, quoteGasAdjusted, methodParameters } = swap!; + + await validateSwapRoute(quote, quoteGasAdjusted, tradeType, 100, 10) + + await validateExecuteSwap( + quote, + tokenIn, + tokenOut, + methodParameters, + tradeType, + 100, + 100 + ); + }); + + it(`erc20 -> eth large trade`, async () => { + // Trade of this size almost always results in splits. + const tokenIn = USDC_MAINNET; + const tokenOut = Ether.onChain(1) as Currency; + const amount = + tradeType == TradeType.EXACT_INPUT + ? parseAmount('1000000', tokenIn) + : parseAmount('100', tokenOut); + + const swap = await alphaRouter.route( + amount, + getQuoteToken(tokenIn, tokenOut, tradeType), + tradeType, + { + recipient: alice._address, + slippageTolerance: SLIPPAGE, + deadline: parseDeadline(360), + simulate: {fromAddress: WHALES(tokenIn)} + }, + { + ...ROUTING_CONFIG, + } + ); + expect(swap).toBeDefined(); + expect(swap).not.toBeNull(); + + const { quote, quoteGasAdjusted, methodParameters, estimatedGasUsed, simulationError, estimatedGasUsedQuoteToken } = swap!; + + expect(quoteGasAdjusted.subtract(quote).equalTo(estimatedGasUsedQuoteToken)) + + // Expect tenderly simulation to be successful + expect(simulationError).toBeUndefined(); + + await validateExecuteSwap( + quote, + tokenIn, + tokenOut, + methodParameters, + tradeType, + 1000000, + undefined, + estimatedGasUsed + ); + }); + + it(`eth -> erc20`, async () => { + /// Fails for v3 for some reason, ProviderGasError + const tokenIn = Ether.onChain(1) as Currency; + const tokenOut = UNI_MAINNET; + const amount = + tradeType == TradeType.EXACT_INPUT + ? parseAmount('10', tokenIn) + : parseAmount('10000', tokenOut); + + const swap = await alphaRouter.route( + amount, + getQuoteToken(tokenIn, tokenOut, tradeType), + tradeType, + { + recipient: alice._address, + slippageTolerance: SLIPPAGE, + deadline: parseDeadline(360), + simulate: {fromAddress: WHALES(tokenIn)} + }, + { + ...ROUTING_CONFIG, + protocols: [Protocol.V2], + } + ); + expect(swap).toBeDefined(); + expect(swap).not.toBeNull(); + + const { quote, quoteGasAdjusted, simulationError, estimatedGasUsedQuoteToken } = swap!; + expect(quoteGasAdjusted.subtract(quote).equalTo(estimatedGasUsedQuoteToken)) + + // Expect tenderly simulation to be successful + expect(simulationError).toBeUndefined(); + }); + + it(`weth -> erc20`, async () => { + const tokenIn = WETH9[1]; + const tokenOut = DAI_MAINNET; + const amount = + tradeType == TradeType.EXACT_INPUT + ? parseAmount('100', tokenIn) + : parseAmount('100', tokenOut); + + const swap = await alphaRouter.route( + amount, + getQuoteToken(tokenIn, tokenOut, tradeType), + tradeType, + { + recipient: alice._address, + slippageTolerance: SLIPPAGE, + deadline: parseDeadline(360), + simulate: {fromAddress: WHALES(tokenIn)} + }, + { + ...ROUTING_CONFIG, + } + ); + expect(swap).toBeDefined(); + expect(swap).not.toBeNull(); + + const { quote, quoteGasAdjusted, methodParameters, estimatedGasUsed, simulationError, estimatedGasUsedQuoteToken } = swap!; + + expect(quoteGasAdjusted.subtract(quote).equalTo(estimatedGasUsedQuoteToken)) + + // Expect tenderly simulation to be successful + expect(simulationError).toBeUndefined(); + + await validateExecuteSwap( + quote, + tokenIn, + tokenOut, + methodParameters, + tradeType, + 100, + 100, + estimatedGasUsed + ); + }); + + it(`erc20 -> weth`, async () => { + const tokenIn = USDC_MAINNET; + const tokenOut = WETH9[1]; + const amount = + tradeType == TradeType.EXACT_INPUT + ? parseAmount('100', tokenIn) + : parseAmount('100', tokenOut); + + const swap = await alphaRouter.route( + amount, + getQuoteToken(tokenIn, tokenOut, tradeType), + tradeType, + { + recipient: alice._address, + slippageTolerance: SLIPPAGE, + deadline: parseDeadline(360), + simulate: {fromAddress: WHALES(tokenIn)} + }, + { + ...ROUTING_CONFIG, + } + ); + expect(swap).toBeDefined(); + expect(swap).not.toBeNull(); + + const { quote, quoteGasAdjusted, methodParameters, estimatedGasUsed, simulationError, estimatedGasUsedQuoteToken } = swap!; + + expect(quoteGasAdjusted.subtract(quote).equalTo(estimatedGasUsedQuoteToken)) + + // Expect tenderly simulation to be successful + expect(simulationError).toBeUndefined(); + + await validateExecuteSwap( + quote, + tokenIn, + tokenOut, + methodParameters, + tradeType, + 100, + 100, + estimatedGasUsed + ); + }); + + it('erc20 -> erc20 v3 only', async () => { + const tokenIn = USDC_MAINNET; + const tokenOut = USDT_MAINNET; + const amount = + tradeType == TradeType.EXACT_INPUT + ? parseAmount('100', tokenIn) + : parseAmount('100', tokenOut); + + const swap = await alphaRouter.route( + amount, + getQuoteToken(tokenIn, tokenOut, tradeType), + tradeType, + { + recipient: alice._address, + slippageTolerance: SLIPPAGE, + deadline: parseDeadline(360), + simulate: {fromAddress: WHALES(tokenIn)} + }, + { + ...ROUTING_CONFIG, + protocols: [Protocol.V3], + } + ); + expect(swap).toBeDefined(); + expect(swap).not.toBeNull(); + + const { quote, quoteGasAdjusted, methodParameters, estimatedGasUsed, simulationError, estimatedGasUsedQuoteToken } = swap!; + expect(quoteGasAdjusted.subtract(quote).equalTo(estimatedGasUsedQuoteToken)) + + // Expect tenderly simulation to be successful + expect(simulationError).toBeUndefined(); + + await validateExecuteSwap( + quote, + tokenIn, + tokenOut, + methodParameters, + tradeType, + 100, + 100, + estimatedGasUsed + ); + }); + + it('erc20 -> erc20 v2 only', async () => { + const tokenIn = USDC_MAINNET; + const tokenOut = USDT_MAINNET; + const amount = + tradeType == TradeType.EXACT_INPUT + ? parseAmount('100', tokenIn) + : parseAmount('100', tokenOut); + + const swap = await alphaRouter.route( + amount, + getQuoteToken(tokenIn, tokenOut, tradeType), + tradeType, + { + recipient: alice._address, + slippageTolerance: SLIPPAGE, + deadline: parseDeadline(360), + simulate: {fromAddress: WHALES(tokenIn)} + }, + { + ...ROUTING_CONFIG, + protocols: [Protocol.V2], + } + ); + expect(swap).toBeDefined(); + expect(swap).not.toBeNull(); + + const { quote, quoteGasAdjusted, methodParameters, estimatedGasUsed, simulationError, estimatedGasUsedQuoteToken } = swap!; + + expect(quoteGasAdjusted.subtract(quote).equalTo(estimatedGasUsedQuoteToken)) + + // Expect tenderly simulation to be successful + expect(simulationError).toBeUndefined(); + + await validateExecuteSwap( + quote, + tokenIn, + tokenOut, + methodParameters, + tradeType, + 100, + 100, + estimatedGasUsed + ); + }); + + it('erc20 -> erc20 forceCrossProtocol', async () => { + const tokenIn = USDC_MAINNET; + const tokenOut = USDT_MAINNET; + const amount = + tradeType == TradeType.EXACT_INPUT + ? parseAmount('100', tokenIn) + : parseAmount('100', tokenOut); + + const swap = await alphaRouter.route( + amount, + getQuoteToken(tokenIn, tokenOut, tradeType), + tradeType, + { + recipient: alice._address, + slippageTolerance: SLIPPAGE, + deadline: parseDeadline(360), + simulate: {fromAddress: WHALES(tokenIn)} + }, + { + ...ROUTING_CONFIG, + forceCrossProtocol: true, + } + ); + expect(swap).toBeDefined(); + expect(swap).not.toBeNull(); + + const { quote, quoteGasAdjusted, methodParameters, estimatedGasUsed, simulationError, estimatedGasUsedQuoteToken } = swap!; + + expect(quoteGasAdjusted.subtract(quote).equalTo(estimatedGasUsedQuoteToken)) + + // Expect tenderly simulation to be successful + expect(simulationError).toBeUndefined(); + + await validateExecuteSwap( + quote, + tokenIn, + tokenOut, + methodParameters, + tradeType, + 100, + 100, + estimatedGasUsed + ); + }); + }); it(`erc20 -> erc20 no recipient/deadline/slippage`, async () => { const tokenIn = USDC_MAINNET; @@ -1113,6 +1481,8 @@ describe('quote for other networks', () => { for (const chain of _.filter( SUPPORTED_CHAINS, (c) => + c != ChainId.RINKEBY && + c != ChainId.ROPSTEN && c != ChainId.OPTIMISTIC_KOVAN && c != ChainId.POLYGON_MUMBAI && c != ChainId.ARBITRUM_RINKEBY && @@ -1138,175 +1508,313 @@ describe('quote for other networks', () => { chain, provider ); + const v3PoolProvider = new CachingV3PoolProvider( + ChainId.MAINNET, + new V3PoolProvider(ChainId.MAINNET, multicall2Provider), + new NodeJSCache(new NodeCache({ stdTTL: 360, useClones: false })) + ); + const v2PoolProvider = new V2PoolProvider(ChainId.MAINNET, multicall2Provider); + + const simulator = new FallbackTenderlySimulator(process.env.TENDERLY_BASE_URL!, process.env.TENDERLY_USER!, process.env.TENDERLY_PROJECT!, process.env.TENDERLY_ACCESS_KEY!, provider, v2PoolProvider, v3PoolProvider) + alphaRouter = new AlphaRouter({ + chainId: ChainId.MAINNET, + provider: provider, + multicall2Provider, + v2PoolProvider, + v3PoolProvider, + simulator + }); beforeAll(async () => { alphaRouter = new AlphaRouter({ chainId: chain, provider, multicall2Provider, + simulator }); }); - it(`${wrappedNative.symbol} -> erc20`, async () => { - const tokenIn = wrappedNative; - const tokenOut = erc1; - const amount = - tradeType == TradeType.EXACT_INPUT - ? parseAmount('10', tokenIn) - : parseAmount('10', tokenOut); + describe(`Swap`, function () { + it(`${wrappedNative.symbol} -> erc20`, async () => { + const tokenIn = wrappedNative; + const tokenOut = erc1; + const amount = + tradeType == TradeType.EXACT_INPUT + ? parseAmount('10', tokenIn) + : parseAmount('10', tokenOut); - const swap = await alphaRouter.route( - amount, - getQuoteToken(tokenIn, tokenOut, tradeType), - tradeType, - undefined, - { - // @ts-ignore[TS7053] - complaining about switch being non exhaustive - ...DEFAULT_ROUTING_CONFIG_BY_CHAIN[chain], - protocols: [Protocol.V3, Protocol.V2], - } - ); - expect(swap).toBeDefined(); - expect(swap).not.toBeNull(); + const swap = await alphaRouter.route( + amount, + getQuoteToken(tokenIn, tokenOut, tradeType), + tradeType, + undefined, + { + // @ts-ignore[TS7053] - complaining about switch being non exhaustive + ...DEFAULT_ROUTING_CONFIG_BY_CHAIN[chain], + protocols: [Protocol.V3, Protocol.V2], + } + ); + expect(swap).toBeDefined(); + expect(swap).not.toBeNull(); - // Scope limited for non mainnet network tests to validating the swap - }); + // Scope limited for non mainnet network tests to validating the swap + }); - it(`erc20 -> erc20`, async () => { - const tokenIn = erc1; - const tokenOut = erc2; - const amount = - tradeType == TradeType.EXACT_INPUT - ? parseAmount('1', tokenIn) - : parseAmount('1', tokenOut); + it(`erc20 -> erc20`, async () => { + const tokenIn = erc1; + const tokenOut = erc2; + const amount = + tradeType == TradeType.EXACT_INPUT + ? parseAmount('1', tokenIn) + : parseAmount('1', tokenOut); - const swap = await alphaRouter.route( - amount, - getQuoteToken(tokenIn, tokenOut, tradeType), - tradeType, - undefined, - { - // @ts-ignore[TS7053] - complaining about switch being non exhaustive - ...DEFAULT_ROUTING_CONFIG_BY_CHAIN[chain], - protocols: [Protocol.V3, Protocol.V2], - } - ); - expect(swap).toBeDefined(); - expect(swap).not.toBeNull(); - }); + const swap = await alphaRouter.route( + amount, + getQuoteToken(tokenIn, tokenOut, tradeType), + tradeType, + undefined, + { + // @ts-ignore[TS7053] - complaining about switch being non exhaustive + ...DEFAULT_ROUTING_CONFIG_BY_CHAIN[chain], + protocols: [Protocol.V3, Protocol.V2], + } + ); + expect(swap).toBeDefined(); + expect(swap).not.toBeNull(); + }); - const native = NATIVE_CURRENCY[chain]; + const native = NATIVE_CURRENCY[chain]; - it(`${native} -> erc20`, async () => { - const tokenIn = nativeOnChain(chain); - const tokenOut = erc2; + it(`${native} -> erc20`, async () => { + const tokenIn = nativeOnChain(chain); + const tokenOut = erc2; - // Celo currently has low liquidity and will not be able to find route for - // large input amounts - // TODO: Simplify this when Celo has more liquidity - const amount = - chain == ChainId.CELO || chain == ChainId.CELO_ALFAJORES - ? tradeType == TradeType.EXACT_INPUT - ? parseAmount('10', tokenIn) - : parseAmount('10', tokenOut) - : tradeType == TradeType.EXACT_INPUT - ? parseAmount('100', tokenIn) - : parseAmount('100', tokenOut); + // Celo currently has low liquidity and will not be able to find route for + // large input amounts + // TODO: Simplify this when Celo has more liquidity + const amount = + chain == ChainId.CELO || chain == ChainId.CELO_ALFAJORES + ? tradeType == TradeType.EXACT_INPUT + ? parseAmount('10', tokenIn) + : parseAmount('10', tokenOut) + : tradeType == TradeType.EXACT_INPUT + ? parseAmount('100', tokenIn) + : parseAmount('100', tokenOut); - const swap = await alphaRouter.route( - amount, - getQuoteToken(tokenIn, tokenOut, tradeType), - tradeType, - undefined, - { - // @ts-ignore[TS7053] - complaining about switch being non exhaustive - ...DEFAULT_ROUTING_CONFIG_BY_CHAIN[chain], - protocols: [Protocol.V3, Protocol.V2], - } - ); - expect(swap).toBeDefined(); - expect(swap).not.toBeNull(); - }); + const swap = await alphaRouter.route( + amount, + getQuoteToken(tokenIn, tokenOut, tradeType), + tradeType, + undefined, + { + // @ts-ignore[TS7053] - complaining about switch being non exhaustive + ...DEFAULT_ROUTING_CONFIG_BY_CHAIN[chain], + protocols: [Protocol.V3, Protocol.V2], + } + ); + expect(swap).toBeDefined(); + expect(swap).not.toBeNull(); + }); - it(`has quoteGasAdjusted values`, async () => { - const tokenIn = erc1; - const tokenOut = erc2; - const amount = - tradeType == TradeType.EXACT_INPUT - ? parseAmount('1', tokenIn) - : parseAmount('1', tokenOut); + it(`has quoteGasAdjusted values`, async () => { + const tokenIn = erc1; + const tokenOut = erc2; + const amount = + tradeType == TradeType.EXACT_INPUT + ? parseAmount('1', tokenIn) + : parseAmount('1', tokenOut); - const swap = await alphaRouter.route( - amount, - getQuoteToken(tokenIn, tokenOut, tradeType), - tradeType, - undefined, - { - // @ts-ignore[TS7053] - complaining about switch being non exhaustive - ...DEFAULT_ROUTING_CONFIG_BY_CHAIN[chain], - protocols: [Protocol.V3, Protocol.V2], + const swap = await alphaRouter.route( + amount, + getQuoteToken(tokenIn, tokenOut, tradeType), + tradeType, + undefined, + { + // @ts-ignore[TS7053] - complaining about switch being non exhaustive + ...DEFAULT_ROUTING_CONFIG_BY_CHAIN[chain], + protocols: [Protocol.V3, Protocol.V2], + } + ); + expect(swap).toBeDefined(); + expect(swap).not.toBeNull(); + + const { quote, quoteGasAdjusted } = swap!; + + if (tradeType == TradeType.EXACT_INPUT) { + // === .lessThanOrEqualTo + expect(!quoteGasAdjusted.greaterThan(quote)).toBe(true); + } else { + // === .greaterThanOrEqualTo + expect(!quoteGasAdjusted.lessThan(quote)).toBe(true); } - ); - expect(swap).toBeDefined(); - expect(swap).not.toBeNull(); + }); - const { quote, quoteGasAdjusted } = swap!; + it(`does not error when protocols array is empty`, async () => { + const tokenIn = erc1; + const tokenOut = erc2; + const amount = + tradeType == TradeType.EXACT_INPUT + ? parseAmount('1', tokenIn) + : parseAmount('1', tokenOut); - if (tradeType == TradeType.EXACT_INPUT) { - // === .lessThanOrEqualTo - expect(!quoteGasAdjusted.greaterThan(quote)).toBe(true); - } else { - // === .greaterThanOrEqualTo - expect(!quoteGasAdjusted.lessThan(quote)).toBe(true); + const swap = await alphaRouter.route( + amount, + getQuoteToken(tokenIn, tokenOut, tradeType), + tradeType, + undefined, + { + // @ts-ignore[TS7053] - complaining about switch being non exhaustive + ...DEFAULT_ROUTING_CONFIG_BY_CHAIN[chain], + protocols: [], + } + ); + expect(swap).toBeDefined(); + expect(swap).not.toBeNull(); + }); + + if (!V2_SUPPORTED.includes(chain)) { + it(`is null when considering MIXED on non supported chains for exactInput & exactOutput`, async () => { + const tokenIn = erc1; + const tokenOut = erc2; + const amount = + tradeType == TradeType.EXACT_INPUT + ? parseAmount('1', tokenIn) + : parseAmount('1', tokenOut); + + const swap = await alphaRouter.route( + amount, + getQuoteToken(tokenIn, tokenOut, tradeType), + tradeType, + undefined, + { + // @ts-ignore[TS7053] - complaining about switch being non exhaustive + ...DEFAULT_ROUTING_CONFIG_BY_CHAIN[chain], + protocols: [Protocol.MIXED], + } + ); + expect(swap).toBeNull(); + }); } }); + describe(`Simulate + Swap`, function () { + // Tenderly does not support Celo + if([ChainId.CELO, ChainId.CELO_ALFAJORES].includes(chain)) { + return + } + it(`${wrappedNative.symbol} -> erc20`, async () => { + const tokenIn = wrappedNative; + const tokenOut = erc1; + const amount = + tradeType == TradeType.EXACT_INPUT + ? parseAmount('10', tokenIn) + : parseAmount('10', tokenOut); + + const swap = await alphaRouter.route( + amount, + getQuoteToken(tokenIn, tokenOut, tradeType), + tradeType, + { + recipient: WHALES(tokenIn), + slippageTolerance: SLIPPAGE, + deadline: parseDeadline(360), + simulate: {fromAddress: WHALES(tokenIn)} + }, + { + // @ts-ignore[TS7053] - complaining about switch being non exhaustive + ...DEFAULT_ROUTING_CONFIG_BY_CHAIN[chain], + protocols: [Protocol.V3, Protocol.V2], + } + ); + expect(swap).toBeDefined(); + expect(swap).not.toBeNull(); + if(swap) { + expect(swap.quoteGasAdjusted.subtract(swap.quote).equalTo(swap.estimatedGasUsedQuoteToken)) - it(`does not error when protocols array is empty`, async () => { - const tokenIn = erc1; - const tokenOut = erc2; - const amount = - tradeType == TradeType.EXACT_INPUT - ? parseAmount('1', tokenIn) - : parseAmount('1', tokenOut); - - const swap = await alphaRouter.route( - amount, - getQuoteToken(tokenIn, tokenOut, tradeType), - tradeType, - undefined, - { - // @ts-ignore[TS7053] - complaining about switch being non exhaustive - ...DEFAULT_ROUTING_CONFIG_BY_CHAIN[chain], - protocols: [], + // Expect tenderly simulation to be successful + expect(swap.simulationError).toBeUndefined(); } - ); - expect(swap).toBeDefined(); - expect(swap).not.toBeNull(); - }); - - if (!V2_SUPPORTED.includes(chain)) { - it(`is null when considering MIXED on non supported chains for exactInput & exactOutput`, async () => { + + // Scope limited for non mainnet network tests to validating the swap + }); + + it(`erc20 -> erc20`, async () => { const tokenIn = erc1; const tokenOut = erc2; const amount = tradeType == TradeType.EXACT_INPUT ? parseAmount('1', tokenIn) : parseAmount('1', tokenOut); + + const swap = await alphaRouter.route( + amount, + getQuoteToken(tokenIn, tokenOut, tradeType), + tradeType, + { + recipient: WHALES(tokenIn), + slippageTolerance: SLIPPAGE, + deadline: parseDeadline(360), + simulate: {fromAddress: WHALES(tokenIn)} + }, + { + // @ts-ignore[TS7053] - complaining about switch being non exhaustive + ...DEFAULT_ROUTING_CONFIG_BY_CHAIN[chain], + protocols: [Protocol.V3, Protocol.V2], + } + ); + expect(swap).toBeDefined(); + expect(swap).not.toBeNull(); + if(swap) { + expect(swap.quoteGasAdjusted.subtract(swap.quote).equalTo(swap.estimatedGasUsedQuoteToken)) + // Expect tenderly simulation to be successful + expect(swap.simulationError).toBeUndefined(); + } + }); + + const native = NATIVE_CURRENCY[chain]; + + it(`${native} -> erc20`, async () => { + const tokenIn = nativeOnChain(chain); + const tokenOut = erc2; + + // Celo currently has low liquidity and will not be able to find route for + // large input amounts + // TODO: Simplify this when Celo has more liquidity + const amount = + chain == ChainId.CELO || chain == ChainId.CELO_ALFAJORES + ? tradeType == TradeType.EXACT_INPUT + ? parseAmount('10', tokenIn) + : parseAmount('10', tokenOut) + : tradeType == TradeType.EXACT_INPUT + ? parseAmount('100', tokenIn) + : parseAmount('100', tokenOut); + const swap = await alphaRouter.route( amount, getQuoteToken(tokenIn, tokenOut, tradeType), tradeType, - undefined, + { + recipient: WHALES(tokenIn), + slippageTolerance: SLIPPAGE, + deadline: parseDeadline(360), + simulate: {fromAddress: WHALES(tokenIn)} + }, { // @ts-ignore[TS7053] - complaining about switch being non exhaustive ...DEFAULT_ROUTING_CONFIG_BY_CHAIN[chain], - protocols: [Protocol.MIXED], + protocols: [Protocol.V3, Protocol.V2], } ); - expect(swap).toBeNull(); + expect(swap).toBeDefined(); + expect(swap).not.toBeNull(); + if(swap) { + expect(swap.quoteGasAdjusted.subtract(swap.quote).equalTo(swap.estimatedGasUsedQuoteToken)) + + // Expect tenderly simulation to be successful + expect(swap.simulationError).toBeUndefined(); + } }); - } + }) }); } } diff --git a/test/test-util/whales.ts b/test/test-util/whales.ts new file mode 100644 index 000000000..8ac26fcbb --- /dev/null +++ b/test/test-util/whales.ts @@ -0,0 +1,90 @@ +import { CEUR_CELO, CEUR_CELO_ALFAJORES, ChainId, CUSD_CELO, DAI_MAINNET, DAI_ON, ExtendedEther, nativeOnChain, UNI_GÖRLI, UNI_MAINNET, USDC_MAINNET, USDC_ON, USDT_MAINNET, WETH9, WNATIVE_ON } from "../../src" +import { Currency, Ether } from '@uniswap/sdk-core'; + +export const WHALES = (token:Currency):string => { + switch(token) { + case Ether.onChain(1) as Currency: + return '0x0716a17FBAeE714f1E6aB0f9d59edbC5f09815C0' + case ExtendedEther.onChain(1): + return '0x0716a17FBAeE714f1E6aB0f9d59edbC5f09815C0' + case ExtendedEther.onChain(5): + return '0xe0a2bd4258d2768837baa26a28fe71dc079f84c7' + case ExtendedEther.onChain(42161): + return '0xf977814e90da44bfa03b6295a0616a897441acec' + case ExtendedEther.onChain(42): + return '0xb425fdbe8275361230a633c8d80ea371224d925c' + case nativeOnChain(137): + return '0xe7804c37c13166ff0b37f5ae0bb07a3aebb6e245' + case WETH9[1]: + return '0x06920c9fc643de77b99cb7670a944ad31eaaa260' + case WNATIVE_ON(ChainId.MAINNET): + return '0xf04a5cc80b1e94c69b48f5ee68a08cd2f09a7c3e' + case WNATIVE_ON(ChainId.ARBITRUM_ONE): + return '0x80a9ae39310abf666a87c743d6ebbd0e8c42158e' + case WNATIVE_ON(ChainId.KOVAN): + return '0xa71937147b55deb8a530c7229c442fd3f31b7db2' + case WNATIVE_ON(ChainId.RINKEBY): + return '0xf1c9dc0baa21bb260e192c8a52ee97c887456fb2' + case WNATIVE_ON(ChainId.GÖRLI): + return '0x2372031bb0fc735722aa4009aebf66e8beaf4ba1' + case WNATIVE_ON(ChainId.ROPSTEN): + return '0xc1a0babbe0e77ba1e8d9f627d281823518735839' + case WNATIVE_ON(ChainId.POLYGON): + return '0x369582d2010b6ed950b571f4101e3bb9b554876f' + case USDC_MAINNET: + case UNI_MAINNET: + case DAI_MAINNET: + case USDT_MAINNET: + return '0x47ac0fb4f2d84898e4d9e7b4dab3c24507a6d503' + case USDC_ON(ChainId.ROPSTEN): + return '0x366d1dd8558b59398439a01fb6935f6f40ebcd60' + case USDC_ON(ChainId.RINKEBY): + return '0x65671d573fc0e62139fbde470bfd03a38b4d5f26' + case UNI_GÖRLI: + return '0x41653c7d61609d856f29355e404f310ec4142cfb' + case USDC_ON(ChainId.KOVAN): + return '0x9b332466798a7e98bff1107d0846c195a99c1fc5' + case USDC_ON(ChainId.OPTIMISM): + return '0xad7b4c162707e0b2b5f6fddbd3f8538a5fba0d60' + case USDC_ON(ChainId.OPTIMISTIC_KOVAN): + return '0x81df215205befdad042b54d4c0dbe44e07748c1f' + case USDC_ON(ChainId.ARBITRUM_ONE): + return '0x1714400ff23db4af24f9fd64e7039e6597f18c2b' + case USDC_ON(ChainId.ARBITRUM_RINKEBY): + return '0xa2aad83466241232290bebcd43dcbff6a7f8d23a' + case USDC_ON(ChainId.POLYGON): + return '0xe7804c37c13166ff0b37f5ae0bb07a3aebb6e245' + case USDC_ON(ChainId.POLYGON_MUMBAI): + return '0x48520ff9b32d8b5bf87abf789ea7b3c394c95ebe' + case DAI_ON(ChainId.ROPSTEN): + return '0x922b992698381c7dc8d23684e2caef396b0b73a4' + case DAI_ON(ChainId.RINKEBY): + return '0xcea4e535d03086dbaa04c71675129654e92cc055' + case DAI_ON(ChainId.GÖRLI): + return '0x20918f71e99c09ae2ac3e33dbde33457d3be01f4' + case DAI_ON(ChainId.KOVAN): + return '0x9b332466798a7e98bff1107d0846c195a99c1fc5' + case DAI_ON(ChainId.OPTIMISM): + return '0x100bdc1431a9b09c61c0efc5776814285f8fb248' + case DAI_ON(ChainId.OPTIMISTIC_KOVAN): + return '0xf1c9dc0baa21bb260e192c8a52ee97c887456fb2' + case DAI_ON(ChainId.ARBITRUM_ONE): + return '0xba479d5585ecec47edc2a571da430a40f43c3851' + case DAI_ON(ChainId.ARBITRUM_RINKEBY): + return '0x7c8942a8aa007fc46ee50ddbaeb5d294b49b7efc' + case DAI_ON(ChainId.POLYGON): + return '0xf04adbf75cdfc5ed26eea4bbbb991db002036bdd' + case DAI_ON(ChainId.POLYGON_MUMBAI): + return '0xda8ab4137fe28f969b27c780d313d1bb62c8341e' + case CEUR_CELO: + return '0x612A7c4E40EAcb63dADaD4939dFedb9d3397E6fd' + case CEUR_CELO_ALFAJORES: + return '0x489324b266DFb125CC791B91Bc68F307cE3f6691' + case WNATIVE_ON(ChainId.CELO): + return '0x6cC083Aed9e3ebe302A6336dBC7c921C9f03349E' + case CUSD_CELO: + return '0xC32cBaf3D44dA6fbC761289b871af1A30cc7f993' + default: + return '0xf04a5cc80b1e94c69b48f5ee68a08cd2f09a7c3e' + } +} \ No newline at end of file diff --git a/test/unit/routers/alpha-router/alpha-router.test.ts b/test/unit/routers/alpha-router/alpha-router.test.ts index 9c35a69f4..6d7c2327b 100644 --- a/test/unit/routers/alpha-router/alpha-router.test.ts +++ b/test/unit/routers/alpha-router/alpha-router.test.ts @@ -15,6 +15,7 @@ import { CurrencyAmount, DAI_MAINNET as DAI, ETHGasStationInfoProvider, + FallbackTenderlySimulator, MixedRoute, MixedRouteWithValidQuote, OnChainQuoteProvider, @@ -99,6 +100,8 @@ describe.only('alpha router', () => { let mockBlockTokenListProvider: sinon.SinonStubbedInstance; let mockTokenValidatorProvider: sinon.SinonStubbedInstance; + let mockFallbackTenderlySimulator: sinon.SinonStubbedInstance; + let alphaRouter: AlphaRouter; const ROUTING_CONFIG: AlphaRouterConfig = { @@ -367,6 +370,9 @@ describe.only('alpha router', () => { getValidationByToken: () => TokenValidationResult.UNKN, }); + mockFallbackTenderlySimulator = sinon.createStubInstance(FallbackTenderlySimulator) + mockFallbackTenderlySimulator.simulateTransaction.callsFake(async (_fromAddress, route)=>route) + alphaRouter = new AlphaRouter({ chainId: 1, provider: mockProvider, @@ -385,6 +391,7 @@ describe.only('alpha router', () => { v2SubgraphProvider: mockV2SubgraphProvider, swapRouterProvider: mockSwapRouterProvider, tokenValidatorProvider: mockTokenValidatorProvider, + simulator: mockFallbackTenderlySimulator }); }); @@ -1843,11 +1850,12 @@ describe.only('alpha router', () => { sinon.assert.notCalled(mockOnChainQuoteProvider.getQuotesManyExactOut); }); - test('succeeds to route and generates calldata on v3 only', async () => { + test('succeeds to route and generates calldata on v3 only and simulates', async () => { const swapParams = { deadline: Math.floor(Date.now() / 1000) + 1000000, recipient: '0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B', slippageTolerance: new Percent(500, 10_000), + simulate: {fromAddress: '0x63946551716781C32f0269F87DC08521818b6292'} }; const swap = await alphaRouter.route( @@ -1855,10 +1863,11 @@ describe.only('alpha router', () => { USDC, TradeType.EXACT_OUTPUT, swapParams, - { ...ROUTING_CONFIG, protocols: [Protocol.V3] } + { ...ROUTING_CONFIG, protocols: [Protocol.V3] }, ); expect(swap).toBeDefined(); + expect(mockFallbackTenderlySimulator.simulateTransaction.called).toBeTruthy() expect(mockProvider.getBlockNumber.called).toBeTruthy(); expect(mockGasPriceProvider.getGasPrice.called).toBeTruthy();