feat: vm.PrecompiledStatefulContract can make CALLs (#40)

* feat: `vm.PrecompiledStatefulContract` can make `CALL`s

* fix: caller propagation

* feat: precompile can override default caller in `Call()`

* refactor: `WithUNSAFEForceDelegate()` replaces `WithCaller()`

* refactor: `WithUNSAFECallerAddressProxying()` instead of `ForceDelegate`

This matches the pattern used by `ava-labs/coreth` `NativeAssetCall`.

* refactor: `type callType` replaces `rwInheritance` + `delegation` types

* refactor: abstract return-data-proxy contract bytecode

* doc: fix comments from `46346f51`

* fix: `PrecompileEnvironment.Addresses()` for all call types

* chore: readability, linting & mark upstream test flaky

* test: `PrecompileEnvironment.Call()`

* refactor: improved {read,maintain}ability

* doc: fix `evmCallArgs` example

* test: `PrecompileEnvironment.Call()` input data

* fix: write protection for non-zero call value
This commit is contained in:
Arran Schlosberg 2024-09-30 17:26:50 +01:00 committed by GitHub
parent f1dba53688
commit 210f8ab8e1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 441 additions and 139 deletions

View file

@ -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}");

View file

@ -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
// 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)
}
//
// ****** 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 ******
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
var _ PrecompileEnvironment = (*evmCallArgs)(nil)
case delegateCall:
value = nil
fallthrough
case callCode:
self = args.caller.Address()
}
func (args *evmCallArgs) ChainConfig() *params.ChainConfig { return args.evm.chainConfig }
func (args *evmCallArgs) Rules() params.Rules { return args.evm.chainRules }
// 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) ReadOnly() bool {
if args.readWrite == inheritReadOnly {
if args.evm.interpreter.readOnly { //nolint:gosimple // Clearer code coverage for difficult-to-test branch
return true
}
return &environment{
evm: args.evm,
self: contract,
forceReadOnly: args.readOnly(),
}
}
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
}
// 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
}
func (args *evmCallArgs) StateDB() StateDB {
if args.ReadOnly() {
return nil
}
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,
}
}
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 *hdr, nil
}
func (args *evmCallArgs) BlockNumber() *big.Int {
return new(big.Int).Set(args.evm.Context.BlockNumber)
}
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

View file

@ -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
}
return ret
// 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)
}
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))
})
}
}

View file

@ -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
// <http://www.gnu.org/licenses/>.
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)
}
}

View file

@ -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

38
core/vm/options.libevm.go Normal file
View file

@ -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
// <http://www.gnu.org/licenses/>.
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() {}