Skip to content

feat!: disambiguate EVM-semantic and raw caller/self addresses for precompiles #211

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

Merged
merged 4 commits into from
Aug 7, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
8 changes: 5 additions & 3 deletions core/vm/contracts.libevm.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,9 +223,11 @@ func (args *evmCallArgs) env() *environment {
}

return &environment{
evm: args.evm,
self: contract,
callType: args.callType,
evm: args.evm,
self: contract,
callType: args.callType,
rawCaller: args.caller.Address(),
rawSelf: args.addr,
}
}

Expand Down
146 changes: 92 additions & 54 deletions core/vm/contracts.libevm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ type statefulPrecompileOutput struct {
func (o statefulPrecompileOutput) String() string {
var lines []string
out := reflect.ValueOf(o)
FieldLoop:
for i, n := 0, out.NumField(); i < n; i++ {
name := out.Type().Field(i).Name
fld := out.Field(i).Interface()
Expand All @@ -129,7 +130,12 @@ func (o statefulPrecompileOutput) String() string {
case []byte:
verb = "%#x"
case *libevm.AddressContext:
verb = "%+v"
lines = append(
lines,
fmt.Sprintf("EVMSemantic addresses: %+v", o.Addresses.EVMSemantic),
fmt.Sprintf("Raw addresses: %+v", o.Addresses.Raw),
)
continue FieldLoop
case vm.CallType:
verb = "%d (%[2]q)"
}
Expand Down Expand Up @@ -211,6 +217,13 @@ func TestNewStatefulPrecompile(t *testing.T) {
state.SetBalance(caller, new(uint256.Int).Not(uint256.NewInt(0)))
evm.Origin = eoa

// By definition, the raw caller and self are the same for every test case,
// regardless of the incoming call type.
rawAddresses := libevm.CallerAndSelf{
Caller: caller,
Self: precompile,
}

tests := []struct {
name string
call func() ([]byte, uint64, error)
Expand All @@ -227,9 +240,9 @@ func TestNewStatefulPrecompile(t *testing.T) {
return evm.Call(callerContract, precompile, input, gasLimit, transferValue)
},
wantAddresses: &libevm.AddressContext{
Origin: eoa,
Caller: caller,
Self: precompile,
Origin: eoa,
EVMSemantic: rawAddresses,
Raw: &rawAddresses,
},
wantReadOnly: false,
wantTransferValue: transferValue,
Expand All @@ -242,8 +255,11 @@ func TestNewStatefulPrecompile(t *testing.T) {
},
wantAddresses: &libevm.AddressContext{
Origin: eoa,
Caller: caller,
Self: caller,
EVMSemantic: libevm.CallerAndSelf{
Caller: caller,
Self: caller,
},
Raw: &rawAddresses,
},
wantReadOnly: false,
wantTransferValue: transferValue,
Expand All @@ -256,8 +272,11 @@ func TestNewStatefulPrecompile(t *testing.T) {
},
wantAddresses: &libevm.AddressContext{
Origin: eoa,
Caller: eoa, // inherited from caller
Self: caller,
EVMSemantic: libevm.CallerAndSelf{
Caller: eoa, // inherited from caller
Self: caller,
},
Raw: &rawAddresses,
},
wantReadOnly: false,
wantTransferValue: uint256.NewInt(0),
Expand All @@ -269,9 +288,9 @@ func TestNewStatefulPrecompile(t *testing.T) {
return evm.StaticCall(callerContract, precompile, input, gasLimit)
},
wantAddresses: &libevm.AddressContext{
Origin: eoa,
Caller: caller,
Self: precompile,
Origin: eoa,
EVMSemantic: rawAddresses,
Raw: &rawAddresses,
},
wantReadOnly: true,
wantTransferValue: uint256.NewInt(0),
Expand Down Expand Up @@ -527,7 +546,7 @@ func TestCanCreateContract(t *testing.T) {
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)
return fmt.Errorf("Origin: %v Caller: %v Contract: %v State: %v", cc.Origin, cc.EVMSemantic.Caller, cc.EVMSemantic.Self, stateVal)
}
hooks := &hookstest.Stub{
CanCreateContractFn: func(cc *libevm.AddressContext, gas uint64, s libevm.StateReader) (uint64, error) {
Expand Down Expand Up @@ -555,14 +574,34 @@ func TestCanCreateContract(t *testing.T) {
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),
wantErr: makeErr(
&libevm.AddressContext{
Origin: origin,
EVMSemantic: libevm.CallerAndSelf{
Caller: caller,
Self: create,
},
// `Raw` is documented as always being nil.
},
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),
wantErr: makeErr(
&libevm.AddressContext{
Origin: origin,
EVMSemantic: libevm.CallerAndSelf{
Caller: caller,
Self: create2,
},
// As above re `Raw` always being nil.
},
value,
),
},
}

Expand Down Expand Up @@ -630,7 +669,10 @@ func TestPrecompileMakeCall(t *testing.T) {
if bytes.Equal(input, unsafeCallerProxyOptSentinel) {
opts = append(opts, vm.WithUNSAFECallerAddressProxying())
}
// We are ultimately testing env.Call(), hence why this is the SUT.
// We are ultimately testing env.Call(), hence why this is the
// SUT. If this is ever extended to include DELEGATECALL or
// CALLCODE then the expected [libevm.AddressContext.Raw] values
// of the tests cases also need to change.
return env.Call(dest, precompileCallData, env.Gas(), uint256.NewInt(0), opts...)
}),
dest: vm.NewStatefulPrecompile(func(env vm.PrecompileEnvironment, input []byte) (ret []byte, err error) {
Expand Down Expand Up @@ -663,8 +705,10 @@ func TestPrecompileMakeCall(t *testing.T) {
want: statefulPrecompileOutput{
Addresses: &libevm.AddressContext{
Origin: eoa,
Caller: sut,
Self: dest,
EVMSemantic: libevm.CallerAndSelf{
Caller: sut,
Self: dest,
},
},
Input: precompileCallData,
},
Expand All @@ -675,8 +719,10 @@ func TestPrecompileMakeCall(t *testing.T) {
want: statefulPrecompileOutput{
Addresses: &libevm.AddressContext{
Origin: eoa,
Caller: caller, // overridden by CallOption
Self: dest,
EVMSemantic: libevm.CallerAndSelf{
Caller: caller, // overridden by CallOption
Self: dest,
},
},
Input: precompileCallData,
},
Expand All @@ -686,8 +732,10 @@ func TestPrecompileMakeCall(t *testing.T) {
want: statefulPrecompileOutput{
Addresses: &libevm.AddressContext{
Origin: eoa,
Caller: caller, // SUT runs as its own caller because of CALLCODE
Self: dest,
EVMSemantic: libevm.CallerAndSelf{
Caller: caller, // SUT runs as its own caller because of CALLCODE
Self: dest,
},
},
Input: precompileCallData,
},
Expand All @@ -698,8 +746,10 @@ func TestPrecompileMakeCall(t *testing.T) {
want: statefulPrecompileOutput{
Addresses: &libevm.AddressContext{
Origin: eoa,
Caller: caller, // CallOption is a NOOP
Self: dest,
EVMSemantic: libevm.CallerAndSelf{
Caller: caller, // CallOption is a NOOP
Self: dest,
},
},
Input: precompileCallData,
},
Expand All @@ -709,8 +759,10 @@ func TestPrecompileMakeCall(t *testing.T) {
want: statefulPrecompileOutput{
Addresses: &libevm.AddressContext{
Origin: eoa,
Caller: caller, // as with CALLCODE
Self: dest,
EVMSemantic: libevm.CallerAndSelf{
Caller: caller, // as with CALLCODE
Self: dest,
},
},
Input: precompileCallData,
},
Expand All @@ -721,8 +773,10 @@ func TestPrecompileMakeCall(t *testing.T) {
want: statefulPrecompileOutput{
Addresses: &libevm.AddressContext{
Origin: eoa,
Caller: caller, // CallOption is a NOOP
Self: dest,
EVMSemantic: libevm.CallerAndSelf{
Caller: caller, // CallOption is a NOOP
Self: dest,
},
},
Input: precompileCallData,
},
Expand All @@ -732,8 +786,10 @@ func TestPrecompileMakeCall(t *testing.T) {
want: statefulPrecompileOutput{
Addresses: &libevm.AddressContext{
Origin: eoa,
Caller: sut,
Self: dest,
EVMSemantic: libevm.CallerAndSelf{
Caller: sut,
Self: dest,
},
},
Input: precompileCallData,
// This demonstrates that even though the precompile makes a
Expand All @@ -749,8 +805,10 @@ func TestPrecompileMakeCall(t *testing.T) {
want: statefulPrecompileOutput{
Addresses: &libevm.AddressContext{
Origin: eoa,
Caller: caller, // overridden by CallOption
Self: dest,
EVMSemantic: libevm.CallerAndSelf{
Caller: caller, // overridden by CallOption
Self: dest,
},
},
Input: precompileCallData,
ReadOnly: true,
Expand All @@ -760,6 +818,9 @@ func TestPrecompileMakeCall(t *testing.T) {

for _, tt := range tests {
t.Run(tt.incomingCallType.String(), func(t *testing.T) {
// From the perspective of `dest` after a CALL from `sut`.
tt.want.Addresses.Raw = &tt.want.Addresses.EVMSemantic

t.Logf("calldata = %q", tt.eoaTxCallData)
state, evm := ethtest.NewZeroEVM(t)
evm.Origin = eoa
Expand Down Expand Up @@ -816,26 +877,3 @@ func TestPrecompileCallWithTracer(t *testing.T) {
require.NoErrorf(t, json.Unmarshal(gotJSON, &got), "json.Unmarshal(%T.GetResult(), %T)", tracer, &got)
require.Equal(t, value, got[contract].Storage[zeroHash], "value loaded with SLOAD")
}

//nolint:testableexamples // Including output would only make the example more complicated and hide the true intent
func ExamplePrecompileEnvironment() {
// To determine the actual caller of a precompile, as against the effective
// caller (under EVM rules, as exposed by `Addresses().Caller`):
actualCaller := func(env vm.PrecompileEnvironment) common.Address {
if env.IncomingCallType() == vm.DelegateCall {
// DelegateCall acts as if it were its own caller.
return env.Addresses().Self
}
// CallCode could return either `Self` or `Caller` as it acts as its
// caller but doesn't inherit the caller's caller as DelegateCall does.
// Having it handled here is arbitrary from a behavioural perspective
// and is done only to simplify the code.
//
// Call and StaticCall don't affect self/caller semantics in any way.
return env.Addresses().Caller
}

// actualCaller would typically be a top-level function. It's only a
// variable to include it in this example function.
_ = actualCaller
}
12 changes: 10 additions & 2 deletions core/vm/environment.libevm.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ type environment struct {
evm *EVM
self *Contract
callType CallType

rawSelf, rawCaller common.Address
}

func (e *environment) Gas() uint64 { return e.self.Gas }
Expand Down Expand Up @@ -79,8 +81,14 @@ func (e *environment) ReadOnly() bool {
func (e *environment) Addresses() *libevm.AddressContext {
return &libevm.AddressContext{
Origin: e.evm.Origin,
Caller: e.self.CallerAddress,
Self: e.self.Address(),
EVMSemantic: libevm.CallerAndSelf{
Caller: e.self.CallerAddress,
Self: e.self.Address(),
},
Raw: &libevm.CallerAndSelf{
Caller: e.rawCaller,
Self: e.rawSelf,
},
}
}

Expand Down
14 changes: 11 additions & 3 deletions core/vm/evm.libevm.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,15 @@ import (
// canCreateContract is a convenience wrapper for calling the
// [params.RulesHooks.CanCreateContract] hook.
func (evm *EVM) canCreateContract(caller ContractRef, contractToCreate common.Address, gas uint64) (remainingGas uint64, _ error) {
addrs := &libevm.AddressContext{Origin: evm.Origin, Caller: caller.Address(), Self: contractToCreate}
addrs := &libevm.AddressContext{
Origin: evm.Origin,
EVMSemantic: libevm.CallerAndSelf{
Caller: caller.Address(),
Self: contractToCreate,
},
// The "raw" caller isn't guaranteed to be known if the caller is a
// delegate so the `Raw` field is documented as always being nil.
}
gas, err := evm.chainRules.Hooks().CanCreateContract(addrs, gas, evm.StateDB)

// NOTE that this block only performs logging and that all paths propagate
Expand All @@ -34,8 +42,8 @@ func (evm *EVM) canCreateContract(caller ContractRef, contractToCreate common.Ad
log.Debug(
"Contract creation blocked by libevm hook",
"origin", addrs.Origin,
"caller", addrs.Caller,
"contract", addrs.Self,
"caller", addrs.EVMSemantic.Caller,
"contract", addrs.EVMSemantic.Self,
"hooks", log.TypeOf(evm.chainRules.Hooks()),
"reason", err,
)
Expand Down
28 changes: 24 additions & 4 deletions libevm/libevm.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,30 @@ type StateReader interface {
// AddressContext carries addresses available to contexts such as calls and
// contract creation.
//
// With respect to contract creation, the Self address MAY be the predicted
// address of the contract about to be deployed, which may not exist yet.
// With respect to contract creation, the EVMSemantic.Self address MAY be the
// predicted address of the contract about to be deployed, which might not exist
// yet.
type AddressContext struct {
Origin common.Address // equivalent to vm.ORIGIN op code
Caller common.Address // equivalent to vm.CALLER op code
Self common.Address // equivalent to vm.ADDRESS op code
// EVMSemantic addresses are those defined by the rules of the EVM, based on
// the type of call made to a contract; i.e. the addresses pushed to the
// stack by the vm.CALLER and vm.SELF op codes, respectively.
EVMSemantic CallerAndSelf
// Raw addresses are those that would be available to a contract under a
// standard CALL; i.e. not interpreted according EVM rules. They are the
// "intuitive" addresses such that the `Caller` is the account that called
// `Self` even if it did so via DELEGATECALL or CALLCODE (in which cases
// `Raw` and `EVMSemantic` would differ).
//
// Raw MUST NOT be nil when returned to a precompile implementation but MAY
// be nil in other situations (e.g. hooks), which MUST document behaviour on
// a case-by-case basis.
Raw *CallerAndSelf
}

// CallerAndSelf carries said addresses for use in an [AddressContext], where
// the definitions of `Caller` and `Self` are defined based on context.
type CallerAndSelf struct {
Caller common.Address
Self common.Address
}
Loading