Skip to content
Open
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
5 changes: 5 additions & 0 deletions core/vm/contracts.libevm.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,11 @@ type PrecompileEnvironment interface {
// 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 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)
}

Expand Down
55 changes: 55 additions & 0 deletions libevm/reentrancy/guard.go
Original file line number Diff line number Diff line change
@@ -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
// <http://www.gnu.org/licenses/>.

// 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)
75 changes: 75 additions & 0 deletions libevm/reentrancy/guard_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// 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
// <http://www.gnu.org/licenses/>.

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"
)

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")
}
Loading