Skip to content

Commit ecac678

Browse files
committed
test: PrecompileEnvironment mutability limitation
1 parent 7a5c68f commit ecac678

File tree

3 files changed

+84
-13
lines changed

3 files changed

+84
-13
lines changed

core/vm/contracts.libevm.go

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -92,13 +92,13 @@ func (t CallType) OpCode() OpCode {
9292
type StateMutability uint
9393

9494
const (
95-
unknownStateMutability StateMutability = iota
96-
MutableState
97-
// ReadOnlyState is equivalent to Solidity's "view".
98-
ReadOnlyState
9995
// Pure is a Solidity concept disallowing all access, read or write, to
10096
// state.
101-
Pure
97+
Pure StateMutability = iota + 1
98+
// ReadOnlyState is equivalent to Solidity's "view".
99+
ReadOnlyState
100+
// MutableState can be both read from and written to.
101+
MutableState
102102
)
103103

104104
// String returns a human-readable representation of the StateMutability.
@@ -177,9 +177,10 @@ type PrecompileEnvironment interface {
177177
// context, but [Pure] is a Solidity concept that is enforced by user code.
178178
StateMutability() StateMutability
179179
// AsReadOnly returns a copy of the current environment for which
180-
// StateMutability() returns [ReadOnlyState]. It can be used as a guard
181-
// against accidental writes when a read-only function is invoked with EVM
182-
// call() instead of staticcall().
180+
// StateMutability() is at most [ReadOnlyState]; i.e. if mutability is
181+
// already limited to [Pure], AsReadOnly() will not expand access. It can be
182+
// used as a guard against accidental writes when a read-only function is
183+
// invoked with EVM call() instead of staticcall().
183184
AsReadOnly() PrecompileEnvironment
184185
// AsPure returns a copy of the current environment that has no access to
185186
// state; i.e. StateMutability() returns [Pure]. All calls to both StateDB()

core/vm/contracts.libevm_test.go

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -149,9 +149,6 @@ func TestNewStatefulPrecompile(t *testing.T) {
149149
gasCost := rng.Uint64n(gasLimit)
150150

151151
run := func(env vm.PrecompileEnvironment, input []byte, suppliedGas uint64) ([]byte, uint64, error) {
152-
if got, want := env.StateDB() != nil, env.StateMutability() == vm.MutableState; got != want {
153-
return nil, 0, fmt.Errorf("PrecompileEnvironment().StateDB() must be non-nil i.f.f. state is mutable; got non-nil? %t; want %t", got, want)
154-
}
155152
hdr, err := env.BlockHeader()
156153
if err != nil {
157154
return nil, 0, err
@@ -770,3 +767,75 @@ func ExamplePrecompileEnvironment() {
770767
// variable to include it in this example function.
771768
_ = actualCaller
772769
}
770+
771+
func TestStateMutability(t *testing.T) {
772+
rng := ethtest.NewPseudoRand(0)
773+
precompile := rng.Address()
774+
chainID := rng.BigUint64()
775+
776+
const precompileReturn = "precompile executed"
777+
hooks := &hookstest.Stub{
778+
PrecompileOverrides: map[common.Address]libevm.PrecompiledContract{
779+
precompile: vm.NewStatefulPrecompile(func(env vm.PrecompileEnvironment, input []byte) (ret []byte, err error) {
780+
781+
tests := []struct {
782+
name string
783+
env vm.PrecompileEnvironment
784+
want vm.StateMutability
785+
}{
786+
{
787+
name: "incoming argument",
788+
env: env,
789+
want: vm.MutableState,
790+
},
791+
{
792+
name: "AsReadOnly()",
793+
env: env.AsReadOnly(),
794+
want: vm.ReadOnlyState,
795+
},
796+
{
797+
name: "AsPure()",
798+
env: env.AsPure(),
799+
want: vm.Pure,
800+
},
801+
{
802+
name: "AsPure().AsReadOnly() is still pure",
803+
env: env.AsPure().AsReadOnly(),
804+
want: vm.Pure,
805+
},
806+
}
807+
808+
for _, tt := range tests {
809+
t.Run(tt.name, func(t *testing.T) {
810+
env := tt.env // deliberately shadow the incoming arg
811+
t.Run("mutability_and_access", func(t *testing.T) {
812+
assert.Equal(t, tt.want, env.StateMutability(), "env.StateMutability()")
813+
assert.Equal(t, env.StateDB() != nil, tt.want == vm.MutableState, "env.StateDB() != nil i.f.f. MutableState")
814+
assert.Equal(t, env.ReadOnlyState() != nil, tt.want != vm.Pure, "env.ReadOnlyState() != nil i.f.f !Pure")
815+
})
816+
817+
t.Run("environment_unmodified", func(t *testing.T) {
818+
// Each of these demonstrate that the underlying
819+
// copy of the environment propagates everything but
820+
// mutability.
821+
assert.Equal(t, chainID, env.ChainConfig().ChainID, "Chain ID preserved")
822+
assert.Equalf(t, precompile, env.Addresses().Self, "%T preserved", env.Addresses())
823+
assert.Equalf(t, vm.Call, env.IncomingCallType(), "%T preserved", env.IncomingCallType())
824+
})
825+
})
826+
}
827+
828+
return []byte(precompileReturn), nil
829+
}),
830+
},
831+
}
832+
hooks.Register(t)
833+
834+
_, evm := ethtest.NewZeroEVM(t, ethtest.WithChainConfig(&params.ChainConfig{
835+
ChainID: chainID,
836+
}))
837+
got, _, err := evm.Call(vm.AccountRef{}, precompile, nil, 0, uint256.NewInt(0))
838+
if got, want := string(got), precompileReturn; err != nil || got != want {
839+
t.Errorf("%T.Call([precompile]) got {%q, %v}; want {%q, nil}", evm, got, err, want)
840+
}
841+
}

core/vm/environment.libevm.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ func (e *environment) StateMutability() StateMutability {
8181
// A switch statement provides clearer code coverage for difficult-to-test
8282
// cases.
8383
switch {
84+
// cases MUST be ordered from most to least restrictive
85+
case e.pure:
86+
return Pure
8487
case e.callType == StaticCall:
8588
// evm.interpreter.readOnly is only set to true via a call to
8689
// EVMInterpreter.Run() so, if a precompile is called directly with
@@ -90,8 +93,6 @@ func (e *environment) StateMutability() StateMutability {
9093
return ReadOnlyState
9194
case e.view:
9295
return ReadOnlyState
93-
case e.pure:
94-
return Pure
9596
default:
9697
return MutableState
9798
}

0 commit comments

Comments
 (0)