From edc06482033658e3e3f6d3d8c9b420ff9476662d Mon Sep 17 00:00:00 2001 From: Arran Schlosberg Date: Wed, 6 Aug 2025 08:20:48 +0100 Subject: [PATCH 1/2] 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/2] 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) }