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
27 changes: 24 additions & 3 deletions core/state_transition.go
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ func (st *stateTransition) buyGas() error {
if overflow {
return fmt.Errorf("%w: address %v required balance exceeds 256 bits", ErrInsufficientFunds, st.msg.From.Hex())
}
if have, want := st.state.GetBalance(st.msg.From), balanceCheckU256; have.Cmp(want) < 0 {
if have, want := st.state.GetBalance(st.msg.From), balanceCheckU256; !st.evm.Config.DisableBalanceCheck && have.Cmp(want) < 0 {
return fmt.Errorf("%w: address %v have %v want %v", ErrInsufficientFunds, st.msg.From.Hex(), have, want)
}
if err := st.gp.SubGas(st.msg.GasLimit); err != nil {
Expand All @@ -303,7 +303,22 @@ func (st *stateTransition) buyGas() error {

st.initialGas = st.msg.GasLimit
mgvalU256, _ := uint256.FromBig(mgval)
if st.evm.Config.DisableBalanceCheck {
// Simulations charge the effective gas cost with saturating subtraction.
// Normal unused-gas refunds still apply, even when this debit is capped.
if balance := st.state.GetBalance(st.msg.From); balance.Cmp(mgvalU256) < 0 {
mgvalU256.Set(balance)
}
}
st.state.SubBalance(st.msg.From, mgvalU256, tracing.BalanceDecreaseGasBuy)
if st.evm.Config.DisableBalanceCheck {
// Fund only the top-level value transfer. Nested calls retain their
// ordinary balance checks, and a revert keeps this transient funding.
value := uint256.MustFromBig(st.msg.Value)
if balance := st.state.GetBalance(st.msg.From); balance.Cmp(value) < 0 {
st.state.AddBalance(st.msg.From, value.Sub(value, balance), tracing.BalanceChangeUnspecified)
}
}
return nil
}

Expand Down Expand Up @@ -351,13 +366,13 @@ func (st *stateTransition) preCheck() error {
return fmt.Errorf("%w: address %v, maxPriorityFeePerGas bit length: %d", ErrTipVeryHigh,
msg.From.Hex(), l)
}
if msg.GasFeeCap.Cmp(msg.GasTipCap) < 0 {
if !st.evm.Config.DisablePriorityFeeCheck && msg.GasFeeCap.Cmp(msg.GasTipCap) < 0 {
return fmt.Errorf("%w: address %v, maxPriorityFeePerGas: %s, maxFeePerGas: %s", ErrTipAboveFeeCap,
msg.From.Hex(), msg.GasTipCap, msg.GasFeeCap)
}
// This will panic if baseFee is nil, but basefee presence is verified
// as part of header validation.
if msg.GasFeeCap.Cmp(st.evm.Context.BaseFee) < 0 {
if !st.evm.Config.DisableBaseFeeCheck && msg.GasFeeCap.Cmp(st.evm.Context.BaseFee) < 0 {
return fmt.Errorf("%w: address %v, maxFeePerGas: %s, baseFee: %s", ErrFeeCapTooLow,
msg.From.Hex(), msg.GasFeeCap, st.evm.Context.BaseFee)
}
Expand Down Expand Up @@ -548,6 +563,12 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
effectiveTip := msg.GasPrice
if rules.IsLondon {
effectiveTip = new(big.Int).Sub(msg.GasPrice, st.evm.Context.BaseFee)
if st.evm.Config.DisableBaseFeeCheck && effectiveTip.Sign() < 0 {
// A relaxed base-fee check may admit a price below the base fee.
// Such a transaction pays no tip; converting a negative tip to
// uint256 would otherwise wrap the beneficiary's balance.
effectiveTip.SetUint64(0)
}
}
effectiveTipU256, _ := uint256.FromBig(effectiveTip)

Expand Down
274 changes: 274 additions & 0 deletions core/state_transition_policy_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
// 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 core

import (
"errors"
"math/big"
"testing"

"github.com/holiman/uint256"
"github.com/tenderly/polygon-bor/common"
"github.com/tenderly/polygon-bor/core/state"
"github.com/tenderly/polygon-bor/core/tracing"
"github.com/tenderly/polygon-bor/core/types"
"github.com/tenderly/polygon-bor/core/vm"
"github.com/tenderly/polygon-bor/params"
)

type executionCheckFixture struct {
state *state.StateDB
evm *vm.EVM
msg *Message
pool *GasPool
}

func newExecutionCheckFixture(t *testing.T, config vm.Config) *executionCheckFixture {
t.Helper()
statedb, err := state.New(types.EmptyRootHash, state.NewDatabaseForTesting())
if err != nil {
t.Fatal(err)
}
from, to, coinbase := common.HexToAddress("0x100"), common.HexToAddress("0x200"), common.HexToAddress("0x300")
statedb.AddBalance(from, uint256.NewInt(1_000_000), tracing.BalanceChangeUnspecified)
context := vm.BlockContext{
CanTransfer: CanTransfer,
Transfer: Transfer,
GetHash: func(uint64) common.Hash { return common.Hash{} },
Coinbase: coinbase,
BlockNumber: big.NewInt(1),
Time: 1,
Difficulty: new(big.Int),
GasLimit: 1_000_000,
BaseFee: big.NewInt(1),
}
return &executionCheckFixture{
state: statedb,
evm: vm.NewEVM(context, statedb, params.AllEthashProtocolChanges, config),
msg: &Message{
From: from, To: &to, Value: new(big.Int), GasLimit: 100_000,
GasPrice: big.NewInt(2), GasFeeCap: big.NewInt(2), GasTipCap: big.NewInt(1),
},
pool: new(GasPool).AddGas(context.GasLimit),
}
}

func (f *executionCheckFixture) fees(baseFee, feeCap, tipCap int64) {
f.evm.Context.BaseFee.SetInt64(baseFee)
f.msg.GasFeeCap.SetInt64(feeCap)
f.msg.GasTipCap.SetInt64(tipCap)
f.msg.GasPrice.SetInt64(min(feeCap, baseFee+tipCap))
}

func TestExecutionCheckFeePolicy(t *testing.T) {
for _, test := range []struct {
name string
config vm.Config
base, fee, tip int64
wantError error
wantBeneficiaryTip uint64
}{
{name: "default valid fees", base: 1, fee: 2, tip: 1, wantBeneficiaryTip: 1},
{name: "default rejects below base fee", base: 10, fee: 2, tip: 1, wantError: ErrFeeCapTooLow},
{name: "legacy NoBaseFee still rejects nonzero low fee", config: vm.Config{NoBaseFee: true}, base: 10, fee: 2, tip: 1, wantError: ErrFeeCapTooLow},
{name: "legacy NoBaseFee accepts zero fees", config: vm.Config{NoBaseFee: true}, base: 10},
{name: "base fee bypass accepts nonzero low fee", config: vm.Config{DisableBaseFeeCheck: true}, base: 10, fee: 2, tip: 1},
{name: "base fee bypass accepts zero fees", config: vm.Config{DisableBaseFeeCheck: true}, base: 10},
{name: "base fee bypass retains priority check", config: vm.Config{DisableBaseFeeCheck: true}, base: 1, fee: 2, tip: 3, wantError: ErrTipAboveFeeCap},
{name: "priority bypass accepts excess tip", config: vm.Config{DisablePriorityFeeCheck: true}, base: 1, fee: 2, tip: 3, wantBeneficiaryTip: 1},
{name: "priority bypass retains base fee check", config: vm.Config{DisablePriorityFeeCheck: true}, base: 10, fee: 2, tip: 3, wantError: ErrFeeCapTooLow},
{name: "both fee bypasses", config: vm.Config{DisableBaseFeeCheck: true, DisablePriorityFeeCheck: true}, base: 10, fee: 2, tip: 3},
} {
t.Run(test.name, func(t *testing.T) {
f := newExecutionCheckFixture(t, test.config)
f.fees(test.base, test.fee, test.tip)
result, err := ApplyMessage(f.evm, f.msg, f.pool)
if !errors.Is(err, test.wantError) {
t.Fatalf("execution error = %v, want %v", err, test.wantError)
}
if err != nil {
if result != nil || f.state.GetNonce(f.msg.From) != 0 {
t.Fatal("rejected transaction executed")
}
return
}
if result.Err != nil {
t.Fatal(result.Err)
}
wantBalance := uint256.NewInt(1_000_000 - result.UsedGas*uint64(f.msg.GasPrice.Int64()))
if have := f.state.GetBalance(f.msg.From); !have.Eq(wantBalance) {
t.Fatalf("sender balance = %v, want %v", have, wantBalance)
}
wantTip := uint256.NewInt(result.UsedGas * test.wantBeneficiaryTip)
if have := f.state.GetBalance(f.evm.Context.Coinbase); !have.Eq(wantTip) {
t.Fatalf("beneficiary balance = %v, want %v", have, wantTip)
}
})
}
}

func TestExecutionCheckFeeOpcodes(t *testing.T) {
for _, test := range []struct {
name string
base, fee, tip int64
}{
{name: "nonzero below base", base: 10, fee: 2, tip: 1},
{name: "zero fee", base: 10},
{name: "priority above cap", base: 1, fee: 2, tip: 3},
} {
t.Run(test.name, func(t *testing.T) {
f := newExecutionCheckFixture(t, vm.Config{DisableBaseFeeCheck: true, DisablePriorityFeeCheck: true})
f.fees(test.base, test.fee, test.tip)
f.state.SetCode(*f.msg.To, []byte{
byte(vm.BASEFEE), byte(vm.PUSH1), 0, byte(vm.MSTORE),
byte(vm.GASPRICE), byte(vm.PUSH1), 32, byte(vm.MSTORE),
byte(vm.PUSH1), 64, byte(vm.PUSH1), 0, byte(vm.RETURN),
}, tracing.CodeChangeUnspecified)
result, err := ApplyMessage(f.evm, f.msg, f.pool)
if err != nil {
t.Fatal(err)
}
if result.Err != nil || len(result.ReturnData) != 64 {
t.Fatalf("execution result = %+v", result)
}
if have := new(big.Int).SetBytes(result.ReturnData[:32]); have.Int64() != test.base {
t.Fatalf("BASEFEE = %v, want %d", have, test.base)
}
if have := new(big.Int).SetBytes(result.ReturnData[32:]); have.Cmp(f.msg.GasPrice) != 0 {
t.Fatalf("GASPRICE = %v, want %v", have, f.msg.GasPrice)
}
})
}
}

func TestExecutionCheckBalancePolicy(t *testing.T) {
for _, test := range []struct {
name string
balance uint64
disabled bool
feeCap int64
wantError error
wantSender uint64
}{
{name: "default rejects unfunded transfer", wantError: ErrInsufficientFunds},
{name: "unfunded transfer", disabled: true, wantSender: 79_000},
{name: "gas deficit", balance: 50_000, disabled: true, wantSender: 79_000},
{name: "value deficit after gas debit", balance: 100_005, disabled: true, wantSender: 79_000},
{name: "funded transfer", balance: 200_000, disabled: true, wantSender: 178_993},
{name: "default checks maximum fee", balance: 200_000, feeCap: 100, wantError: ErrInsufficientFunds},
{name: "bypass charges effective fee", balance: 200_000, feeCap: 100, disabled: true, wantSender: 178_993},
} {
t.Run(test.name, func(t *testing.T) {
f := newExecutionCheckFixture(t, vm.Config{DisableBalanceCheck: test.disabled})
f.fees(0, 1, 1)
if test.feeCap != 0 {
f.msg.GasFeeCap.SetInt64(test.feeCap)
}
f.msg.Value.SetInt64(7)
f.state.SetBalance(f.msg.From, uint256.NewInt(test.balance), tracing.BalanceChangeUnspecified)
result, err := ApplyMessage(f.evm, f.msg, f.pool)
if !errors.Is(err, test.wantError) {
t.Fatalf("execution error = %v, want %v", err, test.wantError)
}
if err != nil {
return
}
if result.Err != nil || result.UsedGas != 21_000 {
t.Fatalf("execution result = %+v", result)
}
if have := f.state.GetBalance(f.msg.From); !have.Eq(uint256.NewInt(test.wantSender)) {
t.Fatalf("sender balance = %v, want %d", have, test.wantSender)
}
if have := f.state.GetBalance(*f.msg.To); !have.Eq(uint256.NewInt(7)) {
t.Fatalf("recipient balance = %v, want 7", have)
}
if have := f.state.GetBalance(f.evm.Context.Coinbase); !have.Eq(uint256.NewInt(21_000)) {
t.Fatalf("beneficiary balance = %v, want 21000", have)
}
if nonce := f.state.GetNonce(f.msg.From); nonce != 1 {
t.Fatalf("sender nonce = %d, want 1", nonce)
}
})
}
}

func TestExecutionCheckBalanceRevertAndRefund(t *testing.T) {
for _, test := range []struct {
name string
code []byte
wantRevert bool
wantRefund bool
}{
{name: "revert", code: []byte{byte(vm.PUSH1), 0, byte(vm.PUSH1), 0, byte(vm.REVERT)}, wantRevert: true},
{name: "storage refund", code: []byte{byte(vm.PUSH1), 1, byte(vm.PUSH1), 0, byte(vm.SSTORE), byte(vm.PUSH1), 0, byte(vm.PUSH1), 0, byte(vm.SSTORE)}, wantRefund: true},
} {
t.Run(test.name, func(t *testing.T) {
f := newExecutionCheckFixture(t, vm.Config{DisableBalanceCheck: true})
f.fees(0, 1, 1)
f.msg.Value.SetInt64(7)
f.state.SetBalance(f.msg.From, new(uint256.Int), tracing.BalanceChangeUnspecified)
f.state.SetCode(*f.msg.To, test.code, tracing.CodeChangeUnspecified)
result, err := ApplyMessage(f.evm, f.msg, f.pool)
if err != nil {
t.Fatal(err)
}
if gotRevert := errors.Is(result.Err, vm.ErrExecutionReverted); gotRevert != test.wantRevert || result.Err != nil && !gotRevert {
t.Fatalf("execution error = %v, want revert %v", result.Err, test.wantRevert)
}
if test.wantRefund && result.MaxUsedGas <= result.UsedGas {
t.Fatal("expected a storage gas refund")
}
wantSender, wantRecipient := f.msg.GasLimit-result.UsedGas, uint64(7)
if test.wantRevert {
wantSender += 7
wantRecipient = 0
}
if have := f.state.GetBalance(f.msg.From); !have.Eq(uint256.NewInt(wantSender)) {
t.Fatalf("sender balance = %v, want %d", have, wantSender)
}
if have := f.state.GetBalance(*f.msg.To); !have.Eq(uint256.NewInt(wantRecipient)) {
t.Fatalf("recipient balance = %v, want %d", have, wantRecipient)
}
})
}
}

func TestExecutionCheckBypassesRetainValidation(t *testing.T) {
for _, test := range []struct {
name string
change func(*executionCheckFixture)
wantError error
}{
{name: "fee width", change: func(f *executionCheckFixture) { f.msg.GasFeeCap.Lsh(big.NewInt(1), 256) }, wantError: ErrFeeCapVeryHigh},
{name: "tip width", change: func(f *executionCheckFixture) { f.msg.GasTipCap.Lsh(big.NewInt(1), 256) }, wantError: ErrTipVeryHigh},
{name: "value width", change: func(f *executionCheckFixture) { f.msg.Value.Lsh(big.NewInt(1), 256) }, wantError: ErrInsufficientFunds},
{name: "intrinsic gas", change: func(f *executionCheckFixture) { f.msg.GasLimit = 20_999 }, wantError: ErrIntrinsicGas},
{name: "block gas budget", change: func(f *executionCheckFixture) { f.pool = new(GasPool).AddGas(f.msg.GasLimit - 1) }, wantError: ErrGasLimitReached},
{name: "nonce", change: func(f *executionCheckFixture) { f.msg.Nonce = 1 }, wantError: ErrNonceTooHigh},
{name: "sender code", change: func(f *executionCheckFixture) {
f.state.SetCode(f.msg.From, []byte{byte(vm.STOP)}, tracing.CodeChangeUnspecified)
}, wantError: ErrSenderNoEOA},
} {
t.Run(test.name, func(t *testing.T) {
f := newExecutionCheckFixture(t, vm.Config{DisableBaseFeeCheck: true, DisablePriorityFeeCheck: true, DisableBalanceCheck: true})
test.change(f)
if _, err := ApplyMessage(f.evm, f.msg, f.pool); !errors.Is(err, test.wantError) {
t.Fatalf("execution error = %v, want %v", err, test.wantError)
}
})
}
}
5 changes: 4 additions & 1 deletion core/vm/interpreter.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,19 @@ package vm
import (
"fmt"

"github.com/holiman/uint256"
"github.com/tenderly/polygon-bor/common"
"github.com/tenderly/polygon-bor/common/math"
"github.com/tenderly/polygon-bor/core/tracing"
"github.com/holiman/uint256"
)

// Config are the configuration options for the Interpreter
type Config struct {
Tracer *tracing.Hooks
NoBaseFee bool // Forces the EIP-1559 baseFee to 0 (needed for 0 price calls)
DisableBaseFeeCheck bool // Allows gas fees below the block base fee without changing fee values.
DisablePriorityFeeCheck bool // Allows a priority fee above the fee cap.
DisableBalanceCheck bool // Funds the top-level caller when gas or value exceeds its balance.
EnablePreimageRecording bool // Enables recording of SHA3/keccak preimages
ExtraEips []int // Additional EIPS that are to be enabled

Expand Down
Loading