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

Add Base network support #223

Merged
merged 5 commits into from
Mar 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ env:
ALCHEMY_TOKEN: ${{ secrets.ALCHEMY_TOKEN }}
ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY }}
OPTIMISTIC_API_KEY: ${{ secrets.OPTIMISTIC_API_KEY }}
BASE_API_KEY: ${{ secrets.BASE_API_KEY }}

jobs:
lint:
Expand Down
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ const config: HardhatUserConfig = {
| blobBaseFeeApi | _string_ | - | URL to fetch live *execution* network blob base fee from. (By default, this is auto-configured based on the `L1` or `L2` setting) |
| gasPriceApi | _string_ | - | URL to fetch live *execution* network gas price from. (By default, this is auto-configured based on the `L1` or `L2` setting) |
| getBlockApi | _string_ | - | URL to fetch L1 block header from when simulating L2. (By default, this is auto-configured based on the `L2` setting) |
| opStackBaseFeeScalar | _number_ | - | Scalar applied to L1 base fee when calculating L1 data cost (see [Advanced Usage][12]) |
| opStackBlobBaseFeeScalar | _number_ | - | Scalar applied to L1 blob base fee when calculating L1 data cost (see [Advanced Usage][12]) |
| token | _string_ | - | Network token gas fees are denominated in (ex:"ETH"). (By default, this is auto-configured based on the `L1` or `L2` setting) |
| tokenPrice | _string_ | - | Network token price per nation state currency unit. (To denominate costs *in network token* set this to `"1"`) |

Expand All @@ -132,11 +134,12 @@ npx hardhat hhgas:merge "gasReporterOutput-*.json"

## Supported Networks

API keys for the networks this plugin auto-configures via the `L1` and `L2` options are available from the links below. These aren't strictly required - you only need to set them if you start seeing rate-limit warnings.
API keys for the networks this plugin auto-configures via the `L1` and `L2` options are available from the links below. In many cases these aren't equired - you'll only need to set them if you start seeing rate-limit warnings.

**L2**

+ [optimism][109]
+ [base][110] (live `blobBaseFee` prices require an API key)
+ [optimism][109] (live `blobBaseFee` prices require an API key)

**L1**

Expand Down Expand Up @@ -165,6 +168,7 @@ You can support hardhat-gas-reporter via [DRIPS][11], a public goods protocol th
[9]: https://github.com/cgewecke/hardhat-gas-reporter/blob/master/docs/advanced.md#json-output
[10]: https://github.com/cgewecke/hardhat-gas-reporter/blob/master/docs/advanced.md#markdown-format-example
[11]: https://www.drips.network/app/projects/github/cgewecke/hardhat-gas-reporter
[12]: https://github.com/cgewecke/hardhat-gas-reporter/blob/master/docs/advanced.md#op-stack-l1-data-costs
[100]: https://optimistic.etherscan.io
[101]: https://docs.etherscan.io/getting-started/viewing-api-usage-statistics
[102]: https://docs.polygonscan.com/getting-started/viewing-api-usage-statistics
Expand All @@ -175,3 +179,4 @@ You can support hardhat-gas-reporter via [DRIPS][11], a public goods protocol th
[107]: https://docs.gnosisscan.io/getting-started/viewing-api-usage-statistics
[108]: https://snowtrace.io/
[109]: https://docs.optimism.etherscan.io/getting-started/viewing-api-usage-statistics
[110]: https://docs.basescan.org/getting-started/viewing-api-usage-statistics
34 changes: 30 additions & 4 deletions docs/advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
* [Proxy Resolvers](#proxy-resolvers)
* [Remote Contracts](#remote-contracts)
* [Intrinsic Gas](#intrinsic-gas)
* [OP Stack L1 Data Costs](#op-stack-l1-data-costs)
* [JSON output](#json-output)
* [Markdown Format Example](#markdown-format-example)

## Config Examples

Expand All @@ -31,6 +33,7 @@ const config: HardhatUserConfig = {
const config: HardhatUserConfig = {
gasReporter: {
L2: "optimism",
L2Etherscan: "ABC...",
currency: "EUR",
coinmarketcap: "abc...",
}
Expand Down Expand Up @@ -95,15 +98,16 @@ const config: HardhatUserConfig = {
}
```

### Offline and L2
### L2, offline, cost in network token
```ts
const config: HardhatUserConfig = {
gasReporter: {
offline: true,
L2: "optimism",
gasPrice: .00325, // gwei on L2
baseFee: 35, // gwei on L1
tokenPrice: "4000.00", // USD per ETH
gasPrice: .00325, // gwei (L2)
baseFee: 35, // gwei (L1)
blobBaseFee: 20, // gwei (L1)
tokenPrice: "1", // ETH per ETH
token: "ETH"
}
}
Expand Down Expand Up @@ -205,6 +209,25 @@ Gas Calculation:

:warning: The execution costs minus intrinsic gas overhead reported by the plugin are **only lower bounds** of what the actual cost will be. (Read [wolfio/evm-opcodes/gas][8] for more info about the complex accounting applied to any given method invocation in reality)

## OP Stack L1 Data Costs

Optimism and Base networks L1 data costs are calculated using [formulae from their docs][9]. If you've configured the reporter to fetch market data, the `baseFee` and `blobBaseFee` components of these cost equations are pulled from the live network. However, the scalar constant components are hardcoded in the plugin (because they're set by the network operator & don't fluctuate in a demand auction).

You can verify that the scalar values correctly reflect the current network settings by checking them at:
+ [Base GasPriceOracle][10]
+ [Optimism GasPriceOracle][11]

You can override the plugin's defaults by configuring the relevant options:
```ts
const config: HardhatUserConfig = {
gasReporter: {
opStackBaseFeeScalar: 1101,
opStackBlobBaseFeeScalar: 659851
}
}
```


## JSON Output

The JSON output includes the full gas reporter option state (API keys are redacted) and all collected gas data for methods and deployments. The object has the following interface:
Expand Down Expand Up @@ -313,3 +336,6 @@ Example of a report produced with `reportFormat: "markdown"`:
[4]: https://github.com/cgewecke/hardhat-gas-reporter/blob/master/src/types.ts
[5]: https://github.com/cgewecke/hardhat-gas-reporter/issues/195
[8]: https://github.com/wolflo/evm-opcodes/blob/main/gas.md#a0-0-intrinsic-gas
[9]: https://docs.optimism.io/stack/transactions/fees#ecotone
[10]: https://basescan.org/address/0x420000000000000000000000000000000000000F#readProxyContract
[11]: https://optimistic.etherscan.io/address/0x4200000000000000000000000000000000000015#readProxyContract
14 changes: 14 additions & 0 deletions scripts/gen-options-md.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,20 @@ advancedSubtitle,
"URL to fetch L1 block header from when simulating L2. (By default, this is auto-configured " +
"based on the `L2` setting)"
],
// opStackBaseFeeScalar
[
"opStackBaseFeeScalar",
"_number_",
"-",
"Scalar applied to L1 base fee when calculating L1 data cost (see [Advanced Usage][12])"
],
// opStackBlobBaseFeeScalar
[
"opStackBlobBaseFeeScalar",
"_number_",
"-",
"Scalar applied to L1 blob base fee when calculating L1 data cost (see [Advanced Usage][12])"
],
// token
[
"token",
Expand Down
9 changes: 6 additions & 3 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,18 @@ export const OPTIMISM_BEDROCK_FIXED_OVERHEAD = 188;
export const OPTIMISM_BEDROCK_DYNAMIC_OVERHEAD = 0.684;

// These params are configured by node operators and may vary
// Values are suggested default values from:
// https://docs.optimism.io/builders/chain-operators/management/blobs
// Values were read from the GasPriceOracle contract at:
// https://optimistic.etherscan.io/address/0x420000000000000000000000000000000000000F
export const OPTIMISM_ECOTONE_BASE_FEE_SCALAR = 1368
export const OPTIMISM_ECOTONE_BLOB_BASE_FEE_SCALAR = 810949

// https://basescan.org/address/0x420000000000000000000000000000000000000F
export const BASE_ECOTONE_BASE_FEE_SCALAR = 1101;
export const BASE_ECOTONE_BLOB_BASE_FEE_SCALAR = 659851;

export const UNICODE_CIRCLE = "◯";
export const UNICODE_TRIANGLE = "△"

export const OPTIMISM_GAS_ORACLE_ADDRESS = "0xb528d11cc114e026f138fe568744c6d45ce6da7a";
export const OPTIMISM_GAS_ORACLE_ABI_PARTIAL = [
{
constant: true,
Expand Down
31 changes: 29 additions & 2 deletions src/lib/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import {
DEFAULT_CURRENCY_DISPLAY_PRECISION,
DEFAULT_JSON_OUTPUT_FILE,
DEFAULT_OPTIMISM_HARDFORK,
BASE_ECOTONE_BASE_FEE_SCALAR,
BASE_ECOTONE_BLOB_BASE_FEE_SCALAR,
OPTIMISM_ECOTONE_BASE_FEE_SCALAR,
OPTIMISM_ECOTONE_BLOB_BASE_FEE_SCALAR,
TABLE_NAME_TERMINAL
} from "../constants";

Expand Down Expand Up @@ -40,13 +44,34 @@ function isOptimismHardfork(hardfork: string | undefined) {
export function getDefaultOptions(userConfig: Readonly<HardhatUserConfig>): GasReporterOptions {
// let arbitrumHardfork: ArbitrumHardfork;
let optimismHardfork: OptimismHardfork;
let opStackBaseFeeScalar: number = 0;
let opStackBlobBaseFeeScalar: number = 0;

const userOptions = userConfig.gasReporter;

// NB: silently coercing to default if there's a misspelling or option not avail
if (userOptions) {
if (userOptions.L2 === "optimism" && !isOptimismHardfork(userOptions.optimismHardfork)){
optimismHardfork = DEFAULT_OPTIMISM_HARDFORK;
if (userOptions.L2 === "optimism" || userOptions.L2 === "base")
if (!isOptimismHardfork(userOptions.optimismHardfork)){
optimismHardfork = DEFAULT_OPTIMISM_HARDFORK;
}

if (userOptions.L2 === "optimism") {
if (!userOptions.opStackBaseFeeScalar) {
opStackBaseFeeScalar = OPTIMISM_ECOTONE_BASE_FEE_SCALAR;
}
if (!userOptions.opStackBlobBaseFeeScalar) {
opStackBlobBaseFeeScalar = OPTIMISM_ECOTONE_BLOB_BASE_FEE_SCALAR
}
}

if (userOptions.L2 === "base") {
if (!userOptions.opStackBaseFeeScalar) {
opStackBaseFeeScalar = BASE_ECOTONE_BASE_FEE_SCALAR;
}
if (!userOptions.opStackBlobBaseFeeScalar) {
opStackBlobBaseFeeScalar = BASE_ECOTONE_BLOB_BASE_FEE_SCALAR
}
}

// TODO: enable when arbitrum support added
Expand All @@ -69,6 +94,8 @@ export function getDefaultOptions(userConfig: Readonly<HardhatUserConfig>): GasR
L1: "ethereum",
noColors: false,
offline: false,
opStackBaseFeeScalar,
opStackBlobBaseFeeScalar,
optimismHardfork,
outputJSON: false,
outputJSONFile: DEFAULT_JSON_OUTPUT_FILE,
Expand Down
25 changes: 25 additions & 0 deletions src/lib/render/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ export function generateTerminalTextTable(
// NETWORK CONFIG
// ==============
let networkConfig: HorizontalTableRow = [];
const opStackConfig: HorizontalTableRow = [];

if (options.tokenPrice && options.gasPrice) {
const {
Expand Down Expand Up @@ -313,6 +314,25 @@ export function generateTerminalTextTable(
colSpan: 2,
content: chalk.magenta(`${rate} ${currency}/${token}`)
});

if (options.L2 === "optimism" || options.L2 === "base") {
opStackConfig.push({ colSpan: 2, content: " " })
opStackConfig.push({
hAlign: "left",
colSpan: 2,
content: chalk.cyan(`L1: ${options.blobBaseFee!} gwei (blobBaseFee)`)
});
opStackConfig.push({
hAlign: "left",
colSpan: 2,
content: chalk.cyan(`Base Fee Scalar: ${options.opStackBaseFeeScalar!}`)
});
opStackConfig.push({
hAlign: "left",
colSpan: 2,
content: chalk.cyan(`Blob Fee Scalar: ${options.opStackBlobBaseFeeScalar!}`)
});
}
} else {
networkConfig = [
{ hAlign: "left", colSpan: numberOfCols, content: chalk.green.bold("Methods") }
Expand Down Expand Up @@ -362,6 +382,11 @@ export function generateTerminalTextTable(
table.push(title);
table.push(solcConfig);
table.push(networkConfig);

if (opStackConfig.length > 0) {
table.push(opStackConfig);
}

table.push(methodsHeader);

methodRows.sort((a, b) => {
Expand Down
8 changes: 7 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export interface GasReporterOptions {

// TODO: Enable arbitrum when support added
/** @property L2 Network to calculate execution costs for */
L2?: "optimism" // | "arbitrum"
L2?: "optimism" | "base" // | "arbitrum"

/** @property Etherscan API key for L1 networks */
L1Etherscan?: string;
Expand All @@ -94,6 +94,12 @@ export interface GasReporterOptions {
/** @property Optimism client version to emulate gas costs for. Only applied when L2 is "optimism" */
optimismHardfork?: OptimismHardfork,

/** @property Scalar applied to L1 base fee (see Optimism gas fee documentation for details) */
opStackBaseFeeScalar?: number;

/** @property Scalar applied to L1 blob base fee (see Optimism gas fee documentation for details) */
opStackBlobBaseFeeScalar?: number;

/** @property Relative path to a file to output terminal table to (instead of stdout) */
outputFile?: string;

Expand Down
5 changes: 5 additions & 0 deletions src/utils/chains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,11 @@ export const L2 = {
gasPriceOracle: "0x420000000000000000000000000000000000000F",
token: "ETH"
},
base: {
baseUrl: "https://api.basescan.org/api?module=proxy&",
gasPriceOracle: "0x420000000000000000000000000000000000000F",
token: "ETH"
},
arbitrum: {
baseUrl: "https://api.arbiscan.io/api?module=proxy&",
token: "ETH"
Expand Down
26 changes: 16 additions & 10 deletions src/utils/gas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ import { serializeTransaction, Hex } from 'viem';
import {
EVM_BASE_TX_COST,
OPTIMISM_BEDROCK_DYNAMIC_OVERHEAD,
OPTIMISM_BEDROCK_FIXED_OVERHEAD,
OPTIMISM_ECOTONE_BASE_FEE_SCALAR,
OPTIMISM_ECOTONE_BLOB_BASE_FEE_SCALAR
OPTIMISM_BEDROCK_FIXED_OVERHEAD
} from "../constants";

import { GasReporterOptions, JsonRpcTx } from "../types";
Expand Down Expand Up @@ -107,13 +105,15 @@ export function getOptimismEcotoneL1Gas(tx: JsonRpcTx) {
* return fee / (16 * 10 ** DECIMALS);
* }
*/
export function getOptimismEcotoneL1Cost(
export function getOPStackEcotoneL1Cost(
txSerialized: number,
baseFee: number,
blobBaseFee: number
blobBaseFee: number,
opStackBaseFeeScalar: number,
opStackBlobBaseFeeScalar: number
): number {
const weightedBaseFee = 16 * OPTIMISM_ECOTONE_BASE_FEE_SCALAR * baseFee;
const weightedBlobBaseFee = OPTIMISM_ECOTONE_BLOB_BASE_FEE_SCALAR * blobBaseFee;
const weightedBaseFee = 16 * opStackBaseFeeScalar * baseFee;
const weightedBlobBaseFee = opStackBlobBaseFeeScalar * blobBaseFee;
return (txSerialized * (weightedBaseFee + weightedBlobBaseFee)) / 16000000;
}

Expand Down Expand Up @@ -245,7 +245,7 @@ export function getCalldataGasForNetwork(
options: GasReporterOptions,
tx: JsonRpcTx
) : number {
if (options.L2 === "optimism") {
if (options.L2 === "optimism" || options.L2 === "base") {
switch (options.optimismHardfork){
case "bedrock": return getOptimismBedrockL1Gas(tx);
case "ecotone": return getOptimismEcotoneL1Gas(tx);
Expand Down Expand Up @@ -277,10 +277,16 @@ export function getCalldataCostForNetwork(
options: GasReporterOptions,
gas: number,
) : number {
if (options.L2 === "optimism") {
if (options.L2 === "optimism" || options.L2 === "base") {
switch (options.optimismHardfork){
case "bedrock": return getOptimismBedrockL1Cost(gas, options.baseFee!);
case "ecotone": return getOptimismEcotoneL1Cost(gas, options.baseFee!, options.blobBaseFee!);
case "ecotone": return getOPStackEcotoneL1Cost(
gas,
options.baseFee!,
options.blobBaseFee!,
options.opStackBaseFeeScalar!,
options.opStackBlobBaseFeeScalar!
);
default: return 0; /** This shouldn't happen */
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/utils/prices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export async function setGasAndPriceRates(options: GasReporterOptions): Promise<

// blobBaseFee data: etherscan eth_call to OP Stack gas oracle on L2
if (
options.L2 === "optimism" &&
(options.L2 === "optimism" || options.L2 === "base") &&
options.optimismHardfork === "ecotone" &&
!options.blobBaseFee
) {
Expand All @@ -115,7 +115,7 @@ export async function setGasAndPriceRates(options: GasReporterOptions): Promise<
options.blobBaseFee = Math.round(hexWeiToIntGwei(blobBaseFee.data.result))
} catch (error) {
options.blobBaseFee = DEFAULT_BLOB_BASE_FEE;
warnings.push(warnBlobBaseFeeRemoteCallFailed(error, blobBaseFeeUrl));
warnings.push(warnBlobBaseFeeRemoteCallFailed(error));
}
}

Expand Down
9 changes: 5 additions & 4 deletions src/utils/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,11 @@ export function warnBaseFeeRemoteCallFailed(err: any, url: string): string {
}${remoteCallEndMessage(err)}`;
}

export function warnBlobBaseFeeRemoteCallFailed(err: any, url: string): string {
export function warnBlobBaseFeeRemoteCallFailed(err: any): string {
return `${
startWarning }${EOL
}${chalk.bold(`Failed to get blob base fee from ${url}`) }${EOL
}${chalk.bold(`Failed to fetch blob base fee, defaulting to 10 gwei.`) }${EOL
}${chalk.bold(`Try setting an API key for the "L2Etherscan" option.`) }${EOL
}${remoteCallEndMessage(err)}`;
}

Expand Down Expand Up @@ -201,9 +202,9 @@ export function getCommonTableVals(options: GasReporterOptions) {
const usingL1 = options.L2 === undefined;

let token = "";
let l1gwei: string | number = (usingL1) ? options.gasPrice!: options.blobBaseFee!;
let l1gwei: string | number = (usingL1) ? options.gasPrice!: options.baseFee!;
let l2gwei: string | number = (usingL1) ? "" : options.gasPrice!;
const l1gweiNote: string = (usingL1) ? "" : "(blobBaseFee)";
const l1gweiNote: string = (usingL1) ? "" : "(baseFee)";
const l2gweiNote: string = (usingL1) ? "" : "(gasPrice)";
const network = (usingL1) ? options.L1!.toUpperCase() : options.L2!.toUpperCase();

Expand Down
Loading
Loading