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: