Files
evm/precompile/registry/bridge_precompileenv_test.go
zeekay 06c4d8916c precompile env: gate the Call surface to the DEX settlement family (0x9999/0x9996)
GetPrecompileEnv() is on the SHARED external contract.AccessibleState interface,
so the live Call surface (whose sub-call msg.sender is the asserting precompile's
OWN address) is reachable by every external precompile that type-asserts the
optional callableEnv capability. Bound that blast radius with defense-in-depth:

- precompile/registry/bridge.go: callableEnvAllowlist keyed on the geth env's
  self-address (env.Addresses().Self), populated from the canonical
  dex.LXSettleAddress (0x9999) + dex.DEXPositionManagerAddress (0x9996) constants
  (one source of truth). Every other external precompile (PQ/ZK verifiers at
  0x0122xx, future ones) gets a nil callable env and falls back to fail-secure
  refusal. No live behaviour change today (none assert callableEnv yet) — it
  prevents a FUTURE precompile silently acquiring sub-call power. Sits on top of
  each settlement Run's CALL-only guard.
- core/precompile_overrider.go: GetPrecompileEnv comment corrected — the accessor
  is off the EVM-INTERNAL interface but IS on the shared external one; reach is
  bounded by the bridge gate + the settle CALL-only guard.
- precompile/registry/bridge_precompileenv_test.go: recordingEnv carries a self;
  TestBridge_GetPrecompileEnv_NonSettlementSelf_Gated (0x012204 verifier -> nil,
  no Call) + _PositionManagerSelf_Allowed (0x9996 -> non-nil); WiredForwardsCall
  now models the 0x9999 self.
2026-06-22 14:37:01 -07:00

213 lines
11 KiB
Go

// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package registry
import (
"context"
"math/big"
"testing"
"github.com/luxfi/evm/precompile/contract"
"github.com/luxfi/evm/precompile/precompileconfig"
"github.com/luxfi/geth/common"
"github.com/luxfi/geth/core/types"
gethvm "github.com/luxfi/geth/core/vm"
"github.com/luxfi/geth/params"
"github.com/luxfi/precompile/dex"
)
// settleSelf is the 0x9999 DEX settlement address — the canonical self the bridge
// gates the Call surface to. The production DEX path runs with this self, so the
// WIRED tests model it.
var settleSelf = common.HexToAddress(dex.LXSettleAddress)
// bridge_precompileenv_test.go pins the CONSUMER side of the ERC-20 settlement env
// wiring — the fix that makes accessibleStateBridge.GetPrecompileEnv() return a LIVE
// Call surface instead of nil. Before the fix, this returned nil unconditionally, and
// the DEX 0x9999 precompile's ERC-20 leg refused every transferFrom/transfer with
// ErrERC20VaultUnavailable (no token could ever move cross-chain). The bridge now
// forwards the geth env's Call to the external precompile via the callableEnv shape.
//
// Three properties are pinned:
// 1. WIRED: when the internal adapter exposes a geth env (production), the bridge
// returns a non-nil external env whose Call routes to the geth env.
// 2. FAIL-SECURE: when the internal adapter does NOT expose a geth env (a test mock
// or non-EVM caller), the bridge returns nil so the precompile refuses rather
// than mint an unbacked claim.
// 3. SIGNATURE: the returned env satisfies the EXACT callableEnv shape the DEX
// precompile (luxfi/precompile/dex/module_erc20.go) type-asserts.
// callableEnv is byte-identical to the assertion in luxfi/precompile/dex's
// module_erc20.go. Keep it in lock-step: if the precompile's expected Call signature
// drifts from what precompileEnvBridge provides, this test fails to compile.
type callableEnv interface {
Call(addr common.Address, input []byte, gas uint64, value *big.Int) (ret []byte, gasLeft uint64, err error)
}
// recordingEnv is a fake geth vm.PrecompileEnvironment whose Call records its
// arguments and returns a sentinel — enough to prove the bridge forwards Call to the
// underlying geth env with the value, no caller proxying, and the variadic tail
// dropped. All other methods are unused stubs (the bridge only forwards Call/ReadOnly).
type recordingEnv struct {
calls int
lastAddr common.Address
lastIn []byte
lastGas uint64
lastVal *big.Int
lastOpts int // number of CallOption passed — MUST be 0 (no proxying)
readOnly bool
self common.Address // the precompile self the bridge gates on (Addresses().Self)
}
func (e *recordingEnv) Call(addr common.Address, input []byte, gas uint64, value *big.Int, opts ...gethvm.CallOption) ([]byte, uint64, error) {
e.calls++
e.lastAddr = addr
e.lastIn = input
e.lastGas = gas
e.lastVal = value
e.lastOpts = len(opts)
return []byte("ok"), gas, nil
}
func (e *recordingEnv) ReadOnly() bool { return e.readOnly }
func (e *recordingEnv) BlockHeader() (*types.Header, error) { return &types.Header{}, nil }
func (e *recordingEnv) Rules() params.Rules { return params.Rules{} }
func (e *recordingEnv) BlockNumber() *big.Int { return big.NewInt(0) }
func (e *recordingEnv) BlockTime() uint64 { return 0 }
func (e *recordingEnv) Addresses() gethvm.PrecompileAddresses {
return gethvm.PrecompileAddresses{Self: e.self}
}
func (e *recordingEnv) ChainConfig() *params.ChainConfig { return nil }
func (e *recordingEnv) StateDB() gethvm.StateDB { return nil }
func (e *recordingEnv) ReadOnlyState() gethvm.StateDB { return nil }
func (e *recordingEnv) UseGas(uint64) bool { return true }
func (e *recordingEnv) Gas() uint64 { return 0 }
func (e *recordingEnv) ConsensusContext() context.Context { return context.Background() }
func (e *recordingEnv) CallIndex() uint32 { return 0 }
var _ gethvm.PrecompileEnvironment = (*recordingEnv)(nil)
// internalWithEnv is a minimal internal contract.AccessibleState that ALSO exposes the
// geth env via GetPrecompileEnv — the production shape (evm/core's accessibleStateAdapter).
type internalWithEnv struct {
env gethvm.PrecompileEnvironment
}
func (m *internalWithEnv) GetStateDB() contract.StateDB { return nil }
func (m *internalWithEnv) GetBlockContext() contract.BlockContext { return nil }
func (m *internalWithEnv) GetConsensusContext() context.Context { return context.Background() }
func (m *internalWithEnv) GetChainConfig() precompileconfig.ChainConfig { return nil }
func (m *internalWithEnv) GetPrecompileEnv() gethvm.PrecompileEnvironment { return m.env }
var _ contract.AccessibleState = (*internalWithEnv)(nil)
// internalNoEnv is an internal contract.AccessibleState that does NOT expose an env
// (a test mock / non-EVM caller). The bridge must return nil for it (fail-secure).
type internalNoEnv struct{}
func (m *internalNoEnv) GetStateDB() contract.StateDB { return nil }
func (m *internalNoEnv) GetBlockContext() contract.BlockContext { return nil }
func (m *internalNoEnv) GetConsensusContext() context.Context { return context.Background() }
func (m *internalNoEnv) GetChainConfig() precompileconfig.ChainConfig { return nil }
var _ contract.AccessibleState = (*internalNoEnv)(nil)
// TestBridge_GetPrecompileEnv_WiredForwardsCall proves property (1) + (3): with a geth
// env present, the bridge returns a non-nil external env that satisfies callableEnv
// and forwards Call to the geth env — with the value passed through and ZERO call
// options (no caller proxying, so the token sees msg.sender == the precompile self,
// matching the depositor's approve(0x9999) allowance).
func TestBridge_GetPrecompileEnv_WiredForwardsCall(t *testing.T) {
rec := &recordingEnv{readOnly: false, self: settleSelf} // DEX settlement self (allowlisted)
bridge := &accessibleStateBridge{internal: &internalWithEnv{env: rec}}
env := bridge.GetPrecompileEnv()
if env == nil {
t.Fatal("GetPrecompileEnv() returned nil with a geth env present — ERC-20 settlement would refuse on-chain")
}
if env.ReadOnly() != false {
t.Fatal("ReadOnly() not forwarded")
}
// Property (3): the EXACT shape the DEX precompile type-asserts.
c, ok := env.(callableEnv)
if !ok {
t.Fatal("returned env does NOT satisfy callableEnv — DEX ERC-20 leg would refuse with ErrERC20VaultUnavailable")
}
// Property (1): Call forwards to the geth env with value and no options.
tokenAddr := common.HexToAddress("0x000000000000000000000000000000000000C0FE")
input := []byte{0x23, 0xb8, 0x72, 0xdd, 0x01} // transferFrom selector + a byte
ret, gasLeft, err := c.Call(tokenAddr, input, 100_000, big.NewInt(0))
if err != nil {
t.Fatalf("Call forwarded an error: %v", err)
}
if string(ret) != "ok" || gasLeft != 100_000 {
t.Fatalf("Call did not forward to the geth env: ret=%q gasLeft=%d", ret, gasLeft)
}
if rec.calls != 1 {
t.Fatalf("geth env Call invoked %d times, want 1", rec.calls)
}
if rec.lastAddr != tokenAddr || string(rec.lastIn) != string(input) || rec.lastGas != 100_000 {
t.Fatalf("Call args not forwarded faithfully: addr=%s gas=%d", rec.lastAddr, rec.lastGas)
}
if rec.lastOpts != 0 {
t.Fatalf("Call forwarded %d CallOptions, want 0 — caller proxying would break the ERC-20 allowance (token would check allowance for the wrong spender)", rec.lastOpts)
}
}
// TestBridge_GetPrecompileEnv_NoEnv_FailsSecure proves property (2): without a geth
// env, the bridge returns nil so the precompile fails-secure (ErrERC20VaultUnavailable)
// rather than mint an unbacked claim. This is the safe default on any chain/harness
// that does not provide an EVM execution environment.
func TestBridge_GetPrecompileEnv_NoEnv_FailsSecure(t *testing.T) {
bridge := &accessibleStateBridge{internal: &internalNoEnv{}}
if env := bridge.GetPrecompileEnv(); env != nil {
t.Fatalf("GetPrecompileEnv() returned non-nil (%T) without a geth env — would expose a broken Call surface", env)
}
}
// TestBridge_GetPrecompileEnv_TypedNilEnv_FailsSecure guards the typed-nil trap: when
// the internal adapter exposes the envProvider accessor but its env is nil, the bridge
// MUST still return an untyped nil (not a *precompileEnvBridge wrapping a nil env),
// so the precompile's nil check fires instead of nil-dereferencing on Call.
func TestBridge_GetPrecompileEnv_TypedNilEnv_FailsSecure(t *testing.T) {
bridge := &accessibleStateBridge{internal: &internalWithEnv{env: nil}}
if env := bridge.GetPrecompileEnv(); env != nil {
t.Fatalf("GetPrecompileEnv() returned non-nil (%T) for a nil env — typed-nil trap; precompile would nil-deref on Call", env)
}
}
// TestBridge_GetPrecompileEnv_NonSettlementSelf_Gated proves the defense-in-depth
// allowlist: a precompile whose self is NOT a DEX settlement address (e.g. a PQ/ZK
// verifier at 0x0122xx, or any future external precompile) gets a nil callable env
// even when a live geth env is present. The Call surface (whose sub-call msg.sender
// is the asserting precompile's own address) is reserved for the DEX settlement
// family; everything else fails-secure. This bounds the blast radius of
// GetPrecompileEnv() being on the shared external AccessibleState interface.
func TestBridge_GetPrecompileEnv_NonSettlementSelf_Gated(t *testing.T) {
// A representative non-settlement precompile address (Pulsar threshold, 0x012204).
verifier := common.HexToAddress("0x0000000000000000000000000000000000012204")
rec := &recordingEnv{readOnly: false, self: verifier}
bridge := &accessibleStateBridge{internal: &internalWithEnv{env: rec}}
if env := bridge.GetPrecompileEnv(); env != nil {
t.Fatalf("GetPrecompileEnv() returned non-nil (%T) for non-settlement self %s — the Call surface must be gated to the DEX settlement family", env, verifier)
}
if rec.calls != 0 {
t.Fatal("a gated env must never have forwarded a Call")
}
}
// TestBridge_GetPrecompileEnv_PositionManagerSelf_Allowed proves 0x9996 (the LP
// PositionManager that composes 0x9999 and moves ERC-20 liquidity) is on the
// allowlist — its commit leg legitimately needs the Call surface.
func TestBridge_GetPrecompileEnv_PositionManagerSelf_Allowed(t *testing.T) {
pm := common.HexToAddress(dex.DEXPositionManagerAddress)
rec := &recordingEnv{readOnly: false, self: pm}
bridge := &accessibleStateBridge{internal: &internalWithEnv{env: rec}}
if env := bridge.GetPrecompileEnv(); env == nil {
t.Fatalf("GetPrecompileEnv() returned nil for 0x9996 PositionManager self %s — its ERC-20 LP commit would refuse on-chain", pm)
}
}