Fix erc20_bloater running out of gas on Amsterdam - #269
Open
damilolaedwards wants to merge 2 commits into
Open
Conversation
Under Amsterdam (EIP-8037), creating a new storage slot also charges state-creation gas from a second, separate budget. A transaction only receives a share of that budget once its total requested gas exceeds the EIP-7825 ceiling; anything at or below that ceiling gets a state-gas reservoir of exactly zero, so every new slot's state-creation cost spills entirely into regular gas instead. bloatStorage() deliberately stayed just under that ceiling to satisfy EIP-7825, which is exactly what starves it of any state-gas reservoir. Every bloat transaction then needs roughly 4-5x the gas the fixed batch size assumed, and reverts out of gas partway through the loop. The per-transaction gas limit and address count are now computed from the chain's live fork state and gas ceiling instead of a fixed constant, mirroring the approach already used in storagerefundtx. Pre-Amsterdam behavior is unchanged. Batches are smaller than before on Amsterdam chains since the real per-address cost is much higher than the old model assumed, so reaching the same target storage size takes correspondingly more transactions.
TxPool.IsAmsterdam() is a static, operator-set preference that defaults to true - it does not reflect whether the connected chain has actually activated Amsterdam. On a chain that is Osaka-active but has not yet activated Amsterdam, a transaction whose Gas exceeds the EIP-7825 ceiling is rejected outright by the node before it ever reaches a block. The previous fix computed exactly such a value by default, so a run spanning the Amsterdam activation boundary (the scenario this fix exists for) would have every bloat transaction rejected during the pre-fork portion of the run, which is a worse failure than the original out-of-gas revert it replaced. The per-round build-and-send logic is now a standalone function so it can be retried with different parameters. When a round's transactions are rejected specifically for exceeding the gas limit, the scenario logs why and retries that one round with the pre-Amsterdam sizing. Nothing is cached across rounds: the next round tries the Amsterdam sizing again, so throughput recovers automatically once the chain actually activates Amsterdam, without operator intervention in either direction. Rejected-at-submission transactions never reach the chain, so the nonces they allocated are released for reuse instead of leaving a permanent gap; transactions that revert on-chain already consumed their nonce legitimately and are left alone. Adds a test proving the exact fallback values are never subject to the rejection this is recovering from, and a test covering the error-detection helper against the real wrapped error chain and against unrelated errors that happen to mention gas or limits.
damilolaedwards
marked this pull request as ready for review
August 6, 2026 21:04
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #260
Under Amsterdam (EIP-8037), creating a new storage slot also charges state-creation gas from a second, separate budget. A transaction only receives a share of that budget once its total requested gas exceeds the EIP-7825 ceiling; anything at or below that ceiling gets a state-gas reservoir of exactly zero, so every new slot's state-creation cost spills entirely into regular gas instead.
bloatStorage() deliberately stayed just under that ceiling to satisfy EIP-7825, which is exactly what starved it of any state-gas reservoir. Every bloat transaction then needed roughly 4-5x the gas the fixed batch size assumed, and reverted out of gas partway through the loop.
The per-transaction gas limit and address count are now computed from the chain's live fork state and gas ceiling instead of a fixed constant, mirroring the approach already used in storagerefundtx. Batches are smaller than before on Amsterdam chains since the real per-address cost is much higher than the old model assumed, so reaching the same target storage size takes correspondingly more transactions.
That alone isn't quite enough on its own, though. TxPool.IsAmsterdam() is a static, operator-set preference that defaults to true, not a live check against what the connected chain has actually activated. On a chain that is Osaka-active but not yet Amsterdam-active, a transaction whose gas exceeds the EIP-7825 ceiling is rejected outright before it ever reaches a block, so a run spanning the Amsterdam activation boundary would have every pre-fork transaction rejected, which is a worse failure than the original revert.
To handle that, the per-round send logic is now retried once with the pre-Amsterdam sizing whenever a round is rejected specifically for exceeding the gas limit. Nothing is cached across rounds, so the next round tries the Amsterdam sizing again and throughput recovers on its own once the chain actually activates Amsterdam. Nonces from rejected-at-submission transactions are released so the retry can reuse them instead of leaving a gap; transactions that revert on-chain are left alone since those nonces are already legitimately consumed.
Pre-Amsterdam behavior (no override, chain never touches the Amsterdam path) is unchanged throughout.