diff --git a/examples/fee-tester/.eslintrc.js b/examples/fee-tester/.eslintrc.js new file mode 100644 index 000000000..eca768e2f --- /dev/null +++ b/examples/fee-tester/.eslintrc.js @@ -0,0 +1,14 @@ +module.exports = { + extends: ["plugin:prettier/recommended"], + parserOptions: { + ecmaVersion: "latest", + }, + env: { + es6: true, + node: true, + }, + rules: { + "no-console": "error", + }, + ignorePatterns: [".eslintrc.js", "artifacts/*", "cache/*"], +}; diff --git a/examples/fee-tester/.gitignore b/examples/fee-tester/.gitignore new file mode 100644 index 000000000..2b8069dd9 --- /dev/null +++ b/examples/fee-tester/.gitignore @@ -0,0 +1,13 @@ +node_modules +.env +coverage +coverage.json +typechain +typechain-types + +#Hardhat files +cache +artifacts + +ignition/deployments + diff --git a/examples/fee-tester/.prettierignore b/examples/fee-tester/.prettierignore new file mode 100644 index 000000000..6da739a76 --- /dev/null +++ b/examples/fee-tester/.prettierignore @@ -0,0 +1,5 @@ +/node_modules +/artifacts +/cache +/coverage +/.nyc_output diff --git a/examples/fee-tester/README.md b/examples/fee-tester/README.md new file mode 100644 index 000000000..4fddc8ef3 --- /dev/null +++ b/examples/fee-tester/README.md @@ -0,0 +1 @@ +# Hardhat Fee Tester for Hardhat Ignition diff --git a/examples/fee-tester/contracts/FeeTester.sol b/examples/fee-tester/contracts/FeeTester.sol new file mode 100644 index 000000000..a76dbdf57 --- /dev/null +++ b/examples/fee-tester/contracts/FeeTester.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.9; + + +contract FeeTester { + address public owner; + + constructor() { + owner = msg.sender; + } + + // arbitrary function to call during fee testing + function deleteOwner() public { + delete owner; + } +} diff --git a/examples/fee-tester/hardhat.config.js b/examples/fee-tester/hardhat.config.js new file mode 100644 index 000000000..c0eacc6fd --- /dev/null +++ b/examples/fee-tester/hardhat.config.js @@ -0,0 +1,7 @@ +require("@nomicfoundation/hardhat-toolbox"); +require("@nomicfoundation/hardhat-ignition-ethers"); + +/** @type import('hardhat/config').HardhatUserConfig */ +module.exports = { + solidity: "0.8.19", +}; diff --git a/examples/fee-tester/ignition/modules/FeeTesterModule.js b/examples/fee-tester/ignition/modules/FeeTesterModule.js new file mode 100644 index 000000000..36dc5f22a --- /dev/null +++ b/examples/fee-tester/ignition/modules/FeeTesterModule.js @@ -0,0 +1,12 @@ +// ./ignition/FeeTesterModule.js +const { buildModule } = require("@nomicfoundation/hardhat-ignition/modules"); + +module.exports = buildModule("FeeTesterModule", (m) => { + const feeTester1 = m.contract("FeeTester"); + + m.call(feeTester1, "deleteOwner"); + + const feeTester2 = m.contract("FeeTester", [], { id: "feeTester2" }); + + return { feeTester1, feeTester2 }; +}); diff --git a/examples/fee-tester/package.json b/examples/fee-tester/package.json new file mode 100644 index 000000000..b3d408075 --- /dev/null +++ b/examples/fee-tester/package.json @@ -0,0 +1,18 @@ +{ + "name": "@nomicfoundation/ignition-fee-tester-example", + "private": true, + "version": "0.15.2", + "scripts": { + "test": "hardhat test", + "lint": "npm run prettier -- --check && npm run eslint", + "lint:fix": "npm run prettier -- --write && npm run eslint -- --fix", + "eslint": "eslint \"ignition/**/*.{js,jsx}\" \"test/**/*.{js,jsx}\"", + "prettier": "prettier \"*.{js,md,json}\" \"ignition/modules/*.{js,md,json}\" \"test/*.{js,md,json}\" \"contracts/**/*.sol\"" + }, + "devDependencies": { + "@nomicfoundation/hardhat-ignition-ethers": "workspace:^", + "@nomicfoundation/hardhat-toolbox": "4.0.0", + "hardhat": "^2.18.0", + "prettier-plugin-solidity": "1.1.3" + } +} diff --git a/packages/core/src/deploy.ts b/packages/core/src/deploy.ts index bed82f401..68d81293f 100644 --- a/packages/core/src/deploy.ts +++ b/packages/core/src/deploy.ts @@ -53,6 +53,8 @@ export async function deploy< strategyConfig, maxFeePerGasLimit, maxPriorityFeePerGas, + gasPrice, + disableFeeBumping, }: { config?: Partial; artifactResolver: ArtifactResolver; @@ -71,6 +73,8 @@ export async function deploy< strategyConfig?: StrategyConfig[StrategyT]; maxFeePerGasLimit?: bigint; maxPriorityFeePerGas?: bigint; + gasPrice?: bigint; + disableFeeBumping?: boolean; }): Promise { const executionStrategy: ExecutionStrategy = resolveStrategy( strategy, @@ -117,6 +121,7 @@ export async function deploy< const jsonRpcClient = new EIP1193JsonRpcClient(provider, { maxFeePerGasLimit, maxPriorityFeePerGas, + gasPrice, }); const isAutominedNetwork = await checkAutominedNetwork(provider); @@ -126,6 +131,7 @@ export async function deploy< requiredConfirmations: isAutominedNetwork ? DEFAULT_AUTOMINE_REQUIRED_CONFIRMATIONS : config.requiredConfirmations ?? defaultConfig.requiredConfirmations, + disableFeeBumping: disableFeeBumping ?? defaultConfig.disableFeeBumping, ...config, }; diff --git a/packages/core/src/internal/defaultConfig.ts b/packages/core/src/internal/defaultConfig.ts index e357867e4..3d7670ffb 100644 --- a/packages/core/src/internal/defaultConfig.ts +++ b/packages/core/src/internal/defaultConfig.ts @@ -8,6 +8,7 @@ export const defaultConfig: DeployConfig = { timeBeforeBumpingFees: 3 * 60 * 1_000, maxFeeBumps: 4, requiredConfirmations: 5, + disableFeeBumping: false, }; /** diff --git a/packages/core/src/internal/deployer.ts b/packages/core/src/internal/deployer.ts index 1aaa7837a..e264e1d03 100644 --- a/packages/core/src/internal/deployer.ts +++ b/packages/core/src/internal/deployer.ts @@ -83,7 +83,8 @@ export class Deployer { ignitionModule.id, this._deploymentDir, isResumed, - this._config.maxFeeBumps + this._config.maxFeeBumps, + this._config.disableFeeBumping ); const contracts = @@ -188,7 +189,8 @@ export class Deployer { this._config.requiredConfirmations, this._config.timeBeforeBumpingFees, this._config.maxFeeBumps, - this._config.blockPollingInterval + this._config.blockPollingInterval, + this._config.disableFeeBumping ); deploymentState = await executionEngine.executeModule( @@ -270,7 +272,8 @@ export class Deployer { moduleId: string, deploymentDir: string | undefined, isResumed: boolean, - maxFeeBumps: number + maxFeeBumps: number, + disableFeeBumping: boolean ): void { if (this._executionEventListener === undefined) { return; @@ -282,6 +285,7 @@ export class Deployer { deploymentDir: deploymentDir ?? undefined, isResumed, maxFeeBumps, + disableFeeBumping, }); } diff --git a/packages/core/src/internal/execution/execution-engine.ts b/packages/core/src/internal/execution/execution-engine.ts index 29aeb677b..f511bae7c 100644 --- a/packages/core/src/internal/execution/execution-engine.ts +++ b/packages/core/src/internal/execution/execution-engine.ts @@ -42,7 +42,8 @@ export class ExecutionEngine { private readonly _requiredConfirmations: number, private readonly _millisecondBeforeBumpingFees: number, private readonly _maxFeeBumps: number, - private readonly _blockPollingInterval: number + private readonly _blockPollingInterval: number, + private readonly _disableFeeBumping: boolean ) {} /** @@ -98,7 +99,8 @@ export class ExecutionEngine { this._maxFeeBumps, accounts, deploymentParameters, - defaultSender + defaultSender, + this._disableFeeBumping ); const futures = getFuturesFromModule(module); diff --git a/packages/core/src/internal/execution/future-processor/future-processor.ts b/packages/core/src/internal/execution/future-processor/future-processor.ts index 32c2ed421..293da6f57 100644 --- a/packages/core/src/internal/execution/future-processor/future-processor.ts +++ b/packages/core/src/internal/execution/future-processor/future-processor.ts @@ -49,7 +49,8 @@ export class FutureProcessor { private readonly _maxFeeBumps: number, private readonly _accounts: string[], private readonly _deploymentParameters: DeploymentParameters, - private readonly _defaultSender: string + private readonly _defaultSender: string, + private readonly _disableFeeBumping: boolean ) {} /** @@ -209,7 +210,9 @@ export class FutureProcessor { this._transactionTrackingTimer, this._requiredConfirmations, this._millisecondBeforeBumpingFees, - this._maxFeeBumps + this._maxFeeBumps, + undefined, + this._disableFeeBumping ); } } diff --git a/packages/core/src/internal/execution/future-processor/handlers/monitor-onchain-interaction.ts b/packages/core/src/internal/execution/future-processor/handlers/monitor-onchain-interaction.ts index a0528271d..b6f7be76b 100644 --- a/packages/core/src/internal/execution/future-processor/handlers/monitor-onchain-interaction.ts +++ b/packages/core/src/internal/execution/future-processor/handlers/monitor-onchain-interaction.ts @@ -52,6 +52,8 @@ export interface GetTransactionRetryConfig { * of a transaction. * @param maxFeeBumps The maximum number of times we can bump the fees of a transaction * before considering the onchain interaction timed out. + * @param getTransactionRetryConfig This is really only a parameter to help with testing this function + * @param disableFeeBumping Disables fee bumping for all transactions. * @returns A message indicating the result of checking the transactions of the latest * network interaction. */ @@ -68,7 +70,8 @@ export async function monitorOnchainInteraction( getTransactionRetryConfig: GetTransactionRetryConfig = { maxRetries: 10, retryInterval: 1000, - } + }, + disableFeeBumping: boolean ): Promise< | TransactionConfirmMessage | OnchainInteractionBumpFeesMessage @@ -143,7 +146,10 @@ export async function monitorOnchainInteraction( return undefined; } - if (lastNetworkInteraction.transactions.length > maxFeeBumps) { + if ( + disableFeeBumping || + lastNetworkInteraction.transactions.length > maxFeeBumps + ) { return { type: JournalMessageType.ONCHAIN_INTERACTION_TIMEOUT, futureId: exState.id, diff --git a/packages/core/src/internal/execution/jsonrpc-client.ts b/packages/core/src/internal/execution/jsonrpc-client.ts index c71449f35..17b76d81c 100644 --- a/packages/core/src/internal/execution/jsonrpc-client.ts +++ b/packages/core/src/internal/execution/jsonrpc-client.ts @@ -192,6 +192,7 @@ export class EIP1193JsonRpcClient implements JsonRpcClient { private readonly _config?: { maxFeePerGasLimit?: bigint; maxPriorityFeePerGas?: bigint; + gasPrice?: bigint; } ) {} @@ -643,9 +644,13 @@ export class EIP1193JsonRpcClient implements JsonRpcClient { ]); // We prioritize EIP-1559 fees over legacy gasPrice fees, however, - // polygon (chainId 137) requires legacy gasPrice fees - // so we skip EIP-1559 logic in that case - if (latestBlock.baseFeePerGas !== undefined && chainId !== 137) { + // polygon (chainId 137) and polygon's amoy testnet (chainId 80002) + // both require legacy gasPrice fees so we skip EIP-1559 logic in those cases + if ( + latestBlock.baseFeePerGas !== undefined && + chainId !== 137 && + chainId !== 80002 + ) { // Support zero gas fee chains, such as a private instances // of blockchains using Besu. We explicitly exclude BNB // Smartchain (chainId 56) and its testnet (chainId 97) @@ -674,6 +679,28 @@ export class EIP1193JsonRpcClient implements JsonRpcClient { }; } + /** + * Polygon amoy testnet (chainId 80002) currently has a bug causing the + * `eth_gasPrice` RPC call to return an amount that is way too high. + * We hardcode the gas price for this chain for now until the bug is fixed. + * See: https://github.com/maticnetwork/bor/issues/1213 + * + * Note that at the time of this implementation, the issue was autoclosed by a bot + * as a maintainer had not responded to the issue yet. Users continue to report + * the bug in the issue comments, however. + * + * All of that to say, when evaluating whether this logic is still needed in the future, + * it will likely be required to read through the issue above, rather than relying on the + * status of the github issue itself. + */ + if (chainId === 80002) { + return { gasPrice: 32000000000n }; + } + + if (this._config?.gasPrice !== undefined) { + return { gasPrice: this._config.gasPrice }; + } + const response = await this._provider.request({ method: "eth_gasPrice", params: [], @@ -699,7 +726,7 @@ export class EIP1193JsonRpcClient implements JsonRpcClient { } try { - return await this._getMaxPrioirtyFeePerGas(); + return await this._getMaxPriorityFeePerGas(); } catch { // the max priority fee RPC call is not supported by // this chain @@ -708,7 +735,7 @@ export class EIP1193JsonRpcClient implements JsonRpcClient { return DEFAULT_MAX_PRIORITY_FEE_PER_GAS; } - private async _getMaxPrioirtyFeePerGas(): Promise { + private async _getMaxPriorityFeePerGas(): Promise { const fee = await this._provider.request({ method: "eth_maxPriorityFeePerGas", }); diff --git a/packages/core/src/types/deploy.ts b/packages/core/src/types/deploy.ts index a3c404b41..a11a10cac 100644 --- a/packages/core/src/types/deploy.ts +++ b/packages/core/src/types/deploy.ts @@ -28,6 +28,11 @@ export interface DeployConfig { * a transaction to be confirmed during Ignition execution. */ requiredConfirmations: number; + + /** + * Disables fee bumping for all transactions. + */ + disableFeeBumping: boolean; } /** diff --git a/packages/core/src/types/execution-events.ts b/packages/core/src/types/execution-events.ts index 35c6a8433..5d328ee72 100644 --- a/packages/core/src/types/execution-events.ts +++ b/packages/core/src/types/execution-events.ts @@ -83,6 +83,7 @@ export interface DeploymentStartEvent { deploymentDir: string | undefined; isResumed: boolean; maxFeeBumps: number; + disableFeeBumping: boolean; } /** diff --git a/packages/core/test/execution/future-processor/helpers/network-interaction-execution.ts b/packages/core/test/execution/future-processor/helpers/network-interaction-execution.ts index c943c062a..3a343fe9a 100644 --- a/packages/core/test/execution/future-processor/helpers/network-interaction-execution.ts +++ b/packages/core/test/execution/future-processor/helpers/network-interaction-execution.ts @@ -242,7 +242,8 @@ describe("Network interactions", () => { requiredConfirmations, millisecondBeforeBumpingFees, maxFeeBumps, - testGetTransactionRetryConfig + testGetTransactionRetryConfig, + false ); if (message === undefined) { @@ -286,7 +287,8 @@ describe("Network interactions", () => { requiredConfirmations, millisecondBeforeBumpingFees, maxFeeBumps, - testGetTransactionRetryConfig + testGetTransactionRetryConfig, + false ), /IGN401: Error while executing test: all the transactions of its network interaction 1 were dropped\. Please try rerunning Hardhat Ignition\./ ); diff --git a/packages/core/test/execution/future-processor/utils.ts b/packages/core/test/execution/future-processor/utils.ts index c37ded417..26a011cce 100644 --- a/packages/core/test/execution/future-processor/utils.ts +++ b/packages/core/test/execution/future-processor/utils.ts @@ -64,7 +64,8 @@ export async function setupFutureProcessor( 100, // maxFeeBumps exampleAccounts, {}, - getDefaultSender(exampleAccounts) + getDefaultSender(exampleAccounts), + false // disableFeeBumping ); return { processor, storedDeployedAddresses }; diff --git a/packages/hardhat-plugin/src/index.ts b/packages/hardhat-plugin/src/index.ts index b144741d7..9746b36f9 100644 --- a/packages/hardhat-plugin/src/index.ts +++ b/packages/hardhat-plugin/src/index.ts @@ -52,6 +52,8 @@ extendConfig((config, userConfig) => { config.networks[networkName].ignition = { maxFeePerGasLimit: userNetworkConfig.ignition?.maxFeePerGasLimit, maxPriorityFeePerGas: userNetworkConfig.ignition?.maxPriorityFeePerGas, + gasPrice: userNetworkConfig.ignition?.gasPrice, + disableFeeBumping: userNetworkConfig.ignition?.disableFeeBumping, }; }); @@ -320,6 +322,9 @@ ignitionScope maxPriorityFeePerGas: hre.config.networks[hre.network.name]?.ignition .maxPriorityFeePerGas, + gasPrice: hre.config.networks[hre.network.name]?.ignition.gasPrice, + disableFeeBumping: + hre.config.networks[hre.network.name]?.ignition.disableFeeBumping, }); try { diff --git a/packages/hardhat-plugin/src/type-extensions.ts b/packages/hardhat-plugin/src/type-extensions.ts index c87988ef6..e79353260 100644 --- a/packages/hardhat-plugin/src/type-extensions.ts +++ b/packages/hardhat-plugin/src/type-extensions.ts @@ -17,6 +17,8 @@ declare module "hardhat/types/config" { ignition?: { maxFeePerGasLimit?: bigint; maxPriorityFeePerGas?: bigint; + gasPrice?: bigint; + disableFeeBumping?: boolean; }; } @@ -24,6 +26,8 @@ declare module "hardhat/types/config" { ignition: { maxFeePerGasLimit?: bigint; maxPriorityFeePerGas?: bigint; + gasPrice?: bigint; + disableFeeBumping?: boolean; }; } @@ -31,6 +35,8 @@ declare module "hardhat/types/config" { ignition?: { maxFeePerGasLimit?: bigint; maxPriorityFeePerGas?: bigint; + gasPrice?: bigint; + disableFeeBumping?: boolean; }; } @@ -38,6 +44,8 @@ declare module "hardhat/types/config" { ignition: { maxFeePerGasLimit?: bigint; maxPriorityFeePerGas?: bigint; + gasPrice?: bigint; + disableFeeBumping?: boolean; }; } diff --git a/packages/hardhat-plugin/src/ui/pretty-event-handler.ts b/packages/hardhat-plugin/src/ui/pretty-event-handler.ts index ec503f6f1..a5d7070fe 100644 --- a/packages/hardhat-plugin/src/ui/pretty-event-handler.ts +++ b/packages/hardhat-plugin/src/ui/pretty-event-handler.ts @@ -66,6 +66,7 @@ export class PrettyEventHandler implements ExecutionEventListener { warnings: [], isResumed: null, maxFeeBumps: 0, + disableFeeBumping: null, gasBumps: {}, strategy: null, ledger: false, @@ -94,6 +95,7 @@ export class PrettyEventHandler implements ExecutionEventListener { deploymentDir: event.deploymentDir, isResumed: event.isResumed, maxFeeBumps: event.maxFeeBumps, + disableFeeBumping: event.disableFeeBumping, }; process.stdout.write(calculateStartingMessage(this.state)); diff --git a/packages/hardhat-plugin/src/ui/types.ts b/packages/hardhat-plugin/src/ui/types.ts index 40e805e47..1eb6a1563 100644 --- a/packages/hardhat-plugin/src/ui/types.ts +++ b/packages/hardhat-plugin/src/ui/types.ts @@ -64,6 +64,7 @@ export interface UiState { isResumed: boolean | null; maxFeeBumps: number; gasBumps: Record; + disableFeeBumping: boolean | null; strategy: string | null; ledger: boolean; ledgerMessage: string; diff --git a/packages/hardhat-plugin/test/config.ts b/packages/hardhat-plugin/test/config.ts index d4a96b4bd..05cc7a6c2 100644 --- a/packages/hardhat-plugin/test/config.ts +++ b/packages/hardhat-plugin/test/config.ts @@ -39,6 +39,10 @@ describe("config", () => { }); }); + it("should apply disableFeeBumping at the top level", async function () { + assert.equal(loadedOptions.disableFeeBumping, true); + }); + it("should apply maxFeePerGasLimit", async function () { assert.equal( this.hre.config.networks.hardhat.ignition.maxFeePerGasLimit, @@ -53,9 +57,21 @@ describe("config", () => { ); }); + it("should apply gasPrice", async function () { + assert.equal(this.hre.config.networks.hardhat.ignition.gasPrice, 1n); + }); + + it("should apply disableFeeBumping at the network level", async function () { + assert.equal( + this.hre.config.networks.hardhat.ignition.disableFeeBumping, + false + ); + }); + it("should only have known config", () => { const configOptions: KeyListOf = [ "blockPollingInterval", + "disableFeeBumping", "maxFeeBumps", "requiredConfirmations", "strategyConfig", diff --git a/packages/hardhat-plugin/test/fixture-projects/with-config/hardhat.config.js b/packages/hardhat-plugin/test/fixture-projects/with-config/hardhat.config.js index c1bfe3856..1a2ca9a96 100644 --- a/packages/hardhat-plugin/test/fixture-projects/with-config/hardhat.config.js +++ b/packages/hardhat-plugin/test/fixture-projects/with-config/hardhat.config.js @@ -9,6 +9,8 @@ module.exports = { ignition: { maxFeePerGasLimit: 2n, maxPriorityFeePerGas: 3n, + gasPrice: 1n, + disableFeeBumping: false, }, }, }, @@ -22,5 +24,6 @@ module.exports = { salt: "custom-salt", }, }, + disableFeeBumping: true, }, }; diff --git a/packages/hardhat-plugin/test/test-helpers/use-ignition-project.ts b/packages/hardhat-plugin/test/test-helpers/use-ignition-project.ts index 6a77dc0da..7f2bbceb9 100644 --- a/packages/hardhat-plugin/test/test-helpers/use-ignition-project.ts +++ b/packages/hardhat-plugin/test/test-helpers/use-ignition-project.ts @@ -25,6 +25,7 @@ const defaultTestConfig: DeployConfig = { timeBeforeBumpingFees: 3 * 60 * 1000, blockPollingInterval: 200, requiredConfirmations: 1, + disableFeeBumping: false, }; export function useEphemeralIgnitionProject(fixtureProjectName: string) { diff --git a/packages/hardhat-plugin/test/ui/helpers/calculate-batch-display.ts b/packages/hardhat-plugin/test/ui/helpers/calculate-batch-display.ts index f77aa8a1f..64559c324 100644 --- a/packages/hardhat-plugin/test/ui/helpers/calculate-batch-display.ts +++ b/packages/hardhat-plugin/test/ui/helpers/calculate-batch-display.ts @@ -21,6 +21,7 @@ const exampleState: UiState = { warnings: [], isResumed: false, maxFeeBumps: 0, + disableFeeBumping: false, gasBumps: {}, strategy: null, ledger: false, diff --git a/packages/hardhat-plugin/test/ui/helpers/calculate-deploying-module-panel-display.ts b/packages/hardhat-plugin/test/ui/helpers/calculate-deploying-module-panel-display.ts index 4fe05ab54..3fbbc7adb 100644 --- a/packages/hardhat-plugin/test/ui/helpers/calculate-deploying-module-panel-display.ts +++ b/packages/hardhat-plugin/test/ui/helpers/calculate-deploying-module-panel-display.ts @@ -19,6 +19,7 @@ describe("ui - calculate starting message display", () => { warnings: [], isResumed: null, maxFeeBumps: 0, + disableFeeBumping: false, gasBumps: {}, strategy: "basic", ledger: false,