diff --git a/src/Nethermind/Nethermind.Core.Test/Eip8038ConstantsTests.cs b/src/Nethermind/Nethermind.Core.Test/Eip8038ConstantsTests.cs
index 8ae66c23c39..eab65c91d80 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()
diff --git a/src/Nethermind/Nethermind.Core/CodeSizeConstants.cs b/src/Nethermind/Nethermind.Core/CodeSizeConstants.cs
index bcd2406faf9..8d5a4a06e91 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
}
diff --git a/src/Nethermind/Nethermind.Core/Eip8038Constants.cs b/src/Nethermind/Nethermind.Core/Eip8038Constants.cs
index c85d78caf55..e0172aa96a9 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,24 @@ 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
+
+ ///
+ /// 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.Core/GasCostOf.cs b/src/Nethermind/Nethermind.Core/GasCostOf.cs
index 4e82ca45de2..9fb62fb286b 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.Core/Specs/SpecGasCosts.cs b/src/Nethermind/Nethermind.Core/Specs/SpecGasCosts.cs
index 124848462b4..9b8950d7d65 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;
@@ -108,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.Test/Eip2780Tests.cs b/src/Nethermind/Nethermind.Evm.Test/Eip2780Tests.cs
index f4bdf7af6d9..01065347610 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 23074d114ed..9bd575ce489 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.Test/Eip8037Tests.cs b/src/Nethermind/Nethermind.Evm.Test/Eip8037Tests.cs
index 79d3437c745..5de6064825f 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 b77338c0010..182c3a3b88b 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 e31ba2ba44c..cf16fc32c82 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));
}
diff --git a/src/Nethermind/Nethermind.Evm/GasPolicy/Eip8037BlockGasInclusionCheck.cs b/src/Nethermind/Nethermind.Evm/GasPolicy/Eip8037BlockGasInclusionCheck.cs
index 3412bd2fc0b..07aaafc0c6b 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/GasPolicy/EthereumGasPolicy.cs b/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs
index bafc87a46a4..43961fc843c 100644
--- a/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs
+++ b/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs
@@ -236,12 +236,12 @@ 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
+ : spec.IsEip2780Enabled && !hasCode ? GasCostOf.ColdAccountAccessNoCodeEip2780
: GasCostOf.ColdAccountAccess;
public static bool ConsumeStorageAccessGas(ref EthereumGasPolicy gas,
@@ -260,7 +260,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;
}
@@ -303,12 +305,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),
};
}
@@ -395,32 +401,36 @@ 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)
=> 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;
@@ -529,7 +539,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,18 +553,51 @@ 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. Dispatches to the EIP-8038 (glamsterdam-devnet-6)
+ /// flat model when EIP-8038 is active, otherwise to the standalone EIP-2780 two-tier model.
///
///
- /// 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.
+ /// 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)
+ return hasValue ? GasCostOf.TransferLogEip2780 : 0;
+
+ // Self-transfers coalesce into the sender leaf write already priced into TX_BASE_COST.
+ if (tx.SenderAddress == tx.To) return 0;
+
+ long cost = Eip8038Constants.ColdAccountAccess;
+ if (hasValue)
+ cost += GasCostOf.TransferLogEip2780 + GasCostOf.TxValueCostEip2780;
+ 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;
@@ -609,4 +654,5 @@ private static long RecipientTouchCost(IReleaseSpec spec, IReadOnlyStateProvider
}
return set;
}
+
}
diff --git a/src/Nethermind/Nethermind.Evm/GasPolicy/IGasPolicy.cs b/src/Nethermind/Nethermind.Evm/GasPolicy/IGasPolicy.cs
index 4574bb88be0..a9c133ba441 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);
@@ -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);
diff --git a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Call.cs b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Call.cs
index 18a31f275e3..4633f242794 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);
@@ -247,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));
@@ -280,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(
@@ -296,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.
@@ -334,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/Instructions/EvmInstructions.CodeCopy.cs b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.CodeCopy.cs
index e3780702b6e..6efcb4c6f11 100644
--- a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.CodeCopy.cs
+++ b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.CodeCopy.cs
@@ -169,7 +169,7 @@ public static EvmExceptionType InstructionExtCodeCopy(
// 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 e9196fd7fdd..8bf938864cf 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 !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;
diff --git a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Create.cs b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Create.cs
index 948995bae57..4ff65e86d0c 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(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.
diff --git a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Storage.cs b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Storage.cs
index 2d34bf5a34f..96d7e7a0a59 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(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);
@@ -388,16 +417,31 @@ private TransactionResult ExecuteSimpleTransfer(
bool senderIsRecipient = tx.SenderAddress == recipient;
bool isTracingActions = tracer.IsTracingActions;
+ // 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.IsEip8037Enabled && 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 +473,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)
{
@@ -625,7 +669,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;
}
@@ -704,6 +751,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
{
@@ -1209,6 +1263,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)
@@ -1317,7 +1376,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");
@@ -1559,11 +1618,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)
@@ -1698,8 +1760,7 @@ private static long Calculate8037BlockRegularGas(
initialRegularGas,
remainingRegularGas,
stateGasSpill,
- stateGasSpillReclassified,
- floorGas);
+ stateGasSpillReclassified);
}
protected virtual void PayRefund(Transaction tx, UInt256 refundAmount, IReleaseSpec spec)
diff --git a/src/Nethermind/Nethermind.Evm/VirtualMachine.cs b/src/Nethermind/Nethermind.Evm/VirtualMachine.cs
index 3dff3674a4d..e9cf2ce4321 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));
+ }
}
}
}
@@ -302,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);
}
@@ -591,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;
@@ -602,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;
@@ -773,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;
@@ -784,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 8e3f4ba4557..c329fe545bf 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)
{
diff --git a/src/Nethermind/Nethermind.Specs/Forks/25_Amsterdam.cs b/src/Nethermind/Nethermind.Specs/Forks/25_Amsterdam.cs
index 424b474ba53..c014b7073f7 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,8 @@ 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.IsEip8038Enabled = true;
+ spec.IsEip8246Enabled = true;
spec.EngineApiNewPayloadVersion = EngineApiVersions.NewPayload.V5;
spec.EngineApiGetPayloadVersion = EngineApiVersions.GetPayload.V6;
spec.EngineApiForkchoiceVersion = EngineApiVersions.Fcu.V4;