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() {}