Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support opt-in use of viem for gas pricing #745

Merged
merged 14 commits into from
Nov 13, 2024
197 changes: 34 additions & 163 deletions e2e/oracle.e2e.ts
pxrl marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,97 +1,58 @@
// @note: This test is _not_ run automatically as part of git hooks or CI.
import dotenv from "dotenv";
import winston from "winston";
import { providers, utils as ethersUtils } from "ethers";
import { custom } from "viem";
import { providers } from "ethers";
import { getGasPriceEstimate } from "../src/gasPriceOracle";
import { BigNumber } from "../src/utils";
import { assertPromiseError, expect } from "../test/utils";
import { BigNumber, bnZero, parseUnits } from "../src/utils";
import { expect } from "../test/utils";
dotenv.config({ path: ".env" });

const dummyLogger = winston.createLogger({
level: "debug",
transports: [new winston.transports.Console()],
});

type FeeData = providers.FeeData;

class MockedProvider extends providers.StaticJsonRpcProvider {
// Unknown type => exercise our validation logic
public testFeeData: unknown;
public testGasPrice: unknown;

constructor(url: string) {
super(url);
}

override async getFeeData(): Promise<FeeData> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (this.testFeeData as any) ?? (await super.getFeeData());
}

override async getGasPrice(): Promise<BigNumber> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return this.testGasPrice !== undefined ? (this.testGasPrice as any) : await super.getGasPrice();
}
}

/**
* Note: If NODE_URL_<chainId> envvars exist, they will be used. The
* RPCs defined below are otherwise used as default/fallback options.
* These may be subject to rate-limiting, in which case the retrieved
* price will revert to 0.
*
* Note also that Optimism is only supported as a fallback/legacy test
* case. It works, but is not the recommended method for conjuring gas
* prices on Optimism.
*/
const networks: { [chainId: number]: string } = {
1: "https://eth.llamarpc.com",
10: "https://mainnet.optimism.io",
137: "https://polygon.llamarpc.com",
288: "https://mainnet.boba.network",
324: "https://mainnet.era.zksync.io",
8453: "https://mainnet.base.org",
42161: "https://rpc.ankr.com/arbitrum",
534352: "https://rpc.scroll.io",
};

const stdGasPrice = ethersUtils.parseUnits("10", 9);
const stdMaxPriorityFeePerGas = ethersUtils.parseUnits("1.5", 9); // EIP-1559 chains only
const stdLastBaseFeePerGas = stdGasPrice.sub(stdMaxPriorityFeePerGas);
const stdMaxFeePerGas = stdGasPrice;
const eip1559Chains = [1, 10, 137, 8453, 42161];
const legacyChains = [288, 324, 534352];
const stdLastBaseFeePerGas = parseUnits("12", 9);
const stdMaxPriorityFeePerGas = parseUnits("1", 9); // EIP-1559 chains only
const stdMaxFeePerGas = stdLastBaseFeePerGas.add(stdMaxPriorityFeePerGas);
const stdGasPrice = stdMaxFeePerGas;

const customTransport = custom({
async request({ method, params }: { method: string; params: unknown }) {

Check failure on line 22 in e2e/oracle.e2e.ts

View workflow job for this annotation

GitHub Actions / Lint

Async method 'request' has no 'await' expression
params; // lint
switch (method) {
case "eth_gasPrice":
return BigInt(stdGasPrice.toString());
case "eth_getBlockByNumber":
return { baseFeePerGas: BigInt(stdLastBaseFeePerGas.mul(100).div(120).toString()) };
case "eth_maxPriorityFeePerGas":
return BigInt(stdMaxPriorityFeePerGas.toString());
default:
console.log(`Unsupported method: ${method}.`);
}
},
});

let providerInstances: { [chainId: number]: MockedProvider } = {};
const eip1559Chains = [1, 10, 137, 324, 8453, 42161, 534352];
const chainIds = [...eip1559Chains, 1337];
let providerInstances: { [chainId: number]: providers.StaticJsonRpcProvider } = {};

describe("Gas Price Oracle", function () {
before(() => {
providerInstances = Object.fromEntries(
Object.entries(networks).map(([_chainId, _rpcUrl]) => {
const chainId = Number(_chainId);
const rpcUrl: string = process.env[`NODE_URL_${chainId}`] ?? _rpcUrl;
const provider = new MockedProvider(rpcUrl);
chainIds.map((chainId) => {
const provider = new providers.StaticJsonRpcProvider("https://eth.llamarpc.com");
return [chainId, provider];
})
);
});

beforeEach(() => {
for (const provider of Object.values(providerInstances)) {
provider.testFeeData = {
gasPrice: stdGasPrice,
lastBaseFeePerGas: stdLastBaseFeePerGas,
maxPriorityFeePerGas: stdMaxPriorityFeePerGas,
};
provider.testGasPrice = stdGasPrice; // Required: same as provider.feeData.gasPrice.
}
});

it("Gas Price Retrieval", async function () {
for (const [_chainId, provider] of Object.entries(providerInstances)) {
const chainId = Number(_chainId);

const { maxFeePerGas, maxPriorityFeePerGas } = await getGasPriceEstimate(provider);
const { maxFeePerGas, maxPriorityFeePerGas } = await getGasPriceEstimate(provider, customTransport);
dummyLogger.debug({
at: "Gas Price Oracle#Gas Price Retrieval",
message: `Retrieved gas price estimate for chain ID ${chainId}`,
Expand All @@ -113,104 +74,14 @@
expect(maxFeePerGas.eq(stdLastBaseFeePerGas.add(1))).to.be.true;
expect(maxPriorityFeePerGas.eq(1)).to.be.true;
} else {
expect(maxFeePerGas.eq(stdMaxFeePerGas)).to.be.true;
expect(maxPriorityFeePerGas.eq(stdMaxPriorityFeePerGas)).to.be.true;
expect(maxFeePerGas.gt(bnZero)).to.be.true;
expect(maxPriorityFeePerGas.gt(bnZero)).to.be.true;
}
} else {
// Defaults to Legacy (Type 0)
expect(maxFeePerGas.eq(stdGasPrice)).to.be.true;
expect(maxPriorityFeePerGas.eq(0)).to.be.true;
expect(maxFeePerGas.eq(stdMaxFeePerGas)).to.be.true;
expect(maxPriorityFeePerGas.eq(bnZero)).to.be.true;
}
}
});

it("Gas Price Retrieval Failure", async function () {
const feeDataFields = ["gasPrice", "lastBaseFeePerGas", "maxPriorityFeePerGas"];
const feeDataValues = [null, "test", "1234", 5678, BigNumber.from(-1)];

// Iterate over various faulty values for gasPrice & feeData.
// Loop one chain at a time to minimise rate-limiting in case a public RPC is being used.
for (const field of feeDataFields) {
for (const value of feeDataValues) {
for (const [_chainId, provider] of Object.entries(providerInstances)) {
const chainId = Number(_chainId);

provider.testGasPrice = field === "gasPrice" ? value : stdGasPrice;
provider.testFeeData = {
gasPrice: field === "gasPrice" ? value : stdGasPrice,
lastBaseFeePerGas: field === "lastBaseFeePerGas" ? value : stdLastBaseFeePerGas,
maxPriorityFeePerGas: field === "maxPriorityFeePerGas" ? value : stdMaxPriorityFeePerGas,
};

// Malformed inputs were supplied; ensure an exception is thrown.
if (
(legacyChains.includes(chainId) && ["gasPrice"].includes(field)) ||
(chainId !== 137 &&
eip1559Chains.includes(chainId) &&
["lastBaseFeePerGas", "maxPriorityFeePerGas"].includes(field))
) {
provider.testGasPrice = field === "gasPrice" ? value : stdGasPrice;
await assertPromiseError(getGasPriceEstimate(provider, chainId));
} else {
// Expect sane results to be returned; validate them.
const { maxFeePerGas, maxPriorityFeePerGas } = await getGasPriceEstimate(provider, chainId);

dummyLogger.debug({
at: "Gas Price Oracle#Gas Price Retrieval Failure",
message: `Retrieved gas price estimate for chain ID ${chainId}.`,
maxFeePerGas,
maxPriorityFeePerGas,
});

expect(BigNumber.isBigNumber(maxFeePerGas)).to.be.true;
expect(BigNumber.isBigNumber(maxPriorityFeePerGas)).to.be.true;

if (eip1559Chains.includes(chainId)) {
if (chainId === 137) {
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) {
expect(maxFeePerGas.eq(stdLastBaseFeePerGas.add(1))).to.be.true;
expect(maxPriorityFeePerGas.eq(1)).to.be.true;
} else {
expect(maxFeePerGas.eq(stdMaxFeePerGas)).to.be.true;
expect(maxPriorityFeePerGas.eq(stdMaxPriorityFeePerGas)).to.be.true;
}
} else {
// Legacy
expect(maxFeePerGas.eq(stdGasPrice)).to.be.true;
expect(maxPriorityFeePerGas.eq(0)).to.be.true;
}
}
}
}
}
});

it("Gas Price Fallback Behaviour", async function () {
for (const provider of Object.values(providerInstances)) {
const fakeChainId = 1337;

const chainId = (await provider.getNetwork()).chainId;
dummyLogger.debug({
at: "Gas Price Oracle#Gas Price Fallback Behaviour",
message: `Testing on chainId ${chainId}.`,
});

provider.testGasPrice = stdMaxFeePerGas; // Suppress RPC lookup.

const { maxFeePerGas, maxPriorityFeePerGas } = await getGasPriceEstimate(provider, fakeChainId, true);

// Require legacy pricing when fallback is permitted.
expect(BigNumber.isBigNumber(maxFeePerGas)).to.be.true;
expect(maxFeePerGas.eq(stdMaxFeePerGas)).to.be.true;

expect(BigNumber.isBigNumber(maxPriorityFeePerGas)).to.be.true;
expect(maxPriorityFeePerGas.eq(0)).to.be.true;

// Verify an assertion is thrown when fallback is not permitted.
await assertPromiseError(getGasPriceEstimate(provider, fakeChainId, false));
}
});
});
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,8 @@
"lodash": "^4.17.21",
"lodash.get": "^4.4.2",
"superstruct": "^0.15.4",
"tslib": "^2.6.2"
"tslib": "^2.6.2",
"viem": "^2.21.15"
},
"publishConfig": {
"registry": "https://registry.npmjs.com/",
Expand Down
37 changes: 13 additions & 24 deletions src/gasPriceOracle/adapters/arbitrum.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,14 @@
import { providers } from "ethers";
import { BigNumber, bnOne, parseUnits } from "../../utils";
import { GasPriceEstimate } from "../types";
import * as ethereum from "./ethereum";

let DEFAULT_PRIORITY_FEE: BigNumber | undefined = undefined;

// Arbitrum Nitro implements EIP-1559 pricing, but the priority fee is always refunded to the caller. Further,
// ethers typically hardcodes the priority fee to 1.5 Gwei. So, confirm that the priority fee supplied was 1.5
// Gwei, and then drop it to 1 Wei. Reference: https://developer.arbitrum.io/faqs/gas-faqs#q-priority
export async function eip1559(provider: providers.Provider, chainId: number): Promise<GasPriceEstimate> {
DEFAULT_PRIORITY_FEE ??= parseUnits("1.5", 9);
const { maxFeePerGas: _maxFeePerGas, maxPriorityFeePerGas } = await ethereum.eip1559(provider, chainId);

// If this throws, ethers default behaviour has changed, or Arbitrum RPCs are returning something more sensible.
if (!maxPriorityFeePerGas.eq(DEFAULT_PRIORITY_FEE)) {
throw new Error(`Expected hardcoded 1.5 Gwei priority fee on Arbitrum, got ${maxPriorityFeePerGas}`);
}

// eip1559() sets maxFeePerGas = lastBaseFeePerGas + maxPriorityFeePerGas, so revert that.
// The caller may apply scaling as they wish afterwards.
const maxFeePerGas = _maxFeePerGas.sub(maxPriorityFeePerGas).add(bnOne);

return { maxPriorityFeePerGas: bnOne, maxFeePerGas };
import { PublicClient } from "viem";
import { InternalGasPriceEstimate } from "../types";

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> {

Check failure on line 9 in src/gasPriceOracle/adapters/arbitrum.ts

View workflow job for this annotation

GitHub Actions / Lint

'_chainId' is defined but never used
let { maxFeePerGas, maxPriorityFeePerGas } = await provider.estimateFeesPerGas();

Check failure on line 10 in src/gasPriceOracle/adapters/arbitrum.ts

View workflow job for this annotation

GitHub Actions / Lint

'maxPriorityFeePerGas' is never reassigned. Use 'const' instead
console.log(`arbitrum: got maxFeePerGas ${maxFeePerGas}, maxPriorityFeePerGas: ${maxPriorityFeePerGas}.`);
maxFeePerGas = BigInt(maxFeePerGas) - maxPriorityFeePerGas + MAX_PRIORITY_FEE_PER_GAS;
return { maxFeePerGas, maxPriorityFeePerGas: MAX_PRIORITY_FEE_PER_GAS };
}
25 changes: 6 additions & 19 deletions src/gasPriceOracle/adapters/ethereum.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,15 @@
import { providers } from "ethers";
import { BigNumber, bnZero } from "../../utils";
import { GasPriceEstimate } from "../types";
import { gasPriceError } from "../util";
import { PublicClient } from "viem";
import { InternalGasPriceEstimate } from "../types";

export async function eip1559(provider: providers.Provider, chainId: number): Promise<GasPriceEstimate> {
const feeData = await provider.getFeeData();

[feeData.lastBaseFeePerGas, feeData.maxPriorityFeePerGas].forEach((field: BigNumber | null) => {
if (!BigNumber.isBigNumber(field) || field.lt(bnZero)) gasPriceError("getFeeData()", chainId, feeData);
});

const maxPriorityFeePerGas = feeData.maxPriorityFeePerGas as BigNumber;
const maxFeePerGas = maxPriorityFeePerGas.add(feeData.lastBaseFeePerGas as BigNumber);

return { maxPriorityFeePerGas, maxFeePerGas };
export function eip1559(provider: PublicClient, _chainId: number): Promise<InternalGasPriceEstimate> {

Check failure on line 4 in src/gasPriceOracle/adapters/ethereum.ts

View workflow job for this annotation

GitHub Actions / Lint

'_chainId' is defined but never used
return provider.estimateFeesPerGas();
dohaki marked this conversation as resolved.
Show resolved Hide resolved
}

export async function legacy(provider: providers.Provider, chainId: number): Promise<GasPriceEstimate> {
export async function legacy(provider: PublicClient, _chainId: number): Promise<InternalGasPriceEstimate> {

Check failure on line 8 in src/gasPriceOracle/adapters/ethereum.ts

View workflow job for this annotation

GitHub Actions / Lint

'_chainId' is defined but never used
const gasPrice = await provider.getGasPrice();

if (!BigNumber.isBigNumber(gasPrice) || gasPrice.lt(bnZero)) gasPriceError("getGasPrice()", chainId, gasPrice);

return {
maxFeePerGas: gasPrice,
maxPriorityFeePerGas: bnZero,
maxPriorityFeePerGas: BigInt(0),
};
}
6 changes: 3 additions & 3 deletions src/gasPriceOracle/adapters/linea.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
// This query is currently only available on Linea Sepolia, ETA mainnet 30 July.
// Until then, just parrot the existing Ethereum EIP-1559 pricing strategy.
// See also: https://docs.linea.build/developers/reference/api/linea-estimategas
import { providers } from "ethers";
import { GasPriceEstimate } from "../types";
import { PublicClient } from "viem";
import { InternalGasPriceEstimate } from "../types";
import * as ethereum from "./ethereum";

export function eip1559(provider: providers.Provider, chainId: number): Promise<GasPriceEstimate> {
export function eip1559(provider: PublicClient, chainId: number): Promise<InternalGasPriceEstimate> {
return ethereum.legacy(provider, chainId);
}
Loading
Loading