diff --git a/core/eip8038_test.go b/core/eip8038_test.go
new file mode 100644
index 0000000000..d8977c6dd2
--- /dev/null
+++ b/core/eip8038_test.go
@@ -0,0 +1,110 @@
+// Copyright 2026 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+// EIP-8038 authorization accounting tests. The per-authorization intrinsic gas
+// pre-charges ACCOUNT_WRITE (regular) on top of REGULAR_PER_AUTH_BASE_COST.
+// applyAuthorization refunds that ACCOUNT_WRITE to the refund counter in exactly
+// the cases where no new account leaf is written: an invalid authorization, or
+// an authority whose account already exists. These white-box tests invoke
+// applyAuthorization directly and read the raw refund counter, so they observe
+// the refund before the EIP-3529 cap is applied.
+
+package core
+
+import (
+ "math/big"
+ "testing"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/state"
+ "github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/core/vm"
+ "github.com/ethereum/go-ethereum/crypto"
+ "github.com/ethereum/go-ethereum/params"
+ "github.com/holiman/uint256"
+)
+
+// newAuthTestTransition builds a minimal stateTransition with a state reservoir,
+// suitable for calling applyAuthorization directly.
+func newAuthTestTransition(sdb *state.StateDB) *stateTransition {
+ st := newStateTransition(amsterdamCoreEVM(sdb), &Message{}, NewGasPool(30_000_000))
+ st.gasRemaining = vm.NewGasBudget(0, 1_000_000) // reservoir for state-gas refills
+ return st
+}
+
+// A net-new delegation on a fresh authority writes a new account leaf, so the
+// intrinsic ACCOUNT_WRITE stands (no refund).
+func TestAuthAccountWriteNetNewNoRefund(t *testing.T) {
+ auth, _ := signAuth(t, authKeyA, delegate8037, 0)
+ st := newAuthTestTransition(mkState(senderAlloc(nil)))
+ if err := st.applyAuthorization(rules8037, &auth, map[common.Address]bool{}); err != nil {
+ t.Fatal(err)
+ }
+ if got := st.state.GetRefund(); got != 0 {
+ t.Fatalf("refund = %d, want 0 (net-new account write)", got)
+ }
+}
+
+// A pre-existing authority writes no new account leaf, so the intrinsic
+// ACCOUNT_WRITE is refunded.
+func TestAuthAccountWriteExistsRefund(t *testing.T) {
+ auth, authority := signAuth(t, authKeyA, delegate8037, 0)
+ st := newAuthTestTransition(mkState(senderAlloc(types.GenesisAlloc{authority: {Balance: big.NewInt(1)}})))
+ if err := st.applyAuthorization(rules8037, &auth, map[common.Address]bool{}); err != nil {
+ t.Fatal(err)
+ }
+ if got := st.state.GetRefund(); got != params.AccountWriteAmsterdam {
+ t.Fatalf("refund = %d, want %d (account already exists)", got, params.AccountWriteAmsterdam)
+ }
+}
+
+// An invalid authorization is skipped without writing any account leaf, so its
+// intrinsic ACCOUNT_WRITE is refunded.
+func TestAuthAccountWriteInvalidRefund(t *testing.T) {
+ k, _ := crypto.HexToECDSA(authKeyA)
+ bad, _ := types.SignSetCode(k, types.SetCodeAuthorization{
+ ChainID: *uint256.NewInt(999), Address: delegate8037, Nonce: 0, // wrong chain id
+ })
+ st := newAuthTestTransition(mkState(senderAlloc(nil)))
+ if err := st.applyAuthorization(rules8037, &bad, map[common.Address]bool{}); err == nil {
+ t.Fatal("expected invalid-authorization error")
+ }
+ if got := st.state.GetRefund(); got != params.AccountWriteAmsterdam {
+ t.Fatalf("refund = %d, want %d (invalid authorization)", got, params.AccountWriteAmsterdam)
+ }
+}
+
+// The same authority across two authorizations writes its account leaf only
+// once: the first auth pays ACCOUNT_WRITE, the second (which now sees the
+// account as existing) is refunded.
+func TestAuthAccountWriteDuplicateOnce(t *testing.T) {
+ a0, _ := signAuth(t, authKeyA, delegate8037, 0)
+ a1, _ := signAuth(t, authKeyA, delegate8037, 1)
+ st := newAuthTestTransition(mkState(senderAlloc(nil)))
+ delegates := map[common.Address]bool{}
+ if err := st.applyAuthorization(rules8037, &a0, delegates); err != nil {
+ t.Fatal(err)
+ }
+ if got := st.state.GetRefund(); got != 0 {
+ t.Fatalf("refund after first auth = %d, want 0", got)
+ }
+ if err := st.applyAuthorization(rules8037, &a1, delegates); err != nil {
+ t.Fatal(err)
+ }
+ if got := st.state.GetRefund(); got != params.AccountWriteAmsterdam {
+ t.Fatalf("refund after duplicate auth = %d, want %d", got, params.AccountWriteAmsterdam)
+ }
+}
diff --git a/core/state_transition.go b/core/state_transition.go
index 2923e110c7..877ed0638f 100644
--- a/core/state_transition.go
+++ b/core/state_transition.go
@@ -87,7 +87,7 @@ func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.Set
// Add gas for authorizations
if authList != nil {
if rules.IsAmsterdam {
- gas.RegularGas += uint64(len(authList)) * params.TxAuthTupleRegularGas
+ gas.RegularGas += uint64(len(authList)) * (params.AccountWriteAmsterdam + params.RegularPerAuthBaseCost)
gas.StateGas += uint64(len(authList)) * (params.AuthorizationCreationSize + params.AccountCreationSize) * costPerStateByte
} else {
gas.RegularGas += uint64(len(authList)) * params.CallNewAccountGas
@@ -126,14 +126,22 @@ func IntrinsicGas(data []byte, accessList types.AccessList, authList []types.Set
if accessList != nil {
addresses := uint64(len(accessList))
storageKeys := uint64(accessList.StorageKeys())
- if (math.MaxUint64-gas.RegularGas)/params.TxAccessListAddressGas < addresses {
+
+ // Amsterdam re-prices the per-entry access-list cost
+ addressCost := params.TxAccessListAddressGas
+ storageKeyCost := params.TxAccessListStorageKeyGas
+ if rules.IsAmsterdam {
+ addressCost = params.TxAccessListAddressGasAmsterdam
+ storageKeyCost = params.TxAccessListStorageKeyGasAmsterdam
+ }
+ if (math.MaxUint64-gas.RegularGas)/addressCost < addresses {
return vm.GasCosts{}, ErrGasUintOverflow
}
- gas.RegularGas += addresses * params.TxAccessListAddressGas
- if (math.MaxUint64-gas.RegularGas)/params.TxAccessListStorageKeyGas < storageKeys {
+ gas.RegularGas += addresses * addressCost
+ if (math.MaxUint64-gas.RegularGas)/storageKeyCost < storageKeys {
return vm.GasCosts{}, ErrGasUintOverflow
}
- gas.RegularGas += storageKeys * params.TxAccessListStorageKeyGas
+ gas.RegularGas += storageKeys * storageKeyCost
// EIP-7981: access list data is charged in addition to the base charge.
if rules.IsAmsterdam {
@@ -955,20 +963,13 @@ func (st *stateTransition) validateAuthorization(auth *types.SetCodeAuthorizatio
}
// applyAuthorization applies an EIP-7702 code delegation to the state and,
-// under EIP-8037, reconciles the per-authorization intrinsic state-gas pre-
-// charge so that, per authority:
-//
-// - the account portion (AccountCreationSize × CPSB) is charged at most
-// once, and only when the account did not exist before the tx
-//
-// - the delegation-indicator portion (AuthorizationCreationSize × CPSB) is
-// charged at most once, and only when the authority ends the tx delegated
-// having started it undelegated.
+// adjust the pre-charged intrinsic cost accordingly.
func (st *stateTransition) applyAuthorization(rules params.Rules, auth *types.SetCodeAuthorization, delegates map[common.Address]bool) error {
authority, err := st.validateAuthorization(auth)
if err != nil {
if rules.IsAmsterdam {
st.gasRemaining.RefundState((params.AccountCreationSize + params.AuthorizationCreationSize) * st.evm.Context.CostPerStateByte)
+ st.state.AddRefund(params.AccountWriteAmsterdam)
}
return err
}
@@ -981,6 +982,7 @@ func (st *stateTransition) applyAuthorization(rules params.Rules, auth *types.Se
} else {
if st.state.Exist(authority) {
st.gasRemaining.RefundState(params.AccountCreationSize * st.evm.Context.CostPerStateByte)
+ st.state.AddRefund(params.AccountWriteAmsterdam)
}
authBase := params.AuthorizationCreationSize * st.evm.Context.CostPerStateByte
diff --git a/core/state_transition_test.go b/core/state_transition_test.go
index ace43aa13a..60edad52e5 100644
--- a/core/state_transition_test.go
+++ b/core/state_transition_test.go
@@ -243,7 +243,7 @@ func TestIntrinsicGas(t *testing.T) {
// EIP-2780: zero-value call base is TxBaseCost + ColdAccountAccess
// (15,000). Plus base access-list charge + EIP-7981 extra.
want: vm.GasCosts{RegularGas: params.TxBaseCost2780 + params.ColdAccountAccess2780 +
- 2*params.TxAccessListAddressGas + 3*params.TxAccessListStorageKeyGas +
+ 2*params.TxAccessListAddressGasAmsterdam + 3*params.TxAccessListStorageKeyGasAmsterdam +
2*amsterdamAddressCost + 3*amsterdamStorageKeyCost},
},
{
@@ -298,7 +298,7 @@ func TestIntrinsicGas(t *testing.T) {
want: vm.GasCosts{
RegularGas: params.TxBaseCost2780 + params.CreateAccess2780 +
32*params.TxDataNonZeroGasEIP2028 + 1*params.InitCodeWordGas +
- 1*params.TxAccessListAddressGas + 1*params.TxAccessListStorageKeyGas +
+ 1*params.TxAccessListAddressGasAmsterdam + 1*params.TxAccessListStorageKeyGasAmsterdam +
1*amsterdamAddressCost + 1*amsterdamStorageKeyCost,
StateGas: params.AccountCreationSize * params.CostPerStateByte,
},
@@ -314,15 +314,16 @@ func TestIntrinsicGas(t *testing.T) {
},
isEIP2028: true,
isAmsterdam: true,
- // EIP-8037 splits the auth-tuple charge into regular + state gas:
- // regular: TxAuthTupleRegularGas (7500) per auth
+ // EIP-8037 splits the auth-tuple charge into regular + state gas, with
+ // the values finalized by EIP-8038:
+ // regular: ACCOUNT_WRITE (8,000) + REGULAR_PER_AUTH_BASE_COST (7,500) per auth
// state: (AuthorizationCreationSize + AccountCreationSize) * CostPerStateByte per auth
want: vm.GasCosts{
RegularGas: params.TxBaseCost2780 + params.ColdAccountAccess2780 +
100*params.TxDataNonZeroGasEIP2028 +
- 1*params.TxAccessListAddressGas + 1*params.TxAccessListStorageKeyGas +
+ 1*params.TxAccessListAddressGasAmsterdam + 1*params.TxAccessListStorageKeyGasAmsterdam +
1*amsterdamAddressCost + 1*amsterdamStorageKeyCost +
- 1*params.TxAuthTupleRegularGas,
+ 1*(params.AccountWriteAmsterdam+params.RegularPerAuthBaseCost),
StateGas: 1 * (params.AuthorizationCreationSize + params.AccountCreationSize) * params.CostPerStateByte,
},
},
diff --git a/core/vm/eip8037_test.go b/core/vm/eip8037_test.go
index 07f0099207..0eafc3e4af 100644
--- a/core/vm/eip8037_test.go
+++ b/core/vm/eip8037_test.go
@@ -190,16 +190,25 @@ func TestSStoreChargedAtOpcodeEnd(t *testing.T) {
}
// The SSTORE reentrancy sentry checks gas_left only; the reservoir is excluded.
-// Uses a noop write (1->1->1) so the sentry is the sole gate.
+// Uses a noop write (1->1->1): the two PUSH1s cost 6, leaving gas_left at the
+// sentry (2300) for a 2306 budget. Under EIP-8038 the cold-slot access that
+// follows a cleared sentry costs COLD_STORAGE_ACCESS (3000).
func TestSStoreStipendExcludesReservoir(t *testing.T) {
- // regular at the sentry, huge reservoir: must still fail.
+ // regular at the sentry, huge reservoir: must still fail, proving the
+ // reservoir does not count toward the sentry.
if _, _, err := run8037(t, sstore(0, 1), NewGasBudget(2306, math.MaxUint64/2), new(uint256.Int), setSlot(0, 1)); err == nil {
t.Fatal("expected sentry failure with regular gas at the limit")
}
- // one more regular gas clears the sentry.
- if _, _, err := run8037(t, sstore(0, 1), NewGasBudget(2307, math.MaxUint64/2), new(uint256.Int), setSlot(0, 1)); err != nil {
+ // Enough regular gas to clear the sentry and pay the cold-slot access
+ // (6 for the PUSH1s + COLD_STORAGE_ACCESS) succeeds with a huge reservoir.
+ regular := 6 + params.ColdStorageAccessAmsterdam
+ if _, _, err := run8037(t, sstore(0, 1), NewGasBudget(regular, math.MaxUint64/2), new(uint256.Int), setSlot(0, 1)); err != nil {
t.Fatalf("unexpected failure above sentry: %v", err)
}
+ // One gas short of the cold-slot access still fails (now on OOG, not sentry).
+ if _, _, err := run8037(t, sstore(0, 1), NewGasBudget(regular-1, math.MaxUint64/2), new(uint256.Int), setSlot(0, 1)); err == nil {
+ t.Fatal("expected OOG when regular gas cannot cover cold-slot access")
+ }
}
// ---- CALL / CREATE bytecode helpers ----
diff --git a/core/vm/eip8038_test.go b/core/vm/eip8038_test.go
new file mode 100644
index 0000000000..c08c54fb2c
--- /dev/null
+++ b/core/vm/eip8038_test.go
@@ -0,0 +1,237 @@
+// Copyright 2026 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+// Opcode-level tests for EIP-8038 (state-access gas cost update). They reuse the
+// Amsterdam harness from eip8037_test.go and assert the re-priced regular-gas,
+// state-gas and refund-counter accounting.
+
+package vm
+
+import (
+ "testing"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/state"
+ "github.com/ethereum/go-ethereum/core/tracing"
+ "github.com/ethereum/go-ethereum/core/types"
+ "github.com/ethereum/go-ethereum/params"
+ "github.com/holiman/uint256"
+)
+
+// run8038 executes code at a contract address under the Amsterdam ruleset and
+// returns the resulting budget together with the transaction's refund counter.
+func run8038(t *testing.T, code []byte, gas GasBudget, value *uint256.Int, setup func(*state.StateDB, common.Address)) (GasBudget, uint64, error) {
+ t.Helper()
+ self := common.BytesToAddress([]byte("self"))
+ statedb, _ := state.New(types.EmptyRootHash, state.NewDatabaseForTesting())
+ statedb.CreateAccount(self)
+ statedb.SetCode(self, code, tracing.CodeChangeUnspecified)
+ if setup != nil {
+ setup(statedb, self)
+ }
+ statedb.Finalise(true)
+ _, result, err := amsterdam8037EVM(statedb).Call(common.Address{}, self, nil, gas, value)
+ return result, statedb.GetRefund(), err
+}
+
+// TestEIP8038SStore exercises SSTORE under Amsterdam (EIP-8037 + EIP-8038),
+// asserting the two-dimensional charge (regular + state gas) and the net refund
+// counter. It covers single stores in isolation (the EIP-8038 cases-table rows,
+// cold access), the warm-access variants, the dirty-slot refund reversals and
+// multi-store round trips.
+//
+// Each sstore() is "PUSH1 val; PUSH1 slot; SSTORE", so the non-SSTORE overhead is
+// 6 gas (two PUSH1) per store. The first store to a slot is cold and the rest are
+// warm, so the access component is COLD_STORAGE_ACCESS + (n-1) * WARM_ACCESS for n
+// stores. STORAGE_WRITE is charged once per "first change" (current == original).
+// GAS_STORAGE_SET is state gas, charged when a slot is created from zero and
+// refilled to the reservoir when that creation is undone within the same tx.
+func TestEIP8038SStore(t *testing.T) {
+ const (
+ push = uint64(6) // two PUSH1 per SSTORE
+ cold = params.ColdStorageAccessAmsterdam
+ warm = params.WarmStorageReadCostEIP2929
+ write = params.StorageWriteAmsterdam
+ clear = params.StorageClearRefundAmsterdam
+ )
+ set := uint64(params.StorageCreationSize * params.CostPerStateByte) // GAS_STORAGE_SET
+
+ // access(n) is the access-only regular cost for n stores: cold first, warm rest.
+ access := func(n uint64) uint64 { return cold + (n-1)*warm }
+
+ cases := []struct {
+ name string
+ orig byte // committed (pre-tx) value; 0 means a fresh slot
+ vals []byte // values written to slot 0, in order
+ wantReg uint64
+ wantState int64
+ wantRfnd uint64
+ }{
+ // Single store, cold access (EIP-8038 cases table, Cold rows + noop).
+ {"noop (1->1)", 1, []byte{1}, push + cold, 0, 0},
+ {"create (0->1)", 0, []byte{1}, push + cold + write, int64(set), 0},
+ {"first change (1->2)", 1, []byte{2}, push + cold + write, 0, 0},
+ {"clear (1->0)", 1, []byte{0}, push + cold + write, 0, clear},
+ // Two stores, warm access on the second (Warm rows of the cases table).
+ {"create warm (0->0->1)", 0, []byte{0, 1}, 2*push + access(2) + write, int64(set), 0},
+ {"first change warm (1->1->2)", 1, []byte{1, 2}, 2*push + access(2) + write, 0, 0},
+ {"clear warm (1->1->0)", 1, []byte{1, 0}, 2*push + access(2) + write, 0, clear},
+ {"dirty modified again (1->2->3)", 1, []byte{2, 3}, 2*push + access(2) + write, 0, 0},
+ // Two stores, refund reversals when a slot returns toward its original.
+ {"reset to zero (0->1->0)", 0, []byte{1, 0}, 2*push + access(2) + write, 0, write},
+ {"reset to original (1->2->1)", 1, []byte{2, 1}, 2*push + access(2) + write, 0, write},
+ {"cleared then restored (1->0->1)", 1, []byte{0, 1}, 2*push + access(2) + write, 0, write},
+ {"cleared then new value (1->0->2)", 1, []byte{0, 2}, 2*push + access(2) + write, 0, 0},
+ // Three stores, round trips (note the state-gas refill on the 0-> path).
+ {"0->1->0->1", 0, []byte{1, 0, 1}, 3*push + access(3) + 2*write, int64(set), write},
+ {"1->0->1->0", 1, []byte{0, 1, 0}, 3*push + access(3) + 2*write, 0, clear + write},
+ }
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ var code []byte
+ for _, v := range tc.vals {
+ code = append(code, sstore(0, v)...)
+ }
+ var setup func(*state.StateDB, common.Address)
+ if tc.orig != 0 {
+ setup = setSlot(0, tc.orig)
+ }
+ res, refund, err := run8038(t, code, hugeBudget(), new(uint256.Int), setup)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if res.UsedRegularGas != tc.wantReg {
+ t.Errorf("regular gas = %d, want %d", res.UsedRegularGas, tc.wantReg)
+ }
+ if res.UsedStateGas != tc.wantState {
+ t.Errorf("state gas = %d, want %d", res.UsedStateGas, tc.wantState)
+ }
+ if refund != tc.wantRfnd {
+ t.Errorf("refund = %d, want %d", refund, tc.wantRfnd)
+ }
+ })
+ }
+}
+
+// TestEIP8038SLoad checks the re-priced SLOAD access costs (cold 3000, warm 100).
+func TestEIP8038SLoad(t *testing.T) {
+ push := uint64(3) // PUSH1 slot
+ // PUSH1 0x00; SLOAD
+ cold := []byte{0x60, 0x00, 0x54}
+ res, _, err := run8038(t, cold, hugeBudget(), new(uint256.Int), nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if want := push + params.ColdStorageAccessAmsterdam; res.UsedRegularGas != want {
+ t.Fatalf("cold SLOAD = %d, want %d", res.UsedRegularGas, want)
+ }
+ // PUSH1 0x00; SLOAD; PUSH1 0x00; SLOAD -> second access is warm.
+ warm := []byte{0x60, 0x00, 0x54, 0x60, 0x00, 0x54}
+ res, _, err = run8038(t, warm, hugeBudget(), new(uint256.Int), nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ want := 2*push + params.ColdStorageAccessAmsterdam + params.WarmStorageReadCostEIP2929
+ if res.UsedRegularGas != want {
+ t.Fatalf("cold+warm SLOAD = %d, want %d", res.UsedRegularGas, want)
+ }
+}
+
+// TestEIP8038AccountAccess checks the re-priced cold-account access for the
+// account-reading opcodes and the extra WARM_ACCESS surcharge for EXTCODESIZE
+// and EXTCODECOPY (their second database read).
+func TestEIP8038AccountAccess(t *testing.T) {
+ push20 := uint64(3)
+ addr := common.BytesToAddress([]byte("some-cold-account"))
+
+ // pushAddr emits PUSH20 .
+ pushAddr := func() []byte { return append([]byte{0x73}, addr.Bytes()...) }
+
+ cold := params.ColdAccountAccessAmsterdam
+ warm := params.WarmStorageReadCostEIP2929
+
+ t.Run("BALANCE", func(t *testing.T) {
+ code := append(pushAddr(), 0x31) // BALANCE
+ res, _, err := run8038(t, code, hugeBudget(), new(uint256.Int), nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if want := push20 + cold; res.UsedRegularGas != want {
+ t.Fatalf("cold BALANCE = %d, want %d", res.UsedRegularGas, want)
+ }
+ })
+ t.Run("EXTCODEHASH", func(t *testing.T) {
+ code := append(pushAddr(), 0x3f) // EXTCODEHASH
+ res, _, err := run8038(t, code, hugeBudget(), new(uint256.Int), nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if want := push20 + cold; res.UsedRegularGas != want {
+ t.Fatalf("cold EXTCODEHASH = %d, want %d", res.UsedRegularGas, want)
+ }
+ })
+ t.Run("EXTCODESIZE adds WARM_ACCESS", func(t *testing.T) {
+ code := append(pushAddr(), 0x3b) // EXTCODESIZE
+ res, _, err := run8038(t, code, hugeBudget(), new(uint256.Int), nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if want := push20 + cold + warm; res.UsedRegularGas != want {
+ t.Fatalf("cold EXTCODESIZE = %d, want %d", res.UsedRegularGas, want)
+ }
+ })
+ t.Run("EXTCODECOPY adds WARM_ACCESS", func(t *testing.T) {
+ // PUSH1 0 (length); PUSH1 0 (codeOffset); PUSH1 0 (destOffset); PUSH20 addr; EXTCODECOPY
+ code := []byte{0x60, 0x00, 0x60, 0x00, 0x60, 0x00}
+ code = append(code, pushAddr()...)
+ code = append(code, 0x3c) // EXTCODECOPY
+ res, _, err := run8038(t, code, hugeBudget(), new(uint256.Int), nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ // three PUSH1 + one PUSH20 = 12 gas, zero-length copy => no memory/copy gas.
+ if want := uint64(12) + cold + warm; res.UsedRegularGas != want {
+ t.Fatalf("cold EXTCODECOPY = %d, want %d", res.UsedRegularGas, want)
+ }
+ })
+}
+
+// TestEIP8038SelfdestructAccountWrite checks that SELFDESTRUCT sending a positive
+// balance to an empty account is charged the cold access, an additional
+// ACCOUNT_WRITE (regular) and GAS_NEW_ACCOUNT (state).
+func TestEIP8038SelfdestructAccountWrite(t *testing.T) {
+ beneficiary := common.BytesToAddress([]byte("fresh-beneficiary"))
+ // PUSH20 beneficiary; SELFDESTRUCT
+ code := append([]byte{0x73}, beneficiary.Bytes()...)
+ code = append(code, 0xff)
+
+ // Fund the contract so it sends a positive balance on self-destruct.
+ fundSelf := func(db *state.StateDB, self common.Address) {
+ db.AddBalance(self, uint256.NewInt(1), tracing.BalanceChangeUnspecified)
+ }
+ res, _, err := run8038(t, code, hugeBudget(), new(uint256.Int), fundSelf)
+ if err != nil {
+ t.Fatal(err)
+ }
+ const push20 = uint64(3)
+ wantReg := push20 + params.SelfdestructGasEIP150 + params.ColdAccountAccessAmsterdam + params.AccountWriteAmsterdam
+ if res.UsedRegularGas != wantReg {
+ t.Fatalf("regular gas = %d, want %d", res.UsedRegularGas, wantReg)
+ }
+ if want := int64(params.AccountCreationSize * params.CostPerStateByte); res.UsedStateGas != want {
+ t.Fatalf("state gas = %d, want %d", res.UsedStateGas, want)
+ }
+}
diff --git a/core/vm/eips.go b/core/vm/eips.go
index 814e4be788..8a09856029 100644
--- a/core/vm/eips.go
+++ b/core/vm/eips.go
@@ -43,7 +43,8 @@ var activators = map[int]func(*JumpTable){
7939: enable7939,
8024: enable8024,
7843: enable7843,
- 8037: enable8037,
+ 8037: enable8037And8038,
+ 8038: enable8037And8038,
}
// EnableEIP enables the given EIP on the config.
@@ -584,13 +585,30 @@ func enable7843(jt *JumpTable) {
}
}
-// enable8037 enables the multidimensional-metering as specified in EIP-8037.
-func enable8037(jt *JumpTable) {
- jt[CREATE].constantGas = params.CreateGasAmsterdam
+// enable8037And8038 enables EIP-8037 (multidimensional state-gas metering)
+// together with EIP-8038 (state-access gas cost update).
+func enable8037And8038(jt *JumpTable) {
+ jt[CREATE].constantGas = params.CreateAccessAmsterdam
jt[CREATE].dynamicGas = gasCreateEip8037
- jt[CREATE2].constantGas = params.CreateGasAmsterdam
+ jt[CREATE2].constantGas = params.CreateAccessAmsterdam
jt[CREATE2].dynamicGas = gasCreate2Eip8037
- jt[CALL].dynamicGas = gasCallEIP8037
- jt[SELFDESTRUCT].dynamicGas = gasSelfdestruct8037
- jt[SSTORE].dynamicGas = gasSStore8037
+
+ // Storage-access opcodes
+ jt[SLOAD].dynamicGas = gasSLoad8038
+ jt[SSTORE].dynamicGas = gasSStore8037And8038
+
+ // Account-access opcodes
+ jt[BALANCE].dynamicGas = gasEip8038AccountCheck
+ jt[EXTCODEHASH].dynamicGas = gasEip8038AccountCheck
+ jt[EXTCODESIZE].dynamicGas = gasExtCodeSize8038
+ jt[EXTCODECOPY].dynamicGas = gasExtCodeCopy8038
+
+ // Call family
+ jt[CALL].dynamicGas = gasCall8038
+ jt[CALLCODE].dynamicGas = gasCallCode8038
+ jt[STATICCALL].dynamicGas = gasStaticCall8038
+ jt[DELEGATECALL].dynamicGas = gasDelegateCall8038
+
+ // SELFDESTRUCT
+ jt[SELFDESTRUCT].dynamicGas = gasSelfdestruct8037And8038
}
diff --git a/core/vm/gas_table.go b/core/vm/gas_table.go
index 4bd971e711..8cc63cb236 100644
--- a/core/vm/gas_table.go
+++ b/core/vm/gas_table.go
@@ -478,6 +478,28 @@ func gasCallCodeIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memor
return gas, nil
}
+// gasCallCodeIntrinsic8038 mirrors gasCallCodeIntrinsic but charges the
+// re-priced CALL_VALUE (ACCOUNT_WRITE + CALL_STIPEND) on value transfers per
+// EIP-8038. CALLCODE executes in the caller's context, so it never creates a
+// new account and has no state-gas component.
+func gasCallCodeIntrinsic8038(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
+ memoryGas, err := memoryGasCost(mem, memorySize)
+ if err != nil {
+ return 0, err
+ }
+ var (
+ gas uint64
+ overflow bool
+ )
+ if stack.back(2).Sign() != 0 && !evm.chainRules.IsEIP4762 {
+ gas += params.CallValueTransferAmsterdam
+ }
+ if gas, overflow = math.SafeAdd(gas, memoryGas); overflow {
+ return 0, ErrGasUintOverflow
+ }
+ return gas, nil
+}
+
func gasDelegateCallIntrinsic(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
gas, err := memoryGasCost(mem, memorySize)
if err != nil {
@@ -576,10 +598,11 @@ func gasCreate2Eip8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory,
}, nil
}
-// regularGasCall8037 is the intrinsic gas calculator for CALL in Amsterdam.
-// It computes memory expansion + value transfer gas but excludes new account
-// creation, which is handled as state gas by the wrapper.
-func regularGasCall8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
+// regularGasCall8038 is the intrinsic regular-gas calculator for CALL in
+// Amsterdam. It computes memory expansion plus the re-priced CALL_VALUE
+// (ACCOUNT_WRITE + CALL_STIPEND) on value transfers, but excludes new account
+// creation, which is handled as state gas by stateGasCall8037.
+func regularGasCall8038(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) {
var (
gas uint64
transfersValue = !stack.back(2).IsZero()
@@ -593,7 +616,7 @@ func regularGasCall8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory,
}
var transferGas uint64
if transfersValue && !evm.chainRules.IsEIP4762 {
- transferGas = params.CallValueTransferGas
+ transferGas = params.CallValueTransferAmsterdam
}
var overflow bool
if gas, overflow = math.SafeAdd(memoryGas, transferGas); overflow {
@@ -630,7 +653,9 @@ func stateGasCall8037(evm *EVM, contract *Contract, stack *Stack) (uint64, error
return gas, nil
}
-func gasSelfdestruct8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
+// gasSelfdestruct8037And8038 implements the SELFDESTRUCT gas charging under
+// the EIP8037 and EIP-8038.
+func gasSelfdestruct8037And8038(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
if evm.readOnly {
return GasCosts{}, ErrWriteProtection
}
@@ -639,11 +664,11 @@ func gasSelfdestruct8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory
address = common.Address(stack.peek().Bytes20())
)
if !evm.StateDB.AddressInAccessList(address) {
- // If the caller cannot afford the cost, this change will be rolled back
+ // If the caller cannot afford the cost, this change will be rolled back.
evm.StateDB.AddAddressToAccessList(address)
- gas.RegularGas = params.ColdAccountAccessCostEIP2929
+ gas.RegularGas = params.ColdAccountAccessAmsterdam
}
- // Check we have enough regular gas before we add the address to the BAL
+ // Check we have enough regular gas before we add the address to the BAL.
if contract.Gas.RegularGas < gas.RegularGas {
return gas, ErrOutOfGas
}
@@ -654,79 +679,63 @@ func gasSelfdestruct8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory
//
// Funding such an account makes it permanent state growth and must be charged.
if evm.StateDB.Empty(address) && evm.StateDB.GetBalance(contract.Address()).Sign() != 0 {
+ gas.RegularGas += params.AccountWriteAmsterdam
gas.StateGas += params.AccountCreationSize * evm.Context.CostPerStateByte
}
return gas, nil
}
-func gasSStore8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
+// gasSStore8037And8038 implements the SSTORE gas charging under EIP-8037 and
+// EIP-8038.
+func gasSStore8037And8038(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
if evm.readOnly {
return GasCosts{}, ErrWriteProtection
}
- // If we fail the minimum gas availability invariant, fail (0)
+ // If we fail the minimum gas availability invariant, fail (0).
if contract.Gas.RegularGas <= params.SstoreSentryGasEIP2200 {
return GasCosts{}, errors.New("not enough gas for reentrancy sentry")
}
- // Gas sentry honoured, do the actual gas calculation based on the stored value
var (
y, x = stack.back(1), stack.peek()
slot = common.Hash(x.Bytes32())
current, original = evm.StateDB.GetStateAndCommittedState(contract.Address(), slot)
- cost GasCosts
+ value = common.Hash(y.Bytes32())
)
// Check slot presence in the access list
+ access := params.WarmStorageReadCostEIP2929
if _, slotPresent := evm.StateDB.SlotInAccessList(contract.Address(), slot); !slotPresent {
- cost = GasCosts{RegularGas: params.ColdSloadCostEIP2929}
- // If the caller cannot afford the cost, this change will be rolled back
+ access = params.ColdStorageAccessAmsterdam
evm.StateDB.AddSlotToAccessList(contract.Address(), slot)
}
- value := common.Hash(y.Bytes32())
+ stateSet := params.StorageCreationSize * evm.Context.CostPerStateByte
if current == value { // noop (1)
- // EIP 2200 original clause:
- // return params.SloadGasEIP2200, nil
- return GasCosts{RegularGas: cost.RegularGas + params.WarmStorageReadCostEIP2929}, nil // SLOAD_GAS
+ return GasCosts{RegularGas: access}, nil
}
- if original == current {
+ if original == current { // first change of the slot (2.1)
if original == (common.Hash{}) { // create slot (2.1.1)
return GasCosts{
- RegularGas: cost.RegularGas + params.SstoreResetGasEIP2200 - params.ColdSloadCostEIP2929,
- StateGas: params.StorageCreationSize * evm.Context.CostPerStateByte,
+ RegularGas: access + params.StorageWriteAmsterdam,
+ StateGas: stateSet,
}, nil
}
if value == (common.Hash{}) { // delete slot (2.1.2b)
- evm.StateDB.AddRefund(params.SstoreClearsScheduleRefundEIP3529)
+ evm.StateDB.AddRefund(params.StorageClearRefundAmsterdam)
}
- // EIP-2200 original clause:
- // return params.SstoreResetGasEIP2200, nil // write existing slot (2.1.2)
- return GasCosts{RegularGas: cost.RegularGas + params.SstoreResetGasEIP2200 - params.ColdSloadCostEIP2929}, nil // write existing slot (2.1.2)
+ return GasCosts{RegularGas: access + params.StorageWriteAmsterdam}, nil // write existing slot (2.1.2)
}
if original != (common.Hash{}) {
if current == (common.Hash{}) { // recreate slot (2.2.1.1)
- evm.StateDB.SubRefund(params.SstoreClearsScheduleRefundEIP3529)
+ evm.StateDB.SubRefund(params.StorageClearRefundAmsterdam)
} else if value == (common.Hash{}) { // delete slot (2.2.1.2)
- evm.StateDB.AddRefund(params.SstoreClearsScheduleRefundEIP3529)
+ evm.StateDB.AddRefund(params.StorageClearRefundAmsterdam)
}
}
- if original == value {
+ if original == value { // reset to original value (2.2.2)
if original == (common.Hash{}) { // reset to original inexistent slot (2.2.2.1)
- // EIP-8037 point (2): refund state gas directly to the reservoir
- // at the SSTORE restoration point (0→x→0 in same tx); not to the
- // refund counter, which is capped at gas_used/5.
- contract.Gas.RefundState(params.StorageCreationSize * evm.Context.CostPerStateByte)
-
- // Regular portion of the refund still goes through the refund counter.
- evm.StateDB.AddRefund(params.SstoreResetGasEIP2200 - params.ColdSloadCostEIP2929 - params.WarmStorageReadCostEIP2929)
- } else { // reset to original existing slot (2.2.2.2)
- // EIP 2200 Original clause:
- // evm.StateDB.AddRefund(params.SstoreResetGasEIP2200 - params.SloadGasEIP2200)
- // - SSTORE_RESET_GAS redefined as (5000 - COLD_SLOAD_COST)
- // - SLOAD_GAS redefined as WARM_STORAGE_READ_COST
- // Final: (5000 - COLD_SLOAD_COST) - WARM_STORAGE_READ_COST
- evm.StateDB.AddRefund((params.SstoreResetGasEIP2200 - params.ColdSloadCostEIP2929) - params.WarmStorageReadCostEIP2929)
+ contract.Gas.RefundState(stateSet)
}
+ evm.StateDB.AddRefund(params.StorageWriteAmsterdam)
}
- // EIP-2200 original clause:
- //return params.SloadGasEIP2200, nil // dirty update (2.2)
- return GasCosts{RegularGas: cost.RegularGas + params.WarmStorageReadCostEIP2929}, nil // dirty update (2.2)
+ return GasCosts{RegularGas: access}, nil // dirty update (2.2)
}
diff --git a/core/vm/jump_table.go b/core/vm/jump_table.go
index 5cc5e34ced..6b931083fe 100644
--- a/core/vm/jump_table.go
+++ b/core/vm/jump_table.go
@@ -99,9 +99,9 @@ func newVerkleInstructionSet() JumpTable {
func newAmsterdamInstructionSet() JumpTable {
instructionSet := newOsakaInstructionSet()
- enable7843(&instructionSet) // EIP-7843 (SLOTNUM opcode)
- enable8024(&instructionSet) // EIP-8024 (Backward compatible SWAPN, DUPN, EXCHANGE)
- enable8037(&instructionSet) // EIP-8037 (State creation gas cost increase)
+ enable7843(&instructionSet) // EIP-7843 (SLOTNUM opcode)
+ enable8024(&instructionSet) // EIP-8024 (Backward compatible SWAPN, DUPN, EXCHANGE)
+ enable8037And8038(&instructionSet) // EIP-8037 (state-gas metering) + EIP-8038 (state-access repricing)
return validate(instructionSet)
}
diff --git a/core/vm/operations_acl.go b/core/vm/operations_acl.go
index e07eba451f..111cc6a00c 100644
--- a/core/vm/operations_acl.go
+++ b/core/vm/operations_acl.go
@@ -101,16 +101,25 @@ func makeGasSStoreFunc(clearingRefund uint64) gasFunc {
func gasSLoadEIP2929(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
loc := stack.peek()
slot := common.Hash(loc.Bytes32())
- // Check slot presence in the access list
if _, slotPresent := evm.StateDB.SlotInAccessList(contract.Address(), slot); !slotPresent {
- // If the caller cannot afford the cost, this change will be rolled back
- // If he does afford it, we can skip checking the same thing later on, during execution
evm.StateDB.AddSlotToAccessList(contract.Address(), slot)
return GasCosts{RegularGas: params.ColdSloadCostEIP2929}, nil
}
return GasCosts{RegularGas: params.WarmStorageReadCostEIP2929}, nil
}
+// gasSLoad8038 mirrors gasSLoadEIP2929 but uses the EIP-8038 COLD_STORAGE_ACCESS
+// for a cold slot.
+func gasSLoad8038(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
+ loc := stack.peek()
+ slot := common.Hash(loc.Bytes32())
+ if _, slotPresent := evm.StateDB.SlotInAccessList(contract.Address(), slot); !slotPresent {
+ evm.StateDB.AddSlotToAccessList(contract.Address(), slot)
+ return GasCosts{RegularGas: params.ColdStorageAccessAmsterdam}, nil
+ }
+ return GasCosts{RegularGas: params.WarmStorageReadCostEIP2929}, nil
+}
+
// gasExtCodeCopyEIP2929 implements extcodecopy according to EIP-2929
// EIP spec:
// > If the target is not in accessed_addresses,
@@ -137,6 +146,34 @@ func gasExtCodeCopyEIP2929(evm *EVM, contract *Contract, stack *Stack, mem *Memo
return GasCosts{RegularGas: gas}, nil
}
+// gasExtCodeCopy8038 mirrors gasExtCodeCopyEIP2929 but uses the EIP-8038
+// COLD_ACCOUNT_ACCESS and adds an extra WARM_ACCESS for the second
+// database read EXTCODECOPY performs.
+func gasExtCodeCopy8038(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
+ // memory expansion first (dynamic part of pre-2929 implementation)
+ gasCost, err := gasExtCodeCopy(evm, contract, stack, mem, memorySize)
+ if err != nil {
+ return GasCosts{}, err
+ }
+ gas := gasCost.RegularGas
+ addr := common.Address(stack.peek().Bytes20())
+ // Check slot presence in the access list
+ if !evm.StateDB.AddressInAccessList(addr) {
+ evm.StateDB.AddAddressToAccessList(addr)
+ var overflow bool
+ // We charge (cold-warm), since 'warm' is already charged as constantGas
+ if gas, overflow = math.SafeAdd(gas, params.ColdAccountAccessAmsterdam-params.WarmStorageReadCostEIP2929); overflow {
+ return GasCosts{}, ErrGasUintOverflow
+ }
+ }
+ // Additional WARM_ACCESS for the second database read (contract code).
+ var overflow bool
+ if gas, overflow = math.SafeAdd(gas, params.WarmStorageReadCostEIP2929); overflow {
+ return GasCosts{}, ErrGasUintOverflow
+ }
+ return GasCosts{RegularGas: gas}, nil
+}
+
// gasEip2929AccountCheck checks whether the first stack item (as address) is present in the access list.
// If it is, this method returns '0', otherwise 'cold-warm' gas, presuming that the opcode using it
// is also using 'warm' as constant factor.
@@ -156,6 +193,32 @@ func gasEip2929AccountCheck(evm *EVM, contract *Contract, stack *Stack, mem *Mem
return GasCosts{}, nil
}
+// gasEip8038AccountCheck mirrors gasEip2929AccountCheck but uses the EIP-8038
+// COLD_ACCOUNT_ACCESS. Used by BALANCE and EXTCODEHASH.
+func gasEip8038AccountCheck(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
+ addr := common.Address(stack.peek().Bytes20())
+ // Check slot presence in the access list
+ if !evm.StateDB.AddressInAccessList(addr) {
+ // If the caller cannot afford the cost, this change will be rolled back
+ evm.StateDB.AddAddressToAccessList(addr)
+ // The warm storage read cost is already charged as constantGas
+ return GasCosts{RegularGas: params.ColdAccountAccessAmsterdam - params.WarmStorageReadCostEIP2929}, nil
+ }
+ return GasCosts{}, nil
+}
+
+// gasExtCodeSize8038 prices EXTCODESIZE under EIP-8038: the gasEip8038AccountCheck
+// surcharge plus an additional WARM_ACCESS for the second database read (code size).
+func gasExtCodeSize8038(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
+ cost, err := gasEip8038AccountCheck(evm, contract, stack, mem, memorySize)
+ if err != nil {
+ return GasCosts{}, err
+ }
+ // Additional WARM_ACCESS for the second database read (contract size).
+ cost.RegularGas += params.WarmStorageReadCostEIP2929
+ return cost, nil
+}
+
func makeCallVariantGasCallEIP2929(oldCalculator gasFunc, addressPosition int) gasFunc {
return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
addr := common.Address(stack.back(addressPosition).Bytes20())
@@ -266,11 +329,10 @@ func recordDelegationAccess(evm *EVM, target common.Address) {
}
var (
- innerGasCallEIP7702 = makeCallVariantGasCallEIP7702(gasCallIntrinsic)
- gasDelegateCallEIP7702 = makeCallVariantGasCallEIP7702(gasDelegateCallIntrinsic)
- gasStaticCallEIP7702 = makeCallVariantGasCallEIP7702(gasStaticCallIntrinsic)
- gasCallCodeEIP7702 = makeCallVariantGasCallEIP7702(gasCallCodeIntrinsic)
- innerGasCallEIP8037 = makeCallVariantGasCallEIP8037(regularGasCall8037, stateGasCall8037)
+ innerGasCallEIP7702 = makeCallVariantGasCallEIP7702(gasCallIntrinsic, params.ColdAccountAccessCostEIP2929)
+ gasDelegateCallEIP7702 = makeCallVariantGasCallEIP7702(gasDelegateCallIntrinsic, params.ColdAccountAccessCostEIP2929)
+ gasStaticCallEIP7702 = makeCallVariantGasCallEIP7702(gasStaticCallIntrinsic, params.ColdAccountAccessCostEIP2929)
+ gasCallCodeEIP7702 = makeCallVariantGasCallEIP7702(gasCallCodeIntrinsic, params.ColdAccountAccessCostEIP2929)
)
func gasCallEIP7702(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
@@ -285,15 +347,24 @@ func gasCallEIP7702(evm *EVM, contract *Contract, stack *Stack, mem *Memory, mem
return innerGasCallEIP7702(evm, contract, stack, mem, memorySize)
}
-func gasCallEIP8037(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
+var (
+ innerGasCall8038 = makeCallVariantGasCallEIP8037(regularGasCall8038, stateGasCall8037, params.ColdAccountAccessAmsterdam)
+ gasCallCode8038 = makeCallVariantGasCallEIP7702(gasCallCodeIntrinsic8038, params.ColdAccountAccessAmsterdam)
+ gasDelegateCall8038 = makeCallVariantGasCallEIP7702(gasDelegateCallIntrinsic, params.ColdAccountAccessAmsterdam)
+ gasStaticCall8038 = makeCallVariantGasCallEIP7702(gasStaticCallIntrinsic, params.ColdAccountAccessAmsterdam)
+)
+
+// gasCall8038 prices CALL for Amsterdam, guarding against value transfers in a
+// read-only context before delegating to the state-gas-aware wrapper.
+func gasCall8038(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
transfersValue := !stack.back(2).IsZero()
if evm.readOnly && transfersValue {
return GasCosts{}, ErrWriteProtection
}
- return innerGasCallEIP8037(evm, contract, stack, mem, memorySize)
+ return innerGasCall8038(evm, contract, stack, mem, memorySize)
}
-func makeCallVariantGasCallEIP7702(intrinsicFunc intrinsicGasFunc) gasFunc {
+func makeCallVariantGasCallEIP7702(intrinsicFunc intrinsicGasFunc, coldCost uint64) gasFunc {
return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
var (
eip2929Cost uint64
@@ -308,7 +379,7 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc intrinsicGasFunc) gasFunc {
// The WarmStorageReadCostEIP2929 (100) is already deducted in the form
// of a constant cost, so the cost to charge for cold access, if any,
// is Cold - Warm
- eip2929Cost = params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929
+ eip2929Cost = coldCost - params.WarmStorageReadCostEIP2929
// Charge the remaining difference here already, to correctly calculate
// available gas for call
@@ -339,7 +410,7 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc intrinsicGasFunc) gasFunc {
eip7702Cost = params.WarmStorageReadCostEIP2929
} else {
evm.StateDB.AddAddressToAccessList(target)
- eip7702Cost = params.ColdAccountAccessCostEIP2929
+ eip7702Cost = coldCost
}
if !contract.chargeRegular(eip7702Cost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) {
return GasCosts{}, ErrOutOfGas
@@ -388,7 +459,7 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc intrinsicGasFunc) gasFunc {
// It extends the EIP-7702 pattern with state gas handling and GasUsed tracking.
// intrinsicFunc computes the regular gas (memory + transfer, no new account creation).
// stateGasFunc computes the state gas (new account creation as state gas).
-func makeCallVariantGasCallEIP8037(regularFunc regularGasFunc, stateGasFunc stateGasFunc) gasFunc {
+func makeCallVariantGasCallEIP8037(regularFunc regularGasFunc, stateGasFunc stateGasFunc, coldCost uint64) gasFunc {
return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (GasCosts, error) {
var (
eip2929Cost uint64
@@ -398,7 +469,7 @@ func makeCallVariantGasCallEIP8037(regularFunc regularGasFunc, stateGasFunc stat
// EIP-2929 cold access check.
if !evm.StateDB.AddressInAccessList(addr) {
evm.StateDB.AddAddressToAccessList(addr)
- eip2929Cost = params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929
+ eip2929Cost = coldCost - params.WarmStorageReadCostEIP2929
if !contract.chargeRegular(eip2929Cost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) {
return GasCosts{}, ErrOutOfGas
}
@@ -423,7 +494,7 @@ func makeCallVariantGasCallEIP8037(regularFunc regularGasFunc, stateGasFunc stat
eip7702Cost = params.WarmStorageReadCostEIP2929
} else {
evm.StateDB.AddAddressToAccessList(target)
- eip7702Cost = params.ColdAccountAccessCostEIP2929
+ eip7702Cost = coldCost
}
if !contract.chargeRegular(eip7702Cost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) {
return GasCosts{}, ErrOutOfGas
diff --git a/params/protocol_params.go b/params/protocol_params.go
index 7416eb8cff..a56e810074 100644
--- a/params/protocol_params.go
+++ b/params/protocol_params.go
@@ -88,7 +88,6 @@ const (
LogTopicGas uint64 = 375 // Multiplied by the * of the LOG*, per LOG transaction. e.g. LOG0 incurs 0 * c_txLogTopicGas, LOG4 incurs 4 * c_txLogTopicGas.
CreateGas uint64 = 32000 // Once per CREATE operation & contract-creation transaction.
Create2Gas uint64 = 32000 // Once per CREATE2 operation
- CreateGasAmsterdam uint64 = 9000 // Regular gas portion of CREATE in Amsterdam (EIP-8037); state gas is charged separately.
CreateNGasEip4762 uint64 = 1000 // Once per CREATEn operations post-verkle
SelfdestructRefundGas uint64 = 24000 // Refunded following a selfdestruct operation.
MemoryGas uint64 = 3 // Times the address of the (highest referenced byte in memory + 1). NOTE: referencing happens on read, write and in instructions such as RETURN and CALL.
@@ -101,7 +100,14 @@ const (
TxAccessListAddressGas uint64 = 2400 // Per address specified in EIP 2930 access list
TxAccessListStorageKeyGas uint64 = 1900 // Per storage key specified in EIP 2930 access list
TxAuthTupleGas uint64 = 12500 // Per auth tuple code specified in EIP-7702
- TxAuthTupleRegularGas uint64 = 7500 // Per auth tuple regular gas specified in EIP-8037
+
+ // RegularPerAuthBaseCost is defined in EIP-8037 as the sum of:
+ //
+ // - Calldata cost: 1,616 (101 bytes × 16)
+ // - Recovering authority address (ecRecover)
+ // - Reading nonce and code of authority (cold access)
+ // - Storing values in already warm account: 2 x WARM_ACCESS
+ RegularPerAuthBaseCost uint64 = 7500
// EIP-2780: resource-based intrinsic transaction gas.
TxBaseCost2780 uint64 = 12000
@@ -110,6 +116,17 @@ const (
TxValueCost2780 uint64 = 4244
TransferLogCost2780 uint64 = 1756
+ // EIP-8038: state-access gas cost update (Amsterdam).
+ ColdAccountAccessAmsterdam uint64 = 3000 // COLD_ACCOUNT_ACCESS: cold touch of an account
+ AccountWriteAmsterdam uint64 = 8000 // ACCOUNT_WRITE: surcharge for first-time write to an account
+ CallValueTransferAmsterdam uint64 = 10300 // CALL_VALUE = ACCOUNT_WRITE + CallStipend (2300)
+ ColdStorageAccessAmsterdam uint64 = 3000 // COLD_STORAGE_ACCESS: cold touch of a storage slot
+ StorageWriteAmsterdam uint64 = 10000 // STORAGE_WRITE: surcharge for first-time write to a storage slot
+ StorageClearRefundAmsterdam uint64 = 12480 // STORAGE_CLEAR_REFUND: refund for clearing a storage slot
+ CreateAccessAmsterdam uint64 = 11000 // CREATE_ACCESS = ACCOUNT_WRITE + COLD_STORAGE_ACCESS
+ TxAccessListAddressGasAmsterdam uint64 = 3000 // ACCESS_LIST_ADDRESS_COST
+ TxAccessListStorageKeyGasAmsterdam uint64 = 3000 // ACCESS_LIST_STORAGE_KEY_COST
+
// These have been changed during the course of the chain
CallGasFrontier uint64 = 40 // Once per CALL operation & message call transaction.
CallGasEIP150 uint64 = 700 // Static portion of gas for CALL-derivates after EIP 150 (Tangerine)