From edc06482033658e3e3f6d3d8c9b420ff9476662d Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Wed, 6 Aug 2025 08:20:48 +0100 Subject: [PATCH 1/9] feat: `PrecompileEnvironment.ReentrancyGuard()` --- core/vm/contracts.libevm.go | 3 +++ core/vm/contracts.libevm_test.go | 42 ++++++++++++++++++++++++++++++++ core/vm/environment.libevm.go | 16 ++++++++++++ 3 files changed, 61 insertions(+) diff --git a/core/vm/contracts.libevm.go b/core/vm/contracts.libevm.go index 7d06638ae99..e85c1d4980e 100644 --- a/core/vm/contracts.libevm.go +++ b/core/vm/contracts.libevm.go @@ -190,6 +190,9 @@ type PrecompileEnvironment interface { // Invalidate invalidates the transaction calling this precompile. InvalidateExecution(error) + // ReentrancyGuard ... + ReentrancyGuard(key []byte) error + // 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. diff --git a/core/vm/contracts.libevm_test.go b/core/vm/contracts.libevm_test.go index 6f9eec3b0a7..1c7d6134f3c 100644 --- a/core/vm/contracts.libevm_test.go +++ b/core/vm/contracts.libevm_test.go @@ -839,3 +839,45 @@ func ExamplePrecompileEnvironment() { // variable to include it in this example function. _ = actualCaller } + +func TestReentrancyGuard(t *testing.T) { + sut := common.HexToAddress("7E57ED") + eve := common.HexToAddress("BAD") + eveCalled := false + + zero := func() *uint256.Int { + return uint256.NewInt(0) + } + + returnIfGuarded := []byte("guarded") + + hooks := &hookstest.Stub{ + PrecompileOverrides: map[common.Address]libevm.PrecompiledContract{ + eve: vm.NewStatefulPrecompile(func(env vm.PrecompileEnvironment, input []byte) (ret []byte, err error) { + eveCalled = true + return env.Call(sut, []byte{}, env.Gas(), zero()) // i.e. reenter + }), + sut: vm.NewStatefulPrecompile(func(env vm.PrecompileEnvironment, input []byte) (ret []byte, err error) { + // The argument is optional and used only to allow more than one + // guard in a contract. + if err := env.ReentrancyGuard(nil); err != nil { + return returnIfGuarded, err + } + if env.Addresses().Caller == eve { + // A real precompile MUST NOT panic under any circumstances. + // It is done here to avoid a loop should the guard not + // work. + panic("reentrancy") + } + return env.Call(eve, []byte{}, env.Gas(), zero()) + }), + }, + } + hooks.Register(t) + + _, evm := ethtest.NewZeroEVM(t) + got, _, err := evm.Call(vm.AccountRef{}, sut, []byte{}, 1e6, zero()) + require.True(t, eveCalled, "Malicious contract called") + assert.Equal(t, err, vm.ErrExecutionReverted, "Precompile reverted") + assert.Equal(t, returnIfGuarded, got, "Precompile reverted with expected data") +} diff --git a/core/vm/environment.libevm.go b/core/vm/environment.libevm.go index 7c3ed811f87..16561d4e39b 100644 --- a/core/vm/environment.libevm.go +++ b/core/vm/environment.libevm.go @@ -25,6 +25,7 @@ import ( "github.com/ava-labs/libevm/common" "github.com/ava-labs/libevm/common/math" "github.com/ava-labs/libevm/core/types" + "github.com/ava-labs/libevm/crypto" "github.com/ava-labs/libevm/libevm" "github.com/ava-labs/libevm/libevm/options" "github.com/ava-labs/libevm/params" @@ -103,6 +104,21 @@ func (e *environment) BlockHeader() (types.Header, error) { return *hdr, nil } +func reentrancyGuardSlot(key []byte) common.Hash { + return crypto.Keccak256Hash([]byte("libevm-reentrancy-guard"), key) +} + +func (e *environment) ReentrancyGuard(key []byte) error { + self := e.Addresses().Self + slot := reentrancyGuardSlot(key) + + if e.evm.StateDB.GetTransientState(self, slot) != (common.Hash{}) { + return ErrExecutionReverted + } + e.evm.StateDB.SetTransientState(e.Addresses().Self, slot, common.Hash{1}) + return nil +} + func (e *environment) Call(addr common.Address, input []byte, gas uint64, value *uint256.Int, opts ...CallOption) ([]byte, error) { return e.callContract(Call, addr, input, gas, value, opts...) } From 9148751f51c5afabb429cad5c015082abe92a4b7 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Wed, 6 Aug 2025 08:42:49 +0100 Subject: [PATCH 2/9] doc: `ReentrancyGuard()` and warning on `Call()` --- core/vm/contracts.libevm.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/core/vm/contracts.libevm.go b/core/vm/contracts.libevm.go index e85c1d4980e..4f90f3b07a6 100644 --- a/core/vm/contracts.libevm.go +++ b/core/vm/contracts.libevm.go @@ -190,12 +190,22 @@ type PrecompileEnvironment interface { // Invalidate invalidates the transaction calling this precompile. InvalidateExecution(error) - // ReentrancyGuard ... + // ReentrancyGuard returns [ErrExecutionReverted] i.f.f. it has already been + // called with the same `key` by the same contract, in the same transaction. + // It otherwise returns nil. The `key` MAY be nil. + // + // Contract equality is defined as the [libevm.AddressContext] "self" + // address being the same under EVM semantics. ReentrancyGuard(key []byte) error // 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. + // + // WARNING: using this method makes the precompile susceptible to reentrancy + // attacks as with a regular contract. The `ReentrancyGuard()` method, the + // Checks-Effects-Interactions pattern, or some other protection MUST be + // used in conjunction with `Call()`. Call(addr common.Address, input []byte, gas uint64, value *uint256.Int, _ ...CallOption) (ret []byte, _ error) } From b979e9605bd3acfec0f62c4ce37ef236a58f75b2 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Thu, 25 Sep 2025 12:10:23 +0100 Subject: [PATCH 3/9] refactor: move to `reentrancy` package --- core/vm/contracts.libevm.go | 12 +----- core/vm/contracts.libevm_test.go | 42 ------------------ core/vm/environment.libevm.go | 16 ------- libevm/reentrancy/guard.go | 55 ++++++++++++++++++++++++ libevm/reentrancy/guard_test.go | 74 ++++++++++++++++++++++++++++++++ 5 files changed, 131 insertions(+), 68 deletions(-) create mode 100644 libevm/reentrancy/guard.go create mode 100644 libevm/reentrancy/guard_test.go diff --git a/core/vm/contracts.libevm.go b/core/vm/contracts.libevm.go index 8be51f4f3a0..6d071843a4b 100644 --- a/core/vm/contracts.libevm.go +++ b/core/vm/contracts.libevm.go @@ -217,21 +217,13 @@ type PrecompileEnvironment interface { // Invalidate invalidates the transaction calling this precompile. InvalidateExecution(error) - // ReentrancyGuard returns [ErrExecutionReverted] i.f.f. it has already been - // called with the same `key` by the same contract, in the same transaction. - // It otherwise returns nil. The `key` MAY be nil. - // - // Contract equality is defined as the [libevm.AddressContext] "self" - // address being the same under EVM semantics. - ReentrancyGuard(key []byte) error - // 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. // // WARNING: using this method makes the precompile susceptible to reentrancy - // attacks as with a regular contract. The `ReentrancyGuard()` method, the - // Checks-Effects-Interactions pattern, or some other protection MUST be + // attacks as with a regular contract. The Checks-Effects-Interactions + // pattern, libevm's `reentrancy` package, or some other protection MUST be // used in conjunction with `Call()`. Call(addr common.Address, input []byte, gas uint64, value *uint256.Int, _ ...CallOption) (ret []byte, _ error) } diff --git a/core/vm/contracts.libevm_test.go b/core/vm/contracts.libevm_test.go index b804056bae3..525528fb414 100644 --- a/core/vm/contracts.libevm_test.go +++ b/core/vm/contracts.libevm_test.go @@ -877,45 +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") } - -func TestReentrancyGuard(t *testing.T) { - sut := common.HexToAddress("7E57ED") - eve := common.HexToAddress("BAD") - eveCalled := false - - zero := func() *uint256.Int { - return uint256.NewInt(0) - } - - returnIfGuarded := []byte("guarded") - - hooks := &hookstest.Stub{ - PrecompileOverrides: map[common.Address]libevm.PrecompiledContract{ - eve: vm.NewStatefulPrecompile(func(env vm.PrecompileEnvironment, input []byte) (ret []byte, err error) { - eveCalled = true - return env.Call(sut, []byte{}, env.Gas(), zero()) // i.e. reenter - }), - sut: vm.NewStatefulPrecompile(func(env vm.PrecompileEnvironment, input []byte) (ret []byte, err error) { - // The argument is optional and used only to allow more than one - // guard in a contract. - if err := env.ReentrancyGuard(nil); err != nil { - return returnIfGuarded, err - } - if env.Addresses().EVMSemantic.Caller == eve { - // A real precompile MUST NOT panic under any circumstances. - // It is done here to avoid a loop should the guard not - // work. - panic("reentrancy") - } - return env.Call(eve, []byte{}, env.Gas(), zero()) - }), - }, - } - hooks.Register(t) - - _, evm := ethtest.NewZeroEVM(t) - got, _, err := evm.Call(vm.AccountRef{}, sut, []byte{}, 1e6, zero()) - require.True(t, eveCalled, "Malicious contract called") - assert.Equal(t, err, vm.ErrExecutionReverted, "Precompile reverted") - assert.Equal(t, returnIfGuarded, got, "Precompile reverted with expected data") -} diff --git a/core/vm/environment.libevm.go b/core/vm/environment.libevm.go index f04abe6caa5..6551b21d0a7 100644 --- a/core/vm/environment.libevm.go +++ b/core/vm/environment.libevm.go @@ -25,7 +25,6 @@ import ( "github.com/ava-labs/libevm/common" "github.com/ava-labs/libevm/common/math" "github.com/ava-labs/libevm/core/types" - "github.com/ava-labs/libevm/crypto" "github.com/ava-labs/libevm/libevm" "github.com/ava-labs/libevm/libevm/options" "github.com/ava-labs/libevm/params" @@ -100,21 +99,6 @@ func (e *environment) BlockHeader() (types.Header, error) { return *hdr, nil } -func reentrancyGuardSlot(key []byte) common.Hash { - return crypto.Keccak256Hash([]byte("libevm-reentrancy-guard"), key) -} - -func (e *environment) ReentrancyGuard(key []byte) error { - self := e.Addresses().EVMSemantic.Self - slot := reentrancyGuardSlot(key) - - if e.evm.StateDB.GetTransientState(self, slot) != (common.Hash{}) { - return ErrExecutionReverted - } - e.evm.StateDB.SetTransientState(self, slot, common.Hash{1}) - return nil -} - func (e *environment) Call(addr common.Address, input []byte, gas uint64, value *uint256.Int, opts ...CallOption) ([]byte, error) { return e.callContract(Call, addr, input, gas, value, opts...) } diff --git a/libevm/reentrancy/guard.go b/libevm/reentrancy/guard.go new file mode 100644 index 00000000000..8a782bcf95f --- /dev/null +++ b/libevm/reentrancy/guard.go @@ -0,0 +1,55 @@ +// Copyright 2025 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 reentrancy provides a reentrancy guard for stateful precompiles that +// make outgoing calls to other contracts. +// +// Reentrancy occurs when the contract (C) called by a precompile (P) makes a +// further call back into P, which may result in theft of funds (see DAO hack). +// A reentrancy guard detects these recursive calls and reverts. +package reentrancy + +import ( + "github.com/ava-labs/libevm/common" + "github.com/ava-labs/libevm/core/vm" + "github.com/ava-labs/libevm/crypto" + "github.com/ava-labs/libevm/libevm" +) + +var slotPreimagePrefix = []byte("libevm-reentrancy-guard-") + +// Guard returns [vm.ErrExecutionReverted] i.f.f. it has already been called +// with the same `key`, by the same contract, in the same transaction. It +// otherwise returns nil. The `key` MAY be nil. +// +// Contract equality is defined as the [libevm.AddressContext] "self" address +// being the same under EVM semantics. +func Guard(env vm.PrecompileEnvironment, key []byte) error { + self := env.Addresses().EVMSemantic.Self + slot := crypto.Keccak256Hash(slotPreimagePrefix, key) + + sdb := env.StateDB() + if sdb.GetTransientState(self, slot) != (common.Hash{}) { + return vm.ErrExecutionReverted + } + sdb.SetTransientState(self, slot, common.Hash{1}) + return nil +} + +// Keep the `libevm` import to allow the linked comment on [Guard]. The package +// is imported by `vm` anyway so this is a noop but it improves developer +// experience. +var _ = (*libevm.AddressContext)(nil) diff --git a/libevm/reentrancy/guard_test.go b/libevm/reentrancy/guard_test.go new file mode 100644 index 00000000000..44371376ddf --- /dev/null +++ b/libevm/reentrancy/guard_test.go @@ -0,0 +1,74 @@ +// Copyright 2025 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 reentrancy + +import ( + "testing" + + "github.com/ava-labs/libevm/common" + "github.com/ava-labs/libevm/core/vm" + "github.com/ava-labs/libevm/libevm" + "github.com/ava-labs/libevm/libevm/ethtest" + "github.com/ava-labs/libevm/libevm/hookstest" + "github.com/holiman/uint256" + "github.com/stretchr/testify/require" + "gotest.tools/assert" +) + +func TestGuard(t *testing.T) { + sut := common.HexToAddress("7E57ED") + eve := common.HexToAddress("BAD") + eveCalled := false + + zero := func() *uint256.Int { + return uint256.NewInt(0) + } + + returnIfGuarded := []byte("guarded") + + hooks := &hookstest.Stub{ + PrecompileOverrides: map[common.Address]libevm.PrecompiledContract{ + eve: vm.NewStatefulPrecompile(func(env vm.PrecompileEnvironment, input []byte) (ret []byte, err error) { + eveCalled = true + return env.Call(sut, []byte{}, env.Gas(), zero()) // i.e. reenter + }), + sut: vm.NewStatefulPrecompile(func(env vm.PrecompileEnvironment, input []byte) (ret []byte, err error) { + // The argument is optional and used only to allow more than one + // guard in a contract. + if err := Guard(env, nil); err != nil { + return returnIfGuarded, err + } + if env.Addresses().EVMSemantic.Caller == eve { + // A real precompile MUST NOT panic under any circumstances. + // It is done here to avoid a loop should the guard not + // work. + panic("reentrancy") + } + return env.Call(eve, []byte{}, env.Gas(), zero()) + }), + }, + } + hooks.Register(t) + + _, evm := ethtest.NewZeroEVM(t) + got, _, err := evm.Call(vm.AccountRef{}, sut, []byte{}, 1e6, zero()) + require.True(t, eveCalled, "Malicious contract called") + // The error is propagated Guard() -> reentered SUT -> Eve -> top-level SUT -> evm.Call() + // This MUST NOT be [assert.ErrorIs] as such errors are never wrapped in geth. + assert.Equal(t, err, vm.ErrExecutionReverted, "Precompile reverted") + assert.Equal(t, returnIfGuarded, got, "Precompile reverted with expected data") +} From e6aea8002a3869d4567d099b9f6c8ee1a2adfe3d Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Thu, 25 Sep 2025 12:12:16 +0100 Subject: [PATCH 4/9] chore: placate the linter --- go.mod | 1 + go.sum | 2 ++ 2 files changed, 3 insertions(+) diff --git a/go.mod b/go.mod index 34698e0f3cf..3a5dee3f4fe 100644 --- a/go.mod +++ b/go.mod @@ -74,6 +74,7 @@ require ( golang.org/x/tools v0.15.0 gopkg.in/natefinch/lumberjack.v2 v2.0.0 gopkg.in/yaml.v3 v3.0.1 + gotest.tools v2.2.0+incompatible ) require ( diff --git a/go.sum b/go.sum index 263aad1480a..36f70f376ac 100644 --- a/go.sum +++ b/go.sum @@ -976,6 +976,8 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= From e8dbf7a2389c6d8a4fddb79b46548751ac82df9d Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Thu, 25 Sep 2025 12:12:46 +0100 Subject: [PATCH 5/9] chore: actually, that was `go mod tidy` From ff0ade2bb02fc32a565977ee3cb09e4b6991166c Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Thu, 25 Sep 2025 12:25:37 +0100 Subject: [PATCH 6/9] fix: import correct `assert` package --- libevm/reentrancy/guard_test.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/libevm/reentrancy/guard_test.go b/libevm/reentrancy/guard_test.go index 44371376ddf..deebeb43fe9 100644 --- a/libevm/reentrancy/guard_test.go +++ b/libevm/reentrancy/guard_test.go @@ -19,14 +19,15 @@ package reentrancy import ( "testing" + "github.com/holiman/uint256" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/ava-labs/libevm/common" "github.com/ava-labs/libevm/core/vm" "github.com/ava-labs/libevm/libevm" "github.com/ava-labs/libevm/libevm/ethtest" "github.com/ava-labs/libevm/libevm/hookstest" - "github.com/holiman/uint256" - "github.com/stretchr/testify/require" - "gotest.tools/assert" ) func TestGuard(t *testing.T) { From 28b1c283d3c8fef715f5185c15de62dfac57e7e3 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Thu, 25 Sep 2025 12:27:46 +0100 Subject: [PATCH 7/9] chore: `go mod tidy` --- go.mod | 1 - go.sum | 2 -- 2 files changed, 3 deletions(-) diff --git a/go.mod b/go.mod index 3a5dee3f4fe..34698e0f3cf 100644 --- a/go.mod +++ b/go.mod @@ -74,7 +74,6 @@ require ( golang.org/x/tools v0.15.0 gopkg.in/natefinch/lumberjack.v2 v2.0.0 gopkg.in/yaml.v3 v3.0.1 - gotest.tools v2.2.0+incompatible ) require ( diff --git a/go.sum b/go.sum index 36f70f376ac..263aad1480a 100644 --- a/go.sum +++ b/go.sum @@ -976,8 +976,6 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= -gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= From 358a2d2e8f43a862d7a3ae11c921305aa610c3d3 Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Fri, 3 Oct 2025 20:43:58 +0100 Subject: [PATCH 8/9] test: different combinations of self address and key --- libevm/reentrancy/guard_test.go | 64 +++++++++++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 2 deletions(-) diff --git a/libevm/reentrancy/guard_test.go b/libevm/reentrancy/guard_test.go index deebeb43fe9..1d5449e213e 100644 --- a/libevm/reentrancy/guard_test.go +++ b/libevm/reentrancy/guard_test.go @@ -24,13 +24,17 @@ import ( "github.com/stretchr/testify/require" "github.com/ava-labs/libevm/common" + "github.com/ava-labs/libevm/core/rawdb" + "github.com/ava-labs/libevm/core/state" + "github.com/ava-labs/libevm/core/types" "github.com/ava-labs/libevm/core/vm" + "github.com/ava-labs/libevm/crypto" "github.com/ava-labs/libevm/libevm" "github.com/ava-labs/libevm/libevm/ethtest" "github.com/ava-labs/libevm/libevm/hookstest" ) -func TestGuard(t *testing.T) { +func TestGuardIntegration(t *testing.T) { sut := common.HexToAddress("7E57ED") eve := common.HexToAddress("BAD") eveCalled := false @@ -49,7 +53,7 @@ func TestGuard(t *testing.T) { }), sut: vm.NewStatefulPrecompile(func(env vm.PrecompileEnvironment, input []byte) (ret []byte, err error) { // The argument is optional and used only to allow more than one - // guard in a contract. + // guard in a contract, tested in a separate unit test. if err := Guard(env, nil); err != nil { return returnIfGuarded, err } @@ -73,3 +77,59 @@ func TestGuard(t *testing.T) { assert.Equal(t, err, vm.ErrExecutionReverted, "Precompile reverted") assert.Equal(t, returnIfGuarded, got, "Precompile reverted with expected data") } + +type envStub struct { + self common.Address + db *state.StateDB + vm.PrecompileEnvironment +} + +func (s *envStub) Addresses() *libevm.AddressContext { + return &libevm.AddressContext{ + EVMSemantic: libevm.CallerAndSelf{ + Self: s.self, + }, + } +} + +func (s *envStub) StateDB() vm.StateDB { + return s.db +} + +func TestGuard(t *testing.T) { + db, err := state.New(types.EmptyRootHash, state.NewDatabase(rawdb.NewMemoryDatabase()), nil) + require.NoError(t, err, "state.New()") + env := &envStub{db: db} + + addr0 := common.Address{} + addr1 := common.Address{1} + key0 := []byte{0} + key1 := []byte{1} + + // All tests run on the same [envStub] so are dependent on the effects of + // the one(s) before. + tests := []struct { + self common.Address + key []byte + want error + }{ + {addr0, key0, nil}, + {addr0, key0, vm.ErrExecutionReverted}, + {addr0, key1, nil}, + {addr1, key0, nil}, + {addr1, key1, nil}, + {addr1, key1, vm.ErrExecutionReverted}, + {addr0, key1, vm.ErrExecutionReverted}, + } + + history := make(map[common.Hash]bool) // for better error reporting + for _, tt := range tests { + h := crypto.Keccak256Hash(tt.self[:], tt.key) + already := history[h] + history[h] = true + + env.self = tt.self + // Tests are dependent so we don't use assert.Equalf. + require.Equalf(t, Guard(env, tt.key), tt.want, "Guard([self=%v], %#x) when already called = %t", tt.self, tt.key, already) + } +} From 2f74e672bea2af4119c80d29bdd3e179d3b8312d Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Fri, 3 Oct 2025 20:49:23 +0100 Subject: [PATCH 9/9] chore: nice work, linter --- libevm/reentrancy/guard_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libevm/reentrancy/guard_test.go b/libevm/reentrancy/guard_test.go index 1d5449e213e..83e0de4a44b 100644 --- a/libevm/reentrancy/guard_test.go +++ b/libevm/reentrancy/guard_test.go @@ -130,6 +130,6 @@ func TestGuard(t *testing.T) { env.self = tt.self // Tests are dependent so we don't use assert.Equalf. - require.Equalf(t, Guard(env, tt.key), tt.want, "Guard([self=%v], %#x) when already called = %t", tt.self, tt.key, already) + require.Equalf(t, tt.want, Guard(env, tt.key), "Guard([self=%v], %#x) when already called = %t", tt.self, tt.key, already) } }