DEV Community

Cover image for Your Ethereum Gas Assumptions Are Probably From 2021
Swapcore.Exchange
Swapcore.Exchange

Posted on

Your Ethereum Gas Assumptions Are Probably From 2021

A lot of Ethereum deposit and payout code still carries assumptions from when gas was the dominant cost: hardcoded fee constants, minimums sized for twenty-dollar transfers, defaults that steer users away from mainnet. Through 2026, standard gas has mostly sat well under one gwei. An ERC-20 transfer costs cents.

That doesn't just make old code expensive. It makes some of it wrong. Here's what to revisit if you accept ETH or ERC-20 deposits.

1. Stop hardcoding gas prices and minimums

If a constant like this is still in your codebase, it's overcharging by two orders of magnitude:

# Written for a different fee era
PAYOUT_GAS_PRICE_GWEI = 40
MIN_DEPOSIT_USDT = 50
Enter fullscreen mode Exit fullscreen mode

Since the London upgrade, fees are EIP-1559: a protocol-set base fee plus a priority tip. Estimate both from live data at the moment of payout:

def estimate_fees(w3, priority_percentile=50):
    hist = w3.eth.fee_history(10, "latest", [priority_percentile])
    base_fee = hist["baseFeePerGas"][-1]   # next block's base fee
    tips = [r[0] for r in hist["reward"]]
    tip = sorted(tips)[len(tips) // 2]
    # Allow the base fee to rise for a few blocks before the tx lands
    max_fee = base_fee * 2 + tip
    return {"maxFeePerGas": max_fee, "maxPriorityFeePerGas": tip}
Enter fullscreen mode Exit fullscreen mode

Then derive minimums from the live estimate instead of a fixed value. A minimum sized for 40 gwei turns away users your economics could serve profitably today, and a minimum sized for 0.2 gwei will be wrong the next time the network gets busy. Recompute per quote.

2. Detect internal transactions, not just top-level transfers

The simplest deposit watcher checks each block for transactions whose to is a deposit address and whose value is non-zero. That misses ETH sent from smart contract wallets.

A Safe multisig, for example, executes a transfer through a contract call. On-chain, the ETH movement is an internal call, not a top-level transaction to your address. A naive watcher never sees it.

You have two reliable options:

  • Trace calls. Use debug_traceTransaction or trace_block (provider-dependent) and look for value transfers to your addresses at any call depth.
  • Watch balances. Poll eth_getBalance for deposit addresses at each block, and reconcile any increase against known transactions. Cruder, but it catches everything.

EIP-7702, introduced in the Pectra upgrade, lets ordinary accounts delegate to contract code. As adoption grows, the share of ETH deposits arriving through contract execution rather than plain transfers will grow with it. Plan for internal transfers to be normal.

ERC-20 deposits don't have this problem in the same way, because they emit a Transfer event regardless of who calls the token contract. Watch logs for those.

3. Use finality, not just confirmations

Ethereum produces a block every 12 seconds. Proof-of-stake adds a second signal: finality. A block becomes finalized after two epochs, roughly 13 minutes, at which point reverting it would require a large share of staked ETH to be destroyed.

Many services still use a confirmation count carried over from proof-of-work habits. You can do better with the block tags the JSON-RPC API exposes:

latest    = w3.eth.get_block("latest")
safe      = w3.eth.get_block("safe")       # justified, very unlikely to reorg
finalized = w3.eth.get_block("finalized")  # economically final

def is_credited(deposit_block, amount_usd):
    threshold = safe if amount_usd < 10_000 else finalized
    return deposit_block <= threshold["number"]
Enter fullscreen mode Exit fullscreen mode

safe usually trails the head by a few minutes; finalized by around 13. Tiering by amount gives most users a fast credit and keeps large deposits on economic finality.

4. Treat L2 ETH as a different asset

ETH on mainnet, Arbitrum, Base and Optimism all use identical 0x addresses. A deposit address you generated for mainnet is, byte for byte, a valid address on every one of them.

Users will send L2 ETH to a mainnet deposit address. Your mainnet watcher won't see it. Two things help:

  • Model assets as (chain_id, asset) pairs everywhere, so "ETH on Base" is never conflated with "ETH on mainnet" in quotes, orders or UI.
  • Optionally watch your deposit addresses on the major L2s too. You won't credit those deposits automatically, but you can flag them for support instead of having a user report "lost" funds days later.

5. Warn about the empty-gas-tank problem

A user swapping their entire ETH balance into USDT on Ethereum ends up with tokens they can't move, because they have no ETH for gas.

If your interface offers a "max" button for ETH, consider a soft default that leaves a small reserve, with an explicit override:

const RESERVE_WEI = estimateReserve(liveFees, 5); // ~5 future transfers
const maxSendable = balance - gasForThisTx - RESERVE_WEI;
Enter fullscreen mode Exit fullscreen mode

At current fee levels the reserve is tiny. The point is that it isn't zero.

6. Never ask for approvals on a deposit flow

A deposit-address model is a plain transfer. It needs no approve, no permit, no signed message.

If your product uses deposit addresses, make that explicit in your UI copy. Users have learned that legitimate swaps sometimes ask for approvals (router-based DEX swaps do). A clear statement that yours never will gives them a way to spot clones impersonating you.

Checklist

  • Fees estimated live with EIP-1559 fields; no hardcoded gwei
  • Minimums derived per quote from live fees
  • Deposit detection covers internal transactions
  • Crediting uses safe / finalized tags, tiered by amount
  • Assets keyed by chain ID; L2 deposits to mainnet addresses flagged
  • "Max" leaves a gas reserve by default
  • Deposit flow never requests approvals or signatures

Disclosure: I work on SwapCore (swapcore.exchange), a non-custodial instant exchange. Several of these came from revisiting our own assumptions after Ethereum fees fell.

Top comments (0)