ethers.js icon indicating copy to clipboard operation
ethers.js copied to clipboard

Polygon transactions are stuck/transaction underpriced error

Open moltam89 opened this issue 2 years ago • 25 comments

Ethers Version

5.6.1

Search Terms

polygon maxPriorityFeePerGas maxFeePerGas

Describe the Problem

There is a harcoded (1.5 gwei) maxPriorityFeePerGas (and maxFeePerGas) in index.ts.

This value is used in populateTransaction.

In case of Polygon, this will either result in a transaction stuck in the mempool, or in case og e.g an Alchemy endpoint, "transaction underpriced" error.

Code Snippet

signer.populateTransaction(txParams)
signer.sendTransaction(txParams)

Where txParams don't contain maxPriorityFeePerGas/maxFeePerGas, and is a type 2 transaction.
(Legacy transactions pass as they use gasPrice only)

Contract ABI

No response

Errors

processing response error (body="{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32000,\"message\":\"transaction underpriced\"},\"id\":64}", error={"code":-32000}, requestBody="{\"method\":\"eth_sendRawTransaction\",\"params\":[\"0x02f873818981c98459682f0084596898e882520894c54c244200d657650087455869f1ad168537d3b387038d7ea4c6800080c080a07cb6e05c60a2cb7ffc83349bc52ade79afaf9fdb911c64d57aed421caa1ecbcfa05f30023a4d21dd2eab6ea619c8dbb4820ce40c71841baacf8e82cbde7e87602a\"],\"id\":64,\"jsonrpc\":\"2.0\"}", requestMethod="POST",

Environment

No response

Environment (Other)

No response

moltam89 avatar Mar 19 '22 15:03 moltam89

I have the exact same issue for the exact same use. However I am using "ethers": "^5.4.6",. so the value for me is BigNumber.from("2500000000");

For reference: https://medium.com/stakingbits/polygon-minimum-gas-fee-is-now-30-gwei-to-curb-spam-8bd4313c83a2

Benjythebee avatar Mar 20 '22 23:03 Benjythebee

My current workaround:

// get max fees from gas station
let maxFeePerGas = ethers.BigNumber.from(40000000000) // fallback to 40 gwei
let maxPriorityFeePerGas = ethers.BigNumber.from(40000000000) // fallback to 40 gwei
try {
    const { data } = await axios({
        method: 'get',
        url: isProd
        ? 'https://gasstation-mainnet.matic.network/v2'
        : 'https://gasstation-mumbai.matic.today/v2',
    })
    maxFeePerGas = ethers.utils.parseUnits(
        Math.ceil(data.fast.maxFee) + '',
        'gwei'
    )
    maxPriorityFeePerGas = ethers.utils.parseUnits(
        Math.ceil(data.fast.maxPriorityFee) + '',
        'gwei'
    )
} catch {
    // ignore
}

// send tx with custom gas
const tx = await contract.multicall(calldata, {
    maxFeePerGas,
    maxPriorityFeePerGas,
})

robertu7 avatar Mar 21 '22 02:03 robertu7

And my current workaround:

  const feeData = await maticProvider.getFeeData()

  // Start transaction: populate the transaction
  let populatedTransaction
  try {
    populatedTransaction = await contract.populateTransaction.mint(wallet, arg, { gasPrice: feeData.gasPrice })
  } catch (e) {
    return
  }

I removed gasLimit from the options as it never worked;

Benjythebee avatar Mar 21 '22 03:03 Benjythebee

I have the same problem with the same version (5.6.1)

herzaso avatar Mar 31 '22 14:03 herzaso

@Benjythebee seems like you could then just call provider.getGasPrice() directly

ihorbond avatar Apr 08 '22 22:04 ihorbond

@robertu7 what is this Matic.network site? is that officially supported by Polygon / how reliable is it? thanks for the code!

I've also had this issue with tx getting stuck. Hoping this will fix it.

cupOJoseph avatar Apr 12 '22 21:04 cupOJoseph

@jschiarizzi It's official: https://docs.polygon.technology/docs/develop/tools/polygon-gas-station

robertu7 avatar Apr 13 '22 01:04 robertu7

For your interest @robertu7 I pulled what you did into a new wallet subclass that one day may or may not get sent back into the main lib. It's tuned for our use cases with Polygon but I can't see why it can't be generalized with the help of some interfaces:

import { TransactionRequest } from "@ethersproject/abstract-provider"
import { Provider } from "@ethersproject/abstract-provider"
import { ExternallyOwnedAccount } from "@ethersproject/abstract-signer"
import { BytesLike } from "@ethersproject/bytes"
import { Deferrable } from "@ethersproject/properties"
import { SigningKey } from "@ethersproject/signing-key"
import fetch from "cross-fetch"
import { Wallet as EthersWallet, ethers } from "ethers"

type GasData = {
  fast: {
    maxPriorityFee: number
    maxFee: number
  }
}

const DEFAULT_GAS_DATA: GasData = {
  fast: {
    maxPriorityFee: 40,
    maxFee: 40,
  },
}

class AutomaticGasWallet extends EthersWallet {
  gasStationUrl: string

  constructor(
    privateKey: BytesLike | ExternallyOwnedAccount | SigningKey,
    provider: Provider,
    gasStationUrl: string
  ) {
    super(privateKey, provider)
    this.gasStationUrl = gasStationUrl
  }

  async populateTransaction(
    transaction: Deferrable<TransactionRequest>
  ): Promise<TransactionRequest> {
    const tx = await super.populateTransaction(transaction)

    const data = await this.getGasData()
    const maxFee = ethers.utils.parseUnits(
      Math.ceil(data.fast.maxFee).toString(),
      "gwei"
    )
    const maxPriorityFee = ethers.utils.parseUnits(
      Math.ceil(data.fast.maxPriorityFee).toString(),
      "gwei"
    )

    tx.maxFeePerGas = maxFee
    tx.maxPriorityFeePerGas = maxPriorityFee

    return tx
  }

  async getGasData(): Promise<GasData> {
    if (!this.gasStationUrl) {
      return DEFAULT_GAS_DATA
    }

    try {
      const response = await fetch(this.gasStationUrl)
      const data = (await response.json()) as GasData
      return data
    } catch (e) {
      logger.error(
        `Could not fetch gas data from ${this.gasStationUrl}: ${e.toString()}`
      )
      return DEFAULT_GAS_DATA
    }
  }
}

export default AutomaticGasWallet

impguard avatar May 04 '22 05:05 impguard

Currently, we should do like @robertu7 's way. provider. getFeeData() return only maxPriorityFeePerGas: 1.5gwei

https://github.com/ethers-io/ethers.js/blob/master/packages/abstract-provider/src.ts/index.ts#L250

tomonari-t avatar May 06 '22 10:05 tomonari-t

@Benjythebee seems like you could then just call provider.getGasPrice() directly

My solution above https://github.com/ethers-io/ethers.js/issues/2828#issuecomment-1073458126 seem to be unreliable now. The transaction fails even before asking the user for a transaction most of the time. With error "err: max fee per gas less than block base fee..."

metamask 10.14.1 ethers ^5.4.6

Benjythebee avatar May 10 '22 00:05 Benjythebee

I just stumbled across this after pulling my hair out this past week with transaction errors UNDERPRICED, REPLACEMENT_UNDERPRICED and "tx fee exceeds the configured cap" on maticvigil, polygon-rpc and alchemy. Various multiples of the getFeeData results were used, all with low success rates. I will try @robertu7's workaround later tonight to see if there any improvement - i sure hope so. A clear and concise example in the ethers docs would be TRULY helpful for all of us trying to use Polygon. Note that Mumbai gave no indication that this would be an issue in production.

vicwtang avatar May 16 '22 21:05 vicwtang

same issue here trasactions are pending for ever on mumbai polygon

GolfredoPerezFernandez avatar May 17 '22 02:05 GolfredoPerezFernandez

same issue here on polygon mainnet

clemsos avatar May 17 '22 16:05 clemsos

Interesting. I never had any issues with Mumbai. It was only after going live that the majority of transactions were rejected. And those that went thru the 2nd week of May had wildly fluctuating prices such that same tx went from cents to up to $3. So currently, I am very jaded with Polygon. Anyways, didn't get to trying the work-around. Doing it as shortly, will advise on my experience...

vicwtang avatar May 17 '22 17:05 vicwtang

Thanks for the post @clemsos. I'm curious - is the data from the polygon gas station significantly different from the results from the getFeedata call?

vicwtang avatar May 17 '22 17:05 vicwtang

Here's my results trying @clemsos code: results from getFeeData: { maxFeePerGas: '1500000082' } { maxPriorityFeePerGas: '1500000000' } results from @clemsos: const resp = await fetch('https://gasstation-mainnet.matic.network/v2') const data = await resp.json()

				maxFeePerGas = ethers.utils.parseUnits(
					`${Math.ceil(data.fast.maxFee)}`,
					'gwei'
				)
				maxPriorityFeePerGas = ethers.utils.parseUnits(
					`${Math.ceil(data.fast.maxPriorityFee)}`,
					'gwei'
				)

gasstation { maxFeePerGas: '48000000000' } { maxPriorityFeePerGas: '48000000000' } NOTE: gasstation value is 32x greater than getFeedata value, yet when I was submitting 3x getFeeData.maxFeePerGas values last week, i hit over cap errors

**************** calling secureToken **************** batchMint failed secureData Error: replacement fee too low [ See: https://links.ethers.org/v5-errors-REPLACEMENT_UNDERPRICED ]

reviewing @impguard / @robertu7 next... And that is pretty much the same code... hmm my method call with the gas values: await contract.connect(signer).secureToken( bnTokenId, bnCollectionId, bnChain, meta.metaCid, meta.properties.docroot, '', meta.exchange, owner, { maxFeePerGas, maxPriorityFeePerGas } )

so still dead in water...

vicwtang avatar May 17 '22 17:05 vicwtang

and with @Benjythebee 's use of feeData.gasPrice: gasPrice 37172324995 await contract.connect(signer).secureToken( bnTokenId, bnCollectionId, bnChain, meta.metaCid, meta.properties.docroot, '', meta.exchange, owner, { gasPrice: feeData.gasPrice })

**************** calling secureToken **************** batchMint failed secureData Error: replacement fee too low [ See: https://links.ethers.org/v5-errors-REPLACEMENT_UNDERPRICED ] (error={"reason":"processing response error","code":"SERVER_ERROR","body":"{"jsonrpc":"2.0","id":56,"error":{"code":-32000,"message":"replacement transaction underpriced"}}","error":{"code":-32000},"requestBody":"{"method":"eth_sendRawTransaction"

requestMethod: 'POST',
url: 'https://rpc-mainnet.maticvigil.com/v1/xxxxx'

}, method: 'sendTransaction', transaction: { type: 2, chainId: 137, nonce: 3017, maxPriorityFeePerGas: BigNumber { _hex: '0x08a7a4aa83', _isBigNumber: true }, maxFeePerGas: BigNumber { _hex: '0x08a7a4aa83', _isBigNumber: true }, gasPrice: null, gasLimit: BigNumber { _hex: '0x099b26', _isBigNumber: true }, to: '0xxxxxx', value: BigNumber { _hex: '0x00', _isBigNumber: true },

INTERESTING - gasPrice shown as NULL in the actual transaction despite being submitted in call...

vicwtang avatar May 17 '22 18:05 vicwtang

Am I doing something wrong? I imagine that these workarounds are working for those who posted these solutions. Is it the rpc provider? What rpc provider is used for the gasstation solution versus the gasprice one? I should note: I am making these calls from the BACKEND. I guess I'll check in the DAPP if this helps on that front...

Dang it. Just realized its REPLACEMENT_UNDERPRICED, meaning a previous TX has stuck my new TX. But doesn't the NEW tx with the CORRECT fee override the stuck on and push it through? Trying to figure out how to cancel these pending now. Maybe the code above will work once that's done. I have to say - this has been an awful experience.

vicwtang avatar May 17 '22 18:05 vicwtang

Update. SO the nonce was pending with a previous accepted tx. So, the maxFeePerGas value from the gasstation is getting accepted. However, if I wait for the transaction to confirm, it never returns. If i submit another TX, it gets a REPLACEMENT_UNDERPRICED error. I then have to go to metamask, and send a 0Matic transaction with the SAME NONCE at the fast price to clear the pending tx. GREAT. YAY.

So as a part of all this pricing business, I have TX with an accepted price and a hash, that doesnt confirm within any reasonable timeframe, hence still blocking all other TX. Looking up the TX in polygonscan - i get this: Status: Pending This txn hash was found in our secondary node and should be picked up by our indexer in a short while. for a tx made 5 minutes ago.

trying a different RPC to see if there's any improvement.

I'm updating this info so maybe it helps others? But I sure could use some feedback from the more experienced Polygoner's out there who have figured this out. If there is such a state...

Meanwhile, more 0 matic transfers to reset Tx.

Update: after maybe 15min? explorer shows this: Sorry, We are unable to locate this TxnHash Sooo... the accepted fee wasn't good enough???

Update #2: so it seems now we're cooking with gas :) So a number of issues:

  1. getFeeData is apparently WAY UNDERPRICED compared to the gasstation values - USE the gasstation values for maxFeePerGas and maxPriorityFeePerGas (the tip is the same as the limit??? - whatever)
  2. I changed my RPC provider from maticvigil to polygon-rpc. It apparently was the difference between the await for confirmation timing out and the tx should as pending on a secondary node TO ACTUALLY being confirmed within 30s.
  3. If you see a REPLACEMENT_UNDERPRICED - that means you ALREADY have a pending TX and should ensure this is cancelled or processed before you create a NEW ONE. Your current NODE# is stuck and you will need to unstick it. Doing a zero matic transfer on Metamask for that node # at Aggressive gas seems to take care of that within 30s. And what do you know - Metamask is using polygon-rpc.

So... my batcher is finally chugging await and logging successful tx now. YAY but what a week from HELL. I cannot say I would recommend Polygon, or the support that is available to help. Thanks to the community here for the gas pricing info

NEXT STOP: anyone have any experience setting default gas for the provider, versus per tx. when using an sdk - direct access to setting the gas for a tx is sometimes not available - ie Rarible API or Opensea perhaps. I'm told Web3 has Web3Ethereum that can be supplied with gas option - does this work for maxFeeForGas? Is there an ETHERS equivalent?

vicwtang avatar May 17 '22 20:05 vicwtang

Just completed minting 1000 nfts yesterday on polygon RELIABLY - finally. Here's my code to get around this very frustrating gas pricing issue on Polygon/Mumbai:

	const gasEstimated = await contract.estimateGas.yourMethod(...methodParams)

	const gas = await calcGas('fast', chain) // this is @robertu7 's code - thanks!

	let response = await (
		await contract.connect(signer).yourMethod(...methodParams,
			{ gasLimit: mulDiv(gasEstimated, 110, 100), 
			maxFeePerGas: gas.maxFeePerGas, 
			maxPriorityFeePerGas: gas.maxPriorityFeePerGas }
		)
	).wait()

it irks me that the maxPriorityFeePerGas is so high. I know I saw random transactions I had with default gas paying 1.5gwei but I havent been able to re-achieve this reliably.

Note that I had to include a gasLimit at a 10% premium for improved reliability. the ethers getFeeData did NOT work. it was always underpriced.

But the code is demonstrably reliable in sending the tx and getting it included in the block within a few seconds each time. I added a check to retry the tx later if the maxFeePerGas exceeds 50gwei in production.

Hope that helps someone.

vicwtang avatar May 24 '22 06:05 vicwtang

Just completed minting 1000 nfts yesterday on polygon RELIABLY - finally. Here's my code to get around this very frustrating gas pricing issue on Polygon/Mumbai:

	const gasEstimated = await contract.estimateGas.yourMethod(...methodParams)

	const gas = await calcGas('fast', chain) // this is @robertu7 's code - thanks!

	let response = await (
		await contract.connect(signer).yourMethod(...methodParams,
			{ gasLimit: mulDiv(gasEstimated, 110, 100), 
			maxFeePerGas: gas.maxFeePerGas, 
			maxPriorityFeePerGas: gas.maxPriorityFeePerGas }
		)
	).wait()

it irks me that the maxPriorityFeePerGas is so high. I know I saw random transactions I had with default gas paying 1.5gwei but I havent been able to re-achieve this reliably.

Note that I had to include a gasLimit at a 10% premium for improved reliability. the ethers getFeeData did NOT work. it was always underpriced.

But the code is demonstrably reliable in sending the tx and getting it included in the block within a few seconds each time. I added a check to retry the tx later if the maxFeePerGas exceeds 50gwei in production.

Hope that helps someone.

What's the mulDiv function do? Can you kindly put out the complete code? I am trying to make it work on polygon too. But the transaction is always stuck at pending.

y0unghe avatar Jul 28 '22 05:07 y0unghe

adds a 10% premium to the estimated fee

On Wed, Jul 27, 2022 at 10:32 PM Yong He @.***> wrote:

Just completed minting 1000 nfts yesterday on polygon RELIABLY - finally. Here's my code to get around this very frustrating gas pricing issue on Polygon/Mumbai:

const gasEstimated = await contract.estimateGas.yourMethod(...methodParams)

const gas = await calcGas('fast', chain) // this is @robertu7 's code - thanks!

let response = await ( await contract.connect(signer).yourMethod(...methodParams, { gasLimit: mulDiv(gasEstimated, 110, 100), maxFeePerGas: gas.maxFeePerGas, maxPriorityFeePerGas: gas.maxPriorityFeePerGas } ) ).wait()

it irks me that the maxPriorityFeePerGas is so high. I know I saw random transactions I had with default gas paying 1.5gwei but I havent been able to re-achieve this reliably.

Note that I had to include a gasLimit at a 10% premium for improved reliability. the ethers getFeeData did NOT work. it was always underpriced.

But the code is demonstrably reliable in sending the tx and getting it included in the block within a few seconds each time. I added a check to retry the tx later if the maxFeePerGas exceeds 50gwei in production.

Hope that helps someone.

What's the mulDiv function do?

— Reply to this email directly, view it on GitHub https://github.com/ethers-io/ethers.js/issues/2828#issuecomment-1197682780, or unsubscribe https://github.com/notifications/unsubscribe-auth/AQKOKAZ4PVRIJAFRDVVOSDTVWILNPANCNFSM5REEG7RQ . You are receiving this because you commented.Message ID: <ethers-io/ethers .@.***>

vicwtang avatar Jul 28 '22 17:07 vicwtang

What's the mulDiv function do? Can you kindly put out the complete code? I am trying to make it work on polygon too. But the transaction is always stuck at pending.

Yeah I was reading this thread like "oh yay a solution" and then "wtf is mulDiv?"

After some significant googling, I found that it's an old timey method that just means (a * b) / c. He's using it to calculate 10% higher than the given number.

But after getting it to work, I found that I didn't need to go 10% higher. I included the 10% higher code in a comment, in case anyone needs it.

I adapted and cleaned up the code, and here is what I ended up with.

function parse(data) {
    return ethers.utils.parseUnits(Math.ceil(data) + '', 'gwei');
}

async function calcGas(gasEstimated) {
    let gas = {
        gasLimit: gasEstimated, //.mul(110).div(100)
        maxFeePerGas: ethers.BigNumber.from(40000000000),
        maxPriorityFeePerGas: ethers.BigNumber.from(40000000000)
    };
    try {
        const {data} = await axios({
            method: 'get',
            url: 'https://gasstation-mainnet.matic.network/v2'
        });
        gas.maxFeePerGas = parse(data.fast.maxFee);
        gas.maxPriorityFeePerGas = parse(data.fast.maxPriorityFee);
    } catch (error) {

    }
    return gas;
};

const gasEstimated = await contract.estimateGas.yourMethod();
const gas = await calcGas(gasEstimated);
const tx = await contract.yourMethod(gas);
await tx.wait();

Provided I didn't make any mistakes in converting to ES, this should be a full working snippet. If not, let me know.

Honestly I don't know how I would have ever solved this if not for stumbling upon this particular thread. So thanks for your efforts, and your help.

SethTurin avatar Jul 30 '22 19:07 SethTurin

Here is another example that works. Not the best practice to up the gas price like this, but it definitely solves the problem.

    let txHash: any
    const _gasPrice = await web3.eth.getGasPrice()
    poolContract.methods
      .doSomething(parseUnits(amount.toString(), tokenObject.decimals))
      .send({
        from: props.accounts,
        gasLimit: '20000000',
        gasPrice: Number(_gasPrice * 1.5)
          .toFixed()
          .toString(),
          gas: '20000000',
      })
      .once('transactionHash', (hash: any) => (txHash = hash))
      .then(async (res: any) => {
          console.log('success', res)
          setLoading(false)
      })
      .catch((e: any) => {
        const message = e?.message
        console.log(message)
        setLoading(false)

        if (e.toString().includes('not mined within')) {
          const handle = setInterval(() => {
            web3.eth
              .getTransactionReceipt(txHash)
              .then((resp: { blockNumber: number } | null) => {
                if (resp != null && resp.blockNumber > 0) {
                  clearInterval(handle)
                  alert('network timeout')
                }
              })
          })
        }
      })

blocksurance-dao avatar Aug 22 '22 13:08 blocksurance-dao

I've opened up a separate issue ticket around this - I think we've identified the cause.

Here's the explanation, assuming we're right: there's a minimum 30 gwei priority fee on Polygon, the hardcoded 1.5 gwei priority in Ethers is likely where estimations are underpricing. If this is correct, if you are implementing a workaround, all you should need to do is raise the priority fee to 30 gwei.

If you have additional details, feel free to add there (or here).

wschwab avatar Sep 19 '22 14:09 wschwab

And my current workaround:

  const feeData = await maticProvider.getFeeData()

  // Start transaction: populate the transaction
  let populatedTransaction
  try {
    populatedTransaction = await contract.populateTransaction.mint(wallet, arg, { gasPrice: feeData.gasPrice })
  } catch (e) {
    return
  }

I removed gasLimit from the options as it never worked;

Yes Exactly this issue got resolved by this

chiragjoshi28 avatar Sep 27 '22 04:09 chiragjoshi28

@SethTurin

Thanks for that cleaned code. I have been using it for the past week. But since 2 days ago, my transactions are not going through . await tx.wait();
Never resolve anymore . Transaction is not processed either and it stays like that forever.

Anyone has an idea about what could be the problem ? thanks

bengivre avatar Oct 15 '22 22:10 bengivre

I'm so sick of this constantly changing. What worked 5 months ago didn't work a month ago and what worked 5 days ago doesn't work today. What is changing?

Ok so as much as I can tell maxFeePerGas and maxPriorityFeePerGas are estimated completely wrong by the ethers on mainnet polygon. gasPrice gets closer, but is still dramatically underpriced. So now I'm just back to using gasPrice again.

nickjuntilla avatar Oct 18 '22 04:10 nickjuntilla

Fully agree. What worked months ago just broke last weekend amidst another network congestion. Had to clear the wallet several times due to underpriced transactions blocking others. Looking now for an automated solution to clear stuck wallets :P

On Mon, Oct 17, 2022 at 9:07 PM Nicholas Juntilla @.***> wrote:

I'm so sick of this constantly changing. What worked 5 months ago didn't work a month ago and what worked 5 days ago doesn't work today. What is changing?

— Reply to this email directly, view it on GitHub https://github.com/ethers-io/ethers.js/issues/2828#issuecomment-1281787172, or unsubscribe https://github.com/notifications/unsubscribe-auth/AQKOKA3ZIINK3LLLKNTHRE3WDYO6TANCNFSM5REEG7RQ . You are receiving this because you commented.Message ID: <ethers-io/ethers .@.***>

vicwtang avatar Oct 18 '22 18:10 vicwtang

Fully agree. What worked months ago just broke last weekend amidst another network congestion. Had to clear the wallet several times due to underpriced transactions blocking others. Looking now for an automated solution to clear stuck wallets :P On Mon, Oct 17, 2022 at 9:07 PM Nicholas Juntilla @.> wrote: I'm so sick of this constantly changing. What worked 5 months ago didn't work a month ago and what worked 5 days ago doesn't work today. What is changing? — Reply to this email directly, view it on GitHub <#2828 (comment)>, or unsubscribe https://github.com/notifications/unsubscribe-auth/AQKOKA3ZIINK3LLLKNTHRE3WDYO6TANCNFSM5REEG7RQ . You are receiving this because you commented.Message ID: <ethers-io/ethers .@.>

That's what my company Ownerfy does is queue transactions, but we don't let people import their own wallets because of liability.

nickjuntilla avatar Oct 18 '22 19:10 nickjuntilla