Skip to content

Commit

Permalink
improve(GasPriceOracle): Standardize gas price oracle usage
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
nicholaspai committed Dec 24, 2024
1 parent a2e7b41 commit 71c12b1
Show file tree
Hide file tree
Showing 7 changed files with 134 additions and 77 deletions.
15 changes: 11 additions & 4 deletions e2e/oracle.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });

Expand All @@ -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}`,
Expand Down Expand Up @@ -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}`,
Expand Down
15 changes: 12 additions & 3 deletions src/gasPriceOracle/adapters/arbitrum-viem.ts
Original file line number Diff line number Diff line change
@@ -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<InternalGasPriceEstimate> {
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<InternalGasPriceEstimate> {
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 };
}
12 changes: 10 additions & 2 deletions src/gasPriceOracle/adapters/ethereum-viem.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import { PublicClient } from "viem";
import { InternalGasPriceEstimate } from "../types";
import { BigNumber } from "../../utils";

export function eip1559(provider: PublicClient, _chainId: number): Promise<InternalGasPriceEstimate> {
return provider.estimateFeesPerGas();
export async function eip1559(
provider: PublicClient,
_chainId: number,
baseFeeMultiplier: number
): Promise<InternalGasPriceEstimate> {
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(
Expand Down
18 changes: 12 additions & 6 deletions src/gasPriceOracle/adapters/linea-viem.ts
Original file line number Diff line number Diff line change
@@ -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<InternalGasPriceEstimate> {
export async function eip1559(
provider: PublicClient,
_chainId: number,
baseFeeMultiplier: number,
_unsignedTx?: PopulatedTransaction
): Promise<InternalGasPriceEstimate> {
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,
};
}
9 changes: 7 additions & 2 deletions src/gasPriceOracle/adapters/polygon-viem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,15 +62,20 @@ class PolygonGasStation extends BaseHTTPAdapter {
}
}

export async function gasStation(provider: PublicClient, chainId: number): Promise<InternalGasPriceEstimate> {
export async function gasStation(
provider: PublicClient,
chainId: number,
baseFeeMultiplier: number
): Promise<InternalGasPriceEstimate> {
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
Expand Down
109 changes: 80 additions & 29 deletions src/gasPriceOracle/oracle.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -13,47 +13,83 @@ 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<GasPriceEstimateOptions> {
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<GasPriceEstimateOptions>
): Promise<GasPriceEstimate> {
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,
});
}

/**
* 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.
* @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<GasPriceEstimate> {
const { baseFeeMultiplier, chainId, legacyFallback } = opts;

const gasPriceFeeds = {
[CHAIN_IDs.ALEPH_ZERO]: arbitrum.eip1559,
[CHAIN_IDs.ARBITRUM]: arbitrum.eip1559,
Expand All @@ -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<GasPriceEstimate> {
const { baseFeeMultiplier, unsignedTx, transport } = opts;

const chainId =
typeof providerOrChainId === "number" ? providerOrChainId : (await providerOrChainId.getNetwork()).chainId;
const viemProvider = getPublicClient(chainId, transport);
console.log(viemProvider.transport)

Check warning on line 126 in src/gasPriceOracle/oracle.ts

View workflow job for this annotation

GitHub Actions / Lint

Insert `;`

Check failure on line 126 in src/gasPriceOracle/oracle.ts

View workflow job for this annotation

GitHub Actions / Lint

Missing semicolon

const gasPriceFeeds = {
const gasPriceFeeds: Record<
number,
(
provider: PublicClient,
chainId: number,
baseFeeMultiplier: number,
unsignedTx?: PopulatedTransaction
) => Promise<InternalGasPriceEstimate>
> = {
[CHAIN_IDs.ALEPH_ZERO]: arbitrumViem.eip1559,
[CHAIN_IDs.ARBITRUM]: arbitrumViem.eip1559,
[CHAIN_IDs.LINEA]: lineaViem.eip1559,
Expand All @@ -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()),
};
}
33 changes: 2 additions & 31 deletions src/utils/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand All @@ -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);
}

Expand All @@ -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)));
}

0 comments on commit 71c12b1

Please sign in to comment.