From 7dbf994e696a1530aac0aa9a786f83921deb0ea2 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Tue, 5 Aug 2025 08:27:50 +0100 Subject: [PATCH 1/2] refactor: set `EVM.readOnly` and `depth` before running stateful precompile --- core/vm/contracts.libevm.go | 27 ++++++++++++++++++++------- core/vm/environment.libevm.go | 29 ++--------------------------- 2 files changed, 22 insertions(+), 34 deletions(-) diff --git a/core/vm/contracts.libevm.go b/core/vm/contracts.libevm.go index 7d06638ae99..f6b30738889 100644 --- a/core/vm/contracts.libevm.go +++ b/core/vm/contracts.libevm.go @@ -120,15 +120,28 @@ func (t CallType) OpCode() OpCode { // run runs the [PrecompiledContract], differentiating between stateful and // regular types, updating `args.gasRemaining` in the stateful case. func (args *evmCallArgs) run(p PrecompiledContract, input []byte) (ret []byte, err error) { - switch p := p.(type) { - default: + sp, ok := p.(statefulPrecompile) + if !ok { return p.Run(input) - case statefulPrecompile: - env := args.env() - ret, err := p(env, input) - args.gasRemaining = env.Gas() - return ret, err } + + env := args.env() + // Depth and read-only setting are handled by [EVMInterpreter.Run], + // which isn't used for precompiles, so we need to do it ourselves to + // maintain the expected invariants. + in := env.evm.interpreter + + in.evm.depth++ + defer func() { in.evm.depth-- }() + + if env.callType == StaticCall && !in.readOnly { + in.readOnly = true + defer func() { in.readOnly = false }() + } + + ret, err = sp(env, input) + args.gasRemaining = env.Gas() + return ret, err } // PrecompiledStatefulContract is the stateful equivalent of a diff --git a/core/vm/environment.libevm.go b/core/vm/environment.libevm.go index 7c3ed811f87..d5be1447a0e 100644 --- a/core/vm/environment.libevm.go +++ b/core/vm/environment.libevm.go @@ -61,19 +61,7 @@ func (e *environment) refundGas(add uint64) error { } func (e *environment) ReadOnly() bool { - // A switch statement provides clearer code coverage for difficult-to-test - // cases. - switch { - case e.callType == StaticCall: - // evm.interpreter.readOnly is only set to true via a call to - // EVMInterpreter.Run() so, if a precompile is called directly with - // StaticCall(), then readOnly might not be set yet. - return true - case e.evm.interpreter.readOnly: - return true - default: - return false - } + return e.evm.interpreter.readOnly } func (e *environment) Addresses() *libevm.AddressContext { @@ -108,19 +96,6 @@ func (e *environment) Call(addr common.Address, input []byte, gas uint64, value } func (e *environment) callContract(typ CallType, addr common.Address, input []byte, gas uint64, value *uint256.Int, opts ...CallOption) (retData []byte, retErr error) { - // Depth and read-only setting are handled by [EVMInterpreter.Run], which - // isn't used for precompiles, so we need to do it ourselves to maintain the - // expected invariants. - in := e.evm.interpreter - - in.evm.depth++ - defer func() { in.evm.depth-- }() - - if e.ReadOnly() && !in.readOnly { // i.e. the precompile was StaticCall()ed - in.readOnly = true - defer func() { in.readOnly = false }() - } - var caller ContractRef = e.self if options.As[callConfig](opts...).unsafeCallerAddressProxying { // Note that, in addition to being unsafe, this breaks an EVM @@ -133,7 +108,7 @@ func (e *environment) callContract(typ CallType, addr common.Address, input []by } } - if in.readOnly && value != nil && !value.IsZero() { + if e.ReadOnly() && value != nil && !value.IsZero() { return nil, ErrWriteProtection } if !e.UseGas(gas) { From b0f91b486dc92e6c7c20aaeac252525a2f94d286 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Fri, 8 Aug 2025 00:07:11 +0100 Subject: [PATCH 2/2] refactor: abstract `CallType.readOnly` --- core/vm/contracts.libevm.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/core/vm/contracts.libevm.go b/core/vm/contracts.libevm.go index 2059b12c9f8..19632eaf078 100644 --- a/core/vm/contracts.libevm.go +++ b/core/vm/contracts.libevm.go @@ -101,6 +101,12 @@ func (t CallType) isValid() bool { } } +// readOnly returns whether the CallType induces a read-only state if not +// already in one. +func (t CallType) readOnly() bool { + return t == StaticCall +} + // String returns a human-readable representation of the CallType. func (t CallType) String() string { if t.isValid() { @@ -134,7 +140,7 @@ func (args *evmCallArgs) run(p PrecompiledContract, input []byte) (ret []byte, e in.evm.depth++ defer func() { in.evm.depth-- }() - if env.callType == StaticCall && !in.readOnly { + if env.callType.readOnly() && !in.readOnly { in.readOnly = true defer func() { in.readOnly = false }() }