Ethereum Units and Gas Fees: Wei, Gwei and EIP-1559 Worked Out

Why ether amounts are integers of wei, how base fee, tip and max fee combine into what you pay, and how to do the maths without floating point mistakes.

ยท 7 min read

Screenshot of RedBison's Ethereum Gas Fee Calculator running in the browser

A deploy script sends 0.1 ETH to a new contract and the transaction reverts with "insufficient funds". The wallet holds 0.15 ETH. The bug is not in the contract: the script reserved gasLimit x maxFeePerGas up front, a max fee of 300 gwei on a 200,000 gas limit is 0.06 ETH, and 0.1 + 0.06 is more than 0.15. Two pieces of arithmetic sit under almost every Ethereum fee question, the unit conversion between wei and ether and the EIP-1559 fee formula. Both are simple once you see them as integer maths.

Ether is an integer number of wei

Ethereum has no decimals at the protocol level. Every balance, every msg.value and every fee is an unsigned integer counted in wei. The named units are just powers of ten that people find easier to read.

UnitWeiWhere you meet it
wei1Contract code, RPC responses, logs
kwei10^3Rarely used
mwei10^6Rarely used (same scale as USDC base units)
gwei10^9Gas prices and fees
szabo10^12Old code; Solidity dropped the literal in 0.7.0
finney10^15Old code; also dropped in 0.7.0
ether10^18Balances and amounts people read

The trouble starts in JavaScript. A Number is a 64-bit float, exact for integers only up to 2^53, which is about 9.007 x 10^15. One ether is 10^18 wei, so any non-trivial amount in wei is already past that limit. Sometimes you get lucky: 0.1 x 1e18 prints as 100000000000000000. Often you do not. 1.123456789123456789 x 1e18 becomes 1123456789123456800, and the last two digits are invented. Print 1 wei in ether with plain division and you get "1e-18", which some parsers then reject.

The fix is to never let an amount touch a float. Parse the decimal string, pad the fraction to 18 digits and build a BigInt:

function parseEther(s: string): bigint {
  const [whole, frac = ''] = s.split('.');
  if (frac.length > 18) throw new Error('more than 18 decimals');
  return BigInt(whole + frac.padEnd(18, '0'));
}

parseEther('0.1');                  // 100000000000000000n
parseEther('1.123456789123456789'); // 1123456789123456789n

Libraries already ship this. ethers has parseEther and formatEther, viem has parseEther, formatEther, parseGwei and parseUnits(value, decimals). Use them in application code. The Crypto Unit Converter does the same thing in the browser, so you can check a literal before pasting it into a test.

Tokens follow the same model with their own scale. An ERC-20 exposes decimals(), usually 18, but USDC and USDT use 6 and WBTC uses 8. A transfer of 1000000 base units is one USDC but only one trillionth of a DAI. Read decimals() before formatting anything.

Bitcoin units

Bitcoin works the same way with eight decimals. The base unit is the satoshi, 10^-8 BTC. Wallets also show bits (100 satoshi, the same as a microbitcoin) and mBTC (100,000 satoshi). The 21 million cap is 2,100,000,000,000,000 satoshi, which fits comfortably in a 64-bit integer, so Bitcoin Core never needed big integers. In the converter, pick Bitcoin in the From dropdown and the units switch to satoshi, microbitcoin, millibitcoin and bitcoin. A ninth decimal place on a BTC amount is rejected because it would be a fraction of a satoshi.

How a gas fee is actually computed

Gas measures work. A plain ETH transfer costs exactly 21,000 gas, the fixed intrinsic cost of a transaction. Each byte of input data adds 16 gas if non-zero and 4 gas if zero, and contract execution adds the cost of every opcode it runs. An ERC-20 transfer typically lands somewhere between about 35,000 and 65,000 gas depending on whether storage slots go from zero to non-zero. The price of gas is what changed in August 2021.

Before the London upgrade, a transaction carried one gasPrice and the fee was simply gas used x gasPrice. EIP-1559 replaced the auction with a protocol-set base fee per gas that moves with demand. If a block is more than half full, the next block's base fee goes up by as much as 12.5%; if it is less than half full, it goes down. A transaction now sets two numbers:

  • maxPriorityFeePerGas: the tip per gas you offer the validator.
  • maxFeePerGas: the most you will pay per gas, all in.

The price you actually pay per gas is:

effectivePrice = min(maxFeePerGas, baseFee + maxPriorityFeePerGas)
fee            = gasUsed x effectivePrice
burned         = gasUsed x baseFee
tip            = gasUsed x (effectivePrice - baseFee)

The burned part is destroyed. Only the tip reaches the block proposer. If maxFeePerGas is below the current base fee, the transaction is not invalid, it just waits in the mempool until the base fee falls below it.

Diagram of the EIP-1559 fee formula: base fee plus priority fee is capped by the max fee, multiplied by gas used, and split into a burned part and a tip
Diagram of the EIP-1559 fee formula: base fee plus priority fee is capped by the max fee, multiplied by gas used, and split into a burned part and a tip

Worked example in the Gas Fee Calculator

Open the Ethereum Gas Fee Calculator, choose "ERC-20 token transfer" (65,000 gas), keep the EIP-1559 fee model and enter a base fee of 12 gwei, a priority fee of 1.5 gwei and a max fee of 30 gwei. Press Calculate Gas Fees.

LineValue
Effective price13.5 gwei per gas
Total fee877,500 gwei = 0.0008775 ETH
Burned (base fee)0.00078 ETH
Tip to validator0.0000975 ETH
Max you can be charged0.00195 ETH

The max fee did not bind, because 12 + 1.5 is well under 30. Now change the max fee to 13 gwei and calculate again. The effective price drops to 13 gwei, the card notes "capped by max fee", and the tip falls to 1 gwei per gas. Set the max fee to 11 and the calculator stops with an error, because a transaction that cannot cover the base fee cannot be included.

The ETH price field is yours to fill. The calculator makes no network calls, so the dollar figure is exactly your fee in ETH times the price you typed.

Gas limit, gas used and the up-front reservation

This is the part that caused the "insufficient funds" error in the opening paragraph. When a transaction is validated, the sender must hold:

value + gasLimit x maxFeePerGas

That is a reservation, not the charge. After execution, the protocol charges gasUsed x effectivePrice and returns the rest. So a generous gas limit and a high max fee are both cheap insurance as long as the account can cover the worst case. The "Max you can be charged" line in the calculator is exactly gasLimit x maxFee, so put your gas limit into the Gas Limit field when you want to know how much ETH the wallet needs, and put the gas used from a receipt there when you want to reproduce what was paid.

A common rule of thumb for max fee is roughly twice the current base fee plus your tip. Since the base fee can rise by at most 12.5% per block, doubling covers about six consecutive full blocks before your transaction gets priced out.

Legacy transactions and rollups

Legacy (type 0) transactions still work after London. They pay gasPrice for every unit of gas, and the protocol treats them as if maxFee and maxPriorityFee were both set to that gasPrice: the base fee is burned and everything above it is the tip. In the calculator, switch the Fee Model to Legacy and type one gas price.

Rollups such as Arbitrum, Base and OP Mainnet use the same EIP-1559 fields for L2 execution, often with base fees far below 1 gwei. This is where floating point hurts again. 21,000 gas at 0.01 gwei is 210,000,000,000 wei, or 0.00000021 ETH; a tool that rounds ether to six decimals shows zero. The calculator keeps every value in wei and prints the exact decimal. Remember that rollups also charge an L1 data fee for posting your transaction to Ethereum, which is separate from the gas maths here and often the larger part of the bill.

Common mistakes

  • Treating gas limit as cost. You pay for gas used. A 300,000 gas limit on a call that uses 60,000 costs 60,000 x price.
  • Mixing units in config. One library wants gwei, another wants wei. A gas price of "20" in wei is effectively free and will never confirm. Convert explicitly with the Crypto Unit Converter.
  • Reusing 18 decimals for every token. Formatting USDC with 18 decimals makes a million dollars look like a rounding error.
  • Setting max fee equal to base fee. It leaves no headroom for the next block's increase, and the transaction can stall for many blocks.
  • Doing amount maths with Number. Use BigInt or a library, and format only at the edge for display.

When you need to see what a specific transaction set, decode it rather than guessing: the Ethereum Transaction Decoder shows the max fee, priority fee and gas limit from the raw bytes, and the decoding guide walks through where each field lives. For a quick sanity check on any fee, multiply three numbers in your head: gas used, gwei per gas, and 10^-9. If that answer and your wallet disagree by a factor of a billion, the bug is a unit, not the network.

Open the Ethereum Gas Fee Calculator