DEV Community

metadevdigital
metadevdigital

Posted on

Understanding EIP-1559: The Fee Mechanism That Changed Ethereum

Understanding EIP-1559: The Fee Mechanism That Changed Ethereum

Why I'm Writing This

I've spent the last four years watching developers and traders get absolutely wrecked by misunderstanding Ethereum's fee market. Before EIP-1559, you'd set a gas price, cross your fingers, and hope. After the London fork in August 2021? The rules changed. I've audited contracts that didn't account for dynamic base fees. I've seen MEV bots exploiting transaction ordering because the developer thought they understood gas mechanics. This isn't optional knowledge anymore—it's foundational.

The honest truth: EIP-1559 is elegant but unintuitive. Most developers I talk to still don't fully grasp it.


What EIP-1559 Actually Does (Not the Whitepaper Version)

EIP-1559 fundamentally rewrote how transaction fees work on Ethereum. Before it, you submitted a gas price, miners took all of it as profit, and the market was chaotic. After EIP-1559? The fee market split into two separate components: a base fee that burns and a priority fee (your tip to the validator).

The base fee adjusts every block based on network utilization. If the last block was over 50% full, the base fee increases by roughly 12.5%. If it was under 50% full, it decreases. This creates a feedback loop. Genius design, honestly.

// This is how you'd calculate the actual transaction cost in your smart contract
function estimateTransactionCost(
    uint256 baseFee,
    uint256 priorityFee,
    uint256 gasUsed
) public pure returns (uint256) {
    uint256 totalGasPrice = baseFee + priorityFee;
    return totalGasPrice * gasUsed;
}

// Before EIP-1559, you'd just do: gasPrice * gasUsed
// Miners got everything. Developers didn't care about burning.
Enter fullscreen mode Exit fullscreen mode

The practical difference? Transactions became more predictable. You can now estimate fees based on network congestion trends rather than hoping miners won't raise prices.


The Base Fee Mechanism

Let me be direct: the base fee is automatic. You can't set it. The protocol sets it for you.

Every block, the network checks whether the previous block was "full" compared to a target of 15 million gas. If it was fuller, the base fee goes up. If emptier, it goes down. The adjustment follows this formula:

new_base_fee = old_base_fee * (1 + (excess_gas / target_gas) * base_fee_max_change_rate)
Enter fullscreen mode Exit fullscreen mode

Where base_fee_max_change_rate is 1/8 (12.5%). This is critical: even during absolute network chaos, the base fee can only jump 12.5% per block.

// Simulating base fee calculation across blocks
const MAX_CHANGE_RATE = 1 / 8; // 12.5%
const TARGET_GAS = 15_000_000;

function calculateNextBaseFee(currentBaseFee, gasUsedInLastBlock) {
  const excessGas = Math.max(0, gasUsedInLastBlock - TARGET_GAS);

  const baseFeeChange = Math.floor(
    (currentBaseFee * excessGas) / TARGET_GAS / 8
  );

  return currentBaseFee + baseFeeChange;
}

// Example: Network at max capacity
const baseFeeBlock1 = 50_000_000; // in wei (50 gwei)
const fullBlock = 30_000_000; // way over 15M target

const baseFeeBlock2 = calculateNextBaseFee(baseFeeBlock1, fullBlock);
console.log(`Block 2 base fee: ${baseFeeBlock2 / 1e9} gwei`);
// Output: 56.25 gwei (12.5% increase)
Enter fullscreen mode Exit fullscreen mode

Why does this matter? Because your transaction cost is now somewhat predictable. During mega-congestion like an NFT drop or a DEX craze, you know the base fee won't spike more than 12.5% per block. Plan accordingly.


Priority Fees: The Tip Jar

The priority fee (also called the "miner tip" or "validator tip") is the only part you control directly. This is your bid for inclusion priority.

The total gas price your transaction pays is:

totalGasPrice = baseFee + priorityFee
Enter fullscreen mode Exit fullscreen mode

Both get deducted from your account. The base fee burns. The priority fee goes to the validator.

Here's the confusion I see constantly: developers think they can set a low priority fee and still get included quickly. Wrong. If the network is congested and 1000 other transactions are offering 5 gwei priority fee, and you offer 0.1 gwei, you're waiting.

// This is what happens when your transaction gets included
contract FeeExample {
    function executeTransaction() external {
        // Your transaction costs: (baseFee + priorityFee) * gas_used
        // Example:
        // baseFee = 40 gwei
        // priorityFee = 2 gwei (your tip)
        // gas_used = 21,000 (simple transfer)
        // Total cost = 42 gwei * 21,000 = 882,000 wei = 0.000882 ETH

        uint256 totalCost = (40e9 + 2e9) * 21_000;
    }
}
Enter fullscreen mode Exit fullscreen mode

The tricky part: how do you know what priority fee to set? Most wallets estimate this for you based on recent block data. But if you're building infrastructure, you need to understand it yourself.

// Real-world fee estimation logic (simplified)
const ethers = require('ethers');

async function estimateFees(provider) {
  const feeData = await provider.getFeeData();

  // feeData now includes baseFeePerGas and maxPriorityFeePerGas
  const baseFee = feeData.baseFeePerGas;
  const priorityFee = feeData.maxPriorityFeePerGas;

  // Set a maxFeePerGas slightly above your expected cost
  // (in case base fee jumps between submission and inclusion)
  const maxFeePerGas = baseFee.mul(2).add(priorityFee);

  return {
    baseFee: baseFee.toString(),
    maxPriorityFeePerGas: priorityFee.toString(),
    maxFeePerGas: maxFeePerGas.toString()
  };
}

// You send { maxFeePerGas, maxPriorityFeePerGas } to the network now
// Not just gasPrice like the old days
Enter fullscreen mode Exit fullscreen mode

The Max Fee Per Gas Gotcha

This one trips people up constantly. When you submit a transaction under EIP-1559, you specify two things:

  • maxPriorityFeePerGas: Your tip (goes to validator)
  • maxFeePerGas: The maximum you'll pay per gas (includes base fee)

The network uses: min(maxFeePerGas, baseFee + maxPriorityFeePerGas) as your actual gas price.

So if you set maxFeePerGas = 100 gwei and the base fee is only 40 gwei with your 5 gwei priority fee, you pay 45 gwei total. You don't pay the full 100 gwei—the difference gets refunded. You're setting a ceiling, not a fixed price.

// Actual gas cost calculation (from the protocol perspective)
function getActualGasCost(
    uint256 baseFee,
    uint256 maxPriorityFeePerGas,
    uint256 maxFeePerGas,
    uint256 gasUsed
) public pure returns (uint256) {
    // The actual gas price paid is the minimum of:
    // 1. maxFeePerGas (your ceiling)
    // 2. baseFee + maxPriorityFeePerGas (what you're willing to pay)

    uint256 actualGasPrice = min(
        maxFeePerGas,
        baseFee + maxPriorityFeePerGas
    );

    return actualGasPrice * gasUsed;
}

function min(uint256 a, uint256 b) private pure returns (uint256) {
    return a < b ? a : b;
}

// Scenario: You set maxFeePerGas = 150 gwei, maxPriorityFee = 3 gwei
// Actual base fee when included: 40 gwei
// Actual gas price = min(150, 40 + 3) = 43 gwei
// You only pay 43 gwei, not 150 gwei
Enter fullscreen mode Exit fullscreen mode

Production Gotchas I've Seen (And You Will Too)

1. Legacy Transactions Still Exist

Some wallets and contracts still use the old gasPrice field. This works, but you lose the benefits of EIP-1559. The network treats gasPrice as both the base fee and priority fee. Avoid this if possible.

// Old way (still functional but suboptimal)
const legacyTx = {
  to: recipient,
  gasPrice: ethers.utils.parseUnits('50', 'gwei'),
  gasLimit: 21000
};

// New way (EIP-1559)
const eip1559Tx = {
  to: recipient,
  maxFeePerGas: ethers.utils.parseUnits('100', 'gwei'),
  maxPriorityFeePerGas: ethers.utils.parseUnits('2', 'gwei'),
  gasLimit: 21000
};

// The new way is better because:
// 1. More predictable fees
// 2. You might overpay less (remember the refund mechanism)
// 3. Your tip is separated from the base fee
Enter fullscreen mode Exit fullscreen mode

2. Base Fee Unpredictability During Mega-Spikes

I audited a contract that hardcoded a maxFeePerGas value. It worked fine for six months. Then an NFT drop happened, and the base fee spiked to 3000+ gwei. Every transaction reverted with "transaction underpriced." The maxFeePerGas was set to 500 gwei.

Always use dynamic fee estimation. Always.

// Bad approach (don't do this)
const staticMaxFee = ethers.utils.parseUnits('50', 'gwei');
// ^ This will fail during congestion

// Good approach
async function buildTransaction(provider) {
  const feeData = await provider.getFeeData();

  // Add buffer for base fee movement
  const maxFeePerGas = feeData.baseFeePerGas
    .mul(2)  // 2x multiplier for safety
    .add(feeData.maxPriorityFeePerGas);

  return {
    maxFeePerGas,
    maxPriorityFeePerGas: feeData.maxPriorityFeePerGas
  };
}
Enter fullscreen mode Exit fullscreen mode

3. Transaction Replacement Complexity

Under EIP-1559, you can replace a pending transaction by submitting a new one with the same nonce, but you need to increase both maxFeePerGas AND maxPriorityFeePerGas. I've seen code that only bumped maxFeePerGas. The network rejected it because maxPriorityFeePerGas stayed the same.


javascript
// Original transaction (pending)
const originalTx = {
  nonce: 5,
  maxFeePerGas: ethers.utils.parseUnits('100', 'gwei'),
  maxPriorityFeePerGas: ethers.utils.parseUnits('2', 'gwei')
};

// Wrong: Only increasing maxFeePerGas won't work
const badReplacement = {
  nonce: 5,
  maxFeePerGas: ethers.utils.parseUnits('150', 'gwei'),
  maxPriorityFeePerGas:
Enter fullscreen mode Exit fullscreen mode

Top comments (0)