Skip to content
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
1 change: 1 addition & 0 deletions core/vm/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ var (
ErrCodeStoreOutOfGas = errors.New("contract creation code storage out of gas")
ErrDepth = errors.New("max call depth exceeded")
ErrInsufficientBalance = errors.New("insufficient balance for transfer")
ErrTransferNotAllowed = errors.New("transfer not allowed to recipient")
Comment thread
tomaszslabon marked this conversation as resolved.
ErrContractAddressCollision = errors.New("contract address collision")
ErrExecutionReverted = errors.New("execution reverted")
ErrMaxCodeSizeExceeded = errors.New("max code size exceeded")
Expand Down
25 changes: 25 additions & 0 deletions core/vm/evm.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ import (
type (
// CanTransferFunc is the signature of a transfer guard function
CanTransferFunc func(StateDB, common.Address, *uint256.Int) bool
// CanReceiveTransferFunc is the signature of a recipient-side transfer
// guard function. The address argument is the recipient (credited
// account), not the sender.
CanReceiveTransferFunc func(StateDB, common.Address, *uint256.Int) bool
// TransferFunc is the signature of a transfer function
TransferFunc func(StateDB, common.Address, common.Address, *uint256.Int)
// GetHashFunc returns the n'th block hash in the blockchain
Expand Down Expand Up @@ -61,6 +65,14 @@ type BlockContext struct {
BaseFee *big.Int // Provides information for BASEFEE (0 if vm runs with NoBaseFee flag and 0 gas price)
BlobBaseFee *big.Int // Provides information for BLOBBASEFEE (0 if vm runs with NoBaseFee flag and 0 blob gas price)
Random *common.Hash // Provides information for PREVRANDAO

// CanReceiveTransfer is a Mezo-specific fork addition (does not exist
// in upstream go-ethereum). Optional recipient-side guard invoked by
// Call to fail-fast on transfers the host chain refuses to credit.
// Nil-safe: a nil value disables the check, preserving upstream
// behavior. NewEVMBlockContext leaves it nil; only chain integrators
// (e.g. mezod) populate it.
CanReceiveTransfer CanReceiveTransferFunc
}

// TxContext provides the EVM with information about a transaction.
Expand Down Expand Up @@ -249,6 +261,19 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g
if !value.IsZero() && !evm.Context.CanTransfer(evm.StateDB, caller, value) {
return nil, gas, ErrInsufficientBalance
}
// Recipient-side guard (Mezo-specific fork addition; nil-safe). Gated
// in Call only: DelegateCall and StaticCall perform no value transfer,
// CallCode credits the caller rather than the target, and CREATE /
// CREATE2 credit a freshly-derived contract address that by construction
// cannot collide with the well-known host-chain accounts (module
// accounts, etc.) that this guard is intended to protect — so none of
// those frames need the check. Placed before the StateDB snapshot so
// a refusal aborts the frame cleanly with no state mutation, instead
// of being caught at commit time.
if !value.IsZero() && evm.Context.CanReceiveTransfer != nil &&
Comment thread
tomaszslabon marked this conversation as resolved.
!evm.Context.CanReceiveTransfer(evm.StateDB, addr, value) {
return nil, gas, ErrTransferNotAllowed
}
snapshot := evm.StateDB.Snapshot()
p, isPrecompile := evm.precompile(addr)

Expand Down
97 changes: 97 additions & 0 deletions core/vm/evm_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Copyright 2026 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it 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 go-ethereum library is distributed in the hope that it 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 vm

import (
"errors"
"testing"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/params"
"github.com/holiman/uint256"
"github.com/stretchr/testify/require"
)

// TestEVMCallCanReceiveTransferGuard verifies that a non-nil
// CanReceiveTransfer returning false aborts a value-bearing CALL with
// ErrTransferNotAllowed before any state mutation: gas is left intact,
// the recipient is not created, and Transfer is not invoked.
func TestEVMCallCanReceiveTransferGuard(t *testing.T) {
var (
caller = common.HexToAddress("0x01")
recipient = common.HexToAddress("0x02")
gas = uint64(100_000)
value = uint256.NewInt(1)
)

statedb, err := state.New(types.EmptyRootHash, state.NewDatabaseForTesting())
require.NoError(t, err)

var transferCalled bool
evm := NewEVM(BlockContext{
CanTransfer: func(StateDB, common.Address, *uint256.Int) bool {
return true
},
CanReceiveTransfer: func(StateDB, common.Address, *uint256.Int) bool {
return false
},
Transfer: func(StateDB, common.Address, common.Address, *uint256.Int) {
transferCalled = true
},
}, statedb, params.AllEthashProtocolChanges, Config{})

_, leftOverGas, err := evm.Call(caller, recipient, nil, gas, value)

require.True(t, errors.Is(err, ErrTransferNotAllowed))
require.Equal(t, gas, leftOverGas)
require.False(t, statedb.Exist(recipient))
require.False(t, transferCalled)
}

// TestEVMCallCanReceiveTransferGuardIgnoresZeroValue verifies that the
// recipient-side guard is short-circuited by the value.IsZero() check
// and is not invoked for zero-value CALLs.
func TestEVMCallCanReceiveTransferGuardIgnoresZeroValue(t *testing.T) {
var (
caller = common.HexToAddress("0x01")
recipient = common.HexToAddress("0x02")
gas = uint64(100_000)
value = uint256.NewInt(0)
)

statedb, err := state.New(types.EmptyRootHash, state.NewDatabaseForTesting())
require.NoError(t, err)

var guardCalled bool
evm := NewEVM(BlockContext{
CanTransfer: func(StateDB, common.Address, *uint256.Int) bool {
return true
},
CanReceiveTransfer: func(StateDB, common.Address, *uint256.Int) bool {
guardCalled = true
return false
},
Transfer: func(StateDB, common.Address, common.Address, *uint256.Int) {},
}, statedb, params.AllEthashProtocolChanges, Config{})

_, _, err = evm.Call(caller, recipient, nil, gas, value)

require.NoError(t, err)
require.False(t, guardCalled)
}
Loading