From ca8011ad754935b30f5700b594f451c1a771be30 Mon Sep 17 00:00:00 2001 From: Sahil Vasava Date: Tue, 23 Jun 2026 20:32:07 +0700 Subject: [PATCH 1/3] feat: add max-priority-fee-per-gas-cap to ceiling the oracle tip The gas price manager has floors (floor-max-fee-per-gas / floor-max-priority-fee-per-gas) but no ceiling, so it bids whatever eth_maxPriorityFeePerGas returns verbatim. On low-traffic chains where the bundler is the dominant tx sender, the priority-fee oracle feedback-loops on the bundler's own past tips and reports an absurd value (observed: 500 gwei on Arc mainnet, where base fee sits at the 20 gwei protocol floor and blocks are empty). With no cap, the executor pays a ~500 gwei tip for txs that need ~0/1 gwei (per Arc's docs), costing ~0.24-0.36 USDC each vs a ~0.01 USDC target -- a ~25x overpay that drains the executor wallet. Add an optional max-priority-fee-per-gas-cap (gwei, like the floors). When set, the tip is capped to it and maxFeePerGas is reduced by the same delta, so we never overpay the tip and never push maxFeePerGas below the base-fee component. Applied after the floors so the cap wins if both are configured. No behavior change when unset. Refactors bumpTheGasPrice to a single return so floors + cap compose cleanly. --- src/cli/config/bundler.ts | 4 +++ src/cli/config/options.ts | 6 ++++ src/handlers/gasPriceManager.ts | 54 ++++++++++++++++++++------------- 3 files changed, 43 insertions(+), 21 deletions(-) diff --git a/src/cli/config/bundler.ts b/src/cli/config/bundler.ts index 97472e66..8bd099cd 100644 --- a/src/cli/config/bundler.ts +++ b/src/cli/config/bundler.ts @@ -188,6 +188,10 @@ export const compatibilityArgsSchema = z.object({ .string() .transform((val) => parseGwei(val)) .optional(), + "max-priority-fee-per-gas-cap": z + .string() + .transform((val) => parseGwei(val)) + .optional(), "supports-eip7623": z.boolean().default(false) }) diff --git a/src/cli/config/options.ts b/src/cli/config/options.ts index e7685b1b..1664ddab 100644 --- a/src/cli/config/options.ts +++ b/src/cli/config/options.ts @@ -540,6 +540,12 @@ export const compatibilityOptions: CliCommandOptions = type: "string", require: false }, + "max-priority-fee-per-gas-cap": { + description: + "Maximum value for maxPriorityFeePerGas to enforce (in gwei). Caps the tip taken from the network gas oracle. On low-traffic chains the oracle can feedback-loop on the bundler's own past tips and report an absurd value; without a ceiling the bundler bids it verbatim and overpays. maxFeePerGas is reduced by the same amount the tip is capped.", + type: "string", + require: false + }, "supports-eip7623": { description: "Whether the chain supports EIP-7623 (Increase calldata cost to reduce maximum block size)", diff --git a/src/handlers/gasPriceManager.ts b/src/handlers/gasPriceManager.ts index 8514ed3e..740db55c 100644 --- a/src/handlers/gasPriceManager.ts +++ b/src/handlers/gasPriceManager.ts @@ -131,29 +131,41 @@ export class GasPriceManager { maxPriorityFeePerGas: (maxPriorityFeePerGas * bumpAmount) / 100n } - if ( - this.config.floorMaxFeePerGas || - this.config.floorMaxPriorityFeePerGas - ) { - const maxFeePerGas = this.config.floorMaxFeePerGas - ? maxBigInt(this.config.floorMaxFeePerGas, result.maxFeePerGas) - : result.maxFeePerGas - - const maxPriorityFeePerGas = this.config.floorMaxPriorityFeePerGas - ? maxBigInt( - this.config.floorMaxPriorityFeePerGas, - result.maxPriorityFeePerGas - ) - : result.maxPriorityFeePerGas - - return { - // Ensure that maxFeePerGas is always greater or equal than maxPriorityFeePerGas - maxFeePerGas: maxBigInt(maxFeePerGas, maxPriorityFeePerGas), - maxPriorityFeePerGas - } + let finalMaxFeePerGas = this.config.floorMaxFeePerGas + ? maxBigInt(this.config.floorMaxFeePerGas, result.maxFeePerGas) + : result.maxFeePerGas + + let finalMaxPriorityFeePerGas = this.config.floorMaxPriorityFeePerGas + ? maxBigInt( + this.config.floorMaxPriorityFeePerGas, + result.maxPriorityFeePerGas + ) + : result.maxPriorityFeePerGas + + // Ceiling on the priority fee taken from the network gas oracle. On + // low-traffic chains the eth_maxPriorityFeePerGas oracle can feedback-loop + // on the bundler's own past tips and report an absurd value (e.g. 500 gwei + // on a 20 gwei base-fee, empty-block chain), which we'd otherwise bid + // verbatim and overpay ~25x. Cap the tip and reduce maxFeePerGas by the + // same delta so we don't pay an inflated tip. Applied after the floors so + // the cap always wins if both are configured. The base-fee component of + // maxFeePerGas is preserved, so this never pushes maxFeePerGas below the + // base fee. + if (this.config.maxPriorityFeePerGasCap !== undefined) { + const cappedPriorityFee = minBigInt( + finalMaxPriorityFeePerGas, + this.config.maxPriorityFeePerGasCap + ) + const priorityFeeDelta = finalMaxPriorityFeePerGas - cappedPriorityFee + finalMaxFeePerGas -= priorityFeeDelta + finalMaxPriorityFeePerGas = cappedPriorityFee } - return result + return { + // Ensure that maxFeePerGas is always greater or equal than maxPriorityFeePerGas + maxFeePerGas: maxBigInt(finalMaxFeePerGas, finalMaxPriorityFeePerGas), + maxPriorityFeePerGas: finalMaxPriorityFeePerGas + } } private async getFallBackMaxPriorityFeePerGas( From 8e34ef709abaf364192905e26f1c0b9201b3bc42 Mon Sep 17 00:00:00 2001 From: SahilVasava <24778390+SahilVasava@users.noreply.github.com> Date: Tue, 23 Jun 2026 13:33:02 +0000 Subject: [PATCH 2/3] chore: format --- src/handlers/gasPriceManager.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/handlers/gasPriceManager.ts b/src/handlers/gasPriceManager.ts index 740db55c..407b8d2c 100644 --- a/src/handlers/gasPriceManager.ts +++ b/src/handlers/gasPriceManager.ts @@ -156,14 +156,18 @@ export class GasPriceManager { finalMaxPriorityFeePerGas, this.config.maxPriorityFeePerGasCap ) - const priorityFeeDelta = finalMaxPriorityFeePerGas - cappedPriorityFee + const priorityFeeDelta = + finalMaxPriorityFeePerGas - cappedPriorityFee finalMaxFeePerGas -= priorityFeeDelta finalMaxPriorityFeePerGas = cappedPriorityFee } return { // Ensure that maxFeePerGas is always greater or equal than maxPriorityFeePerGas - maxFeePerGas: maxBigInt(finalMaxFeePerGas, finalMaxPriorityFeePerGas), + maxFeePerGas: maxBigInt( + finalMaxFeePerGas, + finalMaxPriorityFeePerGas + ), maxPriorityFeePerGas: finalMaxPriorityFeePerGas } } From bc90dd1917b0722fcf6f5a922d97059c5ff98c0a Mon Sep 17 00:00:00 2001 From: Sahil Vasava Date: Tue, 23 Jun 2026 21:38:52 +0700 Subject: [PATCH 3/3] fix: apply priority fee cap after gas price normalization --- src/handlers/gasPriceManager.ts | 60 ++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 28 deletions(-) diff --git a/src/handlers/gasPriceManager.ts b/src/handlers/gasPriceManager.ts index 407b8d2c..ee1922e4 100644 --- a/src/handlers/gasPriceManager.ts +++ b/src/handlers/gasPriceManager.ts @@ -131,37 +131,17 @@ export class GasPriceManager { maxPriorityFeePerGas: (maxPriorityFeePerGas * bumpAmount) / 100n } - let finalMaxFeePerGas = this.config.floorMaxFeePerGas + const finalMaxFeePerGas = this.config.floorMaxFeePerGas ? maxBigInt(this.config.floorMaxFeePerGas, result.maxFeePerGas) : result.maxFeePerGas - let finalMaxPriorityFeePerGas = this.config.floorMaxPriorityFeePerGas + const finalMaxPriorityFeePerGas = this.config.floorMaxPriorityFeePerGas ? maxBigInt( this.config.floorMaxPriorityFeePerGas, result.maxPriorityFeePerGas ) : result.maxPriorityFeePerGas - // Ceiling on the priority fee taken from the network gas oracle. On - // low-traffic chains the eth_maxPriorityFeePerGas oracle can feedback-loop - // on the bundler's own past tips and report an absurd value (e.g. 500 gwei - // on a 20 gwei base-fee, empty-block chain), which we'd otherwise bid - // verbatim and overpay ~25x. Cap the tip and reduce maxFeePerGas by the - // same delta so we don't pay an inflated tip. Applied after the floors so - // the cap always wins if both are configured. The base-fee component of - // maxFeePerGas is preserved, so this never pushes maxFeePerGas below the - // base fee. - if (this.config.maxPriorityFeePerGasCap !== undefined) { - const cappedPriorityFee = minBigInt( - finalMaxPriorityFeePerGas, - this.config.maxPriorityFeePerGasCap - ) - const priorityFeeDelta = - finalMaxPriorityFeePerGas - cappedPriorityFee - finalMaxFeePerGas -= priorityFeeDelta - finalMaxPriorityFeePerGas = cappedPriorityFee - } - return { // Ensure that maxFeePerGas is always greater or equal than maxPriorityFeePerGas maxFeePerGas: maxBigInt( @@ -172,6 +152,30 @@ export class GasPriceManager { } } + private applyMaxPriorityFeePerGasCap( + gasPriceParameters: GasPriceParameters + ): GasPriceParameters { + // Apply after final lower-bound checks so the oracle value cannot restore + // an uncapped priority fee. + if (this.config.maxPriorityFeePerGasCap !== undefined) { + const cappedPriorityFee = minBigInt( + gasPriceParameters.maxPriorityFeePerGas, + this.config.maxPriorityFeePerGasCap + ) + const priorityFeeDelta = + gasPriceParameters.maxPriorityFeePerGas - cappedPriorityFee + const maxFeePerGas = + gasPriceParameters.maxFeePerGas - priorityFeeDelta + + return { + maxFeePerGas: maxBigInt(maxFeePerGas, cappedPriorityFee), + maxPriorityFeePerGas: cappedPriorityFee + } + } + + return gasPriceParameters + } + private async getFallBackMaxPriorityFeePerGas( publicClient: PublicClient, gasPrice: bigint @@ -295,7 +299,7 @@ export class GasPriceManager { maxPriorityFeePerGas: polygonEstimate.maxPriorityFeePerGas }) - return { + return this.applyMaxPriorityFeePerGasCap({ maxFeePerGas: maxBigInt( gasPrice.maxFeePerGas, maxFeePerGas @@ -304,7 +308,7 @@ export class GasPriceManager { gasPrice.maxPriorityFeePerGas, maxPriorityFeePerGas ) - } + }) } } @@ -312,13 +316,13 @@ export class GasPriceManager { const gasPrice = this.bumpTheGasPrice( await this.getLegacyTransactionGasPrice() ) - return { + return this.applyMaxPriorityFeePerGasCap({ maxFeePerGas: maxBigInt(gasPrice.maxFeePerGas, maxFeePerGas), maxPriorityFeePerGas: maxBigInt( gasPrice.maxPriorityFeePerGas, maxPriorityFeePerGas ) - } + }) } const estimatedPrice = await this.estimateGasPrice() @@ -330,13 +334,13 @@ export class GasPriceManager { maxFeePerGas, maxPriorityFeePerGas }) - return { + return this.applyMaxPriorityFeePerGasCap({ maxFeePerGas: maxBigInt(gasPrice.maxFeePerGas, maxFeePerGas), maxPriorityFeePerGas: maxBigInt( gasPrice.maxPriorityFeePerGas, maxPriorityFeePerGas ) - } + }) } // This method throws if it can't get a valid RPC response.