Skip to content

Commit b979e96

Browse files
committed
refactor: move to reentrancy package
1 parent 7b43105 commit b979e96

File tree

5 files changed

+131
-68
lines changed

5 files changed

+131
-68
lines changed

core/vm/contracts.libevm.go

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -217,21 +217,13 @@ type PrecompileEnvironment interface {
217217
// Invalidate invalidates the transaction calling this precompile.
218218
InvalidateExecution(error)
219219

220-
// ReentrancyGuard returns [ErrExecutionReverted] i.f.f. it has already been
221-
// called with the same `key` by the same contract, in the same transaction.
222-
// It otherwise returns nil. The `key` MAY be nil.
223-
//
224-
// Contract equality is defined as the [libevm.AddressContext] "self"
225-
// address being the same under EVM semantics.
226-
ReentrancyGuard(key []byte) error
227-
228220
// Call is equivalent to [EVM.Call] except that the `caller` argument is
229221
// removed and automatically determined according to the type of call that
230222
// invoked the precompile.
231223
//
232224
// WARNING: using this method makes the precompile susceptible to reentrancy
233-
// attacks as with a regular contract. The `ReentrancyGuard()` method, the
234-
// Checks-Effects-Interactions pattern, or some other protection MUST be
225+
// attacks as with a regular contract. The Checks-Effects-Interactions
226+
// pattern, libevm's `reentrancy` package, or some other protection MUST be
235227
// used in conjunction with `Call()`.
236228
Call(addr common.Address, input []byte, gas uint64, value *uint256.Int, _ ...CallOption) (ret []byte, _ error)
237229
}

core/vm/contracts.libevm_test.go

Lines changed: 0 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -877,45 +877,3 @@ func TestPrecompileCallWithTracer(t *testing.T) {
877877
require.NoErrorf(t, json.Unmarshal(gotJSON, &got), "json.Unmarshal(%T.GetResult(), %T)", tracer, &got)
878878
require.Equal(t, value, got[contract].Storage[zeroHash], "value loaded with SLOAD")
879879
}
880-
881-
func TestReentrancyGuard(t *testing.T) {
882-
sut := common.HexToAddress("7E57ED")
883-
eve := common.HexToAddress("BAD")
884-
eveCalled := false
885-
886-
zero := func() *uint256.Int {
887-
return uint256.NewInt(0)
888-
}
889-
890-
returnIfGuarded := []byte("guarded")
891-
892-
hooks := &hookstest.Stub{
893-
PrecompileOverrides: map[common.Address]libevm.PrecompiledContract{
894-
eve: vm.NewStatefulPrecompile(func(env vm.PrecompileEnvironment, input []byte) (ret []byte, err error) {
895-
eveCalled = true
896-
return env.Call(sut, []byte{}, env.Gas(), zero()) // i.e. reenter
897-
}),
898-
sut: vm.NewStatefulPrecompile(func(env vm.PrecompileEnvironment, input []byte) (ret []byte, err error) {
899-
// The argument is optional and used only to allow more than one
900-
// guard in a contract.
901-
if err := env.ReentrancyGuard(nil); err != nil {
902-
return returnIfGuarded, err
903-
}
904-
if env.Addresses().EVMSemantic.Caller == eve {
905-
// A real precompile MUST NOT panic under any circumstances.
906-
// It is done here to avoid a loop should the guard not
907-
// work.
908-
panic("reentrancy")
909-
}
910-
return env.Call(eve, []byte{}, env.Gas(), zero())
911-
}),
912-
},
913-
}
914-
hooks.Register(t)
915-
916-
_, evm := ethtest.NewZeroEVM(t)
917-
got, _, err := evm.Call(vm.AccountRef{}, sut, []byte{}, 1e6, zero())
918-
require.True(t, eveCalled, "Malicious contract called")
919-
assert.Equal(t, err, vm.ErrExecutionReverted, "Precompile reverted")
920-
assert.Equal(t, returnIfGuarded, got, "Precompile reverted with expected data")
921-
}

core/vm/environment.libevm.go

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import (
2525
"github.com/ava-labs/libevm/common"
2626
"github.com/ava-labs/libevm/common/math"
2727
"github.com/ava-labs/libevm/core/types"
28-
"github.com/ava-labs/libevm/crypto"
2928
"github.com/ava-labs/libevm/libevm"
3029
"github.com/ava-labs/libevm/libevm/options"
3130
"github.com/ava-labs/libevm/params"
@@ -100,21 +99,6 @@ func (e *environment) BlockHeader() (types.Header, error) {
10099
return *hdr, nil
101100
}
102101

103-
func reentrancyGuardSlot(key []byte) common.Hash {
104-
return crypto.Keccak256Hash([]byte("libevm-reentrancy-guard"), key)
105-
}
106-
107-
func (e *environment) ReentrancyGuard(key []byte) error {
108-
self := e.Addresses().EVMSemantic.Self
109-
slot := reentrancyGuardSlot(key)
110-
111-
if e.evm.StateDB.GetTransientState(self, slot) != (common.Hash{}) {
112-
return ErrExecutionReverted
113-
}
114-
e.evm.StateDB.SetTransientState(self, slot, common.Hash{1})
115-
return nil
116-
}
117-
118102
func (e *environment) Call(addr common.Address, input []byte, gas uint64, value *uint256.Int, opts ...CallOption) ([]byte, error) {
119103
return e.callContract(Call, addr, input, gas, value, opts...)
120104
}

libevm/reentrancy/guard.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Copyright 2025 the libevm authors.
2+
//
3+
// The libevm additions to go-ethereum are free software: you can redistribute
4+
// them and/or modify them under the terms of the GNU Lesser General Public License
5+
// as published by the Free Software Foundation, either version 3 of the License,
6+
// or (at your option) any later version.
7+
//
8+
// The libevm additions are distributed in the hope that they will be useful,
9+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
11+
// General Public License for more details.
12+
//
13+
// You should have received a copy of the GNU Lesser General Public License
14+
// along with the go-ethereum library. If not, see
15+
// <http://www.gnu.org/licenses/>.
16+
17+
// Package reentrancy provides a reentrancy guard for stateful precompiles that
18+
// make outgoing calls to other contracts.
19+
//
20+
// Reentrancy occurs when the contract (C) called by a precompile (P) makes a
21+
// further call back into P, which may result in theft of funds (see DAO hack).
22+
// A reentrancy guard detects these recursive calls and reverts.
23+
package reentrancy
24+
25+
import (
26+
"github.com/ava-labs/libevm/common"
27+
"github.com/ava-labs/libevm/core/vm"
28+
"github.com/ava-labs/libevm/crypto"
29+
"github.com/ava-labs/libevm/libevm"
30+
)
31+
32+
var slotPreimagePrefix = []byte("libevm-reentrancy-guard-")
33+
34+
// Guard returns [vm.ErrExecutionReverted] i.f.f. it has already been called
35+
// with the same `key`, by the same contract, in the same transaction. It
36+
// otherwise returns nil. The `key` MAY be nil.
37+
//
38+
// Contract equality is defined as the [libevm.AddressContext] "self" address
39+
// being the same under EVM semantics.
40+
func Guard(env vm.PrecompileEnvironment, key []byte) error {
41+
self := env.Addresses().EVMSemantic.Self
42+
slot := crypto.Keccak256Hash(slotPreimagePrefix, key)
43+
44+
sdb := env.StateDB()
45+
if sdb.GetTransientState(self, slot) != (common.Hash{}) {
46+
return vm.ErrExecutionReverted
47+
}
48+
sdb.SetTransientState(self, slot, common.Hash{1})
49+
return nil
50+
}
51+
52+
// Keep the `libevm` import to allow the linked comment on [Guard]. The package
53+
// is imported by `vm` anyway so this is a noop but it improves developer
54+
// experience.
55+
var _ = (*libevm.AddressContext)(nil)

libevm/reentrancy/guard_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Copyright 2025 the libevm authors.
2+
//
3+
// The libevm additions to go-ethereum are free software: you can redistribute
4+
// them and/or modify them under the terms of the GNU Lesser General Public License
5+
// as published by the Free Software Foundation, either version 3 of the License,
6+
// or (at your option) any later version.
7+
//
8+
// The libevm additions are distributed in the hope that they will be useful,
9+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
11+
// General Public License for more details.
12+
//
13+
// You should have received a copy of the GNU Lesser General Public License
14+
// along with the go-ethereum library. If not, see
15+
// <http://www.gnu.org/licenses/>.
16+
17+
package reentrancy
18+
19+
import (
20+
"testing"
21+
22+
"github.com/ava-labs/libevm/common"
23+
"github.com/ava-labs/libevm/core/vm"
24+
"github.com/ava-labs/libevm/libevm"
25+
"github.com/ava-labs/libevm/libevm/ethtest"
26+
"github.com/ava-labs/libevm/libevm/hookstest"
27+
"github.com/holiman/uint256"
28+
"github.com/stretchr/testify/require"
29+
"gotest.tools/assert"
30+
)
31+
32+
func TestGuard(t *testing.T) {
33+
sut := common.HexToAddress("7E57ED")
34+
eve := common.HexToAddress("BAD")
35+
eveCalled := false
36+
37+
zero := func() *uint256.Int {
38+
return uint256.NewInt(0)
39+
}
40+
41+
returnIfGuarded := []byte("guarded")
42+
43+
hooks := &hookstest.Stub{
44+
PrecompileOverrides: map[common.Address]libevm.PrecompiledContract{
45+
eve: vm.NewStatefulPrecompile(func(env vm.PrecompileEnvironment, input []byte) (ret []byte, err error) {
46+
eveCalled = true
47+
return env.Call(sut, []byte{}, env.Gas(), zero()) // i.e. reenter
48+
}),
49+
sut: vm.NewStatefulPrecompile(func(env vm.PrecompileEnvironment, input []byte) (ret []byte, err error) {
50+
// The argument is optional and used only to allow more than one
51+
// guard in a contract.
52+
if err := Guard(env, nil); err != nil {
53+
return returnIfGuarded, err
54+
}
55+
if env.Addresses().EVMSemantic.Caller == eve {
56+
// A real precompile MUST NOT panic under any circumstances.
57+
// It is done here to avoid a loop should the guard not
58+
// work.
59+
panic("reentrancy")
60+
}
61+
return env.Call(eve, []byte{}, env.Gas(), zero())
62+
}),
63+
},
64+
}
65+
hooks.Register(t)
66+
67+
_, evm := ethtest.NewZeroEVM(t)
68+
got, _, err := evm.Call(vm.AccountRef{}, sut, []byte{}, 1e6, zero())
69+
require.True(t, eveCalled, "Malicious contract called")
70+
// The error is propagated Guard() -> reentered SUT -> Eve -> top-level SUT -> evm.Call()
71+
// This MUST NOT be [assert.ErrorIs] as such errors are never wrapped in geth.
72+
assert.Equal(t, err, vm.ErrExecutionReverted, "Precompile reverted")
73+
assert.Equal(t, returnIfGuarded, got, "Precompile reverted with expected data")
74+
}

0 commit comments

Comments
 (0)