From 1a40187fbd0c077558387e938df8a35faf203b76 Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Mon, 22 Jun 2026 13:07:24 +0100 Subject: [PATCH 01/27] feat: EIP-7954 increase max contract size to 64 KiB (devnet-6) 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 --- src/Nethermind/Nethermind.Core/CodeSizeConstants.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Nethermind/Nethermind.Core/CodeSizeConstants.cs b/src/Nethermind/Nethermind.Core/CodeSizeConstants.cs index bcd2406faf92..8d5a4a06e91d 100644 --- a/src/Nethermind/Nethermind.Core/CodeSizeConstants.cs +++ b/src/Nethermind/Nethermind.Core/CodeSizeConstants.cs @@ -6,5 +6,5 @@ namespace Nethermind.Core; public static class CodeSizeConstants { public const int MaxCodeSizeEip170 = 24_576; // 24KiB - public const int MaxCodeSizeEip7954 = 32_768; // 32KiB + public const int MaxCodeSizeEip7954 = 65_536; // 64KiB } From 8efe06a2b4ea17658ec18ce637ec478c403a9329 Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Mon, 22 Jun 2026 13:08:49 +0100 Subject: [PATCH 02/27] feat: schedule EIP-2780 and EIP-8246 into Amsterdam (devnet-6) 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 --- src/Nethermind/Nethermind.Specs/Forks/25_Amsterdam.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Nethermind/Nethermind.Specs/Forks/25_Amsterdam.cs b/src/Nethermind/Nethermind.Specs/Forks/25_Amsterdam.cs index 424b474ba532..a65b2cacc7a7 100644 --- a/src/Nethermind/Nethermind.Specs/Forks/25_Amsterdam.cs +++ b/src/Nethermind/Nethermind.Specs/Forks/25_Amsterdam.cs @@ -11,6 +11,7 @@ public class Amsterdam() : NamedReleaseSpec(BPO2.Instance) public override void Apply(NamedReleaseSpec spec) { spec.Name = "Amsterdam"; + spec.IsEip2780Enabled = true; spec.IsEip7976Enabled = true; spec.IsEip7981Enabled = true; spec.IsEip7708Enabled = true; @@ -21,9 +22,7 @@ public override void Apply(NamedReleaseSpec spec) spec.MaxCodeSize = CodeSizeConstants.MaxCodeSizeEip7954; spec.IsEip8024Enabled = true; spec.IsEip8037Enabled = true; - // EIP-8246 is implemented but stays off here: it is still a Draft and not part of the - // EEST `for_amsterdam` fixtures, so enabling it would diverge from conformance tests. - // It can be activated via the Eip8246Transition chainspec parameter when scheduled. + spec.IsEip8246Enabled = true; spec.EngineApiNewPayloadVersion = EngineApiVersions.NewPayload.V5; spec.EngineApiGetPayloadVersion = EngineApiVersions.GetPayload.V6; spec.EngineApiForkchoiceVersion = EngineApiVersions.Fcu.V4; From de79428f9d36798d766085a8204e22e8ff453ec1 Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Mon, 22 Jun 2026 13:19:54 +0100 Subject: [PATCH 03/27] feat: EIP-8038 state-access reprice with final values (devnet-6) 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 --- .../Nethermind.Core/Eip8038Constants.cs | 34 +++++++++---------- .../Nethermind.Specs/Forks/25_Amsterdam.cs | 1 + 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/src/Nethermind/Nethermind.Core/Eip8038Constants.cs b/src/Nethermind/Nethermind.Core/Eip8038Constants.cs index c85d78caf55f..933dbd31e501 100644 --- a/src/Nethermind/Nethermind.Core/Eip8038Constants.cs +++ b/src/Nethermind/Nethermind.Core/Eip8038Constants.cs @@ -8,29 +8,27 @@ namespace Nethermind.Core; /// (regular + state) gas model. /// /// -/// The EIP is a Draft: the final repriced values are still TBD. The base parameters below are -/// therefore placeholders equal to the current (pre-8038) costs, so that enabling the EIP is a no-op -/// for the base constants until the final figures land. The derived parameters are expressed via the -/// EIP's derivation formulas so they recompute automatically once the base values are finalized. +/// Scheduled in Amsterdam by glamsterdam-devnet-6 with the final repriced values below. The derived +/// parameters are expressed via the EIP's derivation formulas so they recompute from the base values. /// public static class Eip8038Constants { - // Base parameters (placeholders == current values; final values TBD). + // Base parameters (final values per EIP-8038, glamsterdam-devnet-6). - /// Cold account-touch cost (EIP-2929 COLD_ACCOUNT_ACCESS). - public const long ColdAccountAccess = GasCostOf.ColdAccountAccess; // 2600 + /// Cold account-touch cost (COLD_ACCOUNT_ACCESS). + public const long ColdAccountAccess = 3000; // was 2600 (EIP-2929) - /// Warm state-access cost (EIP-2929 WARM_ACCESS). - public const long WarmAccess = GasCostOf.WarmStateRead; // 100 + /// Warm state-access cost (WARM_ACCESS). + public const long WarmAccess = GasCostOf.WarmStateRead; // 100 (unchanged) - /// Cold storage-slot access cost (EIP-2929 COLD_STORAGE_ACCESS). - public const long ColdStorageAccess = GasCostOf.ColdSLoad; // 2100 + /// Cold storage-slot access cost (COLD_STORAGE_ACCESS). + public const long ColdStorageAccess = 3000; // was 2100 (EIP-2929) /// The account-write component of value-bearing *CALLs (CALL_VALUE - CALL_STIPEND). - public const long AccountWrite = GasCostOf.CallValue - GasCostOf.CallStipend; // 6700 + public const long AccountWrite = 8000; - /// The regular-gas write component of SSTORE (GAS_STORAGE_UPDATE - COLD_STORAGE_ACCESS - WARM_ACCESS). - public const long StorageWrite = GasCostOf.SReset - ColdStorageAccess - WarmAccess; // 2800 + /// The regular-gas write component of SSTORE (STORAGE_WRITE). + public const long StorageWrite = 10000; /// Stipend forwarded with a value-bearing call (unchanged from EIP-2929). public const long CallStipend = GasCostOf.CallStipend; // 2300 @@ -38,17 +36,17 @@ public static class Eip8038Constants // Derived parameters (EIP-8038 derivation formulas; recompute from the base values above). /// CALL_VALUE = ACCOUNT_WRITE + CALL_STIPEND. - public const long CallValue = AccountWrite + CallStipend; // 9000 + public const long CallValue = AccountWrite + CallStipend; // 10300 /// CREATE_ACCESS = ACCOUNT_WRITE + COLD_STORAGE_ACCESS, charged in regular gas by CREATE/CREATE2. public const long CreateAccess = AccountWrite + ColdStorageAccess; /// Access-list address entry cost, redefined to COLD_ACCOUNT_ACCESS. - public const long AccessListAddressCost = ColdAccountAccess; // 2600 (was 2400) + public const long AccessListAddressCost = ColdAccountAccess; // 3000 (was 2400) /// Access-list storage-key entry cost, redefined to COLD_STORAGE_ACCESS. - public const long AccessListStorageKeyCost = ColdStorageAccess; // 2100 (was 1900) + public const long AccessListStorageKeyCost = ColdStorageAccess; // 3000 (was 1900) /// STORAGE_CLEAR_REFUND = (STORAGE_WRITE + COLD_STORAGE_ACCESS) * 4800 / 5000. - public const long StorageClearRefund = (StorageWrite + ColdStorageAccess) * 4800 / 5000; + public const long StorageClearRefund = (StorageWrite + ColdStorageAccess) * 4800 / 5000; // 12480 } diff --git a/src/Nethermind/Nethermind.Specs/Forks/25_Amsterdam.cs b/src/Nethermind/Nethermind.Specs/Forks/25_Amsterdam.cs index a65b2cacc7a7..c014b7073f7d 100644 --- a/src/Nethermind/Nethermind.Specs/Forks/25_Amsterdam.cs +++ b/src/Nethermind/Nethermind.Specs/Forks/25_Amsterdam.cs @@ -22,6 +22,7 @@ public override void Apply(NamedReleaseSpec spec) spec.MaxCodeSize = CodeSizeConstants.MaxCodeSizeEip7954; spec.IsEip8024Enabled = true; spec.IsEip8037Enabled = true; + spec.IsEip8038Enabled = true; spec.IsEip8246Enabled = true; spec.EngineApiNewPayloadVersion = EngineApiVersions.NewPayload.V5; spec.EngineApiGetPayloadVersion = EngineApiVersions.GetPayload.V6; From 159aebff4210abafe2ff48e06c45f0400ffbb049 Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Mon, 22 Jun 2026 13:36:50 +0100 Subject: [PATCH 04/27] feat: align EIP-2780 intrinsic gas to devnet-6 spec 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 --- src/Nethermind/Nethermind.Core/GasCostOf.cs | 3 +- .../GasPolicy/EthereumGasPolicy.cs | 80 +++++-------------- 2 files changed, 23 insertions(+), 60 deletions(-) diff --git a/src/Nethermind/Nethermind.Core/GasCostOf.cs b/src/Nethermind/Nethermind.Core/GasCostOf.cs index 4e82ca45de26..9fb62fb286be 100644 --- a/src/Nethermind/Nethermind.Core/GasCostOf.cs +++ b/src/Nethermind/Nethermind.Core/GasCostOf.cs @@ -93,7 +93,8 @@ public static class GasCostOf public const long MinModExpEip7883 = 500; // eip-7883 // eip-2780: reduce intrinsic transaction gas and reprice state-touching primitives. - public const long TransactionEip2780 = 4500; // TX_BASE_COST = 3000 ECRECOVER + 1000 STATE_UPDATE + 500 sender cold no-code + public const long TransactionEip2780 = 12000; // TX_BASE_COST: ECDSA recovery + sender account access + sender account write + public const long TxValueCostEip2780 = 4244; // recipient balance write for a value-bearing transfer (non-create) public const long StateUpdateEip2780 = 1000; // one account-leaf write (nonce/balance coalesced) public const long ColdAccountAccessNoCodeEip2780 = 500; // cold touch of an account known to have no code public const long TransferLogEip2780 = 1756; // eip-7708 LOG3 transfer event: 375 + 3*375 + 32*8 diff --git a/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs b/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs index bafc87a46a4f..95ec6fb17a91 100644 --- a/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs +++ b/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs @@ -236,12 +236,11 @@ public static bool ConsumeAccountAccessGas(ref EthereumGasPolicy gas, }; } - // EIP-2780 prices a cold touch of a code-less account cheaper than one with code. - // EIP-8038 otherwise reprices the cold account-access cost. + // EIP-8038 reprices the (flat) cold account-access cost. devnet-6 dropped the earlier + // EIP-2780 two-tier no-code discount, so the touch is independent of whether the account has code. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static long ColdAccountAccessCost(IReleaseSpec spec, bool hasCode) => - spec.IsEip2780Enabled && !hasCode ? GasCostOf.ColdAccountAccessNoCodeEip2780 - : spec.IsEip8038Enabled ? Eip8038Constants.ColdAccountAccess + spec.IsEip8038Enabled ? Eip8038Constants.ColdAccountAccess : GasCostOf.ColdAccountAccess; public static bool ConsumeStorageAccessGas(ref EthereumGasPolicy gas, @@ -529,7 +528,9 @@ public static EthereumGasPolicy CreateAvailableFromIntrinsic(long gasLimit, in E [MethodImpl(MethodImplOptions.AggressiveInlining)] private static long CreateCost(Transaction tx, IReleaseSpec spec) => tx.IsContractCreation && spec.IsEip2Enabled - ? (spec.IsEip8037Enabled ? GasCostOf.CreateRegular : GasCostOf.TxCreate) + ? (spec.IsEip8038Enabled ? Eip8038Constants.CreateAccess + : spec.IsEip8037Enabled ? GasCostOf.CreateRegular + : GasCostOf.TxCreate) : 0; [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -541,72 +542,33 @@ private static long DataCost(Transaction tx, IReleaseSpec spec, long tokensInCal spec.GetBaseDataCost(tx) + tokensInCallData * GasCostOf.TxDataZero; /// - /// EIP-2780 charges on top of TX_BASE_COST: the EIP-7708 transfer log, the new-account surcharge, - /// and the recipient cold/warm touch plus its value-transfer STATE_UPDATE. + /// EIP-2780 recipient charge on top of TX_BASE_COST. For a non-self call: a flat + /// COLD_ACCOUNT_ACCESS touch, plus (for a value transfer) the EIP-7708 transfer log and + /// TX_VALUE_COST recipient balance write. For a CREATE: only the transfer log when value is sent + /// (the CREATE_ACCESS regular cost and NEW_ACCOUNT state cost are added by + /// /). /// /// - /// The recipient touch overrides EIP-2929's "all tx addresses are warm" rule. It is priced here - /// (where pre-state is available) rather than mid-execution; the recipient remains pre-warmed, so - /// no opcode re-charges it. Requires for the state-dependent parts. + /// Mirrors EELS calculate_intrinsic_cost: the recipient touch is a flat cold charge independent of + /// the recipient's existence, code, or access-list membership (so no lookup), + /// overriding EIP-2929's "all tx addresses are warm" rule. The recipient stays pre-warmed for execution. /// private static long Eip2780ExtraGas(Transaction tx, IReleaseSpec spec, IReadOnlyStateProvider? worldState) { if (!spec.IsEip2780Enabled) return 0; - bool isCreate = tx.IsContractCreation; - Address? to = tx.To; bool hasValue = !tx.Value.IsZero; - bool senderIsRecipient = !isCreate && tx.SenderAddress == to; - long cost = 0; - // EIP-7708 transfer log on the top-level value transfer; CREATE endows a distinct address. - if (hasValue && (isCreate || !senderIsRecipient)) - cost += GasCostOf.TransferLogEip2780; + if (tx.IsContractCreation) + return hasValue ? GasCostOf.TransferLogEip2780 : 0; - if (isCreate || to is null || worldState is null) return cost; + // Self-transfers coalesce into the sender leaf write already priced into TX_BASE_COST. + if (tx.SenderAddress == tx.To) return 0; - bool isPrecompile = spec.IsPrecompile(to); - bool recipientDead = worldState.IsDeadAccount(to); - - // New-account surcharge: value transfer to a nonexistent, non-precompile recipient. - if (hasValue && !isPrecompile && recipientDead) - cost += GasCostOf.NewAccount; - - // Self-transfers coalesce into the sender leaf write already priced into TX_BASE_COST; - // precompiles are warm at tx start and charged zero. - if (!senderIsRecipient && !isPrecompile) - { - cost += RecipientTouchCost(spec, worldState, to, AccessListAddresses(tx)); - // The new-account surcharge already covers the recipient leaf write. - if (hasValue && !recipientDead) - cost += GasCostOf.StateUpdateEip2780; - } + long cost = spec.IsEip8038Enabled ? Eip8038Constants.ColdAccountAccess : GasCostOf.ColdAccountAccess; + if (hasValue) + cost += GasCostOf.TransferLogEip2780 + GasCostOf.TxValueCostEip2780; return cost; } - - private static long RecipientTouchCost(IReleaseSpec spec, IReadOnlyStateProvider worldState, Address to, IReadOnlySet
? accessList) - { - long cost = accessList?.Contains(to) == true - ? GasCostOf.WarmStateRead - : worldState.IsContract(to) ? GasCostOf.ColdAccountAccess : GasCostOf.ColdAccountAccessNoCodeEip2780; - - // EIP-7702: a delegated recipient also touches its delegation target (always carries code). - // The EVM warms (does not gas-charge) this target for the top-level frame, so this is the sole charge. - if (spec.IsEip7702Enabled && ICodeInfoRepository.TryGetDelegatedAddress(worldState.GetCode(to).AsSpan(), out Address? target)) - cost += accessList?.Contains(target) == true ? GasCostOf.WarmStateRead : GasCostOf.ColdAccountAccess; - - return cost; - } - - private static IReadOnlySet
? AccessListAddresses(Transaction tx) - { - if (tx.AccessList is null) return null; - HashSet
set = []; - foreach ((Address address, _) in tx.AccessList) - { - set.Add(address); - } - return set; - } } From d78e652953a267a2a318ec4926727eb2e2e375bc Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Mon, 22 Jun 2026 14:10:43 +0100 Subject: [PATCH 05/27] feat: EIP-8038 SSTORE write cost (STORAGE_WRITE) 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 --- .../Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs b/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs index 95ec6fb17a91..6bf4e89967a6 100644 --- a/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs +++ b/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs @@ -302,12 +302,16 @@ public static bool ConsumeStorageWrite(ref EthereumGa where TEip8037 : struct, IFlag where TIsSlotCreation : struct, IFlag { - if (!TIsSlotCreation.IsActive) return UpdateGas(ref gas, spec.GasCosts.SStoreResetCost); + // EIP-8038 reprices the SSTORE write component (charged on the first change to a slot, + // for both fresh slots and resets) to a flat STORAGE_WRITE. + long regularWriteCost = spec.IsEip8038Enabled ? Eip8038Constants.StorageWrite : GasCostOf.SSetRegular; + if (!TIsSlotCreation.IsActive) + return UpdateGas(ref gas, spec.IsEip8038Enabled ? Eip8038Constants.StorageWrite : spec.GasCosts.SStoreResetCost); return TEip8037.IsActive switch { // EIP-8037: charge the regular component first so an OOG halt does not // spill state gas into gas_left and then restore it to the parent frame. - true => TryConsumeStateAndRegularGas(ref gas, GetStorageSetStateCost(in gas), GasCostOf.SSetRegular), + true => TryConsumeStateAndRegularGas(ref gas, GetStorageSetStateCost(in gas), regularWriteCost), false => UpdateGas(ref gas, GasCostOf.SSet), }; } From ac177ae86e47666617adf716dc760ad8f6b14a4d Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Mon, 22 Jun 2026 14:24:39 +0100 Subject: [PATCH 06/27] feat: EIP-8038 SSTORE access folds in warm cost; drop net-metered charge 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 --- src/Nethermind/Nethermind.Core/Specs/SpecGasCosts.cs | 5 ++++- src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Nethermind/Nethermind.Core/Specs/SpecGasCosts.cs b/src/Nethermind/Nethermind.Core/Specs/SpecGasCosts.cs index 124848462b48..8a74053bf829 100644 --- a/src/Nethermind/Nethermind.Core/Specs/SpecGasCosts.cs +++ b/src/Nethermind/Nethermind.Core/Specs/SpecGasCosts.cs @@ -57,8 +57,11 @@ public SpecGasCosts(IReleaseSpec spec) ? GasCostOf.SReset - GasCostOf.ColdSLoad : GasCostOf.SReset; + // EIP-8038 folds the warm-access charge into the SSTORE access cost itself (see + // ConsumeStorageAccessGas), so no separate net-metered charge is added on top. long netMeteredSStoreCost = NetMeteredSStoreCost = - hotCold ? GasCostOf.WarmStateRead + spec.IsEip8038Enabled ? GasCostOf.Free + : hotCold ? GasCostOf.WarmStateRead : netIstanbul ? GasCostOf.SStoreNetMeteredEip2200 : netConstantinople ? GasCostOf.SStoreNetMeteredEip1283 : GasCostOf.Free; diff --git a/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs b/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs index 6bf4e89967a6..791a1e270d44 100644 --- a/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs +++ b/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs @@ -259,7 +259,9 @@ public static bool ConsumeStorageAccessGas(ref EthereumGasPolicy gas, if (accessTracker.WarmUp(in storageCell)) return UpdateGas(ref gas, spec.IsEip8038Enabled ? Eip8038Constants.ColdStorageAccess : GasCostOf.ColdSLoad); - if (storageAccessType == StorageAccessType.SLOAD) + // EIP-8038 charges the warm-access cost on SSTORE too (the net-metered charge is dropped); + // pre-8038, a warm SSTORE access is free here and the warm cost comes from net metering. + if (storageAccessType == StorageAccessType.SLOAD || spec.IsEip8038Enabled) return UpdateGas(ref gas, GasCostOf.WarmStateRead); return true; } From dc56e32302e2500e5416ed8810950d419e528a66 Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Mon, 22 Jun 2026 14:55:59 +0100 Subject: [PATCH 07/27] feat: EIP-8038 SSTORE source-based refunds 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 --- src/Nethermind/Nethermind.Core/Specs/SpecGasCosts.cs | 8 +++++--- .../Instructions/EvmInstructions.Storage.cs | 10 ++++++++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/Nethermind/Nethermind.Core/Specs/SpecGasCosts.cs b/src/Nethermind/Nethermind.Core/Specs/SpecGasCosts.cs index 8a74053bf829..9b8950d7d653 100644 --- a/src/Nethermind/Nethermind.Core/Specs/SpecGasCosts.cs +++ b/src/Nethermind/Nethermind.Core/Specs/SpecGasCosts.cs @@ -111,9 +111,11 @@ public SpecGasCosts(IReleaseSpec spec) ? GasCostOf.TotalCostFloorPerTokenEip7623 : GasCostOf.Free; - SClearRefund = spec.IsEip3529Enabled - ? RefundOf.SClearAfterEip3529 - : RefundOf.SClearBeforeEip3529; + SClearRefund = spec.IsEip8038Enabled + ? Eip8038Constants.StorageClearRefund // 12480 + : spec.IsEip3529Enabled + ? RefundOf.SClearAfterEip3529 + : RefundOf.SClearBeforeEip3529; DestroyRefund = spec.IsEip3529Enabled ? RefundOf.DestroyAfterEip3529 diff --git a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Storage.cs b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Storage.cs index 2d34bf5a34f6..96d7e7a0a590 100644 --- a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Storage.cs +++ b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Storage.cs @@ -560,12 +560,18 @@ internal static EvmExceptionType InstructionSStoreMetered Date: Mon, 22 Jun 2026 15:26:37 +0100 Subject: [PATCH 08/27] feat: EIP-8038 EIP-7702 per-authorization regular cost 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 --- src/Nethermind/Nethermind.Core/Eip8038Constants.cs | 7 +++++++ src/Nethermind/Nethermind.Evm/GasPolicy/IGasPolicy.cs | 4 +++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Nethermind/Nethermind.Core/Eip8038Constants.cs b/src/Nethermind/Nethermind.Core/Eip8038Constants.cs index 933dbd31e501..e0172aa96a93 100644 --- a/src/Nethermind/Nethermind.Core/Eip8038Constants.cs +++ b/src/Nethermind/Nethermind.Core/Eip8038Constants.cs @@ -49,4 +49,11 @@ public static class Eip8038Constants /// STORAGE_CLEAR_REFUND = (STORAGE_WRITE + COLD_STORAGE_ACCESS) * 4800 / 5000. public const long StorageClearRefund = (StorageWrite + ColdStorageAccess) * 4800 / 5000; // 12480 + + /// + /// EIP-7702 per-authorization regular gas: ACCOUNT_WRITE + REGULAR_PER_AUTH_BASE_COST, where + /// the latter is the auth-tuple calldata floor (101 bytes × 16), an ECRECOVER (3000), a cold account + /// touch, and two warm accesses. + /// + public const long PerAuthBaseRegular = AccountWrite + (101 * 16 + 3000 + ColdAccountAccess + 2 * WarmAccess); // 15816 } diff --git a/src/Nethermind/Nethermind.Evm/GasPolicy/IGasPolicy.cs b/src/Nethermind/Nethermind.Evm/GasPolicy/IGasPolicy.cs index 4574bb88be09..c31d7fbb4fbe 100644 --- a/src/Nethermind/Nethermind.Evm/GasPolicy/IGasPolicy.cs +++ b/src/Nethermind/Nethermind.Evm/GasPolicy/IGasPolicy.cs @@ -205,9 +205,11 @@ public static (long RegularCost, long StateCost) AuthorizationListCost(Transacti } long authCount = authList.Length; + // EIP-8038 reprices the per-authorization regular cost (ACCOUNT_WRITE + auth-base). + long perAuthRegular = spec.IsEip8038Enabled ? Eip8038Constants.PerAuthBaseRegular : GasCostOf.PerAuthBaseRegular; return spec.IsEip8037Enabled ? ( - authCount * GasCostOf.PerAuthBaseRegular, + authCount * perAuthRegular, authCount * (GasCostOf.NewAccountState + GasCostOf.PerAuthBaseState) ) : (authCount * GasCostOf.NewAccount, 0); From a13003261fbff8297d819a5b7bf654f4ab3bf7c4 Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Mon, 22 Jun 2026 15:32:30 +0100 Subject: [PATCH 09/27] feat: EIP-2780 intrinsic charges delegated-recipient target touch 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 --- .../GasPolicy/EthereumGasPolicy.cs | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs b/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs index 791a1e270d44..2cb25dc1ca0d 100644 --- a/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs +++ b/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs @@ -556,8 +556,9 @@ private static long DataCost(Transaction tx, IReleaseSpec spec, long tokensInCal /// /// /// Mirrors EELS calculate_intrinsic_cost: the recipient touch is a flat cold charge independent of - /// the recipient's existence, code, or access-list membership (so no lookup), - /// overriding EIP-2929's "all tx addresses are warm" rule. The recipient stays pre-warmed for execution. + /// the recipient's existence or code, overriding EIP-2929's "all tx addresses are warm" rule; the recipient + /// stays pre-warmed for execution. A delegated recipient (EIP-7702) additionally touches its delegation + /// target — priced here from since the top-level frame only warms it. /// private static long Eip2780ExtraGas(Transaction tx, IReleaseSpec spec, IReadOnlyStateProvider? worldState) { @@ -571,10 +572,29 @@ private static long Eip2780ExtraGas(Transaction tx, IReleaseSpec spec, IReadOnly // Self-transfers coalesce into the sender leaf write already priced into TX_BASE_COST. if (tx.SenderAddress == tx.To) return 0; - long cost = spec.IsEip8038Enabled ? Eip8038Constants.ColdAccountAccess : GasCostOf.ColdAccountAccess; + long coldAccess = spec.IsEip8038Enabled ? Eip8038Constants.ColdAccountAccess : GasCostOf.ColdAccountAccess; + long cost = coldAccess; if (hasValue) cost += GasCostOf.TransferLogEip2780 + GasCostOf.TxValueCostEip2780; + // EIP-7702: calling a delegated recipient also touches its delegation target. The EVM warms + // (does not gas-charge) that target for the top-level frame, so it is priced here instead. + if (spec.IsEip7702Enabled && worldState is not null && tx.To is not null + && ICodeInfoRepository.TryGetDelegatedAddress(worldState.GetCode(tx.To).AsSpan(), out Address? target)) + { + cost += TxAccessListContains(tx, target) ? GasCostOf.WarmStateRead : coldAccess; + } + return cost; } + + private static bool TxAccessListContains(Transaction tx, Address address) + { + if (tx.AccessList is null) return false; + foreach ((Address entry, _) in tx.AccessList) + { + if (entry == address) return true; + } + return false; + } } From 79289a223399875f714d8f1a0a49466353cb347b Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Mon, 22 Jun 2026 16:13:55 +0100 Subject: [PATCH 10/27] feat: EIP-2780/8037 top-frame NEW_ACCOUNT charge for new-account transfers 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 --- .../TransactionProcessor.cs | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs b/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs index 32562c9a054b..c65b6ebfc9a9 100644 --- a/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs +++ b/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs @@ -388,16 +388,30 @@ private TransactionResult ExecuteSimpleTransfer( bool senderIsRecipient = tx.SenderAddress == recipient; bool isTracingActions = tracer.IsTracingActions; + // EIP-2780/EIP-8037 top-frame charge: a value transfer that materialises a new + // (dead, non-precompile) recipient pays NEW_ACCOUNT state gas, evaluated against + // pre-transfer state. If the (state) gas cannot be covered the frame is out of gas: + // no value moves and the sender forfeits all gas. + bool newAccountOutOfGas = false; + if (spec.IsEip2780Enabled && hasValueTransfer && !senderIsRecipient + && !spec.IsPrecompile(recipient) && WorldState.IsDeadAccount(recipient)) + { + newAccountOutOfGas = !TGasPolicy.ConsumeStateGas(ref gasAvailable, TGasPolicy.GetNewAccountStateCost(in gasAvailable)); + // Out of gas: consume the whole budget; the failed frame moves no value. + if (newAccountOutOfGas) + TGasPolicy.Consume(ref gasAvailable, TGasPolicy.GetRemainingGas(in gasAvailable)); + } + // Self-send: sender account is already touched/warmed by gas charging and any // +/- value balance ops would cancel to a net no-op, so skip both state writes. - if (!senderIsRecipient) + if (!senderIsRecipient && !newAccountOutOfGas) { if (hasValueTransfer) PayValue(tx, spec, opts); WorldState.AddToBalanceAndCreateIfNotExists(recipient, in hasValueTransfer ? ref value : ref UInt256.Zero, spec); } JournalCollection? logs = null; - if (spec.IsEip7708Enabled && hasValueTransfer && !senderIsRecipient) + if (spec.IsEip7708Enabled && hasValueTransfer && !senderIsRecipient && !newAccountOutOfGas) { LogEntry transferLog = TransferLog.CreateTransfer(tx.SenderAddress!, recipient, in value); logs = [transferLog]; @@ -429,7 +443,7 @@ private TransactionResult ExecuteSimpleTransfer( long postIntrinsicStateReservoir = TGasPolicy.GetStateReservoir(in gasAvailable); GasConsumed spentGas = Refund(tx, header, spec, opts, in substate, in gasAvailable, in opcodeGasPrice, codeInsertRefunds: 0, in floorGas, in standardGas, postIntrinsicStateReservoir); - const int statusCode = StatusCode.Success; + int statusCode = newAccountOutOfGas ? StatusCode.Failure : StatusCode.Success; if (tracer.IsTracingAccess) { From cd1610bf46edadede88f28bf0541a8afa3ec033e Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Mon, 22 Jun 2026 16:48:42 +0100 Subject: [PATCH 11/27] feat: EIP-8038 CALL value/new-account reprice 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 --- .../GasPolicy/EthereumGasPolicy.cs | 9 +++++++-- .../Nethermind.Evm/GasPolicy/IGasPolicy.cs | 2 +- .../Instructions/EvmInstructions.Call.cs | 18 +++++++++++------- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs b/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs index 2cb25dc1ca0d..d7e3c57bbb6d 100644 --- a/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs +++ b/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs @@ -422,10 +422,15 @@ public static long ApplyCodeInsertRefunds(ref EthereumGasPolicy gas, int codeIns public static bool ConsumeCallValueTransfer(ref EthereumGasPolicy gas) => UpdateGas(ref gas, GasCostOf.CallValue); - // EIP-2780 value-moving call cost: subsumes the legacy CallValue + NewAccount charges. + // EIP-2780 value-moving call cost. Under EIP-8038 a value-bearing call charges a flat CALL_VALUE + // (the new-account surcharge moves to a separate NEW_ACCOUNT state charge); the earlier draft used + // a three-tier charge keyed on self-call and recipient existence that subsumed the surcharge. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool ConsumeCallValueTransferEip2780(ref EthereumGasPolicy gas, bool isSelfCall, bool recipientEmpty) + public static bool ConsumeCallValueTransferEip2780(ref EthereumGasPolicy gas, bool isSelfCall, bool recipientEmpty, IReleaseSpec spec) { + if (spec.IsEip8038Enabled) + return UpdateGas(ref gas, Eip8038Constants.CallValue); + long cost = isSelfCall ? GasCostOf.CallValueSelfEip2780 : recipientEmpty ? GasCostOf.CallValueNewAccountEip2780 : GasCostOf.CallValueExistingEip2780; diff --git a/src/Nethermind/Nethermind.Evm/GasPolicy/IGasPolicy.cs b/src/Nethermind/Nethermind.Evm/GasPolicy/IGasPolicy.cs index c31d7fbb4fbe..a9c133ba441a 100644 --- a/src/Nethermind/Nethermind.Evm/GasPolicy/IGasPolicy.cs +++ b/src/Nethermind/Nethermind.Evm/GasPolicy/IGasPolicy.cs @@ -134,7 +134,7 @@ static virtual long ApplyCodeInsertRefunds(ref TSelf gas, int codeInsertRefunds, static abstract bool ConsumeCallValueTransfer(ref TSelf gas); // EIP-2780 three-tier value-moving call cost replacing the legacy CallValue + NewAccount charges. - static abstract bool ConsumeCallValueTransferEip2780(ref TSelf gas, bool isSelfCall, bool recipientEmpty); + static abstract bool ConsumeCallValueTransferEip2780(ref TSelf gas, bool isSelfCall, bool recipientEmpty, IReleaseSpec spec); static abstract bool ConsumeNewAccountCreation(ref TSelf gas) where TEip8037 : struct, IFlag; static abstract bool ConsumeLogEmission(ref TSelf gas, long topicCount, long dataSize); static abstract TSelf Max(in TSelf a, in TSelf b); diff --git a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Call.cs b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Call.cs index 18a31f275e31..368a88e7c89f 100644 --- a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Call.cs +++ b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Call.cs @@ -147,7 +147,7 @@ public static EvmExceptionType InstructionCall !state.AccountExists(target), - true => hasValueTransfer && state.IsDeadAccount(target), - }); + // EIP-8038 charges a value transfer to a dead recipient the NEW_ACCOUNT state cost (separate + // from the flat CALL_VALUE above). The earlier EIP-2780 draft folded creation into the + // value-transfer tier, so it charges nothing extra here. + bool chargesNewAccount = spec.IsEip8038Enabled + ? hasValueTransfer && state.IsDeadAccount(target) + : !spec.IsEip2780Enabled && (spec.ClearEmptyAccountWhenTouched switch + { + false => !state.AccountExists(target), + true => hasValueTransfer && state.IsDeadAccount(target), + }); bool newAccountOutOfGas = chargesNewAccount && !TGasPolicy.ConsumeNewAccountCreation(ref gas); From 611e7fb89f45c51f7ebd90e1b8148eac3554fcfc Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Mon, 22 Jun 2026 17:10:24 +0100 Subject: [PATCH 12/27] feat: EIP-2780/8037 top-frame delegation-target touch in execution 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 --- .../TransactionProcessing/TransactionProcessor.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs b/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs index c65b6ebfc9a9..960c37a3b901 100644 --- a/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs +++ b/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs @@ -299,6 +299,17 @@ private TransactionResult ExecuteEvmTransaction( if (!(result = BuildExecutionEnvironment(tx, spec, _codeInfoRepository, accessTracker, preloadedCodeInfo, preloadedDelegationAddress, out ExecutionEnvironment e))) return result; using ExecutionEnvironment env = e; + // EIP-2780/EIP-8037 top-frame charge: a top-level call whose recipient is an EIP-7702 + // delegation also touches the delegation target with a (flat) cold account access. The + // target is already pre-warmed for the frame, so this is the sole charge for it. If the + // gas cannot be covered the frame is exhausted and the EVM halts out of gas. + if (spec.IsEip2780Enabled && !tx.IsContractCreation && preloadedDelegationAddress is not null) + { + long delegationCold = spec.IsEip8038Enabled ? Eip8038Constants.ColdAccountAccess : GasCostOf.ColdAccountAccess; + if (!TGasPolicy.UpdateGas(ref gasAvailable, delegationCold)) + TGasPolicy.Consume(ref gasAvailable, TGasPolicy.GetRemainingGas(in gasAvailable)); + } + int statusCode = !tracer.IsTracingInstructions ? ExecuteEvmCall(tx, header, spec, tracer, opts, delegationRefunds, intrinsicGas, accessTracker, gasAvailable, env, out TransactionSubstate substate, out GasConsumed spentGas) : ExecuteEvmCall(tx, header, spec, tracer, opts, delegationRefunds, intrinsicGas, accessTracker, gasAvailable, env, out substate, out spentGas); From c3a2aab95dfad7ac26b780ceff38326650d4fe9d Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Mon, 22 Jun 2026 17:24:20 +0100 Subject: [PATCH 13/27] fix: EIP-7778/8037 block gas excludes the calldata floor 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 --- .../GasPolicy/Eip8037BlockGasInclusionCheck.cs | 9 +++++---- .../TransactionProcessing/TransactionProcessor.cs | 3 +-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Nethermind/Nethermind.Evm/GasPolicy/Eip8037BlockGasInclusionCheck.cs b/src/Nethermind/Nethermind.Evm/GasPolicy/Eip8037BlockGasInclusionCheck.cs index 3412bd2fc0ba..07aaafc0c6b0 100644 --- a/src/Nethermind/Nethermind.Evm/GasPolicy/Eip8037BlockGasInclusionCheck.cs +++ b/src/Nethermind/Nethermind.Evm/GasPolicy/Eip8037BlockGasInclusionCheck.cs @@ -45,11 +45,12 @@ public static long CalculateBlockRegularGas( long initialRegularGas, long remainingRegularGas, long stateGasSpill, - long stateGasSpillReclassified, - long floorGas) + long stateGasSpillReclassified) { + // EIP-7778/EIP-8037: the block's regular-gas dimension is the pre-refund regular gas actually + // consumed. The EIP-7623/7976 calldata floor is a minimum charge on the sender (tx_gas_used / + // receipts) only and must NOT inflate the block gasUsed. long executionRegularGasUsed = initialRegularGas - remainingRegularGas - stateGasSpill + stateGasSpillReclassified; - long blockRegularGas = intrinsicRegularGas + executionRegularGasUsed; - return Math.Max(blockRegularGas, floorGas); + return intrinsicRegularGas + executionRegularGasUsed; } } diff --git a/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs b/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs index 960c37a3b901..e51f770b2112 100644 --- a/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs +++ b/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs @@ -1723,8 +1723,7 @@ private static long Calculate8037BlockRegularGas( initialRegularGas, remainingRegularGas, stateGasSpill, - stateGasSpillReclassified, - floorGas); + stateGasSpillReclassified); } protected virtual void PayRefund(Transaction tx, UInt256 refundAmount, IReleaseSpec spec) From 20bfdb9dda6e554c98b1aa78dbb11d5cceffddb6 Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Tue, 23 Jun 2026 00:56:24 +0100 Subject: [PATCH 14/27] fix: EIP-8038 EIP-7702 existing-authority regular ACCOUNT_WRITE refund 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 --- .../GasPolicy/EthereumGasPolicy.cs | 27 +++++++++---------- .../TransactionProcessor.cs | 5 +++- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs b/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs index d7e3c57bbb6d..42fca5104698 100644 --- a/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs +++ b/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs @@ -400,23 +400,22 @@ public static void ResetForHalt(ref EthereumGasPolicy gas, long initialStateRese } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static long GetCodeInsertRegularRefund(int codeInsertRefunds, IReleaseSpec spec) => - spec.IsEip8037Enabled || codeInsertRefunds <= 0 - ? 0 - : (GasCostOf.NewAccount - GasCostOf.PerAuthBaseCost) * codeInsertRefunds; + public static long GetCodeInsertRegularRefund(int codeInsertRefunds, IReleaseSpec spec) + { + if (codeInsertRefunds <= 0) return 0; + // EIP-8038: per existing-authority EIP-7702 refund, the worst-case ACCOUNT_WRITE charged in the + // intrinsic is returned to the regular-gas refund counter (the NEW_ACCOUNT/AUTH_BASE state refunds + // are applied separately in Apply8037DelegationRefunds). + if (spec.IsEip8038Enabled) return Eip8038Constants.AccountWrite * codeInsertRefunds; + if (spec.IsEip8037Enabled) return 0; + return (GasCostOf.NewAccount - GasCostOf.PerAuthBaseCost) * codeInsertRefunds; + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static long ApplyCodeInsertRefunds(ref EthereumGasPolicy gas, int codeInsertRefunds, IReleaseSpec spec, long stateGasFloor) - { - if (codeInsertRefunds > 0 && spec.IsEip8037Enabled) - { - long stateGasRefund = checked(GetNewAccountStateCost(in gas) * codeInsertRefunds); - long refundFloor = Math.Max(0, stateGasFloor - stateGasRefund); - RefundStateGas(ref gas, stateGasRefund, refundFloor, trackSpillRefund: false); - } - - return GetCodeInsertRegularRefund(codeInsertRefunds, spec); - } + // Under EIP-8037 the per-authorization state refund is applied pre-execution in + // Apply8037DelegationRefunds; only the regular refund is surfaced for the refund counter here. + => GetCodeInsertRegularRefund(codeInsertRefunds, spec); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool ConsumeCallValueTransfer(ref EthereumGasPolicy gas) diff --git a/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs b/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs index e51f770b2112..b45412edbbda 100644 --- a/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs +++ b/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs @@ -650,7 +650,10 @@ private TransactionResult Apply8037DelegationRefunds( long refundFloor = Math.Max(0, stateGasFloor - stateGasRefund); TGasPolicy.RefundStateGas(ref gasAvailable, stateGasRefund, refundFloor, trackSpillRefund: false); - delegationRefunds = 0; + // delegationRefunds (existing-authority count) is intentionally NOT zeroed: it flows on to + // Refund as codeInsertRefunds to surface the regular ACCOUNT_WRITE refund. The state refund + // applied just above is the only state-dimension refund, so ApplyCodeInsertRefunds must not + // re-apply it. delegationAuthBaseRefunds = 0; } From 27b5809ded9c3cd139634e00d1ec0223ded6c532 Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Tue, 23 Jun 2026 10:49:33 +0100 Subject: [PATCH 15/27] feat: EIP-8038 SELFDESTRUCT beneficiary ACCOUNT_WRITE charge 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 --- .../Instructions/EvmInstructions.ControlFlow.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.ControlFlow.cs b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.ControlFlow.cs index e9196fd7fdd4..1585d4e7ca58 100644 --- a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.ControlFlow.cs +++ b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.ControlFlow.cs @@ -250,7 +250,17 @@ public static EvmExceptionType InstructionSelfDestruct !inheritorAccountExists && spec.UseShanghaiDDosProtection, }; - bool outOfGas = chargesNewAccount && !(TGasPolicy.ConsumeNewAccountCreation(ref gas)); + bool outOfGas = false; + if (chargesNewAccount) + { + // EIP-8038: sending a positive balance to an empty beneficiary costs ACCOUNT_WRITE regular + // gas in addition to the NEW_ACCOUNT state gas. Charge regular first so a regular-gas OOG does + // not spill state gas. + if (spec.IsEip8038Enabled) + outOfGas = !TGasPolicy.UpdateGas(ref gas, Eip8038Constants.AccountWrite); + if (!outOfGas) + outOfGas = !TGasPolicy.ConsumeNewAccountCreation(ref gas); + } if (outOfGas) goto OutOfGas; From 811b0310dcbc322474416b30eeaab848d5d83027 Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Tue, 23 Jun 2026 14:01:10 +0100 Subject: [PATCH 16/27] feat: EIP-8038 CREATE/CREATE2 opcode account cost (CREATE_ACCESS) 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 --- .../Nethermind.Evm/Instructions/EvmInstructions.Create.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Create.cs b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Create.cs index 948995bae579..4ff65e86d0c9 100644 --- a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Create.cs +++ b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Create.cs @@ -123,7 +123,12 @@ public static EvmExceptionType InstructionCreate Date: Tue, 23 Jun 2026 14:31:41 +0100 Subject: [PATCH 17/27] fix: EIP-8038 refund full auth state/regular gas for invalid authorizations 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 --- .../TransactionProcessing/TransactionProcessor.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs b/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs index b45412edbbda..babbdc04790b 100644 --- a/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs +++ b/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs @@ -732,6 +732,13 @@ private int ProcessDelegations(Transaction tx, IReleaseSpec spec, in StackAccess if (authorizationResult != AuthorizationTupleResult.Valid) { if (Logger.IsDebug) Logger.Debug($"Delegation {authTuple} is invalid with error: {error}"); + // EIP-8038: an invalid authorization touches no state, so the worst-case intrinsic + // charge (NEW_ACCOUNT + AUTH_BASE state and ACCOUNT_WRITE regular) is fully refunded. + if (spec.IsEip8037Enabled) + { + refunds++; + authBaseRefunds++; + } } else { From 4fd85c2bd5ab16460940762525b45e1d82490f17 Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Tue, 23 Jun 2026 14:51:59 +0100 Subject: [PATCH 18/27] fix: EIP-2780 top-frame delegation touch uses post-authorization state 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 --- .../TransactionProcessing/TransactionProcessor.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs b/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs index babbdc04790b..d1c85eeca3dd 100644 --- a/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs +++ b/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs @@ -303,7 +303,11 @@ private TransactionResult ExecuteEvmTransaction( // delegation also touches the delegation target with a (flat) cold account access. The // target is already pre-warmed for the frame, so this is the sole charge for it. If the // gas cannot be covered the frame is exhausted and the EVM halts out of gas. - if (spec.IsEip2780Enabled && !tx.IsContractCreation && preloadedDelegationAddress is not null) + // The delegation is read from post-authorization state, so a delegation installed by this + // same transaction's authorization list is included (and one cleared by it is excluded). + bool recipientIsDelegated = spec.IsEip7702Enabled && tx.To is not null + && _codeInfoRepository.TryGetDelegation(tx.To, spec, out _); + if (spec.IsEip2780Enabled && !tx.IsContractCreation && recipientIsDelegated) { long delegationCold = spec.IsEip8038Enabled ? Eip8038Constants.ColdAccountAccess : GasCostOf.ColdAccountAccess; if (!TGasPolicy.UpdateGas(ref gasAvailable, delegationCold)) From 357a5be4d5ac1db3796f0009ee12bd8fd015b0a2 Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Tue, 23 Jun 2026 17:29:38 +0100 Subject: [PATCH 19/27] test: pin EIP-8038 constants to devnet-6 repriced values Co-Authored-By: Claude Opus 4.8 --- .../Eip8038ConstantsTests.cs | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/Nethermind/Nethermind.Core.Test/Eip8038ConstantsTests.cs b/src/Nethermind/Nethermind.Core.Test/Eip8038ConstantsTests.cs index 8ae66c23c393..eab65c91d80d 100644 --- a/src/Nethermind/Nethermind.Core.Test/Eip8038ConstantsTests.cs +++ b/src/Nethermind/Nethermind.Core.Test/Eip8038ConstantsTests.cs @@ -7,14 +7,14 @@ namespace Nethermind.Core.Test; /// -/// Pins the EIP-8038 gas parameters to the spec's derivation formulas. The base values are -/// placeholders equal to the current (pre-8038) costs while the EIP is a Draft; these tests -/// guard the relationships so the derived values stay correct when the final figures land. +/// Pins the EIP-8038 gas parameters to the final repriced values scheduled in Amsterdam by +/// glamsterdam-devnet-6, and guards the derivation relationships so the derived values stay +/// consistent with the base parameters. /// public class Eip8038ConstantsTests { [Test] - public void Base_parameters_match_their_current_placeholder_values() + public void Base_parameters_match_the_devnet6_repriced_values() { long coldAccountAccess = Eip8038Constants.ColdAccountAccess; long warmAccess = Eip8038Constants.WarmAccess; @@ -25,21 +25,24 @@ public void Base_parameters_match_their_current_placeholder_values() Assert.Multiple(() => { - Assert.That(coldAccountAccess, Is.EqualTo(2600)); + Assert.That(coldAccountAccess, Is.EqualTo(3000)); Assert.That(warmAccess, Is.EqualTo(100)); - Assert.That(coldStorageAccess, Is.EqualTo(2100)); - Assert.That(accountWrite, Is.EqualTo(6700)); - Assert.That(storageWrite, Is.EqualTo(2800)); + Assert.That(coldStorageAccess, Is.EqualTo(3000)); + Assert.That(accountWrite, Is.EqualTo(8000)); + Assert.That(storageWrite, Is.EqualTo(10000)); Assert.That(callStipend, Is.EqualTo(2300)); }); } [Test] - public void Account_write_is_call_value_minus_stipend() - { - long accountWrite = Eip8038Constants.AccountWrite; - Assert.That(accountWrite, Is.EqualTo(GasCostOf.CallValue - GasCostOf.CallStipend)); - } + public void Derived_parameters_match_the_devnet6_repriced_values() => + Assert.Multiple(() => + { + Assert.That(Eip8038Constants.CallValue, Is.EqualTo(10300)); + Assert.That(Eip8038Constants.CreateAccess, Is.EqualTo(11000)); + Assert.That(Eip8038Constants.StorageClearRefund, Is.EqualTo(12480)); + Assert.That(Eip8038Constants.PerAuthBaseRegular, Is.EqualTo(15816)); + }); [Test] public void Call_value_is_account_write_plus_stipend() From 2281bc07587902a4fa0fb34f523a98b0063b9127 Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Tue, 23 Jun 2026 18:21:16 +0100 Subject: [PATCH 20/27] fix: segregate standalone EIP-2780 gas model from EIP-8038 reprice Restore the standalone EIP-2780 two-tier cold-account model and intrinsic NEW_ACCOUNT / delegation-target pricing for the non-EIP-8038 path, and gate the EIP-8037 execution-time NEW_ACCOUNT and delegation-target charges on IsEip8037Enabled. Previously these execution charges fired on IsEip2780Enabled, double-charging the standalone path (which already prices them intrinsically). Amsterdam is unaffected (EIP-8037 always active). Co-Authored-By: Claude Opus 4.8 --- .../Nethermind.Evm.Test/Eip2780Tests.cs | 2 +- .../Eip8037BlockGasInclusionCheckTests.cs | 26 ++--- .../GasPolicy/EthereumGasPolicy.cs | 96 +++++++++++++++---- .../TransactionProcessor.cs | 27 +++--- 4 files changed, 104 insertions(+), 47 deletions(-) diff --git a/src/Nethermind/Nethermind.Evm.Test/Eip2780Tests.cs b/src/Nethermind/Nethermind.Evm.Test/Eip2780Tests.cs index f4bdf7af6d97..010653476103 100644 --- a/src/Nethermind/Nethermind.Evm.Test/Eip2780Tests.cs +++ b/src/Nethermind/Nethermind.Evm.Test/Eip2780Tests.cs @@ -29,7 +29,7 @@ public class Eip2780Tests private static long ChargeCallValue(bool isSelfCall, bool recipientEmpty) { EthereumGasPolicy gas = EthereumGasPolicy.FromLong(1_000_000); - Assert.That(EthereumGasPolicy.ConsumeCallValueTransferEip2780(ref gas, isSelfCall, recipientEmpty), Is.True); + Assert.That(EthereumGasPolicy.ConsumeCallValueTransferEip2780(ref gas, isSelfCall, recipientEmpty, Eip2780Spec), Is.True); return 1_000_000 - EthereumGasPolicy.GetRemainingGas(in gas); } diff --git a/src/Nethermind/Nethermind.Evm.Test/Eip8037BlockGasInclusionCheckTests.cs b/src/Nethermind/Nethermind.Evm.Test/Eip8037BlockGasInclusionCheckTests.cs index 23074d114ed8..9bd575ce4895 100644 --- a/src/Nethermind/Nethermind.Evm.Test/Eip8037BlockGasInclusionCheckTests.cs +++ b/src/Nethermind/Nethermind.Evm.Test/Eip8037BlockGasInclusionCheckTests.cs @@ -184,7 +184,6 @@ public void Calculate_block_regular_gas_keeps_valid_transcripts_non_negative() long stateGasSpill = random.Next(0, (int)Math.Min(spentRegular, int.MaxValue)); long stateGasSpillReclassified = random.Next(0, (int)Math.Min(stateGasSpill, int.MaxValue)); long remainingRegular = initialRegular - spentRegular; - long floorGas = random.Next(21_000, 200_000); long executionRegularGasUsed = initialRegular - remainingRegular - stateGasSpill + stateGasSpillReclassified; long blockRegularGas = Eip8037BlockGasInclusionCheck.CalculateBlockRegularGas( @@ -192,26 +191,27 @@ public void Calculate_block_regular_gas_keeps_valid_transcripts_non_negative() initialRegular, remainingRegular, stateGasSpill, - stateGasSpillReclassified, - floorGas); + stateGasSpillReclassified); Assert.That(executionRegularGasUsed, Is.GreaterThanOrEqualTo(0L)); - Assert.That(blockRegularGas, Is.EqualTo(Math.Max(intrinsicRegular + executionRegularGasUsed, floorGas))); + Assert.That(blockRegularGas, Is.EqualTo(intrinsicRegular + executionRegularGasUsed)); } } - [TestCase(300L, 100L, TestName = "Calculate_block_regular_gas_floor_clamps_low_regular_gas")] - [TestCase(0L, 0L, TestName = "Calculate_block_regular_gas_allows_negative_execution_intermediate")] - public void Calculate_block_regular_gas_clamps_to_floor(long initialRegular, long remainingRegular) + [Test] + public void Calculate_block_regular_gas_ignores_calldata_floor() { + // Regression: the EIP-7623/7976 calldata floor is a minimum charge on the sender + // (tx_gas_used / receipts) only and must NOT inflate the block's regular-gas dimension. + // Here the actual regular gas consumed (21_000) is far below any plausible floor, yet the + // block regular gas must report the consumed amount, not a floor-clamped value. long blockRegularGas = Eip8037BlockGasInclusionCheck.CalculateBlockRegularGas( intrinsicRegularGas: 21_000, - initialRegularGas: initialRegular, - remainingRegularGas: remainingRegular, - stateGasSpill: 200, - stateGasSpillReclassified: 0, - floorGas: 53_000); + initialRegularGas: 0, + remainingRegularGas: 0, + stateGasSpill: 0, + stateGasSpillReclassified: 0); - Assert.That(blockRegularGas, Is.EqualTo(53_000)); + Assert.That(blockRegularGas, Is.EqualTo(21_000)); } } diff --git a/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs b/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs index 42fca5104698..43961fc843cd 100644 --- a/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs +++ b/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs @@ -241,6 +241,7 @@ public static bool ConsumeAccountAccessGas(ref EthereumGasPolicy gas, [MethodImpl(MethodImplOptions.AggressiveInlining)] private static long ColdAccountAccessCost(IReleaseSpec spec, bool hasCode) => spec.IsEip8038Enabled ? Eip8038Constants.ColdAccountAccess + : spec.IsEip2780Enabled && !hasCode ? GasCostOf.ColdAccountAccessNoCodeEip2780 : GasCostOf.ColdAccountAccess; public static bool ConsumeStorageAccessGas(ref EthereumGasPolicy gas, @@ -552,22 +553,30 @@ private static long DataCost(Transaction tx, IReleaseSpec spec, long tokensInCal spec.GetBaseDataCost(tx) + tokensInCallData * GasCostOf.TxDataZero; /// - /// EIP-2780 recipient charge on top of TX_BASE_COST. For a non-self call: a flat - /// COLD_ACCOUNT_ACCESS touch, plus (for a value transfer) the EIP-7708 transfer log and - /// TX_VALUE_COST recipient balance write. For a CREATE: only the transfer log when value is sent - /// (the CREATE_ACCESS regular cost and NEW_ACCOUNT state cost are added by - /// /). + /// EIP-2780 recipient charge on top of TX_BASE_COST. Dispatches to the EIP-8038 (glamsterdam-devnet-6) + /// flat model when EIP-8038 is active, otherwise to the standalone EIP-2780 two-tier model. /// /// - /// Mirrors EELS calculate_intrinsic_cost: the recipient touch is a flat cold charge independent of - /// the recipient's existence or code, overriding EIP-2929's "all tx addresses are warm" rule; the recipient - /// stays pre-warmed for execution. A delegated recipient (EIP-7702) additionally touches its delegation - /// target — priced here from since the top-level frame only warms it. + /// Both models mirror their respective EELS calculate_intrinsic_cost: the recipient touch overrides + /// EIP-2929's "all tx addresses are warm" rule and the recipient stays pre-warmed for execution. See + /// and for the specifics. /// private static long Eip2780ExtraGas(Transaction tx, IReleaseSpec spec, IReadOnlyStateProvider? worldState) { if (!spec.IsEip2780Enabled) return 0; + return spec.IsEip8038Enabled + ? Eip8038IntrinsicRecipientGas(tx, spec, worldState) + : Eip2780StandaloneExtraGas(tx, spec, worldState); + } + // EIP-8038 (glamsterdam-devnet-6): the recipient touch is a flat cold charge independent of the + // recipient's existence or code, and a value transfer adds the EIP-7708 transfer log plus a fixed + // value-move cost. The new-account surcharge moves to a separate NEW_ACCOUNT state charge, and the + // EIP-7702 delegation-target touch is charged at execution time (against post-authorization state) + // rather than here; neither is priced in this method. worldState-independent because the EELS + // intrinsic cost does not consult state. + private static long Eip8038IntrinsicRecipientGas(Transaction tx, IReleaseSpec spec, IReadOnlyStateProvider? worldState) + { bool hasValue = !tx.Value.IsZero; if (tx.IsContractCreation) @@ -576,29 +585,74 @@ private static long Eip2780ExtraGas(Transaction tx, IReleaseSpec spec, IReadOnly // Self-transfers coalesce into the sender leaf write already priced into TX_BASE_COST. if (tx.SenderAddress == tx.To) return 0; - long coldAccess = spec.IsEip8038Enabled ? Eip8038Constants.ColdAccountAccess : GasCostOf.ColdAccountAccess; - long cost = coldAccess; + long cost = Eip8038Constants.ColdAccountAccess; if (hasValue) cost += GasCostOf.TransferLogEip2780 + GasCostOf.TxValueCostEip2780; - // EIP-7702: calling a delegated recipient also touches its delegation target. The EVM warms - // (does not gas-charge) that target for the top-level frame, so it is priced here instead. - if (spec.IsEip7702Enabled && worldState is not null && tx.To is not null - && ICodeInfoRepository.TryGetDelegatedAddress(worldState.GetCode(tx.To).AsSpan(), out Address? target)) + return cost; + } + + // Standalone EIP-2780 (pre-EIP-8038) intrinsic recipient cost: a two-tier cold touch keyed on + // whether the recipient carries code, plus a NEW_ACCOUNT surcharge and a STATE_UPDATE leaf write + // for value transfers, mirroring the EIP-2780 reference table. Requires + // to classify the recipient; superseded by under EIP-8038. + private static long Eip2780StandaloneExtraGas(Transaction tx, IReleaseSpec spec, IReadOnlyStateProvider? worldState) + { + bool isCreate = tx.IsContractCreation; + Address? to = tx.To; + bool hasValue = !tx.Value.IsZero; + bool senderIsRecipient = !isCreate && tx.SenderAddress == to; + + long cost = 0; + // EIP-7708 transfer log on the top-level value transfer; CREATE endows a distinct address. + if (hasValue && (isCreate || !senderIsRecipient)) + cost += GasCostOf.TransferLogEip2780; + + if (isCreate || to is null || worldState is null) return cost; + + bool isPrecompile = spec.IsPrecompile(to); + bool recipientDead = worldState.IsDeadAccount(to); + + // New-account surcharge: value transfer to a nonexistent, non-precompile recipient. + if (hasValue && !isPrecompile && recipientDead) + cost += GasCostOf.NewAccount; + + // Self-transfers coalesce into the sender leaf write already priced into TX_BASE_COST; + // precompiles are warm at tx start and charged zero. + if (!senderIsRecipient && !isPrecompile) { - cost += TxAccessListContains(tx, target) ? GasCostOf.WarmStateRead : coldAccess; + cost += RecipientTouchCost(spec, worldState, to, AccessListAddresses(tx)); + // The new-account surcharge already covers the recipient leaf write. + if (hasValue && !recipientDead) + cost += GasCostOf.StateUpdateEip2780; } return cost; } - private static bool TxAccessListContains(Transaction tx, Address address) + private static long RecipientTouchCost(IReleaseSpec spec, IReadOnlyStateProvider worldState, Address to, IReadOnlySet
? accessList) { - if (tx.AccessList is null) return false; - foreach ((Address entry, _) in tx.AccessList) + long cost = accessList?.Contains(to) == true + ? GasCostOf.WarmStateRead + : worldState.IsContract(to) ? GasCostOf.ColdAccountAccess : GasCostOf.ColdAccountAccessNoCodeEip2780; + + // EIP-7702: a delegated recipient also touches its delegation target (always carries code). + // The EVM warms (does not gas-charge) this target for the top-level frame, so this is the sole charge. + if (spec.IsEip7702Enabled && ICodeInfoRepository.TryGetDelegatedAddress(worldState.GetCode(to).AsSpan(), out Address? target)) + cost += accessList?.Contains(target) == true ? GasCostOf.WarmStateRead : GasCostOf.ColdAccountAccess; + + return cost; + } + + private static IReadOnlySet
? AccessListAddresses(Transaction tx) + { + if (tx.AccessList is null) return null; + HashSet
set = []; + foreach ((Address address, _) in tx.AccessList) { - if (entry == address) return true; + set.Add(address); } - return false; + return set; } + } diff --git a/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs b/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs index d1c85eeca3dd..411bfd94a3cd 100644 --- a/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs +++ b/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs @@ -299,15 +299,17 @@ private TransactionResult ExecuteEvmTransaction( if (!(result = BuildExecutionEnvironment(tx, spec, _codeInfoRepository, accessTracker, preloadedCodeInfo, preloadedDelegationAddress, out ExecutionEnvironment e))) return result; using ExecutionEnvironment env = e; - // EIP-2780/EIP-8037 top-frame charge: a top-level call whose recipient is an EIP-7702 - // delegation also touches the delegation target with a (flat) cold account access. The - // target is already pre-warmed for the frame, so this is the sole charge for it. If the - // gas cannot be covered the frame is exhausted and the EVM halts out of gas. - // The delegation is read from post-authorization state, so a delegation installed by this - // same transaction's authorization list is included (and one cleared by it is excluded). + // EIP-8037 top-frame charge: a top-level call whose recipient is an EIP-7702 delegation + // also touches the delegation target with a (flat) cold account access. The target is + // already pre-warmed for the frame, so this is the sole charge for it. If the gas cannot + // be covered the frame is exhausted and the EVM halts out of gas. The delegation is read + // from post-authorization state, so a delegation installed by this same transaction's + // authorization list is included (and one cleared by it is excluded). Gated on EIP-8037 + // (the 2-D state-gas model); the standalone EIP-2780 model instead prices the delegation + // target touch in the intrinsic cost, so charging here would double-charge. bool recipientIsDelegated = spec.IsEip7702Enabled && tx.To is not null && _codeInfoRepository.TryGetDelegation(tx.To, spec, out _); - if (spec.IsEip2780Enabled && !tx.IsContractCreation && recipientIsDelegated) + if (spec.IsEip8037Enabled && !tx.IsContractCreation && recipientIsDelegated) { long delegationCold = spec.IsEip8038Enabled ? Eip8038Constants.ColdAccountAccess : GasCostOf.ColdAccountAccess; if (!TGasPolicy.UpdateGas(ref gasAvailable, delegationCold)) @@ -403,12 +405,13 @@ private TransactionResult ExecuteSimpleTransfer( bool senderIsRecipient = tx.SenderAddress == recipient; bool isTracingActions = tracer.IsTracingActions; - // EIP-2780/EIP-8037 top-frame charge: a value transfer that materialises a new - // (dead, non-precompile) recipient pays NEW_ACCOUNT state gas, evaluated against - // pre-transfer state. If the (state) gas cannot be covered the frame is out of gas: - // no value moves and the sender forfeits all gas. + // EIP-8037 top-frame charge: a value transfer that materialises a new (dead, non-precompile) + // recipient pays NEW_ACCOUNT state gas, evaluated against pre-transfer state. If the (state) + // gas cannot be covered the frame is out of gas: no value moves and the sender forfeits all + // gas. Gated on EIP-8037 (the 2-D state-gas model); the standalone EIP-2780 model instead + // prices NEW_ACCOUNT in the intrinsic cost, so charging here would double-charge. bool newAccountOutOfGas = false; - if (spec.IsEip2780Enabled && hasValueTransfer && !senderIsRecipient + if (spec.IsEip8037Enabled && hasValueTransfer && !senderIsRecipient && !spec.IsPrecompile(recipient) && WorldState.IsDeadAccount(recipient)) { newAccountOutOfGas = !TGasPolicy.ConsumeStateGas(ref gasAvailable, TGasPolicy.GetNewAccountStateCost(in gasAvailable)); From 300de2bc638f12c3540f6e080ff7f52771b895f3 Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Tue, 23 Jun 2026 23:51:56 +0100 Subject: [PATCH 21/27] fix: EIP-8037 refund NEW_ACCOUNT on successful CREATE to a pre-existing account When CREATE/CREATE2 deploys to an address that already exists (e.g. a pre-funded account), EELS refunds the up-front NEW_ACCOUNT state gas on success, since code is added to an existing leaf rather than materialising a new account. Mirrors EELS amsterdam generic_create target_alive refund. Co-Authored-By: Claude Opus 4.8 --- src/Nethermind/Nethermind.Evm/VirtualMachine.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Nethermind/Nethermind.Evm/VirtualMachine.cs b/src/Nethermind/Nethermind.Evm/VirtualMachine.cs index 3dff3674a4de..47d9e501211e 100644 --- a/src/Nethermind/Nethermind.Evm/VirtualMachine.cs +++ b/src/Nethermind/Nethermind.Evm/VirtualMachine.cs @@ -289,6 +289,14 @@ public virtual TransactionSubstate ExecuteTransaction( if (isCreate) { IncorporateChildStateGasRefunds(previousState); + // EIP-8037: the NEW_ACCOUNT state gas charged up-front at the CREATE/CREATE2 + // opcode is refunded on successful deployment when the target account already + // existed (e.g. a pre-funded address), since the code is added to an existing + // account leaf rather than materialising a new one. + if (previousState.IsCreateOnPreExistingAccount) + { + CreditStateGasRefund(ref _currentState.Gas, TGasPolicy.GetCreateStateCost(in _currentState.Gas)); + } } } } From 431d3ea57fd8173f2e839f304b97ab9232ad0ef9 Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Wed, 24 Jun 2026 10:27:04 +0100 Subject: [PATCH 22/27] fix: EIP-8037 refund NEW_ACCOUNT on successful top-level CREATE tx to a pre-existing account A CREATE transaction charges NEW_ACCOUNT state gas in the intrinsic cost. EELS process_transaction refunds it when the tx errors or the created target was already alive (created_target_alive). Nethermind already refunds on revert/halt; this adds the success-with-pre-existing-target case, mirroring EELS. Co-Authored-By: Claude Opus 4.8 --- .../TransactionProcessing/TransactionProcessor.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs b/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs index 411bfd94a3cd..249e1d8def12 100644 --- a/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs +++ b/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs @@ -1251,6 +1251,11 @@ private int ExecuteEvmCall( Snapshot snapshot = WorldState.TakeSnapshot(); long floorGasLong = TGasPolicy.GetRemainingGas(gas.FloorGas); + // EIP-8037: capture whether the create target already existed (was alive) before deployment. + // On a successful create to a pre-existing account, the up-front NEW_ACCOUNT state gas charged + // in the intrinsic cost is refunded since no new account leaf is materialised. + bool createdTargetAlive = tx.IsContractCreation && !WorldState.IsDeadAccount(env.ExecutingAccount); + PayValue(tx, spec, opts); if (env.CodeInfo is not null) @@ -1359,7 +1364,7 @@ private int ExecuteEvmCall( } } - gasConsumed = Refund(tx, header, spec, opts, in substate, gasAvailable, VirtualMachine.TxExecutionContext.GasPrice, delegationRefunds, gas.FloorGas, gas.Standard, postIntrinsicStateReservoir); + gasConsumed = Refund(tx, header, spec, opts, in substate, gasAvailable, VirtualMachine.TxExecutionContext.GasPrice, delegationRefunds, gas.FloorGas, gas.Standard, postIntrinsicStateReservoir, createdTargetAlive); goto Complete; FailContractCreate: if (Logger.IsTrace) Logger.Trace("Restoring state from before transaction"); @@ -1601,11 +1606,14 @@ protected void TraceLogInvalidTx(Transaction transaction, string reason) } protected virtual GasConsumed Refund(Transaction tx, BlockHeader header, IReleaseSpec spec, ExecutionOptions opts, - in TransactionSubstate substate, in TGasPolicy unspentGas, in UInt256 gasPrice, int codeInsertRefunds, in TGasPolicy floorGas, in TGasPolicy intrinsicGasStandard, long postIntrinsicStateReservoir) + in TransactionSubstate substate, in TGasPolicy unspentGas, in UInt256 gasPrice, int codeInsertRefunds, in TGasPolicy floorGas, in TGasPolicy intrinsicGasStandard, long postIntrinsicStateReservoir, bool createdTargetAlive = false) { TGasPolicy gasAfterExecution = unspentGas; long stateGasFloor = TGasPolicy.GetStateReservoir(in intrinsicGasStandard); - if (substate.ShouldRevert && spec.IsEip8037Enabled) + // EIP-8037: refund the top-level create's up-front NEW_ACCOUNT state gas on a reverted create + // (state is rolled back) or on a successful create whose target already existed (was alive). + // Exceptional halts (substate.IsError) refund it separately via CompleteEip8037Halt below. + if (spec.IsEip8037Enabled && (substate.ShouldRevert || (!substate.IsError && createdTargetAlive))) { long refundedTopLevelCreateStateGas = CalculateTopLevelCreateIntrinsicStateRefund(tx, in intrinsicGasStandard); if (refundedTopLevelCreateStateGas > 0) From 90cb4f751ef1ef8943fa42e69d13d25d706ae72f Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Wed, 24 Jun 2026 10:42:19 +0100 Subject: [PATCH 23/27] fix: EIP-8037 refund NEW_ACCOUNT when value CALL cannot proceed A value transfer to a new account charges NEW_ACCOUNT state gas up-front. When the call cannot proceed (call depth exceeded or insufficient caller balance) no account is created, so EELS refunds it (generic_call / call insufficient-balance path). Mirror that. No-op pre-EIP-8037. Co-Authored-By: Claude Opus 4.8 --- .../Nethermind.Evm/Instructions/EvmInstructions.Call.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Call.cs b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Call.cs index 368a88e7c89f..a429f7c1a0d7 100644 --- a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Call.cs +++ b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Call.cs @@ -251,6 +251,11 @@ delegated is not null && // Refund the remaining gas to the caller. TGasPolicy.UpdateGasUp(ref gas, gasLimitUl); + // EIP-8037: a value transfer to a new account charges NEW_ACCOUNT state gas up-front; when the + // call cannot proceed (call depth exceeded or caller balance too low) no account is created, so + // refund it. No-op pre-EIP-8037 (CreditStateGasRefund self-gates), matching legacy semantics. + if (chargesNewAccount) + vm.CreditStateGasRefund(ref gas, TGasPolicy.GetNewAccountStateCost(in gas), trackSpillRefund: false); if (TTracingInst.IsActive) { vm.TxTracer.ReportGasUpdateForVmTrace(gasLimitUl, TGasPolicy.GetRemainingGas(in gas)); From 09afb9d72f272e57fbafa1cac478df1bd24deacd Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Wed, 24 Jun 2026 11:21:20 +0100 Subject: [PATCH 24/27] test: update intrinsic/EIP-8037/EIP-8038 unit tests to devnet-6 gas model Update stale placeholder expected values to the devnet-6 Amsterdam reprice (TX_BASE=12000, CREATE_ACCESS=11000, COLD_ACCOUNT_ACCESS=3000, repriced access list, PerAuthBaseRegular=15816, recipient cold+value intrinsic charge). Rewrite the code-insert refund test to assert the EIP-8038 regular-gas (ACCOUNT_WRITE) refund semantics instead of the obsolete state-gas credit. Co-Authored-By: Claude Opus 4.8 --- .../Nethermind.Evm.Test/Eip8037Tests.cs | 28 +++++++++++-------- .../Nethermind.Evm.Test/Eip8038Tests.cs | 5 ++-- .../IntrinsicGasCalculatorTests.cs | 15 +++++++--- 3 files changed, 30 insertions(+), 18 deletions(-) diff --git a/src/Nethermind/Nethermind.Evm.Test/Eip8037Tests.cs b/src/Nethermind/Nethermind.Evm.Test/Eip8037Tests.cs index 79d3437c745a..5de6064825f5 100644 --- a/src/Nethermind/Nethermind.Evm.Test/Eip8037Tests.cs +++ b/src/Nethermind/Nethermind.Evm.Test/Eip8037Tests.cs @@ -183,11 +183,14 @@ public void Amsterdam_access_list_floor_pricing_is_added_to_regular_and_floor_in IntrinsicGas splitIntrinsicGas = EthereumGasPolicy.CalculateIntrinsicGas(tx, Amsterdam.Instance); EthereumIntrinsicGas intrinsicGas = IntrinsicGasCalculator.Calculate(tx, Amsterdam.Instance); - long accessListBaseCost = GasCostOf.AccessAccountListEntry + 3 * GasCostOf.AccessStorageListEntry; + // Amsterdam (EIP-2780 + EIP-8038): TX_BASE=12000; access-list entries repriced to COLD_ACCOUNT_ACCESS / + // COLD_STORAGE_ACCESS; the value-bearing recipient touch adds COLD_ACCOUNT_ACCESS + TRANSFER_LOG + TX_VALUE. + long recipientRegular = Eip8038Constants.ColdAccountAccess + GasCostOf.TransferLogEip2780 + GasCostOf.TxValueCostEip2780; + long accessListBaseCost = Eip8038Constants.AccessListAddressCost + 3 * Eip8038Constants.AccessListStorageKeyCost; long accessListFloorTokens = (20L + 3 * 32L) * Amsterdam.Instance.GasCosts.TxDataNonZeroMultiplier; long accessListFloorCost = accessListFloorTokens * Amsterdam.Instance.GasCosts.TotalCostFloorPerToken; - long expectedRegular = GasCostOf.Transaction + accessListBaseCost + accessListFloorCost; - long expectedFloorGas = GasCostOf.Transaction + accessListFloorCost; + long expectedRegular = GasCostOf.TransactionEip2780 + recipientRegular + accessListBaseCost + accessListFloorCost; + long expectedFloorGas = GasCostOf.TransactionEip2780 + accessListFloorCost; Assert.That(splitIntrinsicGas.Standard.Value, Is.EqualTo(expectedRegular)); Assert.That(splitIntrinsicGas.Standard.StateReservoir, Is.Zero); @@ -270,22 +273,23 @@ public void State_refund_is_clamped_to_intrinsic_state_floor() } [Test] - public void Code_insert_state_refund_is_available_to_later_state_gas() + public void Code_insert_refund_credits_regular_gas_not_state_under_eip8038() { - const long intrinsicAuthState = GasCostOf.NewAccountState + GasCostOf.PerAuthBaseState; + // EIP-8038: the per-authorization code-insert (EIP-7702 existing-authority) refund returns the + // worst-case ACCOUNT_WRITE to the regular-gas refund counter and leaves the state-gas dimension + // untouched (the NEW_ACCOUNT / AUTH_BASE state refunds are applied separately, pre-execution). EthereumGasPolicy gas = new() { - Value = 2 * GasCostOf.SSetState - GasCostOf.NewAccountState, - StateGasUsed = intrinsicAuthState, + Value = 0, + StateReservoir = 0, + StateGasUsed = GasCostOf.PerAuthBaseState, }; - long regularRefund = EthereumGasPolicy.ApplyCodeInsertRefunds(ref gas, 1, Amsterdam.Instance, intrinsicAuthState); - Assert.That(EthereumGasPolicy.ConsumeStateGas(ref gas, GasCostOf.SSetState), Is.True); - Assert.That(EthereumGasPolicy.ConsumeStateGas(ref gas, GasCostOf.SSetState), Is.True); + long regularRefund = EthereumGasPolicy.ApplyCodeInsertRefunds(ref gas, 1, Amsterdam.Instance, stateGasFloor: 0); - Assert.That(regularRefund, Is.Zero); + Assert.That(regularRefund, Is.EqualTo(Eip8038Constants.AccountWrite)); Assert.That((gas.Value, gas.StateReservoir, gas.StateGasUsed, gas.StateGasSpill), - Is.EqualTo((0L, 0L, GasCostOf.PerAuthBaseState + 2 * GasCostOf.SSetState, 2 * GasCostOf.SSetState - GasCostOf.NewAccountState))); + Is.EqualTo((0L, 0L, GasCostOf.PerAuthBaseState, 0L))); } [Test] diff --git a/src/Nethermind/Nethermind.Evm.Test/Eip8038Tests.cs b/src/Nethermind/Nethermind.Evm.Test/Eip8038Tests.cs index b77338c00107..182c3a3b88b1 100644 --- a/src/Nethermind/Nethermind.Evm.Test/Eip8038Tests.cs +++ b/src/Nethermind/Nethermind.Evm.Test/Eip8038Tests.cs @@ -39,6 +39,7 @@ public class Eip8038Tests(bool eip8038Enabled) : VirtualMachineTestsBase private static readonly Address Target = TestItem.AddressC; private long ExtraWarmAccess => eip8038Enabled ? Eip8038Constants.WarmAccess : 0; + private long ColdAccountAccess => eip8038Enabled ? Eip8038Constants.ColdAccountAccess : GasCostOf.ColdAccountAccess; [SetUp] public override void Setup() @@ -72,7 +73,7 @@ public void ExtCodeSize_charges_extra_warm_access() Assert.That(result.StatusCode, Is.EqualTo(StatusCode.Success)); long expected = GasCostOf.Transaction + GasCostOf.VeryLow // PUSH20 target - + GasCostOf.ColdAccountAccess // cold EXTCODESIZE access + + ColdAccountAccess // cold EXTCODESIZE access (EIP-8038 repriced when enabled) + ExtraWarmAccess // EIP-8038 extra access + GasCostOf.Base; // POP AssertGas(result, expected); @@ -96,7 +97,7 @@ public void ExtCodeCopy_charges_extra_warm_access() Assert.That(result.StatusCode, Is.EqualTo(StatusCode.Success)); long expected = GasCostOf.Transaction + 4 * GasCostOf.VeryLow // three PUSH1 0x00 + PUSH20 target - + GasCostOf.ColdAccountAccess // cold EXTCODECOPY access + + ColdAccountAccess // cold EXTCODECOPY access (EIP-8038 repriced when enabled) + ExtraWarmAccess; // EIP-8038 extra access AssertGas(result, expected); } diff --git a/src/Nethermind/Nethermind.Evm.Test/IntrinsicGasCalculatorTests.cs b/src/Nethermind/Nethermind.Evm.Test/IntrinsicGasCalculatorTests.cs index e31ba2ba44cd..cf16fc32c823 100644 --- a/src/Nethermind/Nethermind.Evm.Test/IntrinsicGasCalculatorTests.cs +++ b/src/Nethermind/Nethermind.Evm.Test/IntrinsicGasCalculatorTests.cs @@ -235,7 +235,10 @@ public void Eip8037_policy_intrinsic_gas_splits_authorization_cost() .TestObject; IntrinsicGas intrinsicGas = EthereumGasPolicy.CalculateIntrinsicGas(tx, Amsterdam.Instance); - Assert.That(intrinsicGas.Standard.Value, Is.EqualTo(GasCostOf.Transaction + GasCostOf.PerAuthBaseRegular)); + // Amsterdam (EIP-2780 + EIP-8038): TX_BASE=12000; the value-bearing recipient touch adds + // COLD_ACCOUNT_ACCESS + TRANSFER_LOG + TX_VALUE; the authorization adds ACCOUNT_WRITE + base. + long recipientRegular = Eip8038Constants.ColdAccountAccess + GasCostOf.TransferLogEip2780 + GasCostOf.TxValueCostEip2780; + Assert.That(intrinsicGas.Standard.Value, Is.EqualTo(GasCostOf.TransactionEip2780 + recipientRegular + Eip8038Constants.PerAuthBaseRegular)); Assert.That(intrinsicGas.Standard.StateReservoir, Is.EqualTo(GasCostOf.NewAccountState + GasCostOf.PerAuthBaseState)); } @@ -247,7 +250,9 @@ public void Eip8037_nongeneric_intrinsic_gas_includes_state_gas_for_create() .TestObject; EthereumIntrinsicGas gas = IntrinsicGasCalculator.Calculate(tx, Amsterdam.Instance); - long expectedRegular = GasCostOf.Transaction + GasCostOf.CreateRegular; + // Amsterdam (EIP-2780 + EIP-8038): TX_BASE=12000, create regular = CREATE_ACCESS (+ TRANSFER_LOG + // for the value endowment), create state = NEW_ACCOUNT. + long expectedRegular = GasCostOf.TransactionEip2780 + Eip8038Constants.CreateAccess + GasCostOf.TransferLogEip2780; long expectedState = GasCostOf.CreateState; Assert.That(gas.Standard, Is.EqualTo(expectedRegular + expectedState)); Assert.That(gas.MinimalGas, Is.EqualTo(Math.Max(gas.Standard, gas.FloorGas))); @@ -261,7 +266,9 @@ public void Eip8037_nongeneric_intrinsic_gas_includes_state_gas_for_setcode() .TestObject; EthereumIntrinsicGas gas = IntrinsicGasCalculator.Calculate(tx, Amsterdam.Instance); - long expectedRegular = GasCostOf.Transaction + GasCostOf.PerAuthBaseRegular; + // Amsterdam (EIP-2780 + EIP-8038): TX_BASE=12000; value-bearing recipient touch + authorization. + long recipientRegular = Eip8038Constants.ColdAccountAccess + GasCostOf.TransferLogEip2780 + GasCostOf.TxValueCostEip2780; + long expectedRegular = GasCostOf.TransactionEip2780 + recipientRegular + Eip8038Constants.PerAuthBaseRegular; long expectedState = GasCostOf.NewAccountState + GasCostOf.PerAuthBaseState; Assert.That(gas.Standard, Is.EqualTo(expectedRegular + expectedState)); } @@ -275,7 +282,7 @@ public void Eip8037_nongeneric_minimal_gas_is_at_least_regular_plus_state() .TestObject; EthereumIntrinsicGas gas = IntrinsicGasCalculator.Calculate(tx, Amsterdam.Instance); - long regularPlusState = GasCostOf.Transaction + GasCostOf.CreateRegular + GasCostOf.CreateState; + long regularPlusState = GasCostOf.TransactionEip2780 + Eip8038Constants.CreateAccess + GasCostOf.TransferLogEip2780 + GasCostOf.CreateState; Assert.That(gas.MinimalGas, Is.GreaterThanOrEqualTo(regularPlusState)); } From 6240dd67b6198f1735cc6d06c547b09a11a233dc Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Wed, 24 Jun 2026 11:29:12 +0100 Subject: [PATCH 25/27] fix: EIP-8037 refund NEW_ACCOUNT when a value CALL frame reverts or halts A value transfer to a dead recipient charges NEW_ACCOUNT state gas up-front at the *CALL. EELS generic_call refunds it on child error (the account is not created when the frame reverts/halts). Add a NewAccountCharged frame flag and refund it on the revert (VirtualMachine main loop), exceptional-halt (HandleException) and precompile-failure (HandleFailure) frame-pop paths, mirroring the create refund. Co-Authored-By: Claude Opus 4.8 --- .../Instructions/EvmInstructions.Call.cs | 10 +++++++--- .../Nethermind.Evm/VirtualMachine.cs | 20 +++++++++++++++++++ src/Nethermind/Nethermind.Evm/VmState.cs | 14 ++++++++++++- 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Call.cs b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Call.cs index a429f7c1a0d7..4d5c38d6cd7f 100644 --- a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Call.cs +++ b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Call.cs @@ -289,7 +289,7 @@ delegated is not null && return EvmExceptionType.None; } - return CreateFullCallFrame(vm, ref gas, in dataOffset, dataLength, outputOffset, outputLength, codeInfo, target, caller, codeSource, env, in callValue, gasLimitUl); + return CreateFullCallFrame(vm, ref gas, in dataOffset, dataLength, outputOffset, outputLength, codeInfo, target, caller, codeSource, env, in callValue, gasLimitUl, chargesNewAccount); [MethodImpl(MethodImplOptions.NoInlining)] static EvmExceptionType CreateFullCallFrame( @@ -305,7 +305,8 @@ static EvmExceptionType CreateFullCallFrame( Address codeSource, ExecutionEnvironment env, in UInt256 callValue, - long gasLimitUl) + long gasLimitUl, + bool newAccountCharged) { IWorldState state = vm.WorldState; // Take a snapshot of the state for potential rollback. @@ -343,7 +344,10 @@ static EvmExceptionType CreateFullCallFrame( isCreateOnPreExistingAccount: false, env: callEnv, stateForAccessLists: in vm.VmState.AccessTracker, - snapshot: in snapshot); + snapshot: in snapshot, + // EIP-8037/EIP-8038: a value transfer to a dead recipient charged NEW_ACCOUNT state gas up-front; + // refunded on this frame's failure path (revert/halt) since the account is then not created. + newAccountCharged: newAccountCharged); return EvmExceptionType.None; } diff --git a/src/Nethermind/Nethermind.Evm/VirtualMachine.cs b/src/Nethermind/Nethermind.Evm/VirtualMachine.cs index 47d9e501211e..e9cf2ce43213 100644 --- a/src/Nethermind/Nethermind.Evm/VirtualMachine.cs +++ b/src/Nethermind/Nethermind.Evm/VirtualMachine.cs @@ -310,6 +310,12 @@ public virtual TransactionSubstate ExecuteTransaction( { CreditStateGasRefund(ref _currentState.Gas, TGasPolicy.GetCreateStateCost(in _currentState.Gas)); } + else if (previousState.NewAccountCharged) + { + // EIP-8037: the reverted *CALL did not create its (dead) recipient, so refund + // the NEW_ACCOUNT state gas the parent charged up-front for the value transfer. + CreditStateGasRefund(ref _currentState.Gas, TGasPolicy.GetNewAccountStateCost(in _currentState.Gas)); + } // Revert state changes for the previous call frame when a revert condition is signaled. HandleRevert(previousState, callResult, ref previousCallOutput); } @@ -599,6 +605,9 @@ protected TransactionSubstate HandleFailure(Exception failure, str _previousCallResult = StatusCode.FailureBytes; bool failedCreate = _currentState.ExecutionType.IsAnyCreate(); + // Captured before the pop: the failed *CALL did not create its (dead) recipient, so the parent + // refunds the NEW_ACCOUNT state gas it charged up-front for the value transfer (EIP-8037). + bool childNewAccountCharged = _currentState.NewAccountCharged; // Reset output destination and return data. _previousCallOutputDestination = UInt256.Zero; @@ -610,6 +619,10 @@ protected TransactionSubstate HandleFailure(Exception failure, str { CreditStateGasRefund(ref _currentState.Gas, TGasPolicy.GetCreateStateCost(in _currentState.Gas), trackSpillRefund: false); } + else if (childNewAccountCharged) + { + CreditStateGasRefund(ref _currentState.Gas, TGasPolicy.GetNewAccountStateCost(in _currentState.Gas), trackSpillRefund: false); + } shouldExit = false; return default; @@ -781,6 +794,9 @@ protected TransactionSubstate HandleException(scoped in CallResult callResult, s _previousCallResult = StatusCode.FailureBytes; bool failedCreate = _currentState.ExecutionType.IsAnyCreate(); + // Captured before the pop: the halted *CALL did not create its (dead) recipient, so the parent + // refunds the NEW_ACCOUNT state gas it charged up-front for the value transfer (EIP-8037). + bool childNewAccountCharged = _currentState.NewAccountCharged; // Reset output destination and clear return data. _previousCallOutputDestination = UInt256.Zero; @@ -792,6 +808,10 @@ protected TransactionSubstate HandleException(scoped in CallResult callResult, s { CreditStateGasRefund(ref _currentState.Gas, TGasPolicy.GetCreateStateCost(in _currentState.Gas), trackSpillRefund: false); } + else if (childNewAccountCharged) + { + CreditStateGasRefund(ref _currentState.Gas, TGasPolicy.GetNewAccountStateCost(in _currentState.Gas), trackSpillRefund: false); + } // Return null to indicate that the failure was handled and execution should continue in the parent frame. shouldExit = false; diff --git a/src/Nethermind/Nethermind.Evm/VmState.cs b/src/Nethermind/Nethermind.Evm/VmState.cs index 8e3f4ba45573..c329fe545bf9 100644 --- a/src/Nethermind/Nethermind.Evm/VmState.cs +++ b/src/Nethermind/Nethermind.Evm/VmState.cs @@ -43,6 +43,13 @@ public class VmState : IDisposable public bool IsContinuation { get; set; } // TODO: move to CallEnv public bool IsCreateOnPreExistingAccount { get; private set; } // TODO: move to CallEnv + /// + /// EIP-8037/EIP-8038: the parent *CALL charged NEW_ACCOUNT state gas up-front for a value + /// transfer materialising this (previously dead) recipient. If this frame errors or reverts the + /// account is not created, so the parent refunds that state gas on the frame's failure path. + /// + public bool NewAccountCharged { get; private set; } // TODO: move to CallEnv + private bool _isDisposed = true; private EvmPooledMemory _memory; @@ -69,6 +76,7 @@ public static VmState RentTopLevel( isTopLevel: true, isStatic: false, isCreateOnPreExistingAccount: false, + newAccountCharged: false, env: env, stateForAccessLists: accessedItems, snapshot: snapshot); @@ -88,7 +96,8 @@ public static VmState RentFrame( ExecutionEnvironment env, in StackAccessTracker stateForAccessLists, in Snapshot snapshot, - bool isTopLevel = false) + bool isTopLevel = false, + bool newAccountCharged = false) { VmState state = Rent(); state.Initialize( @@ -99,6 +108,7 @@ public static VmState RentFrame( isTopLevel: isTopLevel, isStatic: isStatic, isCreateOnPreExistingAccount: isCreateOnPreExistingAccount, + newAccountCharged: newAccountCharged, env: env, stateForAccessLists: stateForAccessLists, snapshot: snapshot); @@ -117,6 +127,7 @@ private void Initialize( bool isTopLevel, bool isStatic, bool isCreateOnPreExistingAccount, + bool newAccountCharged, ExecutionEnvironment env, in StackAccessTracker stateForAccessLists, in Snapshot snapshot) @@ -146,6 +157,7 @@ private void Initialize( IsStatic = isStatic; IsContinuation = false; IsCreateOnPreExistingAccount = isCreateOnPreExistingAccount; + NewAccountCharged = newAccountCharged; if (!_isDisposed) { From 682818bc4ee6bac5e0833e4e1590149e6120f426 Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Wed, 24 Jun 2026 13:32:50 +0100 Subject: [PATCH 26/27] fix: EIP-7928 avoid spurious BAL read from IsContract in account-access gas The hasCode argument to ConsumeAccountAccessGas evaluated WorldState.IsContract(addr) to select the EIP-2780 two-tier cold cost, but IsContract records a BAL account read. Under EIP-8038 the cold cost is flat (hasCode unused), so this read is spurious and wrongly lands the target in the block access list when the opcode runs out of gas before actually accessing it. Short-circuit IsContract when EIP-8038 is active. Co-Authored-By: Claude Opus 4.8 --- .../Nethermind.Evm/Instructions/EvmInstructions.Call.cs | 2 +- .../Nethermind.Evm/Instructions/EvmInstructions.CodeCopy.cs | 4 ++-- .../Instructions/EvmInstructions.ControlFlow.cs | 2 +- .../Instructions/EvmInstructions.Environment.cs | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Call.cs b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Call.cs index 4d5c38d6cd7f..4633f2427944 100644 --- a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Call.cs +++ b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Call.cs @@ -162,7 +162,7 @@ public static EvmExceptionType InstructionCall( // Charge gas for account access (considering hot/cold storage costs). if (!TGasPolicy.ConsumeAccountAccessGas(ref gas, spec, in vm.VmState.AccessTracker, vm.TxTracer.IsTracingAccess, address, - hasCode: !spec.IsEip2780Enabled || vm.WorldState.IsContract(address))) + hasCode: !spec.IsEip2780Enabled || spec.IsEip8038Enabled || vm.WorldState.IsContract(address))) goto OutOfGas; // EIP-8038 charges an extra warm access for the second DB read EXTCODECOPY performs. @@ -248,7 +248,7 @@ public static EvmExceptionType InstructionExtCodeSize( // Charge gas for accessing the account's state. if (!TGasPolicy.ConsumeAccountAccessGas(ref gas, spec, in vm.VmState.AccessTracker, vm.TxTracer.IsTracingAccess, address, - hasCode: !spec.IsEip2780Enabled || vm.WorldState.IsContract(address))) + hasCode: !spec.IsEip2780Enabled || spec.IsEip8038Enabled || vm.WorldState.IsContract(address))) goto OutOfGas; // EIP-8038 charges an extra warm access for the second DB read EXTCODESIZE performs. diff --git a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.ControlFlow.cs b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.ControlFlow.cs index 1585d4e7ca58..8bf938864cfb 100644 --- a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.ControlFlow.cs +++ b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.ControlFlow.cs @@ -225,7 +225,7 @@ public static EvmExceptionType InstructionSelfDestruct(Virt // Charge gas for account access. If insufficient gas remains, abort. if (!TGasPolicy.ConsumeAccountAccessGas(ref gas, spec, in vm.VmState.AccessTracker, vm.TxTracer.IsTracingAccess, address, - hasCode: !spec.IsEip2780Enabled || vm.WorldState.IsContract(address))) goto OutOfGas; + hasCode: !spec.IsEip2780Enabled || spec.IsEip8038Enabled || vm.WorldState.IsContract(address))) goto OutOfGas; UInt256 result = vm.WorldState.GetBalance(address); return stack.PushUInt256(in result); @@ -612,7 +612,7 @@ public static EvmExceptionType InstructionExtCodeHash( if (address is null) goto StackUnderflow; // Check if enough gas for account access and charge accordingly. if (!TGasPolicy.ConsumeAccountAccessGas(ref gas, spec, in vm.VmState.AccessTracker, vm.TxTracer.IsTracingAccess, address, - hasCode: !spec.IsEip2780Enabled || vm.WorldState.IsContract(address))) goto OutOfGas; + hasCode: !spec.IsEip2780Enabled || spec.IsEip8038Enabled || vm.WorldState.IsContract(address))) goto OutOfGas; IWorldState state = vm.WorldState; // For dead accounts, the specification requires pushing zero. From fed5aa68e9efb442db4e7e74621d9ac4a174a20f Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Wed, 24 Jun 2026 14:15:58 +0100 Subject: [PATCH 27/27] fix: EIP-8037 charge top-level NEW_ACCOUNT for value transfer on the EVM path The top-frame NEW_ACCOUNT state-gas charge for a value transfer materialising a new (dead, non-precompile) recipient existed only in the simple-transfer fast path. EELS charges it for every non-create top-level frame, so transactions carrying code, calldata, or an authorization list (which take the EVM path) were missing it, leaving their block state-gas understated. Mirror the charge in ExecuteEvmTransaction. Co-Authored-By: Claude Opus 4.8 --- .../TransactionProcessing/TransactionProcessor.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs b/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs index 249e1d8def12..4642104dd72f 100644 --- a/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs +++ b/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs @@ -299,6 +299,18 @@ private TransactionResult ExecuteEvmTransaction( if (!(result = BuildExecutionEnvironment(tx, spec, _codeInfoRepository, accessTracker, preloadedCodeInfo, preloadedDelegationAddress, out ExecutionEnvironment e))) return result; using ExecutionEnvironment env = e; + // EIP-8037 top-frame charge: a value transfer materialising a new (dead, non-precompile) + // recipient pays NEW_ACCOUNT state gas, evaluated against pre-transfer state. This mirrors the + // ExecuteSimpleTransfer charge for the EVM path (transactions carrying code, calldata, or an + // authorization list, which bypass the simple-transfer fast path). + if (spec.IsEip8037Enabled && !tx.IsContractCreation && !tx.ValueRef.IsZero + && tx.To is not null && tx.SenderAddress != tx.To + && !spec.IsPrecompile(tx.To) && WorldState.IsDeadAccount(tx.To)) + { + if (!TGasPolicy.ConsumeStateGas(ref gasAvailable, TGasPolicy.GetNewAccountStateCost(in gasAvailable))) + TGasPolicy.Consume(ref gasAvailable, TGasPolicy.GetRemainingGas(in gasAvailable)); + } + // EIP-8037 top-frame charge: a top-level call whose recipient is an EIP-7702 delegation // also touches the delegation target with a (flat) cold account access. The target is // already pre-warmed for the frame, so this is the sole charge for it. If the gas cannot