diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 851cb51eb9b..1282b281d63 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -18,6 +18,6 @@ jobs: go-version: 1.21.4 - name: Run tests run: | # Upstream flakes are race conditions exacerbated by concurrent tests - FLAKY_REGEX='go-ethereum/(eth|accounts/keystore|eth/downloader|miner|ethclient/gethclient|eth/catalyst)$'; + FLAKY_REGEX='go-ethereum/(eth|accounts/keystore|eth/downloader|miner|ethclient|ethclient/gethclient|eth/catalyst)$'; go list ./... | grep -P "${FLAKY_REGEX}" | xargs -n 1 go test -short; go test -short $(go list ./... | grep -Pv "${FLAKY_REGEX}"); diff --git a/core/vm/contracts.libevm.go b/core/vm/contracts.libevm.go index a9312027703..78b634ad4bf 100644 --- a/core/vm/contracts.libevm.go +++ b/core/vm/contracts.libevm.go @@ -30,18 +30,20 @@ import ( // evmCallArgs mirrors the parameters of the [EVM] methods Call(), CallCode(), // DelegateCall() and StaticCall(). Its fields are identical to those of the -// parameters, prepended with the receiver name and appended with additional -// values. As {Delegate,Static}Call don't accept a value, they MUST set the -// respective field to nil. +// parameters, prepended with the receiver name and call type. As +// {Delegate,Static}Call don't accept a value, they MAY set the respective field +// to nil as it will be ignored. // // Instantiation can be achieved by merely copying the parameter names, in // order, which is trivially achieved with AST manipulation: // -// func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas uint64, value *uint256.Int) ... { +// func (evm *EVM) StaticCall(caller ContractRef, addr common.Address, input []byte, gas uint64) ... { // ... -// args := &evmCallArgs{evm, caller, addr, input, gas, value, false} +// args := &evmCallArgs{evm, staticCall, caller, addr, input, gas, nil /*value*/} type evmCallArgs struct { - evm *EVM + evm *EVM + callType callType + // args:start caller ContractRef addr common.Address @@ -49,28 +51,22 @@ type evmCallArgs struct { gas uint64 value *uint256.Int // args:end - - // 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. StaticCall() MUST set - // this to forceReadOnly and all other methods MUST set it to - // inheritReadOnly; i.e. equivalent to the boolean they each pass to - // EVMInterpreter.Run(). - readWrite rwInheritance } -type rwInheritance uint8 +type callType uint8 const ( - inheritReadOnly rwInheritance = iota + 1 - forceReadOnly + call callType = iota + 1 + callCode + delegateCall + staticCall ) // run runs the [PrecompiledContract], differentiating between stateful and // regular types. func (args *evmCallArgs) run(p PrecompiledContract, input []byte, suppliedGas uint64) (ret []byte, remainingGas uint64, err error) { if p, ok := p.(statefulPrecompile); ok { - return p(args, input, suppliedGas) + return p(args.env(), input, suppliedGas) } // Gas consumption for regular precompiles was already handled by the native // RunPrecompiledContract(), which called this method. @@ -107,8 +103,9 @@ func (p statefulPrecompile) Run([]byte) ([]byte, error) { panic(fmt.Sprintf("BUG: call to %T.Run(); MUST call %T itself", p, p)) } -// A PrecompileEnvironment provides information about the context in which a -// precompiled contract is being run. +// A PrecompileEnvironment provides (a) information about the context in which a +// precompiled contract is being run; and (b) a means of calling other +// contracts. type PrecompileEnvironment interface { ChainConfig() *params.ChainConfig Rules() params.Rules @@ -122,78 +119,62 @@ type PrecompileEnvironment interface { BlockHeader() (types.Header, error) BlockNumber() *big.Int BlockTime() uint64 -} - -// -// ****** SECURITY ****** -// -// If you are updating PrecompileEnvironment to provide the ability to call back -// into another contract, you MUST revisit the evmCallArgs.forceReadOnly flag. -// -// It is possible that forceReadOnly is true but evm.interpreter.readOnly is -// false. This is safe for now, but may not be if recursive calling *from* a -// precompile is enabled. -// -// ****** SECURITY ****** - -var _ PrecompileEnvironment = (*evmCallArgs)(nil) - -func (args *evmCallArgs) ChainConfig() *params.ChainConfig { return args.evm.chainConfig } -func (args *evmCallArgs) Rules() params.Rules { return args.evm.chainRules } -func (args *evmCallArgs) ReadOnly() bool { - if args.readWrite == inheritReadOnly { - if args.evm.interpreter.readOnly { //nolint:gosimple // Clearer code coverage for difficult-to-test branch - return true - } - return false - } - // Even though args.readWrite may be some value other than forceReadOnly, - // that would be an invalid use of the API so we default to read-only as the - // safest failure mode. - return true + // Call is equivalent to [EVM.Call] except that the `caller` argument is + // removed and automatically determined according to the type of call that + // invoked the precompile. + Call(addr common.Address, input []byte, gas uint64, value *uint256.Int, _ ...CallOption) (ret []byte, gasRemaining uint64, _ error) } -func (args *evmCallArgs) StateDB() StateDB { - if args.ReadOnly() { - return nil +func (args *evmCallArgs) env() *environment { + var ( + self common.Address + value = args.value + ) + switch args.callType { + case staticCall: + value = new(uint256.Int) + fallthrough + case call: + self = args.addr + + case delegateCall: + value = nil + fallthrough + case callCode: + self = args.caller.Address() } - return args.evm.StateDB -} - -func (args *evmCallArgs) ReadOnlyState() libevm.StateReader { - // Even though we're actually returning a full state database, the user - // would have to actively circumvent the returned interface to use it. At - // that point they're off-piste and it's not our problem. - return args.evm.StateDB -} -func (args *evmCallArgs) Addresses() *libevm.AddressContext { - return &libevm.AddressContext{ - Origin: args.evm.TxContext.Origin, - Caller: args.caller.Address(), - Self: args.addr, + // This is equivalent to the `contract` variables created by evm.*Call*() + // methods, for non precompiles, to pass to [EVMInterpreter.Run]. + contract := NewContract(args.caller, AccountRef(self), value, args.gas) + if args.callType == delegateCall { + contract = contract.AsDelegate() } -} -func (args *evmCallArgs) BlockHeader() (types.Header, error) { - hdr := args.evm.Context.Header - if hdr == nil { - // Although [core.NewEVMBlockContext] sets the field and is in the - // typical hot path (e.g. miner), there are other ways to create a - // [vm.BlockContext] (e.g. directly in tests) that may result in no - // available header. - return types.Header{}, fmt.Errorf("nil %T in current %T", hdr, args.evm.Context) + return &environment{ + evm: args.evm, + self: contract, + forceReadOnly: args.readOnly(), } - return *hdr, nil } -func (args *evmCallArgs) BlockNumber() *big.Int { - return new(big.Int).Set(args.evm.Context.BlockNumber) +func (args *evmCallArgs) readOnly() bool { + // A switch statement provides clearer code coverage for difficult-to-test + // cases. + switch { + case args.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 args.evm.interpreter.readOnly: + return true + default: + return false + } } -func (args *evmCallArgs) BlockTime() uint64 { return args.evm.Context.Time } - var ( // These lock in the assumptions made when implementing [evmCallArgs]. If // these break then the struct fields SHOULD be changed to match these diff --git a/core/vm/contracts.libevm_test.go b/core/vm/contracts.libevm_test.go index b28174b3823..f1cecca30cf 100644 --- a/core/vm/contracts.libevm_test.go +++ b/core/vm/contracts.libevm_test.go @@ -100,7 +100,7 @@ func TestPrecompileOverride(t *testing.T) { type statefulPrecompileOutput struct { ChainID *big.Int - Caller, Self common.Address + Addresses *libevm.AddressContext StateValue common.Hash ReadOnly bool BlockNumber, Difficulty *big.Int @@ -116,17 +116,24 @@ func (o statefulPrecompileOutput) String() string { fld := out.Field(i).Interface() verb := "%v" - if _, ok := fld.([]byte); ok { + switch fld.(type) { + case []byte: verb = "%#x" + case *libevm.AddressContext: + verb = "%+v" } lines = append(lines, fmt.Sprintf("%s: "+verb, name, fld)) } return strings.Join(lines, "\n") } +func (o statefulPrecompileOutput) Bytes() []byte { + return []byte(o.String()) +} + func TestNewStatefulPrecompile(t *testing.T) { + precompile := common.HexToAddress("60C0DE") // GO CODE rng := ethtest.NewPseudoRand(314159) - precompile := rng.Address() slot := rng.Hash() const gasLimit = 1e6 @@ -141,11 +148,9 @@ func TestNewStatefulPrecompile(t *testing.T) { return nil, 0, err } - addrs := env.Addresses() out := &statefulPrecompileOutput{ ChainID: env.ChainConfig().ChainID, - Caller: addrs.Caller, - Self: addrs.Self, + Addresses: env.Addresses(), StateValue: env.ReadOnlyState().GetState(precompile, slot), ReadOnly: env.ReadOnly(), BlockNumber: env.BlockNumber(), @@ -153,7 +158,7 @@ func TestNewStatefulPrecompile(t *testing.T) { Difficulty: hdr.Difficulty, Input: input, } - return []byte(out.String()), suppliedGas - gasCost, nil + return out.Bytes(), suppliedGas - gasCost, nil } hooks := &hookstest.Stub{ PrecompileOverrides: map[common.Address]libevm.PrecompiledContract{ @@ -167,11 +172,14 @@ func TestNewStatefulPrecompile(t *testing.T) { Time: rng.Uint64(), Difficulty: rng.BigUint64(), } - caller := rng.Address() input := rng.Bytes(8) value := rng.Hash() chainID := rng.BigUint64() + caller := common.HexToAddress("CA11E12") // caller of the precompile + eoa := common.HexToAddress("E0A") // caller of the precompile-caller + callerContract := vm.NewContract(vm.AccountRef(eoa), vm.AccountRef(caller), uint256.NewInt(0), 1e6) + state, evm := ethtest.NewZeroEVM( t, ethtest.WithBlockContext( @@ -182,39 +190,61 @@ func TestNewStatefulPrecompile(t *testing.T) { ), ) state.SetState(precompile, slot, value) + evm.Origin = eoa tests := []struct { - name string - call func() ([]byte, uint64, error) - // Note that this only covers evm.readWrite being set to forceReadOnly, - // via StaticCall(). See TestInheritReadOnly for alternate case. + name string + call func() ([]byte, uint64, error) + wantAddresses *libevm.AddressContext + // Note that this only covers evm.readOnly being true because of the + // precompile's call. See TestInheritReadOnly for alternate case. wantReadOnly bool }{ { name: "EVM.Call()", call: func() ([]byte, uint64, error) { - return evm.Call(vm.AccountRef(caller), precompile, input, gasLimit, uint256.NewInt(0)) + return evm.Call(callerContract, precompile, input, gasLimit, uint256.NewInt(0)) + }, + wantAddresses: &libevm.AddressContext{ + Origin: eoa, + Caller: caller, + Self: precompile, }, wantReadOnly: false, }, { name: "EVM.CallCode()", call: func() ([]byte, uint64, error) { - return evm.CallCode(vm.AccountRef(caller), precompile, input, gasLimit, uint256.NewInt(0)) + return evm.CallCode(callerContract, precompile, input, gasLimit, uint256.NewInt(0)) + }, + wantAddresses: &libevm.AddressContext{ + Origin: eoa, + Caller: caller, + Self: caller, }, wantReadOnly: false, }, { name: "EVM.DelegateCall()", call: func() ([]byte, uint64, error) { - return evm.DelegateCall(vm.AccountRef(caller), precompile, input, gasLimit) + return evm.DelegateCall(callerContract, precompile, input, gasLimit) + }, + wantAddresses: &libevm.AddressContext{ + Origin: eoa, + Caller: eoa, // inherited from caller + Self: caller, }, wantReadOnly: false, }, { name: "EVM.StaticCall()", call: func() ([]byte, uint64, error) { - return evm.StaticCall(vm.AccountRef(caller), precompile, input, gasLimit) + return evm.StaticCall(callerContract, precompile, input, gasLimit) + }, + wantAddresses: &libevm.AddressContext{ + Origin: eoa, + Caller: caller, + Self: precompile, }, wantReadOnly: true, }, @@ -222,22 +252,22 @@ func TestNewStatefulPrecompile(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - wantReturnData := statefulPrecompileOutput{ + wantOutput := statefulPrecompileOutput{ ChainID: chainID, - Caller: caller, - Self: precompile, + Addresses: tt.wantAddresses, StateValue: value, ReadOnly: tt.wantReadOnly, BlockNumber: header.Number, BlockTime: header.Time, Difficulty: header.Difficulty, Input: input, - }.String() + } + wantGasLeft := gasLimit - gasCost gotReturnData, gotGasLeft, err := tt.call() require.NoError(t, err) - assert.Equal(t, wantReturnData, string(gotReturnData)) + assert.Equal(t, wantOutput.String(), string(gotReturnData)) assert.Equal(t, wantGasLeft, gotGasLeft) }) } @@ -265,9 +295,7 @@ func TestInheritReadOnly(t *testing.T) { // (1) - var precompile common.Address - const precompileAddr = 255 - precompile[common.AddressLength-1] = precompileAddr + precompile := common.Address{255} const ( ifReadOnly = iota + 1 // see contract bytecode for rationale @@ -293,31 +321,13 @@ func TestInheritReadOnly(t *testing.T) { }) // (2) - - // See CALL signature: https://www.evm.codes/#f1?fork=cancun - const p0 = vm.PUSH0 - contract := []vm.OpCode{ - vm.PUSH1, 1, // retSize (bytes) - p0, // retOffset - p0, // argSize - p0, // argOffset - p0, // value - vm.PUSH1, precompileAddr, - p0, // gas - vm.CALL, - // It's ok to ignore the return status. If the CALL failed then we'll - // return []byte{0} next, and both non-failure return buffers are - // non-zero because of the `iota + 1`. - vm.PUSH1, 1, // size (byte) - p0, - vm.RETURN, - } + contract := makeReturnProxy(t, precompile, vm.CALL) state, evm := ethtest.NewZeroEVM(t) rng := ethtest.NewPseudoRand(42) contractAddr := rng.Address() state.CreateAccount(contractAddr) - state.SetCode(contractAddr, contractCode(contract)) + state.SetCode(contractAddr, convertBytes[vm.OpCode, byte](contract)) // (3) @@ -352,14 +362,54 @@ func TestInheritReadOnly(t *testing.T) { } } -// contractCode converts a slice of op codes into a byte buffer for storage as -// contract code. -func contractCode(ops []vm.OpCode) []byte { - ret := make([]byte, len(ops)) - for i, o := range ops { - ret[i] = byte(o) +// makeReturnProxy returns the bytecode of a contract that will call `dest` with +// the specified call type and propagated the returned value. +// +// The contract does NOT check if the call reverted. In this case, the +// propagated return value will always be an empty slice. Tests using these +// proxies MUST use non-empty slices as test values. +// +// TODO(arr4n): convert this to arr4n/specops for clarity and to make it easier +// to generate a revert check. +func makeReturnProxy(t *testing.T, dest common.Address, call vm.OpCode) []vm.OpCode { + t.Helper() + const p0 = vm.PUSH0 + contract := []vm.OpCode{ + vm.PUSH1, 1, // retSize (bytes) + p0, // retOffset + p0, // argSize + p0, // argOffset + } + + // See CALL signature: https://www.evm.codes/#f1?fork=cancun + switch call { + case vm.CALL, vm.CALLCODE: + contract = append(contract, p0) // value + case vm.DELEGATECALL, vm.STATICCALL: + default: + t.Fatalf("Bad test setup: invalid non-CALL-type opcode %s", call) } - return ret + + contract = append(contract, vm.PUSH20) + contract = append(contract, convertBytes[byte, vm.OpCode](dest[:])...) + + contract = append(contract, + p0, // gas + call, + + // See function comment re ignored reverts. + vm.RETURNDATASIZE, p0, p0, vm.RETURNDATACOPY, + vm.RETURNDATASIZE, p0, vm.RETURN, + ) + return contract +} + +func convertBytes[From ~byte, To ~byte](buf []From) []To { + out := make([]To, len(buf)) + for i, b := range buf { + out[i] = To(b) + } + return out } func TestCanCreateContract(t *testing.T) { @@ -445,3 +495,116 @@ func TestActivePrecompilesOverride(t *testing.T) { require.Equal(t, precompiles, vm.ActivePrecompiles(newRules()), "vm.ActivePrecompiles() returns overridden addresses") } + +func TestPrecompileMakeCall(t *testing.T) { + // There is one test per *CALL* op code: + // + // 1. `eoa` makes a call to a bytecode contract, `caller`; + // 2. `caller` calls `sut`, the precompile under test, via the test's *CALL* op code; + // 3. `sut` makes a Call() to `dest`, which reflects env data for testing. + // + // This acts as a full integration test of a precompile being invoked before + // making an "outbound" call. + eoa := common.HexToAddress("E0A") + caller := common.HexToAddress("CA11E12") + sut := common.HexToAddress("7E57ED") + dest := common.HexToAddress("DE57") + + rng := ethtest.NewPseudoRand(142857) + callData := rng.Bytes(8) + + hooks := &hookstest.Stub{ + PrecompileOverrides: map[common.Address]libevm.PrecompiledContract{ + sut: vm.NewStatefulPrecompile(func(env vm.PrecompileEnvironment, input []byte, suppliedGas uint64) (ret []byte, remainingGas uint64, err error) { + // We are ultimately testing env.Call(), hence why this is the SUT. + return env.Call(dest, callData, suppliedGas, uint256.NewInt(0)) + }), + dest: vm.NewStatefulPrecompile(func(env vm.PrecompileEnvironment, input []byte, suppliedGas uint64) (ret []byte, remainingGas uint64, err error) { + out := &statefulPrecompileOutput{ + Addresses: env.Addresses(), + ReadOnly: env.ReadOnly(), + Input: input, // expected to be callData + } + return out.Bytes(), suppliedGas, nil + }), + }, + } + hookstest.Register(t, params.Extras[*hookstest.Stub, *hookstest.Stub]{ + NewRules: func(_ *params.ChainConfig, r *params.Rules, _ *hookstest.Stub, blockNum *big.Int, isMerge bool, timestamp uint64) *hookstest.Stub { + r.IsCancun = true // enable PUSH0 + return hooks + }, + }) + + tests := []struct { + incomingCallType vm.OpCode + // Unlike TestNewStatefulPrecompile, which tests the AddressContext of + // the precompile itself, these test the AddressContext of a contract + // called by the precompile. + want statefulPrecompileOutput + }{ + { + incomingCallType: vm.CALL, + want: statefulPrecompileOutput{ + Addresses: &libevm.AddressContext{ + Origin: eoa, + Caller: sut, + Self: dest, + }, + Input: callData, + }, + }, + { + incomingCallType: vm.CALLCODE, + want: statefulPrecompileOutput{ + Addresses: &libevm.AddressContext{ + Origin: eoa, + Caller: caller, // SUT runs as its own caller because of CALLCODE + Self: dest, + }, + Input: callData, + }, + }, + { + incomingCallType: vm.DELEGATECALL, + want: statefulPrecompileOutput{ + Addresses: &libevm.AddressContext{ + Origin: eoa, + Caller: caller, // as with CALLCODE + Self: dest, + }, + Input: callData, + }, + }, + { + incomingCallType: vm.STATICCALL, + want: statefulPrecompileOutput{ + Addresses: &libevm.AddressContext{ + Origin: eoa, + Caller: sut, + Self: dest, + }, + Input: callData, + // This demonstrates that even though the precompile makes a + // (non-static) CALL, the read-only state is inherited. Yes, + // this is _another_ way to get a read-only state, different to + // the other tests. + ReadOnly: true, + }, + }, + } + + for _, tt := range tests { + t.Run(fmt.Sprintf("via %s", tt.incomingCallType), func(t *testing.T) { + state, evm := ethtest.NewZeroEVM(t) + evm.Origin = eoa + state.CreateAccount(caller) + proxy := makeReturnProxy(t, sut, tt.incomingCallType) + state.SetCode(caller, convertBytes[vm.OpCode, byte](proxy)) + + got, _, err := evm.Call(vm.AccountRef(eoa), caller, nil, 1e6, uint256.NewInt(0)) + require.NoError(t, err) + require.Equal(t, tt.want.String(), string(got)) + }) + } +} diff --git a/core/vm/environment.libevm.go b/core/vm/environment.libevm.go new file mode 100644 index 00000000000..75a10c0d857 --- /dev/null +++ b/core/vm/environment.libevm.go @@ -0,0 +1,120 @@ +// Copyright 2024 the libevm authors. +// +// The libevm additions to go-ethereum are free software: you can redistribute +// them and/or modify them under the terms of the GNU Lesser General Public License +// as published by the Free Software Foundation, either version 3 of the License, +// or (at your option) any later version. +// +// The libevm additions are distributed in the hope that they will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser +// General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see +// . + +package vm + +import ( + "fmt" + "math/big" + + "github.com/holiman/uint256" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/libevm" + "github.com/ethereum/go-ethereum/params" +) + +var _ PrecompileEnvironment = (*environment)(nil) + +type environment struct { + evm *EVM + self *Contract + forceReadOnly bool +} + +func (e *environment) ChainConfig() *params.ChainConfig { return e.evm.chainConfig } +func (e *environment) Rules() params.Rules { return e.evm.chainRules } +func (e *environment) ReadOnly() bool { return e.forceReadOnly || e.evm.interpreter.readOnly } +func (e *environment) ReadOnlyState() libevm.StateReader { return e.evm.StateDB } +func (e *environment) BlockNumber() *big.Int { return new(big.Int).Set(e.evm.Context.BlockNumber) } +func (e *environment) BlockTime() uint64 { return e.evm.Context.Time } + +func (e *environment) Addresses() *libevm.AddressContext { + return &libevm.AddressContext{ + Origin: e.evm.Origin, + Caller: e.self.CallerAddress, + Self: e.self.Address(), + } +} + +func (e *environment) StateDB() StateDB { + if e.ReadOnly() { + return nil + } + return e.evm.StateDB +} + +func (e *environment) BlockHeader() (types.Header, error) { + hdr := e.evm.Context.Header + if hdr == nil { + // Although [core.NewEVMBlockContext] sets the field and is in the + // typical hot path (e.g. miner), there are other ways to create a + // [vm.BlockContext] (e.g. directly in tests) that may result in no + // available header. + return types.Header{}, fmt.Errorf("nil %T in current %T", hdr, e.evm.Context) + } + return *hdr, nil +} + +func (e *environment) Call(addr common.Address, input []byte, gas uint64, value *uint256.Int, opts ...CallOption) ([]byte, uint64, error) { + return e.callContract(call, addr, input, gas, value, opts...) +} + +func (e *environment) callContract(typ callType, addr common.Address, input []byte, gas uint64, value *uint256.Int, opts ...CallOption) ([]byte, uint64, 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.forceReadOnly && !in.readOnly { // i.e. the precompile was StaticCall()ed + in.readOnly = true + defer func() { in.readOnly = false }() + } + + var caller ContractRef = e.self + for _, o := range opts { + switch o := o.(type) { + case callOptUNSAFECallerAddressProxy: + // Note that, in addition to being unsafe, this breaks an EVM + // assumption that the caller ContractRef is always a *Contract. + caller = AccountRef(e.self.CallerAddress) + case nil: + default: + return nil, gas, fmt.Errorf("unsupported option %T", o) + } + } + + switch typ { + case call: + if in.readOnly && !value.IsZero() { + return nil, gas, ErrWriteProtection + } + return e.evm.Call(caller, addr, input, gas, value) + case callCode, delegateCall, staticCall: + // TODO(arr4n): these cases should be very similar to CALL, hence the + // early abstraction, to signal to future maintainers. If implementing + // them, there's likely no need to honour the + // [callOptUNSAFECallerAddressProxy] because it's purely for backwards + // compatibility. + fallthrough + default: + return nil, gas, fmt.Errorf("unimplemented precompile call type %v", typ) + } +} diff --git a/core/vm/evm.go b/core/vm/evm.go index 83d9d3fbc77..c6c735627fd 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -230,7 +230,7 @@ func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas } if isPrecompile { - args := &evmCallArgs{evm, caller, addr, input, gas, value, inheritReadOnly} + args := &evmCallArgs{evm, call, caller, addr, input, gas, value} ret, gas, err = args.RunPrecompiledContract(p, input, gas) } else { // Initialise a new contract and set the code that is to be used by the EVM. @@ -294,7 +294,7 @@ func (evm *EVM) CallCode(caller ContractRef, addr common.Address, input []byte, // It is allowed to call precompiles, even via delegatecall if p, isPrecompile := evm.precompile(addr); isPrecompile { - args := &evmCallArgs{evm, caller, addr, input, gas, value, inheritReadOnly} + args := &evmCallArgs{evm, callCode, caller, addr, input, gas, value} ret, gas, err = args.RunPrecompiledContract(p, input, gas) } else { addrCopy := addr @@ -340,7 +340,7 @@ func (evm *EVM) DelegateCall(caller ContractRef, addr common.Address, input []by // It is allowed to call precompiles, even via delegatecall if p, isPrecompile := evm.precompile(addr); isPrecompile { - args := &evmCallArgs{evm, caller, addr, input, gas, nil, inheritReadOnly} + args := &evmCallArgs{evm, delegateCall, caller, addr, input, gas, nil} ret, gas, err = args.RunPrecompiledContract(p, input, gas) } else { addrCopy := addr @@ -390,7 +390,7 @@ func (evm *EVM) StaticCall(caller ContractRef, addr common.Address, input []byte } if p, isPrecompile := evm.precompile(addr); isPrecompile { - args := &evmCallArgs{evm, caller, addr, input, gas, nil, forceReadOnly} + args := &evmCallArgs{evm, staticCall, caller, addr, input, gas, nil} ret, gas, err = args.RunPrecompiledContract(p, input, gas) } else { // At this point, we use a copy of address. If we don't, the go compiler will diff --git a/core/vm/options.libevm.go b/core/vm/options.libevm.go new file mode 100644 index 00000000000..94ecdbb0457 --- /dev/null +++ b/core/vm/options.libevm.go @@ -0,0 +1,38 @@ +// Copyright 2024 the libevm authors. +// +// The libevm additions to go-ethereum are free software: you can redistribute +// them and/or modify them under the terms of the GNU Lesser General Public License +// as published by the Free Software Foundation, either version 3 of the License, +// or (at your option) any later version. +// +// The libevm additions are distributed in the hope that they will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser +// General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see +// . + +package vm + +// A CallOption modifies the default behaviour of a contract call. +type CallOption interface { + libevmCallOption() // noop to only allow internally defined options +} + +// WithUNSAFECallerAddressProxying results in precompiles making contract calls +// specifying their own caller's address as the caller. This is NOT SAFE for +// regular use as callers of the precompile may not understand that they are +// escalating the precompile's privileges. +// +// Deprecated: this option MUST NOT be used other than to allow migration to +// libevm when backwards compatibility is required. +func WithUNSAFECallerAddressProxying() CallOption { + return callOptUNSAFECallerAddressProxy{} +} + +// Deprecated: see [WithUNSAFECallerAddressProxying]. +type callOptUNSAFECallerAddressProxy struct{} + +func (callOptUNSAFECallerAddressProxy) libevmCallOption() {}