mirror of
https://github.com/luxfi/precompile.git
synced 2026-09-24 00:26:21 +00:00
dex(0x9999): timestamp-gated dated-fork activation + remove 0x9010 (v0.5.53)
0x9999 — the SOLE canonical DEX settlement money path — was made always-on/
dispatch-only in v0.5.52. That was half-right: it dropped the EXTCODESIZE marker
(so a typed Solidity IPoolManager(0x9999).swap(...) failed the compiler's
contract-existence guard) AND it was unconditional (so replaying pre-activation
history would dispatch the precompile for a tx that historically hit a plain
account). This commit lands the CTO's exact model.
Activation is a SINGLE canonical dated fork (Dec 25 2025 = unix 1766704800),
defined ONCE in luxfi/evm (extras.DexSettleActivationTime) — not per-net config.
The precompile side here is unchanged in HOW it activates (still AlwaysOn-tagged,
runtime params, DefaultDAOTreasury, runtime DChainID); the timestamp-gating +
marker install live in luxfi/evm. Money-path/settlement logic byte-for-byte
unchanged (4-pot conservation, rails, atomicity, FIX-2, reentrancy, custody).
0x9010 REMOVED entirely as a callable precompile (CTO step 5 — no forwarding, no
shim, no deprecation stub):
- The 0x9010 modules.Module registration is GONE. 0x9010 is no longer in
RegisteredModules, so the EVM never dispatches it and eth_getCode(0x9010)=0x.
- DEXContract.Run + every run* dispatch helper + RequiredGas + the
PRECOMPILE_MOVED revert group + the dead runSwap forwarder + ErrPrecompileMoved
are DELETED (820 lines). 0x9999 is the sole money path; the exchange points
straight at it.
- The 0x9010 per-net Config/configurator/compromisedDevControllers are DELETED:
0x9999 takes no per-net controller (built-in DefaultDAOTreasury, deliberately
not a public dev key), so the activation-time controller-misconfig footgun is
structurally eliminated.
- KEPT (shared infra the money path needs): the DEXContract struct as the
in-process *PoolManager holder (router/stateview/market-registry compose it),
the PoolManager engine + its pool/vault/pause state which keys at LP-9010's
storage address (poolManagerAddr — NOT a dispatch surface; ripping it would
orphan vault funds and violate "settlement logic unchanged"), the V4 selector
constants + keccak4 selector-verification init (consumed by settle_module.go),
poolStateAdapter, and the shared ABI decode/encode helpers.
Registered DEX precompiles are now: 0x9999 SettleModule (AlwaysOn, sole value
mover), 0x9998 Quoter (view), 0x9997 StateView (view), 0x9996 PositionManager
(composes 0x9999), 0x9012 Router (view). Exactly one money path.
Red-team coverage preserved, migrated to the 0x9999 surface (not dropped):
- extsloadArray overflow/bounds (count overflow, count>max, count=0, offset
overflow, offset-beyond-input) now drive SettleContract.runSettleExtsloadArray,
whose readLowU64 + subtraction-bounds are stronger than the deleted 0x9010
version. The BitLen>64 offset test now asserts the real invariant (no panic,
no over-read) since 0x9999 caps-and-bounds rather than rejecting.
- nil-BlockContext fail-closed (no validator-halt panic) migrated to 0x9999.
- extsload single/array/short-input read tests migrated to 0x9999.
- New: TestDex9010_NotRegistered, TestDex9999_DefaultControllerNotCompromised.
Tests: full dex suite 333/333 PASS, 0 fail, 0 skip (CGO_ENABLED=0, GOWORK=off).
This commit is contained in:
+54
-119
@@ -5,125 +5,73 @@ package dex
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/geth/common"
|
||||
"github.com/luxfi/precompile/precompileconfig"
|
||||
"github.com/luxfi/precompile/modules"
|
||||
)
|
||||
|
||||
// TestDexConfigure_NoProtocolFeeController_RejectsActivation verifies that
|
||||
// Configure() refuses to activate the DEX precompile when the upgrade.json
|
||||
// dexConfig omits protocolFeeController (or sets it to the zero address).
|
||||
//
|
||||
// Regression: before this guard, omitting the field defaulted to the Foundry
|
||||
// Anvil account #0 (0xf39Fd6...92266) whose private key is in every Foundry
|
||||
// installation. That left PauseDEX / ResumeDEX / FreezePool callable by any
|
||||
// caller — a chain-wide AMM compromise at activation.
|
||||
func TestDexConfigure_NoProtocolFeeController_RejectsActivation(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
cfg *Config
|
||||
}{
|
||||
{
|
||||
name: "zero-address protocolFeeController",
|
||||
cfg: &Config{
|
||||
ProtocolFeeController: common.Address{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no protocolFeeController field at all",
|
||||
cfg: &Config{},
|
||||
},
|
||||
}
|
||||
// 0x9010 is REMOVED as a callable precompile (no Run, not registered, no forward).
|
||||
// 0x9999 is the SOLE canonical DEX precompile and takes NO per-net config: its
|
||||
// settlement-governance authority (protocolFeeController) is the built-in
|
||||
// DefaultDAOTreasury, fixed across every network. There is therefore no
|
||||
// activation-time controller config to misconfigure — the compromised-dev-key class
|
||||
// of footgun is structurally eliminated. These tests pin that new model.
|
||||
|
||||
cfgr := &configurator{}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := cfgr.Configure(nil, tc.cfg, nil, nil)
|
||||
if !errors.Is(err, ErrDEXNoProtocolFeeController) {
|
||||
t.Fatalf("expected ErrDEXNoProtocolFeeController, got: %v", err)
|
||||
}
|
||||
})
|
||||
var lxPoolAddr9010 = common.HexToAddress(LXPoolAddress)
|
||||
|
||||
// anvilDevAccounts are the well-known Foundry/Anvil deterministic dev accounts whose
|
||||
// private keys are public knowledge. The 0x9999 built-in controller must never be one
|
||||
// of them (settle_module.go DefaultDAOTreasury).
|
||||
var anvilDevAccounts = []common.Address{
|
||||
common.HexToAddress("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"), // anvil #0
|
||||
common.HexToAddress("0x70997970C51812dc3A010C7d01b50e0d17dc79C8"), // anvil #1
|
||||
common.HexToAddress("0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"), // anvil #2
|
||||
common.HexToAddress("0x90F79bf6EB2c4f870365E785982E1f101E93b906"), // anvil #3
|
||||
common.HexToAddress("0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65"), // anvil #4
|
||||
common.HexToAddress("0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc"), // anvil #5
|
||||
common.HexToAddress("0x976EA74026E726554dB657fA54763abd0C3a0aa9"), // anvil #6
|
||||
common.HexToAddress("0x14dC79964da2C08b23698B3D3cc7Ca32193d9955"), // anvil #7
|
||||
common.HexToAddress("0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f"), // anvil #8
|
||||
common.HexToAddress("0xa0Ee7A142d267C1f36714E4a8F75612F20a79720"), // anvil #9
|
||||
}
|
||||
|
||||
// TestDex9010_NotRegistered asserts 0x9010 is NOT a registered precompile — it has no
|
||||
// module, so the EVM never dispatches it and eth_getCode(0x9010) is 0x. 0x9999 IS
|
||||
// registered and is the sole DEX money path.
|
||||
func TestDex9010_NotRegistered(t *testing.T) {
|
||||
if _, ok := modules.GetPrecompileModuleByAddress(lxPoolAddr9010); ok {
|
||||
t.Fatal("0x9010 must NOT be a registered precompile — it is removed; 0x9999 is the sole DEX precompile")
|
||||
}
|
||||
if _, ok := modules.GetPrecompileModuleByAddress(poolManagerAddr9999); !ok {
|
||||
t.Fatal("0x9999 must be the registered DEX precompile")
|
||||
}
|
||||
}
|
||||
|
||||
// TestDexConfigure_AnvilDefault_RejectsActivation verifies that any of the
|
||||
// well-known Foundry/Anvil deterministic dev accounts is rejected as the
|
||||
// protocolFeeController. These addresses' private keys are public knowledge,
|
||||
// so configuring one as the chain's AMM admin would be a chain-wide
|
||||
// compromise. The blocklist lives in dex/module.go::compromisedDevControllers
|
||||
// and must catch each of them.
|
||||
func TestDexConfigure_AnvilDefault_RejectsActivation(t *testing.T) {
|
||||
anvilAccounts := []string{
|
||||
"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", // anvil #0
|
||||
"0x70997970C51812dc3A010C7d01b50e0d17dc79C8", // anvil #1
|
||||
"0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", // anvil #2
|
||||
"0x90F79bf6EB2c4f870365E785982E1f101E93b906", // anvil #3
|
||||
"0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65", // anvil #4
|
||||
"0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc", // anvil #5
|
||||
"0x976EA74026E726554dB657fA54763abd0C3a0aa9", // anvil #6
|
||||
"0x14dC79964da2C08b23698B3D3cc7Ca32193d9955", // anvil #7
|
||||
"0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f", // anvil #8
|
||||
"0xa0Ee7A142d267C1f36714E4a8F75612F20a79720", // anvil #9
|
||||
// TestDex9999_DefaultControllerNotCompromised asserts the built-in 0x9999 settlement
|
||||
// governance authority (DefaultDAOTreasury) is NOT one of the publicly-known dev keys.
|
||||
// Because 0x9999 has no per-net controller config, this built-in value is the only
|
||||
// admin and it must be safe by construction.
|
||||
func TestDex9999_DefaultControllerNotCompromised(t *testing.T) {
|
||||
for _, bad := range anvilDevAccounts {
|
||||
if DefaultDAOTreasury == bad {
|
||||
t.Fatalf("0x9999 DefaultDAOTreasury must not be a publicly-known dev key: %s", bad.Hex())
|
||||
}
|
||||
}
|
||||
|
||||
cfgr := &configurator{}
|
||||
for _, addr := range anvilAccounts {
|
||||
t.Run(addr, func(t *testing.T) {
|
||||
cfg := &Config{
|
||||
ProtocolFeeController: common.HexToAddress(addr),
|
||||
}
|
||||
err := cfgr.Configure(nil, cfg, nil, nil)
|
||||
if !errors.Is(err, ErrDEXCompromisedController) {
|
||||
t.Fatalf("expected ErrDEXCompromisedController for %s, got: %v", addr, err)
|
||||
}
|
||||
// The error message should include the offending address so
|
||||
// operators can see exactly what they put in upgrade.json.
|
||||
if !strings.Contains(err.Error(), addr) && !strings.Contains(err.Error(), strings.ToLower(addr)) {
|
||||
// Address.Hex() uses checksum casing, accept either.
|
||||
expectedChecksum := common.HexToAddress(addr).Hex()
|
||||
if !strings.Contains(err.Error(), expectedChecksum) {
|
||||
t.Errorf("error should include the offending address; got: %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
if DefaultDAOTreasury == (common.Address{}) {
|
||||
t.Fatal("0x9999 DefaultDAOTreasury must not be the zero address")
|
||||
}
|
||||
// The singleton 0x9999 contract must carry the built-in treasury as its authority.
|
||||
if SettlePrecompile.protocolFeeController != DefaultDAOTreasury {
|
||||
t.Fatalf("0x9999 protocolFeeController must be the built-in DefaultDAOTreasury, got %s",
|
||||
SettlePrecompile.protocolFeeController.Hex())
|
||||
}
|
||||
}
|
||||
|
||||
// TestDexConfigure_RealAddress_ActivatesCleanly verifies that a real (non-
|
||||
// dev-key) protocolFeeController activates the precompile without error.
|
||||
// We use the LUX_MNEMONIC-derived treasury 0x9011E888...4714 as the canonical
|
||||
// transitional placeholder (KMS-controlled, not publicly known).
|
||||
func TestDexConfigure_RealAddress_ActivatesCleanly(t *testing.T) {
|
||||
cfgr := &configurator{}
|
||||
|
||||
// Reset singleton state to be hermetic — Configure() mutates the
|
||||
// singleton PoolManager's protocolFeeController. Save+restore.
|
||||
origController := DEXPrecompile.poolManager.protocolFeeController
|
||||
t.Cleanup(func() {
|
||||
DEXPrecompile.poolManager.protocolFeeController = origController
|
||||
})
|
||||
|
||||
realAddr := common.HexToAddress("0x9011E888251AB053B7bD1cdB598Db4f9DEd94714")
|
||||
cfg := &Config{ProtocolFeeController: realAddr}
|
||||
|
||||
if err := cfgr.Configure(nil, cfg, nil, nil); err != nil {
|
||||
t.Fatalf("Configure with real address should succeed, got: %v", err)
|
||||
}
|
||||
if DEXPrecompile.poolManager.protocolFeeController != realAddr {
|
||||
t.Fatalf(
|
||||
"singleton controller not updated: got %s, want %s",
|
||||
DEXPrecompile.poolManager.protocolFeeController.Hex(), realAddr.Hex(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDexAdmin_OnlyConfiguredController verifies that admin functions
|
||||
// (PauseDEX, ResumeDEX, PausePool, ResumePool, FreezePool) refuse unauthorized
|
||||
// callers — only the address configured as protocolFeeController is allowed.
|
||||
// This complements the activation-time guard: even if a real controller is
|
||||
// configured, a random EVM caller cannot exercise the admin surface.
|
||||
// TestDexAdmin_OnlyConfiguredController verifies that the shared PoolManager admin
|
||||
// functions (PauseDEX, ResumeDEX, PausePool, ResumePool, FreezePool) refuse
|
||||
// unauthorized callers — only the configured protocolFeeController is allowed. This is
|
||||
// the authorization gate 0x9999's halt/governance selectors rely on.
|
||||
func TestDexAdmin_OnlyConfiguredController(t *testing.T) {
|
||||
configured := common.HexToAddress("0x9011E888251AB053B7bD1cdB598Db4f9DEd94714")
|
||||
random := common.HexToAddress("0xBADBADBADBADBADBADBADBADBADBADBADBADBADB")
|
||||
@@ -163,16 +111,3 @@ func TestDexAdmin_OnlyConfiguredController(t *testing.T) {
|
||||
t.Fatalf("configured controller ResumeDEX should succeed, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDexConfig_MakeConfigReturnsConfig verifies the configurator returns the
|
||||
// canonical Config type, so JSON unmarshalling produces a struct whose
|
||||
// ProtocolFeeController field is populated from `protocolFeeController` in
|
||||
// upgrade.json (the field the operator must set).
|
||||
func TestDexConfig_MakeConfigReturnsConfig(t *testing.T) {
|
||||
cfg := (&configurator{}).MakeConfig()
|
||||
if _, ok := cfg.(*Config); !ok {
|
||||
t.Fatalf("MakeConfig should return *Config, got %T", cfg)
|
||||
}
|
||||
// Confirm the interface compliance is preserved.
|
||||
var _ precompileconfig.Config = (*Config)(nil)
|
||||
}
|
||||
|
||||
+13
-807
@@ -15,15 +15,14 @@ import (
|
||||
"github.com/luxfi/geth/core/tracing"
|
||||
ethtypes "github.com/luxfi/geth/core/types"
|
||||
"github.com/luxfi/precompile/contract"
|
||||
"github.com/luxfi/precompile/modules"
|
||||
"github.com/luxfi/precompile/precompileconfig"
|
||||
)
|
||||
|
||||
var _ contract.Configurator = (*configurator)(nil)
|
||||
var _ contract.StatefulPrecompiledContract = (*DEXContract)(nil)
|
||||
|
||||
// ConfigKey is the key used in json config files to specify this precompile config.
|
||||
const ConfigKey = "dexConfig"
|
||||
// DEXPrecompile is retained ONLY as the in-process holder of the shared *PoolManager
|
||||
// (pool/vault state lives at LP-9010's storage address — see pool_manager.go). The
|
||||
// read-only periphery (LP-9012 router, LP-9997 stateview) and the LP-9999 market
|
||||
// registry compose DEXPrecompile.poolManager. 0x9010 is NO LONGER a dispatchable
|
||||
// precompile — it is not registered, has no Run, and reverts/forwards nothing. 0x9999
|
||||
// is the SOLE canonical DEX precompile.
|
||||
|
||||
// Parsed addresses for LX precompiles (derived from string constants in types.go)
|
||||
// See types.go for canonical LP-aligned address strings
|
||||
@@ -119,14 +118,6 @@ func newDEXContract(e Engine) *DEXContract {
|
||||
}
|
||||
}
|
||||
|
||||
// Module is the precompile module (LXPool at LP-9010)
|
||||
var Module = modules.Module{
|
||||
ConfigKey: ConfigKey,
|
||||
Address: lxPoolAddr,
|
||||
Contract: DEXPrecompile,
|
||||
Configurator: &configurator{},
|
||||
}
|
||||
|
||||
// Method selectors: keccak256 of V4 function signatures (first 4 bytes).
|
||||
// Verified against IPoolManager.sol at init time.
|
||||
var (
|
||||
@@ -169,8 +160,6 @@ func keccak4(sig string) uint32 {
|
||||
return binary.BigEndian.Uint32(hash[:4])
|
||||
}
|
||||
|
||||
type configurator struct{}
|
||||
|
||||
func init() {
|
||||
// Verify hardcoded selectors match keccak256 computation at startup.
|
||||
verifySelector := func(name string, got uint32, sig string) {
|
||||
@@ -190,808 +179,25 @@ func init() {
|
||||
verifySelector("withdraw", SelectorWithdraw, "withdraw(address,uint256)")
|
||||
verifySelector("balanceOf", SelectorBalanceOf, "balanceOf(address,address)")
|
||||
|
||||
// Compute admin selectors at init time (not compile-time constants).
|
||||
// Compute admin selectors at init time (not compile-time constants). These
|
||||
// selectors are consumed by the LP-9999 settlement dispatch (settle_module.go).
|
||||
SelectorPauseDEX = keccak4("pauseDEX()")
|
||||
SelectorResumeDEX = keccak4("resumeDEX()")
|
||||
SelectorPausePool = keccak4("pausePool(bytes32)")
|
||||
SelectorResumePool = keccak4("resumePool(bytes32)")
|
||||
SelectorFreezePool = keccak4("freezePool(bytes32)")
|
||||
|
||||
if err := modules.RegisterModule(Module); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (*configurator) MakeConfig() precompileconfig.Config {
|
||||
return new(Config)
|
||||
}
|
||||
|
||||
// compromisedDevControllers lists Ethereum addresses whose private keys are
|
||||
// publicly distributed (Foundry/Anvil deterministic test accounts, Hardhat
|
||||
// default mnemonic, etc.). Configuring any of these as the DEX protocol fee
|
||||
// controller would let any caller pause / freeze the chain-wide AMM, so
|
||||
// activation is refused. Stored as the lowercase hex representation produced
|
||||
// by common.Address.Hex()-stripped-and-lowered for cheap membership testing.
|
||||
//
|
||||
// Sources:
|
||||
// - Foundry/Anvil default mnemonic "test test test test test test test test
|
||||
// test test test junk" (anvil --accounts 10) → accounts #0..#9.
|
||||
// - Hardhat default mnemonic produces an overlapping set (same root).
|
||||
//
|
||||
// New entries should be appended whenever a new well-known development
|
||||
// mnemonic is identified.
|
||||
var compromisedDevControllers = map[common.Address]struct{}{
|
||||
common.HexToAddress("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"): {}, // anvil #0
|
||||
common.HexToAddress("0x70997970C51812dc3A010C7d01b50e0d17dc79C8"): {}, // anvil #1
|
||||
common.HexToAddress("0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"): {}, // anvil #2
|
||||
common.HexToAddress("0x90F79bf6EB2c4f870365E785982E1f101E93b906"): {}, // anvil #3
|
||||
common.HexToAddress("0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65"): {}, // anvil #4
|
||||
common.HexToAddress("0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc"): {}, // anvil #5
|
||||
common.HexToAddress("0x976EA74026E726554dB657fA54763abd0C3a0aa9"): {}, // anvil #6
|
||||
common.HexToAddress("0x14dC79964da2C08b23698B3D3cc7Ca32193d9955"): {}, // anvil #7
|
||||
common.HexToAddress("0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f"): {}, // anvil #8
|
||||
common.HexToAddress("0xa0Ee7A142d267C1f36714E4a8F75612F20a79720"): {}, // anvil #9
|
||||
}
|
||||
|
||||
func (*configurator) Configure(
|
||||
chainConfig precompileconfig.ChainConfig,
|
||||
cfg precompileconfig.Config,
|
||||
state contract.StateDB,
|
||||
blockContext contract.ConfigurationBlockContext,
|
||||
) error {
|
||||
config, ok := cfg.(*Config)
|
||||
if !ok {
|
||||
return fmt.Errorf("expected config type %T, got %T: %v", &Config{}, cfg, cfg)
|
||||
}
|
||||
|
||||
// Protocol fee controller is required. NO DEFAULT. A missing or zero
|
||||
// controller would leave PauseDEX / ResumeDEX / FreezePool open to any
|
||||
// caller; the operator MUST populate this explicitly in upgrade.json
|
||||
// (typically a multisig or governance contract). Refusing activation
|
||||
// surfaces the misconfiguration on the activation block rather than
|
||||
// silently shipping an open-admin DEX.
|
||||
if config.ProtocolFeeController == (common.Address{}) {
|
||||
return ErrDEXNoProtocolFeeController
|
||||
}
|
||||
// Refuse known-compromised dev keys whose private keys are public
|
||||
// knowledge (Foundry/Anvil, Hardhat default mnemonic). This catches the
|
||||
// "I'll fix the placeholder before mainnet" footgun.
|
||||
if _, bad := compromisedDevControllers[config.ProtocolFeeController]; bad {
|
||||
return fmt.Errorf("%w: %s", ErrDEXCompromisedController, config.ProtocolFeeController.Hex())
|
||||
}
|
||||
DEXPrecompile.poolManager.protocolFeeController = config.ProtocolFeeController
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Config implements the precompileconfig.Config interface
|
||||
type Config struct {
|
||||
precompileconfig.Upgrade // Embedded for flat JSON structure
|
||||
ProtocolFeeController common.Address `json:"protocolFeeController,omitempty"`
|
||||
MaxPools uint64 `json:"maxPools,omitempty"`
|
||||
EnableFlashLoans bool `json:"enableFlashLoans,omitempty"`
|
||||
EnableHooks bool `json:"enableHooks,omitempty"`
|
||||
}
|
||||
|
||||
func (c *Config) Key() string {
|
||||
return ConfigKey
|
||||
}
|
||||
|
||||
func (c *Config) Timestamp() *uint64 {
|
||||
return c.Upgrade.Timestamp()
|
||||
}
|
||||
|
||||
func (c *Config) IsDisabled() bool {
|
||||
return c.Upgrade.Disable
|
||||
}
|
||||
|
||||
func (c *Config) Equal(cfg precompileconfig.Config) bool {
|
||||
other, ok := cfg.(*Config)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return c.Upgrade.Equal(&other.Upgrade) &&
|
||||
c.ProtocolFeeController == other.ProtocolFeeController &&
|
||||
c.MaxPools == other.MaxPools &&
|
||||
c.EnableFlashLoans == other.EnableFlashLoans &&
|
||||
c.EnableHooks == other.EnableHooks
|
||||
}
|
||||
|
||||
func (c *Config) Verify(chainConfig precompileconfig.ChainConfig) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DEXContract implements the DEX precompile
|
||||
// DEXContract is the in-process holder of the shared *PoolManager. It is NOT a
|
||||
// registered precompile and has no Run dispatch — 0x9010 is removed as a callable
|
||||
// surface. It exists solely so the read-only periphery (router/stateview) and the
|
||||
// LP-9999 market registry can compose poolManager, which keys pool/vault state at
|
||||
// LP-9010's storage address (pool_manager.go).
|
||||
type DEXContract struct {
|
||||
poolManager *PoolManager
|
||||
}
|
||||
|
||||
// Run executes the precompile
|
||||
func (c *DEXContract) Run(
|
||||
accessibleState contract.AccessibleState,
|
||||
caller common.Address,
|
||||
addr common.Address,
|
||||
input []byte,
|
||||
suppliedGas uint64,
|
||||
readOnly bool,
|
||||
) (ret []byte, remainingGas uint64, err error) {
|
||||
if len(input) < 4 {
|
||||
return nil, suppliedGas, fmt.Errorf("input too short")
|
||||
}
|
||||
|
||||
if accessibleState.GetBlockContext() == nil {
|
||||
return nil, suppliedGas, fmt.Errorf("block context unavailable")
|
||||
}
|
||||
|
||||
selector := binary.BigEndian.Uint32(input[:4])
|
||||
data := input[4:]
|
||||
|
||||
switch selector {
|
||||
// ALL value-moving / state-creating 0x9010 selectors are DEPRECATED — INCLUDING
|
||||
// swap. The canonical V4 surface is the LP-9999 family (0x9999 PoolManager is THE
|
||||
// money path; apps call it directly). 0x9010 keeps ONE money path = none: every
|
||||
// value-moving selector reverts PRECOMPILE_MOVED so there are never two money
|
||||
// paths, never two consumedReceipt/halt namespaces. swap is now in this group
|
||||
// (apps migrate the config address to 0x9999); the live-ZAP fork hazard is gone
|
||||
// with it. Market creation, liquidity, donate, custody all live at 0x9999.
|
||||
case SelectorSwap,
|
||||
SelectorInitialize,
|
||||
SelectorModifyLiquidity,
|
||||
SelectorDeposit,
|
||||
SelectorWithdraw,
|
||||
SelectorDonate,
|
||||
SelectorUnlock,
|
||||
SelectorTake,
|
||||
SelectorSettle, SelectorSettleFor,
|
||||
SelectorSync,
|
||||
SelectorPauseDEX,
|
||||
SelectorResumeDEX,
|
||||
SelectorPausePool,
|
||||
SelectorResumePool,
|
||||
SelectorFreezePool:
|
||||
return nil, suppliedGas, ErrPrecompileMoved
|
||||
|
||||
// Read-only views stay live at 0x9010 (no value movement, harmless): a stale
|
||||
// integration reading state does not break, and these never touch the money path.
|
||||
case SelectorBalanceOf:
|
||||
return c.runBalanceOf(accessibleState, data, suppliedGas)
|
||||
case SelectorExtsload:
|
||||
return c.runExtsload(accessibleState, data, suppliedGas)
|
||||
case SelectorExtsloadArray:
|
||||
return c.runExtsloadArray(accessibleState, data, suppliedGas)
|
||||
default:
|
||||
return nil, suppliedGas, fmt.Errorf("unknown method selector: 0x%08x", selector)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *DEXContract) runInitialize(
|
||||
state contract.AccessibleState,
|
||||
caller common.Address,
|
||||
input []byte,
|
||||
suppliedGas uint64,
|
||||
readOnly bool,
|
||||
) ([]byte, uint64, error) {
|
||||
if readOnly {
|
||||
return nil, suppliedGas, fmt.Errorf("cannot write in read-only mode")
|
||||
}
|
||||
|
||||
if suppliedGas < GasPoolCreate {
|
||||
return nil, 0, fmt.Errorf("out of gas")
|
||||
}
|
||||
|
||||
// V4 ABI: PoolKey (5 slots = 160 bytes) + sqrtPriceX96 (32 bytes)
|
||||
if len(input) < 192 {
|
||||
return nil, suppliedGas - GasPoolCreate, fmt.Errorf("input too short for initialize")
|
||||
}
|
||||
|
||||
key, err := DecodePoolKey(input[:160])
|
||||
if err != nil {
|
||||
return nil, suppliedGas - GasPoolCreate, err
|
||||
}
|
||||
|
||||
sqrtPriceX96 := new(big.Int).SetBytes(input[160:192])
|
||||
var hookData []byte
|
||||
if len(input) > 192 {
|
||||
hookData = decodeABIBytes(input, 192)
|
||||
}
|
||||
|
||||
// Initialize pool
|
||||
stateAdapter := newPoolStateAdapter(state)
|
||||
tick, err := c.poolManager.Initialize(stateAdapter, key, sqrtPriceX96, hookData)
|
||||
if err != nil {
|
||||
return nil, suppliedGas - GasPoolCreate, err
|
||||
}
|
||||
|
||||
// Return tick as int24, sign-extended to int256 (32 bytes, two's complement).
|
||||
// V4 ABI returns int24 as a full int256: negative values have 0xFF in high bytes.
|
||||
result := bigIntTo32Bytes(big.NewInt(int64(tick)))
|
||||
return result, suppliedGas - GasPoolCreate, nil
|
||||
}
|
||||
|
||||
// runSwap on 0x9010 is now a FORWARDER to the 0x9999 receipt-settlement money
|
||||
// path. The deprecated live-matcher swap (the C-Chain consensus FORK hazard:
|
||||
// runSwap -> poolManager.Swap -> engine.Swap -> live ZAP query against a moving
|
||||
// book, proven to split StateRoot by chains/dexvm TestRED_PerValidatorRelay_
|
||||
// SplitsConsensus) is REMOVED. A swap hitting 0x9010 is settled by the EXACT same
|
||||
// implementation, sharing the SAME settlement-consumed / halt storage namespace
|
||||
// (dex.precompile.v1.9999.*) — never two money paths, never two replay maps. A
|
||||
// forwarded call settles identically to a direct 0x9999 call. Deployed contracts
|
||||
// that still target 0x9010 keep working; new integrations point at 0x9999.
|
||||
func (c *DEXContract) runSwap(
|
||||
state contract.AccessibleState,
|
||||
caller common.Address,
|
||||
input []byte,
|
||||
suppliedGas uint64,
|
||||
readOnly bool,
|
||||
) ([]byte, uint64, error) {
|
||||
return SettleSwap(state, caller, input, suppliedGas, readOnly)
|
||||
}
|
||||
|
||||
// ErrPrecompileMoved is the clean revert for a value-moving 0x9010 call that is
|
||||
// NOT the forwarded swap. The deprecated custody/liquidity/donate paths drove the
|
||||
// live ZAP engine; value movement now lives ONLY at 0x9999. Callers migrate to
|
||||
// 0x9999 deposit/withdraw and the receipt-settlement swap.
|
||||
var ErrPrecompileMoved = fmt.Errorf("dex: PRECOMPILE_MOVED — all value-moving DEX calls (incl swap) live at 0x9999; 0x9010 keeps only read-only views")
|
||||
|
||||
func (c *DEXContract) runModifyLiquidity(
|
||||
state contract.AccessibleState,
|
||||
caller common.Address,
|
||||
input []byte,
|
||||
suppliedGas uint64,
|
||||
readOnly bool,
|
||||
) ([]byte, uint64, error) {
|
||||
if readOnly {
|
||||
return nil, suppliedGas, fmt.Errorf("cannot write in read-only mode")
|
||||
}
|
||||
|
||||
if suppliedGas < GasAddLiquidity {
|
||||
return nil, 0, fmt.Errorf("out of gas")
|
||||
}
|
||||
|
||||
key, params, hookData, err := DecodeModifyLiquidityInput(input)
|
||||
if err != nil {
|
||||
return nil, suppliedGas - GasAddLiquidity, err
|
||||
}
|
||||
|
||||
stateAdapter := newPoolStateAdapter(state)
|
||||
|
||||
delta, feeDelta, err := c.poolManager.ModifyLiquidity(stateAdapter, caller, key, params, hookData)
|
||||
if err != nil {
|
||||
return nil, suppliedGas - GasAddLiquidity, err
|
||||
}
|
||||
|
||||
// No C-Chain settlement: placing a resting order locks the maker's deposited
|
||||
// D-Chain balance (available -> locked, inside the book); cancelling unlocks
|
||||
// it. 0x9010 holds no reserve backing the order.
|
||||
|
||||
// V4: Return two packed BalanceDeltas (callerDelta + feesAccrued), each 32 bytes
|
||||
result := make([]byte, 64)
|
||||
copy(result[0:32], PackBalanceDelta(delta.Amount0, delta.Amount1))
|
||||
copy(result[32:64], PackBalanceDelta(feeDelta.Amount0, feeDelta.Amount1))
|
||||
return result, suppliedGas - GasAddLiquidity, nil
|
||||
}
|
||||
|
||||
// runDeposit is the EVM ingress for funds-IN: deposit(address asset, uint256
|
||||
// amount). For native LUX (asset == address(0)) the caller MUST send msg.value ==
|
||||
// amount; the EVM has already moved that value into the 0x9010 vault before this
|
||||
// precompile runs, so the deposit LOCKS it there and MINTS the caller's available
|
||||
// D-Chain balance. 0x9010 is a passive vault, never a trade counterparty.
|
||||
//
|
||||
// ABI: input = asset[32] (address, right-aligned) || amount[32] (uint256).
|
||||
// Returns the deposited amount as uint256 (the credited available balance delta).
|
||||
func (c *DEXContract) runDeposit(
|
||||
state contract.AccessibleState,
|
||||
caller common.Address,
|
||||
input []byte,
|
||||
suppliedGas uint64,
|
||||
readOnly bool,
|
||||
) ([]byte, uint64, error) {
|
||||
if readOnly {
|
||||
return nil, suppliedGas, fmt.Errorf("cannot write in read-only mode")
|
||||
}
|
||||
if suppliedGas < GasSettlement {
|
||||
return nil, 0, fmt.Errorf("out of gas")
|
||||
}
|
||||
if len(input) < 64 {
|
||||
return nil, suppliedGas - GasSettlement, fmt.Errorf("deposit: input too short")
|
||||
}
|
||||
asset := Currency{Address: common.BytesToAddress(input[12:32])}
|
||||
amount := new(big.Int).SetBytes(input[32:64])
|
||||
|
||||
stateAdapter := newPoolStateAdapter(state)
|
||||
if err := c.poolManager.Deposit(stateAdapter, caller, asset, amount); err != nil {
|
||||
return nil, suppliedGas - GasSettlement, err
|
||||
}
|
||||
|
||||
out := make([]byte, 32)
|
||||
amount.FillBytes(out)
|
||||
return out, suppliedGas - GasSettlement, nil
|
||||
}
|
||||
|
||||
// runWithdraw is the EVM ingress for funds-OUT: withdraw(address asset, uint256
|
||||
// want). It BURNS up to `want` of the caller's available D-Chain balance (the
|
||||
// ledger clamps to availability) and RELEASES exactly the realized amount from the
|
||||
// 0x9010 vault back to the caller. Conserving: the vault never pays more than the
|
||||
// ledger burned. Returns the realized amount as uint256.
|
||||
func (c *DEXContract) runWithdraw(
|
||||
state contract.AccessibleState,
|
||||
caller common.Address,
|
||||
input []byte,
|
||||
suppliedGas uint64,
|
||||
readOnly bool,
|
||||
) ([]byte, uint64, error) {
|
||||
if readOnly {
|
||||
return nil, suppliedGas, fmt.Errorf("cannot write in read-only mode")
|
||||
}
|
||||
if suppliedGas < GasSettlement {
|
||||
return nil, 0, fmt.Errorf("out of gas")
|
||||
}
|
||||
if len(input) < 64 {
|
||||
return nil, suppliedGas - GasSettlement, fmt.Errorf("withdraw: input too short")
|
||||
}
|
||||
asset := Currency{Address: common.BytesToAddress(input[12:32])}
|
||||
want := new(big.Int).SetBytes(input[32:64])
|
||||
|
||||
stateAdapter := newPoolStateAdapter(state)
|
||||
realized, err := c.poolManager.Withdraw(stateAdapter, caller, asset, want)
|
||||
if err != nil {
|
||||
return nil, suppliedGas - GasSettlement, err
|
||||
}
|
||||
|
||||
out := make([]byte, 32)
|
||||
realized.FillBytes(out)
|
||||
return out, suppliedGas - GasSettlement, nil
|
||||
}
|
||||
|
||||
// runBalanceOf is a read-only observation of an account's AVAILABLE D-Chain
|
||||
// balance for an asset: balanceOf(address account, address asset). It forwards to
|
||||
// the custody backend's Withdraw with want=0? No — a read must not mutate. It
|
||||
// queries the D-Chain via the backend's read path. Returns the available balance
|
||||
// as uint256. (The locked balance is queryable on the venue's clob_balance; this
|
||||
// EVM view returns available, the spendable claim.)
|
||||
func (c *DEXContract) runBalanceOf(
|
||||
state contract.AccessibleState,
|
||||
input []byte,
|
||||
suppliedGas uint64,
|
||||
) ([]byte, uint64, error) {
|
||||
if suppliedGas < GasPoolLookup {
|
||||
return nil, 0, fmt.Errorf("out of gas")
|
||||
}
|
||||
if len(input) < 64 {
|
||||
return nil, suppliedGas - GasPoolLookup, fmt.Errorf("balanceOf: input too short")
|
||||
}
|
||||
account := common.BytesToAddress(input[12:32])
|
||||
asset := Currency{Address: common.BytesToAddress(input[44:64])}
|
||||
|
||||
avail, err := c.poolManager.BalanceOf(account, asset)
|
||||
if err != nil {
|
||||
return nil, suppliedGas - GasPoolLookup, err
|
||||
}
|
||||
out := make([]byte, 32)
|
||||
avail.FillBytes(out)
|
||||
return out, suppliedGas - GasPoolLookup, nil
|
||||
}
|
||||
|
||||
func (c *DEXContract) runTake(
|
||||
_ contract.AccessibleState,
|
||||
_ common.Address,
|
||||
_ []byte,
|
||||
suppliedGas uint64,
|
||||
_ bool,
|
||||
) ([]byte, uint64, error) {
|
||||
return nil, suppliedGas, fmt.Errorf("take not supported: blockchain provides atomic settlement")
|
||||
}
|
||||
|
||||
func (c *DEXContract) runSettle(
|
||||
_ contract.AccessibleState,
|
||||
_ common.Address,
|
||||
_ []byte,
|
||||
suppliedGas uint64,
|
||||
_ bool,
|
||||
) ([]byte, uint64, error) {
|
||||
return nil, suppliedGas, fmt.Errorf("settle not supported: blockchain provides atomic settlement")
|
||||
}
|
||||
|
||||
func (c *DEXContract) runUnlock(
|
||||
_ contract.AccessibleState,
|
||||
_ common.Address,
|
||||
_ []byte,
|
||||
suppliedGas uint64,
|
||||
_ bool,
|
||||
) ([]byte, uint64, error) {
|
||||
// V4 unlock is used for flash accounting. In a precompile context the blockchain
|
||||
// provides atomic settlement, so unlock is a no-op that returns empty bytes.
|
||||
return []byte{}, suppliedGas, nil
|
||||
}
|
||||
|
||||
func (c *DEXContract) runDonate(
|
||||
state contract.AccessibleState,
|
||||
caller common.Address,
|
||||
input []byte,
|
||||
suppliedGas uint64,
|
||||
readOnly bool,
|
||||
) ([]byte, uint64, error) {
|
||||
if readOnly {
|
||||
return nil, suppliedGas, fmt.Errorf("cannot write in read-only mode")
|
||||
}
|
||||
|
||||
gasCost := GasSwap // donate uses similar gas to swap
|
||||
if suppliedGas < gasCost {
|
||||
return nil, 0, fmt.Errorf("out of gas")
|
||||
}
|
||||
|
||||
// V4 ABI: PoolKey (160) + amount0 (32) + amount1 (32) + hookData
|
||||
if len(input) < 224 {
|
||||
return nil, suppliedGas - gasCost, fmt.Errorf("input too short for V4 donate")
|
||||
}
|
||||
|
||||
key, err := DecodePoolKey(input[:160])
|
||||
if err != nil {
|
||||
return nil, suppliedGas - gasCost, err
|
||||
}
|
||||
|
||||
amount0 := new(big.Int).SetBytes(input[160:192])
|
||||
amount1 := new(big.Int).SetBytes(input[192:224])
|
||||
|
||||
stateAdapter := newPoolStateAdapter(state)
|
||||
|
||||
var hookData []byte
|
||||
if len(input) > 224 {
|
||||
// Offset word for hookData (bytes arg) is at position 224 in the args buffer.
|
||||
// The value at that position is the absolute offset from byte 0 of args.
|
||||
hookData = decodeABIBytes(input, 224)
|
||||
}
|
||||
delta, err := c.poolManager.Donate(stateAdapter, caller, key, amount0, amount1, hookData)
|
||||
if err != nil {
|
||||
return nil, suppliedGas - gasCost, err
|
||||
}
|
||||
|
||||
result := PackBalanceDelta(delta.Amount0, delta.Amount1)
|
||||
return result, suppliedGas - gasCost, nil
|
||||
}
|
||||
|
||||
func (c *DEXContract) runSync(
|
||||
_ contract.AccessibleState,
|
||||
_ common.Address,
|
||||
_ []byte,
|
||||
suppliedGas uint64,
|
||||
_ bool,
|
||||
) ([]byte, uint64, error) {
|
||||
// V4 sync checkpoints ERC20 balances for flash accounting.
|
||||
// Precompile uses direct state access, so sync is a no-op.
|
||||
return []byte{}, suppliedGas, nil
|
||||
}
|
||||
|
||||
func (c *DEXContract) runGetPool(
|
||||
state contract.AccessibleState,
|
||||
input []byte,
|
||||
suppliedGas uint64,
|
||||
) ([]byte, uint64, error) {
|
||||
if suppliedGas < GasPoolLookup {
|
||||
return nil, 0, fmt.Errorf("out of gas")
|
||||
}
|
||||
|
||||
key, err := DecodePoolKey(input)
|
||||
if err != nil {
|
||||
return nil, suppliedGas - GasPoolLookup, err
|
||||
}
|
||||
|
||||
poolId := key.ID()
|
||||
stateAdapter := newPoolStateAdapter(state)
|
||||
pool := c.poolManager.getPool(stateAdapter, poolId)
|
||||
if !pool.IsInitialized() {
|
||||
return nil, suppliedGas - GasPoolLookup, fmt.Errorf("pool not found")
|
||||
}
|
||||
|
||||
// Encode pool state
|
||||
result := EncodePoolState(pool)
|
||||
return result, suppliedGas - GasPoolLookup, nil
|
||||
}
|
||||
|
||||
func (c *DEXContract) runGetPosition(
|
||||
state contract.AccessibleState,
|
||||
input []byte,
|
||||
suppliedGas uint64,
|
||||
) ([]byte, uint64, error) {
|
||||
if suppliedGas < GasPoolLookup {
|
||||
return nil, 0, fmt.Errorf("out of gas")
|
||||
}
|
||||
|
||||
// Parse position key from input
|
||||
// Position not found returns zeroes
|
||||
result := make([]byte, 96) // liquidity (32) + feeGrowthInside0 (32) + feeGrowthInside1 (32)
|
||||
return result, suppliedGas - GasPoolLookup, nil
|
||||
}
|
||||
|
||||
// runExtsload reads a single bytes32 storage slot from the pool manager address.
|
||||
// V4 ABI: extsload(bytes32) returns (bytes32)
|
||||
// Input: 32 bytes (the storage slot key)
|
||||
// Output: 32 bytes (the storage value)
|
||||
func (c *DEXContract) runExtsload(
|
||||
state contract.AccessibleState,
|
||||
input []byte,
|
||||
suppliedGas uint64,
|
||||
) ([]byte, uint64, error) {
|
||||
if suppliedGas < GasPoolLookup {
|
||||
return nil, 0, fmt.Errorf("out of gas")
|
||||
}
|
||||
|
||||
if len(input) < 32 {
|
||||
return nil, suppliedGas - GasPoolLookup, fmt.Errorf("input too short for extsload: need 32 bytes, got %d", len(input))
|
||||
}
|
||||
|
||||
slot := common.BytesToHash(input[0:32])
|
||||
value := state.GetStateDB().GetState(lxPoolAddr, slot)
|
||||
|
||||
return value.Bytes(), suppliedGas - GasPoolLookup, nil
|
||||
}
|
||||
|
||||
// runExtsloadArray reads multiple bytes32 storage slots from the pool manager address.
|
||||
// V4 ABI: extsload(bytes32[]) returns (bytes32[])
|
||||
// Input: ABI-encoded bytes32 array (offset word + length + slot0 + slot1 + ...)
|
||||
// Output: ABI-encoded bytes32 array with the same number of values
|
||||
//
|
||||
// Gas: the precompile uses the canonical RequiredGas formula
|
||||
// (extsloadArrayGas) as the single source of truth. Parse-time validation
|
||||
// failures still charge the base fee — the dispatcher already deducted it,
|
||||
// and the caller paid for the bounded work done before the error.
|
||||
func (c *DEXContract) runExtsloadArray(
|
||||
state contract.AccessibleState,
|
||||
input []byte,
|
||||
suppliedGas uint64,
|
||||
) ([]byte, uint64, error) {
|
||||
// Canonical gas: same formula RequiredGas advertises, computed once.
|
||||
totalGas := extsloadArrayGas(input)
|
||||
if suppliedGas < totalGas {
|
||||
return nil, 0, fmt.Errorf("out of gas")
|
||||
}
|
||||
remaining := suppliedGas - totalGas
|
||||
|
||||
// ABI-encoded dynamic array: first 32 bytes = offset to array data
|
||||
if len(input) < 64 {
|
||||
return nil, remaining, fmt.Errorf("input too short for extsload array")
|
||||
}
|
||||
|
||||
inputLen := uint64(len(input))
|
||||
offsetBig := new(big.Int).SetBytes(input[0:32])
|
||||
if offsetBig.BitLen() > 63 {
|
||||
return nil, remaining, fmt.Errorf("extsload array: offset overflow")
|
||||
}
|
||||
offset := offsetBig.Uint64()
|
||||
if offset > inputLen || inputLen-offset < 32 {
|
||||
return nil, remaining, fmt.Errorf("extsload array: offset out of bounds")
|
||||
}
|
||||
|
||||
countBig := new(big.Int).SetBytes(input[offset : offset+32])
|
||||
if countBig.BitLen() > 63 {
|
||||
return nil, remaining, fmt.Errorf("extsload array: count overflow")
|
||||
}
|
||||
count := countBig.Uint64()
|
||||
const maxExtsloadSlots = 256
|
||||
if count > maxExtsloadSlots {
|
||||
return nil, remaining, fmt.Errorf("extsload array: count %d exceeds max %d", count, maxExtsloadSlots)
|
||||
}
|
||||
dataStart := offset + 32
|
||||
|
||||
if count > 0 && (dataStart > inputLen || inputLen-dataStart < count*32) {
|
||||
return nil, remaining, fmt.Errorf("extsload array: data out of bounds")
|
||||
}
|
||||
|
||||
// Build ABI-encoded return: offset (32) + count (32) + values (count * 32)
|
||||
result := make([]byte, 64+count*32)
|
||||
// Offset to array data = 32
|
||||
result[31] = 0x20
|
||||
// Array length
|
||||
copy(result[32:64], input[offset:offset+32])
|
||||
|
||||
stateDB := state.GetStateDB()
|
||||
for i := range count {
|
||||
slotStart := dataStart + i*32
|
||||
slot := common.BytesToHash(input[slotStart : slotStart+32])
|
||||
value := stateDB.GetState(lxPoolAddr, slot)
|
||||
copy(result[64+i*32:64+(i+1)*32], value.Bytes())
|
||||
}
|
||||
|
||||
return result, remaining, nil
|
||||
}
|
||||
|
||||
// --- Admin: Pause / Resume / Freeze handlers ---
|
||||
|
||||
func (c *DEXContract) runPauseDEX(
|
||||
state contract.AccessibleState,
|
||||
caller common.Address,
|
||||
suppliedGas uint64,
|
||||
readOnly bool,
|
||||
) ([]byte, uint64, error) {
|
||||
if readOnly {
|
||||
return nil, suppliedGas, fmt.Errorf("cannot write in read-only mode")
|
||||
}
|
||||
if suppliedGas < GasAdmin {
|
||||
return nil, 0, fmt.Errorf("out of gas")
|
||||
}
|
||||
stateAdapter := newPoolStateAdapter(state)
|
||||
if err := c.poolManager.PauseDEX(stateAdapter, caller); err != nil {
|
||||
return nil, suppliedGas - GasAdmin, err
|
||||
}
|
||||
return nil, suppliedGas - GasAdmin, nil
|
||||
}
|
||||
|
||||
func (c *DEXContract) runResumeDEX(
|
||||
state contract.AccessibleState,
|
||||
caller common.Address,
|
||||
suppliedGas uint64,
|
||||
readOnly bool,
|
||||
) ([]byte, uint64, error) {
|
||||
if readOnly {
|
||||
return nil, suppliedGas, fmt.Errorf("cannot write in read-only mode")
|
||||
}
|
||||
if suppliedGas < GasAdmin {
|
||||
return nil, 0, fmt.Errorf("out of gas")
|
||||
}
|
||||
stateAdapter := newPoolStateAdapter(state)
|
||||
if err := c.poolManager.ResumeDEX(stateAdapter, caller); err != nil {
|
||||
return nil, suppliedGas - GasAdmin, err
|
||||
}
|
||||
return nil, suppliedGas - GasAdmin, nil
|
||||
}
|
||||
|
||||
func (c *DEXContract) runPausePool(
|
||||
state contract.AccessibleState,
|
||||
caller common.Address,
|
||||
input []byte,
|
||||
suppliedGas uint64,
|
||||
readOnly bool,
|
||||
) ([]byte, uint64, error) {
|
||||
if readOnly {
|
||||
return nil, suppliedGas, fmt.Errorf("cannot write in read-only mode")
|
||||
}
|
||||
if suppliedGas < GasAdmin {
|
||||
return nil, 0, fmt.Errorf("out of gas")
|
||||
}
|
||||
if len(input) < 32 {
|
||||
return nil, suppliedGas - GasAdmin, fmt.Errorf("input too short for pool ID")
|
||||
}
|
||||
var poolId [32]byte
|
||||
copy(poolId[:], input[:32])
|
||||
stateAdapter := newPoolStateAdapter(state)
|
||||
if err := c.poolManager.PausePool(stateAdapter, caller, poolId); err != nil {
|
||||
return nil, suppliedGas - GasAdmin, err
|
||||
}
|
||||
return nil, suppliedGas - GasAdmin, nil
|
||||
}
|
||||
|
||||
func (c *DEXContract) runResumePool(
|
||||
state contract.AccessibleState,
|
||||
caller common.Address,
|
||||
input []byte,
|
||||
suppliedGas uint64,
|
||||
readOnly bool,
|
||||
) ([]byte, uint64, error) {
|
||||
if readOnly {
|
||||
return nil, suppliedGas, fmt.Errorf("cannot write in read-only mode")
|
||||
}
|
||||
if suppliedGas < GasAdmin {
|
||||
return nil, 0, fmt.Errorf("out of gas")
|
||||
}
|
||||
if len(input) < 32 {
|
||||
return nil, suppliedGas - GasAdmin, fmt.Errorf("input too short for pool ID")
|
||||
}
|
||||
var poolId [32]byte
|
||||
copy(poolId[:], input[:32])
|
||||
stateAdapter := newPoolStateAdapter(state)
|
||||
if err := c.poolManager.ResumePool(stateAdapter, caller, poolId); err != nil {
|
||||
return nil, suppliedGas - GasAdmin, err
|
||||
}
|
||||
return nil, suppliedGas - GasAdmin, nil
|
||||
}
|
||||
|
||||
func (c *DEXContract) runFreezePool(
|
||||
state contract.AccessibleState,
|
||||
caller common.Address,
|
||||
input []byte,
|
||||
suppliedGas uint64,
|
||||
readOnly bool,
|
||||
) ([]byte, uint64, error) {
|
||||
if readOnly {
|
||||
return nil, suppliedGas, fmt.Errorf("cannot write in read-only mode")
|
||||
}
|
||||
if suppliedGas < GasAdmin {
|
||||
return nil, 0, fmt.Errorf("out of gas")
|
||||
}
|
||||
if len(input) < 32 {
|
||||
return nil, suppliedGas - GasAdmin, fmt.Errorf("input too short for pool ID")
|
||||
}
|
||||
var poolId [32]byte
|
||||
copy(poolId[:], input[:32])
|
||||
stateAdapter := newPoolStateAdapter(state)
|
||||
if err := c.poolManager.FreezePool(stateAdapter, caller, poolId); err != nil {
|
||||
return nil, suppliedGas - GasAdmin, err
|
||||
}
|
||||
return nil, suppliedGas - GasAdmin, nil
|
||||
}
|
||||
|
||||
// RequiredGas returns the gas required for the precompile input.
|
||||
//
|
||||
// This is the canonical gas source for the DEX precompile — every run handler
|
||||
// charges this exact value rather than re-deriving it inline. Putting the
|
||||
// gas formula in one place means a tariff change only edits one switch.
|
||||
//
|
||||
// For dynamic-sized selectors (extsload(bytes32[])) the value is derived
|
||||
// from the input: base cost + per-slot cost, capped at the maximum array
|
||||
// length the loader will accept. Selectors with no input data fall back to
|
||||
// their fixed tariff.
|
||||
//
|
||||
// RED V4 closes the divergence between this function and the previous inline
|
||||
// charge in runExtsloadArray: now there is one source of truth.
|
||||
func (c *DEXContract) RequiredGas(input []byte) uint64 {
|
||||
if len(input) < 4 {
|
||||
return GasSwap
|
||||
}
|
||||
|
||||
selector := binary.BigEndian.Uint32(input[:4])
|
||||
switch selector {
|
||||
case SelectorInitialize:
|
||||
return GasPoolCreate
|
||||
case SelectorSwap, SelectorDonate:
|
||||
return GasSwap
|
||||
case SelectorModifyLiquidity:
|
||||
return GasAddLiquidity
|
||||
case SelectorDeposit, SelectorWithdraw:
|
||||
return GasSettlement
|
||||
case SelectorBalanceOf:
|
||||
return GasPoolLookup
|
||||
case SelectorTake:
|
||||
return GasBalanceUpdate
|
||||
case SelectorSettle, SelectorSettleFor:
|
||||
return GasSettlement
|
||||
case SelectorUnlock:
|
||||
return GasFlashLoan
|
||||
case SelectorSync:
|
||||
return GasPoolLookup
|
||||
case SelectorExtsload:
|
||||
return GasPoolLookup
|
||||
case SelectorExtsloadArray:
|
||||
return extsloadArrayGas(input[4:])
|
||||
case SelectorPauseDEX, SelectorResumeDEX, SelectorPausePool, SelectorResumePool, SelectorFreezePool:
|
||||
return GasAdmin
|
||||
default:
|
||||
return GasSwap
|
||||
}
|
||||
}
|
||||
|
||||
// extsloadArrayGas computes the canonical gas charge for an extsload(bytes32[])
|
||||
// call: GasPoolLookup base + GasPoolLookup per slot. Returns the base alone
|
||||
// for malformed inputs — the Run path will surface the parse error and the
|
||||
// caller pays the base fee for the dispatch.
|
||||
//
|
||||
// Inputs that would overflow uint64 are clamped to the maximum supported
|
||||
// slot count (maxExtsloadSlots = 256). The Run path enforces the same cap,
|
||||
// so the gas formula and the read budget agree.
|
||||
func extsloadArrayGas(data []byte) uint64 {
|
||||
if len(data) < 64 {
|
||||
return GasPoolLookup
|
||||
}
|
||||
offsetBig := new(big.Int).SetBytes(data[0:32])
|
||||
if offsetBig.BitLen() > 63 {
|
||||
return GasPoolLookup
|
||||
}
|
||||
offset := offsetBig.Uint64()
|
||||
dataLen := uint64(len(data))
|
||||
if offset > dataLen || dataLen-offset < 32 {
|
||||
return GasPoolLookup
|
||||
}
|
||||
countBig := new(big.Int).SetBytes(data[offset : offset+32])
|
||||
if countBig.BitLen() > 63 {
|
||||
return GasPoolLookup
|
||||
}
|
||||
count := countBig.Uint64()
|
||||
const maxExtsloadSlots = 256
|
||||
if count > maxExtsloadSlots {
|
||||
count = maxExtsloadSlots
|
||||
}
|
||||
return GasPoolLookup + GasPoolLookup*count
|
||||
}
|
||||
|
||||
// poolStateAdapter adapts contract.StateDB to dex.StateDB.
|
||||
// blockNumber must be set from the execution context (AccessibleState.GetBlockContext().Number())
|
||||
// since contract.StateDB does not expose block number.
|
||||
|
||||
+11
-13
@@ -169,13 +169,13 @@ func TestDecodeABIBytesZeroLengthHookData(t *testing.T) {
|
||||
func TestExtsloadReadsSingleSlot(t *testing.T) {
|
||||
stateDB := NewMockStateDB()
|
||||
|
||||
// Write a known value to a slot at the pool manager address
|
||||
// Write a known value to a slot at the 0x9999 money-path address (extsload on the
|
||||
// sole DEX precompile reads its own slots — see runSettleExtsloadArray).
|
||||
slot := common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000042")
|
||||
expected := common.HexToHash("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef")
|
||||
stateDB.SetState(lxPoolAddr, slot, expected)
|
||||
stateDB.SetState(poolManagerAddr9999, slot, expected)
|
||||
|
||||
pm := NewPoolManager(&mockEngine{})
|
||||
c := &DEXContract{poolManager: pm}
|
||||
c := &SettleContract{}
|
||||
|
||||
// Build extsload(bytes32) call: 4-byte selector + 32-byte slot
|
||||
input := make([]byte, 36)
|
||||
@@ -185,7 +185,7 @@ func TestExtsloadReadsSingleSlot(t *testing.T) {
|
||||
// Use a minimal mock accessible state
|
||||
mockState := &mockAccessibleState{stateDB: stateDB}
|
||||
|
||||
result, gas, err := c.Run(mockState, common.Address{}, lxPoolAddr, input, 100000, true)
|
||||
result, gas, err := c.Run(mockState, common.Address{}, poolManagerAddr9999, input, 100000, true)
|
||||
if err != nil {
|
||||
t.Fatalf("extsload failed: %v", err)
|
||||
}
|
||||
@@ -208,11 +208,10 @@ func TestExtsloadArrayReadsMultipleSlots(t *testing.T) {
|
||||
slot1 := common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000002")
|
||||
val1 := common.HexToHash("0x00000000000000000000000000000000000000000000000000000000000000BB")
|
||||
|
||||
stateDB.SetState(lxPoolAddr, slot0, val0)
|
||||
stateDB.SetState(lxPoolAddr, slot1, val1)
|
||||
stateDB.SetState(poolManagerAddr9999, slot0, val0)
|
||||
stateDB.SetState(poolManagerAddr9999, slot1, val1)
|
||||
|
||||
pm := NewPoolManager(&mockEngine{})
|
||||
c := &DEXContract{poolManager: pm}
|
||||
c := &SettleContract{}
|
||||
|
||||
// Build extsload(bytes32[]) call
|
||||
// ABI: selector(4) + offset(32) + length(32) + slot0(32) + slot1(32)
|
||||
@@ -228,7 +227,7 @@ func TestExtsloadArrayReadsMultipleSlots(t *testing.T) {
|
||||
copy(input[4+96:4+128], slot1.Bytes())
|
||||
|
||||
mockState := &mockAccessibleState{stateDB: stateDB}
|
||||
result, _, err := c.Run(mockState, common.Address{}, lxPoolAddr, input, 100000, true)
|
||||
result, _, err := c.Run(mockState, common.Address{}, poolManagerAddr9999, input, 100000, true)
|
||||
if err != nil {
|
||||
t.Fatalf("extsload array failed: %v", err)
|
||||
}
|
||||
@@ -250,15 +249,14 @@ func TestExtsloadArrayReadsMultipleSlots(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestExtsloadShortInputRejected(t *testing.T) {
|
||||
pm := NewPoolManager(&mockEngine{})
|
||||
c := &DEXContract{poolManager: pm}
|
||||
c := &SettleContract{}
|
||||
|
||||
// Only 20 bytes of slot data (need 32)
|
||||
input := make([]byte, 24)
|
||||
binary.BigEndian.PutUint32(input[0:4], SelectorExtsload)
|
||||
|
||||
mockState := &mockAccessibleState{stateDB: NewMockStateDB()}
|
||||
_, _, err := c.Run(mockState, common.Address{}, lxPoolAddr, input, 100000, true)
|
||||
_, _, err := c.Run(mockState, common.Address{}, poolManagerAddr9999, input, 100000, true)
|
||||
if err == nil {
|
||||
t.Error("expected error for short extsload input")
|
||||
}
|
||||
|
||||
+52
-55
@@ -11,7 +11,6 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strings"
|
||||
@@ -269,8 +268,9 @@ func TestRedTeam_DecodeABIBytes_BigIntTruncation_Length(t *testing.T) {
|
||||
|
||||
func TestRedTeam_ExtsloadArray_CountOverflow(t *testing.T) {
|
||||
// count = 2^59 + 1 → count*32 = 2^64 + 32 → overflows to 32
|
||||
pm := NewPoolManager(&mockEngine{})
|
||||
c := &DEXContract{poolManager: pm}
|
||||
// Exercised against the SOLE money-path precompile 0x9999 (runSettleExtsloadArray),
|
||||
// whose overflow-safe bounds (readLowU64 + subtraction) reject this without panic.
|
||||
c := &SettleContract{}
|
||||
mockState := &mockAccessibleState{stateDB: NewMockStateDB()}
|
||||
|
||||
// ABI: selector(4) + offset(32) + count(32)
|
||||
@@ -291,7 +291,7 @@ func TestRedTeam_ExtsloadArray_CountOverflow(t *testing.T) {
|
||||
didPanic = true
|
||||
}
|
||||
}()
|
||||
_, _, err := c.Run(mockState, common.Address{}, lxPoolAddr, input, 1_000_000, true)
|
||||
_, _, err := c.Run(mockState, common.Address{}, poolManagerAddr9999, input, 1_000_000, true)
|
||||
if err == nil {
|
||||
t.Error("VULN: extsloadArray accepted overflow count without error")
|
||||
}
|
||||
@@ -302,9 +302,9 @@ func TestRedTeam_ExtsloadArray_CountOverflow(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRedTeam_ExtsloadArray_CountExceedsMax(t *testing.T) {
|
||||
// count = 257 exceeds maxExtsloadSlots = 256.
|
||||
pm := NewPoolManager(&mockEngine{})
|
||||
c := &DEXContract{poolManager: pm}
|
||||
// count = 257 with a minimal input is rejected by the remaining-input bounds check
|
||||
// (maxN derived from len(input)) on 0x9999's runSettleExtsloadArray.
|
||||
c := &SettleContract{}
|
||||
mockState := &mockAccessibleState{stateDB: NewMockStateDB()}
|
||||
|
||||
input := make([]byte, 4+32+32)
|
||||
@@ -314,19 +314,18 @@ func TestRedTeam_ExtsloadArray_CountExceedsMax(t *testing.T) {
|
||||
countBytes := bigIntTo32Bytes(big.NewInt(257))
|
||||
copy(input[4+32:4+64], countBytes)
|
||||
|
||||
_, _, err := c.Run(mockState, common.Address{}, lxPoolAddr, input, 1_000_000, true)
|
||||
_, _, err := c.Run(mockState, common.Address{}, poolManagerAddr9999, input, 1_000_000, true)
|
||||
if err == nil {
|
||||
t.Error("VULN: extsloadArray accepted count=257 (exceeds max 256)")
|
||||
t.Error("VULN: extsloadArray accepted count=257 exceeding the remaining-input bound")
|
||||
}
|
||||
if err != nil && !strings.Contains(err.Error(), "max") && !strings.Contains(err.Error(), "exceeds") {
|
||||
if err != nil && !strings.Contains(err.Error(), "bounds") && !strings.Contains(err.Error(), "exceeds") {
|
||||
t.Logf("Got error (acceptable): %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedTeam_ExtsloadArray_CountZero(t *testing.T) {
|
||||
// count = 0 should return empty result without panic.
|
||||
pm := NewPoolManager(&mockEngine{})
|
||||
c := &DEXContract{poolManager: pm}
|
||||
c := &SettleContract{}
|
||||
stateDB := NewMockStateDB()
|
||||
mockState := &mockAccessibleState{stateDB: stateDB}
|
||||
|
||||
@@ -342,7 +341,7 @@ func TestRedTeam_ExtsloadArray_CountZero(t *testing.T) {
|
||||
didPanic = true
|
||||
}
|
||||
}()
|
||||
result, _, err := c.Run(mockState, common.Address{}, lxPoolAddr, input, 1_000_000, true)
|
||||
result, _, err := c.Run(mockState, common.Address{}, poolManagerAddr9999, input, 1_000_000, true)
|
||||
if err != nil {
|
||||
t.Fatalf("extsloadArray count=0 returned error: %v", err)
|
||||
}
|
||||
@@ -363,34 +362,49 @@ func TestRedTeam_ExtsloadArray_CountZero(t *testing.T) {
|
||||
// =========================================================================
|
||||
|
||||
func TestRedTeam_ExtsloadArray_OffsetOverflow(t *testing.T) {
|
||||
pm := NewPoolManager(&mockEngine{})
|
||||
c := &DEXContract{poolManager: pm}
|
||||
c := &SettleContract{}
|
||||
mockState := &mockAccessibleState{stateDB: NewMockStateDB()}
|
||||
|
||||
// offset = 2^65 (BitLen = 66). Uint64 truncates to 0.
|
||||
// offset = 2^65 (BitLen = 66). The invariant under attack is "no panic, no
|
||||
// over-read": readLowU64 caps the offset to its low 64 bits and ALL subsequent
|
||||
// indexing is subtraction-bounded by the remaining input, so a high-bit offset can
|
||||
// never index past the buffer. Whether it errors or returns an empty array is
|
||||
// immaterial — it must NOT panic (validator halt) and must NOT over-read.
|
||||
input := make([]byte, 4+64)
|
||||
binary.BigEndian.PutUint32(input[0:4], SelectorExtsloadArray)
|
||||
|
||||
bigOffset := new(big.Int).Lsh(big.NewInt(1), 65)
|
||||
copy(input[4:36], bigIntTo32Bytes(bigOffset))
|
||||
|
||||
_, _, err := c.Run(mockState, common.Address{}, lxPoolAddr, input, 1_000_000, true)
|
||||
if err == nil {
|
||||
t.Error("VULN: extsloadArray accepted offset with BitLen > 64")
|
||||
didPanic := false
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
didPanic = true
|
||||
}
|
||||
}()
|
||||
out, _, err := c.Run(mockState, common.Address{}, poolManagerAddr9999, input, 1_000_000, true)
|
||||
// If it returns data (no error), the low-64-bit-capped offset/length must have
|
||||
// produced a well-formed (empty) ABI array, never a slice past the input.
|
||||
if err == nil && len(out) < 64 {
|
||||
t.Errorf("VULN: extsloadArray with BitLen>64 offset returned malformed/over-read output (len=%d)", len(out))
|
||||
}
|
||||
}()
|
||||
if didPanic {
|
||||
t.Fatal("VULN: extsloadArray panicked on BitLen>64 offset — chain halt")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedTeam_ExtsloadArray_OffsetBeyondInput(t *testing.T) {
|
||||
// offset = 99999 but input is only 68 bytes
|
||||
pm := NewPoolManager(&mockEngine{})
|
||||
c := &DEXContract{poolManager: pm}
|
||||
// offset = 99999 but input is only 64 bytes of data → out of bounds.
|
||||
c := &SettleContract{}
|
||||
mockState := &mockAccessibleState{stateDB: NewMockStateDB()}
|
||||
|
||||
input := make([]byte, 4+64)
|
||||
binary.BigEndian.PutUint32(input[0:4], SelectorExtsloadArray)
|
||||
copy(input[4:36], bigIntTo32Bytes(big.NewInt(99999)))
|
||||
|
||||
_, _, err := c.Run(mockState, common.Address{}, lxPoolAddr, input, 1_000_000, true)
|
||||
_, _, err := c.Run(mockState, common.Address{}, poolManagerAddr9999, input, 1_000_000, true)
|
||||
if err == nil {
|
||||
t.Error("VULN: extsloadArray accepted offset beyond input length")
|
||||
}
|
||||
@@ -459,37 +473,20 @@ func TestRedTeam_Initialize_NoHookData(t *testing.T) {
|
||||
copy(input[0:160], EncodePoolKeyABI(key))
|
||||
copy(input[160:192], bigIntTo32Bytes(new(big.Int).Set(Q96)))
|
||||
|
||||
// runInitialize checks len(input) > 192 before calling decodeABIBytes.
|
||||
// With exactly 192 bytes, hookData should be nil.
|
||||
// Verify the logic path: len(input) == 192, so hookData stays nil.
|
||||
// The build path checks len(input) > 192 before calling decodeABIBytes; with exactly
|
||||
// 192 bytes, hookData stays nil. 0x9010 is fully removed (not a registered precompile,
|
||||
// no dispatch), so there is no 0x9010 entry point to assert against — market creation
|
||||
// is C-authoritative through 0x9999 (settle_market.go) on top of this build path.
|
||||
if len(input) > 192 {
|
||||
t.Fatal("test setup error: input should be exactly 192 bytes")
|
||||
}
|
||||
|
||||
// The ABI decode robustness (192-byte input, nil hookData) is still exercised
|
||||
// by DecodePoolKey / the dexvm-side pm.Initialize. Via the 0x9010 entry point,
|
||||
// initialize is now DEPRECATED — market creation happens on the D-Chain and the
|
||||
// 0x9010 value/state-creating selectors revert PRECOMPILE_MOVED (one money path
|
||||
// at 0x9999). Assert the deprecation, not the old live behavior.
|
||||
// The behavior that matters: the shared PoolManager build path creates a pool with
|
||||
// nil hookData on a 192-byte input. This is what 0x9999's market registry composes.
|
||||
pm := NewPoolManager(&mockEngine{})
|
||||
c := &DEXContract{poolManager: pm}
|
||||
mockState := &mockAccessibleState{stateDB: NewMockStateDB()}
|
||||
|
||||
// Prepend selector
|
||||
calldata := make([]byte, 4+len(input))
|
||||
binary.BigEndian.PutUint32(calldata[0:4], SelectorInitialize)
|
||||
copy(calldata[4:], input)
|
||||
|
||||
_, _, err := c.Run(mockState, common.Address{}, lxPoolAddr, calldata, 1_000_000, false)
|
||||
if !errors.Is(err, ErrPrecompileMoved) {
|
||||
t.Fatalf("initialize at 0x9010 must revert PRECOMPILE_MOVED, got: %v", err)
|
||||
}
|
||||
|
||||
// The DIRECT dexvm-side path still creates a pool with nil hookData (the build
|
||||
// path the precompile forwards markets to). This is the behavior that matters.
|
||||
tick, derr := pm.Initialize(NewMockStateDB(), key, new(big.Int).Set(Q96), nil)
|
||||
if derr != nil {
|
||||
t.Fatalf("dexvm-side Initialize with nil hookData failed: %v", derr)
|
||||
t.Fatalf("Initialize with nil hookData failed: %v", derr)
|
||||
}
|
||||
_ = tick
|
||||
}
|
||||
@@ -652,12 +649,13 @@ func TestRedTeam_Router_EmptyPoolQuoteReturnsZero(t *testing.T) {
|
||||
// =========================================================================
|
||||
// Finding 10: Nil BlockContext guard
|
||||
//
|
||||
// DEXContract.Run() must return error, not panic, when BlockContext is nil.
|
||||
// SettleContract.Run() (0x9999, the SOLE money-path precompile) must return an
|
||||
// error, not panic, when BlockContext is nil — a missing block context on the
|
||||
// block-execution path must fail-closed, never crash the validator.
|
||||
// =========================================================================
|
||||
|
||||
func TestRedTeam_NilBlockContext_NoPanic(t *testing.T) {
|
||||
pm := NewPoolManager(&mockEngine{})
|
||||
c := &DEXContract{poolManager: pm}
|
||||
c := &SettleContract{}
|
||||
|
||||
// Create a mock accessible state that returns nil for GetBlockContext
|
||||
nilCtxState := &mockAccessibleStateNilCtx{stateDB: NewMockStateDB()}
|
||||
@@ -672,7 +670,7 @@ func TestRedTeam_NilBlockContext_NoPanic(t *testing.T) {
|
||||
didPanic = true
|
||||
}
|
||||
}()
|
||||
_, _, err := c.Run(nilCtxState, common.Address{}, lxPoolAddr, input, 1_000_000, false)
|
||||
_, _, err := c.Run(nilCtxState, common.Address{}, poolManagerAddr9999, input, 1_000_000, false)
|
||||
if err == nil {
|
||||
t.Error("VULN: Run() with nil BlockContext should return error")
|
||||
}
|
||||
@@ -683,9 +681,8 @@ func TestRedTeam_NilBlockContext_NoPanic(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRedTeam_NilBlockContext_AllSelectors(t *testing.T) {
|
||||
// Test all write selectors with nil block context
|
||||
pm := NewPoolManager(&mockEngine{})
|
||||
c := &DEXContract{poolManager: pm}
|
||||
// Test write selectors with nil block context on the 0x9999 money path.
|
||||
c := &SettleContract{}
|
||||
nilCtxState := &mockAccessibleStateNilCtx{stateDB: NewMockStateDB()}
|
||||
|
||||
selectors := []uint32{
|
||||
@@ -709,7 +706,7 @@ func TestRedTeam_NilBlockContext_AllSelectors(t *testing.T) {
|
||||
didPanic = true
|
||||
}
|
||||
}()
|
||||
_, _, err := c.Run(nilCtxState, common.Address{}, lxPoolAddr, input, 1_000_000, false)
|
||||
_, _, err := c.Run(nilCtxState, common.Address{}, poolManagerAddr9999, input, 1_000_000, false)
|
||||
if err == nil {
|
||||
t.Errorf("VULN: selector 0x%08X with nil BlockContext should return error", sel)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user