refactor: set EVM.readOnly and depth before running stateful precompile (#210)

## Why this should be merged

Improved clarity.

## How this works

Semantically equivalent refactor. These values aren't accessible /
needed during execution of a stateful precompile, but are important when
making outgoing calls so were originally only set in the
`PrecompileEnvironment.Call()` implementation. This was confusing and
led to a false-positive bug report.

## How this was tested

Existing tests.
This commit is contained in:
Arran Schlosberg 2025-08-12 01:35:13 +01:00 committed by GitHub
parent e7035f19ee
commit 62dc2ea087
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 28 additions and 34 deletions

View file

@ -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() {
@ -120,15 +126,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.readOnly() && !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

View file

@ -63,19 +63,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 {
@ -116,19 +104,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
@ -141,7 +116,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) {