From de3900e3b95ff696c7466856c4359481695f4249 Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Tue, 24 Dec 2024 16:07:31 +0100 Subject: [PATCH 01/10] chore(BlockUtils): Add Ink blocktime override (#806) --- package.json | 2 +- src/utils/BlockUtils.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 4959fd84..a3d70555 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@across-protocol/sdk", "author": "UMA Team", - "version": "3.3.27", + "version": "3.3.28", "license": "AGPL-3.0", "homepage": "https://docs.across.to/reference/sdk", "files": [ diff --git a/src/utils/BlockUtils.ts b/src/utils/BlockUtils.ts index 876d1b2a..24f67e3d 100644 --- a/src/utils/BlockUtils.ts +++ b/src/utils/BlockUtils.ts @@ -34,6 +34,7 @@ const defaultHighBlockOffset = 10; const cacheTTL = 60 * 15; const now = getCurrentTime(); // Seed the cache with initial values. const blockTimes: { [chainId: number]: BlockTimeAverage } = { + [CHAIN_IDs.INK]: { average: 1, timestamp: now, blockRange: 1 }, [CHAIN_IDs.LINEA]: { average: 3, timestamp: now, blockRange: 1 }, [CHAIN_IDs.MAINNET]: { average: 12.5, timestamp: now, blockRange: 1 }, [CHAIN_IDs.OPTIMISM]: { average: 2, timestamp: now, blockRange: 1 }, From 71c12b1a144beb52837dc1f493fc7b1ef263f996 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Tue, 24 Dec 2024 17:53:01 -0500 Subject: [PATCH 02/10] improve(GasPriceOracle): Standardize gas price oracle usage - Removes special case in `src/utils/common.ts#estimateTotalGasRequiredByUnsignedTransaction` that handles Linea gas price estimates by avoiding the gasPriceOracle call - Implements `eip1559()` function in `linea-viem.ts` gas price oracle adapter file so that gas price oracle users now get access to Linea EIP1559 gas price estimates via viem. This requires callers to pass in an optional `unsignedTx` object - Refactors gas price oracle `getGasPriceEstimate()` function and consolidates all optional parameters into a single `GasPriceEstimateOptions` object which will be easier to maintain --- e2e/oracle.e2e.ts | 15 ++- src/gasPriceOracle/adapters/arbitrum-viem.ts | 15 ++- src/gasPriceOracle/adapters/ethereum-viem.ts | 12 +- src/gasPriceOracle/adapters/linea-viem.ts | 18 ++- src/gasPriceOracle/adapters/polygon-viem.ts | 9 +- src/gasPriceOracle/oracle.ts | 109 ++++++++++++++----- src/utils/common.ts | 33 +----- 7 files changed, 134 insertions(+), 77 deletions(-) diff --git a/e2e/oracle.e2e.ts b/e2e/oracle.e2e.ts index 6587dd64..484a61df 100644 --- a/e2e/oracle.e2e.ts +++ b/e2e/oracle.e2e.ts @@ -14,7 +14,7 @@ const dummyLogger = winston.createLogger({ const stdLastBaseFeePerGas = parseUnits("12", 9); const stdMaxPriorityFeePerGas = parseUnits("1", 9); // EIP-1559 chains only -const chainIds = [1, 10, 137, 324, 8453, 42161, 534352]; +const chainIds = [42161]; const customTransport = makeCustomTransport({ stdLastBaseFeePerGas, stdMaxPriorityFeePerGas }); @@ -25,7 +25,10 @@ describe("Gas Price Oracle", function () { for (const chainId of chainIds) { const chainKey = `NEW_GAS_PRICE_ORACLE_${chainId}`; process.env[chainKey] = "true"; - const { maxFeePerGas, maxPriorityFeePerGas } = await getGasPriceEstimate(provider, chainId, 1, customTransport); + const { maxFeePerGas, maxPriorityFeePerGas } = await getGasPriceEstimate(provider, { + chainId, + transport: customTransport, + }); dummyLogger.debug({ at: "Gas Price Oracle#Gas Price Retrieval", message: `Retrieved gas price estimate for chain ID ${chainId}`, @@ -57,8 +60,12 @@ describe("Gas Price Oracle", function () { it("Ethers: applies markup to maxFeePerGas", async function () { for (const chainId of chainIds) { const { maxFeePerGas: markedUpMaxFeePerGas, maxPriorityFeePerGas: markedUpMaxPriorityFeePerGas } = - await getGasPriceEstimate(provider, chainId, 2, customTransport); - const { maxFeePerGas, maxPriorityFeePerGas } = await getGasPriceEstimate(provider, chainId, 1, customTransport); + await getGasPriceEstimate(provider, { chainId, baseFeeMultiplier: 2, transport: customTransport }); + const { maxFeePerGas, maxPriorityFeePerGas } = await getGasPriceEstimate(provider, { + chainId, + baseFeeMultiplier: 1, + transport: customTransport, + }); dummyLogger.debug({ at: "Gas Price Oracle#Gas Price Retrieval", message: `Retrieved gas price estimate for chain ID ${chainId}`, diff --git a/src/gasPriceOracle/adapters/arbitrum-viem.ts b/src/gasPriceOracle/adapters/arbitrum-viem.ts index fc920b33..4947d6a0 100644 --- a/src/gasPriceOracle/adapters/arbitrum-viem.ts +++ b/src/gasPriceOracle/adapters/arbitrum-viem.ts @@ -1,13 +1,22 @@ import { PublicClient } from "viem"; import { InternalGasPriceEstimate } from "../types"; +import { eip1559 as ethereumEip1559 } from "./ethereum-viem"; const MAX_PRIORITY_FEE_PER_GAS = BigInt(1); // Arbitrum Nitro implements EIP-1559 pricing, but the priority fee is always refunded to the caller. // Swap it for 1 Wei to avoid inaccurate transaction cost estimates. // Reference: https://developer.arbitrum.io/faqs/gas-faqs#q-priority -export async function eip1559(provider: PublicClient, _chainId: number): Promise { - const { maxFeePerGas: _maxFeePerGas, maxPriorityFeePerGas } = await provider.estimateFeesPerGas(); - const maxFeePerGas = BigInt(_maxFeePerGas) - maxPriorityFeePerGas + MAX_PRIORITY_FEE_PER_GAS; +export async function eip1559( + provider: PublicClient, + _chainId: number, + baseFeeMultiplier: number +): Promise { + const { maxFeePerGas: _maxFeePerGas, maxPriorityFeePerGas } = await ethereumEip1559( + provider, + _chainId, + baseFeeMultiplier + ); + const maxFeePerGas = _maxFeePerGas - maxPriorityFeePerGas + MAX_PRIORITY_FEE_PER_GAS; return { maxFeePerGas, maxPriorityFeePerGas: MAX_PRIORITY_FEE_PER_GAS }; } diff --git a/src/gasPriceOracle/adapters/ethereum-viem.ts b/src/gasPriceOracle/adapters/ethereum-viem.ts index 050cadba..168f7d59 100644 --- a/src/gasPriceOracle/adapters/ethereum-viem.ts +++ b/src/gasPriceOracle/adapters/ethereum-viem.ts @@ -1,8 +1,16 @@ import { PublicClient } from "viem"; import { InternalGasPriceEstimate } from "../types"; +import { BigNumber } from "../../utils"; -export function eip1559(provider: PublicClient, _chainId: number): Promise { - return provider.estimateFeesPerGas(); +export async function eip1559( + provider: PublicClient, + _chainId: number, + baseFeeMultiplier: number +): Promise { + const { maxFeePerGas: _maxFeePerGas, maxPriorityFeePerGas } = await provider.estimateFeesPerGas(); + const maxFeePerGasScaled = BigNumber.from(_maxFeePerGas.toString()).mul(baseFeeMultiplier); + const maxFeePerGas = BigInt(maxFeePerGasScaled.toString()) + maxPriorityFeePerGas; + return { maxFeePerGas, maxPriorityFeePerGas }; } export async function legacy( diff --git a/src/gasPriceOracle/adapters/linea-viem.ts b/src/gasPriceOracle/adapters/linea-viem.ts index b642604a..cab1de5e 100644 --- a/src/gasPriceOracle/adapters/linea-viem.ts +++ b/src/gasPriceOracle/adapters/linea-viem.ts @@ -1,17 +1,23 @@ -import { PublicClient } from "viem"; +import { Address, PublicClient } from "viem"; import { estimateGas } from "viem/linea"; import { DEFAULT_SIMULATED_RELAYER_ADDRESS as account } from "../../constants"; import { InternalGasPriceEstimate } from "../types"; +import { PopulatedTransaction } from "ethers"; -export async function eip1559(provider: PublicClient, _chainId?: number): Promise { +export async function eip1559( + provider: PublicClient, + _chainId: number, + baseFeeMultiplier: number, + _unsignedTx?: PopulatedTransaction +): Promise { const { baseFeePerGas, priorityFeePerGas } = await estimateGas(provider, { - account, - to: account, - value: BigInt(1), + account: (_unsignedTx?.from as Address) ?? account, + to: (_unsignedTx?.to as Address) ?? account, + value: BigInt(_unsignedTx?.value?.toString() || "1"), }); return { - maxFeePerGas: baseFeePerGas + priorityFeePerGas, + maxFeePerGas: baseFeePerGas * BigInt(baseFeeMultiplier) + priorityFeePerGas, maxPriorityFeePerGas: priorityFeePerGas, }; } diff --git a/src/gasPriceOracle/adapters/polygon-viem.ts b/src/gasPriceOracle/adapters/polygon-viem.ts index c1ea717d..9923a404 100644 --- a/src/gasPriceOracle/adapters/polygon-viem.ts +++ b/src/gasPriceOracle/adapters/polygon-viem.ts @@ -62,15 +62,20 @@ class PolygonGasStation extends BaseHTTPAdapter { } } -export async function gasStation(provider: PublicClient, chainId: number): Promise { +export async function gasStation( + provider: PublicClient, + chainId: number, + baseFeeMultiplier: number +): Promise { const gasStation = new PolygonGasStation({ chainId, timeout: 2000, retries: 0 }); let maxPriorityFeePerGas: bigint; let maxFeePerGas: bigint; try { ({ maxPriorityFeePerGas, maxFeePerGas } = await gasStation.getFeeData()); + maxFeePerGas *= BigInt(baseFeeMultiplier); } catch (err) { // Fall back to the RPC provider. May be less accurate. - ({ maxPriorityFeePerGas, maxFeePerGas } = await eip1559(provider, chainId)); + ({ maxPriorityFeePerGas, maxFeePerGas } = await eip1559(provider, chainId, baseFeeMultiplier)); // Per the GasStation docs, the minimum priority fee on Polygon is 30 Gwei. // https://docs.polygon.technology/tools/gas/polygon-gas-station/#interpretation diff --git a/src/gasPriceOracle/oracle.ts b/src/gasPriceOracle/oracle.ts index 20b5bbb7..61ee4cda 100644 --- a/src/gasPriceOracle/oracle.ts +++ b/src/gasPriceOracle/oracle.ts @@ -1,9 +1,9 @@ import assert from "assert"; -import { Transport } from "viem"; -import { providers } from "ethers"; +import { PublicClient, Transport } from "viem"; +import { PopulatedTransaction, providers } from "ethers"; import { CHAIN_IDs } from "../constants"; import { BigNumber, chainIsOPStack } from "../utils"; -import { GasPriceEstimate } from "./types"; +import { GasPriceEstimate, InternalGasPriceEstimate } from "./types"; import { getPublicClient } from "./util"; import * as arbitrum from "./adapters/arbitrum"; import * as ethereum from "./adapters/ethereum"; @@ -13,32 +13,68 @@ import * as arbitrumViem from "./adapters/arbitrum-viem"; import * as lineaViem from "./adapters/linea-viem"; import * as polygonViem from "./adapters/polygon-viem"; +interface GasPriceEstimateOptions { + // baseFeeMultiplier Multiplier applied to base fee for EIP1559 gas prices (or total fee for legacy). + baseFeeMultiplier: number; + // legacyFallback In the case of an unrecognized chain, fall back to type 0 gas estimation. + legacyFallback: boolean; + // chainId The chain ID to query for gas prices. If omitted can be inferred by provider. + chainId?: number; + // unsignedTx The unsigned transaction used for simulation by Viem provider to produce the priority gas fee. + unsignedTx?: PopulatedTransaction; + // transport Viem Transport object to use for querying gas fees. + transport?: Transport; +} + +interface EthersGasPriceEstimateOptions extends GasPriceEstimateOptions { + chainId: number; +} + +interface ViemGasPriceEstimateOptions extends Partial { + baseFeeMultiplier: number; +} + +const GAS_PRICE_ESTIMATE_DEFAULTS: GasPriceEstimateOptions = { + baseFeeMultiplier: 1, + legacyFallback: true, +}; + /** * Provide an estimate for the current gas price for a particular chain. - * @param chainId The chain ID to query for gas prices. * @param provider A valid ethers provider. - * @param legacyFallback In the case of an unrecognised chain, fall back to type 0 gas estimation. - * @parm baseFeeMarkup Multiplier applied to base fee for EIP1559 gas prices (or total fee for legacy). - * @returns Am object of type GasPriceEstimate. + * @param {opts} GasPriceEstimateOptions optional parameters. + * @returns An object of type GasPriceEstimate. */ export async function getGasPriceEstimate( provider: providers.Provider, - chainId?: number, - baseFeeMultiplier = 1.0, - transport?: Transport, - legacyFallback = true + opts: Partial ): Promise { + const { + baseFeeMultiplier, + chainId: _chainId, + unsignedTx, + transport, + legacyFallback, + }: GasPriceEstimateOptions = { + ...GAS_PRICE_ESTIMATE_DEFAULTS, + ...opts, + }; assert( baseFeeMultiplier >= 1.0 && baseFeeMultiplier <= 5, `Require 1.0 < base fee multiplier (${baseFeeMultiplier}) <= 5.0 for a total gas multiplier within [+1.0, +5.0]` ); - chainId ?? ({ chainId } = await provider.getNetwork()); + const chainId = _chainId ?? (await provider.getNetwork()).chainId; + // We only use the unsignedTx in the viem flow. const useViem = process.env[`NEW_GAS_PRICE_ORACLE_${chainId}`] === "true"; return useViem - ? getViemGasPriceEstimate(chainId, baseFeeMultiplier, transport) - : getEthersGasPriceEstimate(provider, chainId, baseFeeMultiplier, legacyFallback); + ? _getViemGasPriceEstimate(chainId, { baseFeeMultiplier, unsignedTx, transport }) + : _getEthersGasPriceEstimate(provider, { + baseFeeMultiplier, + chainId, + legacyFallback, + }); } /** @@ -46,14 +82,14 @@ export async function getGasPriceEstimate( * @param chainId The chain ID to query for gas prices. * @param provider A valid ethers provider. * @param legacyFallback In the case of an unrecognised chain, fall back to type 0 gas estimation. - * @returns Am object of type GasPriceEstimate. + * @returns An object of type GasPriceEstimate. */ -function getEthersGasPriceEstimate( +function _getEthersGasPriceEstimate( provider: providers.Provider, - chainId: number, - baseFeeMultiplier: number, - legacyFallback = true + opts: EthersGasPriceEstimateOptions ): Promise { + const { baseFeeMultiplier, chainId, legacyFallback } = opts; + const gasPriceFeeds = { [CHAIN_IDs.ALEPH_ZERO]: arbitrum.eip1559, [CHAIN_IDs.ARBITRUM]: arbitrum.eip1559, @@ -74,19 +110,30 @@ function getEthersGasPriceEstimate( /** * Provide an estimate for the current gas price for a particular chain. * @param providerOrChainId A valid ethers provider or a chain ID. - * @param transport An optional transport object for custom gas price retrieval. - * @returns Am object of type GasPriceEstimate. + * @param transport An optional Viem Transport object for custom gas price retrieval. + * @param unsignedTx Only used in Linea provider to estimate priority gas fee. + * @returns An object of type GasPriceEstimate. */ -export async function getViemGasPriceEstimate( +export async function _getViemGasPriceEstimate( providerOrChainId: providers.Provider | number, - baseFeeMultiplier: number, - transport?: Transport + opts: ViemGasPriceEstimateOptions ): Promise { + const { baseFeeMultiplier, unsignedTx, transport } = opts; + const chainId = typeof providerOrChainId === "number" ? providerOrChainId : (await providerOrChainId.getNetwork()).chainId; const viemProvider = getPublicClient(chainId, transport); + console.log(viemProvider.transport) - const gasPriceFeeds = { + const gasPriceFeeds: Record< + number, + ( + provider: PublicClient, + chainId: number, + baseFeeMultiplier: number, + unsignedTx?: PopulatedTransaction + ) => Promise + > = { [CHAIN_IDs.ALEPH_ZERO]: arbitrumViem.eip1559, [CHAIN_IDs.ARBITRUM]: arbitrumViem.eip1559, [CHAIN_IDs.LINEA]: lineaViem.eip1559, @@ -96,18 +143,22 @@ export async function getViemGasPriceEstimate( let maxFeePerGas: bigint; let maxPriorityFeePerGas: bigint; if (gasPriceFeeds[chainId]) { - ({ maxFeePerGas, maxPriorityFeePerGas } = await gasPriceFeeds[chainId](viemProvider, chainId)); + ({ maxFeePerGas, maxPriorityFeePerGas } = await gasPriceFeeds[chainId]( + viemProvider, + chainId, + baseFeeMultiplier, + unsignedTx + )); } else { let gasPrice: bigint | undefined; ({ maxFeePerGas, maxPriorityFeePerGas, gasPrice } = await viemProvider.estimateFeesPerGas()); - maxFeePerGas ??= gasPrice!; + maxFeePerGas ??= gasPrice! * BigInt(baseFeeMultiplier); maxPriorityFeePerGas ??= BigInt(0); } - // Apply markup to base fee which will be more volatile than priority fee. return { - maxFeePerGas: BigNumber.from(maxFeePerGas.toString()).mul(baseFeeMultiplier), + maxFeePerGas: BigNumber.from(maxFeePerGas.toString()), maxPriorityFeePerGas: BigNumber.from(maxPriorityFeePerGas.toString()), }; } diff --git a/src/utils/common.ts b/src/utils/common.ts index 5793250e..c9ab70f0 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -7,10 +7,6 @@ import { getGasPriceEstimate } from "../gasPriceOracle"; import { BigNumber, BigNumberish, BN, bnZero, formatUnits, parseUnits, toBN } from "./BigNumberUtils"; import { ConvertDecimals } from "./FormattingUtils"; import { chainIsOPStack } from "./NetworkUtils"; -import { Address, Transport } from "viem"; -import { CHAIN_IDs } from "@across-protocol/constants"; -import { estimateGas } from "viem/linea"; -import { getPublicClient } from "../gasPriceOracle/util"; export type Decimalish = string | number | Decimal; export const AddressZero = ethers.constants.AddressZero; @@ -271,9 +267,9 @@ export async function estimateTotalGasRequiredByUnsignedTransaction( gasUnits ? Promise.resolve(BigNumber.from(gasUnits)) : voidSigner.estimateGas(unsignedTx), _gasPrice ? Promise.resolve({ maxFeePerGas: _gasPrice }) - : getGasPriceEstimate(provider, chainId, baseFeeMultiplier, transport), + : getGasPriceEstimate(provider, { chainId, baseFeeMultiplier, transport }), ] as const; - let [nativeGasCost, { maxFeePerGas: gasPrice }] = await Promise.all(queries); + const [nativeGasCost, { maxFeePerGas: gasPrice }] = await Promise.all(queries); assert(nativeGasCost.gt(bnZero), "Gas cost should not be 0"); let tokenGasCost: BigNumber; @@ -288,17 +284,6 @@ export async function estimateTotalGasRequiredByUnsignedTransaction( const l2GasCost = nativeGasCost.mul(gasPrice); tokenGasCost = l1GasCost.add(l2GasCost); } else { - if (chainId === CHAIN_IDs.LINEA && process.env[`NEW_GAS_PRICE_ORACLE_${chainId}`] === "true") { - // Permit linea_estimateGas via NEW_GAS_PRICE_ORACLE_59144=true - let baseFeePerGas: BigNumber, priorityFeePerGas: BigNumber; - ({ - gasLimit: nativeGasCost, - baseFeePerGas, - priorityFeePerGas, - } = await getLineaGasFees(chainId, transport, unsignedTx)); - gasPrice = baseFeePerGas.mul(baseFeeMultiplier).add(priorityFeePerGas); - } - tokenGasCost = nativeGasCost.mul(gasPrice); } @@ -309,20 +294,6 @@ export async function estimateTotalGasRequiredByUnsignedTransaction( }; } -async function getLineaGasFees(chainId: number, transport: Transport | undefined, unsignedTx: PopulatedTransaction) { - const { gasLimit, baseFeePerGas, priorityFeePerGas } = await estimateGas(getPublicClient(chainId, transport), { - account: unsignedTx.from as Address, - to: unsignedTx.to as Address, - value: BigInt(unsignedTx.value?.toString() || "1"), - }); - - return { - gasLimit: BigNumber.from(gasLimit.toString()), - baseFeePerGas: BigNumber.from(baseFeePerGas.toString()), - priorityFeePerGas: BigNumber.from(priorityFeePerGas.toString()), - }; -} - export function randomAddress() { return ethers.utils.getAddress(ethers.utils.hexlify(ethers.utils.randomBytes(20))); } From 4961d485180404ef7b9b78cbb8d605e8bb6f8395 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Tue, 24 Dec 2024 17:57:19 -0500 Subject: [PATCH 03/10] fix --- e2e/oracle.e2e.ts | 2 +- src/gasPriceOracle/adapters/arbitrum-viem.ts | 4 +++- src/gasPriceOracle/oracle.ts | 1 - 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/e2e/oracle.e2e.ts b/e2e/oracle.e2e.ts index 484a61df..72c51173 100644 --- a/e2e/oracle.e2e.ts +++ b/e2e/oracle.e2e.ts @@ -14,7 +14,7 @@ const dummyLogger = winston.createLogger({ const stdLastBaseFeePerGas = parseUnits("12", 9); const stdMaxPriorityFeePerGas = parseUnits("1", 9); // EIP-1559 chains only -const chainIds = [42161]; +const chainIds = [1, 10, 137, 324, 8453, 42161, 534352]; const customTransport = makeCustomTransport({ stdLastBaseFeePerGas, stdMaxPriorityFeePerGas }); diff --git a/src/gasPriceOracle/adapters/arbitrum-viem.ts b/src/gasPriceOracle/adapters/arbitrum-viem.ts index 4947d6a0..5b84f38e 100644 --- a/src/gasPriceOracle/adapters/arbitrum-viem.ts +++ b/src/gasPriceOracle/adapters/arbitrum-viem.ts @@ -17,6 +17,8 @@ export async function eip1559( _chainId, baseFeeMultiplier ); - const maxFeePerGas = _maxFeePerGas - maxPriorityFeePerGas + MAX_PRIORITY_FEE_PER_GAS; + // @dev We need to back out the maxPriorityFee twice since its already added in `ethereumEip1559` to the + // maxFeePerGas. + const maxFeePerGas = _maxFeePerGas - maxPriorityFeePerGas * BigInt(2) + MAX_PRIORITY_FEE_PER_GAS; return { maxFeePerGas, maxPriorityFeePerGas: MAX_PRIORITY_FEE_PER_GAS }; } diff --git a/src/gasPriceOracle/oracle.ts b/src/gasPriceOracle/oracle.ts index 61ee4cda..44fff7f8 100644 --- a/src/gasPriceOracle/oracle.ts +++ b/src/gasPriceOracle/oracle.ts @@ -123,7 +123,6 @@ export async function _getViemGasPriceEstimate( const chainId = typeof providerOrChainId === "number" ? providerOrChainId : (await providerOrChainId.getNetwork()).chainId; const viemProvider = getPublicClient(chainId, transport); - console.log(viemProvider.transport) const gasPriceFeeds: Record< number, From bda484302159f72813dfd687a814044cdadcda40 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Tue, 24 Dec 2024 18:00:52 -0500 Subject: [PATCH 04/10] Add unit test --- test/GasPriceOracle.test.ts | 61 +++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 test/GasPriceOracle.test.ts diff --git a/test/GasPriceOracle.test.ts b/test/GasPriceOracle.test.ts new file mode 100644 index 00000000..7a6fae91 --- /dev/null +++ b/test/GasPriceOracle.test.ts @@ -0,0 +1,61 @@ +// @note: This test is _not_ run automatically as part of git hooks or CI. +import dotenv from "dotenv"; +import { providers } from "ethers"; +import { getGasPriceEstimate } from "../src/gasPriceOracle"; +import { BigNumber, bnZero, parseUnits } from "../src/utils"; +import { expect, makeCustomTransport } from "../test/utils"; +dotenv.config({ path: ".env" }); + +const stdLastBaseFeePerGas = parseUnits("12", 9); +const stdMaxPriorityFeePerGas = parseUnits("1", 9); // EIP-1559 chains only +const chainIds = [1, 10, 137, 324, 8453, 42161, 534352]; + +const customTransport = makeCustomTransport({ stdLastBaseFeePerGas, stdMaxPriorityFeePerGas }); + +const provider = new providers.StaticJsonRpcProvider("https://eth.llamarpc.com"); + +describe("Gas Price Oracle", function () { + it("Gas Price Retrieval", async function () { + for (const chainId of chainIds) { + const chainKey = `NEW_GAS_PRICE_ORACLE_${chainId}`; + process.env[chainKey] = "true"; + const { maxFeePerGas, maxPriorityFeePerGas } = await getGasPriceEstimate(provider, { + chainId, + transport: customTransport, + }); + + expect(BigNumber.isBigNumber(maxFeePerGas)).to.be.true; + expect(BigNumber.isBigNumber(maxPriorityFeePerGas)).to.be.true; + + if (chainId === 137) { + // The Polygon gas station isn't mocked, so just ensure that the fees have a valid relationship. + expect(maxFeePerGas.gt(0)).to.be.true; + expect(maxPriorityFeePerGas.gt(0)).to.be.true; + expect(maxPriorityFeePerGas.lt(maxFeePerGas)).to.be.true; + } else if (chainId === 42161) { + // Arbitrum priority fees are refunded, so drop the priority fee from estimates. + // Expect a 1.2x multiplier on the last base fee. + expect(maxFeePerGas.eq(stdLastBaseFeePerGas.mul("120").div("100").add(1))).to.be.true; + expect(maxPriorityFeePerGas.eq(1)).to.be.true; + } else { + expect(maxFeePerGas.gt(bnZero)).to.be.true; + expect(maxPriorityFeePerGas.gt(bnZero)).to.be.true; + } + + delete process.env[chainKey]; + } + }); + it("Ethers: applies markup to maxFeePerGas", async function () { + for (const chainId of chainIds) { + const { maxFeePerGas: markedUpMaxFeePerGas, maxPriorityFeePerGas: markedUpMaxPriorityFeePerGas } = + await getGasPriceEstimate(provider, { chainId, baseFeeMultiplier: 2, transport: customTransport }); + const { maxFeePerGas, maxPriorityFeePerGas } = await getGasPriceEstimate(provider, { + chainId, + baseFeeMultiplier: 1, + transport: customTransport, + }); + expect(markedUpMaxFeePerGas.gt(maxFeePerGas)).to.be.true; + expect(markedUpMaxPriorityFeePerGas).to.equal(maxPriorityFeePerGas); + } + }); +}); From 33e36a47dbeea2d864d8782c13f63856307ce8cc Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Wed, 25 Dec 2024 16:00:43 -0500 Subject: [PATCH 05/10] Remove arbitrum viem provider; add unit tests --- e2e/oracle.e2e.ts | 81 ---------- src/gasPriceOracle/adapters/arbitrum-viem.ts | 24 --- src/gasPriceOracle/adapters/ethereum-viem.ts | 27 ---- src/gasPriceOracle/adapters/ethereum.ts | 3 +- src/gasPriceOracle/adapters/linea-viem.ts | 24 ++- src/gasPriceOracle/adapters/polygon-viem.ts | 91 ----------- src/gasPriceOracle/adapters/polygon.ts | 5 +- src/gasPriceOracle/oracle.ts | 5 - src/utils/common.ts | 2 +- test/GasPriceOracle.test.ts | 160 +++++++++++++++---- test/utils/transport.ts | 7 + 11 files changed, 158 insertions(+), 271 deletions(-) delete mode 100644 e2e/oracle.e2e.ts delete mode 100644 src/gasPriceOracle/adapters/arbitrum-viem.ts delete mode 100644 src/gasPriceOracle/adapters/ethereum-viem.ts delete mode 100644 src/gasPriceOracle/adapters/polygon-viem.ts diff --git a/e2e/oracle.e2e.ts b/e2e/oracle.e2e.ts deleted file mode 100644 index 72c51173..00000000 --- a/e2e/oracle.e2e.ts +++ /dev/null @@ -1,81 +0,0 @@ -// @note: This test is _not_ run automatically as part of git hooks or CI. -import dotenv from "dotenv"; -import winston from "winston"; -import { providers } from "ethers"; -import { getGasPriceEstimate } from "../src/gasPriceOracle"; -import { BigNumber, bnZero, parseUnits } from "../src/utils"; -import { expect, makeCustomTransport } from "../test/utils"; -dotenv.config({ path: ".env" }); - -const dummyLogger = winston.createLogger({ - level: "debug", - transports: [new winston.transports.Console()], -}); - -const stdLastBaseFeePerGas = parseUnits("12", 9); -const stdMaxPriorityFeePerGas = parseUnits("1", 9); // EIP-1559 chains only -const chainIds = [1, 10, 137, 324, 8453, 42161, 534352]; - -const customTransport = makeCustomTransport({ stdLastBaseFeePerGas, stdMaxPriorityFeePerGas }); - -const provider = new providers.StaticJsonRpcProvider("https://eth.llamarpc.com"); - -describe("Gas Price Oracle", function () { - it("Gas Price Retrieval", async function () { - for (const chainId of chainIds) { - const chainKey = `NEW_GAS_PRICE_ORACLE_${chainId}`; - process.env[chainKey] = "true"; - const { maxFeePerGas, maxPriorityFeePerGas } = await getGasPriceEstimate(provider, { - chainId, - transport: customTransport, - }); - dummyLogger.debug({ - at: "Gas Price Oracle#Gas Price Retrieval", - message: `Retrieved gas price estimate for chain ID ${chainId}`, - maxFeePerGas: maxFeePerGas.toString(), - maxPriorityFeePerGas: maxPriorityFeePerGas.toString(), - }); - - expect(BigNumber.isBigNumber(maxFeePerGas)).to.be.true; - expect(BigNumber.isBigNumber(maxPriorityFeePerGas)).to.be.true; - - if (chainId === 137) { - // The Polygon gas station isn't mocked, so just ensure that the fees have a valid relationship. - expect(maxFeePerGas.gt(0)).to.be.true; - expect(maxPriorityFeePerGas.gt(0)).to.be.true; - expect(maxPriorityFeePerGas.lt(maxFeePerGas)).to.be.true; - } else if (chainId === 42161) { - // Arbitrum priority fees are refunded, so drop the priority fee from estimates. - // Expect a 1.2x multiplier on the last base fee. - expect(maxFeePerGas.eq(stdLastBaseFeePerGas.mul("120").div("100").add(1))).to.be.true; - expect(maxPriorityFeePerGas.eq(1)).to.be.true; - } else { - expect(maxFeePerGas.gt(bnZero)).to.be.true; - expect(maxPriorityFeePerGas.gt(bnZero)).to.be.true; - } - - delete process.env[chainKey]; - } - }); - it("Ethers: applies markup to maxFeePerGas", async function () { - for (const chainId of chainIds) { - const { maxFeePerGas: markedUpMaxFeePerGas, maxPriorityFeePerGas: markedUpMaxPriorityFeePerGas } = - await getGasPriceEstimate(provider, { chainId, baseFeeMultiplier: 2, transport: customTransport }); - const { maxFeePerGas, maxPriorityFeePerGas } = await getGasPriceEstimate(provider, { - chainId, - baseFeeMultiplier: 1, - transport: customTransport, - }); - dummyLogger.debug({ - at: "Gas Price Oracle#Gas Price Retrieval", - message: `Retrieved gas price estimate for chain ID ${chainId}`, - maxFeePerGas: maxFeePerGas.toString(), - maxPriorityFeePerGas: maxPriorityFeePerGas.toString(), - markedUpMaxFeePerGas: markedUpMaxFeePerGas.toString(), - markedUpMaxPriorityFeePerGas: markedUpMaxPriorityFeePerGas.toString(), - }); - expect(markedUpMaxFeePerGas.gt(maxFeePerGas)).to.be.true; - expect(markedUpMaxPriorityFeePerGas).to.equal(maxPriorityFeePerGas); - } - }); -}); diff --git a/src/gasPriceOracle/adapters/arbitrum-viem.ts b/src/gasPriceOracle/adapters/arbitrum-viem.ts deleted file mode 100644 index 5b84f38e..00000000 --- a/src/gasPriceOracle/adapters/arbitrum-viem.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { PublicClient } from "viem"; -import { InternalGasPriceEstimate } from "../types"; -import { eip1559 as ethereumEip1559 } from "./ethereum-viem"; - -const MAX_PRIORITY_FEE_PER_GAS = BigInt(1); - -// Arbitrum Nitro implements EIP-1559 pricing, but the priority fee is always refunded to the caller. -// Swap it for 1 Wei to avoid inaccurate transaction cost estimates. -// Reference: https://developer.arbitrum.io/faqs/gas-faqs#q-priority -export async function eip1559( - provider: PublicClient, - _chainId: number, - baseFeeMultiplier: number -): Promise { - const { maxFeePerGas: _maxFeePerGas, maxPriorityFeePerGas } = await ethereumEip1559( - provider, - _chainId, - baseFeeMultiplier - ); - // @dev We need to back out the maxPriorityFee twice since its already added in `ethereumEip1559` to the - // maxFeePerGas. - const maxFeePerGas = _maxFeePerGas - maxPriorityFeePerGas * BigInt(2) + MAX_PRIORITY_FEE_PER_GAS; - return { maxFeePerGas, maxPriorityFeePerGas: MAX_PRIORITY_FEE_PER_GAS }; -} diff --git a/src/gasPriceOracle/adapters/ethereum-viem.ts b/src/gasPriceOracle/adapters/ethereum-viem.ts deleted file mode 100644 index 168f7d59..00000000 --- a/src/gasPriceOracle/adapters/ethereum-viem.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { PublicClient } from "viem"; -import { InternalGasPriceEstimate } from "../types"; -import { BigNumber } from "../../utils"; - -export async function eip1559( - provider: PublicClient, - _chainId: number, - baseFeeMultiplier: number -): Promise { - const { maxFeePerGas: _maxFeePerGas, maxPriorityFeePerGas } = await provider.estimateFeesPerGas(); - const maxFeePerGasScaled = BigNumber.from(_maxFeePerGas.toString()).mul(baseFeeMultiplier); - const maxFeePerGas = BigInt(maxFeePerGasScaled.toString()) + maxPriorityFeePerGas; - return { maxFeePerGas, maxPriorityFeePerGas }; -} - -export async function legacy( - provider: PublicClient, - _chainId: number, - _test?: number -): Promise { - const gasPrice = await provider.getGasPrice(); - - return { - maxFeePerGas: gasPrice, - maxPriorityFeePerGas: BigInt(0), - }; -} diff --git a/src/gasPriceOracle/adapters/ethereum.ts b/src/gasPriceOracle/adapters/ethereum.ts index 6a376632..a9330b1c 100644 --- a/src/gasPriceOracle/adapters/ethereum.ts +++ b/src/gasPriceOracle/adapters/ethereum.ts @@ -36,8 +36,9 @@ export async function eip1559Raw( const maxPriorityFeePerGas = BigNumber.from(_maxPriorityFeePerGas); assert(BigNumber.isBigNumber(baseFeePerGas), `No baseFeePerGas received on ${getNetworkName(chainId)}`); + const scaledBaseFee = baseFeePerGas.mul(baseFeeMultiplier); return { - maxFeePerGas: maxPriorityFeePerGas.add(baseFeePerGas).mul(baseFeeMultiplier), + maxFeePerGas: maxPriorityFeePerGas.add(scaledBaseFee), maxPriorityFeePerGas, }; } diff --git a/src/gasPriceOracle/adapters/linea-viem.ts b/src/gasPriceOracle/adapters/linea-viem.ts index cab1de5e..b10a6f5c 100644 --- a/src/gasPriceOracle/adapters/linea-viem.ts +++ b/src/gasPriceOracle/adapters/linea-viem.ts @@ -1,23 +1,39 @@ -import { Address, PublicClient } from "viem"; +import { Address, Hex, PublicClient } from "viem"; import { estimateGas } from "viem/linea"; import { DEFAULT_SIMULATED_RELAYER_ADDRESS as account } from "../../constants"; import { InternalGasPriceEstimate } from "../types"; import { PopulatedTransaction } from "ethers"; +/** + * @notice The Linea viem provider calls the linea_estimateGas RPC endpoint to estimate gas. Linea is unique + * in that the recommended fee per gas is hardcoded to 7 gwei while the priority fee is dynamic based on the + * compressed transaction size, layer 1 verification costs and capacity, gas price ratio between layer 1 and layer 2, + * the transaction's gas usage, the minimum gas price on layer 2, + * and a minimum margin (for error) for gas price estimation. + * @dev Because the Linea priority fee is more volatile than the base fee, the base fee multiplier will be applied + * to the priority fee. + * @param provider + * @param _chainId + * @param baseFeeMultiplier + * @param _unsignedTx + * @returns + */ export async function eip1559( provider: PublicClient, _chainId: number, baseFeeMultiplier: number, _unsignedTx?: PopulatedTransaction ): Promise { - const { baseFeePerGas, priorityFeePerGas } = await estimateGas(provider, { + const { baseFeePerGas, priorityFeePerGas: _priorityFeePerGas } = await estimateGas(provider, { account: (_unsignedTx?.from as Address) ?? account, to: (_unsignedTx?.to as Address) ?? account, - value: BigInt(_unsignedTx?.value?.toString() || "1"), + value: BigInt(_unsignedTx?.value?.toString() ?? "1"), + data: (_unsignedTx?.data as Hex) ?? "0x", }); + const priorityFeePerGas = _priorityFeePerGas * BigInt(baseFeeMultiplier); return { - maxFeePerGas: baseFeePerGas * BigInt(baseFeeMultiplier) + priorityFeePerGas, + maxFeePerGas: baseFeePerGas + priorityFeePerGas, maxPriorityFeePerGas: priorityFeePerGas, }; } diff --git a/src/gasPriceOracle/adapters/polygon-viem.ts b/src/gasPriceOracle/adapters/polygon-viem.ts deleted file mode 100644 index 9923a404..00000000 --- a/src/gasPriceOracle/adapters/polygon-viem.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { PublicClient } from "viem"; -import { BaseHTTPAdapter, BaseHTTPAdapterArgs } from "../../priceClient/adapters/baseAdapter"; -import { isDefined } from "../../utils"; -import { CHAIN_IDs } from "../../constants"; -import { InternalGasPriceEstimate } from "../types"; -import { gasPriceError } from "../util"; -import { eip1559 } from "./ethereum-viem"; - -type Polygon1559GasPrice = { - maxPriorityFee: number | string; - maxFee: number | string; -}; - -type GasStationV2Response = { - safeLow: Polygon1559GasPrice; - standard: Polygon1559GasPrice; - fast: Polygon1559GasPrice; - estimatedBaseFee: number | string; - blockTime: number | string; - blockNumber: number | string; -}; - -type GasStationArgs = BaseHTTPAdapterArgs & { - chainId?: number; - host?: string; -}; - -const { POLYGON } = CHAIN_IDs; - -const GWEI = BigInt(1_000_000_000); -class PolygonGasStation extends BaseHTTPAdapter { - readonly chainId: number; - - constructor({ chainId = POLYGON, host, timeout = 1500, retries = 1 }: GasStationArgs = {}) { - host = host ?? chainId === POLYGON ? "gasstation.polygon.technology" : "gasstation-testnet.polygon.technology"; - - super("Polygon Gas Station", host, { timeout, retries }); - this.chainId = chainId; - } - - async getFeeData(strategy: "safeLow" | "standard" | "fast" = "fast"): Promise { - const gas = await this.query("v2", {}); - - const gasPrice = (gas as GasStationV2Response)?.[strategy]; - if (!this.isPolygon1559GasPrice(gasPrice)) { - // @todo: generalise gasPriceError() to accept a reason/cause? - gasPriceError("getFeeData()", this.chainId, gasPrice); - } - - const maxPriorityFeePerGas = BigInt(gasPrice.maxPriorityFee) * GWEI; - const maxFeePerGas = BigInt(gasPrice.maxFee) * GWEI; - - return { maxPriorityFeePerGas, maxFeePerGas }; - } - - protected isPolygon1559GasPrice(gasPrice: unknown): gasPrice is Polygon1559GasPrice { - if (!isDefined(gasPrice)) { - return false; - } - const _gasPrice = gasPrice as Polygon1559GasPrice; - return [_gasPrice.maxPriorityFee, _gasPrice.maxFee].every((field) => ["number", "string"].includes(typeof field)); - } -} - -export async function gasStation( - provider: PublicClient, - chainId: number, - baseFeeMultiplier: number -): Promise { - const gasStation = new PolygonGasStation({ chainId, timeout: 2000, retries: 0 }); - let maxPriorityFeePerGas: bigint; - let maxFeePerGas: bigint; - try { - ({ maxPriorityFeePerGas, maxFeePerGas } = await gasStation.getFeeData()); - maxFeePerGas *= BigInt(baseFeeMultiplier); - } catch (err) { - // Fall back to the RPC provider. May be less accurate. - ({ maxPriorityFeePerGas, maxFeePerGas } = await eip1559(provider, chainId, baseFeeMultiplier)); - - // Per the GasStation docs, the minimum priority fee on Polygon is 30 Gwei. - // https://docs.polygon.technology/tools/gas/polygon-gas-station/#interpretation - const minPriorityFee = BigInt(30) * GWEI; - if (minPriorityFee > maxPriorityFeePerGas) { - const priorityDelta = minPriorityFee - maxPriorityFeePerGas; - maxPriorityFeePerGas = minPriorityFee; - maxFeePerGas = maxFeePerGas + priorityDelta; - } - } - - return { maxPriorityFeePerGas, maxFeePerGas }; -} diff --git a/src/gasPriceOracle/adapters/polygon.ts b/src/gasPriceOracle/adapters/polygon.ts index d69d832a..10713db7 100644 --- a/src/gasPriceOracle/adapters/polygon.ts +++ b/src/gasPriceOracle/adapters/polygon.ts @@ -77,10 +77,11 @@ export async function gasStation( let maxFeePerGas: BigNumber; try { ({ maxPriorityFeePerGas, maxFeePerGas } = await gasStation.getFeeData()); - maxFeePerGas = maxFeePerGas.mul(baseFeeMultiplier); + const baseFeeMinusPriorityFee = maxFeePerGas.sub(maxPriorityFeePerGas); + const scaledBaseFee = baseFeeMinusPriorityFee.mul(baseFeeMultiplier); + maxFeePerGas = scaledBaseFee.add(maxPriorityFeePerGas); } catch (err) { // Fall back to the RPC provider. May be less accurate. - // @dev Don't incorporate multiplier until after catch statement ({ maxPriorityFeePerGas, maxFeePerGas } = await eip1559(provider, chainId, baseFeeMultiplier)); // Per the GasStation docs, the minimum priority fee on Polygon is 30 Gwei. diff --git a/src/gasPriceOracle/oracle.ts b/src/gasPriceOracle/oracle.ts index 44fff7f8..0b2aeb67 100644 --- a/src/gasPriceOracle/oracle.ts +++ b/src/gasPriceOracle/oracle.ts @@ -9,9 +9,7 @@ import * as arbitrum from "./adapters/arbitrum"; import * as ethereum from "./adapters/ethereum"; import * as linea from "./adapters/linea"; import * as polygon from "./adapters/polygon"; -import * as arbitrumViem from "./adapters/arbitrum-viem"; import * as lineaViem from "./adapters/linea-viem"; -import * as polygonViem from "./adapters/polygon-viem"; interface GasPriceEstimateOptions { // baseFeeMultiplier Multiplier applied to base fee for EIP1559 gas prices (or total fee for legacy). @@ -133,10 +131,7 @@ export async function _getViemGasPriceEstimate( unsignedTx?: PopulatedTransaction ) => Promise > = { - [CHAIN_IDs.ALEPH_ZERO]: arbitrumViem.eip1559, - [CHAIN_IDs.ARBITRUM]: arbitrumViem.eip1559, [CHAIN_IDs.LINEA]: lineaViem.eip1559, - [CHAIN_IDs.POLYGON]: polygonViem.gasStation, } as const; let maxFeePerGas: bigint; diff --git a/src/utils/common.ts b/src/utils/common.ts index c9ab70f0..ca204875 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -267,7 +267,7 @@ export async function estimateTotalGasRequiredByUnsignedTransaction( gasUnits ? Promise.resolve(BigNumber.from(gasUnits)) : voidSigner.estimateGas(unsignedTx), _gasPrice ? Promise.resolve({ maxFeePerGas: _gasPrice }) - : getGasPriceEstimate(provider, { chainId, baseFeeMultiplier, transport }), + : getGasPriceEstimate(provider, { chainId, baseFeeMultiplier, transport, unsignedTx }), ] as const; const [nativeGasCost, { maxFeePerGas: gasPrice }] = await Promise.all(queries); assert(nativeGasCost.gt(bnZero), "Gas cost should not be 0"); diff --git a/test/GasPriceOracle.test.ts b/test/GasPriceOracle.test.ts index 7a6fae91..64867d6d 100644 --- a/test/GasPriceOracle.test.ts +++ b/test/GasPriceOracle.test.ts @@ -1,61 +1,151 @@ -// @note: This test is _not_ run automatically as part of git hooks or CI. +// @note: This test is more of an e2e test because the Ethers provider tests send real RPC requests +// but I wanted to include it in the unit tests to prevent regressions. We should create mocked +// providers and API's to avoid the API requests. + import dotenv from "dotenv"; +import winston from "winston"; import { providers } from "ethers"; +import { encodeFunctionData } from 'viem'; import { getGasPriceEstimate } from "../src/gasPriceOracle"; import { BigNumber, bnZero, parseUnits } from "../src/utils"; -import { expect, makeCustomTransport } from "../test/utils"; +import { expect, makeCustomTransport, randomAddress } from "../test/utils"; dotenv.config({ path: ".env" }); +const dummyLogger = winston.createLogger({ + level: "debug", + transports: [new winston.transports.Console()], +}); + const stdLastBaseFeePerGas = parseUnits("12", 9); const stdMaxPriorityFeePerGas = parseUnits("1", 9); // EIP-1559 chains only -const chainIds = [1, 10, 137, 324, 8453, 42161, 534352]; +const expectedLineaMaxFeePerGas = parseUnits("7", 9) +const ethersProviderChainIds = [1, 10, 137, 324, 8453, 42161, 534352, 59144]; +const viemProviderChainIds = [59144]; const customTransport = makeCustomTransport({ stdLastBaseFeePerGas, stdMaxPriorityFeePerGas }); const provider = new providers.StaticJsonRpcProvider("https://eth.llamarpc.com"); +const ERC20ABI = [ + { + inputs: [{ name: "to", type: "address" }, { name: "amount", type: "uint256" }], + name: "transfer", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, +]; +const erc20TransferTransactionObject = encodeFunctionData({ + abi: ERC20ABI, + functionName: "transfer", + args: [randomAddress(), 1n] +}) + describe("Gas Price Oracle", function () { - it("Gas Price Retrieval", async function () { - for (const chainId of chainIds) { + it("Viem gas price retrieval", async function () { + for (const chainId of viemProviderChainIds) { const chainKey = `NEW_GAS_PRICE_ORACLE_${chainId}`; process.env[chainKey] = "true"; - const { maxFeePerGas, maxPriorityFeePerGas } = await getGasPriceEstimate(provider, { - chainId, - transport: customTransport, - }); + if (chainId === 59144) { + // For Linea, works with and without passing in a custom Transaction object. + const unsignedTxns = [ + { + to: randomAddress(), + from: randomAddress(), + value: bnZero, + data: erc20TransferTransactionObject + }, + undefined + ] + const baseFeeMultiplier = 2.0; + for (const unsignedTx of unsignedTxns) { + const { maxFeePerGas, maxPriorityFeePerGas } = await getGasPriceEstimate(provider, { + chainId, + transport: customTransport, + unsignedTx, + baseFeeMultiplier + }); + + dummyLogger.debug({ + at: "Viem: Gas Price Oracle", + message: `Retrieved gas price estimate for chain ID ${chainId}`, + maxFeePerGas: maxFeePerGas.toString(), + maxPriorityFeePerGas: maxPriorityFeePerGas.toString(), + unsignedTx + }); - expect(BigNumber.isBigNumber(maxFeePerGas)).to.be.true; - expect(BigNumber.isBigNumber(maxPriorityFeePerGas)).to.be.true; - - if (chainId === 137) { - // The Polygon gas station isn't mocked, so just ensure that the fees have a valid relationship. - expect(maxFeePerGas.gt(0)).to.be.true; - expect(maxPriorityFeePerGas.gt(0)).to.be.true; - expect(maxPriorityFeePerGas.lt(maxFeePerGas)).to.be.true; - } else if (chainId === 42161) { - // Arbitrum priority fees are refunded, so drop the priority fee from estimates. - // Expect a 1.2x multiplier on the last base fee. - expect(maxFeePerGas.eq(stdLastBaseFeePerGas.mul("120").div("100").add(1))).to.be.true; - expect(maxPriorityFeePerGas.eq(1)).to.be.true; - } else { - expect(maxFeePerGas.gt(bnZero)).to.be.true; - expect(maxPriorityFeePerGas.gt(bnZero)).to.be.true; + expect(BigNumber.isBigNumber(maxFeePerGas)).to.be.true; + expect(BigNumber.isBigNumber(maxPriorityFeePerGas)).to.be.true; + + // For Linea, base fee is expected to be hardcoded and unaffected by the base fee multiplier while + // the priority fee gets scaled. + const expectedPriorityFee = stdMaxPriorityFeePerGas.mul(2.0); + expect(maxFeePerGas).to.equal(expectedLineaMaxFeePerGas.add(expectedPriorityFee)); + expect(maxPriorityFeePerGas).to.equal(expectedPriorityFee) + } + } - delete process.env[chainKey]; } }); - it("Ethers: applies markup to maxFeePerGas", async function () { - for (const chainId of chainIds) { - const { maxFeePerGas: markedUpMaxFeePerGas, maxPriorityFeePerGas: markedUpMaxPriorityFeePerGas } = - await getGasPriceEstimate(provider, { chainId, baseFeeMultiplier: 2, transport: customTransport }); - const { maxFeePerGas, maxPriorityFeePerGas } = await getGasPriceEstimate(provider, { - chainId, - baseFeeMultiplier: 1, - transport: customTransport, + it("Ethers gas price retrieval", async function () { + // TODO: Make this test less flaky by creating a mocked Ethers provider as well + // as a fake Polygon gas station API, so it doesn't send real RPC requests. + + const baseFeeMultiplier = 2.0; + // For this test, we only use the raw gas price feed for ethereum just so we can + // test both the bad and raw variants, since other chains will ultimately call the Etheruem + // adapter. + const eip1559RawGasPriceFeedChainIds = [1]; + for (const chainId of ethersProviderChainIds) { + if (eip1559RawGasPriceFeedChainIds.includes(chainId)) { + const chainKey = `GAS_PRICE_EIP1559_RAW_${chainId}`; + process.env[chainKey] = "true"; + } + const [ + { maxFeePerGas: markedUpMaxFeePerGas, maxPriorityFeePerGas: markedUpMaxPriorityFeePerGas }, + { maxFeePerGas, maxPriorityFeePerGas } + ] = + await Promise.all([ + getGasPriceEstimate(provider, { chainId, baseFeeMultiplier, transport: customTransport }), + getGasPriceEstimate(provider, { chainId, baseFeeMultiplier: 1.0, transport: customTransport }), + ] + ); + + dummyLogger.debug({ + at: "Ethers: Gas Price Oracle", + message: `Retrieved gas price estimate for chain ID ${chainId}`, + maxFeePerGas: maxFeePerGas.toString(), + maxPriorityFeePerGas: maxPriorityFeePerGas.toString(), + markedUpMaxFeePerGas: markedUpMaxFeePerGas.toString(), + markedUpMaxPriorityFeePerGas: markedUpMaxPriorityFeePerGas.toString() }); + + expect(BigNumber.isBigNumber(maxFeePerGas)).to.be.true; + expect(BigNumber.isBigNumber(maxPriorityFeePerGas)).to.be.true; + + // @dev: The following tests *might* be flaky because the above two getGasPriceEstimate + // calls are technically two separate API calls and the suggested base and priority fees + // might be different. In practice, the fees rarely change when called in rapid succession. + + // Base fee should be multiplied by multiplier. Returned max fee includes priority fee + // so back it ou. + const expectedMarkedUpMaxFeePerGas = (maxFeePerGas.sub(maxPriorityFeePerGas)).mul(2) + expect(markedUpMaxFeePerGas.sub(markedUpMaxPriorityFeePerGas)).to.equal(expectedMarkedUpMaxFeePerGas); expect(markedUpMaxFeePerGas.gt(maxFeePerGas)).to.be.true; - expect(markedUpMaxPriorityFeePerGas).to.equal(maxPriorityFeePerGas); + + // Priority fees should be the same + expect(markedUpMaxPriorityFeePerGas).to.equal(maxPriorityFeePerGas) + + if (chainId === 42161) { + // Arbitrum priority fee should be 1 wei. + expect(markedUpMaxPriorityFeePerGas).to.equal(1); + expect(maxPriorityFeePerGas).to.equal(1); + } + if (eip1559RawGasPriceFeedChainIds.includes(chainId)) { + const chainKey = `GAS_PRICE_EIP1559_RAW_${chainId}`; + delete process.env[chainKey]; + } } }); }); diff --git a/test/utils/transport.ts b/test/utils/transport.ts index 38915d97..24f7d5d8 100644 --- a/test/utils/transport.ts +++ b/test/utils/transport.ts @@ -18,6 +18,13 @@ export const makeCustomTransport = ( return { baseFeePerGas: BigInt(stdLastBaseFeePerGas.toString()) }; case "eth_maxPriorityFeePerGas": return BigInt(stdMaxPriorityFeePerGas.toString()); + case "linea_estimateGas": + return { + // Linea base fee is always 7 gwei + baseFeePerGas: BigInt(parseUnits("7", 9).toString()), + priorityFeePerGas: BigInt(stdMaxPriorityFeePerGas.toString()), + gasLimit: BigInt("0") + }; default: throw new Error(`Unsupported method: ${method}.`); } From de2a3390b0cdb47e7ac657c4e463bec267d26061 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Wed, 25 Dec 2024 16:04:41 -0500 Subject: [PATCH 06/10] lint --- src/gasPriceOracle/adapters/linea-viem.ts | 12 ++--- test/GasPriceOracle.test.ts | 62 +++++++++++------------ test/utils/transport.ts | 2 +- 3 files changed, 38 insertions(+), 38 deletions(-) diff --git a/src/gasPriceOracle/adapters/linea-viem.ts b/src/gasPriceOracle/adapters/linea-viem.ts index b10a6f5c..d97bf188 100644 --- a/src/gasPriceOracle/adapters/linea-viem.ts +++ b/src/gasPriceOracle/adapters/linea-viem.ts @@ -9,14 +9,14 @@ import { PopulatedTransaction } from "ethers"; * in that the recommended fee per gas is hardcoded to 7 gwei while the priority fee is dynamic based on the * compressed transaction size, layer 1 verification costs and capacity, gas price ratio between layer 1 and layer 2, * the transaction's gas usage, the minimum gas price on layer 2, - * and a minimum margin (for error) for gas price estimation. + * and a minimum margin (for error) for gas price estimation. * @dev Because the Linea priority fee is more volatile than the base fee, the base fee multiplier will be applied * to the priority fee. - * @param provider - * @param _chainId - * @param baseFeeMultiplier - * @param _unsignedTx - * @returns + * @param provider + * @param _chainId + * @param baseFeeMultiplier + * @param _unsignedTx + * @returns */ export async function eip1559( provider: PublicClient, diff --git a/test/GasPriceOracle.test.ts b/test/GasPriceOracle.test.ts index 64867d6d..9a483dba 100644 --- a/test/GasPriceOracle.test.ts +++ b/test/GasPriceOracle.test.ts @@ -5,7 +5,7 @@ import dotenv from "dotenv"; import winston from "winston"; import { providers } from "ethers"; -import { encodeFunctionData } from 'viem'; +import { encodeFunctionData } from "viem"; import { getGasPriceEstimate } from "../src/gasPriceOracle"; import { BigNumber, bnZero, parseUnits } from "../src/utils"; import { expect, makeCustomTransport, randomAddress } from "../test/utils"; @@ -18,7 +18,7 @@ const dummyLogger = winston.createLogger({ const stdLastBaseFeePerGas = parseUnits("12", 9); const stdMaxPriorityFeePerGas = parseUnits("1", 9); // EIP-1559 chains only -const expectedLineaMaxFeePerGas = parseUnits("7", 9) +const expectedLineaMaxFeePerGas = parseUnits("7", 9); const ethersProviderChainIds = [1, 10, 137, 324, 8453, 42161, 534352, 59144]; const viemProviderChainIds = [59144]; @@ -28,7 +28,10 @@ const provider = new providers.StaticJsonRpcProvider("https://eth.llamarpc.com") const ERC20ABI = [ { - inputs: [{ name: "to", type: "address" }, { name: "amount", type: "uint256" }], + inputs: [ + { name: "to", type: "address" }, + { name: "amount", type: "uint256" }, + ], name: "transfer", outputs: [], stateMutability: "nonpayable", @@ -38,8 +41,8 @@ const ERC20ABI = [ const erc20TransferTransactionObject = encodeFunctionData({ abi: ERC20ABI, functionName: "transfer", - args: [randomAddress(), 1n] -}) + args: [randomAddress(), 1n], +}); describe("Gas Price Oracle", function () { it("Viem gas price retrieval", async function () { @@ -53,37 +56,36 @@ describe("Gas Price Oracle", function () { to: randomAddress(), from: randomAddress(), value: bnZero, - data: erc20TransferTransactionObject + data: erc20TransferTransactionObject, }, - undefined - ] + undefined, + ]; const baseFeeMultiplier = 2.0; for (const unsignedTx of unsignedTxns) { const { maxFeePerGas, maxPriorityFeePerGas } = await getGasPriceEstimate(provider, { chainId, transport: customTransport, unsignedTx, - baseFeeMultiplier + baseFeeMultiplier, }); - + dummyLogger.debug({ at: "Viem: Gas Price Oracle", message: `Retrieved gas price estimate for chain ID ${chainId}`, maxFeePerGas: maxFeePerGas.toString(), maxPriorityFeePerGas: maxPriorityFeePerGas.toString(), - unsignedTx + unsignedTx, }); expect(BigNumber.isBigNumber(maxFeePerGas)).to.be.true; expect(BigNumber.isBigNumber(maxPriorityFeePerGas)).to.be.true; - + // For Linea, base fee is expected to be hardcoded and unaffected by the base fee multiplier while // the priority fee gets scaled. const expectedPriorityFee = stdMaxPriorityFeePerGas.mul(2.0); expect(maxFeePerGas).to.equal(expectedLineaMaxFeePerGas.add(expectedPriorityFee)); - expect(maxPriorityFeePerGas).to.equal(expectedPriorityFee) + expect(maxPriorityFeePerGas).to.equal(expectedPriorityFee); } - } delete process.env[chainKey]; } @@ -93,24 +95,22 @@ describe("Gas Price Oracle", function () { // as a fake Polygon gas station API, so it doesn't send real RPC requests. const baseFeeMultiplier = 2.0; - // For this test, we only use the raw gas price feed for ethereum just so we can + // For this test, we only use the raw gas price feed for ethereum just so we can // test both the bad and raw variants, since other chains will ultimately call the Etheruem // adapter. const eip1559RawGasPriceFeedChainIds = [1]; for (const chainId of ethersProviderChainIds) { if (eip1559RawGasPriceFeedChainIds.includes(chainId)) { const chainKey = `GAS_PRICE_EIP1559_RAW_${chainId}`; - process.env[chainKey] = "true"; + process.env[chainKey] = "true"; } const [ { maxFeePerGas: markedUpMaxFeePerGas, maxPriorityFeePerGas: markedUpMaxPriorityFeePerGas }, - { maxFeePerGas, maxPriorityFeePerGas } - ] = - await Promise.all([ - getGasPriceEstimate(provider, { chainId, baseFeeMultiplier, transport: customTransport }), - getGasPriceEstimate(provider, { chainId, baseFeeMultiplier: 1.0, transport: customTransport }), - ] - ); + { maxFeePerGas, maxPriorityFeePerGas }, + ] = await Promise.all([ + getGasPriceEstimate(provider, { chainId, baseFeeMultiplier, transport: customTransport }), + getGasPriceEstimate(provider, { chainId, baseFeeMultiplier: 1.0, transport: customTransport }), + ]); dummyLogger.debug({ at: "Ethers: Gas Price Oracle", @@ -118,30 +118,30 @@ describe("Gas Price Oracle", function () { maxFeePerGas: maxFeePerGas.toString(), maxPriorityFeePerGas: maxPriorityFeePerGas.toString(), markedUpMaxFeePerGas: markedUpMaxFeePerGas.toString(), - markedUpMaxPriorityFeePerGas: markedUpMaxPriorityFeePerGas.toString() + markedUpMaxPriorityFeePerGas: markedUpMaxPriorityFeePerGas.toString(), }); expect(BigNumber.isBigNumber(maxFeePerGas)).to.be.true; expect(BigNumber.isBigNumber(maxPriorityFeePerGas)).to.be.true; - + // @dev: The following tests *might* be flaky because the above two getGasPriceEstimate // calls are technically two separate API calls and the suggested base and priority fees // might be different. In practice, the fees rarely change when called in rapid succession. - // Base fee should be multiplied by multiplier. Returned max fee includes priority fee + // Base fee should be multiplied by multiplier. Returned max fee includes priority fee // so back it ou. - const expectedMarkedUpMaxFeePerGas = (maxFeePerGas.sub(maxPriorityFeePerGas)).mul(2) + const expectedMarkedUpMaxFeePerGas = maxFeePerGas.sub(maxPriorityFeePerGas).mul(2); expect(markedUpMaxFeePerGas.sub(markedUpMaxPriorityFeePerGas)).to.equal(expectedMarkedUpMaxFeePerGas); expect(markedUpMaxFeePerGas.gt(maxFeePerGas)).to.be.true; - + // Priority fees should be the same - expect(markedUpMaxPriorityFeePerGas).to.equal(maxPriorityFeePerGas) - + expect(markedUpMaxPriorityFeePerGas).to.equal(maxPriorityFeePerGas); + if (chainId === 42161) { // Arbitrum priority fee should be 1 wei. expect(markedUpMaxPriorityFeePerGas).to.equal(1); expect(maxPriorityFeePerGas).to.equal(1); - } + } if (eip1559RawGasPriceFeedChainIds.includes(chainId)) { const chainKey = `GAS_PRICE_EIP1559_RAW_${chainId}`; delete process.env[chainKey]; diff --git a/test/utils/transport.ts b/test/utils/transport.ts index 24f7d5d8..660f15eb 100644 --- a/test/utils/transport.ts +++ b/test/utils/transport.ts @@ -23,7 +23,7 @@ export const makeCustomTransport = ( // Linea base fee is always 7 gwei baseFeePerGas: BigInt(parseUnits("7", 9).toString()), priorityFeePerGas: BigInt(stdMaxPriorityFeePerGas.toString()), - gasLimit: BigInt("0") + gasLimit: BigInt("0"), }; default: throw new Error(`Unsupported method: ${method}.`); From e913c7501d2b951b1f8f7ee8be13928adeed1438 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Thu, 26 Dec 2024 09:04:16 -0500 Subject: [PATCH 07/10] gwei to wei --- src/gasPriceOracle/adapters/linea-viem.ts | 1 + test/GasPriceOracle.test.ts | 6 +++++- test/utils/transport.ts | 4 ++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/gasPriceOracle/adapters/linea-viem.ts b/src/gasPriceOracle/adapters/linea-viem.ts index d97bf188..4eb897ae 100644 --- a/src/gasPriceOracle/adapters/linea-viem.ts +++ b/src/gasPriceOracle/adapters/linea-viem.ts @@ -10,6 +10,7 @@ import { PopulatedTransaction } from "ethers"; * compressed transaction size, layer 1 verification costs and capacity, gas price ratio between layer 1 and layer 2, * the transaction's gas usage, the minimum gas price on layer 2, * and a minimum margin (for error) for gas price estimation. + * Source: https://docs.linea.build/get-started/how-to/gas-fees#how-gas-works-on-linea * @dev Because the Linea priority fee is more volatile than the base fee, the base fee multiplier will be applied * to the priority fee. * @param provider diff --git a/test/GasPriceOracle.test.ts b/test/GasPriceOracle.test.ts index 9a483dba..d98a12ac 100644 --- a/test/GasPriceOracle.test.ts +++ b/test/GasPriceOracle.test.ts @@ -18,7 +18,7 @@ const dummyLogger = winston.createLogger({ const stdLastBaseFeePerGas = parseUnits("12", 9); const stdMaxPriorityFeePerGas = parseUnits("1", 9); // EIP-1559 chains only -const expectedLineaMaxFeePerGas = parseUnits("7", 9); +const expectedLineaMaxFeePerGas = BigNumber.from("7"); const ethersProviderChainIds = [1, 10, 137, 324, 8453, 42161, 534352, 59144]; const viemProviderChainIds = [59144]; @@ -142,6 +142,10 @@ describe("Gas Price Oracle", function () { expect(markedUpMaxPriorityFeePerGas).to.equal(1); expect(maxPriorityFeePerGas).to.equal(1); } + if (chainId === 324 || chainId === 534352) { + // Scroll and ZkSync use legacy pricing so priority fee should be 0. + expect(maxPriorityFeePerGas).to.equal(0); + } if (eip1559RawGasPriceFeedChainIds.includes(chainId)) { const chainKey = `GAS_PRICE_EIP1559_RAW_${chainId}`; delete process.env[chainKey]; diff --git a/test/utils/transport.ts b/test/utils/transport.ts index 660f15eb..5ffd7d59 100644 --- a/test/utils/transport.ts +++ b/test/utils/transport.ts @@ -20,8 +20,8 @@ export const makeCustomTransport = ( return BigInt(stdMaxPriorityFeePerGas.toString()); case "linea_estimateGas": return { - // Linea base fee is always 7 gwei - baseFeePerGas: BigInt(parseUnits("7", 9).toString()), + // Linea base fee is always 7 wei + baseFeePerGas: BigInt(7), priorityFeePerGas: BigInt(stdMaxPriorityFeePerGas.toString()), gasLimit: BigInt("0"), }; From ea1ee66a39f4684dac27b4a4e9e32eaf7fec36f9 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Thu, 26 Dec 2024 10:39:52 -0500 Subject: [PATCH 08/10] Add unit tests, refactor all methods --- src/gasPriceOracle/adapters/arbitrum.ts | 13 +- src/gasPriceOracle/adapters/ethereum.ts | 20 +- src/gasPriceOracle/adapters/linea-viem.ts | 15 +- src/gasPriceOracle/adapters/linea.ts | 9 +- src/gasPriceOracle/adapters/polygon.ts | 29 ++- src/gasPriceOracle/oracle.ts | 72 ++---- src/utils/common.ts | 1 + test/GasPriceOracle.test.ts | 268 +++++++++++++--------- test/utils/provider.ts | 59 +++++ test/utils/transport.ts | 13 +- 10 files changed, 298 insertions(+), 201 deletions(-) create mode 100644 test/utils/provider.ts diff --git a/src/gasPriceOracle/adapters/arbitrum.ts b/src/gasPriceOracle/adapters/arbitrum.ts index d4f89977..b76945eb 100644 --- a/src/gasPriceOracle/adapters/arbitrum.ts +++ b/src/gasPriceOracle/adapters/arbitrum.ts @@ -2,19 +2,12 @@ import { providers } from "ethers"; import { bnOne } from "../../utils"; import { GasPriceEstimate } from "../types"; import * as ethereum from "./ethereum"; +import { GasPriceEstimateOptions } from "../oracle"; // Arbitrum Nitro implements EIP-1559 pricing, but the priority fee is always refunded to the caller. // Reference: https://docs.arbitrum.io/how-arbitrum-works/gas-fees -export async function eip1559( - provider: providers.Provider, - chainId: number, - baseFeeMultiplier: number -): Promise { - const { maxFeePerGas: _maxFeePerGas, maxPriorityFeePerGas } = await ethereum.eip1559( - provider, - chainId, - baseFeeMultiplier - ); +export async function eip1559(provider: providers.Provider, opts: GasPriceEstimateOptions): Promise { + const { maxFeePerGas: _maxFeePerGas, maxPriorityFeePerGas } = await ethereum.eip1559(provider, opts); // eip1559() sets maxFeePerGas = lastBaseFeePerGas + maxPriorityFeePerGas, so revert that. // The caller may apply scaling as they wish afterwards. diff --git a/src/gasPriceOracle/adapters/ethereum.ts b/src/gasPriceOracle/adapters/ethereum.ts index a9330b1c..eb8ba21f 100644 --- a/src/gasPriceOracle/adapters/ethereum.ts +++ b/src/gasPriceOracle/adapters/ethereum.ts @@ -3,19 +3,18 @@ import { providers } from "ethers"; import { BigNumber, bnZero, getNetworkName } from "../../utils"; import { GasPriceEstimate } from "../types"; import { gasPriceError } from "../util"; +import { GasPriceEstimateOptions } from "../oracle"; /** * @param provider ethers RPC provider instance. * @param chainId Chain ID of provider instance. * @returns Promise of gas price estimate object. */ -export function eip1559( - provider: providers.Provider, - chainId: number, - baseFeeMultiplier: number -): Promise { - const useRaw = process.env[`GAS_PRICE_EIP1559_RAW_${chainId}`] === "true"; - return useRaw ? eip1559Raw(provider, chainId, baseFeeMultiplier) : eip1559Bad(provider, chainId, baseFeeMultiplier); +export function eip1559(provider: providers.Provider, opts: GasPriceEstimateOptions): Promise { + const useRaw = process.env[`GAS_PRICE_EIP1559_RAW_${opts.chainId}`] === "true"; + return useRaw + ? eip1559Raw(provider, opts.chainId, opts.baseFeeMultiplier) + : eip1559Bad(provider, opts.chainId, opts.baseFeeMultiplier); } /** @@ -67,11 +66,8 @@ export async function eip1559Bad( return { maxPriorityFeePerGas, maxFeePerGas }; } -export async function legacy( - provider: providers.Provider, - chainId: number, - baseFeeMultiplier: number -): Promise { +export async function legacy(provider: providers.Provider, opts: GasPriceEstimateOptions): Promise { + const { chainId, baseFeeMultiplier } = opts; const gasPrice = await provider.getGasPrice(); if (!BigNumber.isBigNumber(gasPrice) || gasPrice.lt(bnZero)) gasPriceError("getGasPrice()", chainId, gasPrice); diff --git a/src/gasPriceOracle/adapters/linea-viem.ts b/src/gasPriceOracle/adapters/linea-viem.ts index 4eb897ae..8a7df135 100644 --- a/src/gasPriceOracle/adapters/linea-viem.ts +++ b/src/gasPriceOracle/adapters/linea-viem.ts @@ -2,7 +2,7 @@ import { Address, Hex, PublicClient } from "viem"; import { estimateGas } from "viem/linea"; import { DEFAULT_SIMULATED_RELAYER_ADDRESS as account } from "../../constants"; import { InternalGasPriceEstimate } from "../types"; -import { PopulatedTransaction } from "ethers"; +import { GasPriceEstimateOptions } from "../oracle"; /** * @notice The Linea viem provider calls the linea_estimateGas RPC endpoint to estimate gas. Linea is unique @@ -21,15 +21,14 @@ import { PopulatedTransaction } from "ethers"; */ export async function eip1559( provider: PublicClient, - _chainId: number, - baseFeeMultiplier: number, - _unsignedTx?: PopulatedTransaction + opts: GasPriceEstimateOptions ): Promise { + const { unsignedTx, baseFeeMultiplier } = opts; const { baseFeePerGas, priorityFeePerGas: _priorityFeePerGas } = await estimateGas(provider, { - account: (_unsignedTx?.from as Address) ?? account, - to: (_unsignedTx?.to as Address) ?? account, - value: BigInt(_unsignedTx?.value?.toString() ?? "1"), - data: (_unsignedTx?.data as Hex) ?? "0x", + account: (unsignedTx?.from as Address) ?? account, + to: (unsignedTx?.to as Address) ?? account, + value: BigInt(unsignedTx?.value?.toString() ?? "1"), + data: (unsignedTx?.data as Hex) ?? "0x", }); const priorityFeePerGas = _priorityFeePerGas * BigInt(baseFeeMultiplier); diff --git a/src/gasPriceOracle/adapters/linea.ts b/src/gasPriceOracle/adapters/linea.ts index fbbaf7d1..369a1198 100644 --- a/src/gasPriceOracle/adapters/linea.ts +++ b/src/gasPriceOracle/adapters/linea.ts @@ -5,11 +5,8 @@ import { providers } from "ethers"; import { GasPriceEstimate } from "../types"; import * as ethereum from "./ethereum"; +import { GasPriceEstimateOptions } from "../oracle"; -export function eip1559( - provider: providers.Provider, - chainId: number, - baseFeeMultiplier: number -): Promise { - return ethereum.legacy(provider, chainId, baseFeeMultiplier); +export function eip1559(provider: providers.Provider, opts: GasPriceEstimateOptions): Promise { + return ethereum.legacy(provider, opts); } diff --git a/src/gasPriceOracle/adapters/polygon.ts b/src/gasPriceOracle/adapters/polygon.ts index 10713db7..2119749b 100644 --- a/src/gasPriceOracle/adapters/polygon.ts +++ b/src/gasPriceOracle/adapters/polygon.ts @@ -5,6 +5,7 @@ import { CHAIN_IDs } from "../../constants"; import { GasPriceEstimate } from "../types"; import { gasPriceError } from "../util"; import { eip1559 } from "./ethereum"; +import { GasPriceEstimateOptions } from "../oracle"; type Polygon1559GasPrice = { maxPriorityFee: number | string; @@ -27,7 +28,7 @@ type GasStationArgs = BaseHTTPAdapterArgs & { const { POLYGON } = CHAIN_IDs; -class PolygonGasStation extends BaseHTTPAdapter { +export class PolygonGasStation extends BaseHTTPAdapter { readonly chainId: number; constructor({ chainId = POLYGON, host, timeout = 1500, retries = 1 }: GasStationArgs = {}) { @@ -67,12 +68,30 @@ class PolygonGasStation extends BaseHTTPAdapter { } } +export class MockPolygonGasStation extends PolygonGasStation { + constructor( + readonly baseFee: BigNumber, + readonly priorityFee: BigNumber, + readonly getFeeDataThrows = false + ) { + super(); + } + + getFeeData(): Promise { + if (this.getFeeDataThrows) throw new Error(); + return Promise.resolve({ + maxPriorityFeePerGas: this.priorityFee, + maxFeePerGas: this.baseFee.add(this.priorityFee), + }); + } +} + export async function gasStation( provider: providers.Provider, - chainId: number, - baseFeeMultiplier: number + opts: GasPriceEstimateOptions ): Promise { - const gasStation = new PolygonGasStation({ chainId: chainId, timeout: 2000, retries: 0 }); + const { chainId, baseFeeMultiplier, polygonGasStation } = opts; + const gasStation = polygonGasStation ?? new PolygonGasStation({ chainId: chainId, timeout: 2000, retries: 0 }); let maxPriorityFeePerGas: BigNumber; let maxFeePerGas: BigNumber; try { @@ -82,7 +101,7 @@ export async function gasStation( maxFeePerGas = scaledBaseFee.add(maxPriorityFeePerGas); } catch (err) { // Fall back to the RPC provider. May be less accurate. - ({ maxPriorityFeePerGas, maxFeePerGas } = await eip1559(provider, chainId, baseFeeMultiplier)); + ({ maxPriorityFeePerGas, maxFeePerGas } = await eip1559(provider, opts)); // Per the GasStation docs, the minimum priority fee on Polygon is 30 Gwei. // https://docs.polygon.technology/tools/gas/polygon-gas-station/#interpretation diff --git a/src/gasPriceOracle/oracle.ts b/src/gasPriceOracle/oracle.ts index 0b2aeb67..b0ac1e82 100644 --- a/src/gasPriceOracle/oracle.ts +++ b/src/gasPriceOracle/oracle.ts @@ -10,29 +10,24 @@ import * as ethereum from "./adapters/ethereum"; import * as linea from "./adapters/linea"; import * as polygon from "./adapters/polygon"; import * as lineaViem from "./adapters/linea-viem"; +import { PolygonGasStation } from "./adapters/polygon"; -interface GasPriceEstimateOptions { +export interface GasPriceEstimateOptions { // baseFeeMultiplier Multiplier applied to base fee for EIP1559 gas prices (or total fee for legacy). baseFeeMultiplier: number; // legacyFallback In the case of an unrecognized chain, fall back to type 0 gas estimation. legacyFallback: boolean; // chainId The chain ID to query for gas prices. If omitted can be inferred by provider. - chainId?: number; - // unsignedTx The unsigned transaction used for simulation by Viem provider to produce the priority gas fee. + chainId: number; + // unsignedTx The unsigned transaction used for simulation by Linea's Viem provider to produce the priority gas fee. unsignedTx?: PopulatedTransaction; - // transport Viem Transport object to use for querying gas fees. + // transport Viem Transport object to use for querying gas fees used for testing. transport?: Transport; + // polygonGasStation Custom Polygon GasStation class used for testing. + polygonGasStation?: PolygonGasStation; } -interface EthersGasPriceEstimateOptions extends GasPriceEstimateOptions { - chainId: number; -} - -interface ViemGasPriceEstimateOptions extends Partial { - baseFeeMultiplier: number; -} - -const GAS_PRICE_ESTIMATE_DEFAULTS: GasPriceEstimateOptions = { +const GAS_PRICE_ESTIMATE_DEFAULTS = { baseFeeMultiplier: 1, legacyFallback: true, }; @@ -47,32 +42,23 @@ export async function getGasPriceEstimate( provider: providers.Provider, opts: Partial ): Promise { - const { - baseFeeMultiplier, - chainId: _chainId, - unsignedTx, - transport, - legacyFallback, - }: GasPriceEstimateOptions = { - ...GAS_PRICE_ESTIMATE_DEFAULTS, - ...opts, - }; + const baseFeeMultiplier = opts.baseFeeMultiplier ?? GAS_PRICE_ESTIMATE_DEFAULTS.baseFeeMultiplier; assert( baseFeeMultiplier >= 1.0 && baseFeeMultiplier <= 5, `Require 1.0 < base fee multiplier (${baseFeeMultiplier}) <= 5.0 for a total gas multiplier within [+1.0, +5.0]` ); - - const chainId = _chainId ?? (await provider.getNetwork()).chainId; + const chainId = opts.chainId ?? (await provider.getNetwork()).chainId; + const optsWithDefaults: GasPriceEstimateOptions = { + ...GAS_PRICE_ESTIMATE_DEFAULTS, + ...opts, + chainId, + }; // We only use the unsignedTx in the viem flow. const useViem = process.env[`NEW_GAS_PRICE_ORACLE_${chainId}`] === "true"; return useViem - ? _getViemGasPriceEstimate(chainId, { baseFeeMultiplier, unsignedTx, transport }) - : _getEthersGasPriceEstimate(provider, { - baseFeeMultiplier, - chainId, - legacyFallback, - }); + ? _getViemGasPriceEstimate(chainId, optsWithDefaults) + : _getEthersGasPriceEstimate(provider, optsWithDefaults); } /** @@ -84,9 +70,9 @@ export async function getGasPriceEstimate( */ function _getEthersGasPriceEstimate( provider: providers.Provider, - opts: EthersGasPriceEstimateOptions + opts: GasPriceEstimateOptions ): Promise { - const { baseFeeMultiplier, chainId, legacyFallback } = opts; + const { chainId, legacyFallback } = opts; const gasPriceFeeds = { [CHAIN_IDs.ALEPH_ZERO]: arbitrum.eip1559, @@ -102,7 +88,7 @@ function _getEthersGasPriceEstimate( assert(gasPriceFeed || legacyFallback, `No suitable gas price oracle for Chain ID ${chainId}`); gasPriceFeed ??= chainIsOPStack(chainId) ? ethereum.eip1559 : ethereum.legacy; - return gasPriceFeed(provider, chainId, baseFeeMultiplier); + return gasPriceFeed(provider, opts); } /** @@ -114,9 +100,9 @@ function _getEthersGasPriceEstimate( */ export async function _getViemGasPriceEstimate( providerOrChainId: providers.Provider | number, - opts: ViemGasPriceEstimateOptions + opts: GasPriceEstimateOptions ): Promise { - const { baseFeeMultiplier, unsignedTx, transport } = opts; + const { baseFeeMultiplier, transport } = opts; const chainId = typeof providerOrChainId === "number" ? providerOrChainId : (await providerOrChainId.getNetwork()).chainId; @@ -124,12 +110,7 @@ export async function _getViemGasPriceEstimate( const gasPriceFeeds: Record< number, - ( - provider: PublicClient, - chainId: number, - baseFeeMultiplier: number, - unsignedTx?: PopulatedTransaction - ) => Promise + (provider: PublicClient, opts: GasPriceEstimateOptions) => Promise > = { [CHAIN_IDs.LINEA]: lineaViem.eip1559, } as const; @@ -137,12 +118,7 @@ export async function _getViemGasPriceEstimate( let maxFeePerGas: bigint; let maxPriorityFeePerGas: bigint; if (gasPriceFeeds[chainId]) { - ({ maxFeePerGas, maxPriorityFeePerGas } = await gasPriceFeeds[chainId]( - viemProvider, - chainId, - baseFeeMultiplier, - unsignedTx - )); + ({ maxFeePerGas, maxPriorityFeePerGas } = await gasPriceFeeds[chainId](viemProvider, opts)); } else { let gasPrice: bigint | undefined; ({ maxFeePerGas, maxPriorityFeePerGas, gasPrice } = await viemProvider.estimateFeesPerGas()); diff --git a/src/utils/common.ts b/src/utils/common.ts index ca204875..aa57a1e9 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -7,6 +7,7 @@ import { getGasPriceEstimate } from "../gasPriceOracle"; import { BigNumber, BigNumberish, BN, bnZero, formatUnits, parseUnits, toBN } from "./BigNumberUtils"; import { ConvertDecimals } from "./FormattingUtils"; import { chainIsOPStack } from "./NetworkUtils"; +import { Transport } from "viem"; export type Decimalish = string | number | Decimal; export const AddressZero = ethers.constants.AddressZero; diff --git a/test/GasPriceOracle.test.ts b/test/GasPriceOracle.test.ts index d98a12ac..8a1e750b 100644 --- a/test/GasPriceOracle.test.ts +++ b/test/GasPriceOracle.test.ts @@ -3,28 +3,25 @@ // providers and API's to avoid the API requests. import dotenv from "dotenv"; -import winston from "winston"; -import { providers } from "ethers"; import { encodeFunctionData } from "viem"; import { getGasPriceEstimate } from "../src/gasPriceOracle"; import { BigNumber, bnZero, parseUnits } from "../src/utils"; import { expect, makeCustomTransport, randomAddress } from "../test/utils"; +import { MockedProvider } from "./utils/provider"; +import { MockPolygonGasStation } from "../src/gasPriceOracle/adapters/polygon"; dotenv.config({ path: ".env" }); -const dummyLogger = winston.createLogger({ - level: "debug", - transports: [new winston.transports.Console()], -}); - const stdLastBaseFeePerGas = parseUnits("12", 9); const stdMaxPriorityFeePerGas = parseUnits("1", 9); // EIP-1559 chains only const expectedLineaMaxFeePerGas = BigNumber.from("7"); -const ethersProviderChainIds = [1, 10, 137, 324, 8453, 42161, 534352, 59144]; -const viemProviderChainIds = [59144]; +// TODO: Mock Polygon gas station +const legacyChainIds = [324, 59144, 534352]; +const arbOrbitChainIds = [42161, 41455]; +const ethersProviderChainIds = [10, 8453, ...legacyChainIds, ...arbOrbitChainIds]; const customTransport = makeCustomTransport({ stdLastBaseFeePerGas, stdMaxPriorityFeePerGas }); -const provider = new providers.StaticJsonRpcProvider("https://eth.llamarpc.com"); +const provider = new MockedProvider(stdLastBaseFeePerGas, stdMaxPriorityFeePerGas); const ERC20ABI = [ { @@ -45,111 +42,166 @@ const erc20TransferTransactionObject = encodeFunctionData({ }); describe("Gas Price Oracle", function () { - it("Viem gas price retrieval", async function () { - for (const chainId of viemProviderChainIds) { - const chainKey = `NEW_GAS_PRICE_ORACLE_${chainId}`; - process.env[chainKey] = "true"; - if (chainId === 59144) { - // For Linea, works with and without passing in a custom Transaction object. - const unsignedTxns = [ - { - to: randomAddress(), - from: randomAddress(), - value: bnZero, - data: erc20TransferTransactionObject, - }, - undefined, - ]; - const baseFeeMultiplier = 2.0; - for (const unsignedTx of unsignedTxns) { - const { maxFeePerGas, maxPriorityFeePerGas } = await getGasPriceEstimate(provider, { - chainId, - transport: customTransport, - unsignedTx, - baseFeeMultiplier, - }); - - dummyLogger.debug({ - at: "Viem: Gas Price Oracle", - message: `Retrieved gas price estimate for chain ID ${chainId}`, - maxFeePerGas: maxFeePerGas.toString(), - maxPriorityFeePerGas: maxPriorityFeePerGas.toString(), - unsignedTx, - }); - - expect(BigNumber.isBigNumber(maxFeePerGas)).to.be.true; - expect(BigNumber.isBigNumber(maxPriorityFeePerGas)).to.be.true; - - // For Linea, base fee is expected to be hardcoded and unaffected by the base fee multiplier while - // the priority fee gets scaled. - const expectedPriorityFee = stdMaxPriorityFeePerGas.mul(2.0); - expect(maxFeePerGas).to.equal(expectedLineaMaxFeePerGas.add(expectedPriorityFee)); - expect(maxPriorityFeePerGas).to.equal(expectedPriorityFee); - } - } - delete process.env[chainKey]; - } + it("Linea Viem gas price retrieval with unsignedTx", async function () { + const chainId = 59144; + const chainKey = `NEW_GAS_PRICE_ORACLE_${chainId}`; + process.env[chainKey] = "true"; + const unsignedTx = { + to: randomAddress(), + from: randomAddress(), + value: bnZero, + data: erc20TransferTransactionObject, + }; + const { maxFeePerGas, maxPriorityFeePerGas } = await getGasPriceEstimate(provider, { + chainId, + transport: customTransport, + unsignedTx, + baseFeeMultiplier: 2.0, + }); + + // For Linea, base fee is expected to be hardcoded and unaffected by the base fee multiplier while + // the priority fee gets scaled. + // Additionally, test that the unsignedTx with a non-empty data field gets passed into the + // Linea viem provider. We've mocked the customTransport to double the priority fee if + // the unsigned tx object has non-empty data + const expectedPriorityFee = stdMaxPriorityFeePerGas.mul(4.0); + expect(maxFeePerGas).to.equal(expectedLineaMaxFeePerGas.add(expectedPriorityFee)); + expect(maxPriorityFeePerGas).to.equal(expectedPriorityFee); + delete process.env[chainKey]; + }); + it("Linea Viem gas price retrieval", async function () { + const chainId = 59144; + const chainKey = `NEW_GAS_PRICE_ORACLE_${chainId}`; + process.env[chainKey] = "true"; + const { maxFeePerGas, maxPriorityFeePerGas } = await getGasPriceEstimate(provider, { + chainId, + transport: customTransport, + baseFeeMultiplier: 2.0, + }); + + // For Linea, base fee is expected to be hardcoded and unaffected by the base fee multiplier while + // the priority fee gets scaled. + const expectedPriorityFee = stdMaxPriorityFeePerGas.mul(2.0); + expect(maxFeePerGas).to.equal(expectedLineaMaxFeePerGas.add(expectedPriorityFee)); + expect(maxPriorityFeePerGas).to.equal(expectedPriorityFee); + delete process.env[chainKey]; }); it("Ethers gas price retrieval", async function () { - // TODO: Make this test less flaky by creating a mocked Ethers provider as well - // as a fake Polygon gas station API, so it doesn't send real RPC requests. - const baseFeeMultiplier = 2.0; - // For this test, we only use the raw gas price feed for ethereum just so we can - // test both the bad and raw variants, since other chains will ultimately call the Etheruem - // adapter. - const eip1559RawGasPriceFeedChainIds = [1]; + const legacyChainIds = [324, 59144, 534352]; + const arbOrbitChainIds = [42161, 41455]; for (const chainId of ethersProviderChainIds) { - if (eip1559RawGasPriceFeedChainIds.includes(chainId)) { - const chainKey = `GAS_PRICE_EIP1559_RAW_${chainId}`; - process.env[chainKey] = "true"; - } - const [ - { maxFeePerGas: markedUpMaxFeePerGas, maxPriorityFeePerGas: markedUpMaxPriorityFeePerGas }, - { maxFeePerGas, maxPriorityFeePerGas }, - ] = await Promise.all([ - getGasPriceEstimate(provider, { chainId, baseFeeMultiplier, transport: customTransport }), - getGasPriceEstimate(provider, { chainId, baseFeeMultiplier: 1.0, transport: customTransport }), - ]); - - dummyLogger.debug({ - at: "Ethers: Gas Price Oracle", - message: `Retrieved gas price estimate for chain ID ${chainId}`, - maxFeePerGas: maxFeePerGas.toString(), - maxPriorityFeePerGas: maxPriorityFeePerGas.toString(), - markedUpMaxFeePerGas: markedUpMaxFeePerGas.toString(), - markedUpMaxPriorityFeePerGas: markedUpMaxPriorityFeePerGas.toString(), - }); - - expect(BigNumber.isBigNumber(maxFeePerGas)).to.be.true; - expect(BigNumber.isBigNumber(maxPriorityFeePerGas)).to.be.true; - - // @dev: The following tests *might* be flaky because the above two getGasPriceEstimate - // calls are technically two separate API calls and the suggested base and priority fees - // might be different. In practice, the fees rarely change when called in rapid succession. - - // Base fee should be multiplied by multiplier. Returned max fee includes priority fee - // so back it ou. - const expectedMarkedUpMaxFeePerGas = maxFeePerGas.sub(maxPriorityFeePerGas).mul(2); - expect(markedUpMaxFeePerGas.sub(markedUpMaxPriorityFeePerGas)).to.equal(expectedMarkedUpMaxFeePerGas); - expect(markedUpMaxFeePerGas.gt(maxFeePerGas)).to.be.true; - - // Priority fees should be the same - expect(markedUpMaxPriorityFeePerGas).to.equal(maxPriorityFeePerGas); - - if (chainId === 42161) { - // Arbitrum priority fee should be 1 wei. + const { maxFeePerGas: markedUpMaxFeePerGas, maxPriorityFeePerGas: markedUpMaxPriorityFeePerGas } = + await getGasPriceEstimate(provider, { chainId, baseFeeMultiplier }); + + // Base fee for EIP1559 gas price feeds should be multiplied by multiplier. + // Returned max fee includes priority fee so back it out. + const expectedMarkedUpMaxFeePerGas = stdLastBaseFeePerGas.mul(2); + + if (arbOrbitChainIds.includes(chainId)) { + expect(markedUpMaxFeePerGas.sub(markedUpMaxPriorityFeePerGas)).to.equal(expectedMarkedUpMaxFeePerGas); + // Arbitrum orbit priority fee should be 1 wei. expect(markedUpMaxPriorityFeePerGas).to.equal(1); - expect(maxPriorityFeePerGas).to.equal(1); - } - if (chainId === 324 || chainId === 534352) { + } else if (legacyChainIds.includes(chainId)) { // Scroll and ZkSync use legacy pricing so priority fee should be 0. - expect(maxPriorityFeePerGas).to.equal(0); - } - if (eip1559RawGasPriceFeedChainIds.includes(chainId)) { - const chainKey = `GAS_PRICE_EIP1559_RAW_${chainId}`; - delete process.env[chainKey]; + expect(markedUpMaxPriorityFeePerGas).to.equal(0); + // Legacy gas price = base fee + priority fee and full value is scaled + expect(markedUpMaxFeePerGas).to.equal(stdLastBaseFeePerGas.add(stdMaxPriorityFeePerGas).mul(2)); + } else { + expect(markedUpMaxFeePerGas.sub(markedUpMaxPriorityFeePerGas)).to.equal(expectedMarkedUpMaxFeePerGas); + // Priority fees should be unscaled + expect(markedUpMaxPriorityFeePerGas).to.equal(stdMaxPriorityFeePerGas); } } }); + it("Ethers EIP1559 Raw", async function () { + const baseFeeMultiplier = 2.0; + const chainId = 1; + const chainKey = `GAS_PRICE_EIP1559_RAW_${chainId}`; + process.env[chainKey] = "true"; + + const { maxFeePerGas: markedUpMaxFeePerGas, maxPriorityFeePerGas: markedUpMaxPriorityFeePerGas } = + await getGasPriceEstimate(provider, { chainId, baseFeeMultiplier }); + + // Base fee should be multiplied by multiplier. Returned max fee includes priority fee + // so back it out before scaling. + const expectedMarkedUpMaxFeePerGas = stdLastBaseFeePerGas.mul(baseFeeMultiplier).add(stdMaxPriorityFeePerGas); + expect(markedUpMaxFeePerGas).to.equal(expectedMarkedUpMaxFeePerGas); + + // Priority fees should be the same + expect(markedUpMaxPriorityFeePerGas).to.equal(stdMaxPriorityFeePerGas); + delete process.env[chainKey]; + }); + it("Ethers EIP1559 Bad", async function () { + // This test should return identical results to the Raw test but it makes different + // provider calls, so we're really testing that the expected provider functions are called. + const baseFeeMultiplier = 2.0; + const chainId = 1; + + const { maxFeePerGas: markedUpMaxFeePerGas, maxPriorityFeePerGas: markedUpMaxPriorityFeePerGas } = + await getGasPriceEstimate(provider, { chainId, baseFeeMultiplier }); + + // Base fee should be multiplied by multiplier. Returned max fee includes priority fee + // so back it out before scaling. + const expectedMarkedUpMaxFeePerGas = stdLastBaseFeePerGas.mul(baseFeeMultiplier).add(stdMaxPriorityFeePerGas); + expect(markedUpMaxFeePerGas).to.equal(expectedMarkedUpMaxFeePerGas); + + // Priority fees should be the same + expect(markedUpMaxPriorityFeePerGas).to.equal(stdMaxPriorityFeePerGas); + }); + it("Ethers Legacy", async function () { + const baseFeeMultiplier = 2.0; + const chainId = 324; + + const { maxFeePerGas: markedUpMaxFeePerGas, maxPriorityFeePerGas: markedUpMaxPriorityFeePerGas } = + await getGasPriceEstimate(provider, { chainId, baseFeeMultiplier }); + + // Legacy gas price is equal to base fee + priority fee and the full amount + // should be multiplied since the RPC won't return the broken down fee. + const expectedGasPrice = stdLastBaseFeePerGas.add(stdMaxPriorityFeePerGas); + const expectedMarkedUpMaxFeePerGas = expectedGasPrice.mul(baseFeeMultiplier); + expect(expectedMarkedUpMaxFeePerGas).to.equal(markedUpMaxFeePerGas); + + // Priority fees should be zero + expect(markedUpMaxPriorityFeePerGas).to.equal(0); + }); + it("Ethers Polygon GasStation", async function () { + const mockPolygonGasStation = new MockPolygonGasStation(stdLastBaseFeePerGas, stdMaxPriorityFeePerGas); + const baseFeeMultiplier = 2.0; + const chainId = 137; + + const { maxFeePerGas, maxPriorityFeePerGas } = await getGasPriceEstimate(provider, { + chainId, + baseFeeMultiplier, + polygonGasStation: mockPolygonGasStation, + }); + + expect(maxFeePerGas).to.equal(stdLastBaseFeePerGas.mul(baseFeeMultiplier).add(stdMaxPriorityFeePerGas)); + expect(maxPriorityFeePerGas).to.equal(stdMaxPriorityFeePerGas); + }); + it("Ethers Polygon GasStation: Fallback", async function () { + const getFeeDataThrows = true; + const mockPolygonGasStation = new MockPolygonGasStation( + stdLastBaseFeePerGas, + stdMaxPriorityFeePerGas, + getFeeDataThrows + ); + const baseFeeMultiplier = 2.0; + const chainId = 137; + + // If GasStation getFeeData throws, then the Polygon gas price oracle adapter should fallback to the + // ethereum EIP1559 logic. There should be logic to ensure the priority fee gets floored at 30 gwei. + const { maxFeePerGas, maxPriorityFeePerGas } = await getGasPriceEstimate(provider, { + chainId, + baseFeeMultiplier, + polygonGasStation: mockPolygonGasStation, + }); + + const minPolygonPriorityFee = parseUnits("30", 9); + const expectedPriorityFee = stdMaxPriorityFeePerGas.gt(minPolygonPriorityFee) + ? stdMaxPriorityFeePerGas + : minPolygonPriorityFee; + expect(maxFeePerGas).to.equal(stdLastBaseFeePerGas.mul(baseFeeMultiplier).add(expectedPriorityFee)); + expect(maxPriorityFeePerGas).to.equal(expectedPriorityFee); + }); }); diff --git a/test/utils/provider.ts b/test/utils/provider.ts new file mode 100644 index 00000000..d0af1bff --- /dev/null +++ b/test/utils/provider.ts @@ -0,0 +1,59 @@ +import { BigNumber, providers } from "ethers"; +import { Block, BlockTag, FeeData } from "@ethersproject/abstract-provider"; +import { bnZero } from "../../src/utils/BigNumberUtils"; + +/** + * @notice Class used to test GasPriceOracle which makes ethers provider calls to the following implemented + * methods. + */ +export class MockedProvider extends providers.StaticJsonRpcProvider { + constructor( + readonly stdLastBaseFeePerGas: BigNumber, + readonly stdMaxPriorityFeePerGas: BigNumber + ) { + super(); + } + + getBlock(_blockHashOrBlockTag: BlockTag | string | Promise): Promise { + const mockBlock: Block = { + transactions: [], + hash: "0x", + parentHash: "0x", + number: 0, + nonce: "0", + difficulty: 0, + _difficulty: bnZero, + timestamp: 0, + gasLimit: bnZero, + gasUsed: bnZero, + baseFeePerGas: this.stdLastBaseFeePerGas, + miner: "0x", + extraData: "0x", + }; + return Promise.resolve(mockBlock); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + send(method: string, _params: Array): Promise { + switch (method) { + case "eth_maxPriorityFeePerGas": + return Promise.resolve(this.stdMaxPriorityFeePerGas); + default: + throw new Error(`MockedProvider#Unimplemented method: ${method}`); + } + } + + getFeeData(): Promise { + return Promise.resolve({ + lastBaseFeePerGas: this.stdLastBaseFeePerGas, + maxPriorityFeePerGas: this.stdMaxPriorityFeePerGas, + // Following fields unused in GasPrice oracle + maxFeePerGas: null, + gasPrice: null, + }); + } + + getGasPrice(): Promise { + return Promise.resolve(this.stdLastBaseFeePerGas.add(this.stdMaxPriorityFeePerGas)); + } +} diff --git a/test/utils/transport.ts b/test/utils/transport.ts index 5ffd7d59..bb344330 100644 --- a/test/utils/transport.ts +++ b/test/utils/transport.ts @@ -2,15 +2,15 @@ import { custom } from "viem"; import { BigNumber, parseUnits } from "../../src/utils"; export const makeCustomTransport = ( - params: Partial<{ stdLastBaseFeePerGas: BigNumber; stdMaxPriorityFeePerGas: BigNumber }> = {} + feeParams: Partial<{ stdLastBaseFeePerGas: BigNumber; stdMaxPriorityFeePerGas: BigNumber }> = {} ) => { - const { stdLastBaseFeePerGas = parseUnits("12", 9), stdMaxPriorityFeePerGas = parseUnits("1", 9) } = params; + const { stdLastBaseFeePerGas = parseUnits("12", 9), stdMaxPriorityFeePerGas = parseUnits("1", 9) } = feeParams; const stdMaxFeePerGas = stdLastBaseFeePerGas.add(stdMaxPriorityFeePerGas); const stdGasPrice = stdMaxFeePerGas; return custom({ // eslint-disable-next-line require-await - async request({ method }: { method: string; params: unknown }) { + async request({ method, params }: { method: string; params: unknown[] }) { switch (method) { case "eth_gasPrice": return BigInt(stdGasPrice.toString()); @@ -19,10 +19,15 @@ export const makeCustomTransport = ( case "eth_maxPriorityFeePerGas": return BigInt(stdMaxPriorityFeePerGas.toString()); case "linea_estimateGas": + // For testing purposes, double the priority fee if txnData is not the empty string "0x" return { // Linea base fee is always 7 wei baseFeePerGas: BigInt(7), - priorityFeePerGas: BigInt(stdMaxPriorityFeePerGas.toString()), + priorityFeePerGas: BigInt( + stdMaxPriorityFeePerGas + .mul((params as { data: string }[])[0]?.data?.slice(2).length > 0 ? 2 : 1) + .toString() + ), gasLimit: BigInt("0"), }; default: From bcd10bb715a81cbf4a943d373b8dd72e4127111a Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Fri, 27 Dec 2024 08:55:09 -0500 Subject: [PATCH 09/10] add baseFeeMultiplier assertion test Signed-off-by: nicholaspai --- test/GasPriceOracle.test.ts | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/test/GasPriceOracle.test.ts b/test/GasPriceOracle.test.ts index 8a1e750b..bf73fb4f 100644 --- a/test/GasPriceOracle.test.ts +++ b/test/GasPriceOracle.test.ts @@ -6,7 +6,7 @@ import dotenv from "dotenv"; import { encodeFunctionData } from "viem"; import { getGasPriceEstimate } from "../src/gasPriceOracle"; import { BigNumber, bnZero, parseUnits } from "../src/utils"; -import { expect, makeCustomTransport, randomAddress } from "../test/utils"; +import { assertPromiseError, expect, makeCustomTransport, randomAddress } from "../test/utils"; import { MockedProvider } from "./utils/provider"; import { MockPolygonGasStation } from "../src/gasPriceOracle/adapters/polygon"; dotenv.config({ path: ".env" }); @@ -42,6 +42,24 @@ const erc20TransferTransactionObject = encodeFunctionData({ }); describe("Gas Price Oracle", function () { + it("baseFeeMultiplier is validated", async function () { + // Too low: + await assertPromiseError( + getGasPriceEstimate(provider, { + chainId: 1, + baseFeeMultiplier: 0.5, + }), + "base fee multiplier" + ); + // Too high: + await assertPromiseError( + getGasPriceEstimate(provider, { + chainId: 1, + baseFeeMultiplier: 5.5, + }), + "base fee multiplier" + ); + }); it("Linea Viem gas price retrieval with unsignedTx", async function () { const chainId = 59144; const chainKey = `NEW_GAS_PRICE_ORACLE_${chainId}`; From 2d4caee5ac5b29f7afe1b3625192737d73231d0b Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Fri, 27 Dec 2024 09:12:58 -0500 Subject: [PATCH 10/10] add linea ethers notes Signed-off-by: nicholaspai --- src/gasPriceOracle/adapters/linea.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/gasPriceOracle/adapters/linea.ts b/src/gasPriceOracle/adapters/linea.ts index 369a1198..24429870 100644 --- a/src/gasPriceOracle/adapters/linea.ts +++ b/src/gasPriceOracle/adapters/linea.ts @@ -8,5 +8,9 @@ import * as ethereum from "./ethereum"; import { GasPriceEstimateOptions } from "../oracle"; export function eip1559(provider: providers.Provider, opts: GasPriceEstimateOptions): Promise { + // We use the legacy method to call `eth_gasPrice` which empirically returns a more accurate + // gas price estimate than `eth_maxPriorityFeePerGas` or ethersProvider.getFeeData in the EIP1559 "raw" or "bad" + // cases. Based on testing `eth_gasPrice` returns the closest price to the Linea-specific `linea_estimateGas` + // endpoint which the Viem Linea adapter queries. return ethereum.legacy(provider, opts); }