Skip to content

Commit e494e60

Browse files
committed
refactor!: fine-grained mutability instead of boolean ReadOnly()
1 parent 495011a commit e494e60

File tree

5 files changed

+109
-50
lines changed

5 files changed

+109
-50
lines changed

core/vm/contracts.libevm.go

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,32 @@ func (t CallType) OpCode() OpCode {
8888
return INVALID
8989
}
9090

91+
// StateMutability describes the available state access.
92+
type StateMutability uint
93+
94+
const (
95+
unknownStateMutability StateMutability = iota
96+
MutableState
97+
// ReadOnlyState is equivalent to Solidity's "view".
98+
ReadOnlyState
99+
// Pure is a Solidity concept disallowing all access, read or write, to
100+
// state.
101+
Pure
102+
)
103+
104+
// String returns a human-readable representation of the StateMutability.
105+
func (m StateMutability) String() string {
106+
switch m {
107+
case MutableState:
108+
return "mutable"
109+
case ReadOnlyState:
110+
return "read-only"
111+
case Pure:
112+
return "no state access"
113+
}
114+
return fmt.Sprintf("unknown %T(%[1]d)", m)
115+
}
116+
91117
// run runs the [PrecompiledContract], differentiating between stateful and
92118
// regular types, updating `args.gasRemaining` in the stateful case.
93119
func (args *evmCallArgs) run(p PrecompiledContract, input []byte) (ret []byte, err error) {
@@ -141,21 +167,24 @@ func (p statefulPrecompile) Run([]byte) ([]byte, error) {
141167
type PrecompileEnvironment interface {
142168
ChainConfig() *params.ChainConfig
143169
Rules() params.Rules
144-
// StateDB will be non-nil i.f.f !ReadOnly().
170+
// StateDB will be non-nil i.f.f StateMutability() returns [MutableState].
145171
StateDB() StateDB
146-
// ReadOnlyState will be non-nil i.f.f. SetPure() has not been called.
172+
// ReadOnlyState will be non-nil i.f.f. StateMutability() does not return
173+
// [Pure].
147174
ReadOnlyState() libevm.StateReader
148175

149-
// SetReadOnly ensures that all future calls to ReadOnly() will return true.
150-
// Is can be used as a guard against accidental writes when a read-only
151-
// function is invoked with EVM call() instead of staticcall().
152-
SetReadOnly()
153-
// SetPure ensures that all future calls to ReadOnly() will return true and
154-
// that calls to ReadOnlyState() will return nil.
155-
// TODO(arr4n) DO NOT MERGE: change this and SetReadOnly to return new
156-
// environments.
157-
SetPure()
158-
ReadOnly() bool
176+
// StateMutability can infer [MutableState] vs [ReadOnlyState] based on EVM
177+
// context, but [Pure] is a Solidity concept that is enforced by user code.
178+
StateMutability() StateMutability
179+
// 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().
183+
AsReadOnly() PrecompileEnvironment
184+
// AsPure returns a copy of the current environment that has no access to
185+
// state; i.e. StateMutability() returns [Pure]. All calls to both StateDB()
186+
// and ReadOnlyState() will return nil.
187+
AsPure() PrecompileEnvironment
159188

160189
IncomingCallType() CallType
161190
Addresses() *libevm.AddressContext

core/vm/contracts.libevm_test.go

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ type statefulPrecompileOutput struct {
108108
Addresses *libevm.AddressContext
109109
StateValue common.Hash
110110
ValueReceived *uint256.Int
111-
ReadOnly bool
111+
Mutability vm.StateMutability
112112
BlockNumber, Difficulty *big.Int
113113
BlockTime uint64
114114
Input []byte
@@ -149,8 +149,8 @@ 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.ReadOnly(); got != want {
153-
return nil, 0, fmt.Errorf("PrecompileEnvironment().StateDB() must be non-nil i.f.f. not read-only; got non-nil? %t; want %t", got, want)
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)
154154
}
155155
hdr, err := env.BlockHeader()
156156
if err != nil {
@@ -162,7 +162,7 @@ func TestNewStatefulPrecompile(t *testing.T) {
162162
Addresses: env.Addresses(),
163163
StateValue: env.ReadOnlyState().GetState(precompile, slot),
164164
ValueReceived: env.Value(),
165-
ReadOnly: env.ReadOnly(),
165+
Mutability: env.StateMutability(),
166166
BlockNumber: env.BlockNumber(),
167167
BlockTime: env.BlockTime(),
168168
Difficulty: hdr.Difficulty,
@@ -216,8 +216,8 @@ func TestNewStatefulPrecompile(t *testing.T) {
216216
wantTransferValue *uint256.Int
217217
// Note that this only covers evm.readOnly being true because of the
218218
// precompile's call. See TestInheritReadOnly for alternate case.
219-
wantReadOnly bool
220-
wantCallType vm.CallType
219+
wantMutability vm.StateMutability
220+
wantCallType vm.CallType
221221
}{
222222
{
223223
name: "EVM.Call()",
@@ -229,7 +229,7 @@ func TestNewStatefulPrecompile(t *testing.T) {
229229
Caller: caller,
230230
Self: precompile,
231231
},
232-
wantReadOnly: false,
232+
wantMutability: vm.MutableState,
233233
wantTransferValue: transferValue,
234234
wantCallType: vm.Call,
235235
},
@@ -243,7 +243,7 @@ func TestNewStatefulPrecompile(t *testing.T) {
243243
Caller: caller,
244244
Self: caller,
245245
},
246-
wantReadOnly: false,
246+
wantMutability: vm.MutableState,
247247
wantTransferValue: transferValue,
248248
wantCallType: vm.CallCode,
249249
},
@@ -257,7 +257,7 @@ func TestNewStatefulPrecompile(t *testing.T) {
257257
Caller: eoa, // inherited from caller
258258
Self: caller,
259259
},
260-
wantReadOnly: false,
260+
wantMutability: vm.MutableState,
261261
wantTransferValue: uint256.NewInt(0),
262262
wantCallType: vm.DelegateCall,
263263
},
@@ -271,7 +271,7 @@ func TestNewStatefulPrecompile(t *testing.T) {
271271
Caller: caller,
272272
Self: precompile,
273273
},
274-
wantReadOnly: true,
274+
wantMutability: vm.ReadOnlyState,
275275
wantTransferValue: uint256.NewInt(0),
276276
wantCallType: vm.StaticCall,
277277
},
@@ -284,7 +284,7 @@ func TestNewStatefulPrecompile(t *testing.T) {
284284
Addresses: tt.wantAddresses,
285285
StateValue: stateValue,
286286
ValueReceived: tt.wantTransferValue,
287-
ReadOnly: tt.wantReadOnly,
287+
Mutability: tt.wantMutability,
288288
BlockNumber: header.Number,
289289
BlockTime: header.Time,
290290
Difficulty: header.Difficulty,
@@ -334,7 +334,7 @@ func TestInheritReadOnly(t *testing.T) {
334334
PrecompileOverrides: map[common.Address]libevm.PrecompiledContract{
335335
precompile: vm.NewStatefulPrecompile(
336336
func(env vm.PrecompileEnvironment, input []byte) ([]byte, error) {
337-
if env.ReadOnly() {
337+
if env.StateMutability() != vm.MutableState {
338338
return []byte{ifReadOnly}, nil
339339
}
340340
return []byte{ifNotReadOnly}, nil
@@ -560,9 +560,9 @@ func TestPrecompileMakeCall(t *testing.T) {
560560
}),
561561
dest: vm.NewStatefulPrecompile(func(env vm.PrecompileEnvironment, input []byte) (ret []byte, err error) {
562562
out := &statefulPrecompileOutput{
563-
Addresses: env.Addresses(),
564-
ReadOnly: env.ReadOnly(),
565-
Input: input, // expected to be callData
563+
Addresses: env.Addresses(),
564+
Mutability: env.StateMutability(),
565+
Input: input, // expected to be callData
566566
}
567567
return out.Bytes(), nil
568568
}),
@@ -591,7 +591,8 @@ func TestPrecompileMakeCall(t *testing.T) {
591591
Caller: sut,
592592
Self: dest,
593593
},
594-
Input: precompileCallData,
594+
Input: precompileCallData,
595+
Mutability: vm.MutableState,
595596
},
596597
},
597598
{
@@ -603,7 +604,8 @@ func TestPrecompileMakeCall(t *testing.T) {
603604
Caller: caller, // overridden by CallOption
604605
Self: dest,
605606
},
606-
Input: precompileCallData,
607+
Input: precompileCallData,
608+
Mutability: vm.MutableState,
607609
},
608610
},
609611
{
@@ -614,7 +616,8 @@ func TestPrecompileMakeCall(t *testing.T) {
614616
Caller: caller, // SUT runs as its own caller because of CALLCODE
615617
Self: dest,
616618
},
617-
Input: precompileCallData,
619+
Input: precompileCallData,
620+
Mutability: vm.MutableState,
618621
},
619622
},
620623
{
@@ -626,7 +629,8 @@ func TestPrecompileMakeCall(t *testing.T) {
626629
Caller: caller, // CallOption is a NOOP
627630
Self: dest,
628631
},
629-
Input: precompileCallData,
632+
Input: precompileCallData,
633+
Mutability: vm.MutableState,
630634
},
631635
},
632636
{
@@ -637,7 +641,8 @@ func TestPrecompileMakeCall(t *testing.T) {
637641
Caller: caller, // as with CALLCODE
638642
Self: dest,
639643
},
640-
Input: precompileCallData,
644+
Input: precompileCallData,
645+
Mutability: vm.MutableState,
641646
},
642647
},
643648
{
@@ -649,7 +654,8 @@ func TestPrecompileMakeCall(t *testing.T) {
649654
Caller: caller, // CallOption is a NOOP
650655
Self: dest,
651656
},
652-
Input: precompileCallData,
657+
Input: precompileCallData,
658+
Mutability: vm.MutableState,
653659
},
654660
},
655661
{
@@ -665,7 +671,7 @@ func TestPrecompileMakeCall(t *testing.T) {
665671
// (non-static) CALL, the read-only state is inherited. Yes,
666672
// this is _another_ way to get a read-only state, different to
667673
// the other tests.
668-
ReadOnly: true,
674+
Mutability: vm.ReadOnlyState,
669675
},
670676
},
671677
{
@@ -677,8 +683,8 @@ func TestPrecompileMakeCall(t *testing.T) {
677683
Caller: caller, // overridden by CallOption
678684
Self: dest,
679685
},
680-
Input: precompileCallData,
681-
ReadOnly: true,
686+
Input: precompileCallData,
687+
Mutability: vm.ReadOnlyState,
682688
},
683689
},
684690
}

core/vm/environment.libevm.go

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package vm
1818

1919
import (
20+
"errors"
2021
"fmt"
2122
"math/big"
2223

@@ -39,6 +40,12 @@ type environment struct {
3940
view, pure bool
4041
}
4142

43+
func (e *environment) copy() *environment {
44+
// This deliberately does not use field names so that a change in the fields
45+
// will break this code and force it to be reviewed.
46+
return &environment{e.evm, e.self, e.callType, e.view, e.pure}
47+
}
48+
4249
func (e *environment) Gas() uint64 { return e.self.Gas }
4350
func (e *environment) UseGas(gas uint64) bool { return e.self.UseGas(gas) }
4451
func (e *environment) Value() *uint256.Int { return new(uint256.Int).Set(e.self.Value()) }
@@ -58,24 +65,35 @@ func (e *environment) refundGas(add uint64) error {
5865
return nil
5966
}
6067

61-
func (e *environment) SetReadOnly() { e.view = true }
62-
func (e *environment) SetPure() { e.pure = true }
68+
func (e *environment) AsReadOnly() PrecompileEnvironment {
69+
cp := e.copy()
70+
cp.view = true
71+
return cp
72+
}
73+
74+
func (e *environment) AsPure() PrecompileEnvironment {
75+
cp := e.copy()
76+
cp.pure = true
77+
return cp
78+
}
6379

64-
func (e *environment) ReadOnly() bool {
80+
func (e *environment) StateMutability() StateMutability {
6581
// A switch statement provides clearer code coverage for difficult-to-test
6682
// cases.
6783
switch {
6884
case e.callType == StaticCall:
6985
// evm.interpreter.readOnly is only set to true via a call to
7086
// EVMInterpreter.Run() so, if a precompile is called directly with
7187
// StaticCall(), then readOnly might not be set yet.
72-
return true
88+
return ReadOnlyState
7389
case e.evm.interpreter.readOnly:
74-
return true
75-
case e.view || e.pure:
76-
return true
90+
return ReadOnlyState
91+
case e.view:
92+
return ReadOnlyState
93+
case e.pure:
94+
return Pure
7795
default:
78-
return false
96+
return MutableState
7997
}
8098
}
8199

@@ -87,7 +105,7 @@ func (e *environment) ReadOnlyState() libevm.StateReader {
87105
}
88106

89107
func (e *environment) StateDB() StateDB {
90-
if e.ReadOnly() {
108+
if e.StateMutability() != MutableState {
91109
return nil
92110
}
93111
return e.evm.StateDB
@@ -117,7 +135,13 @@ func (e *environment) Call(addr common.Address, input []byte, gas uint64, value
117135
return e.callContract(Call, addr, input, gas, value, opts...)
118136
}
119137

138+
var errPureFunctionMakeCall = errors.New("contract call from pure function")
139+
120140
func (e *environment) callContract(typ CallType, addr common.Address, input []byte, gas uint64, value *uint256.Int, opts ...CallOption) (retData []byte, retErr error) {
141+
if e.StateMutability() == Pure {
142+
return nil, errPureFunctionMakeCall
143+
}
144+
121145
// Depth and read-only setting are handled by [EVMInterpreter.Run], which
122146
// isn't used for precompiles, so we need to do it ourselves to maintain the
123147
// expected invariants.
@@ -126,7 +150,7 @@ func (e *environment) callContract(typ CallType, addr common.Address, input []by
126150
in.evm.depth++
127151
defer func() { in.evm.depth-- }()
128152

129-
if e.ReadOnly() && !in.readOnly { // i.e. the precompile was StaticCall()ed
153+
if e.StateMutability() != MutableState && !in.readOnly { // i.e. the precompile was StaticCall()ed
130154
in.readOnly = true
131155
defer func() { in.readOnly = false }()
132156
}

libevm/precompilegen/precompile.go.tmpl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,9 @@ func (p precompile) run(env vm.PrecompileEnvironment, input []byte) ([]byte, err
7878
}
7979
case "payable":
8080
case "pure":
81-
env.SetPure()
81+
env = env.AsPure()
8282
case "view":
83-
env.SetReadOnly()
83+
env = env.AsReadOnly()
8484
default:
8585
// If this happens then `precompilegen` needs to be extended because the
8686
// Solidity ABI spec changed.

libevm/precompilegen/testprecompile/generated.go

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)