EIP-8429: Escalating gas for repeated calls

Discussion topic for EIP-8429

Repetition is underpriced. The k-th call into an opted-in contract within a block pays G_REF * k^2 extra gas.
Half of that is deposited as stake with no withdrawer. Half is staked where the opted-in contract points.
The block producer gets nothing, so nobody has a reason to farm it.

Update Log

  • 2026-09-28: initial draft, PR 12384
  • 2026-09-28: two ratchets, fast per block and slow per originator per window, commit 070e88d
  • 2026-09-28: constants from the Robinhood replay, own-ordinal gate on the fast surcharge, commit acea3dc
  • 2026-09-29: number 8429 assigned, block-sequence rule moved into the Specification, commit 2bfd684

External Reviews

  • 2026-09-28: Measurements of the live token-level deployment from public RPC, confirming the NUMBER aliasing bug and finding that an earlier design charged 68% of references, by wtpk (who discloses holding a token I deployed), PR comment

Outstanding Issues

  • 2026-09-29: Should STATICCALL count as a reference? Batch reads and lending markets make repeated read-only calls with no extractive intent. Replay of both variants in progress, raised here
  • 2026-09-29: “A single swap pays nothing” holds only when the venue’s route stays inside the free allowance. Is K_FREE = 2 right for mainnet?
  • 2026-09-29: The cap binds at the 50th reference per block. Is pricing out contracts above that the right boundary?
  • 2026-09-29: StakeVault reference implementation is still to be supplied.

Where this came from

I did not start from a whiteboard. I started by running the machine this EIP prices.

On Robinhood Chain I watched the same thing happen to launch after launch: a token goes live, and within minutes a handful of wallets open a cluster of pools on it at fee tiers no person would choose, then work the token from one block to the next. I built a rig that detected that cluster of pool births and traded behind it, to learn the shape from the inside. It entered on 65 launches. It worked, which is the problem.

Then I measured what that shape costs everyone else. One launchpad’s transactions were about 42% of all gas fees on the chain. In one 24 hour window, 130 of 3,466 launches were hit by the pool cluster, and 31 of the 33 tokens that graduated that day were among them. The crews pick the winners almost perfectly, and ordinary buyers are the exit.

The conclusion I came to is simple. It is probably in everyone’s interest to tax this behaviour, in the chain’s native gas token, and to make sure nobody can pocket the tax. Half is staked forever with no authority over it at all. Half is staked at the direction of whatever application opts in to the standard. Nothing is burned and nothing goes to the block producer, so nobody has a reason to farm it.

What the EIP does

An account enrolls itself, once and irrevocably. After that, the k-th call into it within a block costs G_REF * k^2 extra gas. The counter lives in client memory, not in state. A second, slower counter prices the same originator returning to the same account over a week. The larger of the two is charged.

A person who buys once pays nothing. A machine that references the same contract fifty times in a block pays quadratically.

What already runs

The protocol form needs a client change. A token-level form of the same rule does not, and it has been live on Robinhood Chain since 2026-09-28.

My own replay, over 45,728 real transfers on the launches my rig entered, with the constants now in the draft:

Charged, in basis points of volume
Wallets matching the machine’s shape 632
Everyone else 19
1 Like

Disclosure: the prose in this draft was written with Claude. The mechanism, the constants, the replays and the deployed contracts are mine, and every number is reproducible from public RPC.

I haven’t fully reviewed the EIP yet, but does the proposal only affect a specific set of contracts selected for the surcharge accounting? Also, if it’s simply about penalizing gas usage, why not just try running a gas-consuming loop within the contract?

Thanks for the questions. Short answers first, with the sections to read if you want the detail.

Does it only affect contracts selected for the surcharge?

Yes. A contract opts in once, for itself, through a registry, and it cannot opt out. Every other account behaves exactly as it does today: same gas, same ordering, nothing new to account for. The client reads the enrollment set at the start of each block, so a contract that enrolls mid-block is only counted from the next block.

See Registry contract and Counted operations.

Why not just burn gas in a loop inside the contract?

Because the goal is not to make the contract expensive to use. It is to make repeating calls into it within one block expensive, while the first call stays free. That needs a count of how many times the contract has been called in the block, across every transaction and every sender, and a contract cannot keep that count well by itself:

  1. Counting costs every caller. A per-block counter in storage is a write on every call, paid by ordinary users too. Transient storage resets after each transaction, so it cannot see a bundle split across several transactions in one block. The client can hold the count in memory at no state cost, the same way it holds the EIP-2929 access sets.
  2. Reverted calls escape it. A counter inside the contract rolls back when the call reverts, so probing it on-chain is free. The protocol counter does not roll back.
  3. It misses netted calls. Venues with flash accounting, like the Uniswap v4 PoolManager, can net many operations into one or two calls, so the contract never sees the repetition.
  4. The cost goes to the wrong place. Gas burned in a loop goes to the base-fee burn and to the block producer, and it takes block space from everyone else. Here the surcharge is accounted separately and paid into stake that nobody owns, and the producer gets none of it, so nobody is rewarded for inviting more of it.

A contract-level version of the rule does exist, and it is live on Robinhood Chain as an ERC-20. It is a useful stopgap, and running it is how these limits showed up.

See Counting calls rather than storage writes and Block scope rather than transaction scope.

But I don’t like this idea if your intention is to penalize Multicall-style contracts. They reduce call data size and gas usage, so there is absolutely no reason to penalize them. If it is a matter of user behavior, that should be managed through the contract code rather than relying on the protocol.

Multicall isn’t penalized. Two things in the draft make sure of that:

  1. Only contracts that enroll themselves are counted. Multicall3 is never charged unless Multicall3 enrolls, and there’s no reason it would.
  2. STATICCALL is not a reference. Batched reads, which is most of what Multicall does, cost exactly what they cost today. (Counting them charged 80% of ordinary wallets in the replay, so they were taken out.)

What does get counted is a batch that writes to an enrolled contract several times in one block. That’s intended: it’s the enrolled contract choosing to price its own repetition, not a tax on the batcher. Multicall’s calldata and gas savings are untouched.

On doing it in contract code instead: that’s what’s live today as an ERC-20, and it’s how the limits in my previous reply showed up. It can’t see bundles split across transactions, reverted calls roll the counter back, and flash-accounted venues net the calls away. The protocol version exists because of what the contract version missed.

I understand. There are two points I’d like to address in this draft:

  1. The contract should be able to revoke a previously registered fee increase request. In other words, I think we should abstract this capability into the protocol: the contract code should decide whether or not to increase the fee, while the protocol is only responsible for providing the mechanism to execute that decision.

  2. If the goal is to allow ERC-20 tokens to track transactions across multiple executions, I think it would be better, at least in the short term, to pursue block-scoped storage or multi-block data storage proposals, such as the ones below. I prefer this approach because it adds a capability to the protocol rather than imposing a specific constraint. The contract can then decide how and when to make use of that capability.

Alternatively, we could use persistent storage. While it is somewhat more expensive, the lower gas costs expected after Glamsterdam should make this trade-off more reasonable.

Thanks, both points are fair.

1. Revocation. You’re right that the contract should be able to undo the decision, and the draft now allows it (c7e3770). scheduleExit() replaces irrevocable enrollment:

  • The exit becomes public on chain when it’s requested.
  • It takes effect at the start of the window after next, so never sooner than a full window (a week on mainnet).
  • Once scheduled it can’t be cancelled.
  • The contract can enroll again only a full window after the exit took effect. Without that, an issuer could schedule an exit, re-enroll inside the exit block, and get one surcharge-free block at a time chosen a week ahead. Fuzzing the registry found exactly that.

What I’d still keep out of contract code is deciding per call whether the surcharge applies. If the code could waive it, the contract could waive it for its own bundles and charge everyone else. And in the cases that matter the code isn’t there to decide anyway: a reverted probe rolls back whatever the contract recorded, and a call netted through flash accounting never reaches the contract.

2. Block-scoped storage. I’d like EIP-8212 to ship. The token-level form would move its per-block counter from SSTORE to BSTORE straight away. But that closes a cost gap, not the gaps that led to this EIP:

  • Reverts: EIP-8212 follows ordinary revert semantics, so a counter in BSTORE rolls back and probing stays free.
  • Netting: a v4-style venue settles many operations in one transfer, so the token sees one call wherever its counter lives.
  • Fee in kind: a contract can only charge in its own token. On Robinhood, a charged sell into a v4 pool reverts because the pool receives less than it’s owed, and launchers that check exact balances had to be exempted one by one. A gas surcharge leaves token amounts alone.

The same applies to persistent storage after Glamsterdam. Price isn’t the blocker; those three are.

jochem-brouwer’s EIP-7928 point on the EIP-8212 thread applies here too, so the same commit adds a section on it. The per-block and per-window counts are recorded in the block access list: one entry per transaction that calls an enrolled contract, plus each sender’s count carried in from earlier blocks of the week. That lets a client price a transaction without executing the ones before it, and a node joining mid-week can work out the counts from the access lists it already keeps. Validators check the carried-in counts against their own. Nothing is added to state. A model of those rules matched sequential execution on all 426k references in 29.5k random blocks.

Could you elaborate on why reverted operations need to be counted as well? Wouldn’t a revert roll back the state changes made earlier, effectively making the spam behavior meaningless? Not rolling back the call count when a call reverts seems to introduce rather unintuitive behavior in the EVM.

Good question. The draft states the rule but never explained it; [fc8e706] adds a rationale section. In short:

A reverted call isn’t meaningless, it’s the cheap half of the strategy. It reads the state it was sent against: a price, a reserve, whether a pool exists, whether a launch is live. The revert is the decision not to act on what it read. That’s how the machine this prices actually works. It sends many attempts at the same contract in a block, each guarded by a condition, and every one reverts except the winner. Inside a single transaction it does the same thing one frame down: it tries calls in subframes and reverts the losers.

If the count rolled back with the frame, every losing attempt would be free and only the winner would be counted. The surcharge would land on one call per block however many were sent, so the repetition would be invisible exactly where it’s heaviest. Blocking that is the whole point of counting.

On EVM semantics. The counter isn’t EVM state. No opcode reads it, so after a revert nothing an EVM program can see has changed. The only effect is on the gas of later calls into that contract. Reverted transactions already have effects of that kind:

  • their gas is used and paid;
  • their nonce is consumed;
  • their gas counts toward block fullness, and so toward the next block’s base fee under EIP-1559.

The closest precedent that goes the other way is EIP-2929: its warm sets do roll back on revert. The difference is deliberate. Warmth describes what a frame has loaded, and a revert throws that away. The counter describes what has been attempted, and a revert can’t undo an attempt.


However, an attacker could simulate most of the operations off-chain and leave only a very small number of trial-and-error operations on-chain. Moreover, they could create multiple accounts to circumvent the fee increase.

Multiple accounts. A per-sender counter is easy to dodge: open a fresh account and the count starts at zero. The global ratchet is a second counter on the target itself, shared by every caller. Account B does not inherit a clean slate. It pays the fee the target has already climbed to because of account A. Making more accounts does not create more cheap calls. It just spreads the same rising fee across more senders.

Off-chain simulation. The fee exists because each on-chain call, including one that reverts, still executes against live state. A revert is not free information. The call read the price, the reserve, or whether the pool is live, and only then decided not to commit. That read is what the ratchet counts. Doing the same search off-chain never asks the chain that question, so there is no call to count and nothing to make more expensive. The few trial calls that still land on-chain are counted, and they pay the level the ratchet is already at. Simulating the rest off-chain is the searcher avoiding the fee, which is the result we want, not a way around it.

If that’s the case, the mechanism is unintentionally penalizing users simply because they call the same contract multiple times during peak periods, while the intended goal is to penalize spam bots.

What I mean is that an attacker could move most of the simulated calls off-chain and leave only a very small number of actual on-chain calls. If the number of repeated calls is too low, it may not generate enough penalty to meaningfully deter the attack.

I think we’ve been talking past each other, and that’s on me for letting “attack” and “penalty” into the thread. This isn’t a spam or DoS defence. Nobody here is trying to hurt the contract.

What it prices is an extraction business. On a new token, the same operators trade it many times per block and come back dozens of times a day: a crew buying from a dozen wallets in the same block, for example. Every one of those calls is profitable. The repetition is the business itself, so it can’t move off-chain: a fill that isn’t on-chain isn’t a fill.

That changes both of your points.

“Too few on-chain calls to deter.” Agreed, and that’s fine. Simulation can replace the probing, and anything that needs only a call or two, like a sandwich, isn’t priced here. What’s left on-chain is the trades themselves, and those are what pay. The goal isn’t deterrence. The operator pays in proportion to how much of the block it takes up on that token, and the payment becomes validator stake.

“It penalizes ordinary users in busy blocks.” Yes: the third call into an enrolled token in a block pays, whoever makes it. That’s deliberate, because any per-wallet exemption is dodged with fresh wallets, which was your multiple-accounts point. The cost is bounded three ways: the first two calls per block are free, the token chose this by enrolling, and a contract with heavy organic traffic shouldn’t enroll. On the live token-level version, over one token’s first two hours (409 trades, 250 ms blocks), a 13-wallet same-block crew paid 19 bp of its volume and everyone else paid 0.6 bp. Grouped into 12-second windows, everyone else paid 21 bp, so block time matters.

Two corrections on my side, both in 68c1fd6:

  • The draft’s definitions had a per-wallet gate that contradicted the abstract and the gas rules. It’s removed.
  • Earlier in this thread I said the protocol version sees calls that flash accounting nets away. It doesn’t: a netted sequence is one or two calls into the token at either layer, and pool initialization never calls the token at all. The draft now has a section listing what it does not price.
1 Like