evm: fix 0x9999 dispatch consensus-divergence on replay path + harden value/genesis edges (v1.99.34)

Red review of the 0x9999 dated-fork activation (v1.99.33) found ONE HIGH that
blocked the production deploy, plus a money-path footgun and two guards. The
money path (V4 swap ABI, EXTCODESIZE marker, two-phase atomic settle) is
byte-for-byte unchanged — only the dispatch-path timestamp source, the SubBalance
fail-mode, the genesis-builder guard, and stale comments change. Bumps
precompile v0.5.53 -> v0.5.54 (Red L1 comment scrub).

H1 (HIGH, blocked deploy) — consensus-divergence on the relaunch/replay path:
  core.LuxPrecompileOverrider.PrecompileOverride read the PROCESS-GLOBAL
  params.lastRulesContext via GetRulesExtra(Rules{}). That global is rewritten
  last-writer-wins by SetRulesContext on every Rules()/RulesAt() — including
  concurrent eth_call/estimateGas/miner goroutines. The geth EVM invokes
  PrecompileOverride LAZILY during opcode execution, a different instant than when
  precompileHook set the global. On a live, RPC-serving post-fork node replaying a
  PRE-fork RLP block (admin.importChain), a concurrent post-fork eth_call could
  overwrite the global timestamp between the verify goroutine's NewEVM(pre-fork
  block) and its tx dispatch -> the pre-fork block sees 0x9999 ENABLED and
  dispatches SettleContract.Run during plain-account execution -> STATE DIVERGENCE.
  Fix: the dispatch gate now decides from the overrider's OWN per-EVM fields
  (o.chainConfig + o.timestamp) via the pure params.GetExtrasRules — which performs
  the SAME dated-fork (extras.IsDexSettleActive) computation GetRulesExtra did,
  minus the racy global read. Audited every GetRulesExtra reader: only the overrider
  was on the precompile-dispatch path; no other dispatch/consensus-replay call site
  consults the 0x9999 gate. Every replay of a given block now yields the same
  enabled set on every validator, independent of concurrent activity.

L2 (money-path footgun) — registry stateDBBridge.SubBalance fail-open silent mint:
  when the subBalancer type-assertion failed it logged + returned a zero 'previous
  balance' WITHOUT subtracting, while the caller treats the debit as successful ->
  native value minted. Now FAILS CLOSED: it panics, which the EVM recovers into a
  reverted call, so the enclosing value move rolls back rather than committing
  half-done. Structurally unreachable in prod (the concrete adapter implements
  SubBalance) but no longer mints if a future StateDB omits it.

INFO1 (latent footgun) — genesis-config builders bypassed the dated fork:
  SetAllGenesisPrecompiles/AllGenesisPrecompiles iterated ALL RegisteredModules()
  incl the AlwaysOn 0x9999 and wrote timestamp-0 genesis configs, which would enable
  0x9999 from block 0 on a fresh chain — bypassing the dated fork and re-introducing
  the genesis-marker mutation. Both builders now skip AlwaysOn modules.

INFO2 (tests):
  - core: -race regression TestDexSettle_PrecompileOverride_NoGlobalRace reproduces
    the exact relaunch race (pre-fork verify vs concurrent post-fork eth_call
    clobbering the global) and asserts the pre-fork side ALWAYS sees 0x9999 ABSENT,
    post-fork ALWAYS PRESENT. PerEVMTimestamp pins the gate reads o.timestamp not the
    global. Replay e2e asserts a pre-fork value transfer to 0x9999 credits a PLAIN
    account WITHOUT invoking the precompile. All proven to FAIL under the old
    global-read code and PASS under the fix; -race clean.
  - extras + core: TestGenesisPrecompileBuilders_SkipAlwaysOn / _Skip9999 pin INFO1.
  - subbalance test comment refreshed to 0x9999 + fail-closed semantics.
This commit is contained in:
2026-06-21 12:42:42 -07:00
parent d34c473034
commit a5110aec52
8 changed files with 278 additions and 28 deletions
+175
View File
@@ -5,8 +5,10 @@ package core
import (
"math/big"
"sync"
"testing"
"github.com/holiman/uint256"
"github.com/luxfi/evm/core/state"
"github.com/luxfi/evm/params"
"github.com/luxfi/evm/params/extras"
@@ -16,6 +18,7 @@ import (
_ "github.com/luxfi/evm/precompile/registry"
"github.com/luxfi/geth/common"
"github.com/luxfi/geth/core/rawdb"
"github.com/luxfi/geth/core/tracing"
"github.com/stretchr/testify/require"
)
@@ -147,4 +150,176 @@ func TestDexSettle_ForkTransitionFiresOnce(t *testing.T) {
"an existing net whose genesis ts < the fork must NOT install the marker at genesis")
}
// TestDexSettle_GenesisBuilders_Skip9999 is the AUTHORITATIVE INFO1 regression: with the
// registry bridge imported (so 0x9999 is a live AlwaysOn module in the EVM registry), the
// genesis-config builders must NOT emit a timestamp-0 genesis config for 0x9999. If they
// did, a fresh chain built via SetAllGenesisPrecompiles/AllGenesisPrecompiles would enable
// 0x9999 from block 0 — bypassing the Dec 25 2025 dated fork and re-introducing the
// genesis-marker mutation the fork exists to avoid. (The extras-package guard test runs
// without the registry import, so 0x9999 is invisible there; THIS test sees it.)
func TestDexSettle_GenesisBuilders_Skip9999(t *testing.T) {
// Precondition: 0x9999 really is registered as AlwaysOn in this package's module view.
m, ok := modules.GetPrecompileModuleByAddress(settleAddr9999)
require.True(t, ok)
require.True(t, m.AlwaysOn)
fromFunc := extras.AllGenesisPrecompiles()
_, in := fromFunc[m.ConfigKey]
require.Falsef(t, in,
"AllGenesisPrecompiles must SKIP the AlwaysOn 0x9999 module (%s) — its activation is the "+
"dated fork, not a genesis config; a genesis entry would bypass the gate", m.ConfigKey)
var cfg extras.ChainConfig
cfg.SetAllGenesisPrecompiles()
_, inMethod := cfg.GenesisPrecompiles[m.ConfigKey]
require.Falsef(t, inMethod,
"SetAllGenesisPrecompiles must SKIP the AlwaysOn 0x9999 module (%s)", m.ConfigKey)
}
// TestDexSettle_PrecompileOverride_PerEVMTimestamp pins the H1 fix: the dispatch gate
// (LuxPrecompileOverrider.PrecompileOverride) must decide 0x9999's enabled-ness from the
// overrider's OWN per-EVM timestamp (o.timestamp), NOT from the process-global
// lastRulesContext. We POISON the global with the opposite timestamp first, then assert
// the override still answers from its own field. Under the pre-fix code (which read
// params.GetRulesExtra(Rules{}) → the global), a pre-fork EVM would wrongly see 0x9999
// ENABLED whenever any concurrent caller had stamped a post-fork timestamp into the
// global — a consensus divergence on the relaunch/replay path.
func TestDexSettle_PrecompileOverride_PerEVMTimestamp(t *testing.T) {
cfg := params.WithExtra(&params.ChainConfig{}, &extras.ChainConfig{})
// Poison the global with a POST-fork timestamp; a correct pre-fork override ignores it.
params.SetRulesContext(&params.Rules{}, cfg, tsPostActivation)
pre := &LuxPrecompileOverrider{chainConfig: cfg, timestamp: tsPreActivation}
_, okPre := pre.PrecompileOverride(settleAddr9999)
require.False(t, okPre,
"pre-fork EVM must see 0x9999 ABSENT even when the global holds a post-fork timestamp "+
"(dispatch gate must read o.timestamp, not lastRulesContext)")
// Poison the global with a PRE-fork timestamp; a correct post-fork override ignores it.
params.SetRulesContext(&params.Rules{}, cfg, tsPreActivation)
post := &LuxPrecompileOverrider{chainConfig: cfg, timestamp: tsPostActivation}
c, okPost := post.PrecompileOverride(settleAddr9999)
require.True(t, okPost,
"post-fork EVM must see 0x9999 PRESENT even when the global holds a pre-fork timestamp")
require.NotNil(t, c, "post-fork override must return the wrapped 0x9999 contract")
}
// TestDexSettle_PrecompileOverride_NoGlobalRace is the -race regression test for H1. It
// reproduces the exact relaunch scenario: a verify goroutine replaying a PRE-fork block
// concurrently with a post-fork eth_call/estimateGas/worker goroutine that rewrites the
// last-writer-wins global timestamp. The pre-fork side MUST deterministically see 0x9999
// ABSENT (plain account, no SettleContract.Run) on every iteration, and the post-fork
// side MUST deterministically see it PRESENT — regardless of who wrote the global last.
//
// Run with: go test -race -run NoGlobalRace ./core/
// Under the pre-fix code this fails the assertion (and/or -race flags the unsynchronised
// last-writer-wins read driving a consensus-relevant branch). Under the fix it is
// deterministic because each override reads only its own immutable fields.
func TestDexSettle_PrecompileOverride_NoGlobalRace(t *testing.T) {
cfg := params.WithExtra(&params.ChainConfig{}, &extras.ChainConfig{})
pre := &LuxPrecompileOverrider{chainConfig: cfg, timestamp: tsPreActivation}
post := &LuxPrecompileOverrider{chainConfig: cfg, timestamp: tsPostActivation}
const iters = 2000
done := make(chan struct{})
// Adversary: continuously clobber the global with alternating timestamps, mimicking
// concurrent eth_call (wall-clock, post-fork) and any pre-fork rules construction. This
// is precisely what SetRulesContext does on every Rules()/RulesAt() across goroutines.
go func() {
r := &params.Rules{}
for {
select {
case <-done:
return
default:
params.SetRulesContext(r, cfg, tsPostActivation)
params.SetRulesContext(r, cfg, tsPreActivation)
}
}
}()
var wg sync.WaitGroup
wg.Add(2)
// Pre-fork replay goroutine: 0x9999 must be ABSENT every single time.
go func() {
defer wg.Done()
for i := 0; i < iters; i++ {
if _, ok := pre.PrecompileOverride(settleAddr9999); ok {
t.Errorf("pre-fork override saw 0x9999 ENABLED at iter %d — consensus divergence: "+
"a concurrent post-fork timestamp leaked through the global", i)
return
}
}
}()
// Post-fork goroutine: 0x9999 must be PRESENT every single time.
go func() {
defer wg.Done()
for i := 0; i < iters; i++ {
if _, ok := post.PrecompileOverride(settleAddr9999); !ok {
t.Errorf("post-fork override saw 0x9999 ABSENT at iter %d — a concurrent pre-fork "+
"timestamp leaked through the global", i)
return
}
}
}()
wg.Wait()
close(done)
}
// TestDexSettle_Replay_PreForkValueTransferCreditsPlainAccount is the replay e2e for
// H1/INFO2: on the relaunch path, replaying a PRE-fork value transfer to 0x9999 must
// credit a PLAIN account and must NOT invoke SettleContract.Run. geth's vm.Call routes
// to the precompile iff PrecompileOverride returns (contract,true); when it returns
// (nil,false), 0x9999 is an ordinary account and the EVM performs the balance transfer.
// We drive that exact branch at the dispatch boundary (CGO-free — no full vm.NewEVM):
//
// - PRE-fork override ⇒ (nil,false) ⇒ we apply the plain-account transfer and assert the
// 0x9999 account balance rose by the sent amount, while 0x9999 carries no activation
// marker (nonce/code stay zero — it is a plain recipient, the precompile never ran).
// - POST-fork override ⇒ (contract,true) ⇒ the EVM would dispatch the precompile instead.
//
// This is the canonical-history guarantee: a historical tx that paid native value into
// 0x9999 before the fork replays as a credit, byte-identical to the committed state.
func TestDexSettle_Replay_PreForkValueTransferCreditsPlainAccount(t *testing.T) {
cfg := params.WithExtra(&params.ChainConfig{}, &extras.ChainConfig{})
const sent uint64 = 7_500_000_000
// Poison the global to a post-fork wall-clock value — the live, RPC-serving post-fork
// node's reality while it replays the imported pre-fork block. The pre-fork override
// must IGNORE this and keep 0x9999 a plain account.
params.SetRulesContext(&params.Rules{}, cfg, tsPostActivation)
db := rawdb.NewMemoryDatabase()
statedb, err := state.New(common.Hash{}, state.NewDatabase(db), nil)
require.NoError(t, err)
pre := &LuxPrecompileOverrider{chainConfig: cfg, timestamp: tsPreActivation}
contract, dispatched := pre.PrecompileOverride(settleAddr9999)
require.False(t, dispatched,
"pre-fork replay must NOT dispatch the 0x9999 precompile — it is a plain account")
require.Nil(t, contract)
// PrecompileOverride said "plain account", so the EVM credits the recipient. Mirror that.
statedb.AddBalance(settleAddr9999, uint256.NewInt(sent), tracing.BalanceChangeTransfer)
require.Equal(t, uint256.NewInt(sent), statedb.GetBalance(settleAddr9999),
"pre-fork value transfer to 0x9999 must credit the plain account by the sent amount")
require.Zero(t, statedb.GetNonce(settleAddr9999),
"replayed pre-fork credit must not touch 0x9999 nonce — the precompile never ran")
require.Empty(t, statedb.GetCode(settleAddr9999),
"replayed pre-fork credit must not install marker code — 0x9999 is a plain account pre-fork")
// Sanity: at/after the fork the SAME address DOES dispatch the precompile.
post := &LuxPrecompileOverrider{chainConfig: cfg, timestamp: tsPostActivation}
c2, dispatched2 := post.PrecompileOverride(settleAddr9999)
require.True(t, dispatched2,
"post-fork: 0x9999 must dispatch the settlement precompile, not behave as a plain account")
require.NotNil(t, c2)
}
func ptr(v uint64) *uint64 { return &v }
+23 -3
View File
@@ -49,10 +49,30 @@ type LuxPrecompileOverrider struct {
// PrecompileOverride returns the precompile at the given address if it's
// an active Lux custom precompile.
//
// The enabled-set decision is computed from THIS overrider's OWN per-EVM fields
// (o.chainConfig + o.timestamp, set in precompileHook at the EVM's construction),
// NOT from the process-global lastRulesContext that params.GetRulesExtra(Rules{})
// would read. The geth EVM invokes PrecompileOverride lazily during opcode
// execution (when a CALL targets a precompile address), which is a DIFFERENT
// point in time than when precompileHook ran. Reading the last-writer-wins global
// here is a consensus-divergence bug: on the relaunch path (admin.importChain of a
// pre-fork RLP snapshot on a live, RPC-serving, post-fork node) a concurrent
// eth_call/estimateGas/worker goroutine can rewrite the global timestamp to a
// wall-clock (post-fork) value between this EVM's construction and its tx dispatch
// — making the pre-fork block see the dated-fork 0x9999 precompile as ENABLED and
// dispatch SettleContract.Run during what must be plain-account execution. Binding
// the gate to o.timestamp makes every replay of a given block produce the SAME
// enabled set on every validator, regardless of concurrent activity.
//
// params.GetExtrasRules is a pure function of its arguments (it does NOT read the
// global); it is the SAME computation params.GetRulesExtra performs internally, so
// the dated-fork 0x9999 injection (extras.IsDexSettleActive gate) is applied here
// identically — minus the racy global read. params.ChainConfig is a type alias of
// geth's ChainConfig, so o.chainConfig is passed directly.
func (o *LuxPrecompileOverrider) PrecompileOverride(addr common.Address) (vm.PrecompiledContract, bool) {
// Get the extras rules to check active precompiles
rulesExtra := params.GetRulesExtra(gethparams.Rules{})
if !rulesExtra.IsPrecompileEnabled(addr) {
extrasRules := params.GetExtrasRules(gethparams.Rules{}, o.chainConfig, o.timestamp)
if cfg, ok := extrasRules.Precompiles[addr]; !ok || cfg.IsDisabled() {
return nil, false
}
+8 -7
View File
@@ -17,13 +17,14 @@ import (
//
// SubBalance(common.Address, *uint256.Int, tracing.BalanceChangeReason) uint256.Int
//
// and falls back to a NO-OP (logging "SubBalance fallback used") when the
// assertion fails. The concrete adapter the EVM hands precompiles is
// *stateDBAdapter; if it does not implement that EXACT signature, a precompile
// that debits native value — e.g. the DEX 0x9010 custody vault, whose withdraw
// debits the vault before releasing to the caller — would silently MINT (caller
// credited, vault never debited). The fix added SubBalance to *stateDBAdapter so
// the bridge's assertion succeeds and the fallback is unreachable.
// and previously fell back to a NO-OP when the assertion failed. The concrete
// adapter the EVM hands precompiles is *stateDBAdapter; if it does not implement
// that EXACT signature, a precompile that debits native value — e.g. the DEX 0x9999
// custody vault, whose withdraw debits the vault before releasing to the caller —
// would silently MINT (caller credited, vault never debited). The fix added
// SubBalance to *stateDBAdapter so the bridge's assertion succeeds and the fallback
// is unreachable; the fallback itself now FAILS CLOSED (panics → reverted call)
// rather than returning a zero "previous balance" without debiting.
// subBalancer is the EXACT interface precompile/registry/bridge.go type-asserts.
// Keep it byte-identical so this test fails if the bridge's expected signature or
+1 -1
View File
@@ -32,7 +32,7 @@ require (
github.com/luxfi/metric v1.5.8
github.com/luxfi/p2p v1.21.1
github.com/luxfi/pq v1.0.3
github.com/luxfi/precompile v0.5.53
github.com/luxfi/precompile v0.5.54
github.com/luxfi/runtime v1.1.1
github.com/luxfi/sys v0.1.0
github.com/luxfi/timer v1.0.2
+2
View File
@@ -280,6 +280,8 @@ github.com/luxfi/precompile v0.5.52 h1:1gw4iJEv5Tj8bBwNctlMympe3lIzhmFymgetKglqZ
github.com/luxfi/precompile v0.5.52/go.mod h1:aR523IXj+VQAIU1CYP/64dVwKK93DDssHj1tGFdzLTY=
github.com/luxfi/precompile v0.5.53 h1:R3WAgHYlk2rEasgFGzDRx6iS49Wn9ApxZUf6Kf2F0KI=
github.com/luxfi/precompile v0.5.53/go.mod h1:aR523IXj+VQAIU1CYP/64dVwKK93DDssHj1tGFdzLTY=
github.com/luxfi/precompile v0.5.54 h1:O/qQWg4CH5eLTOMxGGFTfZd9WhWY1ogUDGmigB6F0R8=
github.com/luxfi/precompile v0.5.54/go.mod h1:aR523IXj+VQAIU1CYP/64dVwKK93DDssHj1tGFdzLTY=
github.com/luxfi/proto v1.3.4 h1:dNrdQ/414Pe9otIekVUmQ9QLqemaMFK43H4N3DIbuzQ=
github.com/luxfi/proto v1.3.4/go.mod h1:S80KrTzISCktptit/pWLzk6nHgagjJS86qbQL6gHutE=
github.com/luxfi/resource v0.0.1 h1:mTh+ICWSy548GTUSSyx7V/X5dV18oEwxZeQEYGJQhD4=
+16
View File
@@ -354,9 +354,19 @@ func (c *ChainConfig) MarshalJSON() ([]byte, error) {
// for all registered precompile modules (timestamp = 0). This ensures deterministic
// genesis hash when "all precompiles active at genesis" is the intended state.
// Call this when building genesis configs for new chains.
//
// AlwaysOn modules (e.g. the 0x9999 DEX settlement money path) are SKIPPED: their
// activation is governed by a single built-in dated fork (extras.DexSettleActivationTime),
// NOT by a genesisPrecompiles entry. Writing a timestamp-0 genesis config for them would
// enable them from block 0 on every chain, bypassing the dated-fork gate and re-introducing
// the genesis-marker mutation the dated fork exists to avoid. AlwaysOn modules carry no
// per-net config, so there is nothing to seed into genesis regardless.
func (c *ChainConfig) SetAllGenesisPrecompiles() {
c.GenesisPrecompiles = make(Precompiles)
for _, module := range modules.RegisteredModules() {
if module.AlwaysOn {
continue
}
c.GenesisPrecompiles[module.ConfigKey] = module.Configurator.MakeGenesisConfig()
}
}
@@ -364,9 +374,15 @@ func (c *ChainConfig) SetAllGenesisPrecompiles() {
// AllGenesisPrecompiles returns a new Precompiles map populated with default
// genesis configs for all registered precompile modules (timestamp = 0).
// This is the authoritative source for "all precompiles active at genesis".
//
// AlwaysOn modules are SKIPPED for the same reason as SetAllGenesisPrecompiles: a
// dated-fork-gated system precompile must never receive a timestamp-0 genesis config.
func AllGenesisPrecompiles() Precompiles {
precompiles := make(Precompiles)
for _, module := range modules.RegisteredModules() {
if module.AlwaysOn {
continue
}
precompiles[module.ConfigKey] = module.Configurator.MakeGenesisConfig().(precompileconfig.Config)
}
return precompiles
+36 -1
View File
@@ -30,13 +30,48 @@ func TestAllGenesisPrecompilesDeterminism(t *testing.T) {
require.Equal(t, uint64(0), *config1.Timestamp(), "genesis config timestamp should be 0")
}
// Verify all registered modules are present
// Verify all registered NON-AlwaysOn modules are present, and that NO AlwaysOn
// module is present. AlwaysOn precompiles (e.g. the 0x9999 DEX settlement money
// path) are activated by a built-in dated fork, NOT by a timestamp-0 genesis config;
// emitting one would enable them from genesis on every chain and bypass the gate.
for _, module := range modules.RegisteredModules() {
_, ok := result1[module.ConfigKey]
if module.AlwaysOn {
require.Falsef(t, ok,
"AlwaysOn module %s must NOT be in AllGenesisPrecompiles — its activation is the dated fork, not a genesis config",
module.ConfigKey)
continue
}
require.True(t, ok, "module %s should be in AllGenesisPrecompiles", module.ConfigKey)
}
}
// TestGenesisPrecompileBuilders_SkipAlwaysOn pins the INFO1 guard: the genesis-config
// builders (AllGenesisPrecompiles and ChainConfig.SetAllGenesisPrecompiles) must never
// emit a timestamp-0 genesis config for an AlwaysOn module. Writing one would enable a
// dated-fork-gated system precompile (0x9999) from block 0 on every chain — bypassing
// the canonical activation boundary and re-introducing the genesis-marker mutation. The
// invariant is enforced positively (skip any module with AlwaysOn) so it holds even if an
// AlwaysOn module is later registered into this package's module view.
func TestGenesisPrecompileBuilders_SkipAlwaysOn(t *testing.T) {
fromFunc := AllGenesisPrecompiles()
var cfg ChainConfig
cfg.SetAllGenesisPrecompiles()
for _, module := range modules.RegisteredModules() {
if !module.AlwaysOn {
continue
}
_, inFunc := fromFunc[module.ConfigKey]
require.Falsef(t, inFunc,
"AllGenesisPrecompiles must skip AlwaysOn module %s — dated-fork activation, not genesis", module.ConfigKey)
_, inMethod := cfg.GenesisPrecompiles[module.ConfigKey]
require.Falsef(t, inMethod,
"SetAllGenesisPrecompiles must skip AlwaysOn module %s — dated-fork activation, not genesis", module.ConfigKey)
}
}
// TestPrecompilesMarshalDeterministic verifies that MarshalJSONDeterministic
// produces consistent output regardless of map iteration order.
func TestPrecompilesMarshalDeterministic(t *testing.T) {
+17 -16
View File
@@ -251,27 +251,28 @@ func (s *stateDBBridge) AddBalance(addr common.Address, amount *uint256.Int, _ t
func (s *stateDBBridge) SubBalance(addr common.Address, amount *uint256.Int, _ tracing.BalanceChangeReason) uint256.Int {
// The internal contract.StateDB doesn't have SubBalance.
// Try to reach the underlying vm.StateDB via type assertion.
// Reach the underlying vm.StateDB via type assertion.
type subBalancer interface {
SubBalance(common.Address, *uint256.Int, tracing.BalanceChangeReason) uint256.Int
}
if sb, ok := s.internal.(subBalancer); ok {
return sb.SubBalance(addr, amount, tracing.BalanceChangeUnspecified)
}
// Fallback: use snapshot + manual balance calculation
// Get current balance, compute new, use AddBalance with negative (not possible)
// Instead, log warning. This path should not be hit in practice because the
// concrete stateDBAdapter in precompile_overrider.go wraps vm.StateDB which has SubBalance.
bal := s.internal.GetBalance(addr)
if bal != nil && bal.Cmp(amount) >= 0 {
// Workaround: We can't subtract directly, so we reconstruct via the underlying
// types. In practice this codepath is unreachable because the internal StateDB
// is always a stateDBAdapter{vm.StateDB} which implements SubBalance.
log.Error("stateDBBridge: SubBalance fallback used — precompile state may be incorrect",
sb, ok := s.internal.(subBalancer)
if !ok {
// FAIL CLOSED. There is NO safe fallback: the internal contract.StateDB has no
// way to subtract, so silently returning a zero "previous balance" without
// debiting (the old behaviour) would let a value-moving precompile — e.g. the
// 0x9999 DEX custody vault on withdraw — treat the debit as successful while the
// balance is untouched, MINTING native value. A failed debit on the money path
// must abort the call, never proceed. We panic: the EVM recovers a precompile
// panic into a reverted call, so the enclosing value move is rolled back rather
// than committed half-done. In production the internal StateDB is always the
// concrete stateDBAdapter{vm.StateDB} (precompile_overrider.go), which DOES
// implement SubBalance, so this panic is structurally unreachable; it exists to
// make any future StateDB that omits SubBalance fail loudly instead of minting.
log.Error("stateDBBridge: SubBalance on a StateDB without subtraction support — failing closed to avoid minting native value",
"address", addr, "amount", amount)
panic(fmt.Sprintf("stateDBBridge: internal StateDB %T cannot SubBalance; refusing to credit without debiting (addr=%s amount=%s)", s.internal, addr, amount))
}
return uint256.Int{}
return sb.SubBalance(addr, amount, tracing.BalanceChangeUnspecified)
}
func (s *stateDBBridge) GetBalanceMultiCoin(addr common.Address, coinID common.Hash) *big.Int {