From c246b1411f0527ae77466951c0175300c8d1cd99 Mon Sep 17 00:00:00 2001 From: kaladinlight <35275952+kaladinlight@users.noreply.github.com> Date: Fri, 10 Jul 2026 11:49:02 -0600 Subject: [PATCH 1/2] fix(evm): buffer arbitrum gasPrice/maxFeePerGas to prevent base-fee-drift tx failures Arbitrum's sequencer orders transactions first-come-first-served and ignores priority fees, so eth_feeHistory reports 0 rewards at every percentile. That collapsed all three fee tiers to identical values and, more importantly, set gasPrice to the bare base fee with zero headroom. Clients submitting that gasPrice failed with "max fee per gas less than block base fee" on any upward base-fee tick. Make the base fee and gas price tier multipliers configurable per chain (defaulting to the previous behavior, so no other coinstack changes) and opt Arbitrum into per-tier buffers: modest headroom on gasPrice to survive base-fee drift without overpaying legacy txs, and larger cap-only headroom on maxFeePerGas. Verified Optimism and Base have non-zero priority fees and stable base fees, so they are unaffected and unchanged. Co-Authored-By: Claude Opus 4.8 --- node/coinstacks/arbitrum/api/src/controller.ts | 10 +++++++++- .../common/api/src/evm/moralisService.ts | 18 ++++++++++++++---- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/node/coinstacks/arbitrum/api/src/controller.ts b/node/coinstacks/arbitrum/api/src/controller.ts index 641373564..d943b3d90 100644 --- a/node/coinstacks/arbitrum/api/src/controller.ts +++ b/node/coinstacks/arbitrum/api/src/controller.ts @@ -34,7 +34,15 @@ const alchemyClient = createPublicClient({ transport: http(`https://arb-mainnet.g.alchemy.com/v2/${ALCHEMY_API_KEY}`), }) -export const service = new MoralisService({ chain: EvmChain.ARBITRUM, logger, client, rpcUrl }) +export const service = new MoralisService({ + chain: EvmChain.ARBITRUM, + logger, + client, + rpcUrl, + baseFeeMultiplier: [1.5, 2, 3], + gasPriceMultiplier: [1.1, 1.2, 1.5], +}) + export const cache = new EventCache({ client, alchemyClient, logger }) // assign service to be used for all instances of EVM diff --git a/node/coinstacks/common/api/src/evm/moralisService.ts b/node/coinstacks/common/api/src/evm/moralisService.ts index c8a811bbe..f8025955d 100644 --- a/node/coinstacks/common/api/src/evm/moralisService.ts +++ b/node/coinstacks/common/api/src/evm/moralisService.ts @@ -29,8 +29,11 @@ const FEE_HISTORY_BLOCK_COUNT = 100 // reward percentiles used to estimate [slow, average, fast] priority fees respectively const REWARD_PERCENTILES = [50, 70, 90] -// multiplier applied to the next base fee when calculating maxFeePerGas so the tx stays valid as the base fee rises -const BASE_FEE_MULTIPLIER = 2 +// default per-tier multipliers [slow, average, fast] for base fee +const DEFAULT_BASE_FEE_MULTIPLIER = [2, 2, 2] + +// default per-tier multipliers [slow, average, fast] for gas price +const DEFAULT_GAS_PRICE_MULTIPLIER = [1, 1, 1] // drop priority fees above the median by this factor to keep outliers (mev/overpayers) from skewing estimates const OUTLIER_THRESHOLD_MULTIPLIER = 10 @@ -52,6 +55,8 @@ export interface MoralisServiceArgs { rpcUrl: string explorerApiUrl?: URL minPriorityFee?: string + baseFeeMultiplier?: number[] + gasPriceMultiplier?: number[] } export class MoralisService implements Omit, API, AddressSubscriptionClient { @@ -61,6 +66,8 @@ export class MoralisService implements Omit, API, AddressSub private readonly rpcUrl: string private readonly explorerApiUrl: URL private readonly minPriorityFee: string + private readonly baseFeeMultiplier: number[] + private readonly gasPriceMultiplier: number[] private secret?: string private streamId?: string @@ -82,6 +89,8 @@ export class MoralisService implements Omit, API, AddressSub this.rpcUrl = args.rpcUrl this.explorerApiUrl = args.explorerApiUrl ?? new URL('about:blank') this.minPriorityFee = args.minPriorityFee ?? '0' + this.baseFeeMultiplier = args.baseFeeMultiplier ?? DEFAULT_BASE_FEE_MULTIPLIER + this.gasPriceMultiplier = args.gasPriceMultiplier ?? DEFAULT_GAS_PRICE_MULTIPLIER void Moralis.start({ evmApiBaseUrl: INDEXER_URL, apiKey: INDEXER_API_KEY }) void Moralis.Streams.setSettings({ region: 'us-east-1' }) @@ -386,10 +395,11 @@ export class MoralisService implements Omit, API, AddressSub let maxPriorityFeePerGas = BigNumber(this.minPriorityFee) const [slow, average, fast] = REWARD_PERCENTILES.map((_, index): Fees => { maxPriorityFeePerGas = BigNumber.max(estimatePriorityFee(index), maxPriorityFeePerGas) - const maxFeePerGas = baseFeePerGas.times(BASE_FEE_MULTIPLIER).plus(maxPriorityFeePerGas) + const maxFeePerGas = baseFeePerGas.times(this.baseFeeMultiplier[index]).plus(maxPriorityFeePerGas) + const gasPrice = baseFeePerGas.times(this.gasPriceMultiplier[index]).plus(maxPriorityFeePerGas) return { - gasPrice: baseFeePerGas.plus(maxPriorityFeePerGas).toFixed(0), + gasPrice: gasPrice.toFixed(0), maxFeePerGas: maxFeePerGas.toFixed(0), maxPriorityFeePerGas: maxPriorityFeePerGas.toFixed(0), } From 07940ac18f1371355003b8af10c424d20db80c5f Mon Sep 17 00:00:00 2001 From: kaladinlight <35275952+kaladinlight@users.noreply.github.com> Date: Fri, 10 Jul 2026 12:06:33 -0600 Subject: [PATCH 2/2] fix(evm): enforce fee multiplier arity with tuple types Type baseFeeMultiplier and gasPriceMultiplier as [number, number, number] so a wrong-length array is caught at compile time rather than silently producing NaN fees when getGasFees indexes a missing tier. Addresses CodeRabbit review feedback. Co-Authored-By: Claude Opus 4.8 --- node/coinstacks/common/api/src/evm/moralisService.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/node/coinstacks/common/api/src/evm/moralisService.ts b/node/coinstacks/common/api/src/evm/moralisService.ts index f8025955d..0b6d65b1a 100644 --- a/node/coinstacks/common/api/src/evm/moralisService.ts +++ b/node/coinstacks/common/api/src/evm/moralisService.ts @@ -30,10 +30,10 @@ const FEE_HISTORY_BLOCK_COUNT = 100 const REWARD_PERCENTILES = [50, 70, 90] // default per-tier multipliers [slow, average, fast] for base fee -const DEFAULT_BASE_FEE_MULTIPLIER = [2, 2, 2] +const DEFAULT_BASE_FEE_MULTIPLIER: [number, number, number] = [2, 2, 2] // default per-tier multipliers [slow, average, fast] for gas price -const DEFAULT_GAS_PRICE_MULTIPLIER = [1, 1, 1] +const DEFAULT_GAS_PRICE_MULTIPLIER: [number, number, number] = [1, 1, 1] // drop priority fees above the median by this factor to keep outliers (mev/overpayers) from skewing estimates const OUTLIER_THRESHOLD_MULTIPLIER = 10 @@ -55,8 +55,8 @@ export interface MoralisServiceArgs { rpcUrl: string explorerApiUrl?: URL minPriorityFee?: string - baseFeeMultiplier?: number[] - gasPriceMultiplier?: number[] + baseFeeMultiplier?: [number, number, number] + gasPriceMultiplier?: [number, number, number] } export class MoralisService implements Omit, API, AddressSubscriptionClient { @@ -66,8 +66,8 @@ export class MoralisService implements Omit, API, AddressSub private readonly rpcUrl: string private readonly explorerApiUrl: URL private readonly minPriorityFee: string - private readonly baseFeeMultiplier: number[] - private readonly gasPriceMultiplier: number[] + private readonly baseFeeMultiplier: [number, number, number] + private readonly gasPriceMultiplier: [number, number, number] private secret?: string private streamId?: string