diff --git a/core/eip2780_test.go b/core/eip2780_test.go
index cee5b3f685..7b421b1fb4 100644
--- a/core/eip2780_test.go
+++ b/core/eip2780_test.go
@@ -17,10 +17,12 @@
package core
import (
+ "errors"
"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"
@@ -109,6 +111,44 @@ func TestEIP2780Intrinsic(t *testing.T) {
}
}
+// TestEIP2780Boundary distinguishes the state-independent intrinsic validity
+// threshold from a valid transaction which later runs out of runtime gas.
+func TestEIP2780Boundary(t *testing.T) {
+ auth, _ := signAuth(t, authKeyA, delegate8037, 0)
+ to := common.HexToAddress("0xe0a0000000000000000000000000000000000008")
+ cases := []struct {
+ name string
+ tx func(uint64) *types.Transaction
+ }{
+ {"call", func(gas uint64) *types.Transaction { return callTx(0, to, 0, gas, nil) }},
+ {"value", func(gas uint64) *types.Transaction { return callTx(0, to, 1, gas, nil) }},
+ {"create", func(gas uint64) *types.Transaction { return createTx(0, gas, nil) }},
+ {"auth", func(gas uint64) *types.Transaction {
+ return setCodeTxGas(0, to, 0, gas, []types.SetCodeAuthorization{auth})
+ }},
+ }
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ msg, err := TransactionToMessage(tc.tx(1_000_000), signer8037, big.NewInt(0))
+ if err != nil {
+ t.Fatal(err)
+ }
+ intrinsic, err := IntrinsicGas(msg.Data, msg.AccessList, msg.SetCodeAuthorizations, msg.From, msg.To, msg.Value, rules8037)
+ if err != nil {
+ t.Fatal(err)
+ }
+ res, _, err := applyMsg(t, mkState(senderAlloc(nil)), tc.tx(intrinsic-1))
+ if res != nil || !errors.Is(err, ErrIntrinsicGas) {
+ t.Fatalf("below intrinsic: result=%v err=%v, want ErrIntrinsicGas", res, err)
+ }
+ res, _, err = applyMsg(t, mkState(senderAlloc(nil)), tc.tx(intrinsic))
+ if err != nil || res == nil {
+ t.Fatalf("at intrinsic must be included: result=%v err=%v", res, err)
+ }
+ })
+ }
+}
+
// TestEIP2780Gas checks every "Transaction reference case" in
// the EIP-2780 specification end-to-end, asserting the two-dimensional charge
// (intrinsic + top-level + execution) recorded in the block gas pool.
@@ -194,6 +234,32 @@ func callTxAL(nonce uint64, to common.Address, value int64, gas uint64, al types
})
}
+func setCodeTxGas(nonce uint64, to common.Address, value, gas uint64, auths []types.SetCodeAuthorization) *types.Transaction {
+ return setCodeTxGasAL(nonce, to, value, gas, nil, auths)
+}
+
+func setCodeTxGasAL(nonce uint64, to common.Address, value, gas uint64, al types.AccessList, auths []types.SetCodeAuthorization) *types.Transaction {
+ return types.MustSignNewTx(senderKey, signer8037, &types.SetCodeTx{
+ ChainID: uint256.MustFromBig(cfg8037.ChainID), Nonce: nonce, To: to,
+ Value: uint256.NewInt(value), Gas: gas, AccessList: al,
+ GasFeeCap: new(uint256.Int), GasTipCap: new(uint256.Int), AuthList: auths,
+ })
+}
+
+func applyMsgCoinbase(t *testing.T, sdb *state.StateDB, tx *types.Transaction, coinbase common.Address) (*ExecutionResult, *GasPool, error) {
+ t.Helper()
+ evm := amsterdamCoreEVM(sdb)
+ evm.Context.Coinbase = coinbase
+ msg, err := TransactionToMessage(tx, signer8037, evm.Context.BaseFee)
+ if err != nil {
+ t.Fatalf("to message: %v", err)
+ }
+ gp := NewGasPool(evm.Context.GasLimit)
+ evm.SetTxContext(NewEVMTxContext(msg))
+ res, err := newStateTransition(evm, msg, gp).execute()
+ return res, gp, err
+}
+
// accessListEntryCost is the total intrinsic cost of one address-only access
// list entry: the EIP-8038 per-address charge plus the EIP-7981 data charge.
const accessListEntryCost = params.TxAccessListAddressGasAmsterdam +
@@ -332,6 +398,71 @@ func TestEIP2780RuntimeOOGRevertsDelegations(t *testing.T) {
}
}
+// TestEIP2780RecipientOOG verifies that an OOG recipient charge rolls back a
+// delegation which was successfully installed earlier in the same transaction.
+func TestEIP2780RecipientOOG(t *testing.T) {
+ auth, authority := signAuth(t, authKeyA, delegate8037, 0)
+ recipient := common.HexToAddress("0xbeef000000000000000000000000000000000004")
+ intrinsic := params.TxBaseCost2780 + params.ColdAccountAccessAmsterdam +
+ params.TxValueCost2780 + params.TransferLogCost2780 + params.RegularPerAuthBaseCost
+ // The reservoir case needs a near-cap intrinsic cost. This leaves just
+ // enough total budget for the authorization but not for the recipient leaf.
+ const (
+ regularLeft = 100_000
+ reservoir = 200_000
+ )
+ al := types.AccessList{{Address: common.HexToAddress("0xa1")}}
+ baseIntrinsic, err := IntrinsicGas(nil, al, []types.SetCodeAuthorization{auth}, senderAddr, &recipient, uint256.NewInt(1), rules8037)
+ if err != nil {
+ t.Fatal(err)
+ }
+ perKey := params.TxAccessListStorageKeyGasAmsterdam + uint64(common.HashLength)*params.TxCostFloorPerToken7976*params.TxTokenPerNonZeroByte
+ al[0].StorageKeys = make([]common.Hash, (params.MaxTxGas-regularLeft-baseIntrinsic)/perKey)
+ alIntrinsic, err := IntrinsicGas(nil, al, []types.SetCodeAuthorization{auth}, senderAddr, &recipient, uint256.NewInt(1), rules8037)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if available := params.MaxTxGas - alIntrinsic + reservoir; available < params.AccountWriteAmsterdam+authWorstState || available >= params.AccountWriteAmsterdam+authWorstState+newAccountState {
+ t.Fatalf("setup: available runtime gas %d does not isolate recipient charge", available)
+ }
+ cases := []struct {
+ name string
+ tx *types.Transaction
+ want uint64
+ }{
+ // This exactly pays the first authorization, leaving no gas for the
+ // fresh recipient's account-leaf charge.
+ {"no-reservoir", setCodeTxGas(0, recipient, 1, intrinsic+params.AccountWriteAmsterdam+authWorstState, []types.SetCodeAuthorization{auth}), intrinsic + params.AccountWriteAmsterdam + authWorstState},
+ // The state reservoir is restored by the halt; only the capped regular
+ // dimension is burnt.
+ {"with-reservoir", setCodeTxGasAL(0, recipient, 1, params.MaxTxGas+reservoir, al, []types.SetCodeAuthorization{auth}), params.MaxTxGas},
+ }
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ sdb := mkState(senderAlloc(nil))
+ res, gp, err := applyMsg(t, sdb, tc.tx)
+ if err != nil {
+ t.Fatalf("transaction should remain valid: %v", err)
+ }
+ if res.Err != vm.ErrOutOfGas {
+ t.Fatalf("execution error = %v, want out of gas", res.Err)
+ }
+ if code := sdb.GetCode(authority); len(code) != 0 || sdb.GetNonce(authority) != 0 {
+ t.Fatalf("authorization persisted after recipient OOG: code=%x nonce=%d", code, sdb.GetNonce(authority))
+ }
+ if sdb.Exist(recipient) {
+ t.Fatal("recipient created despite an unpaid runtime charge")
+ }
+ if sdb.GetNonce(senderAddr) != 1 {
+ t.Fatal("sender nonce not consumed")
+ }
+ if res.UsedGas != tc.want || gp.cumulativeState != 0 || gp.cumulativeRegular != tc.want {
+ t.Fatalf("used/gas = %d/<%d,%d>, want %d/<%d,0>", res.UsedGas, gp.cumulativeRegular, gp.cumulativeState, tc.want, tc.want)
+ }
+ })
+ }
+}
+
// TestEIP2780SelfTransferDelegated verifies that a self-transfer incurs no
// recipient touch or value charges, while resolving the sender's own
// delegation is still paid for.
@@ -400,6 +531,242 @@ func TestEIP2780InsufficientGasForCallCharge(t *testing.T) {
}
}
+// TestEIP2780RecipientKinds covers EIP-161 and precompile distinctions which
+// are invisible to the state-independent intrinsic charge.
+func TestEIP2780RecipientKinds(t *testing.T) {
+ const (
+ base = params.TxBaseCost2780
+ cold = params.ColdAccountAccessAmsterdam
+ valueCst = params.TxValueCost2780 + params.TransferLogCost2780
+ )
+ nonceOnly := common.HexToAddress("0xbeef000000000000000000000000000000000005")
+ precompile := common.BytesToAddress([]byte{4}) // identity; 15 gas for empty input
+ cases := []struct {
+ name string
+ alloc types.GenesisAlloc
+ tx *types.Transaction
+ wantRegular, wantState uint64
+ }{
+ {
+ name: "nonce-only",
+ alloc: types.GenesisAlloc{nonceOnly: {Nonce: 1}},
+ tx: callTx(0, nonceOnly, 1, 100_000, nil),
+ wantRegular: base + cold + valueCst,
+ },
+ {
+ name: "precompile/zero",
+ tx: callTx(0, precompile, 0, 100_000, nil),
+ wantRegular: base + cold + 15,
+ },
+ {
+ name: "precompile/value",
+ tx: callTx(0, precompile, 1, 300_000, nil),
+ wantRegular: base + cold + valueCst + 15,
+ wantState: newAccountState,
+ },
+ }
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ res, gp, err := applyMsg(t, mkState(senderAlloc(tc.alloc)), tc.tx)
+ if err != nil || res.Err != nil {
+ t.Fatalf("result=%v err=%v", res, err)
+ }
+ if gp.cumulativeRegular != tc.wantRegular || gp.cumulativeState != tc.wantState {
+ t.Fatalf("gas = <%d,%d>, want <%d,%d>", gp.cumulativeRegular, gp.cumulativeState, tc.wantRegular, tc.wantState)
+ }
+ })
+ }
+}
+
+// TestEIP2780RecipientRefill covers the empty-precompile path: the account
+// leaf is charged before dispatch, then refilled when the top frame halts.
+func TestEIP2780RecipientRefill(t *testing.T) {
+ // The pairing precompile rejects this malformed input after the recipient's
+ // account-leaf charge. The excess over MaxTxGas is a state reservoir.
+ recipient := common.BytesToAddress([]byte{8})
+ gas := params.MaxTxGas + newAccountState + 50_000
+ sdb := mkState(senderAlloc(nil))
+ res, gp, err := applyMsg(t, sdb, callTx(0, recipient, 1, gas, []byte{0}))
+ if err != nil || res.Err == nil {
+ t.Fatalf("result=%v err=%v, want exceptional halt", res, err)
+ }
+ if gp.cumulativeState != 0 || gp.cumulativeRegular != params.MaxTxGas {
+ t.Fatalf("gas = <%d,%d>, want <%d,0> after refill", gp.cumulativeRegular, gp.cumulativeState, params.MaxTxGas)
+ }
+ if sdb.Exist(recipient) {
+ t.Fatal("empty recipient persisted after halted dispatch")
+ }
+}
+
+// TestEIP2780Coinbase keeps the intrinsic recipient charge separate from the
+// runtime warmth of the coinbase account: calling the coinbase directly is
+// still charged at the cold rate. The warm rate for a coinbase delegation
+// target is covered by TestEIP2780DelegationWarmth.
+func TestEIP2780Coinbase(t *testing.T) {
+ coinbase := common.HexToAddress("0xc01ba5e000000000000000000000000000000001")
+ res, gp, err := applyMsgCoinbase(t, mkState(senderAlloc(nil)), callTx(0, coinbase, 0, 100_000, nil), coinbase)
+ if err != nil || res.Err != nil {
+ t.Fatalf("result=%v err=%v", res, err)
+ }
+ if want := params.TxBaseCost2780 + params.ColdAccountAccessAmsterdam; gp.cumulativeRegular != want {
+ t.Fatalf("regular gas = %d, want %d", gp.cumulativeRegular, want)
+ }
+}
+
+// TestEIP2780DelegationWarmth adds the special targets which are warm before
+// top-level dispatch but are not access-list entries.
+func TestEIP2780DelegationWarmth(t *testing.T) {
+ const (
+ base = params.TxBaseCost2780
+ cold = params.ColdAccountAccessAmsterdam
+ warm = params.WarmAccountAccessAmsterdam
+ )
+ recipient := common.HexToAddress("0xde1e000000000000000000000000000000000008")
+ precompile := common.BytesToAddress([]byte{4})
+ cases := []struct {
+ name string
+ target common.Address
+ coinbase common.Address
+ wantRegular uint64
+ }{
+ {"precompile", precompile, common.Address{}, base + cold + warm},
+ {"coinbase", common.HexToAddress("0xc01ba5e000000000000000000000000000000002"), common.HexToAddress("0xc01ba5e000000000000000000000000000000002"), base + cold + warm},
+ }
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ sdb := mkState(senderAlloc(types.GenesisAlloc{
+ recipient: {Code: types.AddressToDelegation(tc.target)},
+ }))
+ res, gp, err := applyMsgCoinbase(t, sdb, callTx(0, recipient, 0, 100_000, nil), tc.coinbase)
+ if err != nil || res.Err != nil {
+ t.Fatalf("result=%v err=%v", res, err)
+ }
+ if gp.cumulativeRegular != tc.wantRegular {
+ t.Fatalf("regular gas = %d, want %d", gp.cumulativeRegular, tc.wantRegular)
+ }
+ })
+ }
+ // A delegation designator pointing to itself eventually faults as bytecode,
+ // so pin just the pre-frame recipient-target charge rather than the later
+ // frame result. The recipient was warmed as tx.to before this charge.
+ sdb := mkState(senderAlloc(types.GenesisAlloc{
+ recipient: {Code: types.AddressToDelegation(recipient)},
+ }))
+ to := recipient
+ st := newStateTransition(amsterdamCoreEVM(sdb), &Message{To: &to, Value: new(uint256.Int)}, NewGasPool(100_000))
+ st.gasRemaining = vm.NewGasBudget(1_000, 0)
+ sdb.AddAddressToAccessList(recipient)
+ if !st.chargeCallRecipientEIP2780(new(uint256.Int)) || st.gasRemaining.UsedRegularGas != warm {
+ t.Fatalf("recipient target charge = %d, want warm %d", st.gasRemaining.UsedRegularGas, warm)
+ }
+}
+
+// TestEIP2780InstallDispatch covers an authority installed during the
+// pre-frame authorization pass and dispatched to by that same transaction.
+func TestEIP2780InstallDispatch(t *testing.T) {
+ const (
+ base = params.TxBaseCost2780
+ cold = params.ColdAccountAccessAmsterdam
+ perAuth = params.RegularPerAuthBaseCost
+ valueCst = params.TxValueCost2780 + params.TransferLogCost2780
+ )
+ auth, authority := signAuth(t, authKeyA, delegate8037, 0)
+ senderAuth, err := types.SignSetCode(senderKey, types.SetCodeAuthorization{
+ ChainID: *uint256.MustFromBig(cfg8037.ChainID), Address: delegate8037, Nonce: 1,
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ cases := []struct {
+ name string
+ alloc types.GenesisAlloc
+ tx *types.Transaction
+ account common.Address
+ wantRegular, wantState uint64
+ wantNonce uint64
+ wantBalance *big.Int
+ }{
+ {
+ name: "sender",
+ tx: setCodeTxGas(0, senderAddr, 0, 1_000_000, []types.SetCodeAuthorization{senderAuth}),
+ account: senderAddr,
+ wantRegular: base + perAuth + cold,
+ wantState: authBaseState,
+ wantNonce: 2,
+ },
+ {
+ name: "fresh-recipient",
+ tx: setCodeTxGas(0, authority, 1, 1_000_000, []types.SetCodeAuthorization{auth}),
+ account: authority,
+ wantRegular: base + cold + valueCst + perAuth + cold,
+ wantState: authWorstState,
+ wantNonce: 1,
+ wantBalance: big.NewInt(1),
+ },
+ {
+ name: "funded-recipient",
+ alloc: types.GenesisAlloc{authority: {Balance: big.NewInt(3)}},
+ tx: setCodeTxGas(0, authority, 1, 1_000_000, []types.SetCodeAuthorization{auth}),
+ account: authority,
+ wantRegular: base + cold + valueCst + perAuth + cold,
+ wantState: authBaseState,
+ wantNonce: 1,
+ wantBalance: big.NewInt(4),
+ },
+ }
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ sdb := mkState(senderAlloc(tc.alloc))
+ res, gp, err := applyMsg(t, sdb, tc.tx)
+ if err != nil || res.Err != nil {
+ t.Fatalf("result=%v err=%v", res, err)
+ }
+ if _, delegated := types.ParseDelegation(sdb.GetCode(tc.account)); !delegated || sdb.GetNonce(tc.account) != tc.wantNonce {
+ t.Fatalf("delegation/nonce = %x/%d, want delegation/%d", sdb.GetCode(tc.account), sdb.GetNonce(tc.account), tc.wantNonce)
+ }
+ if tc.wantBalance != nil && sdb.GetBalance(tc.account).Cmp(uint256.MustFromBig(tc.wantBalance)) != 0 {
+ t.Fatalf("balance = %v, want %v", sdb.GetBalance(tc.account), tc.wantBalance)
+ }
+ if gp.cumulativeRegular != tc.wantRegular || gp.cumulativeState != tc.wantState {
+ t.Fatalf("gas = <%d,%d>, want <%d,%d>", gp.cumulativeRegular, gp.cumulativeState, tc.wantRegular, tc.wantState)
+ }
+ })
+ }
+}
+
+// TestEIP2780Floor keeps the EIP-8037 calldata floor in the regular dimension
+// when a top-level EIP-2780 account-leaf charge is also present.
+func TestEIP2780Floor(t *testing.T) {
+ recipient := common.HexToAddress("0xbeef000000000000000000000000000000000007")
+ data := make([]byte, 1_000)
+ tx := callTx(0, recipient, 1, 300_000, data)
+ res, gp, err := applyMsg(t, mkState(senderAlloc(nil)), tx)
+ if err != nil || res.Err != nil {
+ t.Fatalf("result=%v err=%v", res, err)
+ }
+ floor, err := FloorDataGas(rules8037, senderAddr, &recipient, uint256.NewInt(1), data, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ intrinsic, err := IntrinsicGas(data, nil, nil, senderAddr, &recipient, uint256.NewInt(1), rules8037)
+ if err != nil {
+ t.Fatal(err)
+ }
+ stateGas := newAccountState
+ // This is the v7.2.0 boundary: the floor lifts only the regular
+ // dimension, while the scalar receipt gas remains the actual intrinsic +
+ // state charge because it is already above the floor.
+ if !(intrinsic < floor && floor < intrinsic+stateGas) {
+ t.Fatalf("expected intrinsic < floor < intrinsic + state: %d < %d < %d", intrinsic, floor, intrinsic+stateGas)
+ }
+ if gp.cumulativeRegular != floor || gp.cumulativeState != stateGas {
+ t.Fatalf("gas = <%d,%d>, want floor/state <%d,%d>", gp.cumulativeRegular, gp.cumulativeState, floor, stateGas)
+ }
+ if want := intrinsic + stateGas; res.UsedGas != want {
+ t.Fatalf("receipt gas = %d, want intrinsic + state = %d", res.UsedGas, want)
+ }
+}
+
// TestEIP2780FirstFrameHaltPreservesPreExecution verifies the gas and state
// semantics when the top-most frame — message call or creation — halts
// exceptionally after the pre-execution phase completed:
diff --git a/core/eip7708_test.go b/core/eip7708_test.go
new file mode 100644
index 0000000000..47199631b3
--- /dev/null
+++ b/core/eip7708_test.go
@@ -0,0 +1,413 @@
+// 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 .
+
+package core
+
+import (
+ "math/big"
+ "testing"
+
+ "github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/consensus/beacon"
+ "github.com/ethereum/go-ethereum/consensus/ethash"
+ "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"
+)
+
+// EIP-7708 transfer-log tests. The older TestEthTransferLogs keeps its
+// historical end-to-end Solidity fixture; these tests use small bytecode
+// programs to pin the normative edge cases independently.
+
+func transferLogs7708(sdb *state.StateDB) []*types.Log {
+ return sdb.GetLogs(common.Hash{}, 0, common.Hash{}, 0)
+}
+
+func assertTransfer7708(t *testing.T, log *types.Log, from, to common.Address, value uint64) {
+ t.Helper()
+ want := types.EthTransferLog(from, to, uint256.NewInt(value))
+ if log.Address != want.Address || len(log.Topics) != 3 || log.Topics[0] != want.Topics[0] ||
+ log.Topics[1] != want.Topics[1] || log.Topics[2] != want.Topics[2] || string(log.Data) != string(want.Data) {
+ t.Fatalf("transfer log = %+v, want %+v", log, want)
+ }
+}
+
+func callCode7708(target common.Address, value byte, suffix []byte) []byte {
+ return callOpcode7708(target, value, 0xf1, suffix)
+}
+
+func callOpcode7708(target common.Address, value, opcode byte, suffix []byte) []byte {
+ // Optional LOG0, then CALL(gas=0xffff, to=target, value=value, empty input
+ // and output), discard the success flag, and run suffix.
+ code := []byte{0x60, 0x00, 0x60, 0x00, 0xa0}
+ code = append(code,
+ 0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x60, 0x00, // output/input sizes and offsets
+ 0x60, value, 0x73)
+ code = append(code, target.Bytes()...)
+ code = append(code, 0x61, 0xff, 0xff, opcode, 0x50)
+ return append(code, suffix...)
+}
+
+func delegateCode7708(target common.Address) []byte {
+ // DELEGATECALL(gas=0xffff, to=target, empty input and output), followed by
+ // STOP. Value is inherited from the outer call's executing context.
+ code := []byte{0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x73}
+ code = append(code, target.Bytes()...)
+ return append(code, 0x61, 0xff, 0xff, 0xf4, 0x50, 0x00)
+}
+
+func createTx7708(value uint64, initcode []byte) *types.Transaction {
+ return types.MustSignNewTx(senderKey, signer8037, &types.DynamicFeeTx{
+ ChainID: cfg8037.ChainID, Nonce: 0, Value: new(big.Int).SetUint64(value),
+ Gas: 500_000, GasFeeCap: new(big.Int), GasTipCap: new(big.Int), Data: initcode,
+ })
+}
+
+// TestEIP7708Transactions covers ordinary transaction value transfers across
+// the principal transaction encodings, including a delegated recipient.
+func TestEIP7708Transactions(t *testing.T) {
+ recipient := common.HexToAddress("0x7708000000000000000000000000000000000001")
+ auth, _ := signAuth(t, authKeyA, delegate8037, 0)
+ cases := []struct {
+ name string
+ tx func() *types.Transaction
+ }{
+ {
+ "legacy",
+ func() *types.Transaction {
+ return types.MustSignNewTx(senderKey, signer8037, &types.LegacyTx{
+ Nonce: 0, To: &recipient, Value: big.NewInt(1), Gas: 100_000, GasPrice: new(big.Int),
+ })
+ },
+ },
+ {
+ "access-list",
+ func() *types.Transaction {
+ return types.MustSignNewTx(senderKey, signer8037, &types.AccessListTx{
+ ChainID: cfg8037.ChainID, Nonce: 0, To: &recipient, Value: big.NewInt(1), Gas: 100_000, GasPrice: new(big.Int),
+ })
+ },
+ },
+ {"dynamic", func() *types.Transaction { return callTx(0, recipient, 1, 100_000, nil) }},
+ {"set-code", func() *types.Transaction {
+ return setCodeTxGas(0, recipient, 1, 1_000_000, []types.SetCodeAuthorization{auth})
+ }},
+ }
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ sdb := mkState(senderAlloc(types.GenesisAlloc{recipient: {Balance: big.NewInt(1)}}))
+ res, _, err := applyMsg(t, sdb, tc.tx())
+ if err != nil || res.Err != nil {
+ t.Fatalf("result=%v err=%v", res, err)
+ }
+ logs := transferLogs7708(sdb)
+ if len(logs) != 1 {
+ t.Fatalf("logs = %+v, want one", logs)
+ }
+ assertTransfer7708(t, logs[0], senderAddr, recipient, 1)
+ })
+ }
+
+ delegated := common.HexToAddress("0x7708000000000000000000000000000000000002")
+ sdb := mkState(senderAlloc(types.GenesisAlloc{
+ delegated: {Code: types.AddressToDelegation(delegate8037)},
+ delegate8037: {Code: []byte{0x00}},
+ }))
+ res, _, err := applyMsg(t, sdb, callTx(0, delegated, 1, 100_000, nil))
+ if err != nil || res.Err != nil {
+ t.Fatalf("delegated result=%v err=%v", res, err)
+ }
+ logs := transferLogs7708(sdb)
+ if len(logs) != 1 {
+ t.Fatalf("delegated logs = %+v, want one", logs)
+ }
+ // The transfer is to the delegation account, never to its implementation.
+ assertTransfer7708(t, logs[0], senderAddr, delegated, 1)
+}
+
+// TestEIP7708Special covers recipients that are frequently warmed or handled
+// specially by the EVM, and checks that fee recipients do not produce logs.
+func TestEIP7708Special(t *testing.T) {
+ precompile := common.BytesToAddress([]byte{4})
+ system := params.SystemAddress
+ coinbase := common.HexToAddress("0x7708000000000000000000000000000000000003")
+ cases := []struct {
+ name string
+ to common.Address
+ coinbase bool
+ }{
+ {"precompile", precompile, false},
+ {"system", system, false},
+ {"coinbase", coinbase, true},
+ }
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ sdb := mkState(senderAlloc(nil))
+ tx := callTx(0, tc.to, 1, 300_000, nil)
+ var (
+ res *ExecutionResult
+ err error
+ )
+ if tc.coinbase {
+ res, _, err = applyMsgCoinbase(t, sdb, tx, coinbase)
+ } else {
+ res, _, err = applyMsg(t, sdb, tx)
+ }
+ if err != nil || res.Err != nil {
+ t.Fatalf("result=%v err=%v", res, err)
+ }
+ logs := transferLogs7708(sdb)
+ if len(logs) != 1 {
+ t.Fatalf("logs = %+v, want one transfer only", logs)
+ }
+ assertTransfer7708(t, logs[0], senderAddr, tc.to, 1)
+ })
+ }
+}
+
+// TestEIP7708Calls checks ordering, attribution, and rollback for value CALLs.
+func TestEIP7708Calls(t *testing.T) {
+ caller := common.HexToAddress("0x7708000000000000000000000000000000000010")
+ callee := common.HexToAddress("0x7708000000000000000000000000000000000011")
+ reverter := common.HexToAddress("0x7708000000000000000000000000000000000012")
+
+ t.Run("order", func(t *testing.T) {
+ sdb := mkState(senderAlloc(types.GenesisAlloc{
+ caller: {Code: callCode7708(callee, 3, []byte{0x00})},
+ callee: {Code: []byte{0x00}},
+ }))
+ res, _, err := applyMsg(t, sdb, callTx(0, caller, 10, 300_000, nil))
+ if err != nil || res.Err != nil {
+ t.Fatalf("result=%v err=%v", res, err)
+ }
+ logs := transferLogs7708(sdb)
+ if len(logs) != 3 {
+ t.Fatalf("logs = %+v, want tx transfer, LOG0, inner transfer", logs)
+ }
+ assertTransfer7708(t, logs[0], senderAddr, caller, 10)
+
+ // Ordinary log emitted by contract
+ if logs[1].Address != caller || len(logs[1].Topics) != 0 || len(logs[1].Data) != 0 {
+ t.Fatalf("ordinary log = %+v, want caller LOG0", logs[1])
+ }
+ assertTransfer7708(t, logs[2], caller, callee, 3)
+ })
+
+ t.Run("inner-revert", func(t *testing.T) {
+ sdb := mkState(senderAlloc(types.GenesisAlloc{
+ caller: {Code: callCode7708(reverter, 3, []byte{0x00})},
+ reverter: {Code: []byte{0x60, 0x00, 0x60, 0x00, 0xfd}},
+ }))
+ res, _, err := applyMsg(t, sdb, callTx(0, caller, 10, 300_000, nil))
+ if err != nil || res.Err != nil {
+ t.Fatalf("result=%v err=%v", res, err)
+ }
+ logs := transferLogs7708(sdb)
+ if len(logs) != 2 { // top-level transfer and the caller's LOG0
+ t.Fatalf("logs = %+v, want no rolled-back inner transfer", logs)
+ }
+ assertTransfer7708(t, logs[0], senderAddr, caller, 10)
+ })
+
+ t.Run("outer-revert", func(t *testing.T) {
+ sdb := mkState(senderAlloc(types.GenesisAlloc{
+ caller: {Code: callCode7708(callee, 3, []byte{0x60, 0x00, 0x60, 0x00, 0xfd})},
+ callee: {Code: []byte{0x00}},
+ }))
+ res, _, err := applyMsg(t, sdb, callTx(0, caller, 10, 300_000, nil))
+ if err != nil || res.Err != vm.ErrExecutionReverted {
+ t.Fatalf("result=%v err=%v, want execution revert", res, err)
+ }
+ if logs := transferLogs7708(sdb); len(logs) != 0 {
+ t.Fatalf("reverted transaction retained logs: %+v", logs)
+ }
+ })
+
+ t.Run("callcode", func(t *testing.T) {
+ sdb := mkState(senderAlloc(types.GenesisAlloc{
+ caller: {Code: callOpcode7708(callee, 3, 0xf2, []byte{0x00})},
+ callee: {Code: []byte{0x00}},
+ }))
+ res, _, err := applyMsg(t, sdb, callTx(0, caller, 10, 300_000, nil))
+ if err != nil || res.Err != nil {
+ t.Fatalf("result=%v err=%v", res, err)
+ }
+ logs := transferLogs7708(sdb)
+ if len(logs) != 2 {
+ t.Fatalf("logs = %+v, want only top-level transfer and LOG0", logs)
+ }
+ assertTransfer7708(t, logs[0], senderAddr, caller, 10)
+ })
+
+ t.Run("delegatecall", func(t *testing.T) {
+ implementation := common.HexToAddress("0x7708000000000000000000000000000000000013")
+ sdb := mkState(senderAlloc(types.GenesisAlloc{
+ caller: {Code: delegateCode7708(implementation)},
+ implementation: {Code: callCode7708(callee, 3, []byte{0x00})},
+ callee: {Code: []byte{0x00}},
+ }))
+ res, _, err := applyMsg(t, sdb, callTx(0, caller, 10, 300_000, nil))
+ if err != nil || res.Err != nil {
+ t.Fatalf("result=%v err=%v", res, err)
+ }
+ logs := transferLogs7708(sdb)
+ if len(logs) != 3 {
+ t.Fatalf("logs = %+v, want top-level transfer, delegated LOG0, inner transfer", logs)
+ }
+ assertTransfer7708(t, logs[0], senderAddr, caller, 10)
+ if logs[1].Address != caller {
+ t.Fatalf("delegated LOG0 address = %s, want executing context %s", logs[1].Address, caller)
+ }
+ assertTransfer7708(t, logs[2], caller, callee, 3)
+ })
+}
+
+// TestEIP7708Create checks successful CREATE/CREATE2 endowments and that
+// failing initcode rolls the transfer log back with the failed creation.
+func TestEIP7708Create(t *testing.T) {
+ t.Run("transaction", func(t *testing.T) {
+ sdb := mkState(senderAlloc(nil))
+ res, _, err := applyMsg(t, sdb, createTx7708(5, []byte{0x00}))
+ if err != nil || res.Err != nil {
+ t.Fatalf("result=%v err=%v", res, err)
+ }
+ logs := transferLogs7708(sdb)
+ if len(logs) != 1 {
+ t.Fatalf("logs = %+v, want one", logs)
+ }
+ assertTransfer7708(t, logs[0], senderAddr, crypto.CreateAddress(senderAddr, 0), 5)
+ })
+
+ t.Run("revert", func(t *testing.T) {
+ sdb := mkState(senderAlloc(nil))
+ res, _, err := applyMsg(t, sdb, createTx7708(5, []byte{0x60, 0x00, 0x60, 0x00, 0xfd}))
+ if err != nil || res.Err != vm.ErrExecutionReverted {
+ t.Fatalf("result=%v err=%v, want execution revert", res, err)
+ }
+ if logs := transferLogs7708(sdb); len(logs) != 0 {
+ t.Fatalf("failed create retained logs: %+v", logs)
+ }
+ })
+
+ t.Run("create2", func(t *testing.T) {
+ sdb := mkState(senderAlloc(nil))
+ evm := amsterdamCoreEVM(sdb)
+ _, created, _, err := evm.Create2(senderAddr, []byte{0x00}, vm.NewGasBudget(500_000, 0), uint256.NewInt(5), new(uint256.Int))
+ if err != nil {
+ t.Fatal(err)
+ }
+ logs := transferLogs7708(sdb)
+ if len(logs) != 1 {
+ t.Fatalf("logs = %+v, want one", logs)
+ }
+ assertTransfer7708(t, logs[0], senderAddr, created, 5)
+ })
+}
+
+// TestEIP7708Selfdestruct verifies the EIP-8246-compatible SELFDESTRUCT
+// cases: different beneficiaries transfer and log, self beneficiaries do not.
+func TestEIP7708Selfdestruct(t *testing.T) {
+ contract := common.HexToAddress("0x7708000000000000000000000000000000000020")
+ beneficiary := common.HexToAddress("0x7708000000000000000000000000000000000021")
+ code := append([]byte{0x73}, beneficiary.Bytes()...)
+ code = append(code, 0xff)
+ sdb := mkState(senderAlloc(types.GenesisAlloc{contract: {Code: code}}))
+ res, _, err := applyMsg(t, sdb, callTx(0, contract, 7, 300_000, nil))
+ if err != nil || res.Err != nil {
+ t.Fatalf("result=%v err=%v", res, err)
+ }
+ logs := transferLogs7708(sdb)
+ if len(logs) != 2 {
+ t.Fatalf("logs = %+v, want transaction and selfdestruct transfers", logs)
+ }
+ assertTransfer7708(t, logs[0], senderAddr, contract, 7)
+ assertTransfer7708(t, logs[1], contract, beneficiary, 7)
+
+ selfCode := append([]byte{0x73}, contract.Bytes()...)
+ selfCode = append(selfCode, 0xff)
+ sdb = mkState(senderAlloc(types.GenesisAlloc{contract: {Code: selfCode}}))
+ res, _, err = applyMsg(t, sdb, callTx(0, contract, 7, 300_000, nil))
+ if err != nil || res.Err != nil {
+ t.Fatalf("self result=%v err=%v", res, err)
+ }
+ logs = transferLogs7708(sdb)
+ if len(logs) != 1 {
+ t.Fatalf("selfdestruct-to-self logs = %+v, want top-level transfer only", logs)
+ }
+ assertTransfer7708(t, logs[0], senderAddr, contract, 7)
+}
+
+// TestEIP7708Negative and TestEIP7708Transition pin the no-log cases and the
+// activation guard independently of transaction construction.
+func TestEIP7708Negative(t *testing.T) {
+ recipient := common.HexToAddress("0x7708000000000000000000000000000000000030")
+ for _, tc := range []struct {
+ name string
+ to common.Address
+ value int64
+ }{
+ {"zero", recipient, 0},
+ {"self", senderAddr, 1},
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ sdb := mkState(senderAlloc(nil))
+ res, _, err := applyMsg(t, sdb, callTx(0, tc.to, tc.value, 100_000, nil))
+ if err != nil || res.Err != nil {
+ t.Fatalf("result=%v err=%v", res, err)
+ }
+ if logs := transferLogs7708(sdb); len(logs) != 0 {
+ t.Fatalf("unexpected logs: %+v", logs)
+ }
+ })
+ }
+}
+
+func TestEIP7708Transition(t *testing.T) {
+ from := common.HexToAddress("0x7708000000000000000000000000000000000040")
+ to := common.HexToAddress("0x7708000000000000000000000000000000000041")
+ sdb := mkState(types.GenesisAlloc{from: {Balance: big.NewInt(2)}})
+ Transfer(sdb, from, to, uint256.NewInt(1), ¶ms.Rules{})
+ if logs := transferLogs7708(sdb); len(logs) != 0 {
+ t.Fatalf("pre-Amsterdam logs = %+v, want none", logs)
+ }
+ Transfer(sdb, from, to, uint256.NewInt(1), &rules8037)
+ logs := transferLogs7708(sdb)
+ if len(logs) != 1 {
+ t.Fatalf("Amsterdam logs = %+v, want one", logs)
+ }
+ assertTransfer7708(t, logs[0], from, to, 1)
+}
+
+// TestEIP7708Fees ensures a priority-fee credit to coinbase remains outside
+// EIP-7708: the transaction value transfer is the receipt's only log.
+func TestEIP7708Fees(t *testing.T) {
+ env := newBALTestEnv(nil)
+ coinbase := common.HexToAddress("0x7708000000000000000000000000000000000050")
+ recipient := common.HexToAddress("0x7708000000000000000000000000000000000051")
+ engine := beacon.New(ethash.NewFaker())
+ _, _, receipts := GenerateChainWithGenesis(env.gspec, engine, 1, func(_ int, g *BlockGen) {
+ g.SetCoinbase(coinbase)
+ g.AddTx(env.tx(0, &recipient, big.NewInt(1), txGasNewAccount, 1, nil))
+ })
+ logs := receipts[0][0].Logs
+ if len(logs) != 1 {
+ t.Fatalf("logs = %+v, want transaction transfer only", logs)
+ }
+ assertTransfer7708(t, logs[0], env.from, recipient, 1)
+}
diff --git a/core/bal_test.go b/core/eip7928_test.go
similarity index 63%
rename from core/bal_test.go
rename to core/eip7928_test.go
index 64c2714b16..816161a34f 100644
--- a/core/bal_test.go
+++ b/core/eip7928_test.go
@@ -152,6 +152,52 @@ func assertEmpty(t *testing.T, aa *bal.AccountAccess) {
}
}
+type balStorageChangeExpectation struct {
+ index uint32
+ value common.Hash
+}
+
+func assertStorageChanges(t *testing.T, aa *bal.AccountAccess, key common.Hash, want []balStorageChangeExpectation) {
+ t.Helper()
+ wantKey := new(uint256.Int).SetBytes(key[:])
+ for _, slot := range aa.StorageChanges {
+ if slot.Slot.Cmp(wantKey) != 0 {
+ continue
+ }
+ if len(slot.SlotChanges) != len(want) {
+ t.Fatalf("slot %x changes: have %+v, want %d entries", key, slot.SlotChanges, len(want))
+ }
+ for i, expected := range want {
+ if slot.SlotChanges[i].BlockAccessIndex != expected.index {
+ t.Fatalf("slot %x change %d index: have %d, want %d", key, i, slot.SlotChanges[i].BlockAccessIndex, expected.index)
+ }
+ wantValue := new(uint256.Int).SetBytes(expected.value[:])
+ if slot.SlotChanges[i].PostValue.Cmp(wantValue) != 0 {
+ t.Fatalf("slot %x change %d value: have %s, want %s", key, i, slot.SlotChanges[i].PostValue, wantValue)
+ }
+ }
+ return
+ }
+ t.Fatalf("slot %x missing from storage_changes", key)
+}
+
+func assertStorageChangeAt(t *testing.T, aa *bal.AccountAccess, key common.Hash, index uint32) {
+ t.Helper()
+ wantKey := new(uint256.Int).SetBytes(key[:])
+ for _, slot := range aa.StorageChanges {
+ if slot.Slot.Cmp(wantKey) != 0 {
+ continue
+ }
+ for _, change := range slot.SlotChanges {
+ if change.BlockAccessIndex == index {
+ return
+ }
+ }
+ t.Fatalf("slot %x has no change at index %d: %+v", key, index, slot.SlotChanges)
+ }
+ t.Fatalf("slot %x missing from storage_changes", key)
+}
+
// txGasNewAccount covers the base tx cost plus the EIP-8037 account-creation
// state-gas charge (STATE_BYTES_PER_NEW_ACCOUNT × CPSB ≈ 183,600) that is
// incurred when value is transferred to a non-existent account under Amsterdam.
@@ -230,21 +276,30 @@ func TestBALEmptyBlockExcludesCoinbase(t *testing.T) {
assertAbsent(t, b, coinbase)
}
-// TestBALCoinbaseTipCapturesBalance: positive priority fee credits coinbase
-// and the balance change appears in the BAL.
-func TestBALCoinbaseTipCapturesBalance(t *testing.T) {
- coinbase := common.Address{0xc0}
- to := common.HexToAddress("0xabba")
+// TestBALCoinbasePerTxBalance checks that fee-recipient balances are recorded
+// after each transaction, rather than only once at the end of the block.
+func TestBALCoinbasePerTxBalance(t *testing.T) {
+ coinbase := common.HexToAddress("0xc01babe")
+ to := common.HexToAddress("0xc0ffee")
env := newBALTestEnv(nil)
- b, _ := env.run(t, func(g *BlockGen) {
+ b, receipts := env.run(t, func(g *BlockGen) {
g.SetCoinbase(coinbase)
- g.AddTx(env.tx(0, &to, big.NewInt(0), params.TxGas, 2 /* gwei tip */, nil))
+ g.AddTx(env.tx(0, &to, big.NewInt(0), 100_000, 1, nil))
+ g.AddTx(env.tx(1, &to, big.NewInt(0), 100_000, 1, nil))
})
- cb := assertPresent(t, b, coinbase)
- if len(cb.BalanceChanges) == 0 || cb.BalanceChanges[0].PostBalance.Sign() == 0 {
- t.Fatalf("coinbase missing positive balance change: %+v", cb.BalanceChanges)
+ feeRecipient := assertPresent(t, b, coinbase)
+ if len(feeRecipient.BalanceChanges) != 2 {
+ t.Fatalf("fee recipient must have one balance per transaction: %+v", feeRecipient.BalanceChanges)
+ }
+ first := new(big.Int).Mul(new(big.Int).SetUint64(receipts[0].GasUsed), newGwei(1))
+ second := new(big.Int).Add(first, new(big.Int).Mul(new(big.Int).SetUint64(receipts[1].GasUsed), newGwei(1)))
+ for i, want := range []*big.Int{first, second} {
+ change := feeRecipient.BalanceChanges[i]
+ if change.BlockAccessIndex != uint32(i+1) || change.PostBalance.ToBig().Cmp(want) != 0 {
+ t.Fatalf("fee recipient change %d: have %+v, want index %d balance %s", i, change, i+1, want)
+ }
}
}
@@ -582,7 +637,7 @@ func TestBALCallCodeToDelegatedTargetBalanceFail(t *testing.T) {
assertEmpty(t, assertPresent(t, b, impl))
}
-// ============================== Revert behaviour ==============================
+// ============================== Reverts and exceptional halts ==============================
// TestBALRevertedTxStillIncluded: a tx whose top-level call REVERTs still
// records the touched contract in the BAL with an empty change set.
@@ -619,30 +674,32 @@ func TestBALSenderRecordedOnRevert(t *testing.T) {
}
}
-// ============================== Storage inclusion ==============================
-
-// TestBALStorageWriteRecorded: SSTORE places the slot in storage_changes and
-// keeps it out of storage_reads.
-func TestBALStorageWriteRecorded(t *testing.T) {
- contract := common.HexToAddress("0xc1")
- slot := common.BigToHash(big.NewInt(0x01))
- // PUSH1 0x42 PUSH1 0x01 SSTORE STOP
- code := []byte{0x60, 0x42, 0x60, 0x01, 0x55, 0x00}
- env := newBALTestEnv(types.GenesisAlloc{contract: {Code: code, Balance: common.Big0}})
-
- b, _ := env.run(t, func(g *BlockGen) {
- g.AddTx(env.tx(0, &contract, big.NewInt(0), 1_000_000, 0, nil))
+// TestBALDelegationTargetOOG exercises the exceptional-halt
+// boundary in EIP-7928. The top-level recipient is loaded to discover its
+// delegation, but the runtime budget is insufficient for the cold target
+// access, so the implementation must not enter the BAL.
+func TestBALDelegationTargetOOG(t *testing.T) {
+ authority := common.HexToAddress("0xa7702")
+ implementation := common.HexToAddress("0x1a11")
+ env := newBALTestEnv(types.GenesisAlloc{
+ authority: {Code: types.AddressToDelegation(implementation), Balance: common.Big0},
+ implementation: {Code: []byte{0x00}, Balance: common.Big0},
})
- aa := assertPresent(t, b, contract)
- if !hasStorageWrite(b, contract, slot) {
- t.Fatalf("expected slot 0x01 in storage_changes\n%s", b.PrettyPrint())
- }
- if hasSlotIn(aa.StorageReads, slot) {
- t.Fatalf("slot 0x01 must NOT appear in storage_reads")
+ b, receipts := env.run(t, func(g *BlockGen) {
+ // This transaction has exactly enough gas for its intrinsic charges, but
+ // less than the 3,000 cold-account runtime charge needed to load target.
+ g.AddTx(env.tx(0, &authority, big.NewInt(0), 15_000, 0, nil))
+ })
+ if receipts[0].Status != types.ReceiptStatusFailed {
+ t.Fatalf("expected runtime out-of-gas receipt, have status %d", receipts[0].Status)
}
+ assertEmpty(t, assertPresent(t, b, authority))
+ assertAbsent(t, b, implementation)
}
+// ============================== Storage inclusion ==============================
+
// TestBALStorageSloadOnly: SLOAD without a write puts the slot in storage_reads.
func TestBALStorageSloadOnly(t *testing.T) {
contract := common.HexToAddress("0xc1")
@@ -890,6 +947,38 @@ func TestBALInEVMCreatePreAccessAbortDestinationExcluded(t *testing.T) {
}
}
+// TestBALInEVMCreateOOGDestination distinguishes a CREATE precheck abort from
+// an account-creation runtime OOG. The latter calls StateDB.Empty on the
+// destination to determine whether the creation charge is due, so the
+// destination has been accessed and must appear in the BAL even though the
+// failed charge halts the transaction before evm.create runs.
+func TestBALInEVMCreateOOGDestination(t *testing.T) {
+ factory := common.HexToAddress("0xfac4")
+ // PUSH1 0 (length) PUSH1 0 (offset) PUSH1 0 (value) CREATE POP STOP.
+ // The factory has enough regular gas for CREATE's opcode cost but not enough
+ // combined gas to pay Amsterdam's 183,600 account-creation state charge.
+ code := []byte{0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0xf0, 0x50, 0x00}
+ env := newBALTestEnv(types.GenesisAlloc{
+ factory: {Code: code, Balance: common.Big0, Nonce: 1},
+ })
+
+ b, receipts := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &factory, big.NewInt(0), 30_000, 0, nil))
+ })
+ if receipts[0].Status != types.ReceiptStatusFailed {
+ t.Fatalf("expected account-creation runtime OOG, have status %d", receipts[0].Status)
+ }
+
+ wouldBeDest := crypto.CreateAddress(factory, 1)
+ assertEmpty(t, assertPresent(t, b, wouldBeDest))
+
+ // evm.create is never entered, so its creator-nonce bump does not occur.
+ aa := assertPresent(t, b, factory)
+ if len(aa.NonceChanges) != 0 {
+ t.Fatalf("factory nonce must not be bumped before account-creation charge succeeds: %+v", aa.NonceChanges)
+ }
+}
+
// TestBALInEVMCreateDeploysContract: a CREATE issued by an existing contract
// (not a top-level CREATE tx) records the deployed address in the BAL.
func TestBALInEVMCreateDeploysContract(t *testing.T) {
@@ -1066,7 +1155,60 @@ func TestBALSelfDestructToSelfPrefundedUnchanged(t *testing.T) {
assertEmpty(t, aa)
}
-// ============================== Mid-tx balance round-trip ==============================
+// TestBALSelfDestructStorageRead checks the in-transaction
+// SELFDESTRUCT rule: storage changed by an account that is subsequently
+// deleted is retained as a read footprint, not as a storage change. The
+// deleted account also must not carry nonce or code changes.
+func TestBALSelfDestructStorageRead(t *testing.T) {
+ beneficiary := common.HexToAddress("0xbeefbeef")
+ slot := common.BigToHash(big.NewInt(0x05))
+ env := newBALTestEnv(nil)
+ // SSTORE(5, 0x42); SELFDESTRUCT(beneficiary). This executes in init code,
+ // so the created account is deleted under EIP-6780.
+ init := []byte{0x60, 0x42, 0x60, 0x05, 0x55, 0x73}
+ init = append(init, beneficiary.Bytes()...)
+ init = append(init, 0xff)
+
+ b, receipts := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, nil, big.NewInt(100), 1_000_000, 0, init))
+ })
+
+ deleted := assertPresent(t, b, receipts[0].ContractAddress)
+ if !hasSlotIn(deleted.StorageReads, slot) {
+ t.Fatalf("deleted account storage slot %x must be in storage_reads\n%s", slot, b.PrettyPrint())
+ }
+ if hasStorageWrite(b, deleted.Address, slot) {
+ t.Fatalf("deleted account storage slot %x must not remain in storage_changes\n%s", slot, b.PrettyPrint())
+ }
+ if len(deleted.NonceChanges) != 0 || len(deleted.CodeChanges) != 0 {
+ t.Fatalf("deleted account must not record nonce or code: %+v", deleted)
+ }
+}
+
+// TestBALSelfDestructDelegatedBeneficiary checks that SELFDESTRUCT treats a
+// delegated authority as its beneficiary account, without loading the
+// implementation as executable code.
+func TestBALSelfDestructDelegatedBeneficiary(t *testing.T) {
+ victim := common.HexToAddress("0x5e1f")
+ authority := common.HexToAddress("0xa7702")
+ implementation := common.HexToAddress("0x1a11")
+ victimCode := append([]byte{0x73}, authority.Bytes()...)
+ victimCode = append(victimCode, 0xff) // SELFDESTRUCT
+ env := newBALTestEnv(types.GenesisAlloc{
+ victim: {Code: victimCode, Balance: common.Big0},
+ authority: {Code: types.AddressToDelegation(implementation), Balance: common.Big0},
+ implementation: {Code: []byte{0x00}, Balance: common.Big0},
+ })
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &victim, big.NewInt(0), 1_000_000, 0, nil))
+ })
+
+ assertEmpty(t, assertPresent(t, b, authority))
+ assertAbsent(t, b, implementation)
+}
+
+// ============================== Balance accounting ==============================
// TestBALMidTxBalanceRoundTrip: when an address's balance changes during a
// transaction but returns to its pre-transaction value, the address is still
@@ -1107,6 +1249,38 @@ func TestBALMidTxBalanceRoundTrip(t *testing.T) {
}
}
+// TestBALGasRefundSenderBalance ensures a gas-refunding SSTORE still records
+// the sender's post-transaction balance, after the refund has been applied
+// to the transaction's final gas charge.
+func TestBALGasRefundSenderBalance(t *testing.T) {
+ contract := common.HexToAddress("0xc1")
+ slot := common.BigToHash(big.NewInt(0x05))
+ env := newBALTestEnv(types.GenesisAlloc{
+ contract: {
+ Code: []byte{0x5f, 0x60, 0x05, 0x55, 0x00}, // SSTORE(5, 0); STOP
+ Balance: common.Big0,
+ Storage: map[common.Hash]common.Hash{slot: common.BigToHash(big.NewInt(1))},
+ },
+ })
+
+ _, blocks, receipts := GenerateChainWithGenesis(env.gspec, beacon.New(ethash.NewFaker()), 1, func(_ int, g *BlockGen) {
+ g.AddTx(env.tx(0, &contract, big.NewInt(0), 1_000_000, 0, nil))
+ })
+ b := blocks[0].AccessList()
+ if b == nil {
+ t.Fatal("expected non-nil block access list")
+ }
+ sender := assertPresent(t, b, env.from)
+ if len(sender.BalanceChanges) != 1 || sender.BalanceChanges[0].BlockAccessIndex != 1 {
+ t.Fatalf("sender needs one post-tx balance at index 1: %+v", sender.BalanceChanges)
+ }
+ gasCost := new(big.Int).Mul(new(big.Int).SetUint64(receipts[0][0].GasUsed), blocks[0].BaseFee())
+ want := new(big.Int).Sub(newGwei(1_000_000_000), gasCost)
+ if sender.BalanceChanges[0].PostBalance.ToBig().Cmp(want) != 0 {
+ t.Fatalf("sender post-refund balance: have %s, want %s", sender.BalanceChanges[0].PostBalance, want)
+ }
+}
+
// ============================== System contracts (pre/post-execution) ==============================
// TestBALSystemContractsPresent: per EIP-7928, "System contract addresses
@@ -1146,6 +1320,107 @@ func TestBALSystemContractsPresent(t *testing.T) {
}
}
+// TestBALPreExecutionStorage checks the precise pre-execution entries required
+// by EIP-7928: both EIP-2935 and EIP-4788 changes belong to index zero, before
+// any transaction in the block.
+func TestBALPreExecutionStorage(t *testing.T) {
+ beaconRoot := common.HexToHash("0xbeac")
+ env := newBALTestEnv(nil)
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.SetParentBeaconRoot(beaconRoot)
+ })
+
+ // The generated block is number 1 and timestamp 10. EIP-2935 stores the
+ // parent hash at (block.number - 1) % 8191, i.e. slot zero here.
+ history := assertPresent(t, b, params.HistoryStorageAddress)
+ assertStorageChanges(t, history, common.Hash{}, []balStorageChangeExpectation{{
+ index: 0,
+ value: env.gspec.ToBlock().Hash(),
+ }})
+
+ // EIP-4788 stores timestamp at timestamp % 8191 and the parent beacon root
+ // at that slot plus 8191.
+ const timestamp = 10
+ beacon := assertPresent(t, b, params.BeaconRootsAddress)
+ assertStorageChanges(t, beacon, common.BigToHash(big.NewInt(timestamp)), []balStorageChangeExpectation{{
+ index: 0,
+ value: common.BigToHash(big.NewInt(timestamp)),
+ }})
+ assertStorageChanges(t, beacon, common.BigToHash(big.NewInt(timestamp+8191)), []balStorageChangeExpectation{{
+ index: 0,
+ value: beaconRoot,
+ }})
+}
+
+// TestBALPostExecutionQueueReads covers the EIP-7002 and EIP-7251 rule that
+// a post-execution system call accesses queue metadata in slots 0..3 at index
+// n+1, while the queued payload slots are read-only.
+func TestBALPostExecutionQueueReads(t *testing.T) {
+ withdrawalData := common.FromHex("b917cfdc0d25b72d55cf94db328e1629b7f4fde2c30cdacf873b664416f76a0c7f7cc50c9f72a3cb84be88144cde91250000000000000d80")
+ consolidationData := common.FromHex("b917cfdc0d25b72d55cf94db328e1629b7f4fde2c30cdacf873b664416f76a0c7f7cc50c9f72a3cb84be88144cde9125b9812f7d0b1f2f969b52bbb2d316b0c2fa7c9dba85c428c5e6c27766bcc4b0c6e874702ff1eb1c7024b08524a9771601")
+
+ for _, tc := range []struct {
+ name string
+ addr common.Address
+ data []byte
+ }{
+ {"withdrawal queue (EIP-7002)", params.WithdrawalQueueAddress, withdrawalData},
+ {"consolidation queue (EIP-7251)", params.ConsolidationQueueAddress, consolidationData},
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ env := newBALTestEnv(nil)
+ // A request transaction writes several new storage slots under
+ // Amsterdam's state-gas schedule. Raise the test chain's gas limit so
+ // all 17 requests fit in the first block.
+ env.gspec.GasLimit = 200_000_000
+ _, blocks, _ := GenerateChainWithGenesis(env.gspec, beacon.New(ethash.NewFaker()), 2, func(i int, g *BlockGen) {
+ if i == 1 {
+ // Make the post-execution system call occur after one ordinary
+ // transaction, proving it uses n + 1 rather than a fixed index.
+ to := common.HexToAddress("0xf00")
+ g.AddTx(env.tx(17, &to, big.NewInt(0), 100_000, 0, nil))
+ return
+ }
+ // The system call processes at most 16 requests per block. Leave one
+ // payload for block 2 so its storage access is genuinely a read.
+ for nonce := uint64(0); nonce < 17; nonce++ {
+ g.AddTx(env.tx(nonce, &tc.addr, newGwei(1), 5_000_000, 0, tc.data))
+ }
+ })
+
+ b := blocks[1].AccessList()
+ if b == nil {
+ t.Fatal("expected non-nil block access list")
+ }
+ aa := assertPresent(t, b, tc.addr)
+ // Block 2 contains one user transaction, so its post-execution call is
+ // index 2 (= n + 1).
+ for slot := int64(0); slot < 4; slot++ {
+ key := common.BigToHash(big.NewInt(slot))
+ if hasSlotIn(aa.StorageReads, key) {
+ continue // A metadata slot whose value is unchanged is read-only.
+ }
+ assertStorageChangeAt(t, aa, key, 2)
+ }
+ foundPayloadRead := false
+ for _, slot := range aa.StorageReads {
+ if slot.Uint64() >= 4 {
+ foundPayloadRead = true
+ }
+ }
+ if !foundPayloadRead {
+ t.Fatalf("post-execution queue call must leave payload slots in storage_reads\n%s", b.PrettyPrint())
+ }
+ for _, change := range aa.StorageChanges {
+ if change.Slot.Uint64() >= 4 {
+ t.Fatalf("post-execution queue call must not write payload slot %s\n%s", change.Slot, b.PrettyPrint())
+ }
+ }
+ })
+ }
+}
+
// ============================== Withdrawals ==============================
// TestBALWithdrawalZeroAmountIncluded: a withdrawal with amount 0 still puts
@@ -1335,6 +1610,49 @@ func TestBALAuthFailedAfterLoadEmptyChangeSet(t *testing.T) {
}
}
+// TestBALAuthOOGRecipientExcluded covers the exceptional halt boundary where
+// an EIP-7702 authorization exhausts runtime gas before the transaction's
+// recipient is loaded. The authority was already loaded by authorization
+// validation, but tx.to must not enter the BAL.
+func TestBALAuthOOGRecipientExcluded(t *testing.T) {
+ authKey, _ := crypto.HexToECDSA("0202020202020202020202020202020202020202020202020202002020202020")
+ authority := crypto.PubkeyToAddress(authKey.PublicKey)
+ recipient := common.HexToAddress("0xdec1a1")
+ implementation := common.HexToAddress("0x1a11")
+ env := newBALTestEnv(nil)
+ auth, err := types.SignSetCode(authKey, types.SetCodeAuthorization{
+ ChainID: *uint256.MustFromBig(env.cfg.ChainID),
+ Address: implementation,
+ Nonce: 0,
+ })
+ if err != nil {
+ t.Fatalf("sign auth: %v", err)
+ }
+
+ b, receipts := env.run(t, func(g *BlockGen) {
+ tx, err := types.SignTx(types.NewTx(&types.SetCodeTx{
+ ChainID: uint256.MustFromBig(env.cfg.ChainID),
+ Nonce: 0,
+ To: recipient,
+ Value: new(uint256.Int),
+ Gas: 30_000,
+ GasFeeCap: uint256.NewInt(uint64(newGwei(10).Int64())),
+ GasTipCap: new(uint256.Int),
+ AuthList: []types.SetCodeAuthorization{auth},
+ }), env.signer, env.key)
+ if err != nil {
+ t.Fatalf("sign SetCodeTx: %v", err)
+ }
+ g.AddTx(tx)
+ })
+ if receipts[0].Status != types.ReceiptStatusFailed {
+ t.Fatalf("expected authorization runtime OOG, have status %d", receipts[0].Status)
+ }
+ assertEmpty(t, assertPresent(t, b, authority))
+ assertAbsent(t, b, recipient)
+ assertAbsent(t, b, implementation)
+}
+
// TestBALMultipleAuthsOnlyLoadedIncluded: a SetCode tx with a mix of valid and
// pre-load-failed auths lists only the loaded authorities in the BAL.
func TestBALMultipleAuthsOnlyLoadedIncluded(t *testing.T) {
@@ -1453,3 +1771,409 @@ func TestBALAuthCodeOverwrittenFinalRecorded(t *testing.T) {
t.Fatalf("expected final nonce 2, got %+v", aa.NonceChanges)
}
}
+
+// TestBALReapplyDelegation covers an authorization whose final code equals its
+// pre-transaction code. The authority nonce changes, but the unchanged
+// delegation must not create a code change or implementation read.
+func TestBALReapplyDelegation(t *testing.T) {
+ authKey, _ := crypto.HexToECDSA("0202020202020202020202020202020202020202020202020202002020202020")
+ authority := crypto.PubkeyToAddress(authKey.PublicKey)
+ implementation := common.HexToAddress("0x1a11")
+ env := newBALTestEnv(types.GenesisAlloc{
+ authority: {Nonce: 1, Code: types.AddressToDelegation(implementation), Balance: common.Big0},
+ implementation: {Code: []byte{0x00}, Balance: common.Big0},
+ })
+ auth, err := types.SignSetCode(authKey, types.SetCodeAuthorization{
+ ChainID: *uint256.MustFromBig(env.cfg.ChainID),
+ Address: implementation,
+ Nonce: 1,
+ })
+ if err != nil {
+ t.Fatalf("sign auth: %v", err)
+ }
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.newSetCodeTx(t, 0, env.from, []types.SetCodeAuthorization{auth}))
+ })
+
+ aa := assertPresent(t, b, authority)
+ if len(aa.NonceChanges) != 1 || aa.NonceChanges[0].PostNonce != 2 {
+ t.Fatalf("reapplied delegation nonce: %+v", aa.NonceChanges)
+ }
+ if len(aa.CodeChanges) != 0 {
+ t.Fatalf("reapplying unchanged delegation must not record code: %+v", aa.CodeChanges)
+ }
+ assertAbsent(t, b, implementation)
+}
+
+// TestBALSetCodeTxDelegatedRecipient verifies that authorizations are applied
+// before the top-level call. A newly delegated tx.to must immediately load and
+// execute its implementation.
+func TestBALSetCodeTxDelegatedRecipient(t *testing.T) {
+ authKey, _ := crypto.HexToECDSA("0202020202020202020202020202020202020202020202020202002020202020")
+ authority := crypto.PubkeyToAddress(authKey.PublicKey)
+ implementation := common.HexToAddress("0x1a11")
+ slot := common.BigToHash(big.NewInt(0x07))
+ implementationCode := []byte{0x60, 0x07, 0x54, 0x50, 0x00} // SLOAD(7); POP; STOP.
+ env := newBALTestEnv(types.GenesisAlloc{
+ implementation: {Code: implementationCode, Balance: common.Big0},
+ })
+ auth, err := types.SignSetCode(authKey, types.SetCodeAuthorization{
+ ChainID: *uint256.MustFromBig(env.cfg.ChainID),
+ Address: implementation,
+ Nonce: 0,
+ })
+ if err != nil {
+ t.Fatalf("sign auth: %v", err)
+ }
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.newSetCodeTx(t, 0, authority, []types.SetCodeAuthorization{auth}))
+ })
+
+ authorityAccess := assertPresent(t, b, authority)
+ if len(authorityAccess.NonceChanges) != 1 || authorityAccess.NonceChanges[0].PostNonce != 1 {
+ t.Fatalf("authority nonce after installation: %+v", authorityAccess.NonceChanges)
+ }
+ if len(authorityAccess.CodeChanges) != 1 || !bytes.Equal(authorityAccess.CodeChanges[0].NewCode, types.AddressToDelegation(implementation)) {
+ t.Fatalf("authority delegation code after installation: %+v", authorityAccess.CodeChanges)
+ }
+ if !hasSlotIn(authorityAccess.StorageReads, slot) {
+ t.Fatalf("same-transaction delegated execution must read authority storage\n%s", b.PrettyPrint())
+ }
+ assertEmpty(t, assertPresent(t, b, implementation))
+}
+
+// ============================== Read-list regression cases ==============================
+
+// TestBALStorageWriteThenRead covers the opposite ordering of
+// TestBALStorageReadThenWriteOnlyInWrites. Once an index changes a slot, all
+// subsequent reads of that slot must be hidden by storage_changes.
+func TestBALStorageWriteThenRead(t *testing.T) {
+ contract := common.HexToAddress("0xc1")
+ slot := common.BigToHash(big.NewInt(0x05))
+ // SSTORE(0x05, 0x42); SLOAD(0x05); POP; STOP.
+ code := []byte{0x60, 0x42, 0x60, 0x05, 0x55, 0x60, 0x05, 0x54, 0x50, 0x00}
+ env := newBALTestEnv(types.GenesisAlloc{contract: {Code: code, Balance: common.Big0}})
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &contract, big.NewInt(0), 1_000_000, 0, nil))
+ })
+
+ aa := assertPresent(t, b, contract)
+ assertStorageChanges(t, aa, slot, []balStorageChangeExpectation{{index: 1, value: common.BigToHash(big.NewInt(0x42))}})
+ if hasSlotIn(aa.StorageReads, slot) {
+ t.Fatalf("written slot %x must not remain in storage_reads\n%s", slot, b.PrettyPrint())
+ }
+}
+
+// TestBALStorageNoOpAfterWrite checks that the no-op decision
+// is relative to the state before the current block-access index. Tx2 writes
+// the value that Tx1 already committed, so it must not replace Tx1's change
+// with a read entry.
+func TestBALStorageNoOpAfterWrite(t *testing.T) {
+ contract := common.HexToAddress("0xc1")
+ slot := common.BigToHash(big.NewInt(0x05))
+ // SSTORE(0x05, 0x42); STOP.
+ code := []byte{0x60, 0x42, 0x60, 0x05, 0x55, 0x00}
+ env := newBALTestEnv(types.GenesisAlloc{contract: {Code: code, Balance: common.Big0}})
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &contract, big.NewInt(0), 1_000_000, 0, nil))
+ g.AddTx(env.tx(1, &contract, big.NewInt(0), 1_000_000, 0, nil))
+ })
+
+ aa := assertPresent(t, b, contract)
+ assertStorageChanges(t, aa, slot, []balStorageChangeExpectation{{index: 1, value: common.BigToHash(big.NewInt(0x42))}})
+ if hasSlotIn(aa.StorageReads, slot) {
+ t.Fatalf("later no-op must not add slot %x to storage_reads\n%s", slot, b.PrettyPrint())
+ }
+}
+
+// TestBALStorageChangesAcrossTxs verifies that a slot changed at two
+// distinct transaction indices retains both post-index values, including a
+// later reset to its pre-block value.
+func TestBALStorageChangesAcrossTxs(t *testing.T) {
+ contract := common.HexToAddress("0xc1")
+ slot := common.BigToHash(big.NewInt(0x05))
+ // CALLDATALOAD(0); SSTORE(0x05, value); STOP.
+ code := []byte{0x60, 0x00, 0x35, 0x60, 0x05, 0x55, 0x00}
+ env := newBALTestEnv(types.GenesisAlloc{contract: {Code: code, Balance: common.Big0}})
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &contract, big.NewInt(0), 1_000_000, 0, common.LeftPadBytes([]byte{0x42}, 32)))
+ g.AddTx(env.tx(1, &contract, big.NewInt(0), 1_000_000, 0, make([]byte, 32)))
+ })
+
+ aa := assertPresent(t, b, contract)
+ assertStorageChanges(t, aa, slot, []balStorageChangeExpectation{
+ {index: 1, value: common.BigToHash(big.NewInt(0x42))},
+ {index: 2, value: common.Hash{}},
+ })
+ if hasSlotIn(aa.StorageReads, slot) {
+ t.Fatalf("slot with committed changes must not be in storage_reads\n%s", b.PrettyPrint())
+ }
+}
+
+// TestBALRevertedSStoreRead verifies that SSTORE performs an implicit read once
+// its access boundary is crossed. The later REVERT discards the write but not
+// that read footprint.
+func TestBALRevertedSStoreRead(t *testing.T) {
+ contract := common.HexToAddress("0xc1")
+ slot := common.BigToHash(big.NewInt(0x05))
+ // SSTORE(0x05, 0x42); REVERT(0, 0).
+ code := []byte{0x60, 0x42, 0x60, 0x05, 0x55, 0x60, 0x00, 0x60, 0x00, 0xfd}
+ env := newBALTestEnv(types.GenesisAlloc{contract: {Code: code, Balance: common.Big0}})
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &contract, big.NewInt(0), 1_000_000, 0, nil))
+ })
+
+ aa := assertPresent(t, b, contract)
+ if !hasSlotIn(aa.StorageReads, slot) {
+ t.Fatalf("reverted SSTORE must leave slot %x in storage_reads\n%s", slot, b.PrettyPrint())
+ }
+ if hasStorageWrite(b, contract, slot) {
+ t.Fatalf("reverted SSTORE must not leave a storage change\n%s", b.PrettyPrint())
+ }
+}
+
+// TestBALParentRevertSStoreRead checks the journal boundary between call frames:
+// a child returns successfully after SSTORE, then its parent reverts the whole
+// call tree. The child's slot is still a BAL read.
+func TestBALParentRevertSStoreRead(t *testing.T) {
+ child := common.HexToAddress("0xc1")
+ parent := common.HexToAddress("0xc2")
+ slot := common.BigToHash(big.NewInt(0x05))
+ childCode := []byte{0x60, 0x42, 0x60, 0x05, 0x55, 0x00}
+ parentCode := makeStubCaller(0xf1 /* CALL */, child)
+ parentCode = append(parentCode[:len(parentCode)-1], 0x60, 0x00, 0x60, 0x00, 0xfd)
+ env := newBALTestEnv(types.GenesisAlloc{
+ child: {Code: childCode, Balance: common.Big0},
+ parent: {Code: parentCode, Balance: common.Big0},
+ })
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &parent, big.NewInt(0), 1_000_000, 0, nil))
+ })
+
+ aa := assertPresent(t, b, child)
+ if !hasSlotIn(aa.StorageReads, slot) {
+ t.Fatalf("parent-reverted child write must leave slot %x in storage_reads\n%s", slot, b.PrettyPrint())
+ }
+ if hasStorageWrite(b, child, slot) {
+ t.Fatalf("parent-reverted child write must not leave a storage change\n%s", b.PrettyPrint())
+ }
+}
+
+// TestBALStorageReadsSorted exercises the final encoding of a read-only set.
+// Repeated reads must produce one key, and keys are ordered lexicographically
+// regardless of execution order.
+func TestBALStorageReadsSorted(t *testing.T) {
+ contract := common.HexToAddress("0xc1")
+ // SLOAD(9); SLOAD(1); SLOAD(9); STOP.
+ code := []byte{
+ 0x60, 0x09, 0x54, 0x50,
+ 0x60, 0x01, 0x54, 0x50,
+ 0x60, 0x09, 0x54, 0x50,
+ 0x00,
+ }
+ env := newBALTestEnv(types.GenesisAlloc{contract: {Code: code, Balance: common.Big0}})
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &contract, big.NewInt(0), 1_000_000, 0, nil))
+ })
+
+ aa := assertPresent(t, b, contract)
+ if len(aa.StorageReads) != 2 || aa.StorageReads[0].Uint64() != 1 || aa.StorageReads[1].Uint64() != 9 {
+ t.Fatalf("storage_reads must be deduplicated and sorted: %+v", aa.StorageReads)
+ }
+}
+
+// TestBALAccessListSlotExcluded ensures an EIP-2930 storage-key warming entry
+// changes gas only. It must not create a storage_reads entry unless the EVM
+// actually executes an access to that slot.
+func TestBALAccessListSlotExcluded(t *testing.T) {
+ contract := common.HexToAddress("0xc1")
+ slot := common.BigToHash(big.NewInt(0x07))
+ env := newBALTestEnv(types.GenesisAlloc{contract: {Code: []byte{0x00}, Balance: common.Big0}})
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ tx := types.MustSignNewTx(env.key, env.signer, &types.DynamicFeeTx{
+ ChainID: env.cfg.ChainID,
+ Nonce: 0,
+ To: &contract,
+ Value: new(big.Int),
+ Gas: 1_000_000,
+ GasFeeCap: newGwei(10),
+ GasTipCap: new(big.Int),
+ AccessList: types.AccessList{{
+ Address: contract,
+ StorageKeys: []common.Hash{slot},
+ }},
+ })
+ g.AddTx(tx)
+ })
+
+ aa := assertPresent(t, b, contract)
+ if hasSlotIn(aa.StorageReads, slot) || hasStorageWrite(b, contract, slot) {
+ t.Fatalf("untouched access-list slot %x must be absent from BAL\n%s", slot, b.PrettyPrint())
+ }
+}
+
+// ============================== EIP-7702 execution-time delegation ==============================
+
+func makeDelegationProbe(op byte, target common.Address) []byte {
+ if op == 0x3c { // EXTCODECOPY needs length, code offset and memory offset.
+ code := []byte{0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x73}
+ code = append(code, target.Bytes()...)
+ return append(code, op, 0x00)
+ }
+ code := append([]byte{0x73}, target.Bytes()...)
+ return append(code, op, 0x50, 0x00) // opcode, POP, STOP
+}
+
+// TestBALDelegationInspection ensures the code-reading opcodes observe an
+// authority's 7702 indicator as its own code. They access the authority
+// but MUST NOT load the implementation address.
+func TestBALDelegationInspection(t *testing.T) {
+ authority := common.HexToAddress("0xa7702")
+ implementation := common.HexToAddress("0x1a11")
+ for _, tc := range []struct {
+ name string
+ op byte
+ }{
+ {"balance", 0x31},
+ {"extcodesize", 0x3b},
+ {"extcodecopy", 0x3c},
+ {"extcodehash", 0x3f},
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ caller := common.HexToAddress("0xca11")
+ env := newBALTestEnv(types.GenesisAlloc{
+ caller: {Code: makeDelegationProbe(tc.op, authority), Balance: common.Big0},
+ authority: {Code: types.AddressToDelegation(implementation), Balance: common.Big0},
+ implementation: {Code: []byte{0x00}, Balance: common.Big0},
+ })
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &caller, big.NewInt(0), 1_000_000, 0, nil))
+ })
+ assertEmpty(t, assertPresent(t, b, authority))
+ assertAbsent(t, b, implementation)
+ })
+ }
+}
+
+// TestBALDelegatedCallStorage checks CALL's storage context. The implementation
+// code is fetched from implementation, but its SLOAD executes in the delegated
+// authority's storage, not implementation's.
+func TestBALDelegatedCallStorage(t *testing.T) {
+ authority := common.HexToAddress("0xa7702")
+ implementation := common.HexToAddress("0x1a11")
+ slot := common.BigToHash(big.NewInt(0x07))
+ implCode := []byte{0x60, 0x07, 0x54, 0x50, 0x00} // SLOAD(7), POP, STOP
+ env := newBALTestEnv(types.GenesisAlloc{
+ authority: {Code: types.AddressToDelegation(implementation), Balance: common.Big0},
+ implementation: {Code: implCode, Balance: common.Big0},
+ })
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &authority, big.NewInt(0), 1_000_000, 0, nil))
+ })
+
+ aa := assertPresent(t, b, authority)
+ if !hasSlotIn(aa.StorageReads, slot) {
+ t.Fatalf("delegated CALL read must belong to authority\n%s", b.PrettyPrint())
+ }
+ assertEmpty(t, assertPresent(t, b, implementation))
+}
+
+// TestBALDelegatedCallFamilyStorage checks the storage context of the CALL
+// family against a delegated authority: the resolved implementation supplies
+// code only, while the storage read belongs to the executing context — the
+// authority for CALL/STATICCALL, the caller for DELEGATECALL/CALLCODE.
+func TestBALDelegatedCallFamilyStorage(t *testing.T) {
+ caller := common.HexToAddress("0xca11")
+ authority := common.HexToAddress("0xa7702")
+ implementation := common.HexToAddress("0x1a11")
+ slot := common.BigToHash(big.NewInt(0x07))
+ implCode := []byte{0x60, 0x07, 0x54, 0x50, 0x00} // SLOAD(7), POP, STOP
+ cases := []struct {
+ name string
+ op byte
+ readOwner common.Address
+ }{
+ {"call", 0xf1, authority},
+ {"callcode", 0xf2, caller},
+ {"delegatecall", 0xf4, caller},
+ {"staticcall", 0xfa, authority},
+ }
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ env := newBALTestEnv(types.GenesisAlloc{
+ caller: {Code: makeStubCaller(tc.op, authority), Balance: common.Big0},
+ authority: {Code: types.AddressToDelegation(implementation), Balance: common.Big0},
+ implementation: {Code: implCode, Balance: common.Big0},
+ })
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &caller, big.NewInt(0), 1_000_000, 0, nil))
+ })
+
+ owner := assertPresent(t, b, tc.readOwner)
+ if !hasSlotIn(owner.StorageReads, slot) {
+ t.Fatalf("%s read must belong to %x\n%s", tc.name, tc.readOwner, b.PrettyPrint())
+ }
+ for _, other := range []common.Address{caller, authority} {
+ if other != tc.readOwner {
+ assertEmpty(t, assertPresent(t, b, other))
+ }
+ }
+ assertEmpty(t, assertPresent(t, b, implementation))
+ })
+ }
+}
+
+// TestBALDelegationOneHop verifies that resolving A -> B does not
+// recursively resolve B -> C. A and B are state accesses; C is not.
+func TestBALDelegationOneHop(t *testing.T) {
+ caller := common.HexToAddress("0xca11")
+ authority := common.HexToAddress("0xa7702")
+ delegated := common.HexToAddress("0xb7702")
+ secondHop := common.HexToAddress("0xc7702")
+ env := newBALTestEnv(types.GenesisAlloc{
+ caller: {Code: makeStubCaller(0xf1 /* CALL */, authority), Balance: common.Big0},
+ authority: {Code: types.AddressToDelegation(delegated), Balance: common.Big0},
+ delegated: {Code: types.AddressToDelegation(secondHop), Balance: common.Big0},
+ secondHop: {Code: []byte{0x00}, Balance: common.Big0},
+ })
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &caller, big.NewInt(0), 1_000_000, 0, nil))
+ })
+
+ assertPresent(t, b, authority)
+ assertPresent(t, b, delegated)
+ assertAbsent(t, b, secondHop)
+}
+
+// TestBALDelegatedSender distinguishes transaction origination from execution.
+// A sender may carry a valid delegation indicator, but its implementation is
+// not accessed unless a call actually targets sender.
+func TestBALDelegatedSender(t *testing.T) {
+ to := common.HexToAddress("0xb0b")
+ implementation := common.HexToAddress("0x1a11")
+ env := newBALTestEnv(types.GenesisAlloc{
+ implementation: {Code: []byte{0x00}, Balance: common.Big0},
+ })
+ sender := env.gspec.Alloc[env.from]
+ sender.Code = types.AddressToDelegation(implementation)
+ env.gspec.Alloc[env.from] = sender
+
+ b, _ := env.run(t, func(g *BlockGen) {
+ g.AddTx(env.tx(0, &to, big.NewInt(0), params.TxGas, 0, nil))
+ })
+
+ assertPresent(t, b, env.from)
+ assertAbsent(t, b, implementation)
+}
diff --git a/core/eip8037_test.go b/core/eip8037_test.go
index 2b56749f0b..80e7e6f1cf 100644
--- a/core/eip8037_test.go
+++ b/core/eip8037_test.go
@@ -192,6 +192,9 @@ func assertPoolSane(t *testing.T, res *ExecutionResult, gp *GasPool, floor uint6
if res.UsedGas > res.MaxUsedGas {
t.Fatalf("post-refund gas %d exceeds peak %d", res.UsedGas, res.MaxUsedGas)
}
+ if gp.cumulativeRegular > res.MaxUsedGas {
+ t.Fatalf("regular %d exceeds peak %d", gp.cumulativeRegular, res.MaxUsedGas)
+ }
if gp.cumulativeState > res.MaxUsedGas {
t.Fatalf("state %d exceeds peak %d", gp.cumulativeState, res.MaxUsedGas)
}
diff --git a/core/eip8246_test.go b/core/eip8246_test.go
index 18049d2279..b37e96d923 100644
--- a/core/eip8246_test.go
+++ b/core/eip8246_test.go
@@ -31,7 +31,8 @@ import (
// TestEIP8246SelfdestructNoBurn verifies that, once EIP-8246 is active
// (Amsterdam), a contract that is created and self-destructs to itself within
// the same transaction keeps its balance instead of burning it: the account
-// survives as a balance-only account (no code, zero nonce, balance preserved).
+// survives as a balance-only account (no code, zero nonce, balance preserved)
+// whose storage is cleared at transaction finalization.
//
// https://eips.ethereum.org/EIPS/eip-8246
func TestEIP8246SelfdestructNoBurn(t *testing.T) {
@@ -42,9 +43,11 @@ func TestEIP8246SelfdestructNoBurn(t *testing.T) {
signer = types.LatestSigner(&config)
engine = beacon.New(ethash.NewFaker())
value = big.NewInt(1_000_000)
- // Init code: ADDRESS (0x30) ; SELFDESTRUCT (0xff). The created contract
- // self-destructs to itself during its own creation transaction.
- initcode = common.FromHex("30ff")
+ slot = common.BigToHash(big.NewInt(0x05))
+ // Init code: SSTORE(5, 0x2a); ADDRESS (0x30); SELFDESTRUCT (0xff). The
+ // created contract stores a value and self-destructs to itself during
+ // its own creation transaction.
+ initcode = []byte{0x60, 0x2a, 0x60, 0x05, 0x55, 0x30, 0xff}
)
// TODO: drop this hacky Amsterdam config initialization once the final
// Amsterdam config is available (mirrors TestEthTransferLogs).
@@ -99,4 +102,147 @@ func TestEIP8246SelfdestructNoBurn(t *testing.T) {
if got := state.GetCodeSize(created); got != 0 {
t.Errorf("created account code size = %d, want 0 (code must be cleared)", got)
}
+ if got := state.GetState(created, slot); got != (common.Hash{}) {
+ t.Errorf("created storage slot %x = %x, want 0 (storage must be cleared)", slot, got)
+ }
+}
+
+// TestEIP8246SelfdestructRefunded verifies that ETH sent back to a
+// same-transaction selfdestructed account is retained at finalization instead
+// of being burned. The factory funds the account twice after SELFDESTRUCT.
+func TestEIP8246SelfdestructRefunded(t *testing.T) {
+ var (
+ key, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
+ sender = crypto.PubkeyToAddress(key.PublicKey)
+ factory = common.HexToAddress("0xfac8246")
+ beneficiary = common.HexToAddress("0xbeef")
+ config = *params.MergedTestChainConfig
+ signer = types.LatestSigner(&config)
+ engine = beacon.New(ethash.NewFaker())
+ )
+ config.AmsterdamTime = new(uint64)
+ // The child initcode selfdestructs to another account. The factory then
+ // sends it 7 and 8 wei after it is marked for selfdestruction.
+ childInit := append([]byte{0x73}, beneficiary.Bytes()...)
+ childInit = append(childInit, 0xff)
+ var word [32]byte
+ copy(word[32-len(childInit):], childInit)
+ factoryCode := append([]byte{0x7f}, word[:]...)
+ factoryCode = append(factoryCode, 0x5f, 0x52) // PUSH0; MSTORE
+ // CREATE(value=0, offset=10, length=22), then store the returned address.
+ factoryCode = append(factoryCode, 0x60, byte(len(childInit)), 0x60, byte(32-len(childInit)), 0x5f, 0xf0, 0x5f, 0x52)
+ for _, amount := range []byte{7, 8} {
+ // CALL(child, value=amount) with empty input/output.
+ factoryCode = append(factoryCode, 0x5f, 0x5f, 0x5f, 0x5f, 0x60, amount, 0x5f, 0x51, 0x5a, 0xf1, 0x50)
+ }
+ factoryCode = append(factoryCode, 0x00)
+ gspec := &Genesis{
+ Config: &config,
+ Alloc: types.GenesisAlloc{
+ sender: {Balance: newGwei(1_000_000_000)},
+ factory: {Nonce: 1, Code: factoryCode, Balance: big.NewInt(15)},
+ },
+ }
+ child := crypto.CreateAddress(factory, 1)
+ db, blocks, _ := GenerateChainWithGenesis(gspec, engine, 1, func(_ int, b *BlockGen) {
+ b.AddTx(types.MustSignNewTx(key, signer, &types.DynamicFeeTx{
+ ChainID: gspec.Config.ChainID,
+ Nonce: 0,
+ To: &factory,
+ Gas: 1_000_000,
+ GasFeeCap: newGwei(5),
+ GasTipCap: newGwei(5),
+ }))
+ })
+ chain, err := NewBlockChain(db, gspec, engine, nil)
+ if err != nil {
+ t.Fatalf("failed to create chain: %v", err)
+ }
+ defer chain.Stop()
+ state, err := chain.StateAt(blocks[0].Header())
+ if err != nil {
+ t.Fatalf("failed to obtain block state: %v", err)
+ }
+ if got := state.GetBalance(child).Uint64(); got != 15 {
+ t.Errorf("refunded child balance = %d, want 15", got)
+ }
+ if got := state.GetNonce(child); got != 0 {
+ t.Errorf("refunded child nonce = %d, want 0", got)
+ }
+ if got := state.GetCodeSize(child); got != 0 {
+ t.Errorf("refunded child code size = %d, want 0", got)
+ }
+}
+
+// TestEIP8246Create2RecreatesBalanceOnly verifies that an EIP-8246
+// balance-only account does not block recreating the same CREATE2 address in a
+// later transaction. The second creation contributes another wei to the
+// preserved balance, proving that it executed rather than collided.
+func TestEIP8246Create2RecreatesBalanceOnly(t *testing.T) {
+ var (
+ key, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
+ sender = crypto.PubkeyToAddress(key.PublicKey)
+ factory = common.HexToAddress("0xfac8246")
+ config = *params.MergedTestChainConfig
+ signer = types.LatestSigner(&config)
+ engine = beacon.New(ethash.NewFaker())
+ init = []byte{0x30, 0xff} // ADDRESS; SELFDESTRUCT
+ )
+ config.AmsterdamTime = new(uint64)
+ var word [32]byte
+ copy(word[32-len(init):], init)
+ factoryCode := append([]byte{0x7f}, word[:]...)
+ factoryCode = append(factoryCode,
+ 0x5f, 0x52, // PUSH0; MSTORE
+ 0x60, 0x01, // salt
+ 0x60, byte(len(init)),
+ 0x60, byte(32-len(init)),
+ 0x34, // CALLVALUE
+ 0xf5, // CREATE2
+ 0x50, // POP
+ 0x00,
+ )
+ gspec := &Genesis{
+ Config: &config,
+ Alloc: types.GenesisAlloc{
+ sender: {Balance: newGwei(1_000_000_000)},
+ factory: {Nonce: 1, Code: factoryCode, Balance: common.Big0},
+ },
+ }
+ var salt [32]byte
+ salt[31] = 1
+ child := crypto.CreateAddress2(factory, salt, crypto.Keccak256(init))
+ db, blocks, _ := GenerateChainWithGenesis(gspec, engine, 2, func(i int, b *BlockGen) {
+ value := big.NewInt(5)
+ if i == 1 {
+ value.SetInt64(1)
+ }
+ b.AddTx(types.MustSignNewTx(key, signer, &types.DynamicFeeTx{
+ ChainID: gspec.Config.ChainID,
+ Nonce: uint64(i),
+ To: &factory,
+ Gas: 1_000_000,
+ GasFeeCap: newGwei(5),
+ GasTipCap: newGwei(5),
+ Value: value,
+ }))
+ })
+ chain, err := NewBlockChain(db, gspec, engine, nil)
+ if err != nil {
+ t.Fatalf("failed to create chain: %v", err)
+ }
+ defer chain.Stop()
+ state, err := chain.StateAt(blocks[1].Header())
+ if err != nil {
+ t.Fatalf("failed to obtain block state: %v", err)
+ }
+ if got := state.GetBalance(child).Uint64(); got != 6 {
+ t.Errorf("CREATE2 child balance = %d, want 6", got)
+ }
+ if got := state.GetNonce(child); got != 0 {
+ t.Errorf("CREATE2 child nonce = %d, want 0", got)
+ }
+ if got := state.GetCodeSize(child); got != 0 {
+ t.Errorf("CREATE2 child code size = %d, want 0", got)
+ }
}
diff --git a/core/vm/eip8038_test.go b/core/vm/eip8038_test.go
index f20a9d8a0e..2d9f50ca8b 100644
--- a/core/vm/eip8038_test.go
+++ b/core/vm/eip8038_test.go
@@ -210,6 +210,145 @@ func TestEIP8038AccountAccess(t *testing.T) {
})
}
+// callFamily8038 builds a zero-input/output call-family operation that forwards
+// all remaining regular gas and discards its success flag. CALL and CALLCODE
+// take a value argument; DELEGATECALL and STATICCALL do not.
+func callFamily8038(to common.Address, op OpCode, value byte) []byte {
+ code := []byte{0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x60, 0x00}
+ if op == CALL || op == CALLCODE {
+ code = append(code, 0x60, value)
+ }
+ code = append(code, 0x73)
+ code = append(code, to.Bytes()...)
+ return append(code, 0x5a, byte(op), 0x50, 0x00) // GAS; ; POP; STOP
+}
+
+// TestEIP8038Calls pins the re-priced account access and value-transfer costs
+// for every member of the CALL family. The opcode's constant cost is the warm
+// access component, so a cold target adds only COLD_ACCOUNT_ACCESS-WARM.
+func TestEIP8038Calls(t *testing.T) {
+ const (
+ push1 = uint64(3)
+ push20 = uint64(3)
+ gasOp = uint64(2)
+ pop = uint64(2)
+ )
+ cold := params.ColdAccountAccessAmsterdam - params.WarmAccountAccessAmsterdam
+ callBase := 5*push1 + push20 + gasOp + pop + params.WarmAccountAccessAmsterdam
+ plainBase := 4*push1 + push20 + gasOp + pop + params.WarmAccountAccessAmsterdam
+ target := common.BytesToAddress([]byte("call-target"))
+
+ cases := []struct {
+ name string
+ op OpCode
+ value byte
+ fundSelf bool
+ wantReg uint64
+ wantState int64
+ }{
+ {"call/cold", CALL, 0, false, callBase + cold, 0},
+ // A callee that immediately returns gives the 2,300 stipend back, so
+ // the net regular cost is ACCOUNT_WRITE.
+ {"call/value", CALL, 1, true, callBase + cold + params.AccountWriteAmsterdam, stateGasNewAccount},
+ {"callcode/value", CALLCODE, 1, true, callBase + cold + params.AccountWriteAmsterdam, 0},
+ {"delegatecall/cold", DELEGATECALL, 0, false, plainBase + cold, 0},
+ {"staticcall/cold", STATICCALL, 0, false, plainBase + cold, 0},
+ }
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ var setup func(*state.StateDB, common.Address)
+ if tc.fundSelf {
+ setup = fund(common.BytesToAddress([]byte("self")), 1)
+ }
+ res, _, err := run8038(t, callFamily8038(target, tc.op, tc.value), hugeBudget(), new(uint256.Int), setup)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if res.UsedRegularGas != tc.wantReg {
+ t.Fatalf("regular gas = %d, want %d", res.UsedRegularGas, tc.wantReg)
+ }
+ if res.UsedStateGas != tc.wantState {
+ t.Fatalf("state gas = %d, want %d", res.UsedStateGas, tc.wantState)
+ }
+ })
+ }
+
+ // The first CALL makes its target warm. A second CALL pays no dynamic
+ // access surcharge, leaving only the CALL base cost (which includes warm).
+ first := callFamily8038(target, CALL, 0)
+ code := append(first[:len(first)-1], callFamily8038(target, CALL, 0)...)
+ res, _, err := run8038(t, code, hugeBudget(), new(uint256.Int), nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if want := 2*callBase + cold; res.UsedRegularGas != want {
+ t.Fatalf("cold+warm CALL = %d, want %d", res.UsedRegularGas, want)
+ }
+
+ // Calling an EIP-7702 authority accesses both the authority and its
+ // delegation target. The authority uses CALL's warm-included pricing; the
+ // separately resolved cold target pays a full COLD_ACCOUNT_ACCESS charge.
+ authority := common.BytesToAddress([]byte("delegated-authority"))
+ implementation := common.BytesToAddress([]byte("delegated-target"))
+ setup := func(db *state.StateDB, _ common.Address) {
+ db.CreateAccount(authority)
+ db.SetCode(authority, types.AddressToDelegation(implementation), tracing.CodeChangeUnspecified)
+ db.CreateAccount(implementation)
+ db.SetCode(implementation, []byte{0x00}, tracing.CodeChangeUnspecified)
+ }
+ res, _, err = run8038(t, callFamily8038(authority, CALL, 0), hugeBudget(), new(uint256.Int), setup)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if want := callBase + cold + params.ColdAccountAccessAmsterdam; res.UsedRegularGas != want {
+ t.Fatalf("delegated CALL = %d, want %d (authority + target)", res.UsedRegularGas, want)
+ }
+
+ // A value CALL receives the 2,300 stipend even when it asks to forward no
+ // regular gas. If the child burns that stipend, the full CALL_VALUE
+ // (ACCOUNT_WRITE + stipend) remains charged to the caller.
+ stipendTarget := common.BytesToAddress([]byte("stipend-target"))
+ base := callFamily8038(stipendTarget, CALL, 1)
+ code = append(base[:len(base)-4], 0x60, 0x00, byte(CALL), 0x50, 0x00) // PUSH1 0; CALL; POP; STOP
+ setup = func(db *state.StateDB, self common.Address) {
+ db.AddBalance(self, uint256.NewInt(1), tracing.BalanceChangeUnspecified)
+ db.CreateAccount(stipendTarget)
+ db.SetCode(stipendTarget, []byte{0xfe}, tracing.CodeChangeUnspecified)
+ }
+ res, _, err = run8038(t, code, hugeBudget(), new(uint256.Int), setup)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if want := 5*push1 + push20 + push1 + pop + params.WarmAccountAccessAmsterdam + cold + params.CallValueTransferAmsterdam; res.UsedRegularGas != want {
+ t.Fatalf("value CALL with burnt stipend = %d, want %d", res.UsedRegularGas, want)
+ }
+}
+
+// TestEIP8038Create checks that CREATE and CREATE2 always pay CREATE_ACCESS
+// in regular gas. With otherwise identical initcode, CREATE2 additionally has
+// one salt push and the address-hash word charge.
+func TestEIP8038Create(t *testing.T) {
+ create, _, err := run8038(t, deployCode(deploy0Init, false, 0), hugeBudget(), new(uint256.Int), nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ create2, _, err := run8038(t, deployCode(deploy0Init, true, 0), hugeBudget(), new(uint256.Int), nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ // Outer setup is PUSH32 + PUSH1 + MSTORE (including one-word expansion),
+ // then three CREATE operands. The child initcode is two PUSH1s and RETURN.
+ const outer = uint64(3 + 3 + 3 + 3 + 3*3)
+ const init = uint64(2 * 3)
+ want := outer + params.CreateAccessAmsterdam + params.InitCodeWordGas + init
+ if create.UsedRegularGas != want {
+ t.Fatalf("CREATE regular gas = %d, want %d", create.UsedRegularGas, want)
+ }
+ if want := create.UsedRegularGas + 3 + params.Keccak256WordGas; create2.UsedRegularGas != want {
+ t.Fatalf("CREATE2 regular gas = %d, want %d", create2.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).