mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-06-20 13:44:31 +00:00
* 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
610 lines
18 KiB
Go
610 lines
18 KiB
Go
// 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_test
|
|
|
|
import (
|
|
"fmt"
|
|
"math/big"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/holiman/uint256"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/exp/rand"
|
|
|
|
"github.com/ethereum/go-ethereum/common"
|
|
"github.com/ethereum/go-ethereum/core"
|
|
"github.com/ethereum/go-ethereum/core/types"
|
|
"github.com/ethereum/go-ethereum/core/vm"
|
|
"github.com/ethereum/go-ethereum/crypto"
|
|
"github.com/ethereum/go-ethereum/libevm"
|
|
"github.com/ethereum/go-ethereum/libevm/ethtest"
|
|
"github.com/ethereum/go-ethereum/libevm/hookstest"
|
|
"github.com/ethereum/go-ethereum/params"
|
|
)
|
|
|
|
type precompileStub struct {
|
|
requiredGas uint64
|
|
returnData []byte
|
|
}
|
|
|
|
func (s *precompileStub) RequiredGas([]byte) uint64 { return s.requiredGas }
|
|
func (s *precompileStub) Run([]byte) ([]byte, error) { return s.returnData, nil }
|
|
|
|
func TestPrecompileOverride(t *testing.T) {
|
|
type test struct {
|
|
name string
|
|
addr common.Address
|
|
requiredGas uint64
|
|
stubData []byte
|
|
}
|
|
|
|
const gasLimit = uint64(1e7)
|
|
|
|
tests := []test{
|
|
{
|
|
name: "arbitrary values",
|
|
addr: common.Address{'p', 'r', 'e', 'c', 'o', 'm', 'p', 'i', 'l', 'e'},
|
|
requiredGas: 314159,
|
|
stubData: []byte("the return data"),
|
|
},
|
|
}
|
|
|
|
rng := rand.New(rand.NewSource(42))
|
|
for _, addr := range vm.PrecompiledAddressesCancun {
|
|
tests = append(tests, test{
|
|
name: fmt.Sprintf("existing precompile %v", addr),
|
|
addr: addr,
|
|
requiredGas: rng.Uint64n(gasLimit),
|
|
stubData: addr[:],
|
|
})
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
hooks := &hookstest.Stub{
|
|
PrecompileOverrides: map[common.Address]libevm.PrecompiledContract{
|
|
tt.addr: &precompileStub{
|
|
requiredGas: tt.requiredGas,
|
|
returnData: tt.stubData,
|
|
},
|
|
},
|
|
}
|
|
hooks.Register(t)
|
|
|
|
t.Run(fmt.Sprintf("%T.Call([overridden precompile address = %v])", &vm.EVM{}, tt.addr), func(t *testing.T) {
|
|
_, evm := ethtest.NewZeroEVM(t)
|
|
gotData, gotGasLeft, err := evm.Call(vm.AccountRef{}, tt.addr, nil, gasLimit, uint256.NewInt(0))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, tt.stubData, gotData, "contract's return data")
|
|
assert.Equal(t, gasLimit-tt.requiredGas, gotGasLeft, "gas left")
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
type statefulPrecompileOutput struct {
|
|
ChainID *big.Int
|
|
Addresses *libevm.AddressContext
|
|
StateValue common.Hash
|
|
ReadOnly bool
|
|
BlockNumber, Difficulty *big.Int
|
|
BlockTime uint64
|
|
Input []byte
|
|
}
|
|
|
|
func (o statefulPrecompileOutput) String() string {
|
|
var lines []string
|
|
out := reflect.ValueOf(o)
|
|
for i, n := 0, out.NumField(); i < n; i++ {
|
|
name := out.Type().Field(i).Name
|
|
fld := out.Field(i).Interface()
|
|
|
|
verb := "%v"
|
|
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)
|
|
slot := rng.Hash()
|
|
|
|
const gasLimit = 1e6
|
|
gasCost := rng.Uint64n(gasLimit)
|
|
|
|
run := func(env vm.PrecompileEnvironment, input []byte, suppliedGas uint64) ([]byte, uint64, error) {
|
|
if got, want := env.StateDB() != nil, !env.ReadOnly(); got != want {
|
|
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)
|
|
}
|
|
hdr, err := env.BlockHeader()
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
out := &statefulPrecompileOutput{
|
|
ChainID: env.ChainConfig().ChainID,
|
|
Addresses: env.Addresses(),
|
|
StateValue: env.ReadOnlyState().GetState(precompile, slot),
|
|
ReadOnly: env.ReadOnly(),
|
|
BlockNumber: env.BlockNumber(),
|
|
BlockTime: env.BlockTime(),
|
|
Difficulty: hdr.Difficulty,
|
|
Input: input,
|
|
}
|
|
return out.Bytes(), suppliedGas - gasCost, nil
|
|
}
|
|
hooks := &hookstest.Stub{
|
|
PrecompileOverrides: map[common.Address]libevm.PrecompiledContract{
|
|
precompile: vm.NewStatefulPrecompile(run),
|
|
},
|
|
}
|
|
hooks.Register(t)
|
|
|
|
header := &types.Header{
|
|
Number: rng.BigUint64(),
|
|
Time: rng.Uint64(),
|
|
Difficulty: rng.BigUint64(),
|
|
}
|
|
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(
|
|
core.NewEVMBlockContext(header, nil, rng.AddressPtr()),
|
|
),
|
|
ethtest.WithChainConfig(
|
|
¶ms.ChainConfig{ChainID: chainID},
|
|
),
|
|
)
|
|
state.SetState(precompile, slot, value)
|
|
evm.Origin = eoa
|
|
|
|
tests := []struct {
|
|
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(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(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(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(callerContract, precompile, input, gasLimit)
|
|
},
|
|
wantAddresses: &libevm.AddressContext{
|
|
Origin: eoa,
|
|
Caller: caller,
|
|
Self: precompile,
|
|
},
|
|
wantReadOnly: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
wantOutput := statefulPrecompileOutput{
|
|
ChainID: chainID,
|
|
Addresses: tt.wantAddresses,
|
|
StateValue: value,
|
|
ReadOnly: tt.wantReadOnly,
|
|
BlockNumber: header.Number,
|
|
BlockTime: header.Time,
|
|
Difficulty: header.Difficulty,
|
|
Input: input,
|
|
}
|
|
|
|
wantGasLeft := gasLimit - gasCost
|
|
|
|
gotReturnData, gotGasLeft, err := tt.call()
|
|
require.NoError(t, err)
|
|
assert.Equal(t, wantOutput.String(), string(gotReturnData))
|
|
assert.Equal(t, wantGasLeft, gotGasLeft)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestInheritReadOnly(t *testing.T) {
|
|
// The regular test of stateful precompiles only checks the read-only state
|
|
// when called directly via vm.EVM.*Call*() methods. That approach will not
|
|
// result in a read-only state via inheritance, which occurs when already in
|
|
// a read-only environment there is a non-static call to a precompile.
|
|
//
|
|
// Test strategy:
|
|
//
|
|
// 1. Create a precompile that echoes its read-only status in the return
|
|
// data. We MUST NOT assert inside the precompile as we need proof that
|
|
// the precompile was actually called.
|
|
//
|
|
// 2. Create a bytecode contract that calls the precompile with CALL and
|
|
// propagates the return data. Using CALL (i.e. not STATICCALL) means
|
|
// that we know for certain that [forceReadOnly] isn't being used and,
|
|
// instead, the read-only state is being read from
|
|
// evm.interpreter.readOnly.
|
|
//
|
|
// 3. Assert that the returned input is as expected for the read-only state.
|
|
|
|
// (1)
|
|
|
|
precompile := common.Address{255}
|
|
|
|
const (
|
|
ifReadOnly = iota + 1 // see contract bytecode for rationale
|
|
ifNotReadOnly
|
|
)
|
|
hooks := &hookstest.Stub{
|
|
PrecompileOverrides: map[common.Address]libevm.PrecompiledContract{
|
|
precompile: vm.NewStatefulPrecompile(
|
|
func(env vm.PrecompileEnvironment, input []byte, suppliedGas uint64) ([]byte, uint64, error) {
|
|
if env.ReadOnly() {
|
|
return []byte{ifReadOnly}, suppliedGas, nil
|
|
}
|
|
return []byte{ifNotReadOnly}, 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
|
|
},
|
|
})
|
|
|
|
// (2)
|
|
contract := makeReturnProxy(t, precompile, vm.CALL)
|
|
|
|
state, evm := ethtest.NewZeroEVM(t)
|
|
rng := ethtest.NewPseudoRand(42)
|
|
contractAddr := rng.Address()
|
|
state.CreateAccount(contractAddr)
|
|
state.SetCode(contractAddr, convertBytes[vm.OpCode, byte](contract))
|
|
|
|
// (3)
|
|
|
|
caller := vm.AccountRef(rng.Address())
|
|
tests := []struct {
|
|
name string
|
|
call func() ([]byte, uint64, error)
|
|
want byte
|
|
}{
|
|
{
|
|
name: "EVM.Call()",
|
|
call: func() ([]byte, uint64, error) {
|
|
return evm.Call(caller, contractAddr, []byte{}, 1e6, uint256.NewInt(0))
|
|
},
|
|
want: ifNotReadOnly,
|
|
},
|
|
{
|
|
name: "EVM.StaticCall()",
|
|
call: func() ([]byte, uint64, error) {
|
|
return evm.StaticCall(vm.AccountRef(rng.Address()), contractAddr, []byte{}, 1e6)
|
|
},
|
|
want: ifReadOnly,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, _, err := tt.call()
|
|
require.NoError(t, err)
|
|
require.Equalf(t, []byte{tt.want}, got, "want %d if read-only, otherwise %d", ifReadOnly, ifNotReadOnly)
|
|
})
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
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) {
|
|
rng := ethtest.NewPseudoRand(142857)
|
|
account := rng.Address()
|
|
slot := rng.Hash()
|
|
|
|
const gasLimit uint64 = 1e6
|
|
gasUsage := rng.Uint64n(gasLimit)
|
|
|
|
makeErr := func(cc *libevm.AddressContext, stateVal common.Hash) error {
|
|
return fmt.Errorf("Origin: %v Caller: %v Contract: %v State: %v", cc.Origin, cc.Caller, cc.Self, stateVal)
|
|
}
|
|
hooks := &hookstest.Stub{
|
|
CanCreateContractFn: func(cc *libevm.AddressContext, gas uint64, s libevm.StateReader) (uint64, error) {
|
|
return gas - gasUsage, makeErr(cc, s.GetState(account, slot))
|
|
},
|
|
}
|
|
hooks.Register(t)
|
|
|
|
origin := rng.Address()
|
|
caller := rng.Address()
|
|
value := rng.Hash()
|
|
code := []byte{byte(vm.STOP)}
|
|
salt := rng.Hash()
|
|
|
|
create := crypto.CreateAddress(caller, 0)
|
|
create2 := crypto.CreateAddress2(caller, salt, crypto.Keccak256(code))
|
|
|
|
tests := []struct {
|
|
name string
|
|
create func(*vm.EVM) ([]byte, common.Address, uint64, error)
|
|
wantErr error
|
|
}{
|
|
{
|
|
name: "Create",
|
|
create: func(evm *vm.EVM) ([]byte, common.Address, uint64, error) {
|
|
return evm.Create(vm.AccountRef(caller), code, gasLimit, uint256.NewInt(0))
|
|
},
|
|
wantErr: makeErr(&libevm.AddressContext{Origin: origin, Caller: caller, Self: create}, value),
|
|
},
|
|
{
|
|
name: "Create2",
|
|
create: func(evm *vm.EVM) ([]byte, common.Address, uint64, error) {
|
|
return evm.Create2(vm.AccountRef(caller), code, gasLimit, uint256.NewInt(0), new(uint256.Int).SetBytes(salt[:]))
|
|
},
|
|
wantErr: makeErr(&libevm.AddressContext{Origin: origin, Caller: caller, Self: create2}, value),
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
state, evm := ethtest.NewZeroEVM(t)
|
|
state.SetState(account, slot, value)
|
|
evm.TxContext.Origin = origin
|
|
|
|
_, _, gasRemaining, err := tt.create(evm)
|
|
require.EqualError(t, err, tt.wantErr.Error())
|
|
// require prints uint64s in hex
|
|
require.Equal(t, int(gasLimit-gasUsage), int(gasRemaining), "gas remaining") //nolint:gosec // G115 won't overflow as <= 1e6
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestActivePrecompilesOverride(t *testing.T) {
|
|
newRules := func() params.Rules {
|
|
return new(params.ChainConfig).Rules(big.NewInt(0), false, 0)
|
|
}
|
|
defaultActive := vm.ActivePrecompiles(newRules())
|
|
|
|
rng := ethtest.NewPseudoRand(0xDecafC0ffeeBad)
|
|
precompiles := make([]common.Address, rng.Intn(10)+5)
|
|
for i := range precompiles {
|
|
precompiles[i] = rng.Address()
|
|
}
|
|
hooks := &hookstest.Stub{
|
|
ActivePrecompilesFn: func(active []common.Address) []common.Address {
|
|
assert.Equal(t, defaultActive, active, "ActivePrecompiles() hook receives default addresses")
|
|
return precompiles
|
|
},
|
|
}
|
|
hooks.Register(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))
|
|
})
|
|
}
|
|
}
|