Retrying an EIP 1559 transaction

This tutorial will walk through how to retry Ethereum transactions using EIP-1559 methods.

When you submit a transaction with a gas price that is too low to be included in the block, the transaction can be pending for a very long time. You might then want to update the transaction's gas price in order to get it mined. This concept becomes a bit more complex when it comes to EIP 1559.

Transactions before and after EIP 1559

Legacy transactions (pre-London fork) only needed a gasPrice field in order to place a bid on gas price. Post-London, with EIP 1559, transactions can include one or both of maxPriorityFeePerGas (a tip to the miner) or maxFeePerGas (a total fee including a tip to the miner). For the purposes of this tutorial, we will assume you have submitted a transaction with one or both of these fields at least once before.

As with legacy transactions, the way to update an EIP 1559 transaction is to make a new transaction call with the same nonce as your pending transaction, but with an updated gas price. In legacy transactions, you only needed to submit an updated gasPrice, which needed to be at least 10% higher than the pending transaction's price in order for miners to reconsider the transaction. The purpose is to convince miners that you are willing to pay more.

Post-London you have to submit an updated maxPriorityFeePerGas, or "tip". This is the amount that will go to the miner. Just as in legacy transactions, the tip needs to increase by at least 10% to be re-considered.

This new setup is not perfect. For example, if you are using a fee estimator like Eth Gas Station, then you are likely submitting transactions with only the maxFeePerGas field. This means that you are not setting a tip yourself but letting the system fill in a default value for you. This can be a problem because to update the transaction you need to submit a new maxPriorityFeePerGas.

In this example, you will need to fetch your pending transaction, check the maxPriorityFeePerGas field, and then submit a new transaction with the same nonce and an increased tip. You will also need to increase your maxFeePerGas by the same amount.

Let's look at updating a transaction that contained one or both of the EIP 1559 fields.

Submit the initial transaction with a low tip amount

To start, we will submit a transaction that is destined to fail. The code below is pseudocode, we assume that you have your own working code that you need to update.

import { Network, Alchemy, Wallet, Utils } from "alchemy-sdk";
import dotenv from "dotenv";
dotenv.config();

const { API_KEY, PRIVATE_KEY } = process.env;
const settings = {
  apiKey: API_KEY,
  network: Network.ETH_GOERLI, // Replace with your network.
};

const alchemy = new Alchemy(settings);
const wallet = new Wallet(PRIVATE_KEY, alchemy);

const transaction = {
  to: "0xa238b6008Bc2FBd9E386A5d4784511980cE504Cd",
  nonce: await alchemy.core.getTransactionCount(wallet.getAddress()),
  value: Utils.parseEther("0.001"),
  maxPriorityFeePerGas: Utils.parseUnits("15", "wei"),
  type: 2,
  chainId: 5, // Corresponds to ETH_GOERLI
};

const sentTx = await wallet.sendTransaction(transaction);
console.log(sentTx);

This transaction is destined to fail because we've set maxPriorityFeePerGas to 15. It's really really unlikely a miner would accept a tip as low as 15 wei.

Update the priority fee

Since we've set the tip explicitly using maxPriorityFeePerGas it's simple for us to update it. To update the tip you have to submit a transaction with the same nonce, just as with legacy transactions.

First, let's see what a failed tip update looks like. Recall that you need to increase the tip by at least 10% for the update to be considered.

Here is an attempt to lower the tip, which obviously won't work.

const transaction = {
  to: "0xa238b6008Bc2FBd9E386A5d4784511980cE504Cd",
  nonce: await alchemy.core.getTransactionCount(wallet.getAddress()),
  value: Utils.parseEther("0.001"),
  maxPriorityFeePerGas: Utils.parseUnits("14", "wei"),
  type: 2,
  chainId: 5, // Corresponds to ETH_GOERLI
};

Which results in

Error: Returned error: replacement transaction underpriced

This is obvious because we lowered the tip from an already low amount. Let's see what happens if we try to update it by less than 10%.

const transaction = {
  to: "0xa238b6008Bc2FBd9E386A5d4784511980cE504Cd",
  nonce: await alchemy.core.getTransactionCount(wallet.getAddress()),
  value: Utils.parseEther("0.001"),
  maxPriorityFeePerGas: Utils.parseUnits("16", "wei"),
  type: 2,
  chainId: 5, // Corresponds to ETH_GOERLI
};

Which has the same error response

Error: Returned error: replacement transaction underpriced

And now a successful update:

const transaction = {
  to: "0xa238b6008Bc2FBd9E386A5d4784511980cE504Cd",
  nonce: await alchemy.core.getTransactionCount(wallet.getAddress()),
  value: Utils.parseEther("0.001"),
  maxPriorityFeePerGas: Utils.parseUnits("18", "wei"),
  type: 2,
  chainId: 5, // Corresponds to ETH_GOERLI
};

Nice! That works.

Retrying a transaction - No priority fee

In the previous section, we basically did the same thing you would do to update a legacy transaction. But if you used a gas estimator to submit your initial transaction then it is likely that you only submitted the maxFeePerGas field with no explicit maxPriorityFeePerGas. In this example, updating the transaction is slightly more complicated.

When you submit a transaction with only the maxFeePerGas field, a default value is filled in for the maxPriorityFeePerGas field. The default value depends on what node provider you are using.

When you submit an updated transaction with a new maxFeePerGas, your node provider will likely not fill in an updated priority fee. That means that no matter how much you bump the maxFeePerGas, your updated transaction will continue to fail. To make things more complicated, when you are submitting a transaction update it is likely that the baseFeePerGas also changed.

Luckily, the math here turns out to be very simple. Recall that:

maxFeePerGas = baseFeePerGas + maxPriorityFeePerGas;

To submit the update, first retrieve an updated maxFeePerGas estimate from Eth Gas Station (or another estimator). Now that we know the new total, we just need to know the new baseFeePerGas and then we can calculate the new tip to submit.

If you're using the alchemy web3 client, then you can fetch the base fee from the pending block like so:

const pendingBlock = await web3.eth.getBlock("pending");
pendingBlock.baseFeePerGas;

Then subtract the base fee from the max fee and we're good to go!

maxPriorityFeePerGas = maxFeePerGas - baseFeePerGas

Submit the transaction again with the same nonce and the updated maxFeePerGas and maxPriorityFeePerGas fields, and you will successfully retry the transaction!

If you're interested in learning more, or have feedback, suggestions, or questions, reach out to us in Discord! Get started with Alchemy today by signing up for free.