From 777f0a52f66343fe0031d64cc601acf352f66fd5 Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Mon, 13 Jan 2025 15:19:32 +0100 Subject: [PATCH 1/4] chore: keep all /swap/* endpoints hot (#1370) --- api/cron-ping-endpoints.ts | 45 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/api/cron-ping-endpoints.ts b/api/cron-ping-endpoints.ts index 7656eddb0..6bdc1fe2d 100644 --- a/api/cron-ping-endpoints.ts +++ b/api/cron-ping-endpoints.ts @@ -24,6 +24,51 @@ const endpoints = [ }, updateIntervalSec: 10, }, + { + url: "https://preview.across.to/api/swap/auth", + params: { + amount: ethers.utils.parseUnits("1", 6).toString(), + tradeType: "minOutput", + inputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.ARBITRUM], + originChainId: CHAIN_IDs.ARBITRUM, + outputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.OPTIMISM], + destinationChainId: CHAIN_IDs.OPTIMISM, + depositor: "0x9A8f92a830A5cB89a3816e3D267CB7791c16b04D", + skipOriginTxEstimation: true, + refundOnOrigin: false, + }, + updateIntervalSec: 10, + }, + { + url: "https://preview.across.to/api/swap/permit", + params: { + amount: ethers.utils.parseUnits("1", 6).toString(), + tradeType: "minOutput", + inputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.ARBITRUM], + originChainId: CHAIN_IDs.ARBITRUM, + outputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.OPTIMISM], + destinationChainId: CHAIN_IDs.OPTIMISM, + depositor: "0x9A8f92a830A5cB89a3816e3D267CB7791c16b04D", + skipOriginTxEstimation: true, + refundOnOrigin: false, + }, + updateIntervalSec: 10, + }, + { + url: "https://preview.across.to/api/swap", + params: { + amount: ethers.utils.parseUnits("1", 6).toString(), + tradeType: "minOutput", + inputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.ARBITRUM], + originChainId: CHAIN_IDs.ARBITRUM, + outputToken: TOKEN_SYMBOLS_MAP.USDC.addresses[CHAIN_IDs.OPTIMISM], + destinationChainId: CHAIN_IDs.OPTIMISM, + depositor: "0x9A8f92a830A5cB89a3816e3D267CB7791c16b04D", + skipOriginTxEstimation: true, + refundOnOrigin: false, + }, + updateIntervalSec: 10, + }, ]; const maxDurationSec = 60; From 8ae03bf738d9c56d847ca5ebf1f5277a3a35ad34 Mon Sep 17 00:00:00 2001 From: "James Morris, MS" <96435344+james-a-morris@users.noreply.github.com> Date: Mon, 13 Jan 2025 13:07:24 -0500 Subject: [PATCH 2/4] improve(api): make buffer cap logic more intuitive (#1350) * improve(api): make buffer cap logic more intuitive Signed-off-by: james-a-morris * Update limits.ts * nit: lint Signed-off-by: james-a-morris --------- Signed-off-by: james-a-morris --- api/_utils.ts | 2 +- api/limits.ts | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/api/_utils.ts b/api/_utils.ts index 20e004d7a..a327d8fd4 100644 --- a/api/_utils.ts +++ b/api/_utils.ts @@ -1885,7 +1885,7 @@ export function getLimitsBufferMultiplier(symbol: string) { limitsBufferMultipliers[symbol] || "0.8" ); const multiplierCap = ethers.utils.parseEther("1"); - return bufferMultiplier.gt(multiplierCap) ? multiplierCap : bufferMultiplier; + return minBN(bufferMultiplier, multiplierCap); } export function getChainInputTokenMaxBalanceInUsd( diff --git a/api/limits.ts b/api/limits.ts index 06a363b14..8c92754df 100644 --- a/api/limits.ts +++ b/api/limits.ts @@ -279,8 +279,10 @@ const handler = async ( getLpCushion(l1Token.symbol, computedOriginChainId, destinationChainId), l1Token.decimals ); - liquidReserves = liquidReserves.sub(lpCushion); - if (liquidReserves.lt(0)) liquidReserves = ethers.BigNumber.from(0); + liquidReserves = maxBN( + liquidReserves.sub(lpCushion), + ethers.BigNumber.from(0) + ); maxDepositInstant = minBN(maxDepositInstant, liquidReserves); maxDepositShortDelay = minBN(maxDepositShortDelay, liquidReserves); From 5b9b48fcf5b80d801a809d074662e713cb991639 Mon Sep 17 00:00:00 2001 From: Matt Rice Date: Mon, 13 Jan 2025 16:41:35 -0500 Subject: [PATCH 3/4] feat: update routing on /deposit/status to use indexer (#1371) --- vercel.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vercel.json b/vercel.json index 00a7f48ec..fa7fa812a 100644 --- a/vercel.json +++ b/vercel.json @@ -25,7 +25,7 @@ "rewrites": [ { "source": "/api/deposit/status", - "destination": "https://public.api.across.to/deposit/status" + "destination": "https://indexer.across.to/deposit/status" } ], "redirects": [ From 09d3771e5d2b3f1e64aac8089e5f192165885434 Mon Sep 17 00:00:00 2001 From: nicholaspai <9457025+nicholaspai@users.noreply.github.com> Date: Mon, 13 Jan 2025 16:50:24 -0700 Subject: [PATCH 4/4] improve(API): Make gasFeeTotal more accurate in /limits and increase cache hit frequency (#1369) * improve(API): Reduce stale-while-revalidate and gas price cache times ## `stale-while-revalidate` We can reduce this cache time to 1s so that after the cached value is >1s old we can immediately start recomputing the limits value. This means in the best case we'll have as fresh gas cost data as possible. ## Gas price caching: We should ideally use less stale gas price data. However, we don't want to increase the /limits response time. We currently use the gas price to compute the gas cost so it makes sense to make the gas price cache time slightly longer or equal to the gas cost. This way if the gas cost cache is set, then we'll use the cached gas cost value. If its stale, then we'll fetch the gas price and hopefully hit the cache sometimes. This is why it doesn't make sense to set the gas price cache less than the gas cost cache time otherwise we'll very rarely hit the gas price cache. * Separate native gas cost and op stack l1 gas cost calculation from tokenGasCost calculation Willl allow us to use more customized caching times for different gas cost components that are expected to change on different time periods * Use gas price cache for Linea as well * fix: only cron cache gas prices for non Linea chains * Update limits.ts * Only pass in depositArgs for Linea * add extra part to cache key * Use sdk for helper methods * Update limits.ts * Fix gas-prices * Use utils in gas-prices.ts to read data from cache * add gas costs to cron job * cache gas prices before cost * remove promise.all * Update _utils.ts * cache op stack l1 costs for op chains only * Test only cache gas prices * debug * Fix cron job * Update cron-cache-gas-prices.ts * fix promise nesting * Update cron-cache-gas-prices.ts * update cache times * Update _utils.ts * Update cron-cache-gas-prices.ts * Add native gas cost caching Keep cache warm * Increase ttl of native gas cost, add gasFeeDetails to response * sdk --- api/_utils.ts | 169 +++++++++++++++++++++++---------- api/cron-cache-gas-prices.ts | 176 ++++++++++++++++++++++++++++++++--- api/gas-prices.ts | 105 +++++++++------------ api/limits.ts | 84 +++++++++++------ package.json | 2 +- yarn.lock | 47 ++++++++-- 6 files changed, 426 insertions(+), 157 deletions(-) diff --git a/api/_utils.ts b/api/_utils.ts index a327d8fd4..fb658cd46 100644 --- a/api/_utils.ts +++ b/api/_utils.ts @@ -1970,94 +1970,169 @@ export function isContractCache(chainId: number, address: string) { ); } -export function getCachedFillGasUsage( +export function getCachedNativeGasCost( deposit: Parameters[0], - gasPrice?: BigNumber, overrides?: Partial<{ relayerAddress: string; }> ) { + // We can use a long TTL since we are fetching only the native gas cost which should rarely change. + // Set this longer than the secondsPerUpdate value in the cron cache gas prices job. const ttlPerChain = { - default: 10, - [CHAIN_IDs.ARBITRUM]: 10, + default: 120, }; - const cacheKey = buildInternalCacheKey( - "fillGasUsage", + "nativeGasCost", deposit.destinationChainId, deposit.outputToken ); - const ttl = ttlPerChain[deposit.destinationChainId] || ttlPerChain.default; const fetchFn = async () => { + const relayerAddress = + overrides?.relayerAddress ?? + sdk.constants.DEFAULT_SIMULATED_RELAYER_ADDRESS; const relayerFeeCalculatorQueries = getRelayerFeeCalculatorQueries( deposit.destinationChainId, overrides ); + const unsignedFillTxn = + await relayerFeeCalculatorQueries.getUnsignedTxFromDeposit( + buildDepositForSimulation(deposit), + relayerAddress + ); + const voidSigner = new ethers.VoidSigner( + relayerAddress, + relayerFeeCalculatorQueries.provider + ); + return voidSigner.estimateGas(unsignedFillTxn); + }; + + return makeCacheGetterAndSetter( + cacheKey, + ttlPerChain.default, + fetchFn, + (nativeGasCostFromCache) => { + return BigNumber.from(nativeGasCostFromCache); + } + ); +} + +export function getCachedOpStackL1DataFee( + deposit: Parameters[0], + nativeGasCost: BigNumber, + overrides?: Partial<{ + relayerAddress: string; + }> +) { + // The L1 data fee should change after each Ethereum block since its based on the L1 base fee. + // However, the L1 base fee should only change by 12.5% at most per block. + // We set this higher than the secondsPerUpdate value in the cron cache gas prices job which will update this + // more frequently. + const ttlPerChain = { + default: 60, + }; + + const cacheKey = buildInternalCacheKey( + "opStackL1DataFee", + deposit.destinationChainId, + deposit.outputToken // This should technically differ based on the output token since the L2 calldata + // size affects the L1 data fee and this calldata can differ based on the output token. + ); + const fetchFn = async () => { // We don't care about the gas token price or the token gas price, only the raw gas units. In the API // we'll compute the gas price separately. - const markups = getGasMarkup(deposit.destinationChainId); - const gasCosts = await relayerFeeCalculatorQueries.getGasCosts( - buildDepositForSimulation(deposit), - overrides?.relayerAddress, - { - gasPrice, - // We want the fee multipliers if the gasPrice is undefined: - baseFeeMultiplier: markups.baseFeeMarkup, - priorityFeeMultiplier: markups.priorityFeeMarkup, - opStackL1GasCostMultiplier: sdk.utils.chainIsOPStack( - deposit.destinationChainId - ) - ? getGasMarkup(deposit.destinationChainId).opStackL1DataFeeMarkup - : undefined, - } + const { opStackL1DataFeeMarkup } = getGasMarkup(deposit.destinationChainId); + const relayerFeeCalculatorQueries = getRelayerFeeCalculatorQueries( + deposit.destinationChainId, + overrides ); - return { - nativeGasCost: gasCosts.nativeGasCost, - tokenGasCost: gasCosts.tokenGasCost, - }; + const unsignedTx = + await relayerFeeCalculatorQueries.getUnsignedTxFromDeposit( + buildDepositForSimulation(deposit), + overrides?.relayerAddress + ); + const opStackL1GasCost = + await relayerFeeCalculatorQueries.getOpStackL1DataFee( + unsignedTx, + overrides?.relayerAddress, + { + opStackL2GasUnits: nativeGasCost, // Passed in here to avoid gas cost recomputation by the SDK + opStackL1DataFeeMultiplier: opStackL1DataFeeMarkup, + } + ); + return opStackL1GasCost; }; - return getCachedValue( + return makeCacheGetterAndSetter( cacheKey, - ttl, + ttlPerChain.default, fetchFn, - (gasCosts: { nativeGasCost: BigNumber; tokenGasCost: BigNumber }) => { - return { - nativeGasCost: BigNumber.from(gasCosts.nativeGasCost), - tokenGasCost: BigNumber.from(gasCosts.tokenGasCost), - }; + (l1DataFeeFromCache) => { + return BigNumber.from(l1DataFeeFromCache); } ); } -export function latestGasPriceCache(chainId: number) { +export function latestGasPriceCache( + chainId: number, + deposit?: Parameters[0], + overrides?: Partial<{ + relayerAddress: string; + }> +) { + // We set this higher than the secondsPerUpdate value in the cron cache gas prices job which will update this + // more frequently. const ttlPerChain = { default: 30, - [CHAIN_IDs.ARBITRUM]: 15, }; - return makeCacheGetterAndSetter( - buildInternalCacheKey("latestGasPriceCache", chainId), - ttlPerChain[chainId] || ttlPerChain.default, - async () => (await getMaxFeePerGas(chainId)).maxFeePerGas, - (bnFromCache) => BigNumber.from(bnFromCache) + // If deposit is defined, then the gas price will be dependent on the fill transaction derived from the deposit. + // Therefore, we technically should cache a different gas price per different types of deposit so we add + // an additional outputToken to the cache key to distinguish between gas prices dependent on deposit args + // for different output tokens, which should be the main factor affecting the fill gas cost. + buildInternalCacheKey( + `latestGasPriceCache${deposit ? `-${deposit.outputToken}` : ""}`, + chainId + ), + ttlPerChain.default, + async () => await getMaxFeePerGas(chainId, deposit, overrides), + (gasPrice: sdk.gasPriceOracle.GasPriceEstimate) => { + return { + maxFeePerGas: BigNumber.from(gasPrice.maxFeePerGas), + maxPriorityFeePerGas: BigNumber.from(gasPrice.maxPriorityFeePerGas), + }; + } ); } -/** - * Resolve the current gas price for a given chain - * @param chainId The chain ID to resolve the gas price for - * @returns The gas price in the native currency of the chain - */ -export function getMaxFeePerGas( - chainId: number +export async function getMaxFeePerGas( + chainId: number, + deposit?: Parameters[0], + overrides?: Partial<{ + relayerAddress: string; + }> ): Promise { + if (deposit && deposit.destinationChainId !== chainId) { + throw new Error( + "Chain ID must match the destination chain ID of the deposit" + ); + } const { baseFeeMarkup: baseFeeMultiplier, priorityFeeMarkup: priorityFeeMultiplier, } = getGasMarkup(chainId); + const relayerFeeCalculatorQueries = getRelayerFeeCalculatorQueries( + chainId, + overrides + ); + const unsignedFillTxn = deposit + ? await relayerFeeCalculatorQueries.getUnsignedTxFromDeposit( + buildDepositForSimulation(deposit), + overrides?.relayerAddress + ) + : undefined; return sdk.gasPriceOracle.getGasPriceEstimate(getProvider(chainId), { chainId, + unsignedTx: unsignedFillTxn, baseFeeMultiplier, priorityFeeMultiplier, }); diff --git a/api/cron-cache-gas-prices.ts b/api/cron-cache-gas-prices.ts index ea494f9c6..6e5f5cb92 100644 --- a/api/cron-cache-gas-prices.ts +++ b/api/cron-cache-gas-prices.ts @@ -2,22 +2,60 @@ import { VercelResponse } from "@vercel/node"; import { TypedVercelRequest } from "./_types"; import { HUB_POOL_CHAIN_ID, + getCachedNativeGasCost, + getCachedOpStackL1DataFee, getLogger, handleErrorCondition, latestGasPriceCache, + resolveVercelEndpoint, } from "./_utils"; import { UnauthorizedError } from "./_errors"; import mainnetChains from "../src/data/chains_1.json"; -import { utils } from "@across-protocol/sdk"; +import { utils, constants } from "@across-protocol/sdk"; +import { CHAIN_IDs, DEFAULT_SIMULATED_RECIPIENT_ADDRESS } from "./_constants"; +import axios from "axios"; +import { ethers } from "ethers"; +type Route = { + originChainId: number; + originToken: string; + destinationChainId: number; + destinationToken: string; + originTokenSymbol: string; + destinationTokenSymbol: string; +}; + +// Set lower than TTL in latestGasPriceCache const updateIntervalsSecPerChain = { + default: 5, +}; + +// Set lower than TTL in getCachedOpStackL1DataFee +// Set lower than the L1 block time so we can try to get as up to date L1 data fees based on L1 base fees as possible. +const updateL1DataFeeIntervalsSecPerChain = { default: 10, - 1: 12, +}; + +// Set lower than TTL in getCachedNativeGasCost. This should rarely change so we should just make sure +// we keep this cache warm. +const updateNativeGasCostIntervalsSecPerChain = { + default: 30, }; const maxDurationSec = 60; +const getDepositArgsForChainId = (chainId: number, tokenAddress: string) => { + return { + amount: ethers.BigNumber.from(100), + inputToken: constants.ZERO_ADDRESS, + outputToken: tokenAddress, + recipientAddress: DEFAULT_SIMULATED_RECIPIENT_ADDRESS, + originChainId: 0, // Shouldn't matter for simulation + destinationChainId: Number(chainId), + }; +}; + const handler = async ( request: TypedVercelRequest>, response: VercelResponse @@ -45,19 +83,30 @@ const handler = async ( return; } + const availableRoutes = ( + await axios(`${resolveVercelEndpoint()}/api/available-routes`) + ).data as Array; + // This marks the timestamp when the function started const functionStart = Date.now(); - // The minimum interval for Vercel Serverless Functions cron jobs is 1 minute. - // But we want to update gas prices more frequently than that. - // To circumvent this, we run the function in a loop and update gas prices every - // `secondsPerUpdateForChain` seconds and stop after `maxDurationSec` seconds (1 minute). - const gasPricePromises = mainnetChains.map(async (chain) => { - const secondsPerUpdateForChain = - updateIntervalsSecPerChain[ - chain.chainId as keyof typeof updateIntervalsSecPerChain - ] || updateIntervalsSecPerChain.default; - const cache = latestGasPriceCache(chain.chainId); + /** + * @notice Updates the gas price cache every `updateIntervalsSecPerChain` seconds up to `maxDurationSec` seconds. + * @param chainId Chain to estimate gas price for + * @param outputTokenAddress Optional param to set if the gas price is dependent on the calldata of the transaction + * to be submitted on the chainId. This output token will be used to construct a fill transaction to simulate. + */ + const updateGasPricePromise = async ( + chainId: number, + outputTokenAddress?: string + ): Promise => { + const secondsPerUpdateForChain = updateIntervalsSecPerChain.default; + const cache = latestGasPriceCache( + chainId, + outputTokenAddress + ? getDepositArgsForChainId(chainId, outputTokenAddress) + : undefined + ); while (true) { const diff = Date.now() - functionStart; @@ -68,8 +117,107 @@ const handler = async ( await cache.set(); await utils.delay(secondsPerUpdateForChain); } - }); - await Promise.all(gasPricePromises); + }; + + /** + * @notice Updates the L1 data fee gas cost cache every `updateL1DataFeeIntervalsSecPerChain` seconds + * up to `maxDurationSec` seconds. + * @param chainId Chain to estimate l1 data fee for + * @param outputTokenAddress This output token will be used to construct a fill transaction to simulate + * gas costs for. + */ + const updateL1DataFeePromise = async ( + chainId: number, + outputTokenAddress: string + ): Promise => { + const secondsPerUpdate = updateL1DataFeeIntervalsSecPerChain.default; + const depositArgs = getDepositArgsForChainId(chainId, outputTokenAddress); + const gasCostCache = getCachedNativeGasCost(depositArgs); + + while (true) { + const diff = Date.now() - functionStart; + // Stop after `maxDurationSec` seconds + if (diff >= maxDurationSec * 1000) { + break; + } + const gasCost = await gasCostCache.get(); + if (utils.chainIsOPStack(chainId)) { + const cache = getCachedOpStackL1DataFee(depositArgs, gasCost); + await cache.set(); + } + await utils.delay(secondsPerUpdate); + } + }; + + /** + * @notice Updates the native gas cost cache every `updateNativeGasCostIntervalsSecPerChain` seconds + * up to `maxDurationSec` seconds. + * @param chainId Chain to estimate gas cost for + * @param outputTokenAddress This output token will be used to construct a fill transaction to simulate + * gas costs for. + */ + const updateNativeGasCostPromise = async ( + chainId: number, + outputTokenAddress: string + ): Promise => { + const secondsPerUpdate = updateNativeGasCostIntervalsSecPerChain.default; + const depositArgs = getDepositArgsForChainId(chainId, outputTokenAddress); + const cache = getCachedNativeGasCost(depositArgs); + + while (true) { + const diff = Date.now() - functionStart; + // Stop after `maxDurationSec` seconds + if (diff >= maxDurationSec * 1000) { + break; + } + await cache.set(); + await utils.delay(secondsPerUpdate); + } + }; + + const lineaDestinationRoutes = availableRoutes.filter( + ({ destinationChainId }) => destinationChainId === CHAIN_IDs.LINEA + ); + // The minimum interval for Vercel Serverless Functions cron jobs is 1 minute. + // But we want to update gas data more frequently than that. + // To circumvent this, we run the function in a loop and update gas prices every + // `secondsPerUpdateForChain` seconds and stop after `maxDurationSec` seconds (1 minute). + await Promise.all([ + // @dev Linea gas prices are dependent on the L2 calldata to be submitted so compute one gas price for each output token, + // so we compute one gas price per output token for Linea + Promise.all( + mainnetChains + .filter((chain) => chain.chainId !== CHAIN_IDs.LINEA) + .map((chain) => updateGasPricePromise(chain.chainId)) + ), + Promise.all( + lineaDestinationRoutes.map(({ destinationToken }) => + updateGasPricePromise(CHAIN_IDs.LINEA, destinationToken) + ) + ), + Promise.all( + mainnetChains.map(async (chain) => { + const routesToChain = availableRoutes.filter( + ({ destinationChainId }) => destinationChainId === chain.chainId + ); + const outputTokensForChain = routesToChain.map( + ({ destinationToken }) => destinationToken + ); + await Promise.all([ + Promise.all( + outputTokensForChain.map((outputToken) => + updateNativeGasCostPromise(chain.chainId, outputToken) + ) + ), + Promise.all( + outputTokensForChain.map((outputToken) => + updateL1DataFeePromise(chain.chainId, outputToken) + ) + ), + ]); + }) + ), + ]); logger.debug({ at: "CronCacheGasPrices", diff --git a/api/gas-prices.ts b/api/gas-prices.ts index 427da081e..83ac3dc5c 100644 --- a/api/gas-prices.ts +++ b/api/gas-prices.ts @@ -1,17 +1,16 @@ import { VercelResponse } from "@vercel/node"; import { - buildDepositForSimulation, + getCachedNativeGasCost, + getCachedOpStackL1DataFee, getGasMarkup, getLogger, - getMaxFeePerGas, - getRelayerFeeCalculatorQueries, handleErrorCondition, + latestGasPriceCache, sendResponse, } from "./_utils"; import { TypedVercelRequest } from "./_types"; -import { ethers, providers, VoidSigner } from "ethers"; +import { ethers } from "ethers"; import * as sdk from "@across-protocol/sdk"; -import { L2Provider } from "@eth-optimism/sdk/dist/interfaces/l2-provider"; import mainnetChains from "../src/data/chains_1.json"; import { @@ -28,6 +27,16 @@ const QueryParamsSchema = object({ }); type QueryParams = Infer; +const getDepositArgsForChainId = (chainId: number, tokenAddress: string) => { + return { + amount: ethers.BigNumber.from(100), + inputToken: sdk.constants.ZERO_ADDRESS, + outputToken: tokenAddress, + recipientAddress: DEFAULT_SIMULATED_RECIPIENT_ADDRESS, + originChainId: 0, // Shouldn't matter for simulation + destinationChainId: Number(chainId), + }; +}; const handler = async ( { query }: TypedVercelRequest, response: VercelResponse @@ -47,56 +56,41 @@ const handler = async ( }) .filter(([, tokenAddress]) => tokenAddress !== undefined) ); - // getMaxFeePerGas will return the gas price after including the baseFeeMultiplier. - const gasPrices = await Promise.all( - Object.keys(chainIdsWithToken).map((chainId) => { - return getMaxFeePerGas(Number(chainId)); + const gasData = await Promise.all( + Object.entries(chainIdsWithToken).map(([chainId, tokenAddress]) => { + const depositArgs = getDepositArgsForChainId( + Number(chainId), + tokenAddress + ); + return Promise.all([ + getCachedNativeGasCost(depositArgs).get(), + latestGasPriceCache( + Number(chainId), + CHAIN_IDs.LINEA === Number(chainId) ? depositArgs : undefined + ).get(), + ]); }) ); + // We query the following gas costs after gas prices because token gas costs and op stack l1 gas costs + // depend on the gas price and native gas unit results. const gasCosts = await Promise.all( Object.entries(chainIdsWithToken).map( async ([chainId, tokenAddress], i) => { - // This is a dummy deposit used to pass into buildDepositForSimulation() to build a fill transaction - // that we can simulate without reversion. The only parameter that matters is that the destinationChainId - // is set to the spoke pool's chain ID we'll be simulating the fill call on. - const depositArgs = { - amount: ethers.BigNumber.from(100), - inputToken: sdk.constants.ZERO_ADDRESS, - outputToken: tokenAddress, - recipientAddress: DEFAULT_SIMULATED_RECIPIENT_ADDRESS, - originChainId: 0, // Shouldn't matter for simulation - destinationChainId: Number(chainId), - }; - const deposit = buildDepositForSimulation(depositArgs); - const relayerFeeCalculatorQueries = getRelayerFeeCalculatorQueries( - Number(chainId) + const depositArgs = getDepositArgsForChainId( + Number(chainId), + tokenAddress ); - const { baseFeeMarkup, priorityFeeMarkup, opStackL1DataFeeMarkup } = - getGasMarkup(Number(chainId)); - const { nativeGasCost, tokenGasCost, opStackL1GasCost, gasPrice } = - await relayerFeeCalculatorQueries.getGasCosts( - deposit, - relayerFeeCalculatorQueries.simulatedRelayerAddress, - { - // Pass in the already-computed gasPrice into this query so that the tokenGasCost includes - // the scaled gas price, - // e.g. tokenGasCost = nativeGasCost * (baseFee * baseFeeMultiplier + priorityFee). - // Except for Linea, where the gas price is dependent on the unsignedTx produced from the deposit, - // so let the SDK compute its gas price here. - gasPrice: - Number(chainId) === CHAIN_IDs.LINEA - ? undefined - : gasPrices[i].maxFeePerGas, - opStackL1GasCostMultiplier: opStackL1DataFeeMarkup, - baseFeeMultiplier: baseFeeMarkup, - priorityFeeMultiplier: priorityFeeMarkup, - } - ); + const [nativeGasCost, gasPrice] = gasData[i]; + const opStackL1GasCost = sdk.utils.chainIsOPStack(Number(chainId)) + ? await getCachedOpStackL1DataFee(depositArgs, nativeGasCost).get() + : undefined; + const tokenGasCost = nativeGasCost + .mul(gasPrice.maxFeePerGas) + .add(opStackL1GasCost ?? ethers.BigNumber.from("0")); return { nativeGasCost, tokenGasCost, opStackL1GasCost, - gasPrice, }; } ) @@ -107,23 +101,12 @@ const handler = async ( Object.keys(chainIdsWithToken).map((chainId, i) => [ chainId, { - gasPrice: - Number(chainId) === CHAIN_IDs.LINEA - ? gasCosts[i].gasPrice.toString() - : gasPrices[i].maxFeePerGas.toString(), + gasPrice: gasData[i][1].maxFeePerGas.toString(), gasPriceComponents: { - // Linea hardcodes base fee at 7 wei so we can always back it out fromthe gasPrice returned by the - // getGasCosts method. - maxFeePerGas: - Number(chainId) === CHAIN_IDs.LINEA - ? gasCosts[i].gasPrice.sub(7).toString() - : gasPrices[i].maxFeePerGas - .sub(gasPrices[i].maxPriorityFeePerGas) - .toString(), - priorityFeePerGas: - Number(chainId) === CHAIN_IDs.LINEA - ? "7" - : gasPrices[i].maxPriorityFeePerGas.toString(), + maxFeePerGas: gasData[i][1].maxFeePerGas + .sub(gasData[i][1].maxPriorityFeePerGas) + .toString(), + priorityFeePerGas: gasData[i][1].maxPriorityFeePerGas.toString(), baseFeeMultiplier: ethers.utils.formatEther( getGasMarkup(chainId).baseFeeMarkup ), diff --git a/api/limits.ts b/api/limits.ts index 8c92754df..d9dd4f06c 100644 --- a/api/limits.ts +++ b/api/limits.ts @@ -32,8 +32,9 @@ import { getCachedLatestBlock, parsableBigNumberString, validateDepositMessage, - getCachedFillGasUsage, latestGasPriceCache, + getCachedNativeGasCost, + getCachedOpStackL1DataFee, } from "./_utils"; import { MissingParamError } from "./_errors"; @@ -164,34 +165,49 @@ const handler = async ( message, }; - const [tokenPriceNative, _tokenPriceUsd, latestBlock, gasPrice] = - await Promise.all([ - getCachedTokenPrice( - l1Token.address, - sdk.utils.getNativeTokenSymbol(destinationChainId).toLowerCase() - ), - getCachedTokenPrice(l1Token.address, "usd"), - getCachedLatestBlock(HUB_POOL_CHAIN_ID), - // If Linea, then we will defer gas price estimation to the SDK in getCachedFillGasUsage because - // the priority fee depends upon the fill transaction calldata. - destinationChainId === CHAIN_IDs.LINEA - ? undefined - : latestGasPriceCache(destinationChainId).get(), - ]); + const [ + tokenPriceNative, + _tokenPriceUsd, + latestBlock, + { maxFeePerGas: gasPrice }, + nativeGasCost, + ] = await Promise.all([ + getCachedTokenPrice( + l1Token.address, + sdk.utils.getNativeTokenSymbol(destinationChainId).toLowerCase() + ), + getCachedTokenPrice(l1Token.address, "usd"), + getCachedLatestBlock(HUB_POOL_CHAIN_ID), + // We only want to derive an unsigned fill txn from the deposit args if the destination chain is Linea + // because only Linea's priority fee depends on the destination chain call data. + latestGasPriceCache( + destinationChainId, + CHAIN_IDs.LINEA === destinationChainId ? depositArgs : undefined, + { + relayerAddress: relayer, + } + ).get(), + isMessageDefined + ? undefined // Only use cached gas units if message is not defined, i.e. standard for standard bridges + : getCachedNativeGasCost(depositArgs, { + relayerAddress: relayer, + }).get(), + ]); const tokenPriceUsd = ethers.utils.parseUnits(_tokenPriceUsd.toString()); const [ - gasCosts, + opStackL1GasCost, multicallOutput, fullRelayerBalances, transferRestrictedBalances, fullRelayerMainnetBalances, ] = await Promise.all([ - isMessageDefined - ? undefined // Only use cached gas units if message is not defined, i.e. standard for standard bridges - : getCachedFillGasUsage(depositArgs, gasPrice, { + nativeGasCost && sdk.utils.chainIsOPStack(destinationChainId) + ? // Only use cached gas units if message is not defined, i.e. standard for standard bridges + getCachedOpStackL1DataFee(depositArgs, nativeGasCost, { relayerAddress: relayer, - }), + }).get() + : undefined, callViaMulticall3(provider, multiCalls, { blockTag: latestBlock.number, }), @@ -221,15 +237,21 @@ const handler = async ( ) ), ]); - // This call should not make any additional RPC queries if gasCosts is defined--for any deposit - // with an empty message. + const tokenGasCost = + nativeGasCost && gasPrice + ? nativeGasCost + .mul(gasPrice) + .add(opStackL1GasCost ?? ethers.BigNumber.from("0")) + : undefined; + // This call should not make any additional RPC queries since we are passing in gasPrice, nativeGasCost + // and tokenGasCost. const relayerFeeDetails = await getRelayerFeeDetails( depositArgs, tokenPriceNative, relayer, gasPrice, - gasCosts?.nativeGasCost, - gasCosts?.tokenGasCost + nativeGasCost, + tokenGasCost ); logger.debug({ at: "Limits", @@ -397,15 +419,23 @@ const handler = async ( capitalFeeTotal: relayerFeeDetails.capitalFeeTotal, capitalFeePercent: relayerFeeDetails.capitalFeePercent, }, + gasFeeDetails: tokenGasCost + ? { + nativeGasCost: nativeGasCost!.toString(), // Should exist if tokenGasCost exists + opStackL1GasCost: opStackL1GasCost?.toString(), + gasPrice: gasPrice.toString(), + tokenGasCost: tokenGasCost.toString(), + } + : undefined, }; logger.debug({ at: "Limits", message: "Response data", responseJson, }); - // Respond with a 200 status code and 10 seconds of cache with - // 45 seconds of stale-while-revalidate. - sendResponse(response, responseJson, 200, 10, 45); + // Respond with a 200 status code and 1 second of cache time with + // 59s to keep serving the stale data while recomputing the cached value. + sendResponse(response, responseJson, 200, 1, 59); } catch (error: unknown) { return handleErrorCondition("limits", response, logger, error); } diff --git a/package.json b/package.json index 4ff85caf5..526b36b83 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "@across-protocol/constants": "^3.1.24", "@across-protocol/contracts": "^3.0.19", "@across-protocol/contracts-v3.0.6": "npm:@across-protocol/contracts@3.0.6", - "@across-protocol/sdk": "^3.4.8", + "@across-protocol/sdk": "^3.4.10", "@amplitude/analytics-browser": "^2.3.5", "@balancer-labs/sdk": "1.1.6-beta.16", "@emotion/react": "^11.13.0", diff --git a/yarn.lock b/yarn.lock index fd3648c77..699f87e8b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16,11 +16,16 @@ "@uma/common" "^2.17.0" hardhat "^2.9.3" -"@across-protocol/constants@^3.1.24", "@across-protocol/constants@^3.1.25": +"@across-protocol/constants@^3.1.24": version "3.1.25" resolved "https://registry.yarnpkg.com/@across-protocol/constants/-/constants-3.1.25.tgz#60d6d9814582ff91faf2b6d9f51d6dccb447b4ce" integrity sha512-GpZoYn7hETYL2BPMM2GqXAer6+l/xuhder+pvpb00HJcb/sqCjF7vaaeKxjKJ3jKtyeulYmdu0NDkeNm5KbNWA== +"@across-protocol/constants@^3.1.27": + version "3.1.28" + resolved "https://registry.yarnpkg.com/@across-protocol/constants/-/constants-3.1.28.tgz#0540f5a44b085b0951a853898afe174ea113db3a" + integrity sha512-rnI1pQgkJ6+hPIQNomsi8eQreVfWKfFn9i9Z39U0fAnoXodZklW0eqj5N0cXlEfahp5j2u1RCs7s6fQ9megCdw== + "@across-protocol/constants@^3.1.9": version "3.1.13" resolved "https://registry.yarnpkg.com/@across-protocol/constants/-/constants-3.1.13.tgz#b4caf494e9d9fe50290cca91b7883ea408fdb90a" @@ -83,14 +88,42 @@ yargs "^17.7.2" zksync-web3 "^0.14.3" -"@across-protocol/sdk@^3.4.8": - version "3.4.8" - resolved "https://registry.yarnpkg.com/@across-protocol/sdk/-/sdk-3.4.8.tgz#070abb97b687cfe22d89349d776f68008af5f6a7" - integrity sha512-m4JnT3Sh+zmTZ/Oi7QrTv3IuNB+myBcbnPnEEssZroyBost/yEPyxXks+EHeU67KrT84t/otGyNb5YpTOvOK0A== +"@across-protocol/contracts@^3.0.20": + version "3.0.20" + resolved "https://registry.yarnpkg.com/@across-protocol/contracts/-/contracts-3.0.20.tgz#5a70782093d21a96b2e955b7ed725bea7af6e804" + integrity sha512-ufyO+MrbY7+0TDm/1cDl9iAeR4P8jt0AM1F9wiCBHVIYtj1wMD4eNm7G5Am3u8p1ruMjRhi6dJEVQcRF2O+LUg== + dependencies: + "@across-protocol/constants" "^3.1.27" + "@coral-xyz/anchor" "^0.30.1" + "@defi-wonderland/smock" "^2.3.4" + "@eth-optimism/contracts" "^0.5.40" + "@ethersproject/abstract-provider" "5.7.0" + "@ethersproject/abstract-signer" "5.7.0" + "@ethersproject/bignumber" "5.7.0" + "@openzeppelin/contracts" "4.9.6" + "@openzeppelin/contracts-upgradeable" "4.9.6" + "@scroll-tech/contracts" "^0.1.0" + "@solana-developers/helpers" "^2.4.0" + "@solana/spl-token" "^0.4.6" + "@solana/web3.js" "^1.31.0" + "@types/yargs" "^17.0.33" + "@uma/common" "^2.37.3" + "@uma/contracts-node" "^0.4.17" + "@uma/core" "^2.61.0" + axios "^1.7.4" + bs58 "^6.0.0" + prettier-plugin-rust "^0.1.9" + yargs "^17.7.2" + zksync-web3 "^0.14.3" + +"@across-protocol/sdk@^3.4.10": + version "3.4.10" + resolved "https://registry.yarnpkg.com/@across-protocol/sdk/-/sdk-3.4.10.tgz#b74c551f1625afccc10f5b792f1f61395771cf40" + integrity sha512-kM+RyTNVXzS4dl5zwJZh6es5FTouN1nECd0cckE7Z/FzEFdMmQmCn4I1Ojgt4gmE5AuUBZef4/11ZvT8uRmutQ== dependencies: "@across-protocol/across-token" "^1.0.0" - "@across-protocol/constants" "^3.1.25" - "@across-protocol/contracts" "^3.0.19" + "@across-protocol/constants" "^3.1.27" + "@across-protocol/contracts" "^3.0.20" "@eth-optimism/sdk" "^3.3.1" "@ethersproject/bignumber" "^5.7.0" "@pinata/sdk" "^2.1.0"