Skip to content

Feature/glamsterdam existing eip updates#12091

Draft
Marchhill wants to merge 18 commits into
feature/eip-8038from
feature/glamsterdam-existing-eip-updates
Draft

Feature/glamsterdam existing eip updates#12091
Marchhill wants to merge 18 commits into
feature/eip-8038from
feature/glamsterdam-existing-eip-updates

Conversation

@Marchhill

Copy link
Copy Markdown
Contributor

Fixes Closes Resolves #

Please choose one of the keywords above to refer to the issue this PR solves followed by the issue number (e.g. Fixes #000). If no issue number, remove the line. Also, remove everything marked optional that is not applicable. Remove this note after reading.

Changes

  • List the changes

Types of changes

What types of changes does your code introduce?

  • Bugfix (a non-breaking change that fixes an issue)
  • New feature (a non-breaking change that adds functionality)
  • Breaking change (a change that causes existing functionality not to work as expected)
  • Optimization
  • Refactoring
  • Documentation update
  • Build-related changes
  • Other: Description

Testing

Requires testing

  • Yes
  • No

If yes, did you write tests?

  • Yes
  • No

Notes on testing

Optional. Remove if not applicable.

Documentation

Requires documentation update

  • Yes
  • No

If yes, link the PR to the docs update or the issue with the details labeled docs. Remove if not applicable.

Requires explanation in Release Notes

  • Yes
  • No

If yes, fill in the details here. Remove if not applicable.

Remarks

Optional. Remove if not applicable.

Marchhill and others added 7 commits June 22, 2026 13:07
glamsterdam-devnet-6 raises the EIP-7954 max contract code size from
32 KiB to 64 KiB (MAX_CODE_SIZE = 0x10000); MaxInitCodeSize derives as 2x.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
glamsterdam-devnet-6 activates EIP-2780 (reduce intrinsic tx gas) and
EIP-8246 (SELFDESTRUCT no burn) in the Amsterdam fork; both now have
for_amsterdam EEST fixtures.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Set the EIP-8038 state-access gas costs to their final glamsterdam-devnet-6
values (COLD_ACCOUNT_ACCESS/COLD_STORAGE_ACCESS 3000, ACCOUNT_WRITE 8000,
STORAGE_WRITE 10000 -> CALL_VALUE 10300, CREATE_ACCESS 11000, access-list
entries 3000, STORAGE_CLEAR_REFUND 12480) and schedule the EIP into Amsterdam.

WIP: SSTORE regular-write wiring (SSetRegular -> StorageWrite) and the
source-based refund model still to follow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Update EIP-2780 to its current spec as scheduled in glamsterdam-devnet-6:
- TX_BASE_COST 4500 -> 12000 (ECDSA recovery + sender access + sender write)
- recipient charge restructured to EELS calculate_intrinsic_cost: a flat
  COLD_ACCOUNT_ACCESS touch (via EIP-8038), plus TRANSFER_LOG_COST +
  TX_VALUE_COST (4244) for value transfers; CREATE adds CREATE_ACCESS (11000)
  + NEW_ACCOUNT state. Drops the earlier-draft two-tier no-code cold cost,
  STATE_UPDATE, and in-intrinsic new-account surcharge.

Validated: value-moving transactions now match expected gas/state/receipts
(remaining diff is the EIP-7928 BAL hash, addressed separately).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Charge the EIP-8038 STORAGE_WRITE (10000) regular cost on the first change
to a storage slot (both fresh-slot creation and reset), replacing the
EIP-8037 SSetRegular (2900) / SStoreResetCost. Validated: slotnum value
tests now match expected gas/state/receipts (BAL hash still pending the
EIP-8282 builder-request reads on the stacked branch).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Under EIP-8038 the SSTORE warm-access cost (WARM_ACCESS=100) is charged by
the access step itself and the separate net-metered SSTORE charge is
dropped, so a no-op or repeat write costs exactly the access (cold 3000 /
warm 100) rather than access + 100. Matches EELS amsterdam sstore.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Align SSTORE refunds to EIP-8038: storage-clear refund 4800 -> 12480
((STORAGE_WRITE + COLD_STORAGE_ACCESS) * 4800/5000), and restore-to-original
refunds the STORAGE_WRITE (10000) regular charge taken on the first change
(a freshly-created slot also refunds its state gas in-frame, unchanged).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions github-actions Bot added the evm label Jun 22, 2026
@claude

claude Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Claude finished @Marchhill's task in 7m 8s —— View job


Review: Feature/glamsterdam existing eip updates

  • Load tools and gather PR diff
  • Read changed files
  • Read relevant rules files
  • Analyze changes for correctness, performance, and style
  • Post inline comments and summary

Summary

This PR aligns the Nethermind implementation to glamsterdam devnet-6: final EIP-8038 repricing (COLD_ACCOUNT_ACCESS 2600→3000, COLD_STORAGE_ACCESS 2100→3000, STORAGE_WRITE replaces SSetRegular/SReset, StorageClearRefund formula), EIP-2780 intrinsic gas simplified to match EELS calculate_intrinsic_cost, EIP-7954 max code size corrected to 64 KiB, and EIP-8246 activated alongside EIP-8038 in Amsterdam.

The arithmetic checks out: StorageClearRefund = (10000 + 3000) * 4800 / 5000 = 12480, CallValue = 8000 + 2300 = 10300, CreateAccess = 8000 + 3000 = 11000. The SSTORE reversal refund (StorageWrite = 10000) and the zero-original state-gas credit path are correctly guarded. The net-metered SSTORE charge being set to Free under EIP-8038 and the warm-access charge being folded into ConsumeStorageAccessGas are consistent.


Findings

Severity Count
Critical 0
High 0
Medium 2
Low 2

Medium

1. Dead worldState parameterEthereumGasPolicy.cs line 562 (inline comment posted)

Eip2780ExtraGas no longer reads worldState anywhere in its body, yet both the private method and the public CalculateIntrinsicGas overload (line 482) still carry it. Callers who pass non-null worldState expecting it to affect gas (as it did pre-PR for account-existence checks) now silently get a different result. The XML doc acknowledges it ("no worldState lookup"), but the parameter should be removed or annotated [Obsolete].

2. Pre-EIP-8038 EIP-2780 standalone behavior changedEthereumGasPolicy.cs lines 574–576 (inline comment posted)

The old Eip2780ExtraGas charged five distinct costs when EIP-2780 was active but EIP-8038 was not: access-list warm discount (100), no-code cold discount (500 vs 2600), new-account surcharge (25 000), state-update for value transfers (1 000), and EIP-7702 delegation-target touch. All are gone. Since Amsterdam co-activates EIP-2780 and EIP-8038 together, the standalone path is effectively dead code — but the code structure still permits it. A brief comment confirming EIP-2780 is always co-activated with EIP-8038 would prevent future regressions, and the missing new-account surcharge / 7702 charge should be explicitly acknowledged if standalone is intentionally dropped.

Low

3. regularWriteCost computed but not used in early-return pathEthereumGasPolicy.cs lines 309–311 (inline comment posted)

regularWriteCost is computed before the !TIsSlotCreation.IsActive early-return, but the early-return re-evaluates spec.IsEip8038Enabled ? … : spec.GasCosts.SStoreResetCost independently. The values are numerically identical today (pre-8038: both 2900; post-8038: both 10000) but the divergence is a latent hazard if the two constants ever diverge.

4. EIP-8246 re-enabled without explanation25_Amsterdam.cs line 26 (inline comment posted)

The prior comment explicitly blocked EIP-8246 because it wasn't in EEST for_amsterdam fixtures. Re-enabling it in the same commit as EIP-8038 activation without any note makes it unclear whether the conformance tests now include EIP-8246 or whether the exclusion was intentionally lifted.


Not mergeable as-is: Medium findings 1 and 2 should be addressed or explicitly acknowledged with rationale before merge.

Under EIP-8038 the per-authorization regular gas is ACCOUNT_WRITE (8000)
plus REGULAR_PER_AUTH_BASE_COST (auth-tuple calldata floor + ECRECOVER +
cold account access + 2 warm accesses) = 15816, replacing the earlier
draft's 7500. The state component (NEW_ACCOUNT + AUTH_BASE) is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
/// the recipient's existence, code, or access-list membership (so no <paramref name="worldState"/> lookup),
/// overriding EIP-2929's "all tx addresses are warm" rule. The recipient stays pre-warmed for execution.
/// </remarks>
private static long Eip2780ExtraGas(Transaction tx, IReleaseSpec spec, IReadOnlyStateProvider? worldState)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium — dead worldState parameter

worldState is no longer read anywhere in this method body (the old RecipientTouchCost/IsDeadAccount calls are gone). It also makes CalculateIntrinsicGas's optional worldState = null silently a no-op for EIP-2780 paths.

The XML doc acknowledges it ("no worldState lookup"), but callers who pass a non-null worldState now get different gas figures than they did pre-PR, with no compile-time warning. Consider removing the parameter from both this private method and the public overload to make the API honest, or at the minimum add a [Obsolete] annotation on the public overload.

Fix this →

Comment on lines +309 to +311
long regularWriteCost = spec.IsEip8038Enabled ? Eip8038Constants.StorageWrite : GasCostOf.SSetRegular;
if (!TIsSlotCreation.IsActive)
return UpdateGas(ref gas, spec.IsEip8038Enabled ? Eip8038Constants.StorageWrite : spec.GasCosts.SStoreResetCost);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low — regularWriteCost is not used in the early-return path

regularWriteCost is computed on line 309 but the !TIsSlotCreation.IsActive branch on line 311 independently re-evaluates spec.IsEip8038Enabled ? Eip8038Constants.StorageWrite : spec.GasCosts.SStoreResetCost rather than using it. The values are numerically identical (pre-8038: SSetRegular == SStoreResetCost == 2900; post-8038: both StorageWrite), so this is not a correctness bug, but it would be cleaner to use regularWriteCost in the early return to make the intent self-evident:

Suggested change
long regularWriteCost = spec.IsEip8038Enabled ? Eip8038Constants.StorageWrite : GasCostOf.SSetRegular;
if (!TIsSlotCreation.IsActive)
return UpdateGas(ref gas, spec.IsEip8038Enabled ? Eip8038Constants.StorageWrite : spec.GasCosts.SStoreResetCost);
long regularWriteCost = spec.IsEip8038Enabled ? Eip8038Constants.StorageWrite : GasCostOf.SSetRegular;
if (!TIsSlotCreation.IsActive)
return UpdateGas(ref gas, spec.IsEip8038Enabled ? Eip8038Constants.StorageWrite : spec.GasCosts.SStoreResetCost);

Actually — since SStoreResetCost and SSetRegular happen to be equal today, the cleaner fix is just to use regularWriteCost for both branches:

Suggested change
long regularWriteCost = spec.IsEip8038Enabled ? Eip8038Constants.StorageWrite : GasCostOf.SSetRegular;
if (!TIsSlotCreation.IsActive)
return UpdateGas(ref gas, spec.IsEip8038Enabled ? Eip8038Constants.StorageWrite : spec.GasCosts.SStoreResetCost);
long regularWriteCost = spec.IsEip8038Enabled ? Eip8038Constants.StorageWrite : GasCostOf.SSetRegular;
if (!TIsSlotCreation.IsActive)
return UpdateGas(ref gas, regularWriteCost);

Note this is a semantic equivalence assumption: if SStoreResetCost and SSetRegular ever diverge in a future fork, the two branches would need different constants again.

Comment on lines +574 to +576
long cost = spec.IsEip8038Enabled ? Eip8038Constants.ColdAccountAccess : GasCostOf.ColdAccountAccess;
if (hasValue)
cost += GasCostOf.TransferLogEip2780 + GasCostOf.TxValueCostEip2780;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium — pre-EIP-8038 EIP-2780 behavior is significantly changed

When IsEip2780Enabled && !IsEip8038Enabled (a configuration that the flag structure still allows), this code now charges a flat GasCostOf.ColdAccountAccess for the recipient touch. The prior implementation charged:

  • GasCostOf.WarmStateRead (100) if the recipient was in the access list
  • GasCostOf.ColdAccountAccessNoCodeEip2780 (500) for code-less accounts
  • GasCostOf.ColdAccountAccess (2600) for code-bearing accounts
  • An additional GasCostOf.NewAccount (25 000) for value transfers to non-existent, non-precompile recipients
  • An additional GasCostOf.StateUpdateEip2780 (1 000) for value transfers to existing accounts
  • An additional EIP-7702 delegation-target touch when applicable

All five charges are now absent for standalone-EIP-2780 configurations, not just the EIP-8038 path.

If EIP-2780 is intentionally always co-activated with EIP-8038 (as in Amsterdam), the standalone path is dead code and the simplification is harmless. In that case, please add a brief comment here — e.g. // EIP-2780 is always co-activated with EIP-8038 (Amsterdam); the pre-8038 branch is kept only for defensive completeness. — so future readers don't reintroduce the old multi-tier logic. If standalone-EIP-2780 is a supported configuration, the missing new-account surcharge and EIP-7702 delegation charge need to be verified against the EELS spec.

// EEST `for_amsterdam` fixtures, so enabling it would diverge from conformance tests.
// It can be activated via the Eip8246Transition chainspec parameter when scheduled.
spec.IsEip8038Enabled = true;
spec.IsEip8246Enabled = true;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low — EIP-8246 re-enabled without visible implementation in this diff

The previous comment said EIP-8246 "is implemented but stays off here" due to it being a Draft not in the EEST for_amsterdam fixtures. This commit silently enables it alongside EIP-8038 with no explanation. Please confirm:

  1. EIP-8246 is now included in the EEST for_amsterdam fixtures (or the conformance test exclusion is intentional).
  2. No separate implementation PRs are needed — the existing implementation is complete and correct for devnet-6.

If this is just a "devnet-6 enables it" change and the implementation was already merged, a one-line comment here noting the devnet reference would help future auditors.

Marchhill and others added 10 commits June 22, 2026 15:32
A value/regular call to an EIP-7702 delegated recipient also accesses the
delegation target. The top-level frame only warms (does not gas-charge)
that target during execution, so price it in the intrinsic: a cold account
access (warm if the target is in the tx access list). Restores the charge
dropped when the recipient touch was simplified to a flat cold cost.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…sfers

A top-level value transfer that materialises a new (dead, non-precompile)
recipient now pays NEW_ACCOUNT state gas in execution, evaluated against
pre-transfer state, routed through the EIP-8037 state reservoir so it lands
in the state dimension of the block's max(regular, state) accounting. If the
state gas cannot be covered the frame is out of gas: no value moves, no
transfer log, and the sender forfeits all gas.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A value-bearing CALL now charges a flat CALL_VALUE (10300) regular under
EIP-8038, and a value transfer to a dead recipient charges NEW_ACCOUNT
(183600) state gas separately (routed through the state reservoir into the
block's max(regular,state) accounting), replacing the earlier EIP-2780
three-tier value cost that folded the new-account surcharge in.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A top-level call to an EIP-7702 delegated recipient charges a flat cold
account access for the delegation target in execution (the target is
already pre-warmed for the frame, so this is its only charge), per EELS
amsterdam top-frame logic. On insufficient gas the frame is exhausted and
the EVM halts out of gas.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The block's regular-gas dimension is the pre-refund regular gas consumed;
the EIP-7623/7976 calldata floor is a sender-only minimum (tx_gas_used /
receipts cumulative) and must not inflate the block header gasUsed. Drop
the Math.Max(blockRegularGas, floorGas) from the EIP-8037 block-regular
calculation so block gasUsed = max(sum regular, sum state) per the spec.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
For each authorization whose account leaf already exists, EELS refunds the
worst-case ACCOUNT_WRITE charged in the intrinsic to the regular-gas refund
counter (in addition to the NEW_ACCOUNT/AUTH_BASE state refunds applied
pre-execution). Nethermind applied only the state refund and zeroed the
delegation-refund count, dropping the regular refund. Now the count flows on
to the refund counter as ACCOUNT_WRITE x existing-authorities, and the
pre-execution state refund is no longer double-applied in ApplyCodeInsertRefunds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sending a positive balance to an empty SELFDESTRUCT beneficiary charges
ACCOUNT_WRITE (8000) regular gas in addition to the NEW_ACCOUNT state gas,
per EELS amsterdam selfdestruct. Nethermind charged only the state gas.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The CREATE/CREATE2 opcode charges CREATE_ACCESS (11000) regular gas for the
new account under EIP-8038, replacing the EIP-8037 CreateRegular (9000), per
EELS amsterdam create. (NEW_ACCOUNT state + init-code + memory unchanged.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ations

An EIP-7702 authorization that fails validation touches no state, so the
worst-case intrinsic charge is fully refunded: NEW_ACCOUNT + AUTH_BASE state
gas and the ACCOUNT_WRITE regular gas (EELS set_delegation, invalid case).
Nethermind refunded nothing for invalid auths, over-charging by up to the
full per-auth state cost.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The top-level delegation-target cold charge now reads the recipient's
delegation from post-authorization state, so a delegation installed by the
same transaction's authorization list is charged (and one cleared by it is
not), matching EELS which reads the recipient code at execution time.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant