EIP-8037: State Creation Gas Cost Increase

Thanks @misilva73 . Sorry, more my stupid question then. I was fooled by forkcast.org:

There it is still in the “considered” stage:

What is the most reliable source for the hardfork state?

It is a fair question. Forkcast follows the Meta EIP, which still lists both 2780 and 8038 as CFI. We haven’t yet move them to SFI as we are still wrapping up the benchmarks, but they will almost certainly be moved to SFI in the next weeks

2 Likes

@hritzdorf Forkcast.org generally has the latest information (based on ACD calls).
AllCoreDevs - Consensus #183 - Forkcast

All devnet-7 EIPs ratified SFI; PRs to update EIP statuses will be opened

From: AllCoreDevs - Consensus #183 - Forkcast


The upgrade meta EIP EIP-7773: Hardfork Meta - Glamsterdam is the source of truth but PRs to update can take a little while (as EIPs need to be in specific states).


For the definitions of network upgrade states please see

2 Likes

tl;dr: as long as same-tx SELFDESTRUCT stays in the EVM (and there’s no alternative for ephemeral contracts), the account charge should be refilled when no state was created.

Coming back to the same-tx SELFDESTRUCT refill since @Helkomine’s question in #26 never got an answer.

As far as I can reconstruct: this was issue 4 in the spec review, Repricings #6 decided in favor of refunding (“Self-destruct refunds all state gas at transaction end”), it got implemented, and then execution-specs#2845 dropped it again for bal-devnet-7. The only rationale I found is in the tests-bal v7.1.0 release notes (Release tests-bal@v7.1.0 · ethereum/execution-specs · GitHub): deprecated opcode, small win on state, complicates the accounting. (def not a small win as there’s a practical use case)

The deprecation argument seems off to me here. The same-tx case is the one behavior 6780 deliberately kept. These contracts never enter the trie, so there’s nothing for a future removal to break, and a refill can’t exceed what the tx just paid.

The complexity objection is reasonable, but it seems aimed at the full version (end-of-tx, per-slot tallies). Would a minimal one work, refill just the fixed account-creation charge, at SELFDESTRUCT, within the creating frame? There’s no loop and LIFO stays intact.

Also, whatever the answer: could the rationale go into the EIP’s Rationale section? Right now the exclusion is there with no explanation, and the spec review itself said it should be documented if kept.

For context on why we should care:

Ephemeral executors behind deposit-address sweeps pay the full 183.6k for an account that never exists. It might be OK at today’s gas (tho spikes make it quite expensive), but it’s a fixed cost per sweep, and CPSB scales with the gas limit. So this gets worse as L1 scales, and the pattern just moves to L2s

4 Likes

A side effect on RPC simulations that the EIP does not address, since it only specifies transactions: in geth master, eth_call goes through the same reservoir split as a transaction, so a call given 50M gas starts with TX_MAX_GAS_LIMIT - intrinsic_gas (about 16.76M) of execution gas and the remaining ~33M sits in the state-gas reservoir, which computation cannot use. After Osaka, clients exempted eth_call from the EIP-7825 cap (geth #32625), so views that compute more than 16.7M gas (on-chain SVG renderers, quoters, aggregators) work today with the default 50M RPC gas cap. Under Amsterdam they fail with a plain out of gas.

Repro against stock geth --dev --override.amsterdam 0 and the full numbers are in eth_call execution gas capped at 2^24 after Amsterdam (EIP-8037) · Issue #35838 · ethereum/go-ethereum · GitHub.

Question for the authors and other client teams: should simulations with no transaction validity checks run with the whole requested gas as execution gas, as they do post-Osaka? If yes, it may be worth a sentence in the EIP (or in execution-apis) so clients converge on the same behaviour.

1 Like

@wighawag , thanks for raising this! The topic was discussed in ACDT #99. Geth already has a fix for this, but is not yet merged. We will also make sure the right behavior is included in the execution-api spec so that all clients behave the same.

2 Likes

tl;dr: in the first 10.5 hours of Glamsterdam on Sepolia, most of the new gas failures traced to fixed gas limits that no longer fit: ETH transfers to new accounts (now 204,600), the first use of an ERC-4337 nonce key (about 98,700 more), and EIP-8038 updates to existing storage (10,100 per warm write). Numbers and repro commands below.

Glamsterdam went live on Sepolia at 13:53:36 UTC on Oct 6. I recorded every Sepolia transaction for two days before the fork and since. In the first 10.5 hours after the fork, failures went from 1.91% to 10.97% of transactions (against the 10.5 hours before), and transactions that burned their whole gas limit from 0.15% to 2.02%. About a third of the extra failures are bots repeating the same failing call. In a random sample of traced failures, gas failures (some call frame ran out of gas) went from 53% to 72% of all failures. Below are the patterns I traced.

ETH transfers to new accounts. One clear pattern is an ETH send with a fixed gas limit, often 21,000, to an address that does not exist yet. Under EIP-8037 creating an account costs 183,600 state gas, so the transfer needs 204,600 (measured as gasUsed on a real client, on the Platåberget devnet and on Sepolia). These transfers are not rejected at submission: they are included and fail with status 0, the sender pays the fee and the recipient gets nothing. In those 10.5 hours, 406 such transfers failed on Sepolia, from 33 senders, at fixed limits from 21,000 to 200,000; with retries that is 1,524 failed transactions. (I left out a handful of senders that look like dust spam.) Most of those retries are one sender in a loop: it pays 2 ETH with a 200,000 gas limit, 4,600 short of 204,600, and has retried the same 7 payouts over 1,000 times, paying the fee each time.

ERC-4337 nonce keys. 8 smart accounts on one paymaster failed 164 bundles with AA26 over verificationGasLimit. Each op used a fresh nonce key, and in EntryPoint v0.7 the first use of a key writes a new storage slot (97,920 gas) inside the verification gas limit. Their limits had no room for it. On Sepolia that write now costs 98,657 more than reusing a key (17,134 before the fork). They recovered at 17:05 UTC when the limits went up.

Storage writes. A trace from the Sepolia node: an approve() with a fixed 100,000 gas limit ran out at SSTORE. It had 83,931 gas left, but a new storage slot now costs 97,920 in state gas plus execution. Under pre-fork rules the same transaction uses 45,969.

Updates to existing storage. Not just new state: EIP-8038 raised the cost of updating a storage slot that already exists from 2,900 to 10,100 gas when warm and from 5,000 to 12,100 when cold; a cold account access went from 2,600 to 3,000 (measured with debug_traceCall on the Sepolia node, before and after the fork). On one DEX, swaps now run out of gas at the pool’s storage updates: none of those writes is a new slot, but each costs about 7,200 more. An example swap had a 212,060 gas limit and needed 264,305. Swaps through a sibling router, sent at a fixed 250,000, failed 1,016 of 1,028 times in the first 10.5 hours (0 of 1,104 in the 10.5 hours before).

What did not change. A value transfer to an existing account is still 21,000. A zero-value transfer is 15,000 (existing or fresh address), a self-transfer 12,000. Cold SLOAD is unchanged at 2,100, so .transfer() / .send() receivers that fit in 2,300 today did not flip in any of my runs.

A tooling trap. anvil’s eth_estimateGas on Amsterdam forks returned 21,000 for a transfer to a new account, which needs 204,600. Foundry fixed it within a day (#17432, merged Oct 7). Until your Foundry build includes the fix, check estimates against a real Glamsterdam node, or measure by submitting.

The checker I used (static rules, an Amsterdam test diff, transaction replay), the commands behind every number here, and the upstream issues it turned up (Foundry, Bee, PoWFaucet, viem) are in the README: GitHub - PyraVim/glamcheck · GitHub

One question for the authors: would a short “what to check before mainnet” list for app developers be useful in the upgrade announcement, covering fixed gas limits on transfers, nonce keys and storage writes? Happy to contribute these patterns and the measurements behind them.

3 Likes

Adding a concrete case and live numbers to @milonite_2’s on the same-transaction SELFDESTRUCT refill.

Bruno is a gas relay. Each relayed call runs in a runner that is CREATE2’d for that one transaction: it makes the call, checks that the relay was paid, and is gone. The fresh contract is there for adversarial isolation. The runner is the call’s msg.sender, so whatever a call grants its caller (approvals, roles, operator rights) goes to an address no other client’s call will ever run from, and never to an account holding the relay’s funds. Its code is generated for each submission, with the call and payment as constants, so there’s no shared executor with state to guard or inputs to trust. It’s the ephemeral-executor pattern #46 describes.

Under 8037 as written, the 183,600 account charge is the same whether the runner is left in the trie or removed. eth_estimateGas through our deployer today:

Runner initcode Leaves behind Sepolia (Glamsterdam) Mainnet (Prague)
PUSH0 PUSH0 RETURN an account, nonce 1, no code, permanently 213,621 53,975
ORIGIN SELFDESTRUCT nothing 218,643 58,977

The runner that creates no state costs 5,022 more than the one that adds an account for good. The repricing is meant to make state growth pay for itself. Here it charges in full for state that is never written, and gives no reason to clean up.

For us it’s the largest single item after Glamsterdam: 183,600 of the roughly 420,000 gas we budget for a minimal relayed call paid by an EIP-3009 transfer. Our runners currently end with RETURN(0, 0), which leaves an account with no code in the trie. We’re switching them to SELFDESTRUCT anyway, because that’s the right thing for the state, but the gas schedule now rewards the opposite.

What I’d ask for:

  1. milonite_2’s minimal refill. When SELFDESTRUCT runs on an account created in the same transaction, refill the fixed account charge (STATE_BYTES_PER_NEW_ACCOUNT × CPSB). It can’t exceed what the transaction already paid, and nothing enters the trie.

  2. If it stays out, put the reason in the Rationale. Today the only reason given is in the tests-bal v7.1.0 release notes.

1 Like