Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: vm.WithUNSAFECallerAddressProxying under DELEGATECALL #50

Merged
merged 7 commits into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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|eth/tracers/logger|accounts/keystore|eth/downloader|miner|ethclient|ethclient/gethclient|eth/catalyst)$';
FLAKY_REGEX='go-ethereum/(eth|eth/tracers/js|eth/tracers/logger|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}");
22 changes: 3 additions & 19 deletions core/vm/contracts.libevm.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,25 +153,9 @@ func (args *evmCallArgs) env() *environment {
}

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
evm: args.evm,
self: contract,
callType: args.callType,
}
}

Expand Down
86 changes: 74 additions & 12 deletions core/vm/contracts.libevm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package vm_test

import (
"bytes"
"fmt"
"math/big"
"reflect"
Expand Down Expand Up @@ -375,10 +376,12 @@ func makeReturnProxy(t *testing.T, dest common.Address, call vm.OpCode) []vm.OpC
t.Helper()
const p0 = vm.PUSH0
contract := []vm.OpCode{
vm.PUSH1, 1, // retSize (bytes)
p0, // retOffset
p0, // argSize
p0, // argOffset
vm.CALLDATASIZE, p0, p0, vm.CALLDATACOPY,

p0, // retSize
p0, // retOffset
vm.CALLDATASIZE, // argSize
p0, // argOffset
}

// See CALL signature: https://www.evm.codes/#f1?fork=cancun
Expand Down Expand Up @@ -511,13 +514,21 @@ func TestPrecompileMakeCall(t *testing.T) {
dest := common.HexToAddress("DE57")

rng := ethtest.NewPseudoRand(142857)
callData := rng.Bytes(8)
precompileCallData := rng.Bytes(8)

// If the SUT precompile receives this as its calldata then it will use the
// vm.WithUNSAFECallerAddressProxying() option.
unsafeCallerProxyOptSentinel := []byte("override-caller sentinel")

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) {
var opts []vm.CallOption
if bytes.Equal(input, unsafeCallerProxyOptSentinel) {
opts = append(opts, vm.WithUNSAFECallerAddressProxying())
}
// We are ultimately testing env.Call(), hence why this is the SUT.
return env.Call(dest, callData, suppliedGas, uint256.NewInt(0))
return env.Call(dest, precompileCallData, suppliedGas, uint256.NewInt(0), opts...)
}),
dest: vm.NewStatefulPrecompile(func(env vm.PrecompileEnvironment, input []byte, suppliedGas uint64) (ret []byte, remainingGas uint64, err error) {
out := &statefulPrecompileOutput{
Expand All @@ -538,6 +549,7 @@ func TestPrecompileMakeCall(t *testing.T) {

tests := []struct {
incomingCallType vm.OpCode
eoaTxCallData []byte
// Unlike TestNewStatefulPrecompile, which tests the AddressContext of
// the precompile itself, these test the AddressContext of a contract
// called by the precompile.
Expand All @@ -551,7 +563,19 @@ func TestPrecompileMakeCall(t *testing.T) {
Caller: sut,
Self: dest,
},
Input: callData,
Input: precompileCallData,
},
},
{
incomingCallType: vm.CALL,
eoaTxCallData: unsafeCallerProxyOptSentinel,
want: statefulPrecompileOutput{
Addresses: &libevm.AddressContext{
Origin: eoa,
Caller: caller, // overridden by CallOption
Self: dest,
},
Input: precompileCallData,
},
},
{
Expand All @@ -562,7 +586,19 @@ func TestPrecompileMakeCall(t *testing.T) {
Caller: caller, // SUT runs as its own caller because of CALLCODE
Self: dest,
},
Input: callData,
Input: precompileCallData,
},
},
{
incomingCallType: vm.CALLCODE,
eoaTxCallData: unsafeCallerProxyOptSentinel,
want: statefulPrecompileOutput{
Addresses: &libevm.AddressContext{
Origin: eoa,
Caller: caller, // CallOption is a NOOP
Self: dest,
},
Input: precompileCallData,
},
},
{
Expand All @@ -573,7 +609,19 @@ func TestPrecompileMakeCall(t *testing.T) {
Caller: caller, // as with CALLCODE
Self: dest,
},
Input: callData,
Input: precompileCallData,
},
},
{
incomingCallType: vm.DELEGATECALL,
eoaTxCallData: unsafeCallerProxyOptSentinel,
want: statefulPrecompileOutput{
Addresses: &libevm.AddressContext{
Origin: eoa,
Caller: caller, // CallOption is a NOOP
Self: dest,
},
Input: precompileCallData,
},
},
{
Expand All @@ -584,25 +632,39 @@ func TestPrecompileMakeCall(t *testing.T) {
Caller: sut,
Self: dest,
},
Input: callData,
Input: precompileCallData,
// 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,
},
},
{
incomingCallType: vm.STATICCALL,
eoaTxCallData: unsafeCallerProxyOptSentinel,
want: statefulPrecompileOutput{
Addresses: &libevm.AddressContext{
Origin: eoa,
Caller: caller, // overridden by CallOption
Self: dest,
},
Input: precompileCallData,
ReadOnly: true,
},
},
}

for _, tt := range tests {
t.Run(fmt.Sprintf("via %s", tt.incomingCallType), func(t *testing.T) {
t.Run(tt.incomingCallType.String(), func(t *testing.T) {
t.Logf("calldata = %q", tt.eoaTxCallData)
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))
got, _, err := evm.Call(vm.AccountRef(eoa), caller, tt.eoaTxCallData, 1e6, uint256.NewInt(0))
require.NoError(t, err)
require.Equal(t, tt.want.String(), string(got))
})
Expand Down
30 changes: 25 additions & 5 deletions core/vm/environment.libevm.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,33 @@ import (
var _ PrecompileEnvironment = (*environment)(nil)

type environment struct {
evm *EVM
self *Contract
forceReadOnly bool
evm *EVM
self *Contract
callType callType
}

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) ReadOnly() bool {
// A switch statement provides clearer code coverage for difficult-to-test
// cases.
switch {
case e.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 e.evm.interpreter.readOnly:
return true
default:
return false
}
}

func (e *environment) Addresses() *libevm.AddressContext {
return &libevm.AddressContext{
Origin: e.evm.Origin,
Expand Down Expand Up @@ -83,7 +98,7 @@ func (e *environment) callContract(typ callType, addr common.Address, input []by
in.evm.depth++
defer func() { in.evm.depth-- }()

if e.forceReadOnly && !in.readOnly { // i.e. the precompile was StaticCall()ed
if e.ReadOnly() && !in.readOnly { // i.e. the precompile was StaticCall()ed
in.readOnly = true
defer func() { in.readOnly = false }()
}
Expand All @@ -95,6 +110,11 @@ func (e *environment) callContract(typ callType, addr common.Address, input []by
// 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)
if e.callType == delegateCall {
// self was created with AsDelegate(), which means that
// CallerAddress was inherited.
caller = AccountRef(e.self.Address())
}
case nil:
default:
return nil, gas, fmt.Errorf("unsupported option %T", o)
Expand Down