From ea242145cff75f9beee6f16701dbac494fbbff94 Mon Sep 17 00:00:00 2001 From: rjl493456442 Date: Wed, 1 Jul 2026 16:48:13 +0800 Subject: [PATCH] core: implement EIP-7928 spec change (#35260) https://github.com/ethereum/EIPs/pull/11838/changes --- core/bal_test.go | 67 +++++++++++++++++++++++++++++++++++++++ core/vm/operations_acl.go | 16 ++++++++++ 2 files changed, 83 insertions(+) diff --git a/core/bal_test.go b/core/bal_test.go index 84579ca564..9409ff32d1 100644 --- a/core/bal_test.go +++ b/core/bal_test.go @@ -518,6 +518,73 @@ func TestBALStaticCallTargetIncluded(t *testing.T) { assertEmpty(t, assertPresent(t, b, target)) } +// makeValueCaller emits a single value-transferring CALL-family op (CALL 0xf1 +// or CALLCODE 0xf2) against `target` with value=1, then STOPs. Used together +// with a zero-balance caller to make the value transfer fail CanTransfer. +func makeValueCaller(op byte, target common.Address) []byte { + code := []byte{ + 0x60, 0x00, // retSize + 0x60, 0x00, // retOff + 0x60, 0x00, // argsSize + 0x60, 0x00, // argsOff + 0x60, 0x01, // value = 1 + 0x73, // PUSH20 target + } + code = append(code, target.Bytes()...) + return append(code, 0x5a, op, 0x50, 0x00) // GAS, op, POP, STOP +} + +// TestBALCallToDelegatedTargetBalanceFail asserts the EIP-7928 rule revised in +// ethereum/EIPs#11838: when a CALL targets an EIP-7702 delegated account and the +// delegated address passes its access_cost gas check, the delegated +// (implementation) address MUST appear in the BAL even when the call then fails +// its sender-balance check, because the delegation is resolved before that +// check. CALL routes through the EIP-8037 gas path. +func TestBALCallToDelegatedTargetBalanceFail(t *testing.T) { + delegated := common.HexToAddress("0xde1e9a7ed") // EOA carrying a 7702 designator + impl := common.HexToAddress("0x111111") // delegation target (implementation) + caller := common.HexToAddress("0xca11") // zero-balance contract issuing the CALL + + env := newBALTestEnv(types.GenesisAlloc{ + caller: {Code: makeValueCaller(0xf1 /* CALL */, delegated), Balance: common.Big0}, + delegated: {Code: types.AddressToDelegation(impl), Balance: common.Big0}, + impl: {Code: []byte{0x00}, Balance: common.Big0}, // STOP + }) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &caller, big.NewInt(0), 1_000_000, 0, nil)) + }) + + assertPresent(t, b, caller) + assertPresent(t, b, delegated) + // The call failed its sender-balance check, so the implementation never + // executed: it is recorded with an empty change set, but it MUST be present. + assertEmpty(t, assertPresent(t, b, impl)) +} + +// TestBALCallCodeToDelegatedTargetBalanceFail is the CALLCODE analogue of +// TestBALCallToDelegatedTargetBalanceFail, exercising the EIP-7702 gas path +// (CALLCODE/STATICCALL/DELEGATECALL) rather than the EIP-8037 one. +func TestBALCallCodeToDelegatedTargetBalanceFail(t *testing.T) { + delegated := common.HexToAddress("0xde1e9a7ed") + impl := common.HexToAddress("0x111111") + caller := common.HexToAddress("0xca11") + + env := newBALTestEnv(types.GenesisAlloc{ + caller: {Code: makeValueCaller(0xf2 /* CALLCODE */, delegated), Balance: common.Big0}, + delegated: {Code: types.AddressToDelegation(impl), Balance: common.Big0}, + impl: {Code: []byte{0x00}, Balance: common.Big0}, // STOP + }) + + b, _ := env.run(t, func(g *BlockGen) { + g.AddTx(env.tx(0, &caller, big.NewInt(0), 1_000_000, 0, nil)) + }) + + assertPresent(t, b, caller) + assertPresent(t, b, delegated) + assertEmpty(t, assertPresent(t, b, impl)) +} + // ============================== Revert behaviour ============================== // TestBALRevertedTxStillIncluded: a tx whose top-level call REVERTs still diff --git a/core/vm/operations_acl.go b/core/vm/operations_acl.go index 8b2a627fef..e07eba451f 100644 --- a/core/vm/operations_acl.go +++ b/core/vm/operations_acl.go @@ -257,6 +257,14 @@ func makeSelfdestructGasFn(refundsEnabled bool) gasFunc { return gasFunc } +// recordDelegationAccess records the EIP-7702 delegated target in the block +// access list (EIP-7928). +func recordDelegationAccess(evm *EVM, target common.Address) { + if evm.chainRules.IsAmsterdam { + evm.StateDB.GetCode(target) + } +} + var ( innerGasCallEIP7702 = makeCallVariantGasCallEIP7702(gasCallIntrinsic) gasDelegateCallEIP7702 = makeCallVariantGasCallEIP7702(gasDelegateCallIntrinsic) @@ -336,6 +344,10 @@ func makeCallVariantGasCallEIP7702(intrinsicFunc intrinsicGasFunc) gasFunc { if !contract.chargeRegular(eip7702Cost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) { return GasCosts{}, ErrOutOfGas } + // The delegated address has passed its gas check; record it in the + // block access list now, before the call's sender-balance and + // call-stack-depth checks. + recordDelegationAccess(evm, target) } // Calculate the gas budget for the nested call. The costs defined by // EIP-2929 and EIP-7702 have already been applied. @@ -416,6 +428,10 @@ func makeCallVariantGasCallEIP8037(regularFunc regularGasFunc, stateGasFunc stat if !contract.chargeRegular(eip7702Cost, evm.Config.Tracer, tracing.GasChangeCallStorageColdAccess) { return GasCosts{}, ErrOutOfGas } + // The delegated address has passed its gas check; record it in the + // block access list now, before the call's sender-balance and + // call-stack-depth checks. + recordDelegationAccess(evm, target) } // Compute and charge state gas (new account creation) AFTER regular gas.